From 3d8177411c410b5ebdedb257d50e30229f76f115 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Wed, 20 May 2026 15:12:07 -0700 Subject: [PATCH 1/9] Constrain Windows sandbox requirements --- codex-rs/cloud-requirements/src/lib.rs | 16 ++ codex-rs/config/src/config_requirements.rs | 139 +++++++++++ codex-rs/config/src/lib.rs | 1 + .../core/src/config/config_loader_tests.rs | 3 + codex-rs/core/src/config/config_tests.rs | 43 ++++ codex-rs/core/src/config/mod.rs | 26 +- codex-rs/tui/src/app/event_dispatch.rs | 17 ++ codex-rs/tui/src/chatwidget.rs | 2 +- ...indows_sandbox_required_enable_prompt.snap | 12 + ...dows_sandbox_required_fallback_prompt.snap | 13 + codex-rs/tui/src/chatwidget/tests.rs | 1 - .../tui/src/chatwidget/tests/permissions.rs | 86 +++++++ .../src/chatwidget/windows_sandbox_prompts.rs | 223 +++++++++++------- codex-rs/tui/src/debug_config.rs | 2 + codex-rs/tui/src/lib.rs | 28 ++- 15 files changed, 522 insertions(+), 90 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_enable_prompt.snap create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_fallback_prompt.snap diff --git a/codex-rs/cloud-requirements/src/lib.rs b/codex-rs/cloud-requirements/src/lib.rs index b9740602dda3..5dfed61c5b9f 100644 --- a/codex-rs/cloud-requirements/src/lib.rs +++ b/codex-rs/cloud-requirements/src/lib.rs @@ -1220,6 +1220,7 @@ mod tests { allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1304,6 +1305,7 @@ mod tests { allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1339,6 +1341,7 @@ mod tests { allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1391,6 +1394,7 @@ mod tests { allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1572,6 +1576,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1654,6 +1659,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1734,6 +1740,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1942,6 +1949,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -1984,6 +1992,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2046,6 +2055,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2104,6 +2114,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2164,6 +2175,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2225,6 +2237,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2290,6 +2303,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2381,6 +2395,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, @@ -2418,6 +2433,7 @@ command = "sample-mcp" allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 8e2e21f0c4b2..06edeb4373ee 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -19,6 +19,7 @@ use crate::Constrained; use crate::ConstraintError; use crate::ManagedHooksRequirementsToml; use crate::mcp_types::AppToolApproval; +use crate::types::WindowsSandboxModeToml; #[derive(Debug, Clone, PartialEq, Eq)] pub enum RequirementSource { @@ -86,6 +87,7 @@ pub struct ConfigRequirements { pub approval_policy: ConstrainedWithSource, pub approvals_reviewer: ConstrainedWithSource, pub permission_profile: ConstrainedWithSource, + pub windows_sandbox_mode: ConstrainedWithSource>, pub web_search_mode: ConstrainedWithSource, pub allow_managed_hooks_only: Option>, pub computer_use: Option>, @@ -118,6 +120,10 @@ impl Default for ConfigRequirements { Constrained::allow_any(PermissionProfile::read_only()), /*source*/ None, ), + windows_sandbox_mode: ConstrainedWithSource::new( + Constrained::allow_any(/*initial_value*/ None), + /*source*/ None, + ), web_search_mode: ConstrainedWithSource::new( Constrained::allow_any(WebSearchMode::Cached), /*source*/ None, @@ -601,6 +607,17 @@ impl ComputerUseRequirementsToml { } } +#[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] +pub struct WindowsRequirementsToml { + pub allowed_sandbox_implementations: Option>, +} + +impl WindowsRequirementsToml { + pub fn is_empty(&self) -> bool { + self.allowed_sandbox_implementations.is_none() + } +} + #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct FeatureRequirementsToml { #[serde(flatten)] @@ -705,6 +722,7 @@ pub struct ConfigRequirementsToml { pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, pub computer_use: Option, + pub windows: Option, #[serde(rename = "features", alias = "feature_requirements")] pub feature_requirements: Option, pub hooks: Option, @@ -755,6 +773,7 @@ pub struct ConfigRequirementsWithSources { pub allowed_web_search_modes: Option>>, pub allow_managed_hooks_only: Option>, pub computer_use: Option>, + pub windows: Option>, pub feature_requirements: Option>, pub hooks: Option>, pub mcp_servers: Option>>, @@ -793,6 +812,7 @@ impl ConfigRequirementsWithSources { allowed_web_search_modes: _, allow_managed_hooks_only: _, computer_use: _, + windows: _, feature_requirements: _, hooks: _, mcp_servers: _, @@ -824,6 +844,7 @@ impl ConfigRequirementsWithSources { allowed_web_search_modes, allow_managed_hooks_only, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -853,6 +874,7 @@ impl ConfigRequirementsWithSources { allowed_web_search_modes, allow_managed_hooks_only, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -872,6 +894,7 @@ impl ConfigRequirementsWithSources { allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value), allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value), computer_use: computer_use.map(|sourced| sourced.value), + windows: windows.map(|sourced| sourced.value), feature_requirements: feature_requirements.map(|sourced| sourced.value), hooks: hooks.map(|sourced| sourced.value), mcp_servers: mcp_servers.map(|sourced| sourced.value), @@ -960,6 +983,10 @@ impl ConfigRequirementsToml { .computer_use .as_ref() .is_none_or(ComputerUseRequirementsToml::is_empty) + && self + .windows + .as_ref() + .is_none_or(WindowsRequirementsToml::is_empty) && self .feature_requirements .as_ref() @@ -999,6 +1026,7 @@ impl TryFrom for ConfigRequirements { allowed_web_search_modes, allow_managed_hooks_only, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -1108,6 +1136,45 @@ impl TryFrom for ConfigRequirements { /*source*/ None, ), }; + let windows_sandbox_mode = match windows { + Some(Sourced { + value: + WindowsRequirementsToml { + allowed_sandbox_implementations: Some(implementations), + }, + source: requirement_source, + }) => { + if implementations.is_empty() { + return Err(ConstraintError::empty_field( + "windows.allowed_sandbox_implementations", + )); + } + // Treat this as an allowlist, not an ordered preference list. If requirements + // force a fallback, prefer the default elevated sandbox whenever it is allowed. + let initial_value = if implementations.contains(&WindowsSandboxModeToml::Elevated) { + WindowsSandboxModeToml::Elevated + } else { + WindowsSandboxModeToml::Unelevated + }; + + let requirement_source_for_error = requirement_source.clone(); + let constrained = + Constrained::new(Some(initial_value), move |candidate| match candidate { + Some(candidate) if implementations.contains(candidate) => Ok(()), + _ => Err(ConstraintError::InvalidValue { + field_name: "windows.sandbox", + candidate: format!("{candidate:?}"), + allowed: format!("{implementations:?}"), + requirement_source: requirement_source_for_error.clone(), + }), + })?; + ConstrainedWithSource::new(constrained, Some(requirement_source)) + } + Some(_) | None => ConstrainedWithSource::new( + Constrained::allow_any(/*initial_value*/ None), + /*source*/ None, + ), + }; let exec_policy = match rules { Some(Sourced { value, source }) => { let policy = value.to_requirements_policy().map_err(|err| { @@ -1233,6 +1300,7 @@ impl TryFrom for ConfigRequirements { approval_policy, approvals_reviewer, permission_profile, + windows_sandbox_mode, web_search_mode, allow_managed_hooks_only, computer_use, @@ -1305,6 +1373,7 @@ mod tests { allowed_web_search_modes, allow_managed_hooks_only, computer_use, + windows, feature_requirements, hooks, mcp_servers, @@ -1328,6 +1397,7 @@ mod tests { allow_managed_hooks_only: allow_managed_hooks_only .map(|value| Sourced::new(value, RequirementSource::Unknown)), computer_use: computer_use.map(|value| Sourced::new(value, RequirementSource::Unknown)), + windows: windows.map(|value| Sourced::new(value, RequirementSource::Unknown)), feature_requirements: feature_requirements .map(|value| Sourced::new(value, RequirementSource::Unknown)), hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)), @@ -1425,6 +1495,7 @@ mod tests { allowed_web_search_modes: Some(allowed_web_search_modes.clone()), allow_managed_hooks_only: Some(true), computer_use: Some(computer_use.clone()), + windows: None, feature_requirements: Some(feature_requirements.clone()), hooks: None, mcp_servers: None, @@ -1460,6 +1531,7 @@ mod tests { enforce_source.clone(), )), computer_use: Some(Sourced::new(computer_use, enforce_source.clone())), + windows: None, feature_requirements: Some(Sourced::new( feature_requirements, enforce_source.clone(), @@ -1504,6 +1576,7 @@ mod tests { allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -1553,6 +1626,7 @@ mod tests { allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -2192,6 +2266,71 @@ allowed_approvals_reviewers = ["user"] Ok(()) } + #[test] + fn deserialize_allowed_windows_sandbox_implementations() -> Result<()> { + let toml_str = r#" + [windows] + allowed_sandbox_implementations = ["elevated"] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; + + assert_eq!( + requirements.windows_sandbox_mode.value(), + Some(WindowsSandboxModeToml::Elevated) + ); + assert!( + requirements + .windows_sandbox_mode + .can_set(&Some(WindowsSandboxModeToml::Elevated)) + .is_ok() + ); + assert!( + requirements + .windows_sandbox_mode + .can_set(&Some(WindowsSandboxModeToml::Unelevated)) + .is_err() + ); + assert!(requirements.windows_sandbox_mode.can_set(&None).is_err()); + + Ok(()) + } + + #[test] + fn empty_allowed_windows_sandbox_implementations_is_rejected() -> Result<()> { + let toml_str = r#" + [windows] + allowed_sandbox_implementations = [] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + + assert_eq!( + ConfigRequirements::try_from(with_unknown_source(config)), + Err(ConstraintError::EmptyField { + field_name: "windows.allowed_sandbox_implementations".to_string(), + }) + ); + + Ok(()) + } + + #[test] + fn allowed_windows_sandbox_implementations_prefer_elevated_fallback() -> Result<()> { + let toml_str = r#" + [windows] + allowed_sandbox_implementations = ["unelevated", "elevated"] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; + + assert_eq!( + requirements.windows_sandbox_mode.value(), + Some(WindowsSandboxModeToml::Elevated) + ); + + Ok(()) + } + #[test] fn deserialize_legacy_allowed_approvals_reviewer() -> Result<()> { let toml_str = r#" diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 32065e368230..d5e80b403a9d 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -62,6 +62,7 @@ pub use config_requirements::ResidencyRequirement; pub use config_requirements::SandboxModeRequirement; pub use config_requirements::Sourced; pub use config_requirements::WebSearchModeRequirement; +pub use config_requirements::WindowsRequirementsToml; pub use config_requirements::sandbox_mode_requirement_for_permission_profile; pub use constraint::Constrained; pub use constraint::ConstraintError; diff --git a/codex-rs/core/src/config/config_loader_tests.rs b/codex-rs/core/src/config/config_loader_tests.rs index 760b1f3fc590..e817024ec194 100644 --- a/codex-rs/core/src/config/config_loader_tests.rs +++ b/codex-rs/core/src/config/config_loader_tests.rs @@ -1099,6 +1099,7 @@ allowed_approval_policies = ["on-request"] allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -1158,6 +1159,7 @@ allowed_approval_policies = ["on-request"] allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -1366,6 +1368,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()> allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 19fd762da916..fcf111f549ab 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -8901,6 +8901,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]), allow_managed_hooks_only: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -9685,6 +9686,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s allowed_web_search_modes: None, allow_managed_hooks_only: None, computer_use: None, + windows: None, feature_requirements: None, hooks: None, mcp_servers: None, @@ -9712,6 +9714,47 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s Ok(()) } +#[tokio::test] +async fn windows_sandbox_mode_falls_back_when_disallowed_by_requirements() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"[windows] +sandbox = "unelevated" +"#, + )?; + + let requirements = codex_config::ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![ + codex_config::types::WindowsSandboxModeToml::Elevated, + ]), + }), + ..Default::default() + }; + + let config = ConfigBuilder::without_managed_config_for_tests() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cloud_requirements(CloudRequirementsLoader::new(async move { + Ok(Some(requirements)) + })) + .build() + .await?; + + assert_eq!( + config.permissions.windows_sandbox_mode, + Some(codex_config::types::WindowsSandboxModeToml::Elevated) + ); + assert!( + config.startup_warnings.iter().any(|warning| warning + .contains("Configured value for `windows.sandbox` is disallowed by requirements")), + "{:?}", + config.startup_warnings + ); + Ok(()) +} + #[tokio::test] async fn permission_profile_override_falls_back_when_disallowed_by_requirements() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 434969e2c5f4..2728859af05e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2426,6 +2426,7 @@ impl Config { approval_policy: mut constrained_approval_policy, approvals_reviewer: mut constrained_approvals_reviewer, permission_profile: mut constrained_permission_profile, + windows_sandbox_mode: mut constrained_windows_sandbox_mode, web_search_mode: mut constrained_web_search_mode, allow_managed_hooks_only: _, computer_use: _, @@ -2548,7 +2549,26 @@ impl Config { &mut startup_warnings, )?; let enable_network_proxy = features.enabled(Feature::NetworkProxy); - let windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile); + let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile); + let selected_windows_sandbox_mode = configured_windows_sandbox_mode.or_else(|| { + match WindowsSandboxLevel::from_features(&features) { + WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated), + WindowsSandboxLevel::RestrictedToken => Some(WindowsSandboxModeToml::Unelevated), + WindowsSandboxLevel::Disabled => None, + } + }); + apply_requirement_constrained_value( + "windows.sandbox", + selected_windows_sandbox_mode, + &mut constrained_windows_sandbox_mode, + &mut startup_warnings, + )?; + let effective_windows_sandbox_mode = *constrained_windows_sandbox_mode.get(); + let windows_sandbox_mode = if constrained_windows_sandbox_mode.source.is_some() { + effective_windows_sandbox_mode + } else { + configured_windows_sandbox_mode + }; let windows_sandbox_private_desktop = resolve_windows_sandbox_private_desktop(&cfg, &config_profile); let resolved_cwd = AbsolutePathBuf::try_from(normalize_for_native_workdir({ @@ -2607,10 +2627,10 @@ impl Config { )); } - let windows_sandbox_level = match windows_sandbox_mode { + let windows_sandbox_level = match effective_windows_sandbox_mode { Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated, Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken, - None => WindowsSandboxLevel::from_features(&features), + None => WindowsSandboxLevel::Disabled, }; let memories_root = memory_root(&codex_home); std::fs::create_dir_all(&memories_root)?; diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index ce0eb0ff936a..f379233f2661 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -1093,6 +1093,23 @@ impl App { } let profile = self.active_profile.as_deref(); let elevated_enabled = matches!(mode, WindowsSandboxEnableMode::Elevated); + let selected_mode = if elevated_enabled { + codex_config::types::WindowsSandboxModeToml::Elevated + } else { + codex_config::types::WindowsSandboxModeToml::Unelevated + }; + if !self.chat_widget.windows_sandbox_mode_allowed(selected_mode) { + tracing::warn!( + ?selected_mode, + "refusing to persist Windows sandbox mode disallowed by requirements" + ); + self.chat_widget.add_info_message( + "That Windows sandbox option is disallowed by requirements." + .to_string(), + /*hint*/ None, + ); + return Ok(AppRunControl::Continue); + } let builder = ConfigEditsBuilder::for_config(&self.config) .with_profile(profile) .set_windows_sandbox_mode(if elevated_enabled { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 0ba974d88e4e..8cdb6c096f1c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -257,7 +257,7 @@ fn queued_message_edit_hint_binding( use crate::app_event::AppEvent; use crate::app_event::ExitMode; use crate::app_event::RateLimitRefreshOrigin; -#[cfg(target_os = "windows")] +#[cfg(any(target_os = "windows", test))] use crate::app_event::WindowsSandboxEnableMode; use crate::app_event_sender::AppEventSender; use crate::auto_review_denials; diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_enable_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_enable_prompt.snap new file mode 100644 index 000000000000..c0c36c3942ef --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_enable_prompt.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/chatwidget/tests/permissions.rs +expression: "render_bottom_popup(&chat, 120)" +--- + Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control + network access. + Learn more + +› 1. Set up default sandbox (requires Administrator permissions) + 2. Quit + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_fallback_prompt.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_fallback_prompt.snap new file mode 100644 index 000000000000..c9195f7a039c --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__windows_sandbox_required_fallback_prompt.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/chatwidget/tests/permissions.rs +expression: popup +--- + Couldn't set up your sandbox with Administrator permissions + + Your organization requires the default sandbox before Codex can continue. + Learn more + +› 1. Try setting up admin sandbox again + 2. Quit + + Press enter to confirm or esc to go back diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 77aa742c0161..9b3841f37ed7 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -123,7 +123,6 @@ pub(super) use codex_config::ConfigLayerStack; pub(super) use codex_config::RequirementSource; pub(super) use codex_config::types::ApprovalsReviewer; pub(super) use codex_config::types::Notifications; -#[cfg(target_os = "windows")] pub(super) use codex_config::types::WindowsSandboxModeToml; pub(super) use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; pub(super) use codex_core_skills::model::SkillMetadata; diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 71483bd39a70..79c9f9086b02 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -223,6 +223,92 @@ async fn startup_prompts_for_windows_sandbox_when_agent_requested() { ); } +#[cfg(target_os = "windows")] +#[tokio::test] +async fn startup_windows_sandbox_prompt_blocks_disallowed_unelevated_fallback() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false); + chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); + chat.config.config_layer_stack = ConfigLayerStack::new( + Vec::new(), + codex_config::ConfigRequirements::default(), + codex_config::ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]), + }), + ..Default::default() + }, + ) + .expect("test config layer stack"); + + chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true); + + let popup = render_bottom_popup(&chat, /*width*/ 120); + assert!( + popup.contains("Your organization requires the default Codex agent sandbox"), + "expected required sandbox prompt copy: {popup}" + ); + assert!( + !popup.contains("Use non-admin sandbox"), + "expected required sandbox prompt to hide non-admin fallback: {popup}" + ); +} + +#[tokio::test] +async fn windows_sandbox_required_enable_prompt_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.config.config_layer_stack = ConfigLayerStack::new( + Vec::new(), + codex_config::ConfigRequirements::default(), + codex_config::ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]), + }), + ..Default::default() + }, + ) + .expect("test config layer stack"); + let preset = builtin_approval_presets() + .into_iter() + .find(|preset| preset.id == "auto") + .expect("auto preset"); + + chat.open_windows_sandbox_enable_prompt(preset); + + assert_chatwidget_snapshot!( + "windows_sandbox_required_enable_prompt", + render_bottom_popup(&chat, /*width*/ 120) + ); +} + +#[tokio::test] +async fn windows_sandbox_required_fallback_prompt_snapshot() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.config.config_layer_stack = ConfigLayerStack::new( + Vec::new(), + codex_config::ConfigRequirements::default(), + codex_config::ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]), + }), + ..Default::default() + }, + ) + .expect("test config layer stack"); + let preset = builtin_approval_presets() + .into_iter() + .find(|preset| preset.id == "auto") + .expect("auto preset"); + + chat.open_windows_sandbox_fallback_prompt(preset); + + let popup = render_bottom_popup(&chat, /*width*/ 120); + assert_chatwidget_snapshot!("windows_sandbox_required_fallback_prompt", popup); +} + #[cfg(target_os = "windows")] #[tokio::test] async fn startup_does_not_prompt_for_windows_sandbox_when_not_requested() { diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index d8ff59da4846..922c11e9b95f 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -3,6 +3,17 @@ use super::*; impl ChatWidget { + #[cfg(any(target_os = "windows", test))] + pub(crate) fn windows_sandbox_mode_allowed(&self, mode: WindowsSandboxModeToml) -> bool { + self.config + .config_layer_stack + .requirements_toml() + .windows + .as_ref() + .and_then(|windows| windows.allowed_sandbox_implementations.as_ref()) + .is_none_or(|implementations| implementations.contains(&mode)) + } + #[cfg(target_os = "windows")] pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec, usize, bool)> { if self @@ -172,7 +183,7 @@ impl ChatWidget { ) { } - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", test))] pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, preset: ApprovalPreset) { use ratatui_macros::line; @@ -229,11 +240,20 @@ impl ChatWidget { &[], ); + let allow_unelevated = + self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated); let mut header = ColumnRenderable::new(); header.push(*Box::new( - Paragraph::new(vec![ - line!["Set up the Codex agent sandbox to protect your files and control network access. Learn more "], - ]) + Paragraph::new(if allow_unelevated { + vec![ + line!["Set up the Codex agent sandbox to protect your files and control network access. Learn more "], + ] + } else { + vec![ + line!["Your organization requires the default Codex agent sandbox to continue. Set it up to protect your files and control network access."], + line!["Learn more "], + ] + }) .wrap(Wrap { trim: false }), )); @@ -241,24 +261,25 @@ impl ChatWidget { let legacy_otel = self.session_telemetry.clone(); let legacy_preset = preset.clone(); let quit_otel = self.session_telemetry.clone(); - let items = vec![ - SelectionItem { - name: "Set up default sandbox (requires Administrator permissions)".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - accept_otel.counter( - "codex.windows_sandbox.elevated_prompt_accept", - /*inc*/ 1, - &[], - ); - tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { - preset: preset.clone(), - }); - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { + let retry_preset = preset.clone(); + let mut items = vec![SelectionItem { + name: "Set up default sandbox (requires Administrator permissions)".to_string(), + description: None, + actions: vec![Box::new(move |tx| { + accept_otel.counter( + "codex.windows_sandbox.elevated_prompt_accept", + /*inc*/ 1, + &[], + ); + tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { + preset: preset.clone(), + }); + })], + dismiss_on_select: true, + ..Default::default() + }]; + if allow_unelevated { + items.push(SelectionItem { name: "Use non-admin sandbox (higher risk if prompt injected)".to_string(), description: None, actions: vec![Box::new(move |tx| { @@ -273,47 +294,62 @@ impl ChatWidget { })], dismiss_on_select: true, ..Default::default() - }, - SelectionItem { - name: "Quit".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - quit_otel.counter( - "codex.windows_sandbox.elevated_prompt_quit", - /*inc*/ 1, - &[], - ); - tx.send(AppEvent::Exit(ExitMode::ShutdownFirst)); - })], - dismiss_on_select: true, - ..Default::default() - }, - ]; + }); + } + items.push(SelectionItem { + name: "Quit".to_string(), + description: None, + actions: vec![Box::new(move |tx| { + quit_otel.counter( + "codex.windows_sandbox.elevated_prompt_quit", + /*inc*/ 1, + &[], + ); + tx.send(AppEvent::Exit(ExitMode::ShutdownFirst)); + })], + dismiss_on_select: true, + ..Default::default() + }); self.bottom_pane.show_selection_view(SelectionViewParams { title: None, footer_hint: Some(standard_popup_hint_line()), items, header: Box::new(header), + on_cancel: (!allow_unelevated).then(|| { + Box::new(move |tx: &AppEventSender| { + tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { + preset: retry_preset.clone(), + }); + }) as _ + }), ..Default::default() }); } - #[cfg(not(target_os = "windows"))] + #[cfg(all(not(target_os = "windows"), not(test)))] pub(crate) fn open_windows_sandbox_enable_prompt(&mut self, _preset: ApprovalPreset) {} - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "windows", test))] pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, preset: ApprovalPreset) { use ratatui_macros::line; + let allow_unelevated = + self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated); let mut lines = Vec::new(); lines.push(line![ "Couldn't set up your sandbox with Administrator permissions".bold() ]); lines.push(line![""]); - lines.push(line![ - "You can still use Codex in a non-admin sandbox. It carries greater risk if prompt injected." - ]); + if allow_unelevated { + lines.push(line![ + "You can still use Codex in a non-admin sandbox. It carries greater risk if prompt injected." + ]); + } else { + lines.push(line![ + "Your organization requires the default sandbox before Codex can continue." + ]); + } lines.push(line![ "Learn more " ]); @@ -323,29 +359,30 @@ impl ChatWidget { let elevated_preset = preset.clone(); let legacy_preset = preset; + let retry_preset = elevated_preset.clone(); let quit_otel = self.session_telemetry.clone(); - let items = vec![ - SelectionItem { - name: "Try setting up admin sandbox again".to_string(), - description: None, - actions: vec![Box::new({ - let otel = self.session_telemetry.clone(); - let preset = elevated_preset; - move |tx| { - otel.counter( - "codex.windows_sandbox.fallback_retry_elevated", - /*inc*/ 1, - &[], - ); - tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { - preset: preset.clone(), - }); - } - })], - dismiss_on_select: true, - ..Default::default() - }, - SelectionItem { + let mut items = vec![SelectionItem { + name: "Try setting up admin sandbox again".to_string(), + description: None, + actions: vec![Box::new({ + let otel = self.session_telemetry.clone(); + let preset = elevated_preset; + move |tx| { + otel.counter( + "codex.windows_sandbox.fallback_retry_elevated", + /*inc*/ 1, + &[], + ); + tx.send(AppEvent::BeginWindowsSandboxElevatedSetup { + preset: preset.clone(), + }); + } + })], + dismiss_on_select: true, + ..Default::default() + }]; + if allow_unelevated { + items.push(SelectionItem { name: "Use Codex with non-admin sandbox".to_string(), description: None, actions: vec![Box::new({ @@ -364,39 +401,59 @@ impl ChatWidget { })], dismiss_on_select: true, ..Default::default() - }, - SelectionItem { - name: "Quit".to_string(), - description: None, - actions: vec![Box::new(move |tx| { - quit_otel.counter( - "codex.windows_sandbox.fallback_prompt_quit", - /*inc*/ 1, - &[], - ); - tx.send(AppEvent::Exit(ExitMode::ShutdownFirst)); - })], - dismiss_on_select: true, - ..Default::default() - }, - ]; + }); + } + items.push(SelectionItem { + name: "Quit".to_string(), + description: None, + actions: vec![Box::new(move |tx| { + quit_otel.counter( + "codex.windows_sandbox.fallback_prompt_quit", + /*inc*/ 1, + &[], + ); + tx.send(AppEvent::Exit(ExitMode::ShutdownFirst)); + })], + dismiss_on_select: true, + ..Default::default() + }); self.bottom_pane.show_selection_view(SelectionViewParams { title: None, footer_hint: Some(standard_popup_hint_line()), items, header: Box::new(header), + on_cancel: (!allow_unelevated).then(|| { + Box::new(move |tx: &AppEventSender| { + tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { + preset: retry_preset.clone(), + }); + }) as _ + }), ..Default::default() }); } - #[cfg(not(target_os = "windows"))] + #[cfg(all(not(target_os = "windows"), not(test)))] pub(crate) fn open_windows_sandbox_fallback_prompt(&mut self, _preset: ApprovalPreset) {} #[cfg(target_os = "windows")] pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) { + let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); + let setup_is_required = windows_sandbox_level == WindowsSandboxLevel::Disabled + || (windows_sandbox_level == WindowsSandboxLevel::Elevated + && self + .config + .config_layer_stack + .requirements_toml() + .windows + .as_ref() + .is_some_and(|windows| windows.allowed_sandbox_implementations.is_some()) + && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( + self.config.codex_home.as_path(), + )); if show_now - && WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Disabled + && setup_is_required && let Some(preset) = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 2075636e3d23..0e0fa2680749 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -701,6 +701,7 @@ mod tests { allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]), allow_managed_hooks_only: Some(true), computer_use: None, + windows: None, guardian_policy_config: Some("Use the managed guardian policy.".to_string()), feature_requirements: Some(FeatureRequirementsToml { entries: BTreeMap::from([("guardian_approval".to_string(), true)]), @@ -916,6 +917,7 @@ approval_policy = "never" allowed_web_search_modes: Some(Vec::new()), allow_managed_hooks_only: None, computer_use: None, + windows: None, guardian_policy_config: None, feature_requirements: None, hooks: None, diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 40cd121c7135..a2a21d65635c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1664,9 +1664,33 @@ async fn run_ratatui_app( set_default_client_residency_requirement(config.enforce_residency.value()); let active_profile = config.active_profile.clone(); let should_show_trust_screen = should_show_trust_screen(&config); + #[cfg(target_os = "windows")] + let windows_sandbox_level = WindowsSandboxLevel::from_config(&config); + #[cfg(target_os = "windows")] + let required_elevated_sandbox_needs_setup = windows_sandbox_level + == WindowsSandboxLevel::Elevated + && config + .config_layer_stack + .requirements_toml() + .windows + .as_ref() + .is_some_and(|windows| windows.allowed_sandbox_implementations.is_some()) + && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( + config.codex_home.as_path(), + ); let should_prompt_windows_sandbox_nux_at_startup = cfg!(target_os = "windows") - && trust_decision_was_made - && WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled; + && ((trust_decision_was_made + && WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled) + || { + #[cfg(target_os = "windows")] + { + required_elevated_sandbox_needs_setup + } + #[cfg(not(target_os = "windows"))] + { + false + } + }); let Cli { prompt, From 75c0ab96f6da3ed5913a9117bf841a04d0510665 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Wed, 20 May 2026 17:05:52 -0700 Subject: [PATCH 2/9] Guard Windows sandbox setup requirements --- .../windows_sandbox_processor.rs | 83 +++++++++++-------- codex-rs/tui/src/chatwidget.rs | 4 +- .../tui/src/chatwidget/tests/permissions.rs | 33 ++++++++ .../src/chatwidget/windows_sandbox_prompts.rs | 35 +++++--- 4 files changed, 104 insertions(+), 51 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index 2392cc807842..261560b3bc9c 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -41,6 +41,41 @@ impl WindowsSandboxRequestProcessor { request_id: &ConnectionRequestId, params: WindowsSandboxSetupStartParams, ) -> Result<(), JSONRPCErrorError> { + // Validate requirements before acknowledging setup so callers do not get a + // `started` response for a Windows sandbox mode that cannot be persisted. + let command_cwd = params + .cwd + .map(PathBuf::from) + .unwrap_or_else(|| self.config.cwd.to_path_buf()); + let config = self + .config_manager + .load_for_cwd( + /*request_overrides*/ None, + ConfigOverrides { + cwd: Some(command_cwd.clone()), + ..Default::default() + }, + Some(command_cwd.clone()), + ) + .await + .map_err(|err| config_load_error(&err))?; + let (mode, requested_mode) = match params.mode { + WindowsSandboxSetupMode::Elevated => ( + CoreWindowsSandboxSetupMode::Elevated, + codex_config::types::WindowsSandboxModeToml::Elevated, + ), + WindowsSandboxSetupMode::Unelevated => ( + CoreWindowsSandboxSetupMode::Unelevated, + codex_config::types::WindowsSandboxModeToml::Unelevated, + ), + }; + config + .config_layer_stack + .requirements() + .windows_sandbox_mode + .can_set(&Some(requested_mode)) + .map_err(|err| invalid_request(format!("invalid Windows sandbox setup mode: {err}")))?; + self.outgoing .send_response( request_id.clone(), @@ -48,47 +83,23 @@ impl WindowsSandboxRequestProcessor { ) .await; - let mode = match params.mode { - WindowsSandboxSetupMode::Elevated => CoreWindowsSandboxSetupMode::Elevated, - WindowsSandboxSetupMode::Unelevated => CoreWindowsSandboxSetupMode::Unelevated, - }; - let config = Arc::clone(&self.config); - let config_manager = self.config_manager.clone(); - let command_cwd = params - .cwd - .map(PathBuf::from) - .unwrap_or_else(|| config.cwd.to_path_buf()); let outgoing = Arc::clone(&self.outgoing); let connection_id = request_id.connection_id; tokio::spawn(async move { - let derived_config = config_manager - .load_for_cwd( - /*request_overrides*/ None, - ConfigOverrides { - cwd: Some(command_cwd.clone()), - ..Default::default() - }, - Some(command_cwd.clone()), - ) - .await; - let setup_result = match derived_config { - Ok(config) => { - let setup_request = WindowsSandboxSetupRequest { - mode, - policy: config - .permissions - .legacy_sandbox_policy(config.cwd.as_path()), - policy_cwd: config.cwd.to_path_buf(), - command_cwd, - env_map: std::env::vars().collect(), - codex_home: config.codex_home.to_path_buf(), - active_profile: config.active_profile.clone(), - }; - codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await - } - Err(err) => Err(err.into()), + let setup_request = WindowsSandboxSetupRequest { + mode, + policy: config + .permissions + .legacy_sandbox_policy(config.cwd.as_path()), + policy_cwd: config.cwd.to_path_buf(), + command_cwd, + env_map: std::env::vars().collect(), + codex_home: config.codex_home.to_path_buf(), + active_profile: config.active_profile.clone(), }; + let setup_result = + codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await; let notification = WindowsSandboxSetupCompletedNotification { mode: match mode { CoreWindowsSandboxSetupMode::Elevated => WindowsSandboxSetupMode::Elevated, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 8cdb6c096f1c..9b08af3b60a7 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -62,7 +62,7 @@ use crate::legacy_core::config::Config; use crate::legacy_core::config::Constrained; use crate::legacy_core::config::ConstraintResult; use crate::legacy_core::config::PermissionProfileSnapshot; -#[cfg(target_os = "windows")] +#[cfg(any(target_os = "windows", test))] use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::mention_codec::LinkedMention; use crate::mention_codec::encode_history_mentions; @@ -152,7 +152,7 @@ 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")] +#[cfg(any(target_os = "windows", test))] use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 79c9f9086b02..3cf50cd37549 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -283,6 +283,39 @@ async fn windows_sandbox_required_enable_prompt_snapshot() { ); } +#[tokio::test] +async fn windows_sandbox_required_enable_prompt_reopens_on_cancel_when_unelevated_allowed() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated); + chat.config.config_layer_stack = ConfigLayerStack::new( + Vec::new(), + codex_config::ConfigRequirements::default(), + codex_config::ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![ + WindowsSandboxModeToml::Elevated, + WindowsSandboxModeToml::Unelevated, + ]), + }), + ..Default::default() + }, + ) + .expect("test config layer stack"); + let preset = builtin_approval_presets() + .into_iter() + .find(|preset| preset.id == "auto") + .expect("auto preset"); + + chat.open_windows_sandbox_enable_prompt(preset); + chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + + assert!(matches!( + rx.try_recv(), + Ok(AppEvent::OpenWindowsSandboxEnablePrompt { .. }) + )); +} + #[tokio::test] async fn windows_sandbox_required_fallback_prompt_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index 922c11e9b95f..7d4d818d8296 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -14,6 +14,21 @@ impl ChatWidget { .is_none_or(|implementations| implementations.contains(&mode)) } + #[cfg(any(target_os = "windows", test))] + fn elevated_windows_sandbox_setup_required(&self) -> bool { + WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Elevated + && self + .config + .config_layer_stack + .requirements_toml() + .windows + .as_ref() + .is_some_and(|windows| windows.allowed_sandbox_implementations.is_some()) + && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( + self.config.codex_home.as_path(), + ) + } + #[cfg(target_os = "windows")] pub(crate) fn world_writable_warning_details(&self) -> Option<(Vec, usize, bool)> { if self @@ -242,6 +257,8 @@ impl ChatWidget { let allow_unelevated = self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated); + let setup_choice_is_required = + !allow_unelevated || self.elevated_windows_sandbox_setup_required(); let mut header = ColumnRenderable::new(); header.push(*Box::new( Paragraph::new(if allow_unelevated { @@ -316,7 +333,7 @@ impl ChatWidget { footer_hint: Some(standard_popup_hint_line()), items, header: Box::new(header), - on_cancel: (!allow_unelevated).then(|| { + on_cancel: setup_choice_is_required.then(|| { Box::new(move |tx: &AppEventSender| { tx.send(AppEvent::OpenWindowsSandboxEnablePrompt { preset: retry_preset.clone(), @@ -336,6 +353,8 @@ impl ChatWidget { let allow_unelevated = self.windows_sandbox_mode_allowed(WindowsSandboxModeToml::Unelevated); + let setup_choice_is_required = + !allow_unelevated || self.elevated_windows_sandbox_setup_required(); let mut lines = Vec::new(); lines.push(line![ "Couldn't set up your sandbox with Administrator permissions".bold() @@ -423,7 +442,7 @@ impl ChatWidget { footer_hint: Some(standard_popup_hint_line()), items, header: Box::new(header), - on_cancel: (!allow_unelevated).then(|| { + on_cancel: setup_choice_is_required.then(|| { Box::new(move |tx: &AppEventSender| { tx.send(AppEvent::OpenWindowsSandboxFallbackPrompt { preset: retry_preset.clone(), @@ -441,17 +460,7 @@ impl ChatWidget { pub(crate) fn maybe_prompt_windows_sandbox_enable(&mut self, show_now: bool) { let windows_sandbox_level = WindowsSandboxLevel::from_config(&self.config); let setup_is_required = windows_sandbox_level == WindowsSandboxLevel::Disabled - || (windows_sandbox_level == WindowsSandboxLevel::Elevated - && self - .config - .config_layer_stack - .requirements_toml() - .windows - .as_ref() - .is_some_and(|windows| windows.allowed_sandbox_implementations.is_some()) - && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( - self.config.codex_home.as_path(), - )); + || self.elevated_windows_sandbox_setup_required(); if show_now && setup_is_required && let Some(preset) = builtin_approval_presets() From 302b767fd9b1e920de7adc3d1898abf1e9a032f5 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Wed, 20 May 2026 17:12:32 -0700 Subject: [PATCH 3/9] Clarify Windows sandbox fallback comment --- codex-rs/config/src/config_requirements.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 06edeb4373ee..01c4ee29b0a3 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -1149,8 +1149,7 @@ impl TryFrom for ConfigRequirements { "windows.allowed_sandbox_implementations", )); } - // Treat this as an allowlist, not an ordered preference list. If requirements - // force a fallback, prefer the default elevated sandbox whenever it is allowed. + // Prefer elevated when both Windows sandbox implementations are allowed. let initial_value = if implementations.contains(&WindowsSandboxModeToml::Elevated) { WindowsSandboxModeToml::Elevated } else { From 1ce5380282758ef9c4d23b61a75f19c34594d7bc Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Wed, 20 May 2026 17:32:34 -0700 Subject: [PATCH 4/9] Simplify Windows sandbox requirement checks --- .../windows_sandbox_processor.rs | 65 ++++++++++++++++ .../tui/src/chatwidget/tests/permissions.rs | 76 +++++++------------ .../src/chatwidget/windows_sandbox_prompts.rs | 17 ++--- codex-rs/tui/src/lib.rs | 8 +- 4 files changed, 106 insertions(+), 60 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index 261560b3bc9c..4d1fdef6fd83 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -154,6 +154,71 @@ fn determine_windows_sandbox_readiness_from_state( #[cfg(test)] mod tests { use super::*; + use crate::error_code::INVALID_REQUEST_ERROR_CODE; + use codex_config::CloudRequirementsLoader; + use codex_config::ConfigRequirementsToml; + use codex_config::LoaderOverrides; + use codex_config::types::WindowsSandboxModeToml; + + #[tokio::test] + async fn windows_sandbox_setup_start_rejects_disallowed_mode() { + let codex_home = tempfile::tempdir().expect("tempdir"); + let config = codex_core::config::ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .build() + .await + .expect("config"); + let config_manager = ConfigManager::new_for_tests( + codex_home.path().to_path_buf(), + Vec::new(), + LoaderOverrides::without_managed_config_for_tests(), + CloudRequirementsLoader::new(async { + Ok(Some(ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![ + WindowsSandboxModeToml::Elevated, + ]), + }), + ..Default::default() + })) + }), + ); + let (outgoing_tx, mut outgoing_rx) = tokio::sync::mpsc::channel(1); + let processor = WindowsSandboxRequestProcessor::new( + Arc::new(OutgoingMessageSender::new( + outgoing_tx, + codex_analytics::AnalyticsEventsClient::disabled(), + )), + Arc::new(config), + config_manager, + ); + + let err = processor + .windows_sandbox_setup_start_inner( + &ConnectionRequestId { + connection_id: ConnectionId(1), + request_id: RequestId::Integer(1), + }, + WindowsSandboxSetupStartParams { + mode: WindowsSandboxSetupMode::Unelevated, + cwd: None, + }, + ) + .await + .expect_err("unelevated setup should be rejected"); + + assert_eq!(err.code, INVALID_REQUEST_ERROR_CODE); + assert!( + err.message.contains("invalid Windows sandbox setup mode"), + "{err:?}" + ); + assert!( + outgoing_rx.try_recv().is_err(), + "disallowed setup should not send a started response" + ); + } #[test] fn determine_windows_sandbox_readiness_reports_not_configured_when_disabled() { diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 3cf50cd37549..e0504d743c60 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -46,6 +46,25 @@ fn app_server_workspace_write_profile(extra_root: AbsolutePathBuf) -> Permission } } +fn windows_sandbox_requirements_stack( + allowed_sandbox_implementations: Vec, +) -> ConfigLayerStack { + let requirements_toml = codex_config::ConfigRequirementsToml { + windows: Some(codex_config::WindowsRequirementsToml { + allowed_sandbox_implementations: Some(allowed_sandbox_implementations), + }), + ..Default::default() + }; + let mut requirements_with_sources = codex_config::ConfigRequirementsWithSources::default(); + requirements_with_sources + .merge_unset_fields(RequirementSource::Unknown, requirements_toml.clone()); + let requirements = codex_config::ConfigRequirements::try_from(requirements_with_sources) + .expect("windows sandbox requirements"); + + ConfigLayerStack::new(Vec::new(), requirements, requirements_toml) + .expect("test config layer stack") +} + #[tokio::test] async fn approvals_selection_popup_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -230,17 +249,8 @@ async fn startup_windows_sandbox_prompt_blocks_disallowed_unelevated_fallback() chat.set_feature_enabled(Feature::WindowsSandbox, /*enabled*/ false); chat.set_feature_enabled(Feature::WindowsSandboxElevated, /*enabled*/ false); - chat.config.config_layer_stack = ConfigLayerStack::new( - Vec::new(), - codex_config::ConfigRequirements::default(), - codex_config::ConfigRequirementsToml { - windows: Some(codex_config::WindowsRequirementsToml { - allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]), - }), - ..Default::default() - }, - ) - .expect("test config layer stack"); + chat.config.config_layer_stack = + windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]); chat.maybe_prompt_windows_sandbox_enable(/*show_now*/ true); @@ -259,17 +269,8 @@ async fn startup_windows_sandbox_prompt_blocks_disallowed_unelevated_fallback() async fn windows_sandbox_required_enable_prompt_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.config.config_layer_stack = ConfigLayerStack::new( - Vec::new(), - codex_config::ConfigRequirements::default(), - codex_config::ConfigRequirementsToml { - windows: Some(codex_config::WindowsRequirementsToml { - allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]), - }), - ..Default::default() - }, - ) - .expect("test config layer stack"); + chat.config.config_layer_stack = + windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]); let preset = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -288,20 +289,10 @@ async fn windows_sandbox_required_enable_prompt_reopens_on_cancel_when_unelevate let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated); - chat.config.config_layer_stack = ConfigLayerStack::new( - Vec::new(), - codex_config::ConfigRequirements::default(), - codex_config::ConfigRequirementsToml { - windows: Some(codex_config::WindowsRequirementsToml { - allowed_sandbox_implementations: Some(vec![ - WindowsSandboxModeToml::Elevated, - WindowsSandboxModeToml::Unelevated, - ]), - }), - ..Default::default() - }, - ) - .expect("test config layer stack"); + chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![ + WindowsSandboxModeToml::Elevated, + WindowsSandboxModeToml::Unelevated, + ]); let preset = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") @@ -320,17 +311,8 @@ async fn windows_sandbox_required_enable_prompt_reopens_on_cancel_when_unelevate async fn windows_sandbox_required_fallback_prompt_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.config.config_layer_stack = ConfigLayerStack::new( - Vec::new(), - codex_config::ConfigRequirements::default(), - codex_config::ConfigRequirementsToml { - windows: Some(codex_config::WindowsRequirementsToml { - allowed_sandbox_implementations: Some(vec![WindowsSandboxModeToml::Elevated]), - }), - ..Default::default() - }, - ) - .expect("test config layer stack"); + chat.config.config_layer_stack = + windows_sandbox_requirements_stack(vec![WindowsSandboxModeToml::Elevated]); let preset = builtin_approval_presets() .into_iter() .find(|preset| preset.id == "auto") diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index 7d4d818d8296..fe22c7f39f15 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -7,11 +7,10 @@ impl ChatWidget { pub(crate) fn windows_sandbox_mode_allowed(&self, mode: WindowsSandboxModeToml) -> bool { self.config .config_layer_stack - .requirements_toml() - .windows - .as_ref() - .and_then(|windows| windows.allowed_sandbox_implementations.as_ref()) - .is_none_or(|implementations| implementations.contains(&mode)) + .requirements() + .windows_sandbox_mode + .can_set(&Some(mode)) + .is_ok() } #[cfg(any(target_os = "windows", test))] @@ -20,10 +19,10 @@ impl ChatWidget { && self .config .config_layer_stack - .requirements_toml() - .windows - .as_ref() - .is_some_and(|windows| windows.allowed_sandbox_implementations.is_some()) + .requirements() + .windows_sandbox_mode + .source + .is_some() && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( self.config.codex_home.as_path(), ) diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index a2a21d65635c..a701f81048a5 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -1671,10 +1671,10 @@ async fn run_ratatui_app( == WindowsSandboxLevel::Elevated && config .config_layer_stack - .requirements_toml() - .windows - .as_ref() - .is_some_and(|windows| windows.allowed_sandbox_implementations.is_some()) + .requirements() + .windows_sandbox_mode + .source + .is_some() && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( config.codex_home.as_path(), ); From 135603fa00c19e039c066f01b5993703fcc122ce Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Thu, 21 May 2026 15:59:37 -0700 Subject: [PATCH 5/9] Block required Windows sandbox setup --- codex-rs/tui/src/app/event_dispatch.rs | 35 ++++++++++- codex-rs/tui/src/chatwidget/input_restore.rs | 7 +++ codex-rs/tui/src/chatwidget/session_flow.rs | 8 +-- .../tui/src/chatwidget/tests/permissions.rs | 60 +++++++++++++++++++ .../src/chatwidget/windows_sandbox_prompts.rs | 2 +- 5 files changed, 102 insertions(+), 10 deletions(-) diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 23eb716a7247..c2d4e8674543 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -885,6 +885,19 @@ impl App { preset, profile_selection, } => { + #[cfg(any(target_os = "windows", test))] + if !self.chat_widget.windows_sandbox_mode_allowed( + codex_config::types::WindowsSandboxModeToml::Elevated, + ) { + tracing::warn!( + "refusing to set up elevated Windows sandbox mode disallowed by requirements" + ); + self.chat_widget.add_info_message( + "That Windows sandbox option is disallowed by requirements.".to_string(), + /*hint*/ None, + ); + return Ok(AppRunControl::Continue); + } #[cfg(target_os = "windows")] { let permission_profile = match self @@ -1009,6 +1022,19 @@ impl App { preset, profile_selection, } => { + #[cfg(any(target_os = "windows", test))] + if !self.chat_widget.windows_sandbox_mode_allowed( + codex_config::types::WindowsSandboxModeToml::Unelevated, + ) { + tracing::warn!( + "refusing to set up unelevated Windows sandbox mode disallowed by requirements" + ); + self.chat_widget.add_info_message( + "That Windows sandbox option is disallowed by requirements.".to_string(), + /*hint*/ None, + ); + return Ok(AppRunControl::Continue); + } #[cfg(target_os = "windows")] { let permission_profile = match self @@ -1248,7 +1274,9 @@ impl App { /*personality*/ None, ), )); - self.apply_permission_profile_selection(selection).await; + if self.apply_permission_profile_selection(selection).await { + self.chat_widget.submit_initial_user_message_if_pending(); + } let _ = mode; self.chat_widget.add_plain_history_lines(vec![ Line::from(vec!["• ".dim(), "Sandbox ready".into()]), @@ -1574,6 +1602,7 @@ impl App { Some(RuntimePermissionProfileOverride::from_config(&self.config)); self.sync_active_thread_permission_settings_to_cached_session() .await; + self.chat_widget.submit_initial_user_message_if_pending(); // If a managed filesystem sandbox is active, run the Windows // world-writable scan. @@ -1608,7 +1637,9 @@ impl App { } } AppEvent::SelectPermissionProfile(selection) => { - self.apply_permission_profile_selection(selection).await; + if self.apply_permission_profile_selection(selection).await { + self.chat_widget.submit_initial_user_message_if_pending(); + } } AppEvent::UpdateApprovalsReviewer(policy) => { self.config.approvals_reviewer = policy; diff --git a/codex-rs/tui/src/chatwidget/input_restore.rs b/codex-rs/tui/src/chatwidget/input_restore.rs index b7e8ee72e0c3..db28f4810e8f 100644 --- a/codex-rs/tui/src/chatwidget/input_restore.rs +++ b/codex-rs/tui/src/chatwidget/input_restore.rs @@ -8,6 +8,13 @@ impl ChatWidget { } pub(crate) fn submit_initial_user_message_if_pending(&mut self) { + if self.suppress_initial_user_message_submit { + return; + } + #[cfg(any(target_os = "windows", test))] + if self.elevated_windows_sandbox_setup_required() { + return; + } if let Some(user_message) = self.initial_user_message.take() { self.submit_user_message(user_message); } diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs index 01a6b7d578f5..6eb5910afe1f 100644 --- a/codex-rs/tui/src/chatwidget/session_flow.rs +++ b/codex-rs/tui/src/chatwidget/session_flow.rs @@ -135,13 +135,7 @@ impl ChatWidget { if self.connectors_enabled() { self.prefetch_connectors(); } - if let Some(user_message) = self.initial_user_message.take() { - if self.suppress_initial_user_message_submit { - self.initial_user_message = Some(user_message); - } else { - self.submit_user_message(user_message); - } - } + self.submit_initial_user_message_if_pending(); if display == SessionConfiguredDisplay::Normal && let Some(forked_from_id) = forked_from_id { diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index b7215e133e38..7e22e63e0a05 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -309,6 +309,66 @@ async fn windows_sandbox_required_enable_prompt_reopens_on_cancel_when_unelevate )); } +#[tokio::test] +async fn required_windows_sandbox_setup_defers_configured_initial_prompt() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let initial_prompt = "fix required sandbox startup".to_string(); + + chat.config.permissions.windows_sandbox_mode = Some(WindowsSandboxModeToml::Elevated); + chat.config.config_layer_stack = windows_sandbox_requirements_stack(vec![ + WindowsSandboxModeToml::Elevated, + WindowsSandboxModeToml::Unelevated, + ]); + chat.initial_user_message = + create_initial_user_message(Some(initial_prompt.clone()), Vec::new(), Vec::new()); + + chat.handle_thread_session(crate::session_state::ThreadSessionState { + thread_id: ThreadId::new(), + forked_from_id: None, + fork_parent_title: None, + thread_name: None, + model: "gpt-test".to_string(), + model_provider_id: "test-provider".to_string(), + service_tier: None, + approval_policy: AskForApproval::OnRequest, + approvals_reviewer: ApprovalsReviewer::User, + permission_profile: PermissionProfile::workspace_write(), + active_permission_profile: None, + cwd: test_project_path().abs(), + runtime_workspace_roots: Vec::new(), + instruction_source_paths: Vec::new(), + reasoning_effort: None, + collaboration_mode: None, + personality: None, + message_history: None, + network_proxy: None, + rollout_path: Some(PathBuf::new()), + }); + drain_insert_history(&mut rx); + + assert!(chat.initial_user_message.is_some()); + while let Ok(op) = op_rx.try_recv() { + assert!( + !matches!(op, Op::UserTurn { .. }), + "required sandbox setup should hold the configured initial prompt" + ); + } + + chat.set_windows_sandbox_mode(Some(WindowsSandboxModeToml::Unelevated)); + chat.submit_initial_user_message_if_pending(); + + let Op::UserTurn { items, .. } = next_submit_op(&mut op_rx) else { + panic!("expected initial prompt submission after setup is no longer required"); + }; + assert_eq!( + items, + vec![UserInput::Text { + text: initial_prompt, + text_elements: Vec::new(), + }] + ); +} + #[tokio::test] async fn windows_sandbox_required_fallback_prompt_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs index 7c7d28ef8544..c3e3fd76fef0 100644 --- a/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs +++ b/codex-rs/tui/src/chatwidget/windows_sandbox_prompts.rs @@ -14,7 +14,7 @@ impl ChatWidget { } #[cfg(any(target_os = "windows", test))] - fn elevated_windows_sandbox_setup_required(&self) -> bool { + pub(super) fn elevated_windows_sandbox_setup_required(&self) -> bool { WindowsSandboxLevel::from_config(&self.config) == WindowsSandboxLevel::Elevated && self .config From fe4e7caf77cbe34328b63a50fac3247b0ce9f6af Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Thu, 21 May 2026 16:26:32 -0700 Subject: [PATCH 6/9] Clarify Windows sandbox startup checks --- codex-rs/core/src/config/mod.rs | 2 ++ codex-rs/tui/src/lib.rs | 27 +++++++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 224b90763b88..84d8e4eaa77d 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -2617,6 +2617,8 @@ impl Config { )?; let enable_network_proxy = features.enabled(Feature::NetworkProxy); let configured_windows_sandbox_mode = resolve_windows_sandbox_mode(&cfg, &config_profile); + // Keep the configured mode separate so a requirement-constrained mode + // does not look like it was explicitly selected in config. let selected_windows_sandbox_mode = configured_windows_sandbox_mode.or_else(|| { match WindowsSandboxLevel::from_features(&features) { WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated), diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 3d84bce00de2..39a920e7f0b0 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -11,6 +11,7 @@ use crate::legacy_core::config::load_config_as_toml_with_cli_and_load_options; use crate::legacy_core::config::resolve_oss_provider; use crate::legacy_core::config::resolve_profile_v2_config_path; use crate::legacy_core::format_exec_policy_error_with_source; +#[cfg(target_os = "windows")] use crate::legacy_core::windows_sandbox::WindowsSandboxLevelExt; use crate::session_resume::ResolveCwdOutcome; use crate::session_resume::resolve_cwd_for_resume_or_fork; @@ -51,6 +52,7 @@ use codex_login::enforce_login_restrictions; use codex_protocol::ThreadId; use codex_protocol::config_types::AltScreenMode; use codex_protocol::config_types::SandboxMode; +#[cfg(target_os = "windows")] use codex_protocol::config_types::WindowsSandboxLevel; use codex_rollout::StateDbHandle; use codex_rollout::state_db; @@ -1360,6 +1362,7 @@ async fn run_ratatui_app( let should_show_trust_screen_flag = !uses_remote_workspace && should_show_trust_screen(&initial_config); + #[cfg(target_os = "windows")] let mut trust_decision_was_made = false; let login_status = if initial_config.model_provider.requires_openai_auth { let Some(app_server) = app_server.as_mut() else { @@ -1405,7 +1408,10 @@ async fn run_ratatui_app( exit_reason: ExitReason::UserRequested, }); } - trust_decision_was_made = onboarding_result.directory_trust_decision.is_some(); + #[cfg(target_os = "windows")] + { + trust_decision_was_made = onboarding_result.directory_trust_decision.is_some(); + } // If this onboarding run included the login step, always refresh cloud requirements and // rebuild config. This avoids missing newly available cloud requirements due to login // status detection edge cases. @@ -1678,19 +1684,12 @@ async fn run_ratatui_app( && !crate::legacy_core::windows_sandbox::sandbox_setup_is_complete( config.codex_home.as_path(), ); - let should_prompt_windows_sandbox_nux_at_startup = cfg!(target_os = "windows") - && ((trust_decision_was_made - && WindowsSandboxLevel::from_config(&config) == WindowsSandboxLevel::Disabled) - || { - #[cfg(target_os = "windows")] - { - required_elevated_sandbox_needs_setup - } - #[cfg(not(target_os = "windows"))] - { - false - } - }); + #[cfg(target_os = "windows")] + let should_prompt_windows_sandbox_nux_at_startup = (trust_decision_was_made + && windows_sandbox_level == WindowsSandboxLevel::Disabled) + || required_elevated_sandbox_needs_setup; + #[cfg(not(target_os = "windows"))] + let should_prompt_windows_sandbox_nux_at_startup = false; let Cli { prompt, From fed349cfb931c1718498f1a57e8a8601e2c8e98c Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Thu, 21 May 2026 22:02:03 -0700 Subject: [PATCH 7/9] Simplify Windows sandbox mode mapping --- codex-rs/tui/src/app/event_dispatch.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index bae052573842..3f349f283675 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -5,6 +5,8 @@ use super::resize_reflow::trailing_run_start; use super::*; +#[cfg(target_os = "windows")] +use codex_config::types::WindowsSandboxModeToml; const SHUTDOWN_FIRST_EXIT_TIMEOUT: Duration = Duration::from_secs(/*secs*/ 2); @@ -1185,12 +1187,11 @@ impl App { ); } let profile = self.active_profile.as_deref(); - let elevated_enabled = matches!(mode, WindowsSandboxEnableMode::Elevated); - let selected_mode = if elevated_enabled { - codex_config::types::WindowsSandboxModeToml::Elevated - } else { - codex_config::types::WindowsSandboxModeToml::Unelevated + let selected_mode = match mode { + WindowsSandboxEnableMode::Elevated => WindowsSandboxModeToml::Elevated, + WindowsSandboxEnableMode::Legacy => WindowsSandboxModeToml::Unelevated, }; + let elevated_enabled = selected_mode == WindowsSandboxModeToml::Elevated; if !self.chat_widget.windows_sandbox_mode_allowed(selected_mode) { tracing::warn!( ?selected_mode, @@ -1282,7 +1283,6 @@ impl App { if self.apply_permission_profile_selection(selection).await { self.chat_widget.submit_initial_user_message_if_pending(); } - let _ = mode; self.chat_widget.add_plain_history_lines(vec![ Line::from(vec!["• ".dim(), "Sandbox ready".into()]), Line::from(vec![ @@ -1316,7 +1316,6 @@ impl App { .send(AppEvent::UpdateActivePermissionProfile( preset.active_permission_profile.clone(), )); - let _ = mode; self.chat_widget.add_plain_history_lines(vec![ Line::from(vec!["• ".dim(), "Sandbox ready".into()]), Line::from(vec![ From 885bed845771d4db14f08b96b730fdfad61bac06 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Fri, 29 May 2026 15:20:01 -0700 Subject: [PATCH 8/9] Address Windows sandbox setup review feedback --- .../windows_sandbox_processor.rs | 117 +++++++----------- 1 file changed, 45 insertions(+), 72 deletions(-) diff --git a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs index d2a6838e4c07..c0917d933bf6 100644 --- a/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs +++ b/codex-rs/app-server/src/request_processors/windows_sandbox_processor.rs @@ -59,22 +59,10 @@ impl WindowsSandboxRequestProcessor { ) .await .map_err(|err| config_load_error(&err))?; - let (mode, requested_mode) = match params.mode { - WindowsSandboxSetupMode::Elevated => ( - CoreWindowsSandboxSetupMode::Elevated, - codex_config::types::WindowsSandboxModeToml::Elevated, - ), - WindowsSandboxSetupMode::Unelevated => ( - CoreWindowsSandboxSetupMode::Unelevated, - codex_config::types::WindowsSandboxModeToml::Unelevated, - ), - }; - config - .config_layer_stack - .requirements() - .windows_sandbox_mode - .can_set(&Some(requested_mode)) - .map_err(|err| invalid_request(format!("invalid Windows sandbox setup mode: {err}")))?; + let setup_mode = resolve_allowed_windows_sandbox_setup_mode( + config.config_layer_stack.requirements(), + params.mode, + )?; self.outgoing .send_response( @@ -88,7 +76,7 @@ impl WindowsSandboxRequestProcessor { tokio::spawn(async move { let setup_request = WindowsSandboxSetupRequest { - mode, + mode: setup_mode, permission_profile: config.permissions.effective_permission_profile(), workspace_roots: config.effective_workspace_roots(), command_cwd, @@ -98,7 +86,7 @@ impl WindowsSandboxRequestProcessor { let setup_result = codex_core::windows_sandbox::run_windows_sandbox_setup(setup_request).await; let notification = WindowsSandboxSetupCompletedNotification { - mode: match mode { + mode: match setup_mode { CoreWindowsSandboxSetupMode::Elevated => WindowsSandboxSetupMode::Elevated, CoreWindowsSandboxSetupMode::Unelevated => WindowsSandboxSetupMode::Unelevated, }, @@ -116,6 +104,28 @@ impl WindowsSandboxRequestProcessor { } } +/// Resolves the requested API mode after checking that managed requirements allow it. +fn resolve_allowed_windows_sandbox_setup_mode( + requirements: &codex_config::ConfigRequirements, + requested_mode: WindowsSandboxSetupMode, +) -> Result { + let (setup_mode, config_mode) = match requested_mode { + WindowsSandboxSetupMode::Elevated => ( + CoreWindowsSandboxSetupMode::Elevated, + codex_config::types::WindowsSandboxModeToml::Elevated, + ), + WindowsSandboxSetupMode::Unelevated => ( + CoreWindowsSandboxSetupMode::Unelevated, + codex_config::types::WindowsSandboxModeToml::Unelevated, + ), + }; + requirements + .windows_sandbox_mode + .can_set(&Some(config_mode)) + .map_err(|err| invalid_request(format!("invalid Windows sandbox setup mode: {err}")))?; + Ok(setup_mode) +} + fn determine_windows_sandbox_readiness(config: &Config) -> WindowsSandboxReadinessResponse { if !cfg!(windows) { return WindowsSandboxReadinessResponse { @@ -152,69 +162,32 @@ fn determine_windows_sandbox_readiness_from_state( mod tests { use super::*; use crate::error_code::INVALID_REQUEST_ERROR_CODE; - use codex_config::CloudRequirementsLoader; - use codex_config::ConfigRequirementsToml; - use codex_config::LoaderOverrides; + use codex_config::ConfigRequirements; + use codex_config::Constrained; + use codex_config::ConstrainedWithSource; use codex_config::types::WindowsSandboxModeToml; - #[tokio::test] - async fn windows_sandbox_setup_start_rejects_disallowed_mode() { - let codex_home = tempfile::tempdir().expect("tempdir"); - let config = codex_core::config::ConfigBuilder::default() - .codex_home(codex_home.path().to_path_buf()) - .fallback_cwd(Some(codex_home.path().to_path_buf())) - .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) - .build() - .await - .expect("config"); - let config_manager = ConfigManager::new_for_tests( - codex_home.path().to_path_buf(), - Vec::new(), - LoaderOverrides::without_managed_config_for_tests(), - CloudRequirementsLoader::new(async { - Ok(Some(ConfigRequirementsToml { - windows: Some(codex_config::WindowsRequirementsToml { - allowed_sandbox_implementations: Some(vec![ - WindowsSandboxModeToml::Elevated, - ]), - }), - ..Default::default() - })) - }), - ); - let (outgoing_tx, mut outgoing_rx) = tokio::sync::mpsc::channel(1); - let processor = WindowsSandboxRequestProcessor::new( - Arc::new(OutgoingMessageSender::new( - outgoing_tx, - codex_analytics::AnalyticsEventsClient::disabled(), - )), - Arc::new(config), - config_manager, - ); + #[test] + fn resolve_allowed_windows_sandbox_setup_mode_rejects_disallowed_mode() { + let requirements = ConfigRequirements { + windows_sandbox_mode: ConstrainedWithSource::new( + Constrained::allow_only(Some(WindowsSandboxModeToml::Elevated)), + /*source*/ None, + ), + ..Default::default() + }; - let err = processor - .windows_sandbox_setup_start_inner( - &ConnectionRequestId { - connection_id: ConnectionId(1), - request_id: RequestId::Integer(1), - }, - WindowsSandboxSetupStartParams { - mode: WindowsSandboxSetupMode::Unelevated, - cwd: None, - }, - ) - .await - .expect_err("unelevated setup should be rejected"); + let err = resolve_allowed_windows_sandbox_setup_mode( + &requirements, + WindowsSandboxSetupMode::Unelevated, + ) + .expect_err("unelevated setup should be rejected"); assert_eq!(err.code, INVALID_REQUEST_ERROR_CODE); assert!( err.message.contains("invalid Windows sandbox setup mode"), "{err:?}" ); - assert!( - outgoing_rx.try_recv().is_err(), - "disallowed setup should not send a started response" - ); } #[test] From 242d56e8d48c8711df42436f458df145006ebbb6 Mon Sep 17 00:00:00 2001 From: Abhinav Vedmala Date: Fri, 29 May 2026 15:55:20 -0700 Subject: [PATCH 9/9] Expose Windows sandbox requirements to apps --- .../codex_app_server_protocol.schemas.json | 9 ++++ .../codex_app_server_protocol.v2.schemas.json | 9 ++++ .../v2/ConfigRequirementsReadResponse.json | 16 ++++++++ .../typescript/v2/ConfigRequirements.ts | 3 +- .../src/protocol/v2/config.rs | 2 + .../src/protocol/v2/tests.rs | 1 + .../request_processors/config_processor.rs | 41 +++++++++++++++++++ 7 files changed, 80 insertions(+), 1 deletion(-) diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 92549d74c8ef..0c2ebf121e83 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -7809,6 +7809,15 @@ "null" ] }, + "allowedWindowsSandboxImplementations": { + "items": { + "$ref": "#/definitions/v2/WindowsSandboxSetupMode" + }, + "type": [ + "array", + "null" + ] + }, "computerUse": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index d266a3e97730..c6ea51680d87 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -4178,6 +4178,15 @@ "null" ] }, + "allowedWindowsSandboxImplementations": { + "items": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "type": [ + "array", + "null" + ] + }, "computerUse": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json index a82421fb0103..0b8170d659c3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json @@ -121,6 +121,15 @@ "null" ] }, + "allowedWindowsSandboxImplementations": { + "items": { + "$ref": "#/definitions/WindowsSandboxSetupMode" + }, + "type": [ + "array", + "null" + ] + }, "computerUse": { "anyOf": [ { @@ -485,6 +494,13 @@ "live" ], "type": "string" + }, + "WindowsSandboxSetupMode": { + "enum": [ + "elevated", + "unelevated" + ], + "type": "string" } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts index c5f3895866a6..3e60e78da53f 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ConfigRequirements.ts @@ -6,5 +6,6 @@ import type { AskForApproval } from "./AskForApproval"; import type { ComputerUseRequirements } from "./ComputerUseRequirements"; import type { ResidencyRequirement } from "./ResidencyRequirement"; import type { SandboxMode } from "./SandboxMode"; +import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode"; -export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; +export type ConfigRequirements = {allowedApprovalPolicies: Array | null, allowedSandboxModes: Array | null, allowedWindowsSandboxImplementations: Array | null, allowedPermissions: Array | null, allowedWebSearchModes: Array | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null}; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index e30b3012efc4..05a31cf44d0e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -1,6 +1,7 @@ use super::ApprovalsReviewer; use super::AskForApproval; use super::SandboxMode; +use super::WindowsSandboxSetupMode; use super::shared::default_enabled; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::AutoCompactTokenLimitScope; @@ -358,6 +359,7 @@ pub struct ConfigRequirements { #[experimental("configRequirements/read.allowedApprovalsReviewers")] pub allowed_approvals_reviewers: Option>, pub allowed_sandbox_modes: Option>, + pub allowed_windows_sandbox_implementations: Option>, pub allowed_permissions: Option>, pub allowed_web_search_modes: Option>, pub allow_managed_hooks_only: Option, diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index a0be5cb15a33..bb6003c7aef6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -1670,6 +1670,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental() }]), allowed_approvals_reviewers: None, allowed_sandbox_modes: None, + allowed_windows_sandbox_implementations: None, allowed_permissions: None, allowed_web_search_modes: None, allow_managed_hooks_only: None, diff --git a/codex-rs/app-server/src/request_processors/config_processor.rs b/codex-rs/app-server/src/request_processors/config_processor.rs index 5ba6d02ecbf6..357102619339 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -30,6 +30,7 @@ use codex_app_server_protocol::NetworkRequirements; use codex_app_server_protocol::NetworkUnixSocketPermission; use codex_app_server_protocol::SandboxMode; use codex_app_server_protocol::ServerNotification; +use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_chatgpt::connectors; use codex_config::ConfigRequirementsToml; use codex_config::HookEventsToml; @@ -420,6 +421,23 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR .filter_map(map_sandbox_mode_requirement_to_api) .collect() }), + allowed_windows_sandbox_implementations: requirements.windows.and_then(|windows| { + windows + .allowed_sandbox_implementations + .map(|implementations| { + implementations + .into_iter() + .map(|implementation| match implementation { + codex_config::types::WindowsSandboxModeToml::Elevated => { + WindowsSandboxSetupMode::Elevated + } + codex_config::types::WindowsSandboxModeToml::Unelevated => { + WindowsSandboxSetupMode::Unelevated + } + }) + .collect() + }) + }), allowed_permissions: requirements.allowed_permissions, allowed_web_search_modes: requirements.allowed_web_search_modes.map(|modes| { let mut normalized = modes @@ -634,8 +652,10 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into) -> #[cfg(test)] mod tests { use super::map_requirements_toml_to_api; + use codex_app_server_protocol::WindowsSandboxSetupMode; use codex_config::ComputerUseRequirementsToml; use codex_config::ConfigRequirementsToml; + use codex_config::WindowsRequirementsToml; use pretty_assertions::assert_eq; #[test] @@ -687,4 +707,25 @@ mod tests { Some(false) ); } + + #[test] + fn requirements_api_includes_allowed_windows_sandbox_implementations() { + let mapped = map_requirements_toml_to_api(ConfigRequirementsToml { + windows: Some(WindowsRequirementsToml { + allowed_sandbox_implementations: Some(vec![ + codex_config::types::WindowsSandboxModeToml::Elevated, + codex_config::types::WindowsSandboxModeToml::Unelevated, + ]), + }), + ..ConfigRequirementsToml::default() + }); + + assert_eq!( + mapped.allowed_windows_sandbox_implementations, + Some(vec![ + WindowsSandboxSetupMode::Elevated, + WindowsSandboxSetupMode::Unelevated, + ]) + ); + } }