Skip to content

Commit c3e37e1

Browse files
committed
feat: declare sandbox state MCP capability
1 parent 69f6261 commit c3e37e1

File tree

8 files changed

+193
-37
lines changed

8 files changed

+193
-37
lines changed

codex-rs/Cargo.lock

Lines changed: 5 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/Cargo.toml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ async-trait = "0.1.89"
108108
axum = { version = "0.8", default-features = false }
109109
base64 = "0.22.1"
110110
bytes = "1.10.1"
111-
chrono = "0.4.42"
112111
chardetng = "0.1.17"
112+
chrono = "0.4.42"
113113
clap = "4"
114114
clap_complete = "4"
115115
color-eyre = "0.6.3"
@@ -120,9 +120,9 @@ diffy = "0.4.2"
120120
dirs = "6"
121121
dotenvy = "0.15.7"
122122
dunce = "1.0.4"
123+
encoding_rs = "0.8.35"
123124
env-flags = "0.1.1"
124125
env_logger = "0.11.5"
125-
encoding_rs = "0.8.35"
126126
escargot = "0.5"
127127
eventsource-stream = "0.2.3"
128128
futures = { version = "0.3", default-features = false }
@@ -167,7 +167,7 @@ ratatui-macros = "0.6.0"
167167
regex-lite = "0.1.7"
168168
regex = "1.11.1"
169169
reqwest = "0.12"
170-
rmcp = { version = "0.8.5", default-features = false }
170+
rmcp = { version = "0.9.0", default-features = false }
171171
schemars = "0.8.22"
172172
seccompiler = "0.5.0"
173173
serde = "1"
@@ -261,11 +261,7 @@ unwrap_used = "deny"
261261
# cargo-shear cannot see the platform-specific openssl-sys usage, so we
262262
# silence the false positive here instead of deleting a real dependency.
263263
[workspace.metadata.cargo-shear]
264-
ignored = [
265-
"icu_provider",
266-
"openssl-sys",
267-
"codex-utils-readiness",
268-
]
264+
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness"]
269265

270266
[profile.release]
271267
lto = "fat"
@@ -286,6 +282,7 @@ opt-level = 0
286282
# ratatui = { path = "../../ratatui" }
287283
crossterm = { git = "https://github.com/nornagon/crossterm", branch = "nornagon/color-query" }
288284
ratatui = { git = "https://github.com/nornagon/ratatui", branch = "nornagon-v0.29.0-patch" }
285+
rmcp = { git = "https://github.com/bolinfest/rust-sdk", branch = "pr556" }
289286

290287
# Uncomment to debug local changes.
291288
# rmcp = { path = "../../rust-sdk/crates/rmcp" }

codex-rs/core/src/codex.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::sync::Arc;
55
use std::sync::atomic::AtomicU64;
66

77
use crate::AuthManager;
8+
use crate::SandboxState;
89
use crate::client_common::REVIEW_PROMPT;
910
use crate::compact;
1011
use crate::compact::run_inline_auto_compact_task;
@@ -612,6 +613,22 @@ impl Session {
612613
)
613614
.await;
614615

616+
let sandbox_state = SandboxState {
617+
sandbox_policy: session_configuration.sandbox_policy.clone(),
618+
codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(),
619+
sandbox_cwd: session_configuration.cwd.clone(),
620+
};
621+
if let Err(e) = sess
622+
.services
623+
.mcp_connection_manager
624+
.read()
625+
.await
626+
.notify_sandbox_state_change(&sandbox_state)
627+
.await
628+
{
629+
tracing::error!("Failed to notify sandbox state change: {e}");
630+
}
631+
615632
// record_initial_history can emit events. We record only after the SessionConfiguredEvent is emitted.
616633
sess.record_initial_history(initial_history).await;
617634

codex-rs/core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ pub mod git_info;
3232
pub mod landlock;
3333
pub mod mcp;
3434
mod mcp_connection_manager;
35+
pub use mcp_connection_manager::MCP_SANDBOX_STATE_CAPABILITY;
36+
pub use mcp_connection_manager::MCP_SANDBOX_STATE_NOTIFICATION;
37+
pub use mcp_connection_manager::SandboxState;
3538
mod mcp_tool_call;
3639
mod message_history;
3740
mod model_provider_info;

