mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user