diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index d35e18ca0..5b14ccb4f 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -1695,6 +1695,10 @@ impl CodexMessageProcessor { .map(codex_core::config::StartedNetworkProxy::proxy), sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level, + windows_sandbox_private_desktop: self + .config + .permissions + .windows_sandbox_private_desktop, justification: None, arg0: None, }; diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index 540298b04..6d8e74225 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -733,6 +733,7 @@ mod tests { expiration: ExecExpiration::DefaultTimeout, sandbox: SandboxType::WindowsRestrictedToken, windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault, sandbox_policy: sandbox_policy.clone(), file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), @@ -844,6 +845,7 @@ mod tests { expiration: ExecExpiration::Cancellation(CancellationToken::new()), sandbox: SandboxType::None, windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, sandbox_permissions: codex_core::sandboxing::SandboxPermissions::UseDefault, sandbox_policy: sandbox_policy.clone(), file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), diff --git a/codex-rs/cli/src/debug_sandbox.rs b/codex-rs/cli/src/debug_sandbox.rs index 999a92db6..cc1f1e051 100644 --- a/codex-rs/cli/src/debug_sandbox.rs +++ b/codex-rs/cli/src/debug_sandbox.rs @@ -165,6 +165,7 @@ async fn run_command_under_sandbox( &cwd_clone, env_map, None, + config.permissions.windows_sandbox_private_desktop, ) } else { run_windows_sandbox_capture( @@ -175,6 +176,7 @@ async fn run_command_under_sandbox( &cwd_clone, env_map, None, + config.permissions.windows_sandbox_private_desktop, ) } }) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 40949dfa6..b2164f723 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1699,6 +1699,10 @@ "properties": { "sandbox": { "$ref": "#/definitions/WindowsSandboxModeToml" + }, + "sandbox_private_desktop": { + "description": "Defaults to `true`. Set to `false` to launch the final sandboxed child process on `Winsta0\\\\Default` instead of a private desktop.", + "type": "boolean" } }, "type": "object" diff --git a/codex-rs/core/src/codex_tests.rs b/codex-rs/core/src/codex_tests.rs index 0d3c69bb7..76ee7bcb5 100644 --- a/codex-rs/core/src/codex_tests.rs +++ b/codex-rs/core/src/codex_tests.rs @@ -4214,6 +4214,10 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { network: None, sandbox_permissions, windows_sandbox_level: turn_context.windows_sandbox_level, + windows_sandbox_private_desktop: turn_context + .config + .permissions + .windows_sandbox_private_desktop, justification: Some("test".to_string()), arg0: None, }; @@ -4226,6 +4230,10 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() { env: HashMap::new(), network: None, windows_sandbox_level: turn_context.windows_sandbox_level, + windows_sandbox_private_desktop: turn_context + .config + .permissions + .windows_sandbox_private_desktop, justification: params.justification.clone(), arg0: None, }; diff --git a/codex-rs/core/src/codex_tests_guardian.rs b/codex-rs/core/src/codex_tests_guardian.rs index b0a87e186..f63c43725 100644 --- a/codex-rs/core/src/codex_tests_guardian.rs +++ b/codex-rs/core/src/codex_tests_guardian.rs @@ -125,6 +125,10 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid network: None, sandbox_permissions: SandboxPermissions::WithAdditionalPermissions, windows_sandbox_level: turn_context.windows_sandbox_level, + windows_sandbox_private_desktop: turn_context + .config + .permissions + .windows_sandbox_private_desktop, justification: Some("test".to_string()), arg0: None, }; diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 0fe4b5815..c95d45a47 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -4082,6 +4082,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> { allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), windows_sandbox_mode: None, + windows_sandbox_private_desktop: true, macos_seatbelt_profile_extensions: None, }, enforce_residency: Constrained::allow_any(None), @@ -4219,6 +4220,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> { allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), windows_sandbox_mode: None, + windows_sandbox_private_desktop: true, macos_seatbelt_profile_extensions: None, }, enforce_residency: Constrained::allow_any(None), @@ -4354,6 +4356,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> { allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), windows_sandbox_mode: None, + windows_sandbox_private_desktop: true, macos_seatbelt_profile_extensions: None, }, enforce_residency: Constrained::allow_any(None), @@ -4475,6 +4478,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> { allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), windows_sandbox_mode: None, + windows_sandbox_private_desktop: true, macos_seatbelt_profile_extensions: None, }, enforce_residency: Constrained::allow_any(None), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index d239c91bb..d73f7cdc6 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -58,6 +58,7 @@ use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS; use crate::unified_exec::MIN_EMPTY_YIELD_TIME_MS; use crate::windows_sandbox::WindowsSandboxLevelExt; use crate::windows_sandbox::resolve_windows_sandbox_mode; +use crate::windows_sandbox::resolve_windows_sandbox_private_desktop; use codex_app_server_protocol::Tools; use codex_app_server_protocol::UserSavedConfig; use codex_protocol::config_types::AltScreenMode; @@ -189,6 +190,8 @@ pub struct Permissions { /// Effective Windows sandbox mode derived from `[windows].sandbox` or /// legacy feature keys. pub windows_sandbox_mode: Option, + /// Whether the final Windows sandboxed child should run on a private desktop. + pub windows_sandbox_private_desktop: bool, /// Optional macOS seatbelt extension profile used to extend default /// seatbelt permissions when running under seatbelt. pub macos_seatbelt_profile_extensions: Option, @@ -1934,6 +1937,8 @@ impl Config { let configured_features = Features::from_config(&cfg, &config_profile, feature_overrides); let features = ManagedFeatures::from_configured(configured_features, feature_requirements)?; let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile); + let windows_sandbox_private_desktop = + resolve_windows_sandbox_private_desktop(&cfg, &config_profile); let resolved_cwd = normalize_for_native_workdir({ use std::env; @@ -2394,6 +2399,7 @@ impl Config { allow_login_shell, shell_environment_policy, windows_sandbox_mode, + windows_sandbox_private_desktop, macos_seatbelt_profile_extensions: None, }, enforce_residency: enforce_residency.value, diff --git a/codex-rs/core/src/config/types.rs b/codex-rs/core/src/config/types.rs index 68ef2a630..af9f496dd 100644 --- a/codex-rs/core/src/config/types.rs +++ b/codex-rs/core/src/config/types.rs @@ -41,6 +41,9 @@ pub enum WindowsSandboxModeToml { #[schemars(deny_unknown_fields)] pub struct WindowsToml { pub sandbox: Option, + /// Defaults to `true`. Set to `false` to launch the final sandboxed child + /// process on `Winsta0\\Default` instead of a private desktop. + pub sandbox_private_desktop: Option, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 867b93ab5..8bb16b2a5 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -81,6 +81,7 @@ pub struct ExecParams { pub network: Option, pub sandbox_permissions: SandboxPermissions, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, pub justification: Option, pub arg0: Option, } @@ -231,6 +232,7 @@ pub fn build_exec_request( network, sandbox_permissions, windows_sandbox_level, + windows_sandbox_private_desktop, justification, arg0: _, } = params; @@ -271,6 +273,7 @@ pub fn build_exec_request( codex_linux_sandbox_exe: codex_linux_sandbox_exe.as_ref(), use_legacy_landlock, windows_sandbox_level, + windows_sandbox_private_desktop, }) .map_err(CodexErr::from)?; Ok(exec_req) @@ -290,6 +293,7 @@ pub(crate) async fn execute_exec_request( expiration, sandbox, windows_sandbox_level, + windows_sandbox_private_desktop, sandbox_permissions, sandbox_policy: _sandbox_policy_from_env, file_system_sandbox_policy, @@ -307,6 +311,7 @@ pub(crate) async fn execute_exec_request( network: network.clone(), sandbox_permissions, windows_sandbox_level, + windows_sandbox_private_desktop, justification, arg0, }; @@ -409,6 +414,7 @@ async fn exec_windows_sandbox( network, expiration, windows_sandbox_level, + windows_sandbox_private_desktop, .. } = params; if let Some(network) = network.as_ref() { @@ -443,6 +449,7 @@ async fn exec_windows_sandbox( &cwd, env, timeout_ms, + windows_sandbox_private_desktop, ) } else { run_windows_sandbox_capture( @@ -453,6 +460,7 @@ async fn exec_windows_sandbox( &cwd, env, timeout_ms, + windows_sandbox_private_desktop, ) } }) diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 550b41af7..10ba5734f 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -319,6 +319,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()> network: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, justification: None, arg0: None, }; @@ -375,6 +376,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> { network: None, sandbox_permissions: SandboxPermissions::UseDefault, windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, justification: None, arg0: None, }; diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index fe4918a26..b6b9fd2f5 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -69,6 +69,7 @@ pub struct ExecRequest { pub expiration: ExecExpiration, pub sandbox: SandboxType, pub windows_sandbox_level: WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, pub sandbox_permissions: SandboxPermissions, pub sandbox_policy: SandboxPolicy, pub file_system_sandbox_policy: FileSystemSandboxPolicy, @@ -96,6 +97,7 @@ pub(crate) struct SandboxTransformRequest<'a> { pub codex_linux_sandbox_exe: Option<&'a PathBuf>, pub use_legacy_landlock: bool, pub windows_sandbox_level: WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, } pub enum SandboxPreference { @@ -593,6 +595,7 @@ impl SandboxManager { codex_linux_sandbox_exe, use_legacy_landlock, windows_sandbox_level, + windows_sandbox_private_desktop, } = request; #[cfg(not(target_os = "macos"))] let macos_seatbelt_profile_extensions = None; @@ -705,6 +708,7 @@ impl SandboxManager { expiration: spec.expiration, sandbox, windows_sandbox_level, + windows_sandbox_private_desktop, sandbox_permissions: spec.sandbox_permissions, sandbox_policy: effective_policy, file_system_sandbox_policy: effective_file_system_policy, diff --git a/codex-rs/core/src/sandboxing/mod_tests.rs b/codex-rs/core/src/sandboxing/mod_tests.rs index c20c2ef41..d252443fb 100644 --- a/codex-rs/core/src/sandboxing/mod_tests.rs +++ b/codex-rs/core/src/sandboxing/mod_tests.rs @@ -169,6 +169,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network() codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, }) .expect("transform"); @@ -502,6 +503,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() { codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, }) .expect("transform"); @@ -574,6 +576,7 @@ fn transform_additional_permissions_preserves_denied_entries() { codex_linux_sandbox_exe: None, use_legacy_landlock: false, windows_sandbox_level: WindowsSandboxLevel::Disabled, + windows_sandbox_private_desktop: false, }) .expect("transform"); diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 9357f154f..64fe70491 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -167,6 +167,10 @@ pub(crate) async fn execute_user_shell_command( expiration: USER_SHELL_TIMEOUT_MS.into(), sandbox: SandboxType::None, windows_sandbox_level: turn_context.windows_sandbox_level, + windows_sandbox_private_desktop: turn_context + .config + .permissions + .windows_sandbox_private_desktop, sandbox_permissions: SandboxPermissions::UseDefault, sandbox_policy: sandbox_policy.clone(), file_system_sandbox_policy: FileSystemSandboxPolicy::from(&sandbox_policy), diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index d8a564b17..a3014a8ed 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -74,6 +74,10 @@ impl ShellHandler { network: turn_context.network.clone(), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, + windows_sandbox_private_desktop: turn_context + .config + .permissions + .windows_sandbox_private_desktop, justification: params.justification.clone(), arg0: None, } @@ -124,6 +128,10 @@ impl ShellCommandHandler { network: turn_context.network.clone(), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), windows_sandbox_level: turn_context.windows_sandbox_level, + windows_sandbox_private_desktop: turn_context + .config + .permissions + .windows_sandbox_private_desktop, justification: params.justification.clone(), arg0: None, }) diff --git a/codex-rs/core/src/tools/js_repl/mod.rs b/codex-rs/core/src/tools/js_repl/mod.rs index 2195e81a4..1ef5be5ff 100644 --- a/codex-rs/core/src/tools/js_repl/mod.rs +++ b/codex-rs/core/src/tools/js_repl/mod.rs @@ -1067,6 +1067,10 @@ impl JsReplManager { codex_linux_sandbox_exe: turn.codex_linux_sandbox_exe.as_ref(), use_legacy_landlock: turn.features.use_legacy_landlock(), windows_sandbox_level: turn.windows_sandbox_level, + windows_sandbox_private_desktop: turn + .config + .permissions + .windows_sandbox_private_desktop, }) .map_err(|err| format!("failed to configure sandbox for js_repl: {err}"))?; diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index d773bd913..db3d05ce7 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -197,6 +197,10 @@ impl ToolOrchestrator { codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(), use_legacy_landlock, windows_sandbox_level: turn_ctx.windows_sandbox_level, + windows_sandbox_private_desktop: turn_ctx + .config + .permissions + .windows_sandbox_private_desktop, }; let (first_result, first_deferred_network_approval) = Self::run_attempt( @@ -319,6 +323,10 @@ impl ToolOrchestrator { codex_linux_sandbox_exe: None, use_legacy_landlock, windows_sandbox_level: turn_ctx.windows_sandbox_level, + windows_sandbox_private_desktop: turn_ctx + .config + .permissions + .windows_sandbox_private_desktop, }; // Second attempt. 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 987fdc2dc..6303f9c49 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -126,6 +126,7 @@ pub(super) async fn try_run_zsh_fork( expiration: _sandbox_expiration, sandbox, windows_sandbox_level, + windows_sandbox_private_desktop: _windows_sandbox_private_desktop, sandbox_permissions, sandbox_policy, file_system_sandbox_policy, @@ -924,6 +925,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor { expiration: ExecExpiration::Cancellation(cancel_rx), sandbox: self.sandbox, windows_sandbox_level: self.windows_sandbox_level, + windows_sandbox_private_desktop: false, sandbox_permissions: self.sandbox_permissions, sandbox_policy: self.sandbox_policy.clone(), file_system_sandbox_policy: self.file_system_sandbox_policy.clone(), @@ -1080,6 +1082,7 @@ impl CoreShellCommandExecutor { codex_linux_sandbox_exe: self.codex_linux_sandbox_exe.as_ref(), use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, + windows_sandbox_private_desktop: false, })?; if let Some(network) = exec_request.network.as_ref() { network.apply_to_env(&mut exec_request.env); diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 94d419798..37c1b53a1 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -730,6 +730,7 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions() allow_login_shell: true, shell_environment_policy: ShellEnvironmentPolicy::default(), windows_sandbox_mode: None, + windows_sandbox_private_desktop: false, macos_seatbelt_profile_extensions: Some(MacOsSeatbeltProfileExtensions { macos_preferences: MacOsPreferencesPermission::ReadWrite, ..Default::default() diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 29a950598..e8ff5a1b8 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -333,6 +333,7 @@ pub(crate) struct SandboxAttempt<'a> { pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, pub use_legacy_landlock: bool, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, + pub windows_sandbox_private_desktop: bool, } impl<'a> SandboxAttempt<'a> { @@ -356,6 +357,7 @@ impl<'a> SandboxAttempt<'a> { codex_linux_sandbox_exe: self.codex_linux_sandbox_exe, 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/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index 25932a962..8cc332cb3 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -75,6 +75,19 @@ pub fn resolve_windows_sandbox_mode( .or_else(|| legacy_windows_sandbox_mode(cfg.features.as_ref())) } +pub fn resolve_windows_sandbox_private_desktop(cfg: &ConfigToml, profile: &ConfigProfile) -> bool { + profile + .windows + .as_ref() + .and_then(|windows| windows.sandbox_private_desktop) + .or_else(|| { + cfg.windows + .as_ref() + .and_then(|windows| windows.sandbox_private_desktop) + }) + .unwrap_or(true) +} + fn legacy_windows_sandbox_keys_present(features: Option<&FeaturesToml>) -> bool { let Some(entries) = features.map(|features| &features.entries) else { return false; diff --git a/codex-rs/core/src/windows_sandbox_tests.rs b/codex-rs/core/src/windows_sandbox_tests.rs index 6bcd493ad..a7506e7de 100644 --- a/codex-rs/core/src/windows_sandbox_tests.rs +++ b/codex-rs/core/src/windows_sandbox_tests.rs @@ -77,12 +77,14 @@ fn resolve_windows_sandbox_mode_prefers_profile_windows() { let cfg = ConfigToml { windows: Some(WindowsToml { sandbox: Some(WindowsSandboxModeToml::Unelevated), + ..Default::default() }), ..Default::default() }; let profile = ConfigProfile { windows: Some(WindowsToml { sandbox: Some(WindowsSandboxModeToml::Elevated), + ..Default::default() }), ..Default::default() }; @@ -130,3 +132,47 @@ fn resolve_windows_sandbox_mode_profile_legacy_false_blocks_top_level_legacy_tru assert_eq!(resolve_windows_sandbox_mode(&cfg, &profile), None); } + +#[test] +fn resolve_windows_sandbox_private_desktop_prefers_profile_windows() { + let cfg = ConfigToml { + windows: Some(WindowsToml { + sandbox: Some(WindowsSandboxModeToml::Unelevated), + sandbox_private_desktop: Some(false), + }), + ..Default::default() + }; + let profile = ConfigProfile { + windows: Some(WindowsToml { + sandbox: Some(WindowsSandboxModeToml::Elevated), + sandbox_private_desktop: Some(true), + }), + ..Default::default() + }; + + assert!(resolve_windows_sandbox_private_desktop(&cfg, &profile)); +} + +#[test] +fn resolve_windows_sandbox_private_desktop_defaults_to_true() { + assert!(resolve_windows_sandbox_private_desktop( + &ConfigToml::default(), + &ConfigProfile::default() + )); +} + +#[test] +fn resolve_windows_sandbox_private_desktop_respects_explicit_cfg_value() { + let cfg = ConfigToml { + windows: Some(WindowsToml { + sandbox_private_desktop: Some(false), + ..Default::default() + }), + ..Default::default() + }; + + assert!(!resolve_windows_sandbox_private_desktop( + &cfg, + &ConfigProfile::default() + )); +} diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index e00ec963d..fc1619b8b 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -41,6 +41,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result, timeout_ms: Option, + use_private_desktop: bool, stdin_pipe: String, stdout_pipe: String, stderr_pipe: String, @@ -103,6 +105,8 @@ pub fn main() -> Result<()> { let req: RunnerRequest = serde_json::from_str(&input).context("parse runner request json")?; let log_dir = Some(req.codex_home.as_path()); hide_current_user_profile_dir(req.codex_home.as_path()); + // Suppress Windows error UI from sandboxed child crashes so callers only observe exit codes. + let _ = unsafe { SetErrorMode(0x0001 | 0x0002) }; // SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX log_note( &format!( "runner start cwd={} cmd={:?} real_codex_home={}", @@ -233,9 +237,10 @@ pub fn main() -> Result<()> { &req.env_map, Some(&req.codex_home), stdio, + req.use_private_desktop, ) }; - let (proc_info, _si) = match spawn_result { + let created = match spawn_result { Ok(v) => v, Err(e) => { log_note(&format!("runner: spawn failed: {e:?}"), log_dir); @@ -248,6 +253,8 @@ pub fn main() -> Result<()> { return Err(e); } }; + let proc_info = created.process_info; + let _desktop = created; // Optional job kill on close. let h_job = unsafe { create_job_kill_on_close().ok() }; diff --git a/codex-rs/windows-sandbox-rs/src/desktop.rs b/codex-rs/windows-sandbox-rs/src/desktop.rs new file mode 100644 index 000000000..822e43b07 --- /dev/null +++ b/codex-rs/windows-sandbox-rs/src/desktop.rs @@ -0,0 +1,196 @@ +use crate::logging; +use crate::token::get_current_token_for_restriction; +use crate::token::get_logon_sid_bytes; +use crate::winutil::format_last_error; +use crate::winutil::to_wide; +use anyhow::Result; +use rand::Rng; +use rand::SeedableRng; +use rand::rngs::SmallRng; +use std::path::Path; +use std::ffi::c_void; +use std::ptr; +use windows_sys::Win32::Foundation::CloseHandle; +use windows_sys::Win32::Foundation::GetLastError; +use windows_sys::Win32::Foundation::HLOCAL; +use windows_sys::Win32::Foundation::ERROR_SUCCESS; +use windows_sys::Win32::Foundation::LocalFree; +use windows_sys::Win32::Security::Authorization::EXPLICIT_ACCESS_W; +use windows_sys::Win32::Security::Authorization::GRANT_ACCESS; +use windows_sys::Win32::Security::Authorization::SE_WINDOW_OBJECT; +use windows_sys::Win32::Security::Authorization::SetEntriesInAclW; +use windows_sys::Win32::Security::Authorization::SetSecurityInfo; +use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_SID; +use windows_sys::Win32::Security::Authorization::TRUSTEE_IS_UNKNOWN; +use windows_sys::Win32::Security::Authorization::TRUSTEE_W; +use windows_sys::Win32::Security::DACL_SECURITY_INFORMATION; +use windows_sys::Win32::System::StationsAndDesktops::CloseDesktop; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_CREATEMENU; +use windows_sys::Win32::System::StationsAndDesktops::CreateDesktopW; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_CREATEWINDOW; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_DELETE; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_ENUMERATE; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_HOOKCONTROL; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_JOURNALPLAYBACK; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_JOURNALRECORD; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_READOBJECTS; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_READ_CONTROL; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_SWITCHDESKTOP; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITE_DAC; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITE_OWNER; +use windows_sys::Win32::System::StationsAndDesktops::DESKTOP_WRITEOBJECTS; + +const DESKTOP_ALL_ACCESS: u32 = DESKTOP_READOBJECTS + | DESKTOP_CREATEWINDOW + | DESKTOP_CREATEMENU + | DESKTOP_HOOKCONTROL + | DESKTOP_JOURNALRECORD + | DESKTOP_JOURNALPLAYBACK + | DESKTOP_ENUMERATE + | DESKTOP_WRITEOBJECTS + | DESKTOP_SWITCHDESKTOP + | DESKTOP_DELETE + | DESKTOP_READ_CONTROL + | DESKTOP_WRITE_DAC + | DESKTOP_WRITE_OWNER; + +pub struct LaunchDesktop { + _private_desktop: Option, + startup_name: Vec, +} + +impl LaunchDesktop { + pub fn prepare(use_private_desktop: bool, logs_base_dir: Option<&Path>) -> Result { + if use_private_desktop { + let private_desktop = PrivateDesktop::create(logs_base_dir)?; + let startup_name = to_wide(format!("Winsta0\\{}", private_desktop.name)); + Ok(Self { + _private_desktop: Some(private_desktop), + startup_name, + }) + } else { + Ok(Self { + _private_desktop: None, + startup_name: to_wide("Winsta0\\Default"), + }) + } + } + + pub fn startup_info_desktop(&self) -> *mut u16 { + self.startup_name.as_ptr() as *mut u16 + } +} + +struct PrivateDesktop { + handle: isize, + name: String, +} + +impl PrivateDesktop { + fn create(logs_base_dir: Option<&Path>) -> Result { + let mut rng = SmallRng::from_entropy(); + let name = format!("CodexSandboxDesktop-{:x}", rng.gen::()); + let name_wide = to_wide(&name); + let handle = unsafe { + CreateDesktopW( + name_wide.as_ptr(), + ptr::null(), + ptr::null_mut(), + 0, + DESKTOP_ALL_ACCESS, + ptr::null_mut(), + ) + }; + if handle == 0 { + let err = unsafe { GetLastError() } as i32; + logging::debug_log( + &format!( + "CreateDesktopW failed for {name}: {} ({})", + err, + format_last_error(err), + ), + logs_base_dir, + ); + return Err(anyhow::anyhow!("CreateDesktopW failed: {err}")); + } + + unsafe { + if let Err(err) = grant_desktop_access(handle, logs_base_dir) { + let _ = CloseDesktop(handle); + return Err(err); + } + } + + Ok(Self { handle, name }) + } +} + +unsafe fn grant_desktop_access(handle: isize, logs_base_dir: Option<&Path>) -> Result<()> { + let token = get_current_token_for_restriction()?; + let mut logon_sid = get_logon_sid_bytes(token)?; + CloseHandle(token); + + let entries = [EXPLICIT_ACCESS_W { + grfAccessPermissions: DESKTOP_ALL_ACCESS, + grfAccessMode: GRANT_ACCESS, + grfInheritance: 0, + Trustee: TRUSTEE_W { + pMultipleTrustee: ptr::null_mut(), + MultipleTrusteeOperation: 0, + TrusteeForm: TRUSTEE_IS_SID, + TrusteeType: TRUSTEE_IS_UNKNOWN, + ptstrName: logon_sid.as_mut_ptr() as *mut c_void as *mut u16, + }, + }]; + + let mut updated_dacl = ptr::null_mut(); + let set_entries_code = SetEntriesInAclW( + entries.len() as u32, + entries.as_ptr(), + ptr::null_mut(), + &mut updated_dacl, + ); + if set_entries_code != ERROR_SUCCESS { + logging::debug_log( + &format!("SetEntriesInAclW failed for private desktop: {set_entries_code}"), + logs_base_dir, + ); + return Err(anyhow::anyhow!( + "SetEntriesInAclW failed for private desktop: {set_entries_code}" + )); + } + + let set_security_code = SetSecurityInfo( + handle, + SE_WINDOW_OBJECT, + DACL_SECURITY_INFORMATION, + ptr::null_mut(), + ptr::null_mut(), + updated_dacl, + ptr::null_mut(), + ); + if !updated_dacl.is_null() { + LocalFree(updated_dacl as HLOCAL); + } + if set_security_code != ERROR_SUCCESS { + logging::debug_log( + &format!("SetSecurityInfo failed for private desktop: {set_security_code}"), + logs_base_dir, + ); + return Err(anyhow::anyhow!( + "SetSecurityInfo failed for private desktop: {set_security_code}" + )); + } + + Ok(()) +} + +impl Drop for PrivateDesktop { + fn drop(&mut self) { + unsafe { + if self.handle != 0 { + let _ = CloseDesktop(self.handle); + } + } + } +} diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index b3a5e9a1a..59edec398 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -196,12 +196,14 @@ mod windows_impl { cwd: PathBuf, env_map: HashMap, timeout_ms: Option, + use_private_desktop: bool, stdin_pipe: String, stdout_pipe: String, stderr_pipe: String, } /// Launches the command runner under the sandbox user and captures its output. + #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( policy_json_or_preset: &str, sandbox_policy_cwd: &Path, @@ -210,6 +212,7 @@ mod windows_impl { cwd: &Path, mut env_map: HashMap, timeout_ms: Option, + use_private_desktop: bool, ) -> Result { let policy = parse_policy(policy_json_or_preset)?; normalize_null_device_env(&mut env_map); @@ -302,6 +305,7 @@ mod windows_impl { cwd: cwd.to_path_buf(), env_map: env_map.clone(), timeout_ms, + use_private_desktop, stdin_pipe: stdin_name.clone(), stdout_pipe: stdout_name.clone(), stderr_pipe: stderr_name.clone(), @@ -530,6 +534,7 @@ mod stub { } /// Stub implementation for non-Windows targets; sandboxing only works on Windows. + #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( _policy_json_or_preset: &str, _sandbox_policy_cwd: &Path, @@ -538,6 +543,7 @@ mod stub { _cwd: &Path, _env_map: HashMap, _timeout_ms: Option, + _use_private_desktop: bool, ) -> Result { bail!("Windows sandbox is only available on Windows") } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index fcb2e9a33..cd3d95911 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -9,6 +9,7 @@ windows_modules!( allow, audit, cap, + desktop, dpapi, env, helper_materialization, @@ -126,6 +127,8 @@ pub use windows_impl::run_windows_sandbox_legacy_preflight; #[cfg(target_os = "windows")] pub use windows_impl::CaptureResult; #[cfg(target_os = "windows")] +pub use winutil::quote_windows_arg; +#[cfg(target_os = "windows")] pub use winutil::string_from_sid_bytes; #[cfg(target_os = "windows")] pub use winutil::to_wide; @@ -158,19 +161,15 @@ mod windows_impl { use super::env::apply_no_network_to_env; use super::env::ensure_non_interactive_pager; use super::env::normalize_null_device_env; - use super::logging::debug_log; use super::logging::log_failure; use super::logging::log_start; use super::logging::log_success; use super::path_normalization::canonicalize_path; use super::policy::parse_policy; use super::policy::SandboxPolicy; - use super::process::make_env_block; + use super::process::create_process_as_user; use super::token::convert_string_sid_to_sid; use super::token::create_workspace_write_token_with_caps_from; - use super::winutil::format_last_error; - use super::winutil::quote_windows_arg; - use super::winutil::to_wide; use super::workspace_acl::is_command_cwd_root; use super::workspace_acl::protect_workspace_agents_dir; use super::workspace_acl::protect_workspace_codex_dir; @@ -187,14 +186,9 @@ mod windows_impl { use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::Foundation::HANDLE_FLAG_INHERIT; use windows_sys::Win32::System::Pipes::CreatePipe; - use windows_sys::Win32::System::Threading::CreateProcessAsUserW; use windows_sys::Win32::System::Threading::GetExitCodeProcess; use windows_sys::Win32::System::Threading::WaitForSingleObject; - use windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT; use windows_sys::Win32::System::Threading::INFINITE; - use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; - use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES; - use windows_sys::Win32::System::Threading::STARTUPINFOW; type PipeHandles = ((HANDLE, HANDLE), (HANDLE, HANDLE), (HANDLE, HANDLE)); @@ -242,6 +236,7 @@ mod windows_impl { pub timed_out: bool, } + #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( policy_json_or_preset: &str, sandbox_policy_cwd: &Path, @@ -250,6 +245,7 @@ mod windows_impl { cwd: &Path, mut env_map: HashMap, timeout_ms: Option, + use_private_desktop: bool, ) -> Result { let policy = parse_policy(policy_json_or_preset)?; let apply_network_block = should_apply_network_block(&policy); @@ -358,61 +354,34 @@ mod windows_impl { let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? }; let ((in_r, in_w), (out_r, out_w), (err_r, err_w)) = (stdin_pair, stdout_pair, stderr_pair); - let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; - si.cb = std::mem::size_of::() as u32; - si.dwFlags |= STARTF_USESTDHANDLES; - si.hStdInput = in_r; - si.hStdOutput = out_w; - si.hStdError = err_w; - - let mut pi: PROCESS_INFORMATION = unsafe { std::mem::zeroed() }; - let cmdline_str = command - .iter() - .map(|a| quote_windows_arg(a)) - .collect::>() - .join(" "); - let mut cmdline: Vec = to_wide(&cmdline_str); - let env_block = make_env_block(&env_map); - let desktop = to_wide("Winsta0\\Default"); - si.lpDesktop = desktop.as_ptr() as *mut u16; let spawn_res = unsafe { - CreateProcessAsUserW( + create_process_as_user( h_token, - ptr::null(), - cmdline.as_mut_ptr(), - ptr::null_mut(), - ptr::null_mut(), - 1, - CREATE_UNICODE_ENVIRONMENT, - env_block.as_ptr() as *mut c_void, - to_wide(cwd).as_ptr(), - &si, - &mut pi, + &command, + cwd, + &env_map, + logs_base_dir, + Some((in_r, out_w, err_w)), + use_private_desktop, ) }; - if spawn_res == 0 { - let err = unsafe { GetLastError() } as i32; - let dbg = format!( - "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}", - err, - format_last_error(err), - cwd.display(), - cmdline_str, - env_block.len(), - si.dwFlags, - ); - debug_log(&dbg, logs_base_dir); - unsafe { - CloseHandle(in_r); - CloseHandle(in_w); - CloseHandle(out_r); - CloseHandle(out_w); - CloseHandle(err_r); - CloseHandle(err_w); - CloseHandle(h_token); + let created = match spawn_res { + Ok(v) => v, + Err(err) => { + unsafe { + CloseHandle(in_r); + CloseHandle(in_w); + CloseHandle(out_r); + CloseHandle(out_w); + CloseHandle(err_r); + CloseHandle(err_w); + CloseHandle(h_token); + } + return Err(err); } - return Err(anyhow::anyhow!("CreateProcessAsUserW failed: {}", err)); - } + }; + let pi = created.process_info; + let _desktop = created; unsafe { CloseHandle(in_r); @@ -617,6 +586,7 @@ mod stub { pub timed_out: bool, } + #[allow(clippy::too_many_arguments)] pub fn run_windows_sandbox_capture( _policy_json_or_preset: &str, _sandbox_policy_cwd: &Path, @@ -625,6 +595,7 @@ mod stub { _cwd: &Path, _env_map: HashMap, _timeout_ms: Option, + _use_private_desktop: bool, ) -> Result { bail!("Windows sandbox is only available on Windows") } diff --git a/codex-rs/windows-sandbox-rs/src/process.rs b/codex-rs/windows-sandbox-rs/src/process.rs index 2dbd152ce..2bbb46700 100644 --- a/codex-rs/windows-sandbox-rs/src/process.rs +++ b/codex-rs/windows-sandbox-rs/src/process.rs @@ -1,3 +1,4 @@ +use crate::desktop::LaunchDesktop; use crate::logging; use crate::winutil::format_last_error; use crate::winutil::quote_windows_arg; @@ -22,6 +23,12 @@ use windows_sys::Win32::System::Threading::PROCESS_INFORMATION; use windows_sys::Win32::System::Threading::STARTF_USESTDHANDLES; use windows_sys::Win32::System::Threading::STARTUPINFOW; +pub struct CreatedProcess { + pub process_info: PROCESS_INFORMATION, + pub startup_info: STARTUPINFOW, + _desktop: LaunchDesktop, +} + pub fn make_env_block(env: &HashMap) -> Vec { let mut items: Vec<(String, String)> = env.iter().map(|(k, v)| (k.clone(), v.clone())).collect(); @@ -68,7 +75,8 @@ pub unsafe fn create_process_as_user( env_map: &HashMap, logs_base_dir: Option<&Path>, stdio: Option<(HANDLE, HANDLE, HANDLE)>, -) -> Result<(PROCESS_INFORMATION, STARTUPINFOW)> { + use_private_desktop: bool, +) -> Result { let cmdline_str = argv .iter() .map(|a| quote_windows_arg(a)) @@ -80,9 +88,9 @@ pub unsafe fn create_process_as_user( si.cb = std::mem::size_of::() as u32; // Some processes (e.g., PowerShell) can fail with STATUS_DLL_INIT_FAILED // if lpDesktop is not set when launching with a restricted token. - // Point explicitly at the interactive desktop. - let desktop = to_wide("Winsta0\\Default"); - si.lpDesktop = desktop.as_ptr() as *mut u16; + // Point explicitly at the interactive desktop or a private desktop. + let desktop = LaunchDesktop::prepare(use_private_desktop, logs_base_dir)?; + si.lpDesktop = desktop.startup_info_desktop(); let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); // Ensure handles are inheritable when custom stdio is supplied. let inherit_handles = match stdio { @@ -107,6 +115,10 @@ pub unsafe fn create_process_as_user( } }; + let creation_flags = CREATE_UNICODE_ENVIRONMENT; + let cwd_wide = to_wide(cwd); + let env_block_len = env_block.len(); + let ok = CreateProcessAsUserW( h_token, std::ptr::null(), @@ -114,25 +126,30 @@ pub unsafe fn create_process_as_user( std::ptr::null_mut(), std::ptr::null_mut(), inherit_handles as i32, - CREATE_UNICODE_ENVIRONMENT, + creation_flags, env_block.as_ptr() as *mut c_void, - to_wide(cwd).as_ptr(), + cwd_wide.as_ptr(), &si, &mut pi, ); if ok == 0 { let err = GetLastError() as i32; let msg = format!( - "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={}", + "CreateProcessAsUserW failed: {} ({}) | cwd={} | cmd={} | env_u16_len={} | si_flags={} | creation_flags={}", err, format_last_error(err), cwd.display(), cmdline_str, - env_block.len(), + env_block_len, si.dwFlags, + creation_flags, ); logging::debug_log(&msg, logs_base_dir); return Err(anyhow!("CreateProcessAsUserW failed: {}", err)); } - Ok((pi, si)) + Ok(CreatedProcess { + process_info: pi, + startup_info: si, + _desktop: desktop, + }) }