codex-rs/core/src/mcp_connection_manager.rs

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::collections::HashMap;
1010
use std::collections::HashSet;
1111
use std::env;
1212
use std::ffi::OsString;
13+
use std::path::PathBuf;
1314
use std::sync::Arc;
1415
use std::time::Duration;
1516

@@ -26,6 +27,7 @@ use codex_protocol::protocol::McpStartupCompleteEvent;
2627
use codex_protocol::protocol::McpStartupFailure;
2728
use codex_protocol::protocol::McpStartupStatus;
2829
use codex_protocol::protocol::McpStartupUpdateEvent;
30+
use codex_protocol::protocol::SandboxPolicy;
2931
use codex_rmcp_client::OAuthCredentialsStoreMode;
3032
use codex_rmcp_client::RmcpClient;
3133
use futures::future::BoxFuture;
@@ -43,6 +45,8 @@ use mcp_types::Resource;
4345
use mcp_types::ResourceTemplate;
4446
use mcp_types::Tool;
4547

48+
use serde::Deserialize;
49+
use serde::Serialize;
4650
use serde_json::json;
4751
use sha1::Digest;
4852
use sha1::Sha1;
@@ -116,6 +120,7 @@ struct ManagedClient {
116120
tools: Vec<ToolInfo>,
117121
tool_filter: ToolFilter,
118122
tool_timeout: Option<Duration>,
123+
server_supports_sandbox_state_capability: bool,
119124
}
120125

121126
#[derive(Clone)]
@@ -150,6 +155,35 @@ impl AsyncManagedClient {
150155
async fn client(&self) -> Result<ManagedClient, StartupOutcomeError> {
151156
self.client.clone().await
152157
}
158+
159+
async fn notify_sandbox_state_change(&self, sandbox_state: &SandboxState) -> Result<()> {
160+
let managed = self.client().await?;
161+
if !managed.server_supports_sandbox_state_capability {
162+
return Ok(());
163+
}
164+
165+
managed
166+
.client
167+
.send_custom_notification(
168+
MCP_SANDBOX_STATE_NOTIFICATION,
169+
Some(serde_json::to_value(sandbox_state)?),
170+
)
171+
.await
172+
}
173+
}
174+
175+
pub const MCP_SANDBOX_STATE_CAPABILITY: &str = "codex/sandbox-state";
176+
177+
/// Custom MCP notification for sandbox state updates.
178+
/// When used, the `params` field of the notification is [`SandboxState`].
179+
pub const MCP_SANDBOX_STATE_NOTIFICATION: &str = "codex/sandbox-state/update";
180+
181+
#[derive(Debug, Clone, Serialize, Deserialize)]
182+
#[serde(rename_all = "camelCase")]
183+
pub struct SandboxState {
184+
pub sandbox_policy: SandboxPolicy,
185+
pub codex_linux_sandbox_exe: Option<PathBuf>,
186+
pub sandbox_cwd: PathBuf,
153187
}
154188

155189
/// A thin wrapper around a set of running [`RmcpClient`] instances.
@@ -477,6 +511,34 @@ impl McpConnectionManager {
477511
.get(tool_name)
478512
.map(|tool| (tool.server_name.clone(), tool.tool_name.clone()))
479513
}
514+
515+
pub async fn notify_sandbox_state_change(&self, sandbox_state: &SandboxState) -> Result<()> {
516+
let mut join_set = JoinSet::new();
517+
518+
for async_managed_client in self.clients.values() {
519+
let sandbox_state = sandbox_state.clone();
520+
let async_managed_client = async_managed_client.clone();
521+
join_set.spawn(async move {
522+
async_managed_client
523+
.notify_sandbox_state_change(&sandbox_state)
524+
.await
525+
});
526+
}
527+
528+
while let Some(join_res) = join_set.join_next().await {
529+
match join_res {
530+
Ok(Ok(())) => {}
531+
Ok(Err(err)) => {
532+
warn!("Failed to notify sandbox state change to MCP server: {err:#}");
533+
}
534+
Err(err) => {
535+
warn!("Task panic when notifying sandbox state change to MCP server: {err:#}");
536+
}
537+
}
538+
}
539+
540+
Ok(())
541+
}
480542
}
481543

