diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index c6b63258f2..962dfa4b40 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -885,6 +885,22 @@ mod tests { Ok(()) } + #[test] + fn empty_allowed_approval_policies_is_rejected() -> Result<()> { + let toml_str = r#" + allowed_approval_policies = [] + "#; + let config: ConfigRequirementsToml = from_str(toml_str)?; + let err = ConfigRequirements::try_from(with_unknown_source(config)) + .expect_err("empty allowlist should be rejected"); + + assert_eq!( + err, + ConstraintError::empty_field("allowed_approval_policies") + ); + Ok(()) + } + #[test] fn deserialize_allowed_sandbox_modes() -> Result<()> { let toml_str = r#" diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 59cbf57dd5..629c841472 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -8034,4 +8034,32 @@ mod tests { pretty_assertions::assert_eq!(output, expected); } + + #[tokio::test] + async fn session_configuration_apply_rejects_disallowed_approval_policy_update() { + let mut session_configuration = make_session_configuration_for_tests().await; + session_configuration.approval_policy = + Constrained::allow_only(AskForApproval::UnlessTrusted); + + let err = match session_configuration.apply(&SessionSettingsUpdate { + approval_policy: Some(AskForApproval::OnRequest), + ..Default::default() + }) { + Ok(_) => panic!("disallowed approval policy update should be rejected"), + Err(err) => err, + }; + + assert!(matches!( + err, + crate::config::ConstraintError::InvalidValue { + candidate, + allowed, + .. + } if candidate == "OnRequest" && allowed == "[UnlessTrusted]" + )); + assert_eq!( + session_configuration.approval_policy.value(), + AskForApproval::UnlessTrusted + ); + } } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 5cfdcf0441..97962bef00 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -5159,6 +5159,166 @@ trust_level = "untrusted" ); Ok(()) } + + #[tokio::test] + async fn explicit_on_request_approval_falls_back_to_untrusted_when_disallowed_by_requirements() + -> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"approval_policy = "on-request" +"#, + )?; + + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cloud_requirements(CloudRequirementsLoader::new(async { + Some(crate::config_loader::ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::UnlessTrusted]), + ..Default::default() + }) + })) + .build() + .await?; + + assert_eq!( + config.permissions.approval_policy.value(), + AskForApproval::UnlessTrusted + ); + Ok(()) + } + + #[tokio::test] + async fn cli_on_request_approval_falls_back_to_untrusted_when_disallowed_by_requirements() + -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cli_overrides(vec![( + "approval_policy".to_string(), + TomlValue::String("on-request".to_string()), + )]) + .cloud_requirements(CloudRequirementsLoader::new(async { + Some(crate::config_loader::ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::UnlessTrusted]), + ..Default::default() + }) + })) + .build() + .await?; + + assert_eq!( + config.permissions.approval_policy.value(), + AskForApproval::UnlessTrusted + ); + Ok(()) + } + + #[tokio::test] + async fn disallowed_approval_policy_adds_startup_warning() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"approval_policy = "on-request" +"#, + )?; + + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cloud_requirements(CloudRequirementsLoader::new(async { + Some(crate::config_loader::ConfigRequirementsToml { + allowed_approval_policies: Some(vec![AskForApproval::UnlessTrusted]), + ..Default::default() + }) + })) + .build() + .await?; + + assert!( + config.startup_warnings.iter().any(|warning| { + warning.contains("Configured value for `approval_policy`") + && warning.contains("falling back to required value") + && warning.contains("UnlessTrusted") + }), + "{:?}", + config.startup_warnings + ); + Ok(()) + } + + #[cfg(target_os = "macos")] + #[tokio::test] + async fn mdm_untrusted_approval_requirement_allows_startup() -> std::io::Result<()> { + use base64::Engine; + + let codex_home = TempDir::new()?; + std::fs::write( + codex_home.path().join(CONFIG_TOML_FILE), + r#"approval_policy = "on-request" +"#, + )?; + + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .loader_overrides(LoaderOverrides { + managed_config_path: None, + managed_preferences_base64: Some(String::new()), + macos_managed_config_requirements_base64: Some( + base64::prelude::BASE64_STANDARD.encode( + br#" +allowed_approval_policies = ["untrusted"] +"#, + ), + ), + }) + .build() + .await?; + + assert_eq!( + config.permissions.approval_policy.value(), + AskForApproval::UnlessTrusted + ); + Ok(()) + } + + #[cfg(target_os = "macos")] + #[tokio::test] + async fn mdm_untrusted_approval_requirement_allows_startup_with_cli_override() + -> std::io::Result<()> { + use base64::Engine; + + let codex_home = TempDir::new()?; + let config = ConfigBuilder::default() + .codex_home(codex_home.path().to_path_buf()) + .fallback_cwd(Some(codex_home.path().to_path_buf())) + .cli_overrides(vec![( + "approval_policy".to_string(), + TomlValue::String("on-request".to_string()), + )]) + .loader_overrides(LoaderOverrides { + managed_config_path: None, + managed_preferences_base64: Some(String::new()), + macos_managed_config_requirements_base64: Some( + base64::prelude::BASE64_STANDARD.encode( + br#" +allowed_approval_policies = ["untrusted"] +"#, + ), + ), + }) + .build() + .await?; + + assert_eq!( + config.permissions.approval_policy.value(), + AskForApproval::UnlessTrusted + ); + Ok(()) + } } #[cfg(test)]