From c379459bba59243aa8732d0d0b93cc2f326f87e4 Mon Sep 17 00:00:00 2001 From: iceweasel-oai Date: Sun, 13 Sep 2026 05:56:16 +0000 Subject: [PATCH] Wire the Windows MXC sandbox into command execution (#45176) ## What changed - Add explicit MXC backend selection and carry its identity through exec-server process reporting and sandbox violation classification. - Launch MXC through the Codex executable with the effective permission profile and command environment. - Reject exec-server MXC requests when native MXC is unavailable or when they request a TTY, an `arg0` override, or managed networking. Reject private desktop isolation during MXC preparation. - Allow an explicitly empty child environment and avoid exposing request payload values in launcher decode errors. ## Testing Add coverage for sandbox selection and unsupported-request rejection, plus Windows RPC tests for stdin writes and temporary-directory permissions derived from the command environment. Native MXC tests skip when MXC is unavailable. GitOrigin-RevId: 3626ff0f9ad7f9b812ce09b68c31ea9a5a9c72b1 --- .../command_exec_processor.rs | 1 + .../request_processors/thread_processor.rs | 2 +- .../windows_sandbox_processor.rs | 4 +- codex-rs/core/src/config/mod.rs | 2 +- codex-rs/core/src/exec.rs | 9 +- codex-rs/core/src/exec_tests.rs | 4 + codex-rs/core/src/sandbox_tags.rs | 9 +- codex-rs/core/src/shell_snapshot_sandbox.rs | 8 +- .../core/src/shell_snapshot_sandbox_tests.rs | 2 +- codex-rs/core/src/shell_snapshot_tests.rs | 2 +- codex-rs/core/src/tools/orchestrator.rs | 15 ++- .../src/tools/runtimes/apply_patch_tests.rs | 4 +- codex-rs/core/src/tools/runtimes/mod_tests.rs | 2 +- .../runtimes/zsh_fork/unix_escalation.rs | 2 +- codex-rs/core/src/tools/sandboxing.rs | 8 +- codex-rs/core/src/tools/sandboxing_tests.rs | 4 +- codex-rs/core/tests/suite/exec.rs | 1 + codex-rs/core/tests/suite/windows_sandbox.rs | 3 + codex-rs/exec-server-protocol/src/protocol.rs | 7 ++ codex-rs/exec-server/src/fs_sandbox.rs | 6 +- codex-rs/exec-server/src/local_process.rs | 1 + codex-rs/exec-server/src/process.rs | 1 + codex-rs/exec-server/src/process_sandbox.rs | 28 ++++- .../exec-server/src/process_sandbox_tests.rs | 27 +++++ codex-rs/exec-server/tests/common/mod.rs | 3 + codex-rs/exec-server/tests/exec_process.rs | 46 ++++++- .../tests/exec_process/windows_sandbox.rs | 112 ++++++++++++++++++ codex-rs/exec/tests/suite/sandbox.rs | 1 + .../linux-sandbox/tests/suite/landlock.rs | 2 + codex-rs/mxc-sandbox/README.md | 5 +- codex-rs/mxc-sandbox/src/lib.rs | 2 +- codex-rs/mxc-sandbox/src/windows.rs | 10 +- codex-rs/protocol/src/config_types.rs | 1 + codex-rs/sandboxing/src/lib.rs | 4 + codex-rs/sandboxing/src/manager.rs | 112 ++++++++++++------ codex-rs/sandboxing/src/manager_tests.rs | 54 ++++++++- codex-rs/sandboxing/src/violation.rs | 3 + 37 files changed, 422 insertions(+), 85 deletions(-) create mode 100644 codex-rs/exec-server/tests/exec_process/windows_sandbox.rs diff --git a/codex-rs/app-server/src/request_processors/command_exec_processor.rs b/codex-rs/app-server/src/request_processors/command_exec_processor.rs index fde1b9e895..9ce19397d2 100644 --- a/codex-rs/app-server/src/request_processors/command_exec_processor.rs +++ b/codex-rs/app-server/src/request_processors/command_exec_processor.rs @@ -315,6 +315,7 @@ impl CommandExecRequestProcessor { &sandbox_cwd, windows_sandbox_workspace_roots.as_slice(), &codex_linux_sandbox_exe, + &self.arg0_paths.codex_self_exe, use_legacy_landlock, ) .map_err(|err| internal_error(format!("exec failed: {err}")))?; diff --git a/codex-rs/app-server/src/request_processors/thread_processor.rs b/codex-rs/app-server/src/request_processors/thread_processor.rs index c6e0524ba3..de98fc7cde 100644 --- a/codex-rs/app-server/src/request_processors/thread_processor.rs +++ b/codex-rs/app-server/src/request_processors/thread_processor.rs @@ -4907,7 +4907,7 @@ impl ThreadRequestProcessor { serde_json::json!("unelevated"), ); } - WindowsSandboxLevel::Disabled => {} + WindowsSandboxLevel::Disabled | WindowsSandboxLevel::Mxc => {} } } let request_overrides = if cli_overrides.is_empty() { diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index 70e0e6efa8..34ffcfc129 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -222,7 +222,9 @@ fn determine_windows_sandbox_readiness_from_state( ) -> WindowsSandboxReadinessResponse { let status = match windows_sandbox_level { WindowsSandboxLevel::Disabled => WindowsSandboxReadiness::NotConfigured, - WindowsSandboxLevel::RestrictedToken => WindowsSandboxReadiness::Ready, + WindowsSandboxLevel::RestrictedToken | WindowsSandboxLevel::Mxc => { + WindowsSandboxReadiness::Ready + } WindowsSandboxLevel::Elevated => { if sandbox_setup_is_complete { WindowsSandboxReadiness::Ready diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 5ae1f366a0..f1aa685185 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -3329,7 +3329,7 @@ impl Config { match WindowsSandboxLevel::from_features(&features) { WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated), WindowsSandboxLevel::RestrictedToken => Some(WindowsSandboxModeToml::Unelevated), - WindowsSandboxLevel::Disabled => None, + WindowsSandboxLevel::Disabled | WindowsSandboxLevel::Mxc => None, } }); apply_requirement_constrained_value( diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 95b78ebace..78c0076f0d 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -300,6 +300,7 @@ pub async fn process_exec_tool_call( sandbox_cwd: &AbsolutePathBuf, windows_sandbox_workspace_roots: &[AbsolutePathBuf], codex_linux_sandbox_exe: &Option, + codex_self_exe: &Option, use_legacy_landlock: bool, stdout_stream: Option, ) -> Result { @@ -309,6 +310,7 @@ pub async fn process_exec_tool_call( sandbox_cwd, windows_sandbox_workspace_roots, codex_linux_sandbox_exe, + codex_self_exe, use_legacy_landlock, )?; @@ -324,6 +326,7 @@ pub fn build_exec_request( sandbox_cwd: &AbsolutePathBuf, windows_sandbox_workspace_roots: &[AbsolutePathBuf], codex_linux_sandbox_exe: &Option, + codex_self_exe: &Option, use_legacy_landlock: bool, ) -> Result { let ExecParams { @@ -390,7 +393,11 @@ pub fn build_exec_request( environment_id: network_environment_id.as_deref(), network: network.as_ref(), sandbox_policy_cwd: &sandbox_policy_cwd_uri, - codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_deref(), + sandbox_exe: if cfg!(windows) { + codex_self_exe.as_deref() + } else { + codex_linux_sandbox_exe.as_deref() + }, use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index fde92af514..93000cfb51 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -515,6 +515,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy( &cwd, std::slice::from_ref(&cwd), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ) @@ -1219,6 +1220,7 @@ fn build_exec_request_preserves_windows_workspace_roots() -> Result<()> { &cwd, workspace_roots.as_slice(), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, )?; @@ -1338,6 +1340,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { &cwd, std::slice::from_ref(&cwd), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ), @@ -1419,6 +1422,7 @@ while :; do sleep 1; done"# &cwd, std::slice::from_ref(&cwd), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ), diff --git a/codex-rs/core/src/sandbox_tags.rs b/codex-rs/core/src/sandbox_tags.rs index 7c453996f4..120dbff0da 100644 --- a/codex-rs/core/src/sandbox_tags.rs +++ b/codex-rs/core/src/sandbox_tags.rs @@ -77,9 +77,12 @@ fn permission_profile_sandbox_tag( } } } - if cfg!(target_os = "windows") && matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated) - { - return "windows_elevated"; + if cfg!(target_os = "windows") { + match windows_sandbox_level { + WindowsSandboxLevel::Mxc => return "windows_mxc", + WindowsSandboxLevel::Elevated => return "windows_elevated", + WindowsSandboxLevel::Disabled | WindowsSandboxLevel::RestrictedToken => {} + } } get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) diff --git a/codex-rs/core/src/shell_snapshot_sandbox.rs b/codex-rs/core/src/shell_snapshot_sandbox.rs index 758aa3dbd5..3bb156dd60 100644 --- a/codex-rs/core/src/shell_snapshot_sandbox.rs +++ b/codex-rs/core/src/shell_snapshot_sandbox.rs @@ -39,7 +39,7 @@ pub(crate) struct ShellSnapshotSandbox { enforce_managed_network: bool, sandbox_policy_cwd: PathUri, workspace_roots: Vec, - codex_linux_sandbox_exe: Option, + sandbox_exe: Option, use_legacy_landlock: bool, windows_sandbox_level: WindowsSandboxLevel, windows_sandbox_private_desktop: bool, @@ -84,7 +84,7 @@ impl ShellSnapshotSandbox { enforce_managed_network: attempt.enforce_managed_network, sandbox_policy_cwd: attempt.sandbox_cwd.clone(), workspace_roots: attempt.workspace_roots.to_vec(), - codex_linux_sandbox_exe: attempt.codex_linux_sandbox_exe.cloned(), + sandbox_exe: attempt.sandbox_exe.cloned(), use_legacy_landlock: attempt.use_legacy_landlock, windows_sandbox_level: attempt.windows_sandbox_level, windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop, @@ -103,7 +103,7 @@ impl ShellSnapshotSandbox { self.enforce_managed_network, &self.sandbox_policy_cwd, &self.workspace_roots, - &self.codex_linux_sandbox_exe, + &self.sandbox_exe, self.use_legacy_landlock, self.windows_sandbox_level, self.windows_sandbox_private_desktop, @@ -172,7 +172,7 @@ impl ShellSnapshotSandbox { environment_id: Some(&self.environment_id), network: self.network.as_ref(), sandbox_policy_cwd: &self.sandbox_policy_cwd, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(), + sandbox_exe: self.sandbox_exe.as_deref(), use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, diff --git a/codex-rs/core/src/shell_snapshot_sandbox_tests.rs b/codex-rs/core/src/shell_snapshot_sandbox_tests.rs index 2a44248977..85fa3b9db0 100644 --- a/codex-rs/core/src/shell_snapshot_sandbox_tests.rs +++ b/codex-rs/core/src/shell_snapshot_sandbox_tests.rs @@ -42,7 +42,7 @@ async fn snapshot_failure_omits_credentials_and_stops_descendants( manager: &manager, sandbox_cwd: &cwd_uri, workspace_roots: std::slice::from_ref(&cwd_uri), - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/shell_snapshot_tests.rs b/codex-rs/core/src/shell_snapshot_tests.rs index 4193e35221..d88af88829 100644 --- a/codex-rs/core/src/shell_snapshot_tests.rs +++ b/codex-rs/core/src/shell_snapshot_tests.rs @@ -416,7 +416,7 @@ async fn inactive_profiles_keep_snapshots_but_active_brokers_require_sandbox() - manager: &manager, sandbox_cwd: &cwd_uri, workspace_roots: std::slice::from_ref(&cwd_uri), - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index ec5be46125..4e66949f24 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -88,7 +88,7 @@ impl ToolOrchestrator { manager: attempt.manager, sandbox_cwd: attempt.sandbox_cwd, workspace_roots: attempt.workspace_roots, - codex_linux_sandbox_exe: attempt.codex_linux_sandbox_exe, + sandbox_exe: attempt.sandbox_exe, use_legacy_landlock: attempt.use_legacy_landlock, windows_sandbox_level: attempt.windows_sandbox_level, windows_sandbox_private_desktop: attempt.windows_sandbox_private_desktop, @@ -290,6 +290,11 @@ impl ToolOrchestrator { .sandbox_cwd(req) .cloned() .unwrap_or_else(|| environment.cwd().clone()); + let codex_sandbox_exe = if cfg!(windows) { + turn_ctx.config.codex_self_exe.as_ref() + } else { + turn_ctx.config.codex_linux_sandbox_exe.as_ref() + }; let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, sandbox_requested, @@ -299,7 +304,7 @@ impl ToolOrchestrator { manager: &sandbox_manager, sandbox_cwd: &sandbox_policy_cwd, workspace_roots, - codex_linux_sandbox_exe: turn_ctx.config.codex_linux_sandbox_exe.as_ref(), + sandbox_exe: codex_sandbox_exe, use_legacy_landlock: sandbox_config.use_legacy_landlock, windows_sandbox_level: sandbox_config.windows_sandbox_level, windows_sandbox_private_desktop: sandbox_config.windows_sandbox_private_desktop, @@ -459,10 +464,10 @@ impl ToolOrchestrator { } else { SandboxType::None }; - let retry_codex_linux_sandbox_exe = if unsandboxed_allowed { + let retry_sandbox_exe = if unsandboxed_allowed { None } else { - turn_ctx.config.codex_linux_sandbox_exe.as_ref() + codex_sandbox_exe }; let retry_attempt = SandboxAttempt { sandbox: retry_sandbox, @@ -473,7 +478,7 @@ impl ToolOrchestrator { manager: &sandbox_manager, sandbox_cwd: &sandbox_policy_cwd, workspace_roots, - codex_linux_sandbox_exe: retry_codex_linux_sandbox_exe, + sandbox_exe: retry_sandbox_exe, use_legacy_landlock: sandbox_config.use_legacy_landlock, windows_sandbox_level: sandbox_config.windows_sandbox_level, windows_sandbox_private_desktop: sandbox_config.windows_sandbox_private_desktop, 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 35470229b4..becb1c4839 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -246,7 +246,7 @@ async fn file_system_sandbox_context_preserves_executor_workspace_permissions() manager: &manager, sandbox_cwd: &sandbox_policy_cwd, workspace_roots: std::slice::from_ref(&sandbox_policy_cwd), - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: true, windows_sandbox_level: WindowsSandboxLevel::RestrictedToken, windows_sandbox_private_desktop: true, @@ -315,7 +315,7 @@ async fn file_system_sandbox_context_respects_sandbox_request() { manager: &manager, sandbox_cwd: &sandbox_policy_cwd, workspace_roots: std::slice::from_ref(&sandbox_policy_cwd), - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/tools/runtimes/mod_tests.rs b/codex-rs/core/src/tools/runtimes/mod_tests.rs index 8d0d9b5f8b..8640654856 100644 --- a/codex-rs/core/src/tools/runtimes/mod_tests.rs +++ b/codex-rs/core/src/tools/runtimes/mod_tests.rs @@ -124,7 +124,7 @@ async fn explicit_escalation_prepares_exec_without_managed_network() -> anyhow:: manager: &manager, sandbox_cwd: &sandbox_policy_cwd, workspace_roots: std::slice::from_ref(&sandbox_policy_cwd), - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs index d0be8e1f9b..b32bbf1a1d 100644 --- a/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/zsh_fork/unix_escalation.rs @@ -789,7 +789,7 @@ impl CoreShellCommandExecutor { environment_id: self.network_environment_id.as_deref(), network: self.network.as_ref(), sandbox_policy_cwd: &sandbox_policy_cwd, - codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_deref(), + sandbox_exe: self.codex_linux_sandbox_exe.as_deref(), use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 615b917413..a9931e54e0 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -394,7 +394,7 @@ pub(crate) struct SandboxAttempt<'a> { pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a PathUri, pub(crate) workspace_roots: &'a [PathUri], - pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, + pub sandbox_exe: Option<&'a std::path::PathBuf>, // TODO(anp): Reconcile these attempt settings with TurnEnvironment::sandbox_context // so process execution and patch writes honor the selected environment's backend. pub use_legacy_landlock: bool, @@ -459,9 +459,7 @@ impl<'a> SandboxAttempt<'a> { environment_id, network, sandbox_policy_cwd: self.sandbox_cwd, - codex_linux_sandbox_exe: self - .codex_linux_sandbox_exe - .map(std::path::PathBuf::as_path), + sandbox_exe: self.sandbox_exe.map(std::path::PathBuf::as_path), use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, @@ -496,7 +494,7 @@ impl<'a> SandboxAttempt<'a> { environment_id: None, network: None, sandbox_policy_cwd: self.sandbox_cwd, - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, diff --git a/codex-rs/core/src/tools/sandboxing_tests.rs b/codex-rs/core/src/tools/sandboxing_tests.rs index 0a9e1fe114..3f47ae13e6 100644 --- a/codex-rs/core/src/tools/sandboxing_tests.rs +++ b/codex-rs/core/src/tools/sandboxing_tests.rs @@ -255,7 +255,7 @@ fn windows_sandbox_env_preserves_denied_reads_or_rejects_unsupported_backend() { manager: &manager, sandbox_cwd: &cwd_uri, workspace_roots: std::slice::from_ref(&cwd_uri), - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Elevated, windows_sandbox_private_desktop: false, @@ -326,7 +326,7 @@ fn exec_server_env_keeps_command_native_and_carries_sandbox_context() { manager: &manager, sandbox_cwd: &cwd_uri, workspace_roots: std::slice::from_ref(&cwd_uri), - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index 355711e345..36f1dbd864 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -55,6 +55,7 @@ where &cwd, std::slice::from_ref(&cwd), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ) diff --git a/codex-rs/core/tests/suite/windows_sandbox.rs b/codex-rs/core/tests/suite/windows_sandbox.rs index 887ed5a42f..14e140ff8e 100644 --- a/codex-rs/core/tests/suite/windows_sandbox.rs +++ b/codex-rs/core/tests/suite/windows_sandbox.rs @@ -375,6 +375,7 @@ async fn windows_restricted_token_rejects_exact_and_glob_deny_read_policy() -> a &cwd, std::slice::from_ref(&cwd), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ) @@ -424,6 +425,7 @@ async fn windows_elevated_does_not_create_missing_workspace_metadata() -> anyhow &cwd, std::slice::from_ref(&cwd), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ) @@ -603,6 +605,7 @@ async fn windows_elevated_enforces_deny_read_and_protects_setup_marker() -> anyh &cwd, std::slice::from_ref(&cwd), &None, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ) diff --git a/codex-rs/exec-server-protocol/src/protocol.rs b/codex-rs/exec-server-protocol/src/protocol.rs index 64b9e2a346..8ef964028a 100644 --- a/codex-rs/exec-server-protocol/src/protocol.rs +++ b/codex-rs/exec-server-protocol/src/protocol.rs @@ -152,6 +152,9 @@ pub struct EnvironmentCapabilities { /// Whether shell state can be cached and restored entirely inside the executor. #[serde(default)] pub shell_snapshot_v2: bool, + /// Whether requests may explicitly select the MXC Windows sandbox backend. + #[serde(default)] + pub windows_mxc: bool, } /// Status returned by an initialized exec-server connection. @@ -243,6 +246,7 @@ impl EnvironmentInfo { http_header_env_vars: true, sandboxed_file_streaming: true, shell_snapshot_v2: cfg!(unix), + windows_mxc: false, }, } } @@ -358,6 +362,7 @@ pub enum ProcessSandboxType { MacosSeatbelt, LinuxSeccomp, WindowsRestrictedToken, + WindowsMxc, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -1057,6 +1062,7 @@ mod tests { http_header_env_vars: false, sandboxed_file_streaming: false, shell_snapshot_v2: false, + windows_mxc: false, } ); } @@ -1078,6 +1084,7 @@ mod tests { "httpHeaderEnvVars": false, "sandboxedFileStreaming": false, "shellSnapshotV2": false, + "windowsMxc": false, }, }); let info: EnvironmentInfo = serde_json::from_value(expected.clone()) diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index cd7c311267..566973e526 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -176,7 +176,11 @@ impl FileSystemSandboxRunner { environment_id: None, network: None, sandbox_policy_cwd: &cwd.uri, - codex_linux_sandbox_exe: self.runtime_paths.codex_linux_sandbox_exe.as_deref(), + sandbox_exe: if cfg!(windows) { + Some(self.runtime_paths.codex_self_exe.as_path()) + } else { + self.runtime_paths.codex_linux_sandbox_exe.as_deref() + }, use_legacy_landlock: sandbox_context.use_legacy_landlock, windows_sandbox_level: sandbox_context.windows_sandbox_level, windows_sandbox_private_desktop: sandbox_context diff --git a/codex-rs/exec-server/src/local_process.rs b/codex-rs/exec-server/src/local_process.rs index c728c7416e..be4606883c 100644 --- a/codex-rs/exec-server/src/local_process.rs +++ b/codex-rs/exec-server/src/local_process.rs @@ -396,6 +396,7 @@ impl LocalProcess { SandboxType::MacosSeatbelt => Some(ProcessSandboxType::MacosSeatbelt), SandboxType::LinuxSeccomp => Some(ProcessSandboxType::LinuxSeccomp), SandboxType::WindowsRestrictedToken => Some(ProcessSandboxType::WindowsRestrictedToken), + SandboxType::WindowsMxc => Some(ProcessSandboxType::WindowsMxc), }; let start = Arc::new(ProcessStart); diff --git a/codex-rs/exec-server/src/process.rs b/codex-rs/exec-server/src/process.rs index 726c8a6392..5e795faa1a 100644 --- a/codex-rs/exec-server/src/process.rs +++ b/codex-rs/exec-server/src/process.rs @@ -35,6 +35,7 @@ pub(crate) fn sandbox_type_from_protocol( Some(ProcessSandboxType::WindowsRestrictedToken) => { Some(SandboxType::WindowsRestrictedToken) } + Some(ProcessSandboxType::WindowsMxc) => Some(SandboxType::WindowsMxc), } } diff --git a/codex-rs/exec-server/src/process_sandbox.rs b/codex-rs/exec-server/src/process_sandbox.rs index a16c182c73..15bff80842 100644 --- a/codex-rs/exec-server/src/process_sandbox.rs +++ b/codex-rs/exec-server/src/process_sandbox.rs @@ -84,6 +84,28 @@ pub(crate) async fn prepare_exec_request_with_telemetry( network_policy_audit_observer: Option, telemetry: &ProcessTelemetry, ) -> Result { + if let Some(sandbox) = params.sandbox.as_ref() + && sandbox.windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Mxc + { + if params.tty || params.arg0.is_some() { + return Err(invalid_params( + "MXC currently supports ordinary pipe launches only".to_owned(), + )); + } + if params.enforce_managed_network + || params.managed_network.is_some() + || params.network_proxy.is_some() + { + return Err(invalid_params( + "MXC managed networking is not supported yet".to_owned(), + )); + } + if !codex_sandboxing::windows_mxc_available() { + return Err(invalid_params( + "native MXC is unavailable on this executor".to_owned(), + )); + } + } #[cfg(target_os = "windows")] let mut env = env; #[cfg(target_os = "windows")] @@ -241,7 +263,11 @@ pub(crate) async fn prepare_exec_request_with_telemetry( environment_id: None, network: None, sandbox_policy_cwd, - codex_linux_sandbox_exe: runtime_paths.codex_linux_sandbox_exe.as_deref(), + sandbox_exe: if cfg!(windows) { + Some(runtime_paths.codex_self_exe.as_path()) + } else { + runtime_paths.codex_linux_sandbox_exe.as_deref() + }, use_legacy_landlock: sandbox_context.use_legacy_landlock, windows_sandbox_level: sandbox_context.windows_sandbox_level, windows_sandbox_private_desktop: sandbox_context.windows_sandbox_private_desktop, diff --git a/codex-rs/exec-server/src/process_sandbox_tests.rs b/codex-rs/exec-server/src/process_sandbox_tests.rs index 00840569c1..d2b65555d0 100644 --- a/codex-rs/exec-server/src/process_sandbox_tests.rs +++ b/codex-rs/exec-server/src/process_sandbox_tests.rs @@ -129,6 +129,33 @@ async fn sandbox_request_wraps_native_argv_on_executor() { prepared.command.first().map(String::as_str), Some("/usr/bin/sandbox-exec") ); + + let mut params = params; + params.sandbox.as_mut().unwrap().windows_sandbox_level = + codex_protocol::config_types::WindowsSandboxLevel::Mxc; + for (tty, managed, message) in [ + (false, false, "native MXC is unavailable on this executor"), + ( + true, + false, + "MXC currently supports ordinary pipe launches only", + ), + (false, true, "MXC managed networking is not supported yet"), + ] { + params.tty = tty; + params.enforce_managed_network = managed; + let error = prepare_exec_request( + ¶ms, + HashMap::new(), + Some(&runtime_paths), + /*network_policy_decider*/ None, + /*network_policy_audit_observer*/ None, + ) + .await + .err() + .expect("unsupported MXC must fail closed"); + assert_eq!(error.message, message); + } } #[cfg(unix)] diff --git a/codex-rs/exec-server/tests/common/mod.rs b/codex-rs/exec-server/tests/common/mod.rs index d3e46aeeb8..6b3d2937d2 100644 --- a/codex-rs/exec-server/tests/common/mod.rs +++ b/codex-rs/exec-server/tests/common/mod.rs @@ -44,6 +44,9 @@ pub static TEST_BINARY_DISPATCH_GUARD: Option = { if argv1 == Some(CODEX_WINDOWS_SANDBOX_ARG1) { return TestBinaryDispatchMode::DispatchArg0Only; } + if argv1 == Some(codex_sandboxing::CODEX_WINDOWS_MXC_ARG1) { + return TestBinaryDispatchMode::DispatchArg0Only; + } if exe_name == CODEX_LINUX_SANDBOX_ARG0 { return TestBinaryDispatchMode::DispatchArg0Only; } diff --git a/codex-rs/exec-server/tests/exec_process.rs b/codex-rs/exec-server/tests/exec_process.rs index f1df5d78ae..b982301949 100644 --- a/codex-rs/exec-server/tests/exec_process.rs +++ b/codex-rs/exec-server/tests/exec_process.rs @@ -1,4 +1,6 @@ mod common; +#[path = "exec_process/windows_sandbox.rs"] +mod windows_sandbox; use std::collections::HashMap; #[cfg(unix)] @@ -102,6 +104,10 @@ async fn create_process_context(use_remote: bool) -> Result { } } +fn selected_windows_sandbox_available(windows_sandbox_level: WindowsSandboxLevel) -> bool { + windows_sandbox_level != WindowsSandboxLevel::Mxc || codex_sandboxing::windows_mxc_available() +} + #[cfg(target_os = "macos")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn codex_home_symlink_opt_out_respects_host_config_and_scope() -> Result<()> { @@ -914,13 +920,20 @@ async fn collect_process_output_from_reads( async fn collect_process_output_from_events( session: Arc, +) -> Result<(String, String, Option, bool)> { + collect_process_output_from_events_with_timeout(session, Duration::from_secs(2)).await +} + +async fn collect_process_output_from_events_with_timeout( + session: Arc, + event_timeout: Duration, ) -> Result<(String, String, Option, bool)> { let mut events = session.subscribe_events(); let mut stdout = String::new(); let mut stderr = String::new(); let mut exit_code = None; loop { - match timeout(Duration::from_secs(2), events.recv()).await?? { + match timeout(event_timeout, events.recv()).await?? { ExecProcessEvent::Output(chunk) => match chunk.stream { ExecOutputStream::Stdout | ExecOutputStream::Pty => { stdout.push_str(&String::from_utf8_lossy(&chunk.chunk.into_inner())); @@ -1281,7 +1294,14 @@ async fn assert_exec_process_write_then_read_without_tty(use_remote: bool) -> Re Ok(()) } -async fn assert_remote_windows_sandbox_process_write() -> Result<()> { +async fn assert_remote_windows_sandbox_process_write( + windows_sandbox_level: WindowsSandboxLevel, + expected_sandbox_type: codex_sandboxing::SandboxType, +) -> Result<()> { + if !selected_windows_sandbox_available(windows_sandbox_level) { + eprintln!("skipping MXC enforcement test: native MXC is unavailable on this host"); + return Ok(()); + } let context = create_process_context(/*use_remote*/ true).await?; let workspace = TempDir::new()?; let blocked_file = workspace.path().join("blocked.txt"); @@ -1290,7 +1310,7 @@ async fn assert_remote_windows_sandbox_process_write() -> Result<()> { SandboxPolicy::new_read_only_policy(), cwd.clone(), )?; - sandbox.windows_sandbox_level = WindowsSandboxLevel::RestrictedToken; + sandbox.windows_sandbox_level = windows_sandbox_level; let session = match context .backend @@ -1325,6 +1345,7 @@ async fn assert_remote_windows_sandbox_process_write() -> Result<()> { Ok(session) => session, Err(err) => return Err(err.into()), }; + assert_eq!(session.sandbox_type, Some(expected_sandbox_type)); let write_response = session.process.write(b"hello\n".to_vec()).await?; assert_eq!(write_response.status, WriteStatus::Accepted); @@ -1743,11 +1764,24 @@ async fn exec_process_write_then_read_without_tty(use_remote: bool) -> Result<() assert_exec_process_write_then_read_without_tty(use_remote).await } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[test_case( + WindowsSandboxLevel::RestrictedToken, + codex_sandboxing::SandboxType::WindowsRestrictedToken; + "restricted_token" +)] +#[test_case( + WindowsSandboxLevel::Mxc, + codex_sandboxing::SandboxType::WindowsMxc; + "mxc" +)] #[cfg_attr(not(windows), ignore = "Windows-only exec-server sandbox process test")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[serial_test::serial(remote_exec_server)] -async fn remote_windows_sandbox_process_accepts_process_write() -> Result<()> { - assert_remote_windows_sandbox_process_write().await +async fn remote_windows_sandbox_process_accepts_process_write( + windows_sandbox_level: WindowsSandboxLevel, + expected_sandbox_type: codex_sandboxing::SandboxType, +) -> Result<()> { + assert_remote_windows_sandbox_process_write(windows_sandbox_level, expected_sandbox_type).await } #[test_case(false ; "local")] diff --git a/codex-rs/exec-server/tests/exec_process/windows_sandbox.rs b/codex-rs/exec-server/tests/exec_process/windows_sandbox.rs new file mode 100644 index 0000000000..1f2868112f --- /dev/null +++ b/codex-rs/exec-server/tests/exec_process/windows_sandbox.rs @@ -0,0 +1,112 @@ +//! Shared Windows sandbox behavior over the real exec-server RPC connection. + +use super::*; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::FileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy; +use pretty_assertions::assert_eq; + +#[cfg_attr(not(windows), ignore = "requires a native Windows sandbox")] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +#[serial_test::serial(remote_exec_server)] +async fn mxc_tmpdir_uses_command_environment_over_rpc() -> Result<()> { + if !selected_windows_sandbox_available(WindowsSandboxLevel::Mxc) { + eprintln!("skipping MXC enforcement test: native MXC is unavailable on this host"); + return Ok(()); + } + let root = TempDir::new()?; + let command_temp = root.path().join("command temp"); + let server_temp = root.path().join("server temp"); + std::fs::create_dir(&command_temp)?; + std::fs::create_dir(&server_temp)?; + let outside = server_temp.join("outside.txt"); + std::fs::write(&outside, "original")?; + let server = common::exec_server::exec_server_with_env( + [ + ("TEMP", server_temp.as_os_str()), + ("TMP", server_temp.as_os_str()), + ], + &[], + ) + .await?; + let environment = Environment::create_for_tests(Some(server.websocket_url().to_owned()))?; + let cwd = PathUri::from_host_native_path(root.path())?; + let fs = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + FileSystemAccessMode::Read, + ), + FileSystemSandboxEntry::new( + FileSystemPath::Special { + value: FileSystemSpecialPath::Tmpdir, + }, + FileSystemAccessMode::Write, + ), + ]); + let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd( + PermissionProfile::from_runtime_permissions(&fs, NetworkSandboxPolicy::Restricted), + cwd.clone(), + ); + sandbox.windows_sandbox_level = WindowsSandboxLevel::Mxc; + let command_temp = command_temp.to_string_lossy().into_owned(); + let started = environment + .get_exec_backend() + .start(ExecParams { + process_id: ProcessId::from("windows-sandbox-temp"), + metadata: None, + argv: vec![ + r"C:\Windows\System32\cmd.exe".to_owned(), + "/D".to_owned(), + "/S".to_owned(), + "/C".to_owned(), + format!( + "echo allowed>\"%TEMP%\\allowed.txt\" & 2>\"%TEMP%\\denied.txt\" echo modified>\"{}\" & exit /b 0", + outside.display() + ), + ], + cwd, + env_policy: None, + shell_snapshot: None, + env: HashMap::from([ + ("SystemRoot".to_owned(), std::env::var("SystemRoot")?), + ("TEMP".to_owned(), command_temp.clone()), + ("TMP".to_owned(), command_temp), + ]), + tty: false, + pipe_stdin: false, + arg0: None, + sandbox: Some(sandbox), + enforce_managed_network: false, + managed_network: None, + network_proxy: None, + }) + .await?; + assert_eq!( + started.sandbox_type, + Some(codex_sandboxing::SandboxType::WindowsMxc) + ); + assert_eq!( + collect_process_output_from_events_with_timeout( + started.process, + Duration::from_secs(/*secs*/ 30), + ) + .await?, + (String::new(), String::new(), Some(0), true) + ); + assert_eq!( + ( + std::fs::read_to_string(root.path().join("command temp").join("allowed.txt"))? + .trim_end() + .to_owned(), + std::fs::read_to_string(outside)? + ), + ("allowed".to_owned(), "original".to_owned()) + ); + Ok(()) +} diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index 29dd4af60b..285cb73ae5 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -52,6 +52,7 @@ pub(super) async fn spawn_command_under_sandbox( sandbox_cwd, std::slice::from_ref(sandbox_cwd), &codex_linux_sandbox_exe, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, ) .map_err(|err| io::Error::other(err.to_string()))?; diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 56c0180124..c21f6c21cc 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -195,6 +195,7 @@ async fn run_cmd_result_with_permission_profile_for_cwd( &sandbox_cwd, std::slice::from_ref(&sandbox_cwd), &codex_linux_sandbox_exe, + /*codex_self_exe*/ &None, use_legacy_landlock, /*stdout_stream*/ None, ) @@ -891,6 +892,7 @@ async fn assert_network_blocked(cmd: &[&str]) { &sandbox_cwd, std::slice::from_ref(&sandbox_cwd), &codex_linux_sandbox_exe, + /*codex_self_exe*/ &None, /*use_legacy_landlock*/ false, /*stdout_stream*/ None, ) diff --git a/codex-rs/mxc-sandbox/README.md b/codex-rs/mxc-sandbox/README.md index e1c4380fcf..b864424e4c 100644 --- a/codex-rs/mxc-sandbox/README.md +++ b/codex-rs/mxc-sandbox/README.md @@ -7,9 +7,8 @@ dispatcher, edits host ACLs, creates sandbox users, runs setup, or requests elevation. The existing Codex Windows sandboxes remain separate backends. Windows executors record `codex.windows_mxc.available` once per process with an -`available=true|false` tag. This measures capability independently of selection. -The exec-server handshake also exposes `windows_mxc_available`; older or -unsupported Windows executors reject MXC requests before execution. +`available=true|false` tag. This measures runtime availability independently of +selection. Unsupported Windows executors reject MXC requests before execution. `is_available()` uses MXC's cached create/close probe, rather than an OS build number or the SDK's broad `platform_support()` result. The latter also reports diff --git a/codex-rs/mxc-sandbox/src/lib.rs b/codex-rs/mxc-sandbox/src/lib.rs index 4be53b323c..cb006b0e34 100644 --- a/codex-rs/mxc-sandbox/src/lib.rs +++ b/codex-rs/mxc-sandbox/src/lib.rs @@ -106,7 +106,7 @@ pub fn run_main() -> ! { match windows::run() { Ok(exit_code) => std::process::exit(exit_code), Err(error) => { - eprintln!("MXC sandbox: {error:#}"); + eprintln!("MXC launcher: {error:#}"); std::process::exit(1); } } diff --git a/codex-rs/mxc-sandbox/src/windows.rs b/codex-rs/mxc-sandbox/src/windows.rs index 871921289d..872792ecbc 100644 --- a/codex-rs/mxc-sandbox/src/windows.rs +++ b/codex-rs/mxc-sandbox/src/windows.rs @@ -43,11 +43,11 @@ pub(super) fn run() -> Result { Ok((key, value)) }) .collect::>>()?; - let command = crate::transport::decode(&mut env)?; - ensure!( - !env.is_empty(), - "MXC requires an explicit child environment" - ); + // Serde errors can quote arbitrary payload values; do not print them to stderr. + let command = crate::transport::decode(&mut env) + .map_err(|_| anyhow::anyhow!("invalid MXC launcher request"))?; + // The transport itself proves this is an explicit child environment; it + // may intentionally contain no variables once launcher state is removed. let mask = unsafe { GetLogicalDrives() }; ensure!( mask != 0, diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index b5ac448ebc..900950cb8a 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -299,6 +299,7 @@ pub enum WindowsSandboxLevel { Disabled, RestrictedToken, Elevated, + Mxc, } /// Controls whether a Windows sandbox launch reconciles persistent proxy settings or preserves diff --git a/codex-rs/sandboxing/src/lib.rs b/codex-rs/sandboxing/src/lib.rs index f85396dc78..3a1c883ce4 100644 --- a/codex-rs/sandboxing/src/lib.rs +++ b/codex-rs/sandboxing/src/lib.rs @@ -18,6 +18,7 @@ pub use bwrap::find_system_bwrap_in_path; #[cfg(target_os = "linux")] pub use bwrap::system_bwrap_warning; pub use codex_mxc_sandbox::CODEX_WINDOWS_MXC_ARG1; +pub use codex_mxc_sandbox::is_available as windows_mxc_available; pub use codex_mxc_sandbox::run_main as run_windows_mxc_main; pub use codex_windows_sandbox::WindowsSandboxProxySettingsMode; pub use denial::is_likely_executor_managed_sandbox_denied; @@ -73,6 +74,9 @@ impl From for CodexErr { SandboxTransformError::EnvironmentNetworkProxy(message) => { CodexErr::UnsupportedOperation(message) } + SandboxTransformError::WindowsMxcPreparation(message) => { + CodexErr::UnsupportedOperation(message) + } #[cfg(target_os = "macos")] SandboxTransformError::SeatbeltPreparation(message) => { CodexErr::UnsupportedOperation(message) diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 2b16db917e..9d0beadc7c 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -44,6 +44,7 @@ pub enum SandboxType { MacosSeatbelt, LinuxSeccomp, WindowsRestrictedToken, + WindowsMxc, } impl SandboxType { @@ -53,6 +54,7 @@ impl SandboxType { SandboxType::MacosSeatbelt => "seatbelt", SandboxType::LinuxSeccomp => "seccomp", SandboxType::WindowsRestrictedToken => "windows_sandbox", + SandboxType::WindowsMxc => "windows_mxc", } } } @@ -145,7 +147,7 @@ pub struct SandboxTransformRequest<'a> { // to make shared ownership explicit across runtime/sandbox plumbing. pub network: Option<&'a NetworkProxy>, pub sandbox_policy_cwd: &'a PathUri, - pub codex_linux_sandbox_exe: Option<&'a Path>, + pub sandbox_exe: Option<&'a Path>, // TODO(anp): Reconcile these backend inputs with the supplied sandbox context // (TurnEnvironment::sandbox_context for turns) so selection shares its authority. pub use_legacy_landlock: bool, @@ -215,6 +217,7 @@ pub enum SandboxTransformError { source: io::Error, }, MissingLinuxSandboxExecutable, + WindowsMxcPreparation(String), EnvironmentNetworkProxy(String), #[cfg(target_os = "macos")] SeatbeltPreparation(String), @@ -242,6 +245,9 @@ impl std::fmt::Display for SandboxTransformError { Self::MissingLinuxSandboxExecutable => { write!(f, "missing codex-linux-sandbox executable path") } + Self::WindowsMxcPreparation(err) => { + write!(f, "failed to prepare MXC sandbox: {err}") + } Self::EnvironmentNetworkProxy(err) => { write!(f, "failed to prepare environment network proxy: {err}") } @@ -267,6 +273,7 @@ impl std::error::Error for SandboxTransformError { Self::InvalidCommandCwd { source, .. } | Self::InvalidSandboxPolicyCwd { source, .. } => Some(source), Self::MissingLinuxSandboxExecutable => None, + Self::WindowsMxcPreparation(_) => None, Self::EnvironmentNetworkProxy(_) => None, #[cfg(target_os = "macos")] Self::SeatbeltPreparation(_) => None, @@ -324,12 +331,14 @@ impl SandboxManager { #[cfg(windows)] crate::windows_mxc::record_availability_once(); - if self.should_sandbox(permission_profile, pref, has_managed_network_requirements) { - get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) - .unwrap_or(SandboxType::None) - } else { - SandboxType::None + if !self.should_sandbox(permission_profile, pref, has_managed_network_requirements) { + return SandboxType::None; } + if cfg!(windows) && windows_sandbox_level == WindowsSandboxLevel::Mxc { + return SandboxType::WindowsMxc; + } + get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) + .unwrap_or(SandboxType::None) } /// Returns whether the request needs a sandbox, independently of whether @@ -367,7 +376,7 @@ impl SandboxManager { environment_id, network, sandbox_policy_cwd, - codex_linux_sandbox_exe, + sandbox_exe, use_legacy_landlock, windows_sandbox_level, windows_sandbox_private_desktop, @@ -386,11 +395,62 @@ impl SandboxManager { managed_mitm_ca_trust_bundle_path.as_ref(), ); let mut argv = Vec::with_capacity(1 + command.args.len()); - argv.push(command.program); - argv.extend(command.args.into_iter().map(OsString::from)); + argv.push(os_string_to_command_component(command.program)); + argv.extend(command.args); let (argv, arg0_override, pending_sandboxed_request) = match sandbox { - SandboxType::None => (os_argv_to_strings(argv), None, None), + SandboxType::None => (argv, None, None), + SandboxType::WindowsMxc => { + if windows_sandbox_private_desktop { + return Err(SandboxTransformError::WindowsMxcPreparation( + "private desktop isolation is not supported by MXC".to_string(), + )); + } + if !codex_mxc_sandbox::is_available() { + return Err(SandboxTransformError::WindowsMxcPreparation( + "native MXC is unavailable on this executor".to_string(), + )); + } + if enforce_managed_network && command.managed_network.is_none() { + let network = network.ok_or_else(|| { + SandboxTransformError::WindowsMxcPreparation( + "managed networking requires an executor-local proxy".to_string(), + ) + })?; + let prepared = network + .prepare_for_optional_environment( + std::mem::take(&mut command.env), + environment_id, + ) + .map_err(|err| { + SandboxTransformError::EnvironmentNetworkProxy(err.to_string()) + })?; + command.env = prepared.env; + command.managed_network = Some(prepared.sandbox_context); + } + let managed_network = command.managed_network.filter(|_| enforce_managed_network); + let pending = pending_sandboxed_request?; + let exe = sandbox_exe.ok_or_else(|| { + SandboxTransformError::WindowsMxcPreparation( + "missing Codex executable path".to_string(), + ) + })?; + let mut full_command = + vec![os_string_to_command_component(exe.as_os_str().to_owned())]; + full_command.extend( + codex_mxc_sandbox::create_command_args( + codex_mxc_sandbox::CreateMxcCommandArgsParams { + command: argv, + permission_profile: &pending.effective_permission_profile, + sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(), + managed_network: managed_network.as_ref(), + env: &mut command.env, + }, + ) + .map_err(|err| SandboxTransformError::WindowsMxcPreparation(err.to_string()))?, + ); + (full_command, None, Some(pending)) + } #[cfg(target_os = "macos")] SandboxType::MacosSeatbelt => { use crate::seatbelt::CreateSeatbeltCommandArgsParams; @@ -404,7 +464,7 @@ impl SandboxManager { .to_runtime_permissions(); let mut args = create_seatbelt_command_args_with_profile( CreateSeatbeltCommandArgsParams { - command: os_argv_to_strings(argv), + command: argv, file_system_sandbox_policy: &file_system_sandbox_policy, network_sandbox_policy, sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(), @@ -434,8 +494,8 @@ impl SandboxManager { SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable), SandboxType::LinuxSeccomp => { let pending = pending_sandboxed_request?; - let exe = codex_linux_sandbox_exe - .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; + let exe = + sandbox_exe.ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?; let allow_proxy_network = allow_network_for_proxy(enforce_managed_network); #[cfg(target_os = "linux")] ensure_linux_bubblewrap_is_supported( @@ -447,7 +507,7 @@ impl SandboxManager { is_wsl1(), )?; let mut args = create_linux_sandbox_command_args_for_permission_profile( - os_argv_to_strings(argv), + argv, pending.native_command_cwd.as_path(), &pending.effective_permission_profile, pending.native_sandbox_policy_cwd.as_path(), @@ -487,14 +547,10 @@ impl SandboxManager { )], ); } - (os_argv_to_strings(argv), None, Some(pending)) + (argv, None, Some(pending)) } #[cfg(not(target_os = "windows"))] - SandboxType::WindowsRestrictedToken => ( - os_argv_to_strings(argv), - None, - Some(pending_sandboxed_request?), - ), + SandboxType::WindowsRestrictedToken => (argv, None, Some(pending_sandboxed_request?)), }; // Unsandboxed exec-server requests may have foreign cwd values that cannot be prepared @@ -525,16 +581,12 @@ impl SandboxManager { request: SandboxDirectSpawnTransformRequest<'_>, ) -> Result { #[cfg(target_os = "windows")] - { + if request.transform.sandbox == SandboxType::WindowsRestrictedToken { let codex_home = codex_utils_home_dir::find_codex_home() .map_err(|err| SandboxTransformError::WindowsSandboxPreparation(err.to_string()))?; - self.transform_for_direct_spawn_with_codex_home(request, codex_home.as_path()) - } - - #[cfg(not(target_os = "windows"))] - { - self.transform(request.transform) + return self.transform_for_direct_spawn_with_codex_home(request, codex_home.as_path()); } + self.transform(request.transform) } #[cfg(target_os = "windows")] @@ -751,12 +803,6 @@ fn ensure_linux_bubblewrap_is_supported( Ok(()) } -fn os_argv_to_strings(argv: Vec) -> Vec { - argv.into_iter() - .map(os_string_to_command_component) - .collect() -} - fn os_string_to_command_component(value: OsString) -> String { value .into_string() diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index 8954f11d48..ba01f9a280 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -25,6 +25,48 @@ use pretty_assertions::assert_eq; use std::collections::HashMap; use tempfile::TempDir; +#[test] +fn mxc_selection_preserves_unsandboxed_requests_and_other_platforms() { + let manager = SandboxManager::new(); + let platform_sandbox = if cfg!(windows) { + SandboxType::WindowsMxc + } else { + get_platform_sandbox(/*windows_sandbox_enabled*/ false).unwrap_or(SandboxType::None) + }; + for (permissions, preference, expected) in [ + ( + PermissionProfile::read_only(), + SandboxablePreference::Auto, + platform_sandbox, + ), + ( + PermissionProfile::Disabled, + SandboxablePreference::Require, + platform_sandbox, + ), + ( + PermissionProfile::read_only(), + SandboxablePreference::Forbid, + SandboxType::None, + ), + ( + PermissionProfile::Disabled, + SandboxablePreference::Auto, + SandboxType::None, + ), + ] { + assert_eq!( + manager.select_initial( + &permissions, + preference, + WindowsSandboxLevel::Mxc, + /*has_managed_network_requirements*/ false, + ), + expected, + ); + } +} + #[test] fn danger_full_access_defaults_to_no_sandbox_without_network_requirements() { let manager = SandboxManager::new(); @@ -103,7 +145,7 @@ fn unsandboxed_transform_preserves_foreign_cwd_and_unrestricted_file_system_poli environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -160,7 +202,7 @@ fn symlinked_workspace_reports_seatbelt_preparation_error() { environment_id: None, network: None, sandbox_policy_cwd: &workspace_uri, - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -215,7 +257,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -286,7 +328,7 @@ fn transform_additional_permissions_preserves_denied_entries() { environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -388,7 +430,7 @@ fn transform_linux_seccomp_request( environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, - codex_linux_sandbox_exe: Some(codex_linux_sandbox_exe), + sandbox_exe: Some(codex_linux_sandbox_exe), use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, windows_sandbox_private_desktop: false, @@ -590,7 +632,7 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() { environment_id: None, network: None, sandbox_policy_cwd: &cwd_uri, - codex_linux_sandbox_exe: None, + sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Elevated, windows_sandbox_private_desktop: false, diff --git a/codex-rs/sandboxing/src/violation.rs b/codex-rs/sandboxing/src/violation.rs index 4023fb4494..0d00a0f605 100644 --- a/codex-rs/sandboxing/src/violation.rs +++ b/codex-rs/sandboxing/src/violation.rs @@ -50,6 +50,7 @@ pub enum SandboxViolationBackend { ManagedNetworkProxy, Seatbelt, WindowsSandbox, + WindowsMxc, } impl SandboxViolationBackend { @@ -59,6 +60,7 @@ impl SandboxViolationBackend { Self::ManagedNetworkProxy => "managed_network_proxy", Self::Seatbelt => "seatbelt", Self::WindowsSandbox => "windows_sandbox", + Self::WindowsMxc => "windows_mxc", } } } @@ -143,6 +145,7 @@ fn classify_filesystem_sandbox_violation( SandboxType::MacosSeatbelt => SandboxViolationBackend::Seatbelt, SandboxType::LinuxSeccomp => SandboxViolationBackend::LinuxSandbox, SandboxType::WindowsRestrictedToken => SandboxViolationBackend::WindowsSandbox, + SandboxType::WindowsMxc => SandboxViolationBackend::WindowsMxc, }; if let Some((reason, output)) = filesystem_reason_from_output(exec_output) {