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 72ddca52f6..665a790784 100644 --- a/codex-rs/app-server/src/request_processors/config_processor.rs +++ b/codex-rs/app-server/src/request_processors/config_processor.rs @@ -1218,6 +1218,7 @@ client_id = "mcp-client" codex_config::WindowsSandboxImplementationToml::Elevated, codex_config::WindowsSandboxImplementationToml::Unelevated, ]), + allow_mxc: None, }), ..ConfigRequirementsToml::default() }); diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 624a7af0f4..52eefeb6cc 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -870,6 +870,8 @@ impl fmt::Display for WebSearchModeRequirement { #[derive(Deserialize, Debug, Clone, Default, PartialEq, Eq)] pub struct WindowsRequirementsToml { pub allowed_sandbox_implementations: Option>, + /// False blocks both explicit MXC configuration and automatic selection. + pub allow_mxc: Option, } #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)] @@ -881,7 +883,7 @@ pub enum WindowsSandboxImplementationToml { impl WindowsRequirementsToml { pub fn is_empty(&self) -> bool { - self.allowed_sandbox_implementations.is_none() + self.allowed_sandbox_implementations.is_none() && self.allow_mxc.is_none() } } @@ -1915,11 +1917,16 @@ impl TryFrom for ConfigRequirements { /*source*/ None, ), }; - let windows_sandbox_mode = match windows { + let mxc_requirement_source = windows + .as_ref() + .filter(|windows| windows.value.allow_mxc == Some(false)) + .map(|windows| windows.source.clone()); + let mut windows_sandbox_mode = match windows { Some(Sourced { value: WindowsRequirementsToml { allowed_sandbox_implementations: Some(implementations), + allow_mxc: _, }, source: requirement_source, }) => { @@ -1965,6 +1972,7 @@ impl TryFrom for ConfigRequirements { value: WindowsRequirementsToml { allowed_sandbox_implementations: None, + allow_mxc: _, }, .. }) @@ -1973,6 +1981,18 @@ impl TryFrom for ConfigRequirements { /*source*/ None, ), }; + if let Some(requirement_source) = mxc_requirement_source { + windows_sandbox_mode.add_validator(move |candidate| match candidate { + Some(WindowsSandboxModeToml::Mxc) => Err(ConstraintError::InvalidValue { + field_name: "windows.sandbox", + candidate: format!("{candidate:?}"), + allowed: "windows.allow_mxc = false".to_string(), + requirement_source: requirement_source.clone(), + }), + Some(WindowsSandboxModeToml::Elevated | WindowsSandboxModeToml::Unelevated) + | None => Ok(()), + })?; + } let exec_policy = match rules { Some(Sourced { value, source }) => { let policy = value.to_requirements_policy().map_err(|err| { @@ -2827,6 +2847,7 @@ mod tests { }; let windows = WindowsRequirementsToml { allowed_sandbox_implementations: Some(vec![WindowsSandboxImplementationToml::Elevated]), + allow_mxc: None, }; let enforce_residency = ResidencyRequirement::Us; let enforce_source = source.clone(); @@ -3812,6 +3833,22 @@ allowed_approvals_reviewers = ["user"] ); assert!(requirements.windows_sandbox_mode.can_set(&None).is_err()); + for allow_mxc in [true, false] { + let config = from_str(&format!("{toml_str}\nallow_mxc = {allow_mxc}"))?; + let requirements: ConfigRequirements = with_unknown_source(config).try_into()?; + let constraint = &requirements.windows_sandbox_mode; + assert_eq!( + [ + Some(WindowsSandboxModeToml::Elevated), + Some(WindowsSandboxModeToml::Unelevated), + Some(WindowsSandboxModeToml::Mxc), + None, + ] + .map(|mode| constraint.can_set(&mode).is_ok()), + [true, false, allow_mxc, false] + ); + } + Ok(()) } diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 4fb1e74cc9..d3e3b12437 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -11102,22 +11102,41 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s #[tokio::test] async fn local_mxc_preference_preserves_configured_backend() -> anyhow::Result<()> { use codex_sandboxing::SandboxType::WindowsMxc; - use codex_sandboxing::SandboxType::WindowsRestrictedToken; + use codex_sandboxing::SandboxType::WindowsRestrictedToken as RestrictedToken; let codex_home = TempDir::new()?; - for (prefer, resolved_preference, binding, mode, expected) in [ - (true, true, true, "unelevated", WindowsMxc), - (true, false, true, "unelevated", WindowsRestrictedToken), - (true, false, false, "unelevated", WindowsRestrictedToken), - (false, false, true, "unelevated", WindowsRestrictedToken), - (false, false, false, "mxc", WindowsMxc), + for (prefer, resolved_preference, binding, allow_mxc, mode, expected) in [ + (true, true, true, true, "unelevated", WindowsMxc), + (true, false, true, true, "unelevated", RestrictedToken), + (true, false, true, false, "unelevated", RestrictedToken), + (true, false, false, true, "unelevated", RestrictedToken), + (false, false, true, true, "unelevated", RestrictedToken), + (false, false, false, true, "mxc", WindowsMxc), ] { let cfg: ConfigToml = toml::from_str(&format!( "[windows]\nsandbox = {mode:?}\n[features]\nprefer_mxc = {prefer}\n\ [features.network_proxy]\nenabled = true\nallow_local_binding = {binding}\n" ))?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + toml::to_string(&cfg)?, + )?; + let mut 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_config_bundle( + CloudConfigBundleFixture::loader_with_enterprise_requirement(format!( + "[windows]\nallow_mxc = {allow_mxc}\n" + )), + ) + .build() + .await?; assert_eq!( - network_config_allows_mxc( + config_allows_mxc( + &config + .config_layer_stack + .requirements() + .windows_sandbox_mode, &EffectivePermissionSelection { profiles: None, selected_profile_id: None, @@ -11130,20 +11149,11 @@ async fn local_mxc_preference_preserves_configured_backend() -> anyhow::Result<( cfg.features.as_ref(), /*enable_network_proxy*/ true, )?, - binding, + binding && allow_mxc, ); - let mut config = Config::load_from_base_config_with_overrides( - cfg, - ConfigOverrides { - cwd: Some(codex_home.path().to_path_buf()), - ..Default::default() - }, - codex_home.abs(), - ) - .await?; assert_eq!( config.prefer_mxc, - prefer && binding && codex_sandboxing::windows_mxc_available(), + prefer && binding && allow_mxc && codex_sandboxing::windows_mxc_available(), ); // Exercise both resolved decisions independently of the host's native support. config.prefer_mxc = resolved_preference; @@ -11156,7 +11166,7 @@ async fn local_mxc_preference_preserves_configured_backend() -> anyhow::Result<( if mode == "mxc" { WindowsMxc } else { - WindowsRestrictedToken + RestrictedToken }, expected ), diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 57f8140720..b58256da6a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -218,7 +218,7 @@ pub use permissions::resolve_permission_profile; pub(crate) use resolved_permission_profile::PermissionProfileState; pub use token_budget_startup::TokenBudgetStartupConfig; pub use windows_sandbox_config::PreparedWindowsSandboxConfig; -use windows_sandbox_config::network_config_allows_mxc; +use windows_sandbox_config::config_allows_mxc; pub use windows_sandbox_config::prepare_windows_sandbox_config; use windows_sandbox_config::resolve_windows_sandbox_type; @@ -3518,7 +3518,8 @@ impl Config { permission_config_syntax, ); let prefer_mxc = features.enabled(Feature::PreferMxc) - && network_config_allows_mxc( + && config_allows_mxc( + &constrained_windows_sandbox_mode, &effective_permission_selection, profiles_are_active, permission_profile.as_ref(), diff --git a/codex-rs/core/src/config/windows_sandbox_config.rs b/codex-rs/core/src/config/windows_sandbox_config.rs index c4f4530321..75ff2013c5 100644 --- a/codex-rs/core/src/config/windows_sandbox_config.rs +++ b/codex-rs/core/src/config/windows_sandbox_config.rs @@ -58,6 +58,9 @@ pub fn prepare_windows_sandbox_config( constraint: &mut ConstrainedWithSource>, warnings: &mut Vec, ) -> std::io::Result { + if configured_mode == Some(WindowsSandboxModeToml::Mxc) { + constraint.can_set(&configured_mode)?; + } let selected_mode = configured_mode.or(match feature_level { WindowsSandboxLevel::Elevated => Some(WindowsSandboxModeToml::Elevated), WindowsSandboxLevel::RestrictedToken => Some(WindowsSandboxModeToml::Unelevated), @@ -93,7 +96,8 @@ pub fn prepare_windows_sandbox_config( /// Managed requirements take precedence; otherwise preserve explicit /// feature-level or active-profile binding denials during automatic selection. -pub(super) fn network_config_allows_mxc( +pub(super) fn config_allows_mxc( + windows_sandbox_mode: &ConstrainedWithSource>, permission_selection: &EffectivePermissionSelection<'_>, profiles_are_active: bool, permission_profile: Option<&PermissionProfile>, @@ -101,6 +105,12 @@ pub(super) fn network_config_allows_mxc( features: Option<&FeaturesToml>, enable_network_proxy: bool, ) -> std::io::Result { + if windows_sandbox_mode + .can_set(&Some(WindowsSandboxModeToml::Mxc)) + .is_err() + { + return Ok(false); + } let profile_local_binding = if profiles_are_active && permission_profile.is_none_or(profile_allows_configured_network_proxy) && let Some(profile) = permission_selection.selected_profile_id diff --git a/codex-rs/core/tests/suite/cloud_config.rs b/codex-rs/core/tests/suite/cloud_config.rs index 60bcc49617..6899ea1b88 100644 --- a/codex-rs/core/tests/suite/cloud_config.rs +++ b/codex-rs/core/tests/suite/cloud_config.rs @@ -1,13 +1,17 @@ use anyhow::Context; use anyhow::Result; use codex_config::CloudConfigBundleLoader; +use codex_config::LoaderOverrides; use codex_config::test_support::CloudConfigBundleFixture; +use codex_config::types::WindowsSandboxModeToml; use codex_core::CodexThreadSettingsOverrides; +use codex_core::config::ConfigBuilder; use codex_features::Feature; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ThreadSettingsOverrides; +use codex_protocol::sandbox::SandboxType; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::responses::start_mock_server; use core_test_support::skip_if_target_windows; @@ -18,6 +22,52 @@ use std::sync::Arc; use std::sync::RwLock; use tempfile::TempDir; +#[tokio::test] +async fn managed_mxc_opt_out_blocks_preferred_and_explicit_selection() -> Result<()> { + for mode in [None, Some("elevated"), Some("mxc")] { + let home = TempDir::new()?; + let sandbox = mode + .map(|mode| format!("sandbox = {mode:?}\n")) + .unwrap_or_default(); + std::fs::write( + home.path().join("config.toml"), + format!("[windows]\n{sandbox}[features]\nprefer_mxc = true\n"), + )?; + let result = ConfigBuilder::default() + .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) + .codex_home(home.path().to_path_buf()) + .fallback_cwd(Some(home.path().to_path_buf())) + .cloud_config_bundle( + CloudConfigBundleFixture::loader_with_enterprise_requirement( + "[windows]\nallow_mxc = false\n[features]\nelevated_windows_sandbox = true\n", + ), + ) + .build() + .await; + if mode == Some("mxc") { + let error = + result.expect_err("explicit MXC should be rejected by managed requirements"); + assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput); + assert!(error.to_string().contains("windows.allow_mxc = false")); + } else { + let config = result?; + assert_eq!( + ( + config.permissions.windows_sandbox_mode, + config.prefer_mxc, + config.effective_local_windows_sandbox_type() + ), + ( + mode.map(|_| WindowsSandboxModeToml::Elevated), + false, + SandboxType::WindowsRestrictedToken, + ), + ); + } + } + Ok(()) +} + #[tokio::test] async fn refreshed_cloud_bundle_updates_later_sessions() -> Result<()> { let server = start_mock_server().await; diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 6225e57194..2840e99609 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -974,6 +974,7 @@ interrupt_message = false in_app_browser: None, windows: Some(WindowsRequirementsToml { allowed_sandbox_implementations: None, + allow_mxc: None, }), additional_developer_instructions: None, guardian_policy_config: Some("Use the managed guardian policy.".to_string()),