mcp: remove codex/sandbox-state custom request support (#17957)

## Why

#17763 moved sandbox-state delivery for MCP tool calls to request
`_meta` via the `codex/sandbox-state-meta` experimental capability.
Keeping the older `codex/sandbox-state` capability meant Codex still
maintained a second transport that pushed updates with the custom
`codex/sandbox-state/update` request at server startup and when the
session sandbox policy changed.

That duplicate MCP path is redundant with the per-tool-call metadata
path and makes the sandbox-state contract larger than needed. The
existing managed network proxy refresh on sandbox-policy changes is
still needed, so this keeps that behavior separate from the removed MCP
notification.

## What Changed

- Removed the exported `MCP_SANDBOX_STATE_CAPABILITY` and
`MCP_SANDBOX_STATE_METHOD` constants.
- Removed detection of `codex/sandbox-state` during MCP initialization
and stopped sending `codex/sandbox-state/update` at server startup.
- Removed the `McpConnectionManager::notify_sandbox_state_change`
plumbing while preserving the managed network proxy refresh when a user
turn changes sandbox policy.
- Slimmed `McpConnectionManager::new` so startup paths pass only the
initial `SandboxPolicy` needed for MCP elicitation state.
- Kept `codex/sandbox-state-meta` support intact; servers that opt in
still receive the current `SandboxState` on tool-call request `_meta`
([remaining call
path](ff2d3c1e72/codex-rs/core/src/mcp_tool_call.rs (L487-L526))).
- Added regression coverage for refreshing the live managed network
proxy on a per-turn sandbox-policy change.

## Verification

- `cargo test -p codex-core
new_turn_refreshes_managed_network_proxy_for_sandbox_change`
- `cargo test -p codex-mcp`
This commit is contained in:
Michael Bolin
2026-04-15 12:02:40 -07:00
committed by GitHub
parent 83abf67d20
commit 66533ddc61
7 changed files with 97 additions and 155 deletions

View File

@@ -36,9 +36,7 @@ pub use mcp::tool_plugin_provenance;
pub use mcp::with_codex_apps_mcp;
pub use mcp_connection_manager::CodexAppsToolsCacheKey;
pub use mcp_connection_manager::DEFAULT_STARTUP_TIMEOUT;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_META_CAPABILITY;
pub use mcp_connection_manager::MCP_SANDBOX_STATE_METHOD;
pub use mcp_connection_manager::McpConnectionManager;
pub use mcp_connection_manager::SandboxState;
pub use mcp_connection_manager::ToolInfo;

View File

@@ -35,7 +35,6 @@ use codex_protocol::protocol::SandboxPolicy;
use serde_json::Value;
use crate::mcp_connection_manager::McpConnectionManager;
use crate::mcp_connection_manager::SandboxState;
use crate::mcp_connection_manager::codex_apps_tools_cache_key;
pub type McpManager = McpConnectionManager;
@@ -347,14 +346,6 @@ pub async fn collect_mcp_snapshot_with_detail(
let (tx_event, rx_event) = unbounded();
drop(rx_event);
// Use ReadOnly sandbox policy for MCP snapshot collection (safest default)
let sandbox_state = SandboxState {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
use_legacy_landlock: config.use_legacy_landlock,
};
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
&mcp_servers,
config.mcp_oauth_credentials_store_mode,
@@ -362,7 +353,7 @@ pub async fn collect_mcp_snapshot_with_detail(
&config.approval_policy,
submit_id,
tx_event,
sandbox_state,
SandboxPolicy::new_read_only_policy(),
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
tool_plugin_provenance,
@@ -421,13 +412,6 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
let (tx_event, rx_event) = unbounded();
drop(rx_event);
let sandbox_state = SandboxState {
sandbox_policy: SandboxPolicy::new_read_only_policy(),
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
use_legacy_landlock: config.use_legacy_landlock,
};
let (mcp_connection_manager, cancel_token) = McpConnectionManager::new(
&mcp_servers,
config.mcp_oauth_credentials_store_mode,
@@ -435,7 +419,7 @@ pub async fn collect_mcp_server_status_snapshot_with_detail(
&config.approval_policy,
submit_id,
tx_event,
sandbox_state,
SandboxPolicy::new_read_only_policy(),
config.codex_home.clone(),
codex_apps_tools_cache_key(auth),
tool_plugin_provenance,

View File

@@ -439,7 +439,6 @@ struct ManagedClient {
tool_filter: ToolFilter,
tool_timeout: Option<Duration>,
server_instructions: Option<String>,
server_supports_sandbox_state_capability: bool,
server_supports_sandbox_state_meta_capability: bool,
codex_apps_tools_cache_context: Option<CodexAppsToolsCacheContext>,
}
@@ -469,22 +468,6 @@ impl ManagedClient {
self.tools.clone()
}
/// Returns once the server has ack'd the sandbox state update.
async fn notify_sandbox_state_change(&self, sandbox_state: &SandboxState) -> Result<()> {
if !self.server_supports_sandbox_state_capability {
return Ok(());
}
let _response = self
.client
.send_custom_request(
MCP_SANDBOX_STATE_METHOD,
Some(serde_json::to_value(sandbox_state)?),
)
.await?;
Ok(())
}
}
#[derive(Clone)]
@@ -642,19 +625,8 @@ impl AsyncManagedClient {
};
tools.map(annotate_tools)
}
async fn notify_sandbox_state_change(&self, sandbox_state: &SandboxState) -> Result<()> {
let managed = self.client().await?;
managed.notify_sandbox_state_change(sandbox_state).await
}
}
pub const MCP_SANDBOX_STATE_CAPABILITY: &str = "codex/sandbox-state";
/// Custom MCP request to push sandbox state updates.
/// When used, the `params` field of the notification is [`SandboxState`].
pub const MCP_SANDBOX_STATE_METHOD: &str = "codex/sandbox-state/update";
/// MCP server capability indicating that Codex should include [`SandboxState`]
/// in tool-call request `_meta` under this key.
pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta";
@@ -735,7 +707,7 @@ impl McpConnectionManager {
approval_policy: &Constrained<AskForApproval>,
submit_id: String,
tx_event: Sender<Event>,
initial_sandbox_state: SandboxState,
initial_sandbox_policy: SandboxPolicy,
codex_home: PathBuf,
codex_apps_tools_cache_key: CodexAppsToolsCacheKey,
tool_plugin_provenance: ToolPluginProvenance,
@@ -744,10 +716,8 @@ impl McpConnectionManager {
let mut clients = HashMap::new();
let mut server_origins = HashMap::new();
let mut join_set = JoinSet::new();
let elicitation_requests = ElicitationRequestManager::new(
approval_policy.value(),
initial_sandbox_state.sandbox_policy.clone(),
);
let elicitation_requests =
ElicitationRequestManager::new(approval_policy.value(), initial_sandbox_policy);
let tool_plugin_provenance = Arc::new(tool_plugin_provenance);
let startup_submit_id = submit_id.clone();
let mcp_servers = mcp_servers.clone();
@@ -787,25 +757,13 @@ impl McpConnectionManager {
let tx_event = tx_event.clone();
let submit_id = startup_submit_id.clone();
let auth_entry = auth_entries.get(&server_name).cloned();
let sandbox_state = initial_sandbox_state.clone();
join_set.spawn(async move {
let outcome = async_managed_client.client().await;
if cancel_token.is_cancelled() {
return (server_name, Err(StartupOutcomeError::Cancelled));
}
let status = match &outcome {
Ok(_) => {
// Send sandbox state notification immediately after Ready
if let Err(e) = async_managed_client
.notify_sandbox_state_change(&sandbox_state)
.await
{
warn!(
"Failed to notify sandbox state to MCP server {server_name}: {e:#}",
);
}
McpStartupStatus::Ready
}
Ok(_) => McpStartupStatus::Ready,
Err(error) => {
let error_str = mcp_init_error_display(
server_name.as_str(),
@@ -1219,34 +1177,6 @@ impl McpConnectionManager {
.into_values()
.find(|tool| tool.canonical_tool_name() == *tool_name)
}
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(
@@ -1492,12 +1422,6 @@ async fn start_server_task(
.await
.map_err(StartupOutcomeError::from)?;
let server_supports_sandbox_state_capability = initialize_result
.capabilities
.experimental
.as_ref()
.and_then(|exp| exp.get(MCP_SANDBOX_STATE_CAPABILITY))
.is_some();
let server_supports_sandbox_state_meta_capability = initialize_result
.capabilities
.experimental
@@ -1539,7 +1463,6 @@ async fn start_server_task(
tool_timeout: Some(tool_timeout),
tool_filter,
server_instructions: initialize_result.instructions,
server_supports_sandbox_state_capability,
server_supports_sandbox_state_meta_capability,
codex_apps_tools_cache_context,
};