diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 843419f278..3b40559069 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -84,7 +84,10 @@ pub struct UnifiedExecRuntime<'a> { shell_mode: UnifiedExecShellMode, } -fn build_remote_exec_sandbox_config(attempt: &SandboxAttempt<'_>) -> Option { +fn build_remote_exec_sandbox_config( + attempt: &SandboxAttempt<'_>, + additional_permissions: Option, +) -> Option { if matches!(attempt.sandbox, codex_sandboxing::SandboxType::None) { return None; } @@ -95,6 +98,7 @@ fn build_remote_exec_sandbox_config(attempt: &SandboxAttempt<'_>) -> Option) -> Option bool { - !has_additional_permissions && !has_network_proxy -} - impl<'a> UnifiedExecRuntime<'a> { /// Creates a runtime bound to the shared unified-exec process manager. pub fn new(manager: &'a UnifiedExecProcessManager, shell_mode: UnifiedExecShellMode) -> Self { @@ -245,36 +242,40 @@ impl<'a> ToolRuntime for UnifiedExecRunt if let Some(network) = req.network.as_ref() { network.apply_to_env(&mut env); } - // Remote exec-server now owns sandbox argv construction, so this branch - // keeps sending raw command data until we collapse the launch APIs. - if ctx.turn.environment.exec_server_url().is_some() - && should_remote_exec_server_build_sandbox( - req.additional_permissions.is_some(), - req.network.is_some(), - ) - { - let exec_params = codex_exec_server::ExecParams { - process_id: req.process_id.to_string().into(), - argv: command, - cwd: req.cwd.clone(), - env, - tty: req.tty, - arg0: None, - sandbox: build_remote_exec_sandbox_config(attempt), - }; - return self - .manager - .open_session_with_remote_exec(exec_params, ctx.turn.environment.as_ref()) - .await - .map_err(|err| match err { - UnifiedExecError::SandboxDenied { output, .. } => { - ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { - output: Box::new(output), - network_policy_decision: None, - })) - } - other => ToolError::Rejected(other.to_string()), - }); + if ctx.turn.environment.exec_server_url().is_some() { + if let UnifiedExecShellMode::ZshFork(_) = &self.shell_mode { + return Err(ToolError::Rejected( + "unified_exec zsh-fork is not supported when exec_server_url is configured" + .to_string(), + )); + } + if req.network.is_none() { + let exec_params = codex_exec_server::ExecParams { + process_id: req.process_id.to_string().into(), + argv: command, + cwd: req.cwd.clone(), + env, + tty: req.tty, + arg0: None, + sandbox: build_remote_exec_sandbox_config( + attempt, + req.additional_permissions.clone(), + ), + }; + return self + .manager + .open_session_with_remote_exec(exec_params, ctx.turn.environment.as_ref()) + .await + .map_err(|err| match err { + UnifiedExecError::SandboxDenied { output, .. } => { + ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { + output: Box::new(output), + network_policy_decision: None, + })) + } + other => ToolError::Rejected(other.to_string()), + }); + } } if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode { let command = @@ -297,14 +298,6 @@ impl<'a> ToolRuntime for UnifiedExecRunt .await? { Some(prepared) => { - // TODO: Move unified-exec zsh-fork into exec-server once - // remote launch can participate in the existing approval - // and escalation flow. - if ctx.turn.environment.exec_server_url().is_some() { - return Err(ToolError::Rejected( - "unified_exec zsh-fork is not supported when exec_server_url is configured".to_string(), - )); - } return self .manager .open_session_with_exec_env( @@ -362,29 +355,3 @@ impl<'a> ToolRuntime for UnifiedExecRunt }) } } - -#[cfg(test)] -mod tests { - use super::should_remote_exec_server_build_sandbox; - - #[test] - fn remote_exec_server_builds_sandbox_for_simple_requests() { - assert!(should_remote_exec_server_build_sandbox( - /*has_additional_permissions*/ false, /*has_network_proxy*/ false, - )); - } - - #[test] - fn remote_exec_server_falls_back_for_requests_with_additional_permissions() { - assert!(!should_remote_exec_server_build_sandbox( - /*has_additional_permissions*/ true, /*has_network_proxy*/ false, - )); - } - - #[test] - fn remote_exec_server_falls_back_for_requests_with_network_proxy() { - assert!(!should_remote_exec_server_build_sandbox( - /*has_additional_permissions*/ false, /*has_network_proxy*/ true, - )); - } -} diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index d6db082f5a..2f72277086 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -947,15 +947,13 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<( let open_call_id = "uexec-delayed-open"; let open_args = json!({ - "cmd": "sleep 2 && echo MARKER1 && sleep 5 && echo MARKER2", + "cmd": "sleep 3 && echo MARKER1 && sleep 3 && echo MARKER2", "yield_time_ms": 10, "tty": true, }); // Poll stdin three times: first for no output, second after the first marker, - // and a final long poll to capture the second marker. Keep a wider buffer - // between MARKER1 and process exit so slower environments still reach the - // third write while the session is live. + // and a final long poll to capture the second marker. let first_poll_call_id = "uexec-delayed-poll-1"; let first_poll_args = json!({ "chars": "x", @@ -967,7 +965,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<( let second_poll_args = json!({ "chars": "x", "session_id": 1000, - "yield_time_ms": 3500, + "yield_time_ms": 4000, }); let third_poll_call_id = "uexec-delayed-poll-3"; diff --git a/codex-rs/exec-server/Cargo.toml b/codex-rs/exec-server/Cargo.toml index 6242524b75..f7bc313e22 100644 --- a/codex-rs/exec-server/Cargo.toml +++ b/codex-rs/exec-server/Cargo.toml @@ -15,10 +15,12 @@ path = "src/bin/codex-exec-server.rs" workspace = true [dependencies] +anyhow = { workspace = true } arc-swap = { workspace = true } async-trait = { workspace = true } base64 = { workspace = true } clap = { workspace = true, features = ["derive"] } +codex-arg0 = { workspace = true } codex-sandboxing = { workspace = true } codex-app-server-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } diff --git a/codex-rs/exec-server/src/bin/codex-exec-server.rs b/codex-rs/exec-server/src/bin/codex-exec-server.rs index 82fa9ec00f..8bad21e3d4 100644 --- a/codex-rs/exec-server/src/bin/codex-exec-server.rs +++ b/codex-rs/exec-server/src/bin/codex-exec-server.rs @@ -1,4 +1,6 @@ use clap::Parser; +use codex_arg0::Arg0DispatchPaths; +use codex_arg0::arg0_dispatch_or_else; #[derive(Debug, Parser)] struct ExecServerArgs { @@ -11,8 +13,11 @@ struct ExecServerArgs { listen: String, } -#[tokio::main] -async fn main() -> Result<(), Box> { - let args = ExecServerArgs::parse(); - codex_exec_server::run_main_with_listen_url(&args.listen).await +fn main() -> anyhow::Result<()> { + arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move { + let args = ExecServerArgs::parse(); + codex_exec_server::configure_arg0_paths(arg0_paths); + codex_exec_server::run_main_with_listen_url(&args.listen).await?; + Ok(()) + }) } diff --git a/codex-rs/exec-server/src/lib.rs b/codex-rs/exec-server/src/lib.rs index 77834c6143..aebce1cfe1 100644 --- a/codex-rs/exec-server/src/lib.rs +++ b/codex-rs/exec-server/src/lib.rs @@ -13,6 +13,8 @@ mod remote_process; mod rpc; mod server; +use codex_arg0::Arg0DispatchPaths; + pub use client::ExecServerClient; pub use client::ExecServerError; pub use client_api::ExecServerClientConnectOptions; @@ -65,3 +67,7 @@ pub use server::DEFAULT_LISTEN_URL; pub use server::ExecServerListenUrlParseError; pub use server::run_main; pub use server::run_main_with_listen_url; + +pub fn configure_arg0_paths(arg0_paths: Arg0DispatchPaths) { + local_process::configure_codex_linux_sandbox_exe(arg0_paths.codex_linux_sandbox_exe); +} diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index f03b2045e0..a57767cdfb 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -3,13 +3,19 @@ use std::collections::VecDeque; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; +use std::sync::OnceLock; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::time::Duration; use async_trait::async_trait; use codex_app_server_protocol::JSONRPCErrorError; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxCommand; +use codex_sandboxing::SandboxExecRequest; use codex_sandboxing::SandboxType; use codex_sandboxing::landlock::CODEX_LINUX_SANDBOX_ARG0; use codex_utils_pty::ExecCommandSession; @@ -48,6 +54,7 @@ use crate::rpc::invalid_request; const RETAINED_OUTPUT_BYTES_PER_PROCESS: usize = 1024 * 1024; const NOTIFICATION_CHANNEL_CAPACITY: usize = 256; +static CONFIGURED_CODEX_LINUX_SANDBOX_EXE: OnceLock> = OnceLock::new(); #[cfg(test)] const EXITED_PROCESS_RETENTION: Duration = Duration::from_millis(25); #[cfg(not(test))] @@ -104,6 +111,11 @@ struct ExecServerRuntimeConfig { impl ExecServerRuntimeConfig { fn detect() -> Self { + if let Some(codex_linux_sandbox_exe) = CONFIGURED_CODEX_LINUX_SANDBOX_EXE.get().cloned() { + return Self { + codex_linux_sandbox_exe, + }; + } let env_path = std::env::var_os("CODEX_LINUX_SANDBOX_EXE").map(PathBuf::from); let sibling_path = std::env::current_exe().ok().and_then(|current_exe| { current_exe @@ -117,11 +129,14 @@ impl ExecServerRuntimeConfig { } } -struct PreparedExecLaunch { - argv: Vec, - env: HashMap, - arg0: Option, +struct StartedProcess { + process_id: ProcessId, sandbox_type: SandboxType, + wake_tx: watch::Sender, +} + +pub(crate) fn configure_codex_linux_sandbox_exe(codex_linux_sandbox_exe: Option) { + let _ = CONFIGURED_CODEX_LINUX_SANDBOX_EXE.set(codex_linux_sandbox_exe); } impl Default for LocalProcess { @@ -196,15 +211,12 @@ impl LocalProcess { Ok(()) } - async fn start_process( - &self, - params: ExecParams, - ) -> Result<(ExecResponse, watch::Sender), JSONRPCErrorError> { + async fn start_process(&self, params: ExecParams) -> Result { self.require_initialized_for("exec")?; let process_id = params.process_id.clone(); let launch = prepare_exec_launch(¶ms, &self.inner.runtime)?; let (program, args) = launch - .argv + .command .split_first() .ok_or_else(|| invalid_params("argv must not be empty".to_string()))?; @@ -299,19 +311,19 @@ impl LocalProcess { output_notify, )); - Ok(( - ExecResponse { - process_id, - sandbox_type: launch.sandbox_type, - }, + Ok(StartedProcess { + process_id, + sandbox_type: launch.sandbox, wake_tx, - )) + }) } pub(crate) async fn exec(&self, params: ExecParams) -> Result { self.start_process(params) .await - .map(|(response, _)| response) + .map(|started| ExecResponse { + process_id: started.process_id, + }) } pub(crate) async fn exec_read( @@ -455,17 +467,17 @@ impl LocalProcess { #[async_trait] impl ExecBackend for LocalProcess { async fn start(&self, params: ExecParams) -> Result { - let (response, wake_tx) = self + let started = self .start_process(params) .await .map_err(map_handler_error)?; Ok(StartedExecProcess { process: Arc::new(LocalExecProcess { - process_id: response.process_id, + process_id: started.process_id, backend: self.clone(), - wake_tx, + wake_tx: started.wake_tx, }), - sandbox_type: response.sandbox_type, + sandbox_type: started.sandbox_type, }) } } @@ -504,6 +516,7 @@ fn build_sandbox_command( argv: &[String], cwd: &Path, env: &HashMap, + additional_permissions: Option, ) -> Result { let (program, args) = argv .split_first() @@ -513,25 +526,37 @@ fn build_sandbox_command( args: args.to_vec(), cwd: cwd.to_path_buf(), env: env.clone(), - additional_permissions: None, + additional_permissions, }) } fn prepare_exec_launch( params: &ExecParams, runtime: &ExecServerRuntimeConfig, -) -> Result { +) -> Result { let Some(sandbox) = params.sandbox.as_ref() else { - return Ok(PreparedExecLaunch { - argv: params.argv.clone(), + return Ok(SandboxExecRequest { + command: params.argv.clone(), + cwd: params.cwd.clone(), env: params.env.clone(), arg0: params.arg0.clone(), - sandbox_type: SandboxType::None, + network: None, + sandbox: SandboxType::None, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + sandbox_policy: SandboxPolicy::DangerFullAccess, + file_system_sandbox_policy: FileSystemSandboxPolicy::unrestricted(), + network_sandbox_policy: NetworkSandboxPolicy::Enabled, }); }; - let command = build_sandbox_command(¶ms.argv, params.cwd.as_path(), ¶ms.env)?; - let transformed = sandbox + let command = build_sandbox_command( + ¶ms.argv, + params.cwd.as_path(), + ¶ms.env, + sandbox.additional_permissions.clone(), + )?; + sandbox .transform( command, // TODO: Thread managed-network proxy state across exec-server so @@ -540,14 +565,7 @@ fn prepare_exec_launch( None, runtime.codex_linux_sandbox_exe.as_ref(), ) - .map_err(|err| internal_error(format!("failed to build sandbox launch: {err}")))?; - - Ok(PreparedExecLaunch { - argv: transformed.command, - env: transformed.env, - arg0: transformed.arg0, - sandbox_type: transformed.sandbox, - }) + .map_err(|err| internal_error(format!("failed to build sandbox launch: {err}"))) } impl LocalProcess { diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index 903e695b12..5b03fa3ac5 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_sandboxing::SandboxLaunchConfig; -use codex_sandboxing::SandboxType; use serde::Deserialize; use serde::Serialize; @@ -70,7 +69,6 @@ pub struct ExecParams { #[serde(rename_all = "camelCase")] pub struct ExecResponse { pub process_id: ProcessId, - pub sandbox_type: SandboxType, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/codex-rs/exec-server/src/remote_process.rs b/codex-rs/exec-server/src/remote_process.rs index 74d6f092ca..bfcf072237 100644 --- a/codex-rs/exec-server/src/remote_process.rs +++ b/codex-rs/exec-server/src/remote_process.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; +use codex_sandboxing::SandboxType; use tokio::sync::watch; use tracing::trace; @@ -34,18 +35,22 @@ impl RemoteProcess { impl ExecBackend for RemoteProcess { async fn start(&self, params: ExecParams) -> Result { let process_id = params.process_id.clone(); + let sandbox_type = params + .sandbox + .as_ref() + .map_or(SandboxType::None, |sandbox| sandbox.sandbox_type()); let session = self.client.register_session(&process_id).await?; - let response = match self.client.exec(params).await { - Ok(response) => response, + match self.client.exec(params).await { + Ok(_) => {} Err(err) => { session.unregister().await; return Err(err); } - }; + } Ok(StartedExecProcess { process: Arc::new(RemoteExecProcess { session }), - sandbox_type: response.sandbox_type, + sandbox_type, }) } } diff --git a/codex-rs/exec-server/tests/common/exec_server.rs b/codex-rs/exec-server/tests/common/exec_server.rs index df74440e95..c7c120ee1d 100644 --- a/codex-rs/exec-server/tests/common/exec_server.rs +++ b/codex-rs/exec-server/tests/common/exec_server.rs @@ -44,11 +44,6 @@ pub(crate) async fn exec_server() -> anyhow::Result { let binary = cargo_bin("codex-exec-server")?; let mut child = Command::new(binary); child.args(["--listen", "ws://127.0.0.1:0"]); - if cfg!(target_os = "linux") - && let Ok(sandbox_binary) = cargo_bin("codex-linux-sandbox") - { - child.env("CODEX_LINUX_SANDBOX_EXE", sandbox_binary); - } child.stdin(Stdio::null()); child.stdout(Stdio::piped()); child.stderr(Stdio::inherit()); diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index 9dcb8363b3..f6edc6446a 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -12,7 +12,15 @@ use codex_exec_server::ExecProcess; use codex_exec_server::ProcessId; use codex_exec_server::ReadResponse; use codex_exec_server::StartedExecProcess; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_protocol::protocol::SandboxPolicy; +use codex_sandboxing::SandboxLaunchConfig; +use codex_sandboxing::SandboxLaunchMode; +use codex_sandboxing::SandboxType; use pretty_assertions::assert_eq; +use tempfile::TempDir; use test_case::test_case; use tokio::sync::watch; use tokio::time::Duration; @@ -213,6 +221,76 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe( Ok(()) } +fn platform_sandbox_type() -> SandboxType { + if cfg!(target_os = "macos") { + SandboxType::MacosSeatbelt + } else if cfg!(target_os = "linux") { + SandboxType::LinuxSeccomp + } else { + unreachable!("unix exec-server tests only run on macOS and Linux"); + } +} + +fn write_outside_workspace_sandbox(workspace_root: &std::path::Path) -> SandboxLaunchConfig { + let policy = SandboxPolicy::new_workspace_write_policy(); + SandboxLaunchConfig { + mode: SandboxLaunchMode::Require, + policy: policy.clone(), + file_system_policy: FileSystemSandboxPolicy::from_legacy_sandbox_policy( + &policy, + workspace_root, + ), + network_policy: NetworkSandboxPolicy::from(&policy), + sandbox_policy_cwd: workspace_root.to_path_buf(), + additional_permissions: None, + enforce_managed_network: false, + windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + use_legacy_landlock: false, + } +} + +async fn assert_exec_process_sandbox_denies_write_outside_workspace( + use_remote: bool, +) -> Result<()> { + let temp_dir = TempDir::new()?; + let workspace_root = temp_dir.path().join("workspace"); + std::fs::create_dir(&workspace_root)?; + let blocked_path = temp_dir.path().join("blocked.txt"); + let context = create_process_context(use_remote).await?; + let session = context + .backend + .start(ExecParams { + process_id: ProcessId::from("proc-sandbox-denied"), + argv: vec![ + "/usr/bin/python3".to_string(), + "-c".to_string(), + "from pathlib import Path; import sys; Path(sys.argv[1]).write_text('blocked')" + .to_string(), + blocked_path.to_string_lossy().into_owned(), + ], + cwd: workspace_root.clone(), + env: Default::default(), + tty: false, + arg0: None, + sandbox: Some(write_outside_workspace_sandbox(&workspace_root)), + }) + .await?; + + assert_eq!(session.sandbox_type, platform_sandbox_type()); + let StartedExecProcess { process, .. } = session; + let wake_rx = process.subscribe_wake(); + let (_output, exit_code, closed) = collect_process_output_from_reads(process, wake_rx).await?; + + assert_ne!(exit_code, Some(0)); + assert!(closed); + assert!( + !blocked_path.exists(), + "sandboxed process unexpectedly wrote outside the workspace root" + ); + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_exec_process_reports_transport_disconnect() -> Result<()> { let mut context = create_process_context(/*use_remote*/ true).await?; @@ -284,3 +362,10 @@ async fn exec_process_write_then_read(use_remote: bool) -> Result<()> { async fn exec_process_preserves_queued_events_before_subscribe(use_remote: bool) -> Result<()> { assert_exec_process_preserves_queued_events_before_subscribe(use_remote).await } + +#[test_case(false ; "local")] +#[test_case(true ; "remote")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn exec_process_sandbox_denies_write_outside_workspace(use_remote: bool) -> Result<()> { + assert_exec_process_sandbox_denies_write_outside_workspace(use_remote).await +} diff --git a/codex-rs/exec-server/tests/process.rs b/codex-rs/exec-server/tests/process.rs index 82bab71d9a..6dfa611a78 100644 --- a/codex-rs/exec-server/tests/process.rs +++ b/codex-rs/exec-server/tests/process.rs @@ -7,7 +7,6 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_exec_server::ExecResponse; use codex_exec_server::InitializeParams; use codex_exec_server::ProcessId; -use codex_sandboxing::SandboxType; use common::exec_server::exec_server; use pretty_assertions::assert_eq; @@ -38,16 +37,6 @@ async fn initialize_server( Ok(()) } -fn sandbox_wire_test_mode() -> (&'static str, SandboxType) { - if cfg!(target_os = "macos") { - ("require", SandboxType::MacosSeatbelt) - } else if cfg!(target_os = "linux") { - ("disabled", SandboxType::None) - } else { - unreachable!("unix exec-server tests only run on macOS and Linux"); - } -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> { let mut server = exec_server().await?; @@ -83,68 +72,6 @@ async fn exec_server_starts_process_over_websocket() -> anyhow::Result<()> { process_start_response, ExecResponse { process_id: ProcessId::from("proc-1"), - sandbox_type: SandboxType::None, - } - ); - - server.shutdown().await?; - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn exec_server_starts_sandboxed_process_over_websocket() -> anyhow::Result<()> { - let mut server = exec_server().await?; - initialize_server(&mut server).await?; - - let cwd = std::env::current_dir()?; - let (sandbox_mode, expected_sandbox_type) = sandbox_wire_test_mode(); - let process_start_id = server - .send_request( - "process/start", - serde_json::json!({ - "processId": "proc-sandbox", - "argv": ["true"], - "cwd": cwd, - "env": {}, - "tty": false, - "arg0": null, - "sandbox": { - "mode": sandbox_mode, - "policy": { - "type": "danger-full-access" - }, - "fileSystemPolicy": { - "kind": "unrestricted", - "entries": [] - }, - "networkPolicy": "enabled", - "sandboxPolicyCwd": cwd, - "enforceManagedNetwork": false, - "windowsSandboxLevel": "disabled", - "windowsSandboxPrivateDesktop": false, - "useLegacyLandlock": false - } - }), - ) - .await?; - let response = server - .wait_for_event(|event| { - matches!( - event, - JSONRPCMessage::Response(JSONRPCResponse { id, .. }) if id == &process_start_id - ) - }) - .await?; - let JSONRPCMessage::Response(JSONRPCResponse { id, result }) = response else { - panic!("expected process/start response"); - }; - assert_eq!(id, process_start_id); - let process_start_response: ExecResponse = serde_json::from_value(result)?; - assert_eq!( - process_start_response, - ExecResponse { - process_id: ProcessId::from("proc-sandbox"), - sandbox_type: expected_sandbox_type, } ); diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 3b226dc7d2..a0cb4df6a6 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -52,8 +52,9 @@ pub enum SandboxablePreference { #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum SandboxLaunchMode { - Disabled, + /// Choose the platform sandbox only when the policy requires it. Auto, + /// Always launch with the platform sandbox when one is available. Require, } @@ -65,6 +66,7 @@ pub struct SandboxLaunchConfig { pub file_system_policy: FileSystemSandboxPolicy, pub network_policy: NetworkSandboxPolicy, pub sandbox_policy_cwd: PathBuf, + pub additional_permissions: Option, pub enforce_managed_network: bool, pub windows_sandbox_level: WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, @@ -74,7 +76,6 @@ pub struct SandboxLaunchConfig { impl SandboxLaunchConfig { pub fn sandbox_type(&self) -> SandboxType { let preference = match self.mode { - SandboxLaunchMode::Disabled => return SandboxType::None, SandboxLaunchMode::Auto => SandboxablePreference::Auto, SandboxLaunchMode::Require => SandboxablePreference::Require, }; diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index 315d633f8b..a2f7bdd4da 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -28,8 +28,10 @@ use tempfile::TempDir; fn assert_seatbelt_denied(stderr: &[u8], path: &Path) { let stderr = String::from_utf8_lossy(stderr); let path_display = path.display().to_string(); + let denied_path = stderr.contains(&path_display); + let denied_operation = stderr.contains("Operation not permitted"); assert!( - (stderr.contains(&path_display) && stderr.contains("Operation not permitted")) + (denied_path && denied_operation) || stderr.contains("sandbox-exec: sandbox_apply: Operation not permitted"), "unexpected stderr: {stderr}" );