482544
async fn emit_update(
@@ -639,7 +701,7 @@ async fn start_server_work(
639701
protocol_version: mcp_types::MCP_SCHEMA_VERSION.to_owned(),
640702
};
641703

642-
let client_result = match transport {
704+
let (client, initialize_result) = match transport {
643705
McpServerTransportConfig::Stdio {
644706
command,
645707
args,
@@ -655,7 +717,7 @@ async fn start_server_work(
655717
client
656718
.initialize(params.clone(), Some(startup_timeout))
657719
.await
658-
.map(|_| client)
720+
.map(|initialize_result| (client, initialize_result))
659721
}
660722
Err(err) => Err(err.into()),
661723
}
@@ -686,19 +748,12 @@ async fn start_server_work(
686748
client
687749
.initialize(params.clone(), Some(startup_timeout))
688750
.await
689-
.map(|_| client)
751+
.map(|initialize_result| (client, initialize_result))
690752
}
691753
Err(err) => Err(err),
692754
}
693755
}
694-
};
695-
696-
let client = match client_result {
697-
Ok(client) => client,
698-
Err(error) => {
699-
return Err(error.into());
700-
}
701-
};
756+
}?;
702757

703758
let tools = match list_tools_for_client(&server_name, &client, startup_timeout).await {
704759
Ok(tools) => tools,
@@ -707,11 +762,19 @@ async fn start_server_work(
707762
}
708763
};
709764

765+
let server_supports_sandbox_state_capability = initialize_result
766+
.capabilities
767+
.experimental
768+
.as_ref()
769+
.and_then(|exp| exp.get(MCP_SANDBOX_STATE_CAPABILITY))
770+
.is_some();
771+
710772
let managed = ManagedClient {
711773
client: Arc::clone(&client),
712774
tools,
713775
tool_timeout: Some(tool_timeout),
714776
tool_filter,
777+
server_supports_sandbox_state_capability,
715778
};
716779

717780
Ok(managed)

codex-rs/exec-server/src/posix/escalate_server.rs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use std::time::Duration;
88
use anyhow::Context as _;
99
use path_absolutize::Absolutize as _;
1010

11+
use codex_core::SandboxState;
1112
use codex_core::exec::process_exec_tool_call;
12-
use codex_core::protocol::SandboxPolicy;
1313
use tokio::process::Command;
1414
use tokio_util::sync::CancellationToken;
1515

@@ -48,6 +48,7 @@ impl EscalateServer {
4848
&self,
4949
params: ExecParams,
5050
cancel_rx: CancellationToken,
51+
sandbox_state: &SandboxState,
5152
) -> anyhow::Result<ExecResult> {
5253
let (escalate_server, escalate_client) = AsyncDatagramSocket::pair()?;
5354
let client_socket = escalate_client.into_inner();
@@ -64,12 +65,6 @@ impl EscalateServer {
6465
self.execve_wrapper.to_string_lossy().to_string(),
6566
);
6667

67-
// TODO: use the sandbox policy and cwd from the calling client.
68-
// Note that sandbox_cwd is ignored for ReadOnly, but needs to be legit
69-
// for `SandboxPolicy::WorkspaceWrite`.
70-
let sandbox_policy = SandboxPolicy::ReadOnly;
71-
let sandbox_cwd = PathBuf::from("/__NONEXISTENT__");
72-
7368
let ExecParams {
7469
command,
7570
workdir,
@@ -94,9 +89,9 @@ impl EscalateServer {
9489
justification: None,
9590
arg0: None,
9691
},
97-
&sandbox_policy,
98-
&sandbox_cwd,
99-
&None,
92+
&sandbox_state.sandbox_policy,
93+
&sandbox_state.sandbox_cwd,
94+
&sandbox_state.codex_linux_sandbox_exe,
10095
None,
10196
)
10297
.await?;

0 commit comments

Comments
 (0)