Files
codex/codex-rs/codex-mcp/src/sandbox_metadata.rs
T
Michael Bolin 7d0b91d5d6 Separate MCP sandbox policy from launcher metadata (#53709)
## Why

MCP servers must forward sandbox policy unchanged to `codex sandbox --sandbox-state-json`, while interpreting launcher information to find the Codex CLI. Keep those contracts separate so consumers can preserve unknown policy fields.

## What changed

- Accept the experimental `codex/sandbox-state` capability alongside the deprecated `codex/sandbox-state-meta` capability, and send identical policy values under both `_meta` keys.
- Move `codexExecutable` out of the policy into `codex/sandbox-launcher`, supplied only when a verified CLI path is available for a local stdio server.
- Replace stale sandbox metadata while retaining unrelated entries, and remove stale launcher metadata when no launcher is available.
- Document the metadata contract and how servers should forward policy and retain authority for long-lived subprocesses.

## Testing

Update unit and integration coverage for policy serialization, identical state aliases, launcher availability across execution environments, and stale metadata replacement.

GitOrigin-RevId: e544c06e6e023953b1fc8069b7b1affa065262d1
2026-10-13 16:23:44 +00:00

83 lines
3.4 KiB
Rust

//! Keep opaque sandbox policy separate from the launcher used to apply it.
//! Both state names carry identical values during migration; neither contains
//! executable discovery data or deprecation messages.
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;
use std::path::PathBuf;
/// Servers advertise this experimental capability to receive opaque sandbox state
/// in tool-call `_meta`, along with a local launcher when one is available.
pub const MCP_SANDBOX_STATE_CAPABILITY: &str = "codex/sandbox-state";
/// Deprecated capability and metadata spelling, retained as an identical alias.
/// Deprecation guidance is outside the opaque policy object.
pub const MCP_SANDBOX_STATE_META_CAPABILITY: &str = "codex/sandbox-state-meta";
/// Tool-call metadata describing a sandbox launcher on the MCP server's host.
pub const MCP_SANDBOX_LAUNCHER_META_KEY: &str = "codex/sandbox-launcher";
/// Opaque policy forwarded unchanged to `codex sandbox --sandbox-state-json`.
///
/// Servers advertise `codex/sandbox-state` (or its deprecated `-meta` alias) to
/// receive the same object under both state keys. Consumers must preserve unknown
/// fields, not deserialize and reconstruct the policy. Launcher information lives
/// separately under `codex/sandbox-launcher`; only its documented fields may be
/// interpreted. Long-lived subprocesses retain both their state and launcher and
/// must not be reused under different authority without a lifecycle decision.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SandboxState {
pub permission_profile: PermissionProfile,
pub codex_linux_sandbox_exe: Option<PathBuf>,
pub sandbox_cwd: PathUri,
#[serde(default)]
pub use_legacy_landlock: bool,
#[serde(default)]
pub use_mxc: bool,
}
/// The known launcher envelope is distinct from the opaque policy. Absence never
/// authorizes searching PATH or falling back to an unsandboxed process.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct SandboxLauncher {
/// A full CLI on the MCP server's host, supporting `sandbox --sandbox-state-json`.
/// Remote/HTTP servers and hosts without a verified, UTF-8 CLI path omit the envelope.
codex_executable: AbsolutePathBuf,
}
pub(crate) fn augment_metadata(
meta: &mut Option<Value>,
state: SandboxState,
executable: Option<AbsolutePathBuf>,
) -> anyhow::Result<()> {
let state = serde_json::to_value(state)?;
let launcher = executable
.map(|codex_executable| serde_json::to_value(SandboxLauncher { codex_executable }))
.transpose()?;
let Some(metadata) = meta
.get_or_insert_with(|| Value::Object(Map::new()))
.as_object_mut()
else {
return Ok(());
};
metadata.insert(MCP_SANDBOX_STATE_CAPABILITY.to_owned(), state.clone());
metadata.insert(MCP_SANDBOX_STATE_META_CAPABILITY.to_owned(), state);
// Reserved launcher metadata must not survive from a previous/local call
// when this server's execution environment has no verified executable.
metadata.remove(MCP_SANDBOX_LAUNCHER_META_KEY);
if let Some(launcher) = launcher {
metadata.insert(MCP_SANDBOX_LAUNCHER_META_KEY.to_owned(), launcher);
}
Ok(())
}
#[cfg(test)]
#[path = "sandbox_metadata_tests.rs"]
mod tests;