Enforce managed MXC requirements on Windows (#52193)

## What changed

Add `windows.require_mxc` to managed requirements. When set to `true` on Windows, reject configurations whose effective local sandbox backend is not MXC without changing the selected backend.

Validate after cloud configuration loads and outside startup recovery fallbacks. Apply the check to app-server startup and configuration loads, in-process bootstrap, session creation, sandbox debugging, and runtime configuration refreshes.

## Testing

Extend configuration tests to cover required and optional MXC selection, separately loaded policy requirements, and runtime refresh validation. Expand app-server startup tests to cover local and cloud MXC requirements, cloud preference selection, and failures without default-config recovery.

GitOrigin-RevId: 16456611bfd9df77e7f959b52515f1885b59ac75
This commit is contained in:
iceweasel-oai
2026-10-08 17:48:32 +00:00
committed by copyberry
parent 5fe2152b60
commit f90a578f76
12 changed files with 146 additions and 12 deletions
@@ -399,6 +399,10 @@ impl ConfigManager {
cwd,
)
.await
.and_then(|config| {
config.validate_windows_mxc_requirement()?;
Ok(config)
})
}
/// Reload sources using the task's session flags before materializing config.
@@ -437,6 +441,10 @@ impl ConfigManager {
Some(cwd.to_path_buf()),
)
.await
.and_then(|config| {
config.validate_windows_mxc_requirement()?;
Ok(config)
})
}
#[instrument(level = "trace", skip_all)]
@@ -133,6 +133,7 @@ pub(super) async fn configure(
let policy_config = config_manager
.load_startup_config(Some(config.cwd.to_path_buf()))
.await?;
config.validate_windows_mxc_requirement_from(&policy_config)?;
let auth_config = codex_login::AuthConfig {
chatgpt_base_url: Some(policy_config.chatgpt_base_url.clone()),
forced_login_method: policy_config.forced_login_method,
+1
View File
@@ -579,6 +579,7 @@ pub async fn run_main_with_transport_options(
})?
}
};
config.validate_windows_mxc_requirement()?;
config.auth_config().validate()?;
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false)
@@ -1246,6 +1246,7 @@ client_id = "mcp-client"
codex_config::WindowsSandboxImplementationToml::Unelevated,
]),
allow_mxc: None,
require_mxc: None,
}),
..ConfigRequirementsToml::default()
});
+43 -7
View File
@@ -413,10 +413,30 @@ async fn logout_account_succeeds_when_config_reload_fails() -> Result<()> {
Ok(())
}
#[test_case(false, false; "local_auth_before_cloud")]
#[test_case(true, false; "local_mxc_after_cloud_selection")]
#[test_case(false, true; "cloud_mxc_without_default_recovery")]
#[tokio::test]
async fn startup_enforces_local_auth_requirements_before_cloud_fetch() -> Result<()> {
async fn startup_enforces_requirements_at_the_correct_load_stage(
local_mxc: bool,
cloud_mxc: bool,
) -> Result<()> {
let codex_home = TempDir::new()?;
let mock_server = MockServer::start().await;
let require_mxc = "[windows]\nrequire_mxc = true\n";
let fetch_cloud = local_mxc || cloud_mxc;
if fetch_cloud {
let bundle = codex_config::test_support::CloudConfigBundleFixture::enterprise_requirement(
if cloud_mxc { require_mxc } else { "" },
)
.add_enterprise_config(format!("[features]\nprefer_mxc = {local_mxc}\n"))
.into_bundle();
Mock::given(path("/backend-api/wham/config/bundle"))
.respond_with(ResponseTemplate::new(200).set_body_json(bundle))
.expect(1..)
.mount(&mock_server)
.await;
}
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
@@ -426,7 +446,13 @@ async fn startup_enforces_local_auth_requirements_before_cloud_fetch() -> Result
)?;
std::fs::write(
codex_home.path().join("requirements.toml"),
"allowed_login_methods = [\"api\"]\n",
if local_mxc {
require_mxc
} else if cloud_mxc {
""
} else {
"allowed_login_methods = [\"api\"]\n"
},
)?;
write_chatgpt_auth(
codex_home.path(),
@@ -441,20 +467,30 @@ async fn startup_enforces_local_auth_requirements_before_cloud_fetch() -> Result
let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
.build()
.await?;
assert!(
if cfg!(windows) && fetch_cloud && (!local_mxc || !codex_sandboxing::windows_mxc_available()) {
assert!(
!timeout(DEFAULT_READ_TIMEOUT, mcp.wait_for_exit())
.await??
.success()
);
} else {
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
if !fetch_cloud {
assert_eq!(read_account(&mut mcp).await?.account, None);
}
}
assert_eq!(
mock_server
.received_requests()
.await
.expect("recorded requests")
.is_empty(),
"disallowed ChatGPT auth must not fetch cloud requirements"
!fetch_cloud,
);
assert_eq!(read_account(&mut mcp).await?.account, None);
Ok(())
}
+3 -1
View File
@@ -754,7 +754,9 @@ async fn build_debug_sandbox_config_with_loader_overrides(
.codex_home(codex_home.clone())
.fallback_cwd(Some(codex_home));
}
builder.build().await
let config = builder.build().await?;
config.validate_windows_mxc_requirement()?;
Ok(config)
}
fn config_uses_permission_profiles(config: &Config) -> bool {
+11 -1
View File
@@ -872,6 +872,8 @@ pub struct WindowsRequirementsToml {
pub allowed_sandbox_implementations: Option<Vec<WindowsSandboxImplementationToml>>,
/// False blocks both explicit MXC configuration and automatic selection.
pub allow_mxc: Option<bool>,
/// Require MXC as the selected local Windows backend.
pub require_mxc: Option<bool>,
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
@@ -883,7 +885,9 @@ pub enum WindowsSandboxImplementationToml {
impl WindowsRequirementsToml {
pub fn is_empty(&self) -> bool {
self.allowed_sandbox_implementations.is_none() && self.allow_mxc.is_none()
self.allowed_sandbox_implementations.is_none()
&& self.allow_mxc.is_none()
&& self.require_mxc.is_none()
}
}
@@ -1927,6 +1931,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
WindowsRequirementsToml {
allowed_sandbox_implementations: Some(implementations),
allow_mxc: _,
require_mxc: _,
},
source: requirement_source,
}) => {
@@ -1973,6 +1978,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
WindowsRequirementsToml {
allowed_sandbox_implementations: None,
allow_mxc: _,
require_mxc: _,
},
..
})
@@ -2850,6 +2856,7 @@ mod tests {
let windows = WindowsRequirementsToml {
allowed_sandbox_implementations: Some(vec![WindowsSandboxImplementationToml::Elevated]),
allow_mxc: None,
require_mxc: Some(true),
};
let enforce_residency = ResidencyRequirement::Us;
let enforce_source = source.clone();
@@ -3804,6 +3811,9 @@ allowed_approvals_reviewers = ["user"]
#[test]
fn deserialize_allowed_windows_sandbox_implementations() -> Result<()> {
let requirements: ConfigRequirementsToml = from_str("[windows]\nrequire_mxc = true")?;
assert!(!requirements.is_empty());
let toml_str = r#"
[windows]
allowed_sandbox_implementations = ["elevated"]
+44 -3
View File
@@ -11094,8 +11094,12 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
Ok(())
}
#[test_case::test_case(false; "optional")]
#[test_case::test_case(true; "required")]
#[tokio::test]
async fn local_mxc_preference_preserves_configured_backend() -> anyhow::Result<()> {
async fn local_mxc_preference_preserves_configured_backend(
require_mxc: bool,
) -> anyhow::Result<()> {
use codex_sandboxing::SandboxType::WindowsMxc;
use codex_sandboxing::SandboxType::WindowsRestrictedToken as RestrictedToken;
@@ -11122,16 +11126,53 @@ async fn local_mxc_preference_preserves_configured_backend() -> anyhow::Result<(
codex_home.path().join(CONFIG_TOML_FILE),
toml::to_string(&cfg)?,
)?;
let mut config = ConfigBuilder::without_managed_config_for_tests()
let loaded = 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"
"[windows]\nallow_mxc = {allow_mxc}\nrequire_mxc = {require_mxc}\n"
)),
)
.build()
.await?;
let mut config = Config::rebuild_with_session_layers(
&loaded.config_layer_stack,
loaded.cwd.to_path_buf(),
&loaded.config_layer_stack,
loaded.codex_home.clone(),
/*default_zsh_path*/ None,
)
.await?;
let mut caller_config = config.clone();
caller_config.config_layer_stack = ConfigLayerStack::default();
caller_config.prefer_mxc = false;
caller_config.permissions.windows_sandbox_type = RestrictedToken;
assert_eq!(
caller_config
.validate_windows_mxc_requirement_from(&config)
.is_err(),
cfg!(windows) && require_mxc,
);
let result = config.validate_windows_mxc_requirement();
caller_config.config_layer_stack = config.config_layer_stack.clone();
assert_eq!(
config
.resolve_runtime_refresh(&caller_config, RuntimeConfigRefresh::Mcp)
.is_err(),
result.is_err(),
);
if cfg!(windows)
&& require_mxc
&& mode != "mxc"
&& !(prefer && binding && allow_mxc && codex_sandboxing::windows_mxc_available())
{
let error = result.expect_err("MXC must already be selected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("windows.require_mxc = true"));
continue;
}
result?;
assert_eq!(
config_allows_mxc(
&config
@@ -1,5 +1,6 @@
//! Resolves refreshable MCP inputs without rebuilding unrelated session configuration.
//! Existing sessions refresh ordinary MCP state without gaining enterprise authority.
//! Validate the retained backend after applying the refreshable requirements.
use crate::config::Config;
use crate::config::ManagedFeatures;
@@ -168,6 +169,7 @@ impl Config {
if enterprise_retired {
config.disable_mcp_enterprise_auth();
}
config.validate_windows_mxc_requirement()?;
Ok(config)
}
@@ -1,6 +1,7 @@
//! Configured Windows sandbox modes and the effective local backend.
//! Keep the configured backend for remote inheritance; apply the local rollout
//! using the preference resolved during config loading.
//! Validate MXC requirements only after bootstrap has loaded the complete config.
use super::Config;
use super::EffectivePermissionSelection;
@@ -19,6 +20,35 @@ use codex_protocol::models::PermissionProfile;
use codex_sandboxing::SandboxType;
impl Config {
/// Reject a non-MXC selection without changing the final configuration.
/// Call after cloud configuration loads and outside config-recovery fallbacks.
pub fn validate_windows_mxc_requirement(&self) -> std::io::Result<()> {
self.validate_windows_mxc_requirement_from(self)
}
/// Check this execution config against separately loaded managed requirements.
pub fn validate_windows_mxc_requirement_from(
&self,
policy_config: &Self,
) -> std::io::Result<()> {
if cfg!(windows)
&& policy_config
.config_layer_stack
.requirements_toml()
.windows
.as_ref()
.and_then(|windows| windows.require_mxc)
== Some(true)
&& self.effective_local_windows_sandbox_type() != SandboxType::WindowsMxc
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"windows.require_mxc = true requires MXC to be selected on this Windows host",
));
}
Ok(())
}
/// Configured backend, without the local rollout preference. Remote executors inherit this.
pub fn windows_sandbox_type_from_config(&self) -> SandboxType {
self.permissions.windows_sandbox_type
+1
View File
@@ -553,6 +553,7 @@ impl Session {
}
async fn spawn_internal(args: SessionSpawnArgs) -> CodexResult<(Arc<Self>, SessionIo)> {
args.config.validate_windows_mxc_requirement()?;
let SessionSpawnArgs {
startup,
config,
+1
View File
@@ -975,6 +975,7 @@ interrupt_message = false
windows: Some(WindowsRequirementsToml {
allowed_sandbox_implementations: None,
allow_mxc: None,
require_mxc: None,
}),
additional_developer_instructions: None,
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),