feat(requirements): support allowed_approval_reviewers (#16701)

## Description

Add requirements.toml support for `allowed_approvals_reviewers =
["user", "guardian_subagent"]`, so admins can now restrict the use of
guardian mode.

Note: If a user sets a reviewer that isn’t allowed by requirements.toml,
config loading falls back to the first allowed reviewer and emits a
startup warning.

The table below describes the possible admin controls.
| Admin intent | `requirements.toml` | User `config.toml` | End result |
|---|---|---|---|
| Leave Guardian optional | omit `allowed_approvals_reviewers` or set
`["user", "guardian_subagent"]` | user chooses `approvals_reviewer =
"user"` or `"guardian_subagent"` | Guardian off for `user`, on for
`guardian_subagent` + `approval_policy = "on-request"` |
| Force Guardian off | `allowed_approvals_reviewers = ["user"]` | any
user value | Effective reviewer is `user`; Guardian off |
| Force Guardian on | `allowed_approvals_reviewers =
["guardian_subagent"]` and usually `allowed_approval_policies =
["on-request"]` | any user reviewer value; user should also have
`approval_policy = "on-request"` unless policy is forced | Effective
reviewer is `guardian_subagent`; Guardian on when effective approval
policy is `on-request` |
| Allow both, but default to manual if user does nothing |
`allowed_approvals_reviewers = ["user", "guardian_subagent"]` | omit
`approvals_reviewer` | Effective reviewer is `user`; Guardian off |
| Allow both, and user explicitly opts into Guardian |
`allowed_approvals_reviewers = ["user", "guardian_subagent"]` |
`approvals_reviewer = "guardian_subagent"` and `approval_policy =
"on-request"` | Guardian on |
| Invalid admin config | `allowed_approvals_reviewers = []` | anything |
Config load error |
This commit is contained in:
Owen Lin
2026-04-06 11:11:44 -07:00
committed by GitHub
parent 4ce97cef02
commit ded559680d
16 changed files with 513 additions and 191 deletions
+1 -8
View File
@@ -5,7 +5,6 @@ use crate::agent::agent_status_from_event;
use crate::config::AgentRoleConfig;
use crate::config::Config;
use crate::config::ConfigBuilder;
use crate::config_loader::LoaderOverrides;
use crate::contextual_user_message::SUBAGENT_NOTIFICATION_OPEN_TAG;
use assert_matches::assert_matches;
use chrono::Utc;
@@ -36,15 +35,9 @@ async fn test_config_with_cli_overrides(
cli_overrides: Vec<(String, TomlValue)>,
) -> (TempDir, Config) {
let home = TempDir::new().expect("create temp dir");
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(home.path().to_path_buf())
.cli_overrides(cli_overrides)
.loader_overrides(LoaderOverrides {
#[cfg(target_os = "macos")]
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(String::new()),
..LoaderOverrides::default()
})
.build()
.await
.expect("load default test config");
+1 -1
View File
@@ -2216,7 +2216,7 @@ fn text_block(s: &str) -> serde_json::Value {
}
async fn build_test_config(codex_home: &Path) -> Config {
ConfigBuilder::default()
ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.to_path_buf())
.build()
.await
+161 -37
View File
@@ -1626,7 +1626,7 @@ profile = "project"
"#,
)?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(workspace.path().to_path_buf()),
@@ -1817,12 +1817,7 @@ async fn managed_config_overrides_oauth_store_mode() -> anyhow::Result<()> {
std::fs::write(&config_path, "mcp_oauth_credentials_store = \"file\"\n")?;
std::fs::write(&managed_path, "mcp_oauth_credentials_store = \"keyring\"\n")?;
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
};
let overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone());
let cwd = codex_home.path().abs();
let config_layer_stack = load_config_layers_state(
@@ -1947,12 +1942,7 @@ async fn managed_config_wins_over_cli_overrides() -> anyhow::Result<()> {
)?;
std::fs::write(&managed_path, "model = \"managed_config\"\n")?;
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
};
let overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path);
let cwd = codex_home.path().abs();
let config_layer_stack = load_config_layers_state(
@@ -3286,7 +3276,7 @@ nickname_candidates = ["Hypatia", "Noether"]
)
.await?;
let config = ConfigBuilder::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()))
.build()
@@ -3340,7 +3330,7 @@ nickname_candidates = ["Noether"]
)
.await?;
let config = ConfigBuilder::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()))
.build()
@@ -3403,7 +3393,7 @@ model = "gpt-5"
)
.await?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(nested_cwd),
@@ -3457,7 +3447,7 @@ config_file = "./agents/researcher.toml"
)
.await?;
let config = ConfigBuilder::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()))
.build()
@@ -3510,7 +3500,7 @@ description = "Review role"
)
.await?;
let config = ConfigBuilder::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()))
.build()
@@ -3572,7 +3562,7 @@ developer_instructions = "Review carefully"
)
.await?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(nested_cwd),
@@ -3627,7 +3617,7 @@ config_file = "./agents/researcher.toml"
)
.await?;
let config = ConfigBuilder::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()))
.build()
@@ -3679,7 +3669,7 @@ nickname_candidates = ["Atlas"]
)
.await?;
let config = ConfigBuilder::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()))
.build()
@@ -3813,7 +3803,7 @@ developer_instructions = "Write carefully"
"#,
)?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(nested_cwd),
@@ -3937,7 +3927,7 @@ model = "gpt-5"
)
.await?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(nested_cwd),
@@ -4057,7 +4047,7 @@ model = "gpt-5-mini"
)
.await?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.harness_overrides(ConfigOverrides {
cwd: Some(nested_cwd),
@@ -4957,6 +4947,7 @@ fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset() -> any
let requirements_toml = crate::config_loader::ConfigRequirementsToml {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
allowed_web_search_modes: Some(vec![
crate::config_loader::WebSearchModeRequirement::Cached,
@@ -5549,7 +5540,7 @@ async fn requirements_disallowing_default_sandbox_falls_back_to_required_default
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
@@ -5579,6 +5570,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
let requirements = crate::config_loader::ConfigRequirementsToml {
allowed_approval_policies: None,
allowed_approvals_reviewers: None,
allowed_sandbox_modes: Some(vec![crate::config_loader::SandboxModeRequirement::ReadOnly]),
allowed_web_search_modes: None,
feature_requirements: None,
@@ -5590,7 +5582,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
guardian_developer_instructions: None,
};
let config = ConfigBuilder::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 {
@@ -5615,7 +5607,7 @@ async fn requirements_web_search_mode_overrides_danger_full_access_default() ->
"#,
)?;
let config = ConfigBuilder::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 {
@@ -5656,7 +5648,7 @@ trust_level = "untrusted"
),
)?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.fallback_cwd(Some(workspace.path().to_path_buf()))
.cloud_requirements(CloudRequirementsLoader::new(async {
@@ -5685,7 +5677,7 @@ async fn explicit_approval_policy_falls_back_when_disallowed_by_requirements() -
"#,
)?;
let config = ConfigBuilder::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 {
@@ -5707,7 +5699,7 @@ async fn explicit_approval_policy_falls_back_when_disallowed_by_requirements() -
async fn feature_requirements_normalize_effective_feature_values() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
@@ -5749,7 +5741,7 @@ shell_tool = true
"#,
)?;
let config = ConfigBuilder::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 {
@@ -5785,7 +5777,7 @@ async fn approvals_reviewer_defaults_to_manual_only_without_guardian_feature() -
{
let codex_home = TempDir::new()?;
let config = ConfigBuilder::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()))
.build()
@@ -5835,7 +5827,7 @@ guardian_approval = true
"#,
)?;
let config = ConfigBuilder::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()))
.build()
@@ -5855,7 +5847,7 @@ async fn approvals_reviewer_can_be_set_in_config_without_guardian_approval() ->
"#,
)?;
let config = ConfigBuilder::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()))
.build()
@@ -5878,7 +5870,7 @@ approvals_reviewer = "guardian_subagent"
"#,
)?;
let config = ConfigBuilder::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()))
.build()
@@ -5891,6 +5883,138 @@ approvals_reviewer = "guardian_subagent"
Ok(())
}
#[tokio::test]
async fn requirements_disallowing_default_approvals_reviewer_falls_back_to_required_default()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(codex_home.path().to_path_buf())
.cloud_requirements(CloudRequirementsLoader::new(async {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::GuardianSubagent]),
..Default::default()
}))
}))
.build()
.await?;
assert_eq!(
config.approvals_reviewer,
ApprovalsReviewer::GuardianSubagent
);
Ok(())
}
#[tokio::test]
async fn root_approvals_reviewer_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#"approvals_reviewer = "user"
"#,
)?;
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 {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::GuardianSubagent]),
..Default::default()
}))
}))
.build()
.await?;
assert_eq!(
config.approvals_reviewer,
ApprovalsReviewer::GuardianSubagent
);
assert!(
config.startup_warnings.iter().any(|warning| {
warning
.contains("Configured value for `approvals_reviewer` is disallowed by requirements")
}),
"{:?}",
config.startup_warnings
);
Ok(())
}
#[tokio::test]
async fn profile_approvals_reviewer_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#"profile = "default"
[profiles.default]
approvals_reviewer = "user"
"#,
)?;
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 {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
allowed_approvals_reviewers: Some(vec![ApprovalsReviewer::GuardianSubagent]),
..Default::default()
}))
}))
.build()
.await?;
assert_eq!(
config.approvals_reviewer,
ApprovalsReviewer::GuardianSubagent
);
Ok(())
}
#[tokio::test]
async fn approvals_reviewer_preserves_valid_user_choice_when_allowed_by_requirements()
-> std::io::Result<()> {
let codex_home = TempDir::new()?;
std::fs::write(
codex_home.path().join(CONFIG_TOML_FILE),
r#"approvals_reviewer = "guardian_subagent"
"#,
)?;
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 {
Ok(Some(crate::config_loader::ConfigRequirementsToml {
allowed_approvals_reviewers: Some(vec![
ApprovalsReviewer::User,
ApprovalsReviewer::GuardianSubagent,
]),
..Default::default()
}))
}))
.build()
.await?;
assert_eq!(
config.approvals_reviewer,
ApprovalsReviewer::GuardianSubagent
);
assert!(
config
.startup_warnings
.iter()
.all(|warning| !warning.contains("approvals_reviewer")),
"{:?}",
config.startup_warnings
);
Ok(())
}
#[tokio::test]
async fn smart_approvals_alias_is_ignored() -> std::io::Result<()> {
let codex_home = TempDir::new()?;
@@ -5901,7 +6025,7 @@ smart_approvals = true
"#,
)?;
let config = ConfigBuilder::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()))
.build()
@@ -5930,7 +6054,7 @@ smart_approvals = true
"#,
)?;
let config = ConfigBuilder::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()))
.build()
+26 -2
View File
@@ -689,6 +689,11 @@ impl ConfigBuilder {
config_layer_stack,
)
}
#[cfg(test)]
pub(crate) fn without_managed_config_for_tests() -> Self {
Self::default().loader_overrides(LoaderOverrides::without_managed_config_for_tests())
}
}
impl Config {
@@ -2060,6 +2065,7 @@ impl Config {
// Config.
let ConfigRequirements {
approval_policy: mut constrained_approval_policy,
approvals_reviewer: mut constrained_approvals_reviewer,
sandbox_policy: mut constrained_sandbox_policy,
web_search_mode: mut constrained_web_search_mode,
feature_requirements,
@@ -2308,10 +2314,22 @@ impl Config {
);
approval_policy = constrained_approval_policy.value();
}
let approvals_reviewer = approvals_reviewer_override
let approvals_reviewer_was_explicit = approvals_reviewer_override.is_some()
|| config_profile.approvals_reviewer.is_some()
|| cfg.approvals_reviewer.is_some();
let mut approvals_reviewer = approvals_reviewer_override
.or(config_profile.approvals_reviewer)
.or(cfg.approvals_reviewer)
.unwrap_or(ApprovalsReviewer::User);
if !approvals_reviewer_was_explicit
&& let Err(err) = constrained_approvals_reviewer.can_set(&approvals_reviewer)
{
tracing::warn!(
error = %err,
"default approvals reviewer is disallowed by requirements; falling back to required default"
);
approvals_reviewer = constrained_approvals_reviewer.value();
}
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features)
.unwrap_or(WebSearchMode::Cached);
let web_search_config = resolve_web_search_config(&cfg, &config_profile);
@@ -2554,6 +2572,12 @@ impl Config {
&mut constrained_approval_policy,
&mut startup_warnings,
)?;
apply_requirement_constrained_value(
"approvals_reviewer",
approvals_reviewer,
&mut constrained_approvals_reviewer,
&mut startup_warnings,
)?;
apply_requirement_constrained_value(
"sandbox_mode",
sandbox_policy,
@@ -2639,7 +2663,7 @@ impl Config {
windows_sandbox_mode,
windows_sandbox_private_desktop,
},
approvals_reviewer,
approvals_reviewer: constrained_approvals_reviewer.value(),
enforce_residency: enforce_residency.value,
notify: cfg.notify,
user_instructions,
+10
View File
@@ -140,6 +140,16 @@ impl ConfigService {
}
}
#[cfg(test)]
pub(crate) fn without_managed_config_for_tests(codex_home: PathBuf) -> Self {
Self::new(
codex_home,
Vec::new(),
LoaderOverrides::without_managed_config_for_tests(),
CloudRequirementsLoader::default(),
)
}
pub async fn read(
&self,
params: ConfigReadParams,
+26 -61
View File
@@ -74,7 +74,7 @@ unified_exec = true
"#;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), original)?;
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -108,7 +108,7 @@ async fn write_value_supports_nested_app_paths() -> Result<()> {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -178,12 +178,7 @@ async fn read_includes_origins_and_layers() {
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
},
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
);
@@ -245,23 +240,23 @@ async fn write_value_succeeds_when_managed_preferences_expand_home_directory_pat
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n")?;
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(tmp.path().join("managed_config.toml")),
managed_preferences_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
let mut loader_overrides =
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml"));
loader_overrides.managed_preferences_base64 = Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
writable_roots = ["~/code"]
"#
.as_bytes(),
),
),
macos_managed_config_requirements_base64: None,
},
.as_bytes(),
),
);
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
loader_overrides,
CloudRequirementsLoader::default(),
);
@@ -301,12 +296,7 @@ async fn write_value_reports_override() {
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
},
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
);
@@ -352,7 +342,7 @@ async fn version_conflict_rejected() {
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&user_path, "model = \"user\"").unwrap();
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -375,7 +365,7 @@ async fn write_value_defaults_to_user_config_path() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap();
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: None,
@@ -405,12 +395,7 @@ async fn invalid_user_value_rejected_even_if_overridden_by_managed() {
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
},
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
);
@@ -439,7 +424,7 @@ async fn reserved_builtin_provider_override_rejected() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n").unwrap();
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
@@ -470,12 +455,7 @@ async fn write_value_rejects_feature_requirement_conflict() {
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
},
LoaderOverrides::without_managed_config_for_tests(),
CloudRequirementsLoader::new(async {
Ok(Some(ConfigRequirementsToml {
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
@@ -521,12 +501,7 @@ async fn write_value_rejects_profile_feature_requirement_conflict() {
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: None,
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
},
LoaderOverrides::without_managed_config_for_tests(),
CloudRequirementsLoader::new(async {
Ok(Some(ConfigRequirementsToml {
feature_requirements: Some(crate::config_loader::FeatureRequirementsToml {
@@ -583,12 +558,7 @@ async fn read_reports_managed_overrides_user_and_session_flags() {
let service = ConfigService::new(
tmp.path().to_path_buf(),
cli_overrides,
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
},
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
);
@@ -641,12 +611,7 @@ async fn write_value_reports_managed_override() {
let service = ConfigService::new(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
},
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudRequirementsLoader::default(),
);
@@ -698,7 +663,7 @@ alpha = "a"
std::fs::write(&path, base)?;
let service = ConfigService::new_with_defaults(tmp.path().to_path_buf());
let service = ConfigService::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
+23
View File
@@ -11,6 +11,7 @@ use codex_app_server_protocol::ConfigLayerSource;
use codex_config::CONFIG_TOML_FILE;
use codex_config::ConfigRequirementsWithSources;
use codex_git_utils::resolve_root_git_project_for_trust;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::config_types::TrustLevel;
use codex_protocol::protocol::AskForApproval;
@@ -880,6 +881,7 @@ async fn load_project_layers(
#[derive(Deserialize, Debug, Clone, Default, PartialEq)]
struct LegacyManagedConfigToml {
approval_policy: Option<AskForApproval>,
approvals_reviewer: Option<ApprovalsReviewer>,
sandbox_mode: Option<SandboxMode>,
}
@@ -889,11 +891,15 @@ impl From<LegacyManagedConfigToml> for ConfigRequirementsToml {
let LegacyManagedConfigToml {
approval_policy,
approvals_reviewer,
sandbox_mode,
} = legacy;
if let Some(approval_policy) = approval_policy {
config_requirements_toml.allowed_approval_policies = Some(vec![approval_policy]);
}
if let Some(approvals_reviewer) = approvals_reviewer {
config_requirements_toml.allowed_approvals_reviewers = Some(vec![approvals_reviewer]);
}
if let Some(sandbox_mode) = sandbox_mode {
let required_mode: SandboxModeRequirement = sandbox_mode.into();
// Allowing read-only is a requirement for Codex to function correctly.
@@ -957,6 +963,7 @@ foo = "xyzzy"
fn legacy_managed_config_backfill_includes_read_only_sandbox_mode() {
let legacy = LegacyManagedConfigToml {
approval_policy: None,
approvals_reviewer: None,
sandbox_mode: Some(SandboxMode::WorkspaceWrite),
};
@@ -971,6 +978,22 @@ foo = "xyzzy"
);
}
#[test]
fn legacy_managed_config_backfill_includes_approvals_reviewer() {
let legacy = LegacyManagedConfigToml {
approval_policy: None,
approvals_reviewer: Some(ApprovalsReviewer::GuardianSubagent),
sandbox_mode: None,
};
let requirements = ConfigRequirementsToml::from(legacy);
assert_eq!(
requirements.allowed_approvals_reviewers,
Some(vec![ApprovalsReviewer::GuardianSubagent])
);
}
#[cfg(windows)]
#[test]
fn windows_system_requirements_toml_file_uses_expected_suffix() {
+56 -74
View File
@@ -115,10 +115,7 @@ async fn returns_config_error_for_invalid_managed_config_toml() {
let contents = "model = \"gpt-4\"\ninvalid = [";
std::fs::write(&managed_path, contents).expect("write managed config");
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path.clone()),
..Default::default()
};
let overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone());
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let err = load_config_layers_state(
@@ -202,12 +199,7 @@ extra = true
)
.expect("write managed config");
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
managed_preferences_base64: None,
macos_managed_config_requirements_base64: None,
};
let overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path);
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let state = load_config_layers_state(
@@ -239,14 +231,7 @@ async fn returns_empty_when_all_layers_missing() {
let tmp = tempdir().expect("tempdir");
let managed_path = tmp.path().join("managed_config.toml");
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
#[cfg(target_os = "macos")]
// Force managed preferences to resolve as empty so this test does not
// inherit non-empty machine-specific managed state.
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: None,
};
let overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path);
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let layers = load_config_layers_state(
@@ -337,13 +322,9 @@ value = "managed"
flag = false
"#;
let overrides = LoaderOverrides {
managed_config_path: Some(managed_path),
managed_preferences_base64: Some(
base64::prelude::BASE64_STANDARD.encode(raw_managed_preferences.as_bytes()),
),
macos_managed_config_requirements_base64: None,
};
let mut overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path);
overrides.managed_preferences_base64 =
Some(base64::prelude::BASE64_STANDARD.encode(raw_managed_preferences.as_bytes()));
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
let state = load_config_layers_state(
@@ -391,23 +372,23 @@ async fn managed_preferences_expand_home_directory_in_workspace_write_roots() ->
};
let tmp = tempdir()?;
let config = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(LoaderOverrides {
managed_config_path: Some(tmp.path().join("managed_config.toml")),
managed_preferences_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
let mut loader_overrides =
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml"));
loader_overrides.managed_preferences_base64 = Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
writable_roots = ["~/code"]
"#
.as_bytes(),
),
),
macos_managed_config_requirements_base64: None,
})
.as_bytes(),
),
);
let config = ConfigBuilder::default()
.codex_home(tmp.path().to_path_buf())
.fallback_cwd(Some(tmp.path().to_path_buf()))
.loader_overrides(loader_overrides)
.build()
.await?;
@@ -435,23 +416,23 @@ async fn managed_preferences_requirements_are_applied() -> anyhow::Result<()> {
let tmp = tempdir()?;
let mut loader_overrides =
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml"));
loader_overrides.macos_managed_config_requirements_base64 = Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
allowed_approval_policies = ["never"]
allowed_sandbox_modes = ["read-only"]
"#
.as_bytes(),
),
);
let state = load_config_layers_state(
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
LoaderOverrides {
managed_config_path: Some(tmp.path().join("managed_config.toml")),
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
allowed_approval_policies = ["never"]
allowed_sandbox_modes = ["read-only"]
"#
.as_bytes(),
),
),
},
loader_overrides,
CloudRequirementsLoader::default(),
)
.await?;
@@ -498,22 +479,21 @@ async fn managed_preferences_requirements_take_precedence() -> anyhow::Result<()
tokio::fs::write(&managed_path, "approval_policy = \"on-request\"\n").await?;
let mut loader_overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path);
loader_overrides.macos_managed_config_requirements_base64 = Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
allowed_approval_policies = ["never"]
"#
.as_bytes(),
),
);
let state = load_config_layers_state(
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
LoaderOverrides {
managed_config_path: Some(managed_path),
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
allowed_approval_policies = ["never"]
"#
.as_bytes(),
),
),
},
loader_overrides,
CloudRequirementsLoader::default(),
)
.await?;
@@ -631,24 +611,24 @@ async fn cloud_requirements_take_precedence_over_mdm_requirements() -> anyhow::R
use base64::Engine;
let tmp = tempdir()?;
let mut loader_overrides = LoaderOverrides::without_managed_config_for_tests();
loader_overrides.macos_managed_config_requirements_base64 = Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
allowed_approval_policies = ["on-request"]
"#
.as_bytes(),
),
);
let state = load_config_layers_state(
tmp.path(),
Some(AbsolutePathBuf::try_from(tmp.path())?),
&[] as &[(String, TomlValue)],
LoaderOverrides {
macos_managed_config_requirements_base64: Some(
base64::prelude::BASE64_STANDARD.encode(
r#"
allowed_approval_policies = ["on-request"]
"#
.as_bytes(),
),
),
..LoaderOverrides::default()
},
loader_overrides,
CloudRequirementsLoader::new(async {
Ok(Some(ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
@@ -700,6 +680,7 @@ allowed_approval_policies = ["on-request"]
RequirementSource::CloudRequirements,
ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
@@ -740,6 +721,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
let requirements = ConfigRequirementsToml {
allowed_approval_policies: Some(vec![AskForApproval::Never]),
allowed_approvals_reviewers: None,
allowed_sandbox_modes: None,
allowed_web_search_modes: None,
feature_requirements: None,
+1 -8
View File
@@ -6,7 +6,6 @@ use crate::config_loader::ConfigLayerStack;
use crate::config_loader::ConfigLayerStackOrdering;
use crate::config_loader::ConfigRequirements;
use crate::config_loader::ConfigRequirementsToml;
use crate::config_loader::LoaderOverrides;
use crate::config_loader::RequirementSource;
use crate::config_loader::Sourced;
use codex_app_server_protocol::ConfigLayerSource;
@@ -87,14 +86,8 @@ fn external_file_system_sandbox_policy() -> FileSystemSandboxPolicy {
async fn test_config() -> (TempDir, Config) {
let home = TempDir::new().expect("create temp dir");
let config = ConfigBuilder::default()
let config = ConfigBuilder::without_managed_config_for_tests()
.codex_home(home.path().to_path_buf())
.loader_overrides(LoaderOverrides {
#[cfg(target_os = "macos")]
managed_preferences_base64: Some(String::new()),
macos_managed_config_requirements_base64: Some(String::new()),
..LoaderOverrides::default()
})
.build()
.await
.expect("load default test config");