feat: declare sandbox state MCP capability

This commit is contained in:
Michael Bolin
2025-11-21 15:01:08 -08:00
parent 67975ed33a
commit 80a9e75369
8 changed files with 190 additions and 27 deletions
@@ -8,8 +8,8 @@ use std::time::Duration;
use anyhow::Context as _;
use path_absolutize::Absolutize as _;
use codex_core::SandboxState;
use codex_core::exec::process_exec_tool_call;
use codex_core::protocol::SandboxPolicy;
use tokio::process::Command;
use tokio_util::sync::CancellationToken;
@@ -48,6 +48,7 @@ impl EscalateServer {
&self,
params: ExecParams,
cancel_rx: CancellationToken,
sandbox_state: &SandboxState,
) -> anyhow::Result<ExecResult> {
let (escalate_server, escalate_client) = AsyncDatagramSocket::pair()?;
let client_socket = escalate_client.into_inner();
@@ -64,12 +65,6 @@ impl EscalateServer {
self.execve_wrapper.to_string_lossy().to_string(),
);
// TODO: use the sandbox policy and cwd from the calling client.
// Note that sandbox_cwd is ignored for ReadOnly, but needs to be legit
// for `SandboxPolicy::WorkspaceWrite`.
let sandbox_policy = SandboxPolicy::ReadOnly;
let sandbox_cwd = PathBuf::from("/__NONEXISTENT__");
let ExecParams {
command,
workdir,
@@ -94,9 +89,9 @@ impl EscalateServer {
justification: None,
arg0: None,
},
&sandbox_policy,
&sandbox_cwd,
&None,
&sandbox_state.sandbox_policy,
&sandbox_state.sandbox_cwd,
&sandbox_state.codex_linux_sandbox_exe,
None,
)
.await?;
+62 -2
View File
@@ -1,8 +1,13 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context as _;
use anyhow::Result;
use codex_core::MCP_SANDBOX_STATE_CAPABILITY;
use codex_core::MCP_SANDBOX_STATE_NOTIFICATION;
use codex_core::SandboxState;
use codex_core::protocol::SandboxPolicy;
use rmcp::ErrorData as McpError;
use rmcp::RoleServer;
use rmcp::ServerHandler;
@@ -17,6 +22,8 @@ use rmcp::tool;
use rmcp::tool_handler;
use rmcp::tool_router;
use rmcp::transport::stdio;
use tokio::sync::RwLock;
use tracing::debug;
use crate::posix::escalate_server::EscalateServer;
use crate::posix::escalate_server::{self};
@@ -27,6 +34,8 @@ use crate::posix::stopwatch::Stopwatch;
/// Path to our patched bash.
const CODEX_BASH_PATH_ENV_VAR: &str = "CODEX_BASH_PATH";
const SANDBOX_STATE_CAPABILITY_VERSION: &str = "1.0.0";
pub(crate) fn get_bash_path() -> Result<PathBuf> {
std::env::var(CODEX_BASH_PATH_ENV_VAR)
.map(PathBuf::from)
@@ -70,6 +79,7 @@ pub struct ExecTool {
bash_path: PathBuf,
execve_wrapper: PathBuf,
policy: ExecPolicy,
sandbox_state: Arc<RwLock<Option<SandboxState>>>,
}
#[tool_router]
@@ -80,6 +90,7 @@ impl ExecTool {
bash_path,
execve_wrapper,
policy,
sandbox_state: Arc::new(RwLock::new(None)),
}
}
@@ -97,13 +108,24 @@ impl ExecTool {
);
let stopwatch = Stopwatch::new(effective_timeout);
let cancel_token = stopwatch.cancellation_token();
let sandbox_state =
self.sandbox_state
.read()
.await
.clone()
.unwrap_or_else(|| SandboxState {
sandbox_policy: SandboxPolicy::ReadOnly,
codex_linux_sandbox_exe: None,
sandbox_cwd: PathBuf::from(&params.workdir),
});
let escalate_server = EscalateServer::new(
self.bash_path.clone(),
self.execve_wrapper.clone(),
McpEscalationPolicy::new(self.policy, context, stopwatch.clone()),
);
let result = escalate_server
.exec(params, cancel_token)
.exec(params, cancel_token, &sandbox_state)
.await
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(CallToolResult::success(vec![Content::json(
@@ -115,9 +137,22 @@ impl ExecTool {
#[tool_handler]
impl ServerHandler for ExecTool {
fn get_info(&self) -> ServerInfo {
let mut experimental_capabilities = ExperimentalCapabilities::new();
let mut sandbox_state_capability = JsonObject::new();
sandbox_state_capability.insert(
"version".to_string(),
serde_json::Value::String(SANDBOX_STATE_CAPABILITY_VERSION.to_string()),
);
experimental_capabilities.insert(
MCP_SANDBOX_STATE_CAPABILITY.to_string(),
sandbox_state_capability,
);
ServerInfo {
protocol_version: ProtocolVersion::V_2025_06_18,
capabilities: ServerCapabilities::builder().enable_tools().build(),
capabilities: ServerCapabilities::builder()
.enable_tools()
.enable_experimental_with(experimental_capabilities)
.build(),
server_info: Implementation::from_build_env(),
instructions: Some(
"This server provides a tool to execute shell commands and return their output."
@@ -133,6 +168,31 @@ impl ServerHandler for ExecTool {
) -> Result<InitializeResult, McpError> {
Ok(self.get_info())
}
async fn on_custom_notification(
&self,
notification: rmcp::model::CustomClientNotification,
_context: rmcp::service::NotificationContext<rmcp::RoleServer>,
) {
let rmcp::model::CustomClientNotification { method, params, .. } = notification;
if method == MCP_SANDBOX_STATE_NOTIFICATION
&& let Some(params) = params
{
match serde_json::from_value::<SandboxState>(params) {
Ok(sandbox_state) => {
debug!(
?sandbox_state.sandbox_policy,
"received sandbox state notification"
);
let mut state = self.sandbox_state.write().await;
*state = Some(sandbox_state);
}
Err(err) => {
tracing::warn!(?err, "failed to deserialize sandbox state notification");
}
}
}
}
}
pub(crate) async fn serve(