feat: declare sandbox state MCP capability

This commit is contained in:
Michael Bolin
2025-11-21 14:14:16 -08:00
parent 597fffe28b
commit 0671cd5dfe
8 changed files with 195 additions and 38 deletions
+17
View File
@@ -5,6 +5,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use crate::AuthManager;
use crate::SandboxState;
use crate::client_common::REVIEW_PROMPT;
use crate::compact;
use crate::compact::run_inline_auto_compact_task;
@@ -612,6 +613,22 @@ impl Session {
)
.await;
let sandbox_state = SandboxState {
sandbox_policy: session_configuration.sandbox_policy.clone(),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: session_configuration.cwd.clone(),
};
if let Err(e) = sess
.services
.mcp_connection_manager
.read()
.await
.notify_sandbox_state_change(&sandbox_state)
.await
{
tracing::error!("Failed to notify sandbox state change: {e}");
}
// record_initial_history can emit events. We record only after the SessionConfiguredEvent is emitted.
sess.record_initial_history(initial_history).await;
+3
View File
@@ -32,6 +32,9 @@ pub mod git_info;
pub mod landlock;
pub mod mcp;
mod mcp_connection_manager;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_NOTIFICATION;
pub use mcp_connection_manager::SandboxState;
mod mcp_tool_call;
mod message_history;
mod model_provider_info;
+74 -11
View File
@@ -10,6 +10,7 @@ use std::collections::HashMap;
use std::collections::HashSet;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
@@ -26,6 +27,7 @@ use codex_protocol::protocol::McpStartupCompleteEvent;
use codex_protocol::protocol::McpStartupFailure;
use codex_protocol::protocol::McpStartupStatus;
use codex_protocol::protocol::McpStartupUpdateEvent;
use codex_protocol::protocol::SandboxPolicy;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_rmcp_client::RmcpClient;
use futures::future::BoxFuture;
@@ -43,6 +45,8 @@ use mcp_types::Resource;
use mcp_types::ResourceTemplate;
use mcp_types::Tool;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use sha1::Digest;
use sha1::Sha1;
@@ -116,6 +120,7 @@ struct ManagedClient {
tools: Vec<ToolInfo>,
tool_filter: ToolFilter,
tool_timeout: Option<Duration>,
server_supports_sandbox_state_capability: bool,
}
#[derive(Clone)]
@@ -150,6 +155,35 @@ impl AsyncManagedClient {
async fn client(&self) -> Result<ManagedClient, StartupOutcomeError> {
self.client.clone().await
}
async fn notify_sandbox_state_change(&self, sandbox_state: &SandboxState) -> Result<()> {
let managed = self.client().await?;
if !managed.server_supports_sandbox_state_capability {
return Ok(());
}
managed
.client
.send_custom_notification(
MCP_SANDBOX_STATE_NOTIFICATION,
Some(serde_json::to_value(sandbox_state)?),
)
.await
}
}
pub const MCP_SANDBOX_STATE_CAPABILITY: &str = "codex/sandbox-state";
/// Custom MCP notification for sandbox state updates.
/// When used, the `params` field of the notification is [`SandboxState`].
pub const MCP_SANDBOX_STATE_NOTIFICATION: &str = "codex/sandbox-state/update";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxState {
pub sandbox_policy: SandboxPolicy,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub sandbox_cwd: PathBuf,
}
/// A thin wrapper around a set of running [`RmcpClient`] instances.
@@ -477,6 +511,34 @@ impl McpConnectionManager {
.get(tool_name)
.map(|tool| (tool.server_name.clone(), tool.tool_name.clone()))
}
pub async fn notify_sandbox_state_change(&self, sandbox_state: &SandboxState) -> Result<()> {
let mut join_set = JoinSet::new();
for async_managed_client in self.clients.values() {
let sandbox_state = sandbox_state.clone();
let async_managed_client = async_managed_client.clone();
join_set.spawn(async move {
async_managed_client
.notify_sandbox_state_change(&sandbox_state)
.await
});
}
while let Some(join_res) = join_set.join_next().await {
match join_res {
Ok(Ok(())) => {}
Ok(Err(err)) => {
warn!("Failed to notify sandbox state change to MCP server: {err:#}");
}
Err(err) => {
warn!("Task panic when notifying sandbox state change to MCP server: {err:#}");
}
}
}
Ok(())
}
}
async fn emit_update(
@@ -639,7 +701,7 @@ async fn start_server_work(
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(),
};
let client_result = match transport {
let (client, initialize_result) = match transport {
McpServerTransportConfig::Stdio {
command,
args,
@@ -655,7 +717,7 @@ async fn start_server_work(
client
.initialize(params.clone(), Some(startup_timeout))
.await
.map(|_| client)
.map(|initialize_result| (client, initialize_result))
}
Err(err) => Err(err.into()),
}
@@ -686,19 +748,12 @@ async fn start_server_work(
client
.initialize(params.clone(), Some(startup_timeout))
.await
.map(|_| client)
.map(|initialize_result| (client, initialize_result))
}
Err(err) => Err(err),
}
}
};
let client = match client_result {
Ok(client) => client,
Err(error) => {
return Err(error.into());
}
};
}?;
let tools = match list_tools_for_client(&server_name, &client, startup_timeout).await {
Ok(tools) => tools,
@@ -707,11 +762,19 @@ async fn start_server_work(
}
};
let server_supports_sandbox_state_capability = initialize_result
.capabilities
.experimental
.as_ref()
.and_then(|exp| exp.get(MCP_SANDBOX_STATE_CAPABILITY))
.is_some();
let managed = ManagedClient {
client: Arc::clone(&client),
tools,
tool_timeout: Some(tool_timeout),
tool_filter,
server_supports_sandbox_state_capability,
};
Ok(managed)