Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 5 additions & 8 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ async-trait = "0.1.89"
axum = { version = "0.8", default-features = false }
base64 = "0.22.1"
bytes = "1.10.1"
chrono = "0.4.42"
chardetng = "0.1.17"
chrono = "0.4.42"
clap = "4"
clap_complete = "4"
color-eyre = "0.6.3"
Expand All @@ -120,9 +120,9 @@ diffy = "0.4.2"
dirs = "6"
dotenvy = "0.15.7"
dunce = "1.0.4"
encoding_rs = "0.8.35"
env-flags = "0.1.1"
env_logger = "0.11.5"
encoding_rs = "0.8.35"
escargot = "0.5"
eventsource-stream = "0.2.3"
futures = { version = "0.3", default-features = false }
Expand Down Expand Up @@ -167,7 +167,7 @@ ratatui-macros = "0.6.0"
regex-lite = "0.1.7"
regex = "1.11.1"
reqwest = "0.12"
rmcp = { version = "0.8.5", default-features = false }
rmcp = { version = "0.9.0", default-features = false }
schemars = "0.8.22"
seccompiler = "0.5.0"
serde = "1"
Expand Down Expand Up @@ -261,11 +261,7 @@ unwrap_used = "deny"
# cargo-shear cannot see the platform-specific openssl-sys usage, so we
# silence the false positive here instead of deleting a real dependency.
[workspace.metadata.cargo-shear]
ignored = [
"icu_provider",
"openssl-sys",
"codex-utils-readiness",
]
ignored = ["icu_provider", "openssl-sys", "codex-utils-readiness"]

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

# Uncomment to debug local changes.
# rmcp = { path = "../../rust-sdk/crates/rmcp" }
17 changes: 17 additions & 0 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -614,6 +615,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;

Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
72 changes: 71 additions & 1 deletion codex-rs/core/src/mcp_connection_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::sync::Mutex;
use std::time::Duration;
Expand All @@ -28,6 +29,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::ElicitationResponse;
use codex_rmcp_client::OAuthCredentialsStoreMode;
use codex_rmcp_client::RmcpClient;
Expand All @@ -48,6 +50,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;
Expand Down Expand Up @@ -174,6 +178,7 @@ struct ManagedClient {
tools: Vec<ToolInfo>,
tool_filter: ToolFilter,
tool_timeout: Option<Duration>,
server_supports_sandbox_state_capability: bool,
}

#[derive(Clone)]
Expand Down Expand Up @@ -222,6 +227,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>,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this is something we can reasonably discover for ourselves in the shell tool; we have access to the same logic there.

pub sandbox_cwd: PathBuf,
}

/// A thin wrapper around a set of running [`RmcpClient`] instances.
Expand Down Expand Up @@ -567,6 +601,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(
Expand Down Expand Up @@ -700,7 +762,7 @@ async fn start_server_task(

let send_elicitation = elicitation_requests.make_sender(server_name.clone(), tx_event);

client
let initialize_result = client
.initialize(params, startup_timeout, send_elicitation)
.await
.map_err(StartupOutcomeError::from)?;
Expand All @@ -709,11 +771,19 @@ 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 managed = ManagedClient {
client: Arc::clone(&client),
tools,
tool_timeout: Some(tool_timeout),
tool_filter,
server_supports_sandbox_state_capability,
};

Ok(managed)
Expand Down
15 changes: 5 additions & 10 deletions codex-rs/exec-server/src/posix/escalate_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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();
Expand All @@ -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,
Expand All @@ -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?;
Expand Down
Loading
Loading