diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index fec9b2abdb3..589b88d9a22 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -169,6 +169,7 @@ use codex_core::read_head_for_summary; use codex_core::read_session_meta_line; use codex_core::rollout_date_parts; use codex_core::sandboxing::SandboxPermissions; +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_feedback::CodexFeedback; use codex_login::ServerOptions as LoginServerOptions; use codex_login::ShutdownHandle; @@ -176,6 +177,7 @@ use codex_login::run_login_server; use codex_protocol::ThreadId; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::Personality; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::dynamic_tools::DynamicToolSpec as CoreDynamicToolSpec; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; @@ -1259,12 +1261,14 @@ impl CodexMessageProcessor { let timeout_ms = params .timeout_ms .and_then(|timeout_ms| u64::try_from(timeout_ms).ok()); + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); let exec_params = ExecParams { command: params.command, cwd, expiration: timeout_ms.into(), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level, justification: None, arg0: None, }; @@ -3887,6 +3891,7 @@ impl CodexMessageProcessor { cwd: params.cwd, approval_policy: params.approval_policy.map(AskForApproval::to_core), sandbox_policy: params.sandbox_policy.map(|p| p.to_core()), + windows_sandbox_level: None, model: params.model, effort: params.effort.map(Some), summary: params.summary, diff --git a/codex-rs/core/src/apply_patch.rs b/codex-rs/core/src/apply_patch.rs index 1a47ca60b7d..f87e07300d1 100644 --- a/codex-rs/core/src/apply_patch.rs +++ b/codex-rs/core/src/apply_patch.rs @@ -42,6 +42,7 @@ pub(crate) async fn apply_patch( turn_context.approval_policy, &turn_context.sandbox_policy, &turn_context.cwd, + turn_context.windows_sandbox_level, ) { SafetyCheck::AutoApprove { user_explicitly_approved, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6401605eec0..baef7f04b35 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -173,11 +173,13 @@ use crate::turn_diff_tracker::TurnDiffTracker; use crate::unified_exec::UnifiedExecProcessManager; use crate::user_notification::UserNotification; use crate::util::backoff; +use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_async_utils::OrCancelExt; use codex_otel::OtelManager; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::Personality; use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::ContentItem; use codex_protocol::models::DeveloperInstructions; use codex_protocol::models::ResponseInputItem; @@ -324,6 +326,7 @@ impl Codex { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source, @@ -444,6 +447,7 @@ pub(crate) struct TurnContext { pub(crate) personality: Option, pub(crate) approval_policy: AskForApproval, pub(crate) sandbox_policy: SandboxPolicy, + pub(crate) windows_sandbox_level: WindowsSandboxLevel, pub(crate) shell_environment_policy: ShellEnvironmentPolicy, pub(crate) tools_config: ToolsConfig, pub(crate) ghost_snapshot: GhostSnapshotConfig, @@ -495,6 +499,7 @@ pub(crate) struct SessionConfiguration { approval_policy: Constrained, /// How to sandbox commands executed in the system sandbox_policy: Constrained, + windows_sandbox_level: WindowsSandboxLevel, /// Working directory that should be treated as the *root* of the /// session. All relative paths supplied by the model as well as the @@ -543,6 +548,9 @@ impl SessionConfiguration { if let Some(sandbox_policy) = updates.sandbox_policy.clone() { next_configuration.sandbox_policy.set(sandbox_policy)?; } + if let Some(windows_sandbox_level) = updates.windows_sandbox_level { + next_configuration.windows_sandbox_level = windows_sandbox_level; + } if let Some(cwd) = updates.cwd.clone() { next_configuration.cwd = cwd; } @@ -555,6 +563,7 @@ pub(crate) struct SessionSettingsUpdate { pub(crate) cwd: Option, pub(crate) approval_policy: Option, pub(crate) sandbox_policy: Option, + pub(crate) windows_sandbox_level: Option, pub(crate) collaboration_mode: Option, pub(crate) reasoning_summary: Option, pub(crate) final_output_json_schema: Option>, @@ -619,6 +628,7 @@ impl Session { personality: session_configuration.personality, approval_policy: session_configuration.approval_policy.value(), sandbox_policy: session_configuration.sandbox_policy.get().clone(), + windows_sandbox_level: session_configuration.windows_sandbox_level, shell_environment_policy: per_turn_config.shell_environment_policy.clone(), tools_config, ghost_snapshot: per_turn_config.ghost_snapshot.clone(), @@ -2144,6 +2154,7 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv cwd, approval_policy, sandbox_policy, + windows_sandbox_level, model, effort, summary, @@ -2167,6 +2178,7 @@ async fn submission_loop(sess: Arc, config: Arc, rx_sub: Receiv cwd, approval_policy, sandbox_policy, + windows_sandbox_level, collaboration_mode: Some(collaboration_mode), reasoning_summary: summary, personality, @@ -2378,6 +2390,7 @@ mod handlers { cwd: Some(cwd), approval_policy: Some(approval_policy), sandbox_policy: Some(sandbox_policy), + windows_sandbox_level: None, collaboration_mode, reasoning_summary: Some(summary), final_output_json_schema: Some(final_output_json_schema), @@ -2865,6 +2878,7 @@ async fn spawn_review_thread( personality: parent_turn_context.personality, approval_policy: parent_turn_context.approval_policy, sandbox_policy: parent_turn_context.sandbox_policy.clone(), + windows_sandbox_level: parent_turn_context.windows_sandbox_level, shell_environment_policy: parent_turn_context.shell_environment_policy.clone(), cwd: parent_turn_context.cwd.clone(), final_output_json_schema: None, @@ -3986,6 +4000,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4066,6 +4081,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4330,6 +4346,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4439,6 +4456,7 @@ mod tests { compact_prompt: config.compact_prompt.clone(), approval_policy: config.approval_policy.clone(), sandbox_policy: config.sandbox_policy.clone(), + windows_sandbox_level: WindowsSandboxLevel::from_config(&config), cwd: config.cwd.clone(), original_config_do_not_use: Arc::clone(&config), session_source: SessionSource::Exec, @@ -4941,6 +4959,7 @@ mod tests { expiration: timeout_ms.into(), env: HashMap::new(), sandbox_permissions, + windows_sandbox_level: turn_context.windows_sandbox_level, justification: Some("test".to_string()), arg0: None, }; @@ -4951,6 +4970,7 @@ mod tests { cwd: params.cwd.clone(), expiration: timeout_ms.into(), env: HashMap::new(), + windows_sandbox_level: turn_context.windows_sandbox_level, justification: params.justification.clone(), arg0: None, }; diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 031f3c6f33e..f7b05335dfe 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -38,6 +38,7 @@ use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; +use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_app_server_protocol::Tools; use codex_app_server_protocol::UserSavedConfig; use codex_protocol::config_types::AltScreenMode; @@ -49,6 +50,7 @@ use codex_protocol::config_types::SandboxMode; use codex_protocol::config_types::TrustLevel; use codex_protocol::config_types::Verbosity; use codex_protocol::config_types::WebSearchMode; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::openai_models::ReasoningEffort; use codex_rmcp_client::OAuthCredentialsStoreMode; use codex_utils_absolute_path::AbsolutePathBuf; @@ -1050,6 +1052,7 @@ impl ConfigToml { &self, sandbox_mode_override: Option, profile_sandbox_mode: Option, + windows_sandbox_level: WindowsSandboxLevel, resolved_cwd: &Path, ) -> SandboxPolicyResolution { let resolved_sandbox_mode = sandbox_mode_override @@ -1088,7 +1091,7 @@ impl ConfigToml { if cfg!(target_os = "windows") && matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) // If the experimental Windows sandbox is enabled, do not force a downgrade. - && crate::safety::get_platform_sandbox().is_none() + && windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Disabled { sandbox_policy = SandboxPolicy::new_read_only_policy(); forced_auto_mode_downgraded_on_windows = true; @@ -1279,16 +1282,6 @@ impl Config { let features = Features::from_config(&cfg, &config_profile, feature_overrides); let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features); - #[cfg(target_os = "windows")] - { - // Base flag controls sandbox on/off; elevated only applies when base is enabled. - let sandbox_enabled = features.enabled(Feature::WindowsSandbox); - crate::safety::set_windows_sandbox_enabled(sandbox_enabled); - let elevated_enabled = - sandbox_enabled && features.enabled(Feature::WindowsSandboxElevated); - crate::safety::set_windows_elevated_sandbox_enabled(elevated_enabled); - } - let resolved_cwd = { use std::env; @@ -1315,10 +1308,16 @@ impl Config { .get_active_project(&resolved_cwd) .unwrap_or(ProjectConfig { trust_level: None }); + let windows_sandbox_level = WindowsSandboxLevel::from_features(&features); let SandboxPolicyResolution { policy: mut sandbox_policy, forced_auto_mode_downgraded_on_windows, - } = cfg.derive_sandbox_policy(sandbox_mode, config_profile.sandbox_mode, &resolved_cwd); + } = cfg.derive_sandbox_policy( + sandbox_mode, + config_profile.sandbox_mode, + windows_sandbox_level, + &resolved_cwd, + ); if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy { for path in additional_writable_roots { if !writable_roots.iter().any(|existing| existing == &path) { @@ -1658,7 +1657,6 @@ impl Config { } pub fn set_windows_sandbox_globally(&mut self, value: bool) { - crate::safety::set_windows_sandbox_enabled(value); if value { self.features.enable(Feature::WindowsSandbox); } else { @@ -1668,7 +1666,6 @@ impl Config { } pub fn set_windows_elevated_sandbox_globally(&mut self, value: bool) { - crate::safety::set_windows_elevated_sandbox_enabled(value); if value { self.features.enable(Feature::WindowsSandboxElevated); } else { @@ -1862,6 +1859,7 @@ network_access = false # This should be ignored. let resolution = sandbox_full_access_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); assert_eq!( @@ -1885,6 +1883,7 @@ network_access = true # This should be ignored. let resolution = sandbox_read_only_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); assert_eq!( @@ -1916,6 +1915,7 @@ exclude_slash_tmp = true let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); if cfg!(target_os = "windows") { @@ -1964,6 +1964,7 @@ trust_level = "trusted" let resolution = sandbox_workspace_write_cfg.derive_sandbox_policy( sandbox_mode_override, None, + WindowsSandboxLevel::Disabled, &PathBuf::from("/tmp/test"), ); if cfg!(target_os = "windows") { @@ -4174,7 +4175,12 @@ trust_level = "untrusted" let cfg = toml::from_str::(config_with_untrusted) .expect("TOML deserialization should succeed"); - let resolution = cfg.derive_sandbox_policy(None, None, &PathBuf::from("/tmp/test")); + let resolution = cfg.derive_sandbox_policy( + None, + None, + WindowsSandboxLevel::Disabled, + &PathBuf::from("/tmp/test"), + ); // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) if cfg!(target_os = "windows") { diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 275f2fb8568..b6737b805de 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -64,6 +64,7 @@ pub struct ExecParams { pub expiration: ExecExpiration, pub env: HashMap, pub sandbox_permissions: SandboxPermissions, + pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, pub justification: Option, pub arg0: Option, } @@ -141,11 +142,15 @@ pub async fn process_exec_tool_call( codex_linux_sandbox_exe: &Option, stdout_stream: Option, ) -> Result { + let windows_sandbox_level = params.windows_sandbox_level; let sandbox_type = match &sandbox_policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { SandboxType::None } - _ => get_platform_sandbox().unwrap_or(SandboxType::None), + _ => get_platform_sandbox( + windows_sandbox_level != codex_protocol::config_types::WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None), }; tracing::debug!("Sandbox type: {sandbox_type:?}"); @@ -155,6 +160,7 @@ pub async fn process_exec_tool_call( expiration, env, sandbox_permissions, + windows_sandbox_level, justification, arg0: _, } = params; @@ -184,6 +190,7 @@ pub async fn process_exec_tool_call( sandbox_type, sandbox_cwd, codex_linux_sandbox_exe.as_ref(), + windows_sandbox_level, ) .map_err(CodexErr::from)?; @@ -202,6 +209,7 @@ pub(crate) async fn execute_exec_env( env, expiration, sandbox, + windows_sandbox_level, sandbox_permissions, justification, arg0, @@ -213,6 +221,7 @@ pub(crate) async fn execute_exec_env( expiration, env, sandbox_permissions, + windows_sandbox_level, justification, arg0, }; @@ -229,7 +238,7 @@ async fn exec_windows_sandbox( sandbox_policy: &SandboxPolicy, ) -> Result { use crate::config::find_codex_home; - use crate::safety::is_windows_elevated_sandbox_enabled; + use codex_protocol::config_types::WindowsSandboxLevel; use codex_windows_sandbox::run_windows_sandbox_capture; use codex_windows_sandbox::run_windows_sandbox_capture_elevated; @@ -238,6 +247,7 @@ async fn exec_windows_sandbox( cwd, env, expiration, + windows_sandbox_level, .. } = params; // TODO(iceweasel-oai): run_windows_sandbox_capture should support all @@ -255,7 +265,7 @@ async fn exec_windows_sandbox( "windows sandbox: failed to resolve codex_home: {err}" ))) })?; - let use_elevated = is_windows_elevated_sandbox_enabled(); + let use_elevated = matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated); let spawn_res = tokio::task::spawn_blocking(move || { if use_elevated { run_windows_sandbox_capture_elevated( @@ -564,6 +574,7 @@ async fn exec( env, arg0, expiration, + windows_sandbox_level: _, .. } = params; @@ -878,6 +889,7 @@ mod tests { expiration: 500.into(), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; @@ -923,6 +935,7 @@ mod tests { expiration: ExecExpiration::Cancellation(cancel_token), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index f662f41b21e..081066cb416 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -125,9 +125,6 @@ pub use exec_policy::ExecPolicyError; pub use exec_policy::check_execpolicy_for_warnings; pub use exec_policy::load_exec_policy; pub use safety::get_platform_sandbox; -pub use safety::is_windows_elevated_sandbox_enabled; -pub use safety::set_windows_elevated_sandbox_enabled; -pub use safety::set_windows_sandbox_enabled; pub use tools::spec::parse_tool_input_schema; // Re-export the protocol types from the standalone `codex-protocol` crate so existing // `codex_core::protocol::...` references continue to work across the workspace. diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 601a5a8b81e..47a12e029ec 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -10,45 +10,7 @@ use crate::util::resolve_path; use crate::protocol::AskForApproval; use crate::protocol::SandboxPolicy; - -#[cfg(target_os = "windows")] -use std::sync::atomic::AtomicBool; -#[cfg(target_os = "windows")] -use std::sync::atomic::Ordering; - -#[cfg(target_os = "windows")] -static WINDOWS_SANDBOX_ENABLED: AtomicBool = AtomicBool::new(false); -#[cfg(target_os = "windows")] -static WINDOWS_ELEVATED_SANDBOX_ENABLED: AtomicBool = AtomicBool::new(false); - -#[cfg(target_os = "windows")] -pub fn set_windows_sandbox_enabled(enabled: bool) { - WINDOWS_SANDBOX_ENABLED.store(enabled, Ordering::Relaxed); -} - -#[cfg(not(target_os = "windows"))] -#[allow(dead_code)] -pub fn set_windows_sandbox_enabled(_enabled: bool) {} - -#[cfg(target_os = "windows")] -pub fn set_windows_elevated_sandbox_enabled(enabled: bool) { - WINDOWS_ELEVATED_SANDBOX_ENABLED.store(enabled, Ordering::Relaxed); -} - -#[cfg(not(target_os = "windows"))] -#[allow(dead_code)] -pub fn set_windows_elevated_sandbox_enabled(_enabled: bool) {} - -#[cfg(target_os = "windows")] -pub fn is_windows_elevated_sandbox_enabled() -> bool { - WINDOWS_ELEVATED_SANDBOX_ENABLED.load(Ordering::Relaxed) -} - -#[cfg(not(target_os = "windows"))] -#[allow(dead_code)] -pub fn is_windows_elevated_sandbox_enabled() -> bool { - false -} +use codex_protocol::config_types::WindowsSandboxLevel; #[derive(Debug, PartialEq)] pub enum SafetyCheck { @@ -67,6 +29,7 @@ pub fn assess_patch_safety( policy: AskForApproval, sandbox_policy: &SandboxPolicy, cwd: &Path, + windows_sandbox_level: WindowsSandboxLevel, ) -> SafetyCheck { if action.is_empty() { return SafetyCheck::Reject { @@ -104,7 +67,7 @@ pub fn assess_patch_safety( // Only auto‑approve when we can actually enforce a sandbox. Otherwise // fall back to asking the user because the patch may touch arbitrary // paths outside the project. - match get_platform_sandbox() { + match get_platform_sandbox(windows_sandbox_level != WindowsSandboxLevel::Disabled) { Some(sandbox_type) => SafetyCheck::AutoApprove { sandbox_type, user_explicitly_approved: false, @@ -122,19 +85,17 @@ pub fn assess_patch_safety( } } -pub fn get_platform_sandbox() -> Option { +pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option { if cfg!(target_os = "macos") { Some(SandboxType::MacosSeatbelt) } else if cfg!(target_os = "linux") { Some(SandboxType::LinuxSeccomp) } else if cfg!(target_os = "windows") { - #[cfg(target_os = "windows")] - { - if WINDOWS_SANDBOX_ENABLED.load(Ordering::Relaxed) { - return Some(SandboxType::WindowsRestrictedToken); - } + if windows_sandbox_enabled { + Some(SandboxType::WindowsRestrictedToken) + } else { + None } - None } else { None } @@ -277,7 +238,13 @@ mod tests { }; assert_eq!( - assess_patch_safety(&add_inside, AskForApproval::OnRequest, &policy, &cwd), + assess_patch_safety( + &add_inside, + AskForApproval::OnRequest, + &policy, + &cwd, + WindowsSandboxLevel::Disabled + ), SafetyCheck::AutoApprove { sandbox_type: SandboxType::None, user_explicitly_approved: false, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index a2c8ad1e31d..fca7adda293 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -21,6 +21,7 @@ use crate::seatbelt::create_seatbelt_command_args; use crate::spawn::CODEX_SANDBOX_ENV_VAR; use crate::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use crate::tools::sandboxing::SandboxablePreference; +use codex_protocol::config_types::WindowsSandboxLevel; pub use codex_protocol::models::SandboxPermissions; use std::collections::HashMap; use std::path::Path; @@ -44,6 +45,7 @@ pub struct ExecEnv { pub env: HashMap, pub expiration: ExecExpiration, pub sandbox: SandboxType, + pub windows_sandbox_level: WindowsSandboxLevel, pub sandbox_permissions: SandboxPermissions, pub justification: Option, pub arg0: Option, @@ -76,19 +78,26 @@ impl SandboxManager { &self, policy: &SandboxPolicy, pref: SandboxablePreference, + windows_sandbox_level: WindowsSandboxLevel, ) -> SandboxType { match pref { SandboxablePreference::Forbid => SandboxType::None, SandboxablePreference::Require => { // Require a platform sandbox when available; on Windows this // respects the experimental_windows_sandbox feature. - crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None) + crate::safety::get_platform_sandbox( + windows_sandbox_level != WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None) } SandboxablePreference::Auto => match policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { SandboxType::None } - _ => crate::safety::get_platform_sandbox().unwrap_or(SandboxType::None), + _ => crate::safety::get_platform_sandbox( + windows_sandbox_level != WindowsSandboxLevel::Disabled, + ) + .unwrap_or(SandboxType::None), }, } } @@ -100,6 +109,7 @@ impl SandboxManager { sandbox: SandboxType, sandbox_policy_cwd: &Path, codex_linux_sandbox_exe: Option<&PathBuf>, + windows_sandbox_level: WindowsSandboxLevel, ) -> Result { let mut env = spec.env; if !policy.has_full_network_access() { @@ -160,6 +170,7 @@ impl SandboxManager { env, expiration: spec.expiration, sandbox, + windows_sandbox_level, sandbox_permissions: spec.sandbox_permissions, justification: spec.justification, arg0: arg0_override, diff --git a/codex-rs/core/src/tasks/user_shell.rs b/codex-rs/core/src/tasks/user_shell.rs index 1f1f1eb59e0..4305417a357 100644 --- a/codex-rs/core/src/tasks/user_shell.rs +++ b/codex-rs/core/src/tasks/user_shell.rs @@ -109,6 +109,7 @@ impl SessionTask for UserShellCommandTask { // should use that instead of an "arbitrarily large" timeout here. expiration: USER_SHELL_TIMEOUT_MS.into(), sandbox: SandboxType::None, + windows_sandbox_level: turn_context.windows_sandbox_level, sandbox_permissions: SandboxPermissions::UseDefault, justification: None, arg0: None, diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index dc9f198cc08..c3628e8c347 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -36,6 +36,7 @@ impl ShellHandler { expiration: params.timeout_ms.into(), env: create_env(&turn_context.shell_environment_policy), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), + windows_sandbox_level: turn_context.windows_sandbox_level, justification: params.justification, arg0: None, } @@ -62,6 +63,7 @@ impl ShellCommandHandler { expiration: params.timeout_ms.into(), env: create_env(&turn_context.shell_environment_policy), sandbox_permissions: params.sandbox_permissions.unwrap_or_default(), + windows_sandbox_level: turn_context.windows_sandbox_level, justification: params.justification, arg0: None, } diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index f0810916a55..e9fdd6208be 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -88,19 +88,22 @@ impl ToolOrchestrator { // 2) First attempt under the selected sandbox. let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) { SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None, - SandboxOverride::NoOverride => self - .sandbox - .select_initial(&turn_ctx.sandbox_policy, tool.sandbox_preference()), + SandboxOverride::NoOverride => self.sandbox.select_initial( + &turn_ctx.sandbox_policy, + tool.sandbox_preference(), + turn_ctx.windows_sandbox_level, + ), }; // Platform-specific flag gating is handled by SandboxManager::select_initial - // via crate::safety::get_platform_sandbox(). + // via crate::safety::get_platform_sandbox(..). let initial_attempt = SandboxAttempt { sandbox: initial_sandbox, policy: &turn_ctx.sandbox_policy, manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: turn_ctx.codex_linux_sandbox_exe.as_ref(), + windows_sandbox_level: turn_ctx.windows_sandbox_level, }; match tool.run(req, &initial_attempt, tool_ctx).await { @@ -151,6 +154,7 @@ impl ToolOrchestrator { manager: &self.sandbox, sandbox_cwd: &turn_ctx.cwd, codex_linux_sandbox_exe: None, + windows_sandbox_level: turn_ctx.windows_sandbox_level, }; // Second attempt. diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index eefce38bc6b..a7d2bca62ac 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -274,6 +274,7 @@ pub(crate) struct SandboxAttempt<'a> { pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a Path, pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, + pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, } impl<'a> SandboxAttempt<'a> { @@ -287,6 +288,7 @@ impl<'a> SandboxAttempt<'a> { self.sandbox, self.sandbox_cwd, self.codex_linux_sandbox_exe, + self.windows_sandbox_level, ) } } diff --git a/codex-rs/core/src/windows_sandbox.rs b/codex-rs/core/src/windows_sandbox.rs index b355bad2802..45212e1d486 100644 --- a/codex-rs/core/src/windows_sandbox.rs +++ b/codex-rs/core/src/windows_sandbox.rs @@ -1,4 +1,8 @@ +use crate::config::Config; +use crate::features::Feature; +use crate::features::Features; use crate::protocol::SandboxPolicy; +use codex_protocol::config_types::WindowsSandboxLevel; use std::collections::HashMap; use std::path::Path; @@ -8,6 +12,36 @@ use std::path::Path; /// prompts users to enable the legacy sandbox feature. pub const ELEVATED_SANDBOX_NUX_ENABLED: bool = true; +pub trait WindowsSandboxLevelExt { + fn from_config(config: &Config) -> WindowsSandboxLevel; + fn from_features(features: &Features) -> WindowsSandboxLevel; +} + +impl WindowsSandboxLevelExt for WindowsSandboxLevel { + fn from_config(config: &Config) -> WindowsSandboxLevel { + Self::from_features(&config.features) + } + + fn from_features(features: &Features) -> WindowsSandboxLevel { + if !features.enabled(Feature::WindowsSandbox) { + return WindowsSandboxLevel::Disabled; + } + if features.enabled(Feature::WindowsSandboxElevated) { + WindowsSandboxLevel::Elevated + } else { + WindowsSandboxLevel::RestrictedToken + } + } +} + +pub fn windows_sandbox_level_from_config(config: &Config) -> WindowsSandboxLevel { + WindowsSandboxLevel::from_config(config) +} + +pub fn windows_sandbox_level_from_features(features: &Features) -> WindowsSandboxLevel { + WindowsSandboxLevel::from_features(features) +} + #[cfg(target_os = "windows")] pub fn sandbox_setup_is_complete(codex_home: &Path) -> bool { codex_windows_sandbox::sandbox_setup_is_complete(codex_home) diff --git a/codex-rs/core/tests/suite/collaboration_instructions.rs b/codex-rs/core/tests/suite/collaboration_instructions.rs index f7183b817e9..0f492d78c9e 100644 --- a/codex-rs/core/tests/suite/collaboration_instructions.rs +++ b/codex-rs/core/tests/suite/collaboration_instructions.rs @@ -104,6 +104,7 @@ async fn user_input_includes_collaboration_instructions_after_override() -> Resu cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -185,6 +186,7 @@ async fn override_then_user_turn_uses_updated_collaboration_instructions() -> Re cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -238,6 +240,7 @@ async fn user_turn_overrides_collaboration_instructions_after_override() -> Resu cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -292,6 +295,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -316,6 +320,7 @@ async fn collaboration_mode_update_emits_new_instruction_message() -> Result<()> cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -361,6 +366,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -385,6 +391,7 @@ async fn collaboration_mode_update_noop_does_not_append() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -436,6 +443,7 @@ async fn resume_replays_collaboration_instructions() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -491,6 +499,7 @@ async fn empty_collaboration_instructions_are_ignored() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/exec.rs b/codex-rs/core/tests/suite/exec.rs index c0934821570..cdf597a4e95 100644 --- a/codex-rs/core/tests/suite/exec.rs +++ b/codex-rs/core/tests/suite/exec.rs @@ -10,6 +10,7 @@ use codex_core::exec::process_exec_tool_call; use codex_core::protocol::SandboxPolicy; use codex_core::sandboxing::SandboxPermissions; use codex_core::spawn::CODEX_SANDBOX_ENV_VAR; +use codex_protocol::config_types::WindowsSandboxLevel; use tempfile::TempDir; use codex_core::error::Result; @@ -27,7 +28,7 @@ fn skip_test() -> bool { #[expect(clippy::expect_used)] async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result { - let sandbox_type = get_platform_sandbox().expect("should be able to get sandbox type"); + let sandbox_type = get_platform_sandbox(false).expect("should be able to get sandbox type"); assert_eq!(sandbox_type, SandboxType::MacosSeatbelt); let params = ExecParams { @@ -36,6 +37,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -161,6 +162,7 @@ async fn override_turn_context_records_environment_update() -> Result<()> { cwd: Some(new_cwd.path().to_path_buf()), approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -198,6 +200,7 @@ async fn override_turn_context_records_collaboration_update() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index f3d9e8d47b2..679c0c1f6c4 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -106,6 +106,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -227,6 +228,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -309,6 +311,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index 410ca08e4e9..7a27866569a 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -210,6 +210,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -362,6 +363,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(remote_slug.to_string()), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 686428b21a4..579a1856d41 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -350,6 +350,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: Some(new_policy.clone()), + windows_sandbox_level: None, model: Some("o3".to_string()), effort: Some(Some(ReasoningEffort::High)), summary: Some(ReasoningSummary::Detailed), @@ -427,6 +428,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul cwd: None, approval_policy: Some(AskForApproval::Never), sandbox_policy: None, + windows_sandbox_level: None, model: Some("gpt-5.1-codex".to_string()), effort: Some(Some(ReasoningEffort::Low)), summary: None, diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 7e5fab98718..71374cb84fd 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -138,6 +138,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(REMOTE_MODEL_SLUG.to_string()), effort: None, summary: None, @@ -367,6 +368,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(model.to_string()), effort: None, summary: None, diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index df678f9e9de..70a820883c4 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -818,6 +818,7 @@ async fn review_uses_overridden_cwd_for_base_branch_merge_base() { cwd: Some(repo_path.to_path_buf()), approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: None, effort: None, summary: None, diff --git a/codex-rs/exec-server/src/posix/escalate_server.rs b/codex-rs/exec-server/src/posix/escalate_server.rs index d99f3007040..ef991ce8cd0 100644 --- a/codex-rs/exec-server/src/posix/escalate_server.rs +++ b/codex-rs/exec-server/src/posix/escalate_server.rs @@ -10,6 +10,7 @@ use path_absolutize::Absolutize as _; use codex_core::SandboxState; use codex_core::exec::process_exec_tool_call; +use codex_core::protocol_config_types::WindowsSandboxLevel; use codex_core::sandboxing::SandboxPermissions; use tokio::process::Command; use tokio_util::sync::CancellationToken; @@ -87,6 +88,7 @@ impl EscalateServer { expiration: ExecExpiration::Cancellation(cancel_rx), env, sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, justification: None, arg0: None, }, diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 663f3cbe215..84f89a16782 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -7,6 +7,7 @@ use codex_core::exec::ExecParams; use codex_core::exec::process_exec_tool_call; use codex_core::exec_env::create_env; use codex_core::protocol::SandboxPolicy; +use codex_core::protocol_config_types::WindowsSandboxLevel; use codex_core::sandboxing::SandboxPermissions; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -60,6 +61,7 @@ async fn run_cmd_output( expiration: timeout_ms.into(), env: create_env_from_core_vars(), sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; @@ -177,6 +179,7 @@ async fn assert_network_blocked(cmd: &[&str]) { expiration: NETWORK_TIMEOUT_MS.into(), env: create_env_from_core_vars(), sandbox_permissions: SandboxPermissions::UseDefault, + windows_sandbox_level: WindowsSandboxLevel::Disabled, justification: None, arg0: None, }; diff --git a/codex-rs/protocol/src/config_types.rs b/codex-rs/protocol/src/config_types.rs index ba6b12aadf2..1ff4f3cd147 100644 --- a/codex-rs/protocol/src/config_types.rs +++ b/codex-rs/protocol/src/config_types.rs @@ -66,6 +66,18 @@ pub enum SandboxMode { DangerFullAccess, } +#[derive( + Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, JsonSchema, TS, +)] +#[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +pub enum WindowsSandboxLevel { + #[default] + Disabled, + RestrictedToken, + Elevated, +} + #[derive( Debug, Serialize, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index e9cb8147d7b..a7e40372ba5 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -16,6 +16,7 @@ use crate::approvals::ElicitationRequestEvent; use crate::config_types::CollaborationMode; use crate::config_types::Personality; use crate::config_types::ReasoningSummary as ReasoningSummaryConfig; +use crate::config_types::WindowsSandboxLevel; use crate::custom_prompts::CustomPrompt; use crate::dynamic_tools::DynamicToolCallRequest; use crate::dynamic_tools::DynamicToolResponse; @@ -157,6 +158,10 @@ pub enum Op { #[serde(skip_serializing_if = "Option::is_none")] sandbox_policy: Option, + /// Updated Windows sandbox mode for tool execution. + #[serde(skip_serializing_if = "Option::is_none")] + windows_sandbox_level: Option, + /// Updated model slug. When set, the model info is derived /// automatically. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index bc006a2928f..2ded7bfd276 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -42,7 +42,6 @@ use codex_core::config::ConfigOverrides; use codex_core::config::edit::ConfigEdit; use codex_core::config::edit::ConfigEditsBuilder; use codex_core::config_loader::ConfigLayerStackOrdering; -#[cfg(target_os = "windows")] use codex_core::features::Feature; use codex_core::models_manager::manager::RefreshStrategy; use codex_core::models_manager::model_presets::HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG; @@ -58,9 +57,13 @@ use codex_core::protocol::SandboxPolicy; use codex_core::protocol::SessionSource; use codex_core::protocol::SkillErrorInfo; use codex_core::protocol::TokenUsage; +#[cfg(target_os = "windows")] +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_otel::OtelManager; use codex_protocol::ThreadId; use codex_protocol::config_types::Personality; +#[cfg(target_os = "windows")] +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::items::TurnItem; use codex_protocol::openai_models::ModelPreset; use codex_protocol::openai_models::ModelUpgrade; @@ -1072,7 +1075,8 @@ impl App { // On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows. #[cfg(target_os = "windows")] { - let should_check = codex_core::get_platform_sandbox().is_some() + let should_check = WindowsSandboxLevel::from_config(&app.config) + != WindowsSandboxLevel::Disabled && matches!( app.config.sandbox_policy.get(), codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } @@ -1668,9 +1672,24 @@ impl App { elevated_enabled, ); self.chat_widget.clear_forced_auto_mode_downgrade(); + let windows_sandbox_level = + WindowsSandboxLevel::from_config(&self.config); if let Some((sample_paths, extra_count, failed_scan)) = self.chat_widget.world_writable_warning_details() { + self.app_event_tx.send(AppEvent::CodexOp( + Op::OverrideTurnContext { + cwd: None, + approval_policy: None, + sandbox_policy: None, + windows_sandbox_level: Some(windows_sandbox_level), + model: None, + effort: None, + summary: None, + collaboration_mode: None, + personality: None, + }, + )); self.app_event_tx.send( AppEvent::OpenWorldWritableWarningConfirmation { preset: Some(preset.clone()), @@ -1685,6 +1704,7 @@ impl App { cwd: None, approval_policy: Some(preset.approval), sandbox_policy: Some(preset.sandbox.clone()), + windows_sandbox_level: Some(windows_sandbox_level), model: None, effort: None, summary: None, @@ -1823,7 +1843,8 @@ impl App { } #[cfg(target_os = "windows")] if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly) - || codex_core::get_platform_sandbox().is_some() + || WindowsSandboxLevel::from_config(&self.config) + != WindowsSandboxLevel::Disabled { self.config.forced_auto_mode_downgraded_on_windows = false; } @@ -1845,7 +1866,8 @@ impl App { return Ok(AppRunControl::Continue); } - let should_check = codex_core::get_platform_sandbox().is_some() + let should_check = WindowsSandboxLevel::from_config(&self.config) + != WindowsSandboxLevel::Disabled && policy_is_workspace_write_or_ro && !self.chat_widget.world_writable_warning_hidden(); if should_check { @@ -1869,6 +1891,12 @@ impl App { if updates.is_empty() { return Ok(AppRunControl::Continue); } + let windows_sandbox_changed = updates.iter().any(|(feature, _)| { + matches!( + feature, + Feature::WindowsSandbox | Feature::WindowsSandboxElevated + ) + }); let mut builder = ConfigEditsBuilder::new(&self.config.codex_home) .with_profile(self.active_profile.as_deref()); for (feature, enabled) in &updates { @@ -1894,6 +1922,24 @@ impl App { } } } + if windows_sandbox_changed { + #[cfg(target_os = "windows")] + { + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + self.app_event_tx + .send(AppEvent::CodexOp(Op::OverrideTurnContext { + cwd: None, + approval_policy: None, + sandbox_policy: None, + windows_sandbox_level: Some(windows_sandbox_level), + model: None, + effort: None, + summary: None, + collaboration_mode: None, + personality: None, + })); + } + } if let Err(err) = builder.apply().await { tracing::error!(error = %err, "failed to persist feature flags"); self.chat_widget.add_error_message(format!( diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index f1ec28f3f13..b65d1efb620 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -178,6 +178,9 @@ pub(crate) enum AppEvent { mode: WindowsSandboxEnableMode, }, + /// Update the Windows sandbox feature mode without changing approval presets. + #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + /// Update the current approval policy in the running app and widget. UpdateAskForApprovalPolicy(AskForApproval), diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index ccf0fac5ddb..8d999d641bc 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -254,6 +254,7 @@ pub(crate) struct ChatComposer { config: ChatComposerConfig, collaboration_mode_indicator: Option, personality_command_enabled: bool, + windows_degraded_sandbox_active: bool, } #[derive(Clone, Debug)] @@ -339,6 +340,7 @@ impl ChatComposer { config, collaboration_mode_indicator: None, personality_command_enabled: false, + windows_degraded_sandbox_active: false, }; // Apply configuration via the setter to keep side-effects centralized. this.set_disable_paste_burst(disable_paste_burst); @@ -386,6 +388,10 @@ impl ChatComposer { fn image_paste_enabled(&self) -> bool { self.config.image_paste_enabled } + #[cfg(target_os = "windows")] + pub fn set_windows_degraded_sandbox_active(&mut self, enabled: bool) { + self.windows_degraded_sandbox_active = enabled; + } fn layout_areas(&self, area: Rect) -> [Rect; 3] { let footer_props = self.footer_props(); let footer_hint_height = self @@ -1699,6 +1705,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) .is_some(); let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:"); @@ -1875,6 +1882,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) { self.textarea.set_text_clearing_elements(""); @@ -1902,6 +1910,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) && cmd == SlashCommand::Review { @@ -2345,6 +2354,7 @@ impl ChatComposer { name, self.collaboration_modes_enabled, self.personality_command_enabled, + self.windows_degraded_sandbox_active, ) { return true; } @@ -2401,6 +2411,7 @@ impl ChatComposer { CommandPopupFlags { collaboration_modes_enabled, personality_command_enabled, + windows_degraded_sandbox_active: self.windows_degraded_sandbox_active, }, ); command_popup.on_composer_text_change(first_line.to_string()); diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 3c54dff0fef..e879e880cc8 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -33,6 +33,7 @@ pub(crate) struct CommandPopup { pub(crate) struct CommandPopupFlags { pub(crate) collaboration_modes_enabled: bool, pub(crate) personality_command_enabled: bool, + pub(crate) windows_degraded_sandbox_active: bool, } impl CommandPopup { @@ -41,6 +42,7 @@ impl CommandPopup { let builtins = slash_commands::builtins_for_input( flags.collaboration_modes_enabled, flags.personality_command_enabled, + flags.windows_degraded_sandbox_active, ); // Exclude prompts that collide with builtin command names and sort by name. let exclude: HashSet = builtins.iter().map(|(n, _)| (*n).to_string()).collect(); @@ -461,6 +463,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, personality_command_enabled: true, + windows_degraded_sandbox_active: false, }, ); popup.on_composer_text_change("/collab".to_string()); @@ -478,6 +481,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, personality_command_enabled: false, + windows_degraded_sandbox_active: false, }, ); popup.on_composer_text_change("/pers".to_string()); @@ -503,6 +507,7 @@ mod tests { CommandPopupFlags { collaboration_modes_enabled: true, personality_command_enabled: true, + windows_degraded_sandbox_active: false, }, ); popup.on_composer_text_change("/personality".to_string()); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index a435046cba3..ce6b18f714d 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -208,6 +208,12 @@ impl BottomPane { self.request_redraw(); } + #[cfg(target_os = "windows")] + pub fn set_windows_degraded_sandbox_active(&mut self, enabled: bool) { + self.composer.set_windows_degraded_sandbox_active(enabled); + self.request_redraw(); + } + pub fn set_collaboration_mode_indicator( &mut self, indicator: Option, diff --git a/codex-rs/tui/src/bottom_pane/slash_commands.rs b/codex-rs/tui/src/bottom_pane/slash_commands.rs index 55765a00da9..e0707a42285 100644 --- a/codex-rs/tui/src/bottom_pane/slash_commands.rs +++ b/codex-rs/tui/src/bottom_pane/slash_commands.rs @@ -8,20 +8,12 @@ use codex_common::fuzzy_match::fuzzy_match; use crate::slash_command::SlashCommand; use crate::slash_command::built_in_slash_commands; -/// Whether the Windows degraded-sandbox elevation flow is currently allowed. -pub(crate) fn windows_degraded_sandbox_active() -> bool { - cfg!(target_os = "windows") - && codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED - && codex_core::get_platform_sandbox().is_some() - && !codex_core::is_windows_elevated_sandbox_enabled() -} - /// Return the built-ins that should be visible/usable for the current input. pub(crate) fn builtins_for_input( collaboration_modes_enabled: bool, personality_command_enabled: bool, + allow_elevate_sandbox: bool, ) -> Vec<(&'static str, SlashCommand)> { - let allow_elevate_sandbox = windows_degraded_sandbox_active(); built_in_slash_commands() .into_iter() .filter(|(_, cmd)| allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox) @@ -35,11 +27,16 @@ pub(crate) fn find_builtin_command( name: &str, collaboration_modes_enabled: bool, personality_command_enabled: bool, + allow_elevate_sandbox: bool, ) -> Option { - builtins_for_input(collaboration_modes_enabled, personality_command_enabled) - .into_iter() - .find(|(command_name, _)| *command_name == name) - .map(|(_, cmd)| cmd) + builtins_for_input( + collaboration_modes_enabled, + personality_command_enabled, + allow_elevate_sandbox, + ) + .into_iter() + .find(|(command_name, _)| *command_name == name) + .map(|(_, cmd)| cmd) } /// Whether any visible built-in fuzzily matches the provided prefix. @@ -47,8 +44,13 @@ pub(crate) fn has_builtin_prefix( name: &str, collaboration_modes_enabled: bool, personality_command_enabled: bool, + allow_elevate_sandbox: bool, ) -> bool { - builtins_for_input(collaboration_modes_enabled, personality_command_enabled) - .into_iter() - .any(|(command_name, _)| fuzzy_match(command_name, name).is_some()) + builtins_for_input( + collaboration_modes_enabled, + personality_command_enabled, + allow_elevate_sandbox, + ) + .into_iter() + .any(|(command_name, _)| fuzzy_match(command_name, name).is_some()) } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6f45ec25ce2..67a367ff2a2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -88,6 +88,8 @@ use codex_core::protocol::WarningEvent; use codex_core::protocol::WebSearchBeginEvent; use codex_core::protocol::WebSearchEndEvent; use codex_core::skills::model::SkillMetadata; +#[cfg(target_os = "windows")] +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_otel::OtelManager; use codex_protocol::ThreadId; use codex_protocol::account::PlanType; @@ -97,6 +99,8 @@ use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Personality; use codex_protocol::config_types::Settings; +#[cfg(target_os = "windows")] +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::local_image_label_text; use codex_protocol::parse_command::ParsedCommand; use codex_protocol::request_user_input::RequestUserInputEvent; @@ -2026,6 +2030,14 @@ impl ChatWidget { widget.config.features.enabled(Feature::CollaborationModes), ); widget.sync_personality_command_enabled(); + #[cfg(target_os = "windows")] + widget.bottom_pane.set_windows_degraded_sandbox_active( + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&widget.config), + WindowsSandboxLevel::RestrictedToken + ), + ); widget.update_collaboration_mode_indicator(); widget @@ -2277,6 +2289,14 @@ impl ChatWidget { widget.config.features.enabled(Feature::CollaborationModes), ); widget.sync_personality_command_enabled(); + #[cfg(target_os = "windows")] + widget.bottom_pane.set_windows_degraded_sandbox_active( + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&widget.config), + WindowsSandboxLevel::RestrictedToken + ), + ); widget.update_collaboration_mode_indicator(); widget @@ -2532,9 +2552,9 @@ impl ChatWidget { SlashCommand::ElevateSandbox => { #[cfg(target_os = "windows")] { - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox() - .is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let windows_degraded_sandbox_enabled = + matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); if !windows_degraded_sandbox_enabled || !codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED { @@ -3309,6 +3329,7 @@ impl ChatWidget { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(switch_model.clone()), effort: Some(Some(default_effort)), summary: None, @@ -3431,6 +3452,7 @@ impl ChatWidget { effort: None, summary: None, collaboration_mode: None, + windows_sandbox_level: None, personality: Some(personality), })); tx.send(AppEvent::UpdatePersonality(personality)); @@ -3700,6 +3722,7 @@ impl ChatWidget { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(model_for_action.clone()), effort: Some(effort_for_action), summary: None, @@ -3873,6 +3896,7 @@ impl ChatWidget { cwd: None, approval_policy: None, sandbox_policy: None, + windows_sandbox_level: None, model: Some(model.clone()), effort: Some(effort), summary: None, @@ -3913,8 +3937,10 @@ impl ChatWidget { let presets: Vec = builtin_approval_presets(); #[cfg(target_os = "windows")] - let windows_degraded_sandbox_enabled = codex_core::get_platform_sandbox().is_some() - && !codex_core::is_windows_elevated_sandbox_enabled(); + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + #[cfg(target_os = "windows")] + let windows_degraded_sandbox_enabled = + matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken); #[cfg(not(target_os = "windows"))] let windows_degraded_sandbox_enabled = false; @@ -3955,7 +3981,9 @@ impl ChatWidget { } else if preset.id == "auto" { #[cfg(target_os = "windows")] { - if codex_core::get_platform_sandbox().is_none() { + if WindowsSandboxLevel::from_config(&self.config) + == WindowsSandboxLevel::Disabled + { let preset_clone = preset.clone(); if codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED && codex_core::windows_sandbox::sandbox_setup_is_complete( @@ -4057,6 +4085,7 @@ impl ChatWidget { cwd: None, approval_policy: Some(approval), sandbox_policy: Some(sandbox_clone.clone()), + windows_sandbox_level: None, model: None, effort: None, summary: None, @@ -4560,7 +4589,7 @@ impl ChatWidget { #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self) { if self.config.forced_auto_mode_downgraded_on_windows - && codex_core::get_platform_sandbox().is_none() + && WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled && let Some(preset) = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -4620,7 +4649,7 @@ impl ChatWidget { pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> { #[cfg(target_os = "windows")] let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly) - || codex_core::get_platform_sandbox().is_some(); + || WindowsSandboxLevel::from_config(&self.config) != WindowsSandboxLevel::Disabled; self.config.sandbox_policy.set(policy)?; @@ -4654,6 +4683,19 @@ impl ChatWidget { self.refresh_model_display(); self.request_redraw(); } + #[cfg(target_os = "windows")] + if matches!( + feature, + Feature::WindowsSandbox | Feature::WindowsSandboxElevated + ) { + self.bottom_pane.set_windows_degraded_sandbox_active( + codex_core::windows_sandbox::ELEVATED_SANDBOX_NUX_ENABLED + && matches!( + WindowsSandboxLevel::from_config(&self.config), + WindowsSandboxLevel::RestrictedToken + ), + ); + } } pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 44aa0340cb4..76d56000104 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -92,16 +92,6 @@ use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::unbounded_channel; use toml::Value as TomlValue; -#[cfg(target_os = "windows")] -fn set_windows_sandbox_enabled(enabled: bool) { - codex_core::set_windows_sandbox_enabled(enabled); -} - -#[cfg(target_os = "windows")] -fn set_windows_elevated_sandbox_enabled(enabled: bool) { - codex_core::set_windows_elevated_sandbox_enabled(enabled); -} - async fn test_config() -> Config { // Use base defaults to avoid depending on host state. let codex_home = std::env::temp_dir(); @@ -3050,16 +3040,9 @@ async fn approvals_selection_popup_snapshot() { async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - let was_sandbox_enabled = codex_core::get_platform_sandbox().is_some(); - let was_elevated_enabled = codex_core::is_windows_elevated_sandbox_enabled(); - chat.config.notices.hide_full_access_warning = None; - chat.config.features.enable(Feature::WindowsSandbox); - chat.config - .features - .disable(Feature::WindowsSandboxElevated); - set_windows_sandbox_enabled(true); - set_windows_elevated_sandbox_enabled(false); + chat.set_feature_enabled(Feature::WindowsSandbox, true); + chat.set_feature_enabled(Feature::WindowsSandboxElevated, false); chat.open_approvals_popup(); @@ -3067,10 +3050,6 @@ async fn approvals_selection_popup_snapshot_windows_degraded_sandbox() { insta::with_settings!({ snapshot_suffix => "windows_degraded" }, { assert_snapshot!("approvals_selection_popup", popup); }); - - // Avoid leaking sandbox global state into other tests. - set_windows_sandbox_enabled(was_sandbox_enabled); - set_windows_elevated_sandbox_enabled(was_elevated_enabled); } #[tokio::test] @@ -3133,7 +3112,8 @@ async fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() { async fn startup_prompts_for_windows_sandbox_when_agent_requested() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None).await; - set_windows_sandbox_enabled(false); + chat.set_feature_enabled(Feature::WindowsSandbox, false); + chat.set_feature_enabled(Feature::WindowsSandboxElevated, false); chat.config.forced_auto_mode_downgraded_on_windows = true; chat.maybe_prompt_windows_sandbox_enable(); @@ -3151,8 +3131,6 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() { popup.contains("Stay in"), "expected startup prompt to offer staying in current mode: {popup}" ); - - set_windows_sandbox_enabled(true); } #[tokio::test] diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 0fac506d320..17b839f7a8d 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -26,13 +26,14 @@ use codex_core::config::resolve_oss_provider; use codex_core::config_loader::ConfigLoadError; use codex_core::config_loader::format_config_error_with_source; use codex_core::find_thread_path_by_id_str; -use codex_core::get_platform_sandbox; use codex_core::path_utils; use codex_core::protocol::AskForApproval; use codex_core::read_session_meta_line; use codex_core::terminal::Multiplexer; +use codex_core::windows_sandbox::WindowsSandboxLevelExt; use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::SandboxMode; +use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_utils_absolute_path::AbsolutePathBuf; @@ -816,7 +817,9 @@ async fn load_config_or_exit_with_fallback_cwd( /// or if the current cwd project is already trusted. If not, we need to /// show the trust screen. fn should_show_trust_screen(config: &Config) -> bool { - if cfg!(target_os = "windows") && get_platform_sandbox().is_none() { + if cfg!(target_os = "windows") + && WindowsSandboxLevel::from_config(config) == WindowsSandboxLevel::Disabled + { // If the experimental sandbox is not enabled, Native Windows cannot enforce sandboxed write access; skip the trust prompt entirely. return false; }