From bd2968a4db3057657f6d242010bfd15f589124f3 Mon Sep 17 00:00:00 2001 From: jif Date: Sun, 21 Jun 2026 11:33:21 +0100 Subject: [PATCH] Carry sandbox intent to remote exec servers (#29108) ## What changed PR #29099 stopped sending the orchestrator's concrete sandbox wrapper to a remote exec-server. Remote commands now arrive as plain native argv. This PR adds the next piece: Codex also sends portable sandbox intent next to that plain argv. For a remote unified-exec command, the request can now include: - the canonical permission profile before local workspace-root materialization - the sandbox cwd and workspace roots as `PathUri` values - Windows sandbox settings - the legacy Landlock setting - whether managed networking must be enforced The important part is that symbolic entries such as `:workspace_roots` stay symbolic while crossing the boundary. The executor can then bind them to its own workspace-root paths instead of receiving orchestrator-local absolute paths. The data travels through `ExecRequest` into `ExecParams`. Older exec-servers can still deserialize requests because the new fields have defaults. ## Why The orchestrator should not decide how another machine implements sandboxing. For example: - a local macOS Codex would normally build a Seatbelt command - a remote Linux executor needs a Linux sandbox command instead The orchestrator now sends the plain command plus the policy it intended to enforce. A later PR can let the exec-server choose and build the correct sandbox for its own operating system. ## Important detail This keeps the portable intent separate from the local `SandboxType`. `SandboxType::None` is ambiguous: - it can mean the command was explicitly approved to run without a sandbox - it can also mean the orchestrator host has no concrete sandbox implementation available Those cases are different for remote execution. This PR adds `sandbox_requested` so an executor can still receive sandbox intent when the orchestrator cannot build a local wrapper. Explicit unsandboxed retries still send no sandbox context. ## Behavior today This PR only transports the intent. The exec-server accepts the new fields but does not apply them yet. Remote commands therefore remain unsandboxed after this PR, just as they are after PR #29099. ## Follow-up The next PR will make exec-server read this portable intent, bind symbolic workspace permissions to executor-native roots, choose the sandbox for its own operating system, build the wrapper locally, and then spawn the command. --- codex-rs/core/src/exec.rs | 2 + codex-rs/core/src/sandboxing/mod.rs | 7 +++ codex-rs/core/src/tasks/user_shell.rs | 2 + codex-rs/core/src/tools/orchestrator.rs | 42 +++++++++++++---- .../src/tools/runtimes/apply_patch_tests.rs | 4 ++ codex-rs/core/src/tools/runtimes/mod_tests.rs | 2 + .../tools/runtimes/shell/unix_escalation.rs | 4 ++ codex-rs/core/src/tools/sandboxing.rs | 30 +++++++++++- codex-rs/core/src/tools/sandboxing_tests.rs | 29 ++++++++---- .../core/src/unified_exec/process_manager.rs | 2 + .../src/unified_exec/process_manager_tests.rs | 2 + codex-rs/exec-server/src/environment.rs | 2 + codex-rs/exec-server/src/local_process.rs | 2 + codex-rs/exec-server/src/protocol.rs | 6 +++ .../exec-server/src/server/handler/tests.rs | 2 + codex-rs/exec-server/src/server/processor.rs | 2 + codex-rs/exec-server/tests/exec_process.rs | 24 ++++++++++ codex-rs/exec-server/tests/relay.rs | 2 + .../rmcp-client/src/stdio_server_launcher.rs | 2 + codex-rs/sandboxing/src/manager.rs | 46 ++++++++++++------- 20 files changed, 178 insertions(+), 36 deletions(-) diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 9d55ae886..99c87cdd9 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -456,6 +456,8 @@ pub(crate) async fn execute_exec_request( windows_sandbox_filesystem_overrides, network_environment_id, arg0, + exec_server_sandbox: _, + exec_server_enforce_managed_network: _, } = exec_request; // TODO(anp): Keep PathUri through the local process launch boundary. diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index c156b0d70..d0006f865 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -14,6 +14,7 @@ use crate::exec::execute_exec_request; #[cfg(target_os = "macos")] use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; +use codex_file_system::FileSystemSandboxContext; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::exec_output::ExecToolCallOutput; @@ -60,6 +61,8 @@ pub struct ExecRequest { pub network_sandbox_policy: NetworkSandboxPolicy, pub(crate) windows_sandbox_filesystem_overrides: Option, pub arg0: Option, + pub(crate) exec_server_sandbox: Option, + pub(crate) exec_server_enforce_managed_network: bool, } impl ExecRequest { @@ -102,6 +105,8 @@ impl ExecRequest { network_sandbox_policy, windows_sandbox_filesystem_overrides: None, arg0, + exec_server_sandbox: None, + exec_server_enforce_managed_network: false, } } @@ -158,6 +163,8 @@ impl ExecRequest { network_sandbox_policy, windows_sandbox_filesystem_overrides: None, arg0, + exec_server_sandbox: None, + exec_server_enforce_managed_network: false, } } } diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index c5b44a649..844e327bd 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -224,6 +224,8 @@ pub(crate) async fn execute_user_shell_command( network_sandbox_policy: permission_profile.network_sandbox_policy(), windows_sandbox_filesystem_overrides: None, arg0: None, + exec_server_sandbox: None, + exec_server_enforce_managed_network: false, }; let stdout_stream = Some(StdoutStream { diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index 803b8d6a9..aaa0dfe9d 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -84,7 +84,9 @@ impl ToolOrchestrator { }; let attempt_with_network_approval = SandboxAttempt { sandbox: attempt.sandbox, + sandbox_requested: attempt.sandbox_requested, permissions: attempt.permissions, + exec_server_permissions: attempt.exec_server_permissions, enforce_managed_network: attempt.enforce_managed_network, manager: attempt.manager, sandbox_cwd: attempt.sandbox_cwd, @@ -225,16 +227,27 @@ impl ToolOrchestrator { &file_system_sandbox_policy, ); let managed_network_active = turn_ctx.network.is_some(); - let initial_sandbox = match sandbox_override { - SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None, - SandboxOverride::NoOverride => self.sandbox.select_initial( + let sandbox_preference = tool.sandbox_preference(); + let sandbox_requested = match sandbox_override { + SandboxOverride::BypassSandboxFirstAttempt => false, + SandboxOverride::NoOverride => self.sandbox.should_sandbox( &file_system_sandbox_policy, network_sandbox_policy, - tool.sandbox_preference(), - turn_ctx.windows_sandbox_level, + sandbox_preference, managed_network_active, ), }; + let initial_sandbox = if sandbox_requested { + self.sandbox.select_initial( + &file_system_sandbox_policy, + network_sandbox_policy, + sandbox_preference, + turn_ctx.windows_sandbox_level, + managed_network_active, + ) + } else { + SandboxType::None + }; // Platform-specific flag gating is handled by SandboxManager::select_initial. let use_legacy_landlock = turn_ctx.config.features.use_legacy_landlock(); @@ -246,7 +259,9 @@ impl ToolOrchestrator { let workspace_roots = turn_ctx.config.effective_workspace_roots(); let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, + sandbox_requested, permissions: &turn_ctx.permission_profile, + exec_server_permissions: turn_ctx.config.permissions.permission_profile(), enforce_managed_network: managed_network_active, manager: &self.sandbox, sandbox_cwd: &sandbox_policy_cwd, @@ -401,16 +416,23 @@ impl ToolOrchestrator { .await?; } - let retry_sandbox = if unsandboxed_allowed { - SandboxType::None - } else { + let retry_sandbox_requested = !unsandboxed_allowed + && self.sandbox.should_sandbox( + &file_system_sandbox_policy, + network_sandbox_policy, + sandbox_preference, + managed_network_active, + ); + let retry_sandbox = if retry_sandbox_requested { self.sandbox.select_initial( &file_system_sandbox_policy, network_sandbox_policy, - tool.sandbox_preference(), + sandbox_preference, turn_ctx.windows_sandbox_level, managed_network_active, ) + } else { + SandboxType::None }; let retry_codex_linux_sandbox_exe = if unsandboxed_allowed { None @@ -419,7 +441,9 @@ impl ToolOrchestrator { }; let retry_attempt = SandboxAttempt { sandbox: retry_sandbox, + sandbox_requested: retry_sandbox_requested, permissions: &turn_ctx.permission_profile, + exec_server_permissions: turn_ctx.config.permissions.permission_profile(), enforce_managed_network: managed_network_active, manager: &self.sandbox, sandbox_cwd: &sandbox_policy_cwd, diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 054f2c7c1..f1c6f43aa 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -220,7 +220,9 @@ async fn file_system_sandbox_context_uses_active_attempt() { let sandbox_policy_cwd = PathUri::from_abs_path(&path); let attempt = SandboxAttempt { sandbox: SandboxType::MacosSeatbelt, + sandbox_requested: true, permissions: &permissions, + exec_server_permissions: &permissions, enforce_managed_network: false, manager: &manager, sandbox_cwd: &sandbox_policy_cwd, @@ -286,7 +288,9 @@ async fn no_sandbox_attempt_has_no_file_system_context() { let sandbox_policy_cwd = PathUri::from_abs_path(&path); let attempt = SandboxAttempt { sandbox: SandboxType::None, + sandbox_requested: false, permissions: &permissions, + exec_server_permissions: &permissions, enforce_managed_network: false, manager: &manager, sandbox_cwd: &sandbox_policy_cwd, diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 91db26aaa..9b485dbe5 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -106,7 +106,9 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow:: let manager = SandboxManager::new(); let attempt = SandboxAttempt { sandbox: SandboxType::None, + sandbox_requested: false, permissions: &permissions, + exec_server_permissions: &permissions, enforce_managed_network: false, manager: &manager, sandbox_cwd: &sandbox_policy_cwd, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index c08148c7e..a77645f94 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -166,6 +166,8 @@ pub(super) async fn try_run_zsh_fork( network_sandbox_policy, windows_sandbox_filesystem_overrides: _windows_sandbox_filesystem_overrides, arg0, + exec_server_sandbox: _, + exec_server_enforce_managed_network: _, } = sandbox_exec_request; let ParsedShellCommand { script, login, .. } = extract_shell_script(&command)?; let effective_timeout = Duration::from_millis( @@ -898,6 +900,8 @@ impl CoreShellCommandExecutor { network_sandbox_policy: self.network_sandbox_policy, windows_sandbox_filesystem_overrides: None, arg0: self.arg0.clone(), + exec_server_sandbox: None, + exec_server_enforce_managed_network: false, }, /*stdout_stream*/ None, after_spawn, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 847a3c758..f9d21dccf 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -11,6 +11,7 @@ use crate::session::turn_context::TurnContext; use crate::state::SessionServices; use crate::tools::hook_names::HookToolName; use crate::tools::network_approval::NetworkApprovalSpec; +use codex_file_system::FileSystemSandboxContext; use codex_network_proxy::NetworkProxy; use codex_protocol::approvals::ExecPolicyAmendment; use codex_protocol::approvals::NetworkApprovalContext; @@ -24,6 +25,7 @@ use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxTransformRequest; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; +use codex_sandboxing::policy_transforms::effective_permission_profile; use codex_tools::ToolName; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; @@ -408,7 +410,11 @@ pub(crate) trait ToolRuntime: Approvable + Sandboxable { pub(crate) struct SandboxAttempt<'a> { pub sandbox: SandboxType, + /// Whether policy requested sandboxing, independent of this host's concrete wrapper. + pub sandbox_requested: bool, pub permissions: &'a codex_protocol::models::PermissionProfile, + /// Canonical permissions before this host materializes workspace roots. + pub exec_server_permissions: &'a codex_protocol::models::PermissionProfile, pub enforce_managed_network: bool, pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a PathUri, @@ -460,6 +466,10 @@ impl<'a> SandboxAttempt<'a> { network: Option<&NetworkProxy>, environment_id: Option<&str>, ) -> Result { + let exec_server_permissions = effective_permission_profile( + self.exec_server_permissions, + command.additional_permissions.as_ref(), + ); let request = self .manager .transform(SandboxTransformRequest { @@ -477,11 +487,27 @@ impl<'a> SandboxAttempt<'a> { windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, }) .map_err(CodexErr::from)?; - Ok(crate::sandboxing::ExecRequest::from_sandbox_exec_request( + let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request( request, options, self.workspace_roots.to_vec(), - )) + ); + if self.sandbox_requested { + exec_request.exec_server_sandbox = Some(FileSystemSandboxContext { + permissions: exec_server_permissions.into(), + cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()), + workspace_roots: self + .workspace_roots + .iter() + .map(PathUri::from_abs_path) + .collect(), + windows_sandbox_level: self.windows_sandbox_level, + windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, + use_legacy_landlock: self.use_legacy_landlock, + }); + exec_request.exec_server_enforce_managed_network = self.enforce_managed_network; + } + Ok(exec_request) } } diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index f4d6e6ce4..47b0b4e89 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -4,7 +4,6 @@ use crate::tools::hook_names::HookToolName; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; -use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::GranularApprovalConfig; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxManager; @@ -202,21 +201,23 @@ fn deny_read_blocks_explicit_escalation_and_policy_bypass() { } #[test] -fn exec_server_env_keeps_command_native() { +fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { let cwd: AbsolutePathBuf = std::env::current_dir() .expect("current dir") .try_into() .expect("absolute cwd"); let cwd_uri = PathUri::from_abs_path(&cwd); - let permissions = codex_protocol::models::PermissionProfile::from_runtime_permissions( - &FileSystemSandboxPolicy::default(), - NetworkSandboxPolicy::Restricted, - ); + let exec_server_permissions = codex_protocol::models::PermissionProfile::workspace_write(); + let permissions = exec_server_permissions + .clone() + .materialize_project_roots_with_workspace_roots(std::slice::from_ref(&cwd)); let manager = SandboxManager::new(); let attempt = SandboxAttempt { - sandbox: SandboxType::MacosSeatbelt, + sandbox: SandboxType::None, + sandbox_requested: true, permissions: &permissions, - enforce_managed_network: false, + exec_server_permissions: &exec_server_permissions, + enforce_managed_network: true, manager: &manager, sandbox_cwd: &cwd_uri, workspace_roots: std::slice::from_ref(&cwd), @@ -252,4 +253,16 @@ fn exec_server_env_keeps_command_native() { ); assert_eq!(request.arg0, None); assert_eq!(request.sandbox, SandboxType::None); + assert_eq!( + request.exec_server_sandbox, + Some(codex_exec_server::FileSystemSandboxContext { + permissions: exec_server_permissions.into(), + cwd: Some(cwd_uri), + workspace_roots: vec![PathUri::from_abs_path(&cwd)], + windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, + use_legacy_landlock: false, + }) + ); + assert!(request.exec_server_enforce_managed_network); } diff --git a/codex-rs/core/src/unified_exec/process_manager.rs b/codex-rs/core/src/unified_exec/process_manager.rs index fc5508eb8..a3b4bcf9c 100644 --- a/codex-rs/core/src/unified_exec/process_manager.rs +++ b/codex-rs/core/src/unified_exec/process_manager.rs @@ -166,6 +166,8 @@ fn exec_server_params_for_request( tty, pipe_stdin: false, arg0: request.arg0.clone(), + sandbox: request.exec_server_sandbox.clone(), + enforce_managed_network: request.exec_server_enforce_managed_network, } } diff --git a/codex-rs/core/src/unified_exec/process_manager_tests.rs b/codex-rs/core/src/unified_exec/process_manager_tests.rs index 5abd926c6..b6afdc85b 100644 --- a/codex-rs/core/src/unified_exec/process_manager_tests.rs +++ b/codex-rs/core/src/unified_exec/process_manager_tests.rs @@ -111,6 +111,8 @@ fn exec_server_params_use_path_uri_and_env_policy_overlay_contract() { network_sandbox_policy, windows_sandbox_filesystem_overrides: None, arg0: None, + exec_server_sandbox: None, + exec_server_enforce_managed_network: false, }; let params = diff --git a/codex-rs/exec-server/src/environment.rs b/codex-rs/exec-server/src/environment.rs index 9496bb8be..d3e7e5e98 100644 --- a/codex-rs/exec-server/src/environment.rs +++ b/codex-rs/exec-server/src/environment.rs @@ -1156,6 +1156,8 @@ mod tests { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await .expect("start process"); diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index 5e1b52b11..1a6f2cf00 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -902,6 +902,8 @@ mod tests { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, } } diff --git a/codex-rs/exec-server/src/protocol.rs b/codex-rs/exec-server/src/protocol.rs index 97ef86aad..e05595f27 100644 --- a/codex-rs/exec-server/src/protocol.rs +++ b/codex-rs/exec-server/src/protocol.rs @@ -103,6 +103,12 @@ pub struct ExecParams { /// Optional process-visible argv0 override. Values such as `codex-linux-sandbox` are command /// names rather than paths, so this is not a [`PathUri`]. pub arg0: Option, + /// Portable sandbox intent. Concrete wrapper argv is resolved by the exec-server. + #[serde(default)] + pub sandbox: Option, + /// Whether the eventual executor-side sandbox must enforce managed networking. + #[serde(default)] + pub enforce_managed_network: bool, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] diff --git a/codex-rs/exec-server/src/server/handler/tests.rs b/codex-rs/exec-server/src/server/handler/tests.rs index 0bd682bd9..9cbbf78d0 100644 --- a/codex-rs/exec-server/src/server/handler/tests.rs +++ b/codex-rs/exec-server/src/server/handler/tests.rs @@ -33,6 +33,8 @@ fn exec_params_with_argv(process_id: &str, argv: Vec) -> ExecParams { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, } } diff --git a/codex-rs/exec-server/src/server/processor.rs b/codex-rs/exec-server/src/server/processor.rs index 952f41421..b2daba7ca 100644 --- a/codex-rs/exec-server/src/server/processor.rs +++ b/codex-rs/exec-server/src/server/processor.rs @@ -403,6 +403,8 @@ mod tests { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, } } diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index 11a3f3ece..ac38f50ea 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -81,6 +81,8 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), "proc-1"); @@ -222,6 +224,8 @@ async fn assert_exec_process_streams_output(use_remote: bool) -> Result<()> { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -253,6 +257,8 @@ async fn assert_exec_process_pushes_events(use_remote: bool) -> Result<()> { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -300,6 +306,8 @@ async fn assert_exec_process_replays_events_after_close(use_remote: bool) -> Res tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -348,6 +356,8 @@ async fn assert_exec_process_retains_output_after_exit_until_streams_close( tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -421,6 +431,8 @@ async fn assert_exec_process_write_then_read(use_remote: bool) -> Result<()> { tty: true, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -458,6 +470,8 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re tty: false, pipe_stdin: true, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -491,6 +505,8 @@ async fn assert_exec_process_rejects_write_without_pipe_stdin(use_remote: bool) tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -525,6 +541,8 @@ async fn assert_exec_process_signal_interrupts_process(use_remote: bool) -> Resu tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!(session.process.process_id().as_str(), process_id); @@ -578,6 +596,8 @@ async fn assert_exec_process_signal_reports_unsupported_on_windows(use_remote: b tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; @@ -618,6 +638,8 @@ async fn assert_exec_process_preserves_queued_events_before_subscribe( tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; @@ -676,6 +698,8 @@ async fn remote_exec_process_recovers_after_transport_disconnect() -> Result<()> tty: false, pipe_stdin: true, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; diff --git a/codex-rs/exec-server/tests/relay.rs b/codex-rs/exec-server/tests/relay.rs index 5e5861841..5f49655e3 100644 --- a/codex-rs/exec-server/tests/relay.rs +++ b/codex-rs/exec-server/tests/relay.rs @@ -150,6 +150,8 @@ async fn remote_environment_routes_encrypted_exec_server_rpc() -> Result<()> { tty: false, pipe_stdin: false, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await?; assert_eq!( diff --git a/codex-rs/rmcp-client/src/stdio_server_launcher.rs b/codex-rs/rmcp-client/src/stdio_server_launcher.rs index 8ada806ff..94aa45c1e 100644 --- a/codex-rs/rmcp-client/src/stdio_server_launcher.rs +++ b/codex-rs/rmcp-client/src/stdio_server_launcher.rs @@ -503,6 +503,8 @@ impl ExecutorStdioServerLauncher { tty: false, pipe_stdin: true, arg0: None, + sandbox: None, + enforce_managed_network: false, }) .await .map_err(io::Error::other)?; diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index dd7aba361..29b1177b1 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -282,24 +282,36 @@ impl SandboxManager { windows_sandbox_level: WindowsSandboxLevel, has_managed_network_requirements: bool, ) -> SandboxType { + if self.should_sandbox( + file_system_policy, + network_policy, + pref, + has_managed_network_requirements, + ) { + get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) + .unwrap_or(SandboxType::None) + } else { + SandboxType::None + } + } + + /// Returns whether the request needs a sandbox, independently of whether + /// this host can provide a concrete sandbox implementation. + pub fn should_sandbox( + &self, + file_system_policy: &FileSystemSandboxPolicy, + network_policy: NetworkSandboxPolicy, + pref: SandboxablePreference, + has_managed_network_requirements: bool, + ) -> bool { match pref { - SandboxablePreference::Forbid => SandboxType::None, - SandboxablePreference::Require => { - get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) - .unwrap_or(SandboxType::None) - } - SandboxablePreference::Auto => { - if should_require_platform_sandbox( - file_system_policy, - network_policy, - has_managed_network_requirements, - ) { - get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) - .unwrap_or(SandboxType::None) - } else { - SandboxType::None - } - } + SandboxablePreference::Forbid => false, + SandboxablePreference::Require => true, + SandboxablePreference::Auto => should_require_platform_sandbox( + file_system_policy, + network_policy, + has_managed_network_requirements, + ), } }