Files
codex/codex-rs/app-server/src/config_manager_service_tests.rs
T
alexsong-oai 053e5b1ca9 Add model provider configuration writes to the app-server (#58876)
## What changed

Add `modelProvider/config/write` to create custom gateways and patch existing custom gateway, Bedrock, and GCP settings. Return the saved provider using the same safe summary as `modelProvider/list`.

Omitted fields retain their values; explicit `null` clears nullable user overrides. Apply patches through the shared configuration queue with last-write-wins semantics, enforce field editability and validation, and preserve unexposed authentication settings.

Saving leaves provider selection and authentication unchanged. Saving the provider used by `model/list` invalidates its catalog for the next access. Include documentation and generated JSON, TypeScript, and Python protocol types.

## Testing

Add coverage for gateway creation and ID allocation, patch and null semantics, configuration policy enforcement, authentication preservation, and catalog refresh after active-provider edits. Extend Python protocol tests to verify wire round trips and reject mismatched provider IDs or fields.

GitOrigin-RevId: 850fe2c48ef5274944400efb5a859d720179c923
2026-11-02 12:22:22 +00:00

3676 lines
123 KiB
Rust

use super::*;
use anyhow::Result;
use axum::http::HeaderValue;
use codex_app_server_protocol::AppConfig;
use codex_app_server_protocol::AppToolApproval;
use codex_app_server_protocol::AppsConfig;
use codex_app_server_protocol::AskForApproval;
use codex_app_server_protocol::ConfigLayerSource as ApiConfigLayerSource;
use codex_config::CloudConfigBundleLoader;
use codex_config::LoaderOverrides;
use codex_config::test_support::CloudConfigBundleFixture;
use codex_config::types::OtelExporterKind;
use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
#[test]
fn provider_batch_remapping_preserves_ordered_edits() {
let initial = serde_json::json!({
"features": { "network_proxy": { "credentials": {
"a": { "env": ["A_AUTH"], "patterns": ["alpha"], "url_prefixes": ["https://a.example"] },
"b": { "env": ["B_AUTH"], "patterns": ["bravo"], "url_prefixes": ["https://b.example"], "auth": ["bearer"] },
} } },
});
let retained = serde_json::json!({
"env": ["A_AUTH"], "patterns": ["bravo"], "url_prefixes": ["https://b.example"],
});
let mut changed_auth = retained.clone();
changed_auth["auth"] = serde_json::json!(["token"]);
for (path, value, strategy, expected_b) in [
(
"b.auth",
serde_json::json!(["token"]),
MergeStrategy::Upsert,
changed_auth,
),
("b.auth", JsonValue::Null, MergeStrategy::Replace, retained),
(
"b",
JsonValue::Null,
MergeStrategy::Replace,
serde_json::json!({ "env": ["A_AUTH"] }),
),
(
"b",
serde_json::json!({ "patterns": ["replacement"], "url_prefixes": ["https://new.example"] }),
MergeStrategy::Replace,
serde_json::json!({ "env": ["A_AUTH"], "patterns": ["replacement"], "url_prefixes": ["https://new.example"] }),
),
(
"",
JsonValue::Null,
MergeStrategy::Replace,
serde_json::json!({ "env": ["A_AUTH"] }),
),
] {
let mut config: TomlValue = serde_json::from_value(initial.clone()).unwrap();
let tmp = tempdir().unwrap();
let file = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&file, toml::to_string(&config).unwrap()).unwrap();
let mut providers = CredentialProviderEdits::new(&config);
let mut persistence = Vec::new();
let path = if path.is_empty() {
"features.network_proxy.credentials".to_string()
} else {
format!("features.network_proxy.credentials.{path}")
};
for (key_path, value, strategy) in [
(
"features.network_proxy.credentials.a.env",
serde_json::json!(["B_AUTH"]),
MergeStrategy::Upsert,
),
(path.as_str(), value, strategy),
(
"features.network_proxy.credentials.b.env",
serde_json::json!(["A_AUTH"]),
MergeStrategy::Upsert,
),
] {
persistence.extend(
providers
.apply(
&mut config,
&parse_key_path(key_path).unwrap(),
parse_value(value).unwrap().as_ref(),
strategy,
)
.unwrap(),
);
}
persistence.extend(providers.remapping_edits(&config).unwrap());
ConfigEditsBuilder::for_config_path(&file)
.with_edits(persistence)
.apply_blocking()
.unwrap();
assert_eq!(
toml::from_str::<TomlValue>(&std::fs::read_to_string(&file).unwrap()).unwrap(),
config
);
let mut expected = serde_json::json!({ "b": expected_b });
if path != "features.network_proxy.credentials" {
expected["a"] = initial["features"]["network_proxy"]["credentials"]["a"].clone();
expected["a"]["env"] = serde_json::json!(["B_AUTH"]);
}
assert_eq!(
config["features"]["network_proxy"]["credentials"],
serde_json::from_value::<TomlValue>(expected).unwrap(),
"{path}"
);
}
}
#[tokio::test]
async fn provider_edits_preserve_another_writers_sibling_update() -> Result<()> {
let initial: TomlValue = toml::from_str(
r#"
[features.network_proxy.credentials.a]
env = ['A_AUTH']
patterns = ['^alpha_[a-z]{8}$']
url_prefixes = ['https://a.example']
auth = ['bearer']
[features.network_proxy.credentials.b]
env = ['B_AUTH']
patterns = ['^bravo_[a-z]{8}$']
url_prefixes = ['https://b.example']
auth = ['bearer']
[features.network_proxy.credentials.c]
env = ['C_AUTH']
patterns = ['^charlie_[a-z]{8}$']
url_prefixes = ['https://c.example']
auth = ['bearer']
"#,
)?;
for updates in [
vec![("a.auth", serde_json::json!(["token"]))],
vec![("a.env", serde_json::json!(["C_AUTH"]))],
vec![
("a.env", serde_json::json!(["C_AUTH"])),
("c.env", serde_json::json!(["A_AUTH"])),
],
] {
let tmp = tempdir()?;
let file = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&file, toml::to_string(&initial)?)?;
let mut config = initial.clone();
let mut providers = CredentialProviderEdits::new(&config);
let mut persistence = Vec::new();
for (path, value) in updates {
persistence.extend(providers.apply(
&mut config,
&parse_key_path(&format!("features.network_proxy.credentials.{path}")).unwrap(),
parse_value(value).unwrap().as_ref(),
MergeStrategy::Upsert,
)?);
}
persistence.extend(providers.remapping_edits(&config)?);
// W1 has prepared its edits; W2 updates B before W1's persistence reread.
ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf())
.write_value(ConfigValueWriteParams {
file_path: None,
key_path: "features.network_proxy.credentials.b.auth".to_string(),
value: serde_json::json!(["token"]),
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await?;
ConfigEditsBuilder::for_config_path(&file)
.with_edits(persistence)
.apply()
.await?;
config["features"]["network_proxy"]["credentials"]["b"]["auth"] =
TomlValue::Array(vec![TomlValue::String("token".to_string())]);
assert_eq!(
toml::from_str::<TomlValue>(&std::fs::read_to_string(&file)?)?,
config
);
}
Ok(())
}
#[test]
fn provider_remapping_respects_lower_layer_eviction() -> Result<()> {
let tmp = tempdir()?;
let file = AbsolutePathBuf::try_from(tmp.path().join(CONFIG_TOML_FILE))?;
let complete = |id: &str| -> TomlValue {
toml::from_str(&format!(
"[features.network_proxy.credentials.{id}]\nenv = ['VENDOR_PASSWORD']\npatterns = ['^pin_[a-z]{{8}}$']\nurl_prefixes = ['https://old.example']\n"
)).unwrap()
};
// The middle layer must not let an evicted definition's fields reappear later.
for (lower_id, middle, user, valid, strategy, move_source) in [
("a_working", None, "", false, MergeStrategy::Upsert, false),
("a_working", None, "", false, MergeStrategy::Replace, false),
("z_new", None, "", true, MergeStrategy::Upsert, false),
("z_new", None, "", true, MergeStrategy::Replace, false),
(
"z_new",
Some("a_working"),
"",
false,
MergeStrategy::Upsert,
false,
),
(
"z_new",
Some("a_working"),
"a_working",
false,
MergeStrategy::Upsert,
false,
),
("a_working", None, "", false, MergeStrategy::Upsert, true),
(
"a_working",
None,
"a_working",
false,
MergeStrategy::Upsert,
true,
),
] {
let mut user_config = if user.is_empty() {
TomlValue::Table(Default::default())
} else {
complete(user)
};
let original_user_config = user_config.clone();
let mut edits = CredentialProviderEdits::new(&user_config);
if move_source {
edits
.apply(
&mut user_config,
&parse_key_path("features.network_proxy.credentials.a_working.env").unwrap(),
Some(&TomlValue::Array(vec![TomlValue::String(
"OTHER_PASSWORD".into(),
)])),
MergeStrategy::Upsert,
)
.unwrap();
}
edits
.apply(
&mut user_config,
&parse_key_path("features.network_proxy.credentials.z_new.env").unwrap(),
Some(&TomlValue::Array(vec![TomlValue::String(
"VENDOR_PASSWORD".into(),
)])),
strategy,
)
.unwrap();
let mut layers = vec![ConfigLayerEntry::new(
ConfigLayerSource::System { file: file.clone() },
complete(lower_id),
)];
if let Some(id) = middle {
layers.push(ConfigLayerEntry::new(
ConfigLayerSource::EnterpriseManaged {
id: "test".into(),
name: "test".into(),
},
complete(id),
));
}
layers.push(ConfigLayerEntry::new(
ConfigLayerSource::User {
file: file.clone(),
profile: None,
},
user_config.clone(),
));
let layers = ConfigLayerStack::new(layers, Default::default(), Default::default())?;
assert_eq!(
edits
.validate_remapping(&original_user_config, &user_config, &layers)
.is_ok(),
valid,
"{lower_id} / {middle:?} / {user}",
);
}
for pattern in ["[", "^pin_[a-z]{8}$"] {
for reverse in [false, true] {
let original = complete("a_working");
let mut config = original.clone();
let mut edits = CredentialProviderEdits::new(&config);
let mut updates = [
("a_working.env", serde_json::json!(["OTHER_PASSWORD"])),
("z_new.patterns", serde_json::json!([pattern])),
];
if reverse {
updates.reverse();
}
for (path, value) in updates {
edits
.apply(
&mut config,
&parse_key_path(&format!("features.network_proxy.credentials.{path}"))
.unwrap(),
parse_value(value).unwrap().as_ref(),
MergeStrategy::Upsert,
)
.unwrap();
}
let layers = ConfigLayerStack::new(
vec![
ConfigLayerEntry::new(
ConfigLayerSource::System { file: file.clone() },
complete("z_new"),
),
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: file.clone(),
profile: None,
},
config.clone(),
),
],
Default::default(),
Default::default(),
)?;
assert_eq!(
edits
.validate_remapping(&original, &config, &layers)
.is_ok(),
pattern != "[",
"{pattern}, reverse={reverse}"
);
}
}
Ok(())
}
#[test]
fn provider_updates_preserve_working_definitions_and_drafts() -> Result<()> {
let tmp = tempdir()?;
let file = AbsolutePathBuf::try_from(tmp.path().join(CONFIG_TOML_FILE))?;
let pattern = "^pin_[a-z]{8}$";
for (lower_pattern, initial, path, value, valid, managed_pattern) in [
(
pattern,
serde_json::json!({}),
"a.patterns",
serde_json::json!(["["]),
false,
None,
),
(
pattern,
serde_json::json!({}),
"a.auth",
serde_json::json!(["token"]),
true,
None,
),
(
"[",
serde_json::json!({"a": {"patterns": [pattern]}}),
"a",
JsonValue::Null,
true,
None,
),
(
pattern,
serde_json::json!({"a": {"patterns": ["["]}}),
"a.patterns",
serde_json::json!(["("]),
true,
None,
),
(
pattern,
serde_json::json!({"b": {"env": ["NEW_PASSWORD"]}}),
"b.patterns",
serde_json::json!(["["]),
true,
None,
),
(
pattern,
serde_json::json!({"b": {"env": ["NEW_PASSWORD"]}}),
"b",
serde_json::json!({"env": ["NEW_PASSWORD"], "patterns": [pattern]}),
true,
None,
),
(
pattern,
serde_json::json!({"b": {"env": ["NEW_PASSWORD"]}}),
"",
serde_json::json!({"b": {"env": ["NEW_PASSWORD"], "patterns": [pattern]}}),
true,
None,
),
(
"[",
serde_json::json!({"a": {"env": ["VENDOR_PASSWORD"]}}),
"a.env",
serde_json::json!([]),
false,
Some(pattern),
),
] {
let lower: TomlValue = serde_json::from_value(serde_json::json!({
"features": {"network_proxy": {"credentials": {"a": {
"env": ["VENDOR_PASSWORD"], "patterns": [lower_pattern],
"url_prefixes": ["https://api.vendor.example"],
}}}}
}))?;
let original: TomlValue = serde_json::from_value(serde_json::json!({
"features": {"network_proxy": {"credentials": initial}}
}))?;
let mut config = original.clone();
let mut edits = CredentialProviderEdits::new(&config);
let key_path = if path.is_empty() {
"features.network_proxy.credentials".to_string()
} else {
format!("features.network_proxy.credentials.{path}")
};
edits
.apply(
&mut config,
&parse_key_path(&key_path).unwrap(),
parse_value(value).unwrap().as_ref(),
MergeStrategy::Replace,
)
.unwrap();
let mut layers = vec![
ConfigLayerEntry::new(ConfigLayerSource::System { file: file.clone() }, lower),
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: file.clone(),
profile: None,
},
config.clone(),
),
];
if let Some(pattern) = managed_pattern {
layers.push(ConfigLayerEntry::new(
ConfigLayerSource::LegacyManagedConfigTomlFromFile { file: file.clone() },
serde_json::from_value(serde_json::json!({"features": {"network_proxy": {"credentials": {"a": {"patterns": [pattern]}}}}}))?,
));
}
let layers = ConfigLayerStack::new(layers, Default::default(), Default::default())?;
assert_eq!(
edits
.validate_remapping(&original, &config, &layers)
.is_ok(),
valid,
"{path}"
);
}
Ok(())
}
#[test]
fn provider_draft_remapping_uses_final_source_ownership() -> Result<()> {
let tmp = tempdir()?;
let file = AbsolutePathBuf::try_from(tmp.path().join(CONFIG_TOML_FILE))?;
let config = |providers| -> TomlValue {
serde_json::from_value(serde_json::json!({
"features": {"network_proxy": {"credentials": providers}}
}))
.unwrap()
};
let complete = serde_json::json!({
"env": ["VENDOR_PASSWORD"], "patterns": ["^pin_[a-z]{8}$"],
"url_prefixes": ["https://api.vendor.example"],
});
let lower = config(serde_json::json!({"b": complete}));
let inherited_draft = config(serde_json::json!({
"a": complete, "b": {"url_prefixes": []},
}));
let inherited_only_draft = config(serde_json::json!({
"b": {"env": ["VENDOR_PASSWORD"]},
}));
let mut multi_source = serde_json::json!({"a": complete, "c": complete});
multi_source["a"]["env"] = serde_json::json!(["VENDOR_PASSWORD", "VENDOR_ALIAS"]);
multi_source["c"]["env"] = serde_json::json!(["OTHER_PASSWORD"]);
let mut actual = Vec::new();
let mut expected = Vec::new();
for (name, lower, original, updates, strategy, valid) in [
(
"automatic eviction reveals an inherited draft",
inherited_only_draft.clone(),
config(multi_source.clone()),
vec![("c.env", serde_json::json!(["VENDOR_ALIAS"]))],
MergeStrategy::Upsert,
false,
),
(
"explicit deletion after remapping remains allowed",
inherited_only_draft.clone(),
config(multi_source),
vec![
("c.env", serde_json::json!(["VENDOR_ALIAS"])),
("a", JsonValue::Null),
],
MergeStrategy::Upsert,
true,
),
(
"inherited-only draft receives the source",
inherited_only_draft.clone(),
config(serde_json::json!({"a": complete})),
vec![("a.env", serde_json::json!(["OTHER_PASSWORD"]))],
MergeStrategy::Replace,
false,
),
(
"deletion may reveal an inherited-only draft",
inherited_only_draft.clone(),
config(serde_json::json!({"a": complete})),
vec![("a", JsonValue::Null)],
MergeStrategy::Replace,
true,
),
(
"deleting both providers may reveal an inherited draft",
inherited_only_draft,
config(serde_json::json!({"a": complete, "b": {}})),
vec![("a", JsonValue::Null), ("b", JsonValue::Null)],
MergeStrategy::Replace,
true,
),
(
"inherited draft receives the source",
lower.clone(),
inherited_draft.clone(),
vec![("a.env", serde_json::json!(["OTHER_PASSWORD"]))],
MergeStrategy::Replace,
false,
),
(
"whole provider deletion remains allowed",
lower.clone(),
inherited_draft.clone(),
vec![("a", JsonValue::Null)],
MergeStrategy::Replace,
true,
),
(
"deletion with an unchanged draft edit",
lower.clone(),
inherited_draft.clone(),
vec![
("a", JsonValue::Null),
("b.url_prefixes", serde_json::json!([])),
],
MergeStrategy::Replace,
true,
),
(
"batch completes the recipient",
lower,
inherited_draft,
vec![
("a.env", serde_json::json!(["OTHER_PASSWORD"])),
("b.url_prefixes", serde_json::json!(["https://new.example"])),
],
MergeStrategy::Replace,
true,
),
(
"batch restores the draft source",
config(serde_json::json!({})),
config(serde_json::json!({"b": {"env": ["DRAFT_PASSWORD"]}})),
vec![
("b", serde_json::json!({"patterns": ["^pin_[a-z]{8}$"]})),
("b.env", serde_json::json!(["DRAFT_PASSWORD"])),
],
MergeStrategy::Replace,
true,
),
] {
let mut config = original.clone();
let mut edits = CredentialProviderEdits::new(&config);
for (path, value) in updates {
edits
.apply(
&mut config,
&parse_key_path(&format!("features.network_proxy.credentials.{path}")).unwrap(),
parse_value(value).unwrap().as_ref(),
strategy.clone(),
)
.unwrap();
}
let layers = ConfigLayerStack::new(
vec![
ConfigLayerEntry::new(ConfigLayerSource::System { file: file.clone() }, lower),
ConfigLayerEntry::new(
ConfigLayerSource::User {
file: file.clone(),
profile: None,
},
config.clone(),
),
],
Default::default(),
Default::default(),
)?;
actual.push((
name,
edits
.validate_remapping(&original, &config, &layers)
.is_ok(),
));
expected.push((name, valid));
}
assert_eq!(actual, expected);
Ok(())
}
#[test]
fn toml_value_to_item_handles_nested_config_tables() {
let config = r#"
[mcp_servers.docs]
command = "docs-server"
[mcp_servers.docs.http_headers]
X-Doc = "42"
"#;
let value: TomlValue = toml::from_str(config).expect("parse config example");
let item = toml_value_to_item(&value).expect("convert to toml_edit item");
let root = item.as_table().expect("root table");
assert!(!root.is_implicit(), "root table should be explicit");
let mcp_servers = root
.get("mcp_servers")
.and_then(TomlItem::as_table)
.expect("mcp_servers table");
assert!(
!mcp_servers.is_implicit(),
"mcp_servers table should be explicit"
);
let docs = mcp_servers
.get("docs")
.and_then(TomlItem::as_table)
.expect("docs table");
assert_eq!(
docs.get("command")
.and_then(TomlItem::as_value)
.and_then(toml_edit::Value::as_str),
Some("docs-server")
);
let http_headers = docs
.get("http_headers")
.and_then(TomlItem::as_table)
.expect("http_headers table");
assert_eq!(
http_headers
.get("X-Doc")
.and_then(TomlItem::as_value)
.and_then(toml_edit::Value::as_str),
Some("42")
);
}
#[tokio::test]
async fn write_value_preserves_comments_and_order() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let original = r#"# Codex user configuration
model = "gpt-5.2"
approval_policy = "on-request"
[notice]
# Preserve this comment
hide_full_access_warning = true
[features]
unified_exec = true
"#;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), original)?;
let service = ConfigManager::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()),
key_path: "features.personality".to_string(),
value: serde_json::json!(true),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("write succeeds");
let updated = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config");
let expected = r#"# Codex user configuration
model = "gpt-5.2"
approval_policy = "on-request"
[notice]
# Preserve this comment
hide_full_access_warning = true
[features]
unified_exec = true
personality = true
"#;
assert_eq!(updated, expected);
Ok(())
}
#[tokio::test]
async fn psp_feature_configures_first_party_routing() -> Result<()> {
let tmp = tempdir()?;
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
Vec::new(),
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleLoader::default(),
);
let config = service
.load_with_overrides(
Some(
[(
"features".to_string(),
serde_json::json!({ "apps": true, "psp": true }),
)]
.into_iter()
.collect(),
),
Default::default(),
)
.await?;
assert!(config.features.enabled(codex_features::Feature::Psp));
assert_eq!(
config.http_client_factory(),
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault)
.with_system_proxy_fallback()
.with_network_policy(config.application_network_policy.clone())
.with_chatgpt_cookies([HeaderValue::from_static("oai-chat-psp=true")])
);
assert_eq!(
config
.config_layer_stack
.effective_config()
.get("features")
.and_then(|features| features.get("psp")),
Some(&toml::Value::Boolean(true))
);
Ok(())
}
#[tokio::test]
async fn clear_missing_nested_config_is_noop() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "")?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let response = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "features.personality".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("clear missing config succeeds");
assert_eq!(response.status, WriteStatus::Ok);
assert_eq!(response.overridden_metadata, None);
assert_eq!(std::fs::read_to_string(&path)?, "");
Ok(())
}
#[tokio::test]
async fn clearing_user_setting_falls_back_to_packaged_default_without_override() -> Result<()> {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "hide_agent_reasoning = true\n")?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let response = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "hide_agent_reasoning".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await?;
assert_eq!(response.status, WriteStatus::Ok);
assert_eq!(response.overridden_metadata, None);
assert_eq!(std::fs::read_to_string(&path)?, "");
Ok(())
}
#[tokio::test]
async fn write_value_rejects_legacy_profile_selector() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "model = \"gpt-main\"\n")?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "profile".to_string(),
value: serde_json::json!("work"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("legacy profile selector write should fail");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert!(
error
.to_string()
.contains("`profile` is a legacy config selector"),
"{error}"
);
assert_eq!(std::fs::read_to_string(&path)?, "model = \"gpt-main\"\n");
Ok(())
}
#[tokio::test]
async fn write_value_rejects_legacy_profile_table() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "")?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "profiles.work.model".to_string(),
value: serde_json::json!("gpt-work"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("legacy profile table write should fail");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert!(
error
.to_string()
.contains("`profiles` contains legacy config profile tables"),
"{error}"
);
assert_eq!(std::fs::read_to_string(&path)?, "");
Ok(())
}
#[tokio::test]
async fn batch_write_rejects_legacy_profile_selector() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "model = \"gpt-main\"\n")?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.batch_write(ConfigBatchWriteParams {
edits: vec![
codex_app_server_protocol::ConfigEdit {
key_path: "model".to_string(),
value: serde_json::json!("gpt-work"),
merge_strategy: MergeStrategy::Replace,
},
codex_app_server_protocol::ConfigEdit {
key_path: "profile".to_string(),
value: serde_json::json!("work"),
merge_strategy: MergeStrategy::Replace,
},
],
file_path: Some(path.display().to_string()),
expected_version: None,
reload_user_config: false,
})
.await
.expect_err("legacy profile selector batch write should fail");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert!(
error
.to_string()
.contains("`profile` is a legacy config selector"),
"{error}"
);
assert_eq!(std::fs::read_to_string(&path)?, "model = \"gpt-main\"\n");
Ok(())
}
#[tokio::test]
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 = ConfigManager::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()),
key_path: "apps".to_string(),
value: serde_json::json!({
"app1": {
"enabled": false,
"omit_tools_from": ["deferred"],
},
}),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("write apps succeeds");
service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
key_path: "apps.app1.default_tools_approval_mode".to_string(),
value: serde_json::json!("prompt"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("write apps.app1.default_tools_approval_mode succeeds");
let read = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("config read succeeds");
assert_eq!(
read.config.apps,
Some(AppsConfig {
default: None,
apps: std::collections::HashMap::from([(
"app1".to_string(),
AppConfig {
enabled: false,
omit_tools_from: Some(vec![
codex_protocol::config_types::ToolExposureSurface::Deferred
]),
approvals_reviewer: None,
destructive_enabled: None,
open_world_enabled: None,
default_tools_approval_mode: Some(AppToolApproval::Prompt),
default_tools_enabled: None,
tools: None,
links: None,
},
)]),
})
);
Ok(())
}
#[tokio::test]
async fn write_value_supports_custom_mcp_server_default_tool_approval_mode() -> Result<()> {
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
"[mcp_servers.docs]\ncommand = \"docs-server\"\n",
)?;
let service = ConfigManager::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()),
key_path: "mcp_servers.docs.default_tools_approval_mode".to_string(),
value: serde_json::json!("approve"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("write mcp server default_tools_approval_mode succeeds");
let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE))?;
assert!(contents.contains("default_tools_approval_mode = \"approve\""));
let read = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("config read succeeds");
assert_eq!(
read.config
.additional
.get("mcp_servers")
.and_then(|servers| servers.get("docs"))
.and_then(|docs| docs.get("default_tools_approval_mode")),
Some(&serde_json::json!("approve"))
);
Ok(())
}
#[tokio::test]
async fn read_includes_origins_and_layers() {
let tmp = tempdir().expect("tempdir");
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&user_path, "model = \"user\"").unwrap();
let user_file = AbsolutePathBuf::try_from(user_path.clone()).expect("user file");
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudConfigBundleLoader::default(),
);
let response = service
.read(ConfigReadParams {
include_layers: true,
cwd: None,
})
.await
.expect("response");
assert_eq!(response.config.approval_policy, Some(AskForApproval::Never));
assert_eq!(
response
.origins
.get("approval_policy")
.expect("origin")
.name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile {
file: managed_file.clone()
},
);
let layers = response.layers.expect("layers present");
// Local macOS machines can surface an MDM-managed config layer at the
// top of the stack; ignore it so this test stays focused on file/user/system ordering.
let layers = if matches!(
layers.first().map(|layer| &layer.name),
Some(ApiConfigLayerSource::LegacyManagedConfigTomlFromMdm)
) {
&layers[1..]
} else {
layers.as_slice()
};
assert_eq!(layers.len(), 3, "expected three layers");
assert_eq!(
layers.first().unwrap().name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile {
file: managed_file.clone()
}
);
assert_eq!(
layers.get(1).unwrap().name,
ApiConfigLayerSource::User {
file: user_file.clone(),
profile: None,
}
);
assert!(matches!(
layers.get(2).unwrap().name,
ApiConfigLayerSource::System { .. }
));
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn write_value_succeeds_when_managed_preferences_expand_home_directory_paths() -> Result<()> {
use base64::Engine;
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"\n")?;
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(),
),
);
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
loader_overrides,
CloudConfigBundleLoader::default(),
);
let response = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
key_path: "model".to_string(),
value: serde_json::json!("updated"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("write succeeds");
assert_eq!(response.status, WriteStatus::Ok);
assert_eq!(
std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config"),
"model = \"updated\"\n"
);
Ok(())
}
#[tokio::test]
async fn write_value_reports_override() {
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
"approval_policy = \"on-request\"",
)
.unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudConfigBundleLoader::default(),
);
let result = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
key_path: "approval_policy".to_string(),
value: serde_json::json!("never"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("result");
let read_after = service
.read(ConfigReadParams {
include_layers: true,
cwd: None,
})
.await
.expect("read");
assert_eq!(
read_after.config.approval_policy,
Some(AskForApproval::Never)
);
assert_eq!(
read_after
.origins
.get("approval_policy")
.expect("origin")
.name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile {
file: managed_file.clone()
}
);
assert_eq!(result.status, WriteStatus::Ok);
assert!(result.overridden_metadata.is_none());
}
#[tokio::test]
async fn provider_setup_checks_versions_and_provider_write_uses_last_write_wins() -> Result<()> {
let tmp = tempdir()?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let layers = service.load_config_layers(/*cwd*/ None).await?;
let (write, _) = service.prepare_provider_config(
&layers,
codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID,
[("aws.region", Some("us-west-2"))],
)?;
let user_path = tmp.path().join(CONFIG_TOML_FILE);
let newer_config = "model = 'newer'";
std::fs::write(&user_path, newer_config)?;
let error = service.commit_provider_config(write).await.unwrap_err();
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigVersionConflict)
);
assert_eq!(std::fs::read_to_string(user_path)?, newer_config);
// An intervening writer changes both the requested field and an omitted sibling.
// The patch wins for the requested field and preserves the newer sibling value.
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
"model = 'before'\n[model_providers.gateway]\nname = 'Before'\nenv_key = 'BEFORE_KEY'",
)?;
let calls = std::sync::atomic::AtomicUsize::new(/*v*/ 0);
let config_path = tmp.path().join(CONFIG_TOML_FILE);
let loader = CloudConfigBundleLoader::from_getter(move || {
if calls.fetch_add(/*val*/ 1, std::sync::atomic::Ordering::SeqCst) == 1 {
std::fs::write(&config_path, "model = 'newer'\n[model_providers.gateway]\nname = 'Another writer'\nenv_key = 'OTHER_KEY'").expect("intervening config write");
}
std::future::ready(Ok(None))
});
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
loader,
);
let response = service
.model_provider_config_write(serde_json::from_value(serde_json::json!({
"settings":{"type":"customGateway","providerId":"gateway","name":"Gateway", "baseUrl":"https://gateway.example.test"}
}))?)
.await?;
assert_eq!(
serde_json::to_value(response)?,
serde_json::json!({"provider": {
"id":"gateway", "name":"Gateway", "canEdit":true,
"baseUrl":"https://gateway.example.test",
"settings":{"type":"customGateway", "auth":{"mode":"environmentVariable", "envKey":"OTHER_KEY"}}
}})
);
let saved: serde_json::Value =
toml::from_str(&std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE))?)?;
assert_eq!(
saved,
serde_json::json!({"model":"newer", "model_providers":{
"gateway":{"name":"Gateway", "base_url":"https://gateway.example.test", "env_key":"OTHER_KEY"}
}})
);
Ok(())
}
#[tokio::test]
async fn version_conflict_rejected() {
let tmp = tempdir().expect("tempdir");
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&user_path, "model = \"user\"").unwrap();
let service = ConfigManager::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()),
key_path: "model".to_string(),
value: serde_json::json!("gpt-5.2"),
merge_strategy: MergeStrategy::Replace,
expected_version: Some("sha256:bogus".to_string()),
})
.await
.expect_err("should fail");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigVersionConflict)
);
}
#[tokio::test]
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 = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: None,
key_path: "model".to_string(),
value: serde_json::json!("gpt-new"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("write succeeds");
let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config");
assert!(
contents.contains("model = \"gpt-new\""),
"config.toml should be updated even when file_path is omitted"
);
}
#[tokio::test]
async fn model_provider_settings_use_the_active_profile_layer() -> Result<()> {
use codex_app_server_protocol::CustomGatewaySettingsParams;
use codex_app_server_protocol::GatewayAuth;
use codex_app_server_protocol::ModelProviderConfigWriteParams;
use codex_app_server_protocol::ModelProviderListParams;
use codex_app_server_protocol::ProviderSettings;
use codex_app_server_protocol::ProviderSettingsPatch;
let home = tempdir()?;
let profile = home.path().join("work.config.toml");
let system = home.path().join("system.toml");
std::fs::write(
&system,
"model_provider = 'gcp'\n[model_providers.gateway]\nname = 'System'\nenv_key = 'SYSTEM_KEY'",
)?;
let overrides = LoaderOverrides {
system_config_path: Some(system.clone()),
user_config_path: Some(AbsolutePathBuf::from_absolute_path(&profile)?),
user_config_profile: Some("work".parse()?),
..LoaderOverrides::without_managed_config_for_tests()
};
let service = |cli_overrides| {
ConfigManager::new_for_tests(
home.path().to_path_buf(),
cli_overrides,
overrides.clone(),
CloudConfigBundleLoader::default(),
)
};
let manager = service(vec![]);
let list = ModelProviderListParams {};
let initial = manager.model_provider_list(list.clone()).await?;
assert_eq!(initial.default_provider_id, "gcp");
assert!(initial.can_change_default);
std::fs::write(&profile, "model_provider = 'openai'")?;
let write = ModelProviderConfigWriteParams {
settings: ProviderSettingsPatch::CustomGateway(CustomGatewaySettingsParams {
name: Some("Gateway".to_owned()),
base_url: Some(Some("https://gateway.example.test".to_owned())),
..Default::default()
}),
};
let created = manager.model_provider_config_write(write).await?;
let selected = manager.model_provider_list(list.clone()).await?;
assert_eq!(selected.default_provider_id, "openai");
assert!(selected.can_change_default);
assert!(selected.data[0].can_edit);
let write = ModelProviderConfigWriteParams {
settings: ProviderSettingsPatch::CustomGateway(CustomGatewaySettingsParams {
provider_id: Some(created.provider.id.clone()),
name: Some("Renamed".to_owned()),
..Default::default()
}),
};
manager.model_provider_config_write(write).await?;
let saved: TomlValue = toml::from_str(&std::fs::read_to_string(&profile)?)?;
assert_eq!(
saved["model_providers"][&created.provider.id]["name"].as_str(),
Some("Renamed")
);
assert!(!home.path().join(CONFIG_TOML_FILE).exists());
let inherited = |mut settings: serde_json::Value| {
settings["type"] = "customGateway".into();
settings["providerId"] = "gateway".into();
serde_json::from_value(serde_json::json!({"settings":settings}))
};
let before = std::fs::read_to_string(&profile)?;
manager
.model_provider_config_write(inherited(serde_json::json!({"baseUrl":null}))?)
.await?;
assert_eq!(std::fs::read_to_string(&profile)?, before);
manager
.model_provider_config_write(inherited(serde_json::json!({"envKey":"USER_KEY"}))?)
.await?;
let cleared = manager
.model_provider_config_write(inherited(
serde_json::json!({"name":"Work", "envKey":null}),
)?)
.await?;
assert_eq!(
cleared.provider.settings,
ProviderSettings::CustomGateway {
auth: GatewayAuth::EnvironmentVariable {
env_key: "SYSTEM_KEY".to_owned()
}
}
);
let saved: serde_json::Value = toml::from_str(&std::fs::read_to_string(&profile)?)?;
assert_eq!(
saved["model_providers"]["gateway"],
serde_json::json!({"name":"Work"})
);
let pinned_manager = service(vec![(
"model_providers.gateway.env_key".to_owned(),
"CLI_KEY".into(),
)]);
let before = std::fs::read_to_string(&profile)?;
let err = pinned_manager
.model_provider_config_write(inherited(
serde_json::json!({"name":"Must not save", "envKey":"USER_KEY"}),
)?)
.await
.unwrap_err();
assert_eq!(
err.write_error_code(),
Some(ConfigWriteErrorCode::ConfigLayerReadonly)
);
assert_eq!(std::fs::read_to_string(&profile)?, before);
let renamed = pinned_manager
.model_provider_config_write(inherited(serde_json::json!({"name":"Allowed"}))?)
.await?;
assert_eq!(
(renamed.provider.name.as_str(), renamed.provider.settings),
(
"Allowed",
ProviderSettings::CustomGateway {
auth: GatewayAuth::EnvironmentVariable {
env_key: "CLI_KEY".to_owned()
}
}
)
);
let pinned = service(vec![("model_provider".to_owned(), "gcp".into())])
.model_provider_list(list)
.await?;
assert_eq!(pinned.default_provider_id, "gcp");
assert!(!pinned.can_change_default);
Ok(())
}
#[tokio::test]
async fn write_value_defaults_to_selected_user_config_path() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"gpt-main\"").unwrap();
let selected_path = tmp.path().join("work.config.toml");
std::fs::write(&selected_path, "").unwrap();
let mut loader_overrides =
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml"));
loader_overrides.user_config_path =
Some(AbsolutePathBuf::from_absolute_path(&selected_path).expect("selected config path"));
loader_overrides.user_config_profile = Some("work".parse().expect("profile-v2 name"));
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
loader_overrides,
CloudConfigBundleLoader::default(),
);
service
.write_value(ConfigValueWriteParams {
file_path: None,
key_path: "model".to_string(),
value: serde_json::json!("gpt-work"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("write succeeds");
assert_eq!(
std::fs::read_to_string(&selected_path).expect("read selected config"),
"model = \"gpt-work\"\n"
);
assert_eq!(
std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read main config"),
"model = \"gpt-main\""
);
}
#[tokio::test]
async fn load_default_config_preserves_managed_requirements_and_selected_user_config_path() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"gpt-main\"").unwrap();
std::fs::write(
tmp.path().join("requirements.toml"),
"allowed_login_methods = [\"api\"]\nallowed_chatgpt_workspaces = [\"managed-workspace\"]\n",
)
.unwrap();
let selected_path = tmp.path().join("work.config.toml");
std::fs::write(&selected_path, "not valid toml").unwrap();
let selected_file =
AbsolutePathBuf::from_absolute_path(&selected_path).expect("selected config path");
let mut loader_overrides =
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml"));
loader_overrides.user_config_path = Some(selected_file.clone());
loader_overrides.user_config_profile = Some("work".parse().expect("profile-v2 name"));
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
loader_overrides,
CloudConfigBundleLoader::default(),
);
service
.load_latest_config(/*fallback_cwd*/ None)
.await
.expect_err("selected config should fail to load");
let config = service
.load_default_config()
.await
.expect("default config loads after selected config error");
assert_eq!(
config.config_layer_stack.get_user_config_file(),
Some(&selected_file)
);
assert_eq!(
config
.config_layer_stack
.requirements()
.managed_auth_policy(),
codex_config::ManagedAuthPolicy {
allowed_login_methods: Some(vec![codex_protocol::config_types::ForcedLoginMethod::Api]),
allowed_chatgpt_workspaces: Some(vec!["managed-workspace".to_string()]),
}
);
}
#[tokio::test]
async fn managed_auth_policy_survives_unusable_requirements_file_changes() -> Result<()> {
let tmp = tempdir()?;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
let requirements_path = tmp.path().join("requirements.toml");
std::fs::write(
&requirements_path,
"allowed_login_methods = [\"api\"]\nallowed_chatgpt_workspaces = [\"startup\"]\n",
)?;
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
Vec::new(),
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")),
CloudConfigBundleLoader::default(),
);
let startup = service.load_latest_config(/*fallback_cwd*/ None).await?;
let auth_manager = codex_login::AuthManager::shared_from_config(
&startup, /*enable_codex_api_key_env*/ false,
)
.await?;
std::fs::write(
&requirements_path,
"allowed_login_methods = [\"chatgpt\"]\nallowed_chatgpt_workspaces = []\n",
)?;
for refreshed in [
service.load_latest_config(/*fallback_cwd*/ None).await?,
service
.load_latest_config_with_session_layers(&startup.config_layer_stack, &startup.cwd)
.await?,
] {
assert_eq!(refreshed.forced_login_method, None);
assert_eq!(refreshed.forced_chatgpt_workspace_id, None);
}
assert!(
auth_manager.is_login_method_allowed(codex_protocol::config_types::ForcedLoginMethod::Api)
);
assert!(
!auth_manager
.is_login_method_allowed(codex_protocol::config_types::ForcedLoginMethod::Chatgpt)
);
assert_eq!(
auth_manager.effective_chatgpt_workspaces(),
Some(vec!["startup".to_string()])
);
Ok(())
}
#[tokio::test]
async fn provider_remapping_validates_fragments_even_when_source_is_overridden() -> Result<()> {
for (managed_provider, strategy) in [
(None, MergeStrategy::Upsert),
(None, MergeStrategy::Replace),
(Some("z_new"), MergeStrategy::Upsert),
(Some("z_new"), MergeStrategy::Replace),
(Some("z_managed"), MergeStrategy::Upsert),
(Some("z_managed"), MergeStrategy::Replace),
] {
let tmp = tempdir()?;
let initial = r#"[features.network_proxy.credentials.a_working]
env = ["VENDOR_PASSWORD"]
patterns = ["^pin_[a-z]{8}$"]
url_prefixes = ["https://old.example"]
"#;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), initial)?;
let managed_path = tmp.path().join("managed.toml");
if let Some(managed_provider) = managed_provider {
std::fs::write(
&managed_path,
format!(
r#"[features.network_proxy.credentials.{managed_provider}]
env = ["VENDOR_PASSWORD"]
patterns = ["^pin_[a-z]{{8}}$"]
url_prefix_from_env = "VENDOR_ENDPOINT"
"#
),
)?;
}
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path),
CloudConfigBundleLoader::default(),
);
let result = service
.write_value(ConfigValueWriteParams {
file_path: None,
key_path: match strategy {
MergeStrategy::Upsert => "features.network_proxy.credentials.z_new.env",
MergeStrategy::Replace => "features.network_proxy.credentials",
}
.to_string(),
value: match strategy {
MergeStrategy::Upsert => serde_json::json!(["VENDOR_PASSWORD"]),
MergeStrategy::Replace => {
serde_json::json!({"z_new": {"env": ["VENDOR_PASSWORD"]}})
}
},
merge_strategy: strategy,
expected_version: None,
})
.await;
if managed_provider == Some("z_new") {
result?;
} else {
assert_eq!(
result.unwrap_err().write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert_eq!(
std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE))?,
initial
);
}
}
Ok(())
}
#[tokio::test]
async fn thread_refresh_resolves_preserved_layers_before_materialization() -> Result<()> {
use codex_core::config::ConfigOverrides;
use codex_features::Feature;
use std::collections::HashMap;
use std::sync::Arc;
let tmp = tempdir()?;
let selected_path = tmp.path().join("work.config.toml");
std::fs::write(&selected_path, "log_dir = \"old-logs\"\n")?;
let mut loader_overrides = LoaderOverrides::without_managed_config_for_tests();
loader_overrides.user_config_path = Some(AbsolutePathBuf::from_absolute_path(&selected_path)?);
loader_overrides.user_config_profile = Some("work".parse()?);
let service = ConfigManager::new(
tmp.path().to_path_buf(),
Vec::new(),
loader_overrides,
/*strict_config*/ true,
CloudConfigBundleLoader::default(),
Default::default(),
Arc::new(codex_config::NoopThreadConfigLoader),
);
let thread = service
.load_for_cwd(
Some(HashMap::from([(
"model_provider".to_string(),
serde_json::json!("openai"),
)])),
ConfigOverrides::default(),
Some(tmp.path().to_path_buf()),
)
.await?;
service
.extend_runtime_feature_enablement([
("secret_auth_storage".to_string(), true),
("mcp_oauth_refresh_coordination".to_string(), true),
])
.expect("set runtime features");
std::fs::write(
&selected_path,
"model_provider = \"missing-provider\"\nlog_dir = \"new-logs\"\n\
[features]\nmcp_oauth_refresh_coordination = false\n",
)?;
// A throwaway host configuration is invalid; the actual thread's preserved
// provider is valid. Refresh must materialize only the final composed layers.
service
.load_latest_config(Some(tmp.path().to_path_buf()))
.await
.expect_err("host provider is unavailable");
let refreshed = service
.load_latest_config_with_session_layers(&thread.config_layer_stack, &thread.cwd)
.await?;
assert_eq!(refreshed.model_provider_id, "openai");
assert_eq!(refreshed.cwd, thread.cwd);
assert_eq!(refreshed.log_dir, tmp.path().join("new-logs"));
assert!(refreshed.features.enabled(Feature::SecretAuthStorage));
assert!(
!refreshed
.features
.enabled(Feature::McpOAuthRefreshCoordination)
);
std::fs::write(&selected_path, "approval_policy = false\n")?;
let error = service
.load_latest_config_with_session_layers(&thread.config_layer_stack, &thread.cwd)
.await
.expect_err("invalid final config is rejected");
assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
assert!(error.to_string().contains("work.config.toml"), "{error}");
Ok(())
}
#[tokio::test]
async fn invalid_user_value_rejected_even_if_overridden_by_managed() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "model = \"user\"").unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudConfigBundleLoader::default(),
);
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
key_path: "approval_policy".to_string(),
value: serde_json::json!("bogus"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("should fail validation");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config");
assert_eq!(contents.trim(), "model = \"user\"");
}
#[tokio::test]
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 = ConfigManager::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()),
key_path: "model_providers.openai.name".to_string(),
value: serde_json::json!("OpenAI Override"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("should reject reserved provider override");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert!(error.to_string().contains("reserved built-in provider IDs"));
assert!(error.to_string().contains("`openai`"));
let contents = std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).expect("read config");
assert_eq!(contents, "model = \"user\"\n");
}
#[tokio::test]
async fn write_value_rejects_invalid_guardian_review_threshold() -> Result<()> {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
let initial = "[features.guardianv2]\nenabled = true\nreview_threshold = 0.8\n";
std::fs::write(&path, initial)?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "features.guardianv2.review_threshold".to_string(),
value: serde_json::json!(2.0),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("Guardian review thresholds above 1.0 must be rejected");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert_eq!(std::fs::read_to_string(&path)?, initial);
Ok(())
}
#[tokio::test]
async fn write_value_rejects_feature_requirement_conflict() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap();
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
[features]
fast_mode = true
"#,
),
);
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
key_path: "features.fast_mode".to_string(),
value: serde_json::json!(false),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("conflicting feature write should fail");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert!(
error
.to_string()
.contains("invalid value for `features`: `features.fast_mode=false`"),
"{error}"
);
assert_eq!(
std::fs::read_to_string(tmp.path().join(CONFIG_TOML_FILE)).unwrap(),
""
);
}
#[tokio::test]
async fn write_value_rejects_exact_managed_requirement() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "allow_login_shell = true\n").unwrap();
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement("allow_login_shell = false"),
);
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "allow_login_shell".to_string(),
value: serde_json::json!(true),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("managed exact field should be read-only");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigRequirementReadonly)
);
assert!(error.to_string().contains("`allow_login_shell`"));
assert_eq!(
std::fs::read_to_string(path).unwrap(),
"allow_login_shell = true\n"
);
}
#[tokio::test]
async fn write_value_rejects_managed_otel_values_and_preserves_unmanaged_siblings() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "").expect("write user config");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"[otel]
tool_result = { max_bytes = 1024 }
log_user_prompt = false
log_agent_responses = true
log_guardian_assessments = false
exporter = "none"
trace_exporter = "none"
metrics_exporter = "none"
span_attributes = { "service.name" = "managed-service" }
tracestate = { managed = { tenant = "enterprise" }, empty = {} }"#,
),
);
for (key_path, managed) in [
("otel", true),
("otel.log_user_prompt", true),
("otel.log_agent_responses", true),
("otel.log_guardian_assessments", true),
("otel.exporter.otlp-http.endpoint", true),
("otel.trace_exporter.otlp-grpc.endpoint", true),
("otel.metrics_exporter.otlp-http.headers", true),
("otel.span_attributes.\"service.name\"", true),
("otel.tracestate.managed.tenant", true),
("otel.environment", false),
("otel.tool_result.max_bytes", true),
("otel.span_attributes.\"service.version\"", false),
("otel.tracestate.local.tenant", false),
("otel.tracestate.managed.region", false),
("otel.tracestate.empty.tenant", false),
] {
let result = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: key_path.to_string(),
value: serde_json::json!("user"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await;
assert_eq!(
result.err().and_then(|error| error.write_error_code()),
managed.then_some(ConfigWriteErrorCode::ConfigRequirementReadonly),
"OTEL path {key_path}"
);
}
}
fn toml_path(tmp: &Path, name: &str) -> String {
tmp.join(name).to_string_lossy().replace('\\', "\\\\")
}
#[tokio::test]
async fn read_omits_origins_for_exact_managed_values() {
for has_user_values in [true, false] {
let tmp = tempdir().expect("tempdir");
let user_config = if has_user_values {
format!(
r#"model = "user-model"
sqlite_home = "{}"
allow_login_shell = true
[feedback]
enabled = true
"#,
toml_path(tmp.path(), "user-sqlite"),
)
} else {
"model = \"user-model\"\n".to_string()
};
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), user_config).unwrap();
let requirements = format!(
r#"sqlite_home = "{}"
allow_login_shell = false
[feedback]
enabled = false
"#,
toml_path(tmp.path(), "managed-sqlite"),
);
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement(requirements),
);
let response = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("config read should succeed");
assert_eq!(
response.config.additional.get("sqlite_home"),
Some(&serde_json::json!(tmp.path().join("managed-sqlite")))
);
assert_eq!(
response.config.additional.get("allow_login_shell"),
Some(&serde_json::json!(false))
);
assert_eq!(
response.config.additional.get("feedback"),
Some(&serde_json::json!({"enabled": false}))
);
for path in ["sqlite_home", "allow_login_shell", "feedback.enabled"] {
assert!(!response.origins.contains_key(path), "origin for {path}");
}
assert!(response.origins.contains_key("model"));
}
}
#[tokio::test]
async fn read_projects_managed_otel_values_and_preserves_unmanaged_settings() {
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
r#"[otel]
tool_result = { max_bytes = 1234 }
log_user_prompt = true
environment = "user"
exporter = "statsig"
span_attributes = { "service.name" = "user-service", "service.version" = "user-version" }
tracestate = { managed = { tenant = "user-tenant", region = "us" }, local = { tenant = "preserved" }, empty = { tenant = "also-preserved" } }"#,
)
.expect("write user config");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"[otel]
log_user_prompt = false
exporter = "none"
span_attributes = { "service.name" = "managed-service" }
tracestate = { managed = { tenant = "enterprise" }, empty = {} }"#,
),
);
let response = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("config read should succeed");
let otel = response
.config
.additional
.get("otel")
.expect("effective OTEL config");
assert_eq!(
otel,
&serde_json::json!({
"tool_result": { "max_bytes": 1234 },
"log_sse_deltas": null,
"log_user_prompt": false,
"log_agent_responses": null,
"log_guardian_assessments": null,
"environment": "user",
"exporter": "none",
"trace_exporter": null,
"metrics_exporter": null,
"span_attributes": {
"service.name": "managed-service",
"service.version": "user-version",
},
"tracestate": {
"managed": { "tenant": "enterprise", "region": "us" },
"local": { "tenant": "preserved" },
"empty": { "tenant": "also-preserved" },
},
}),
);
for (path, user_owned) in [
("otel.log_user_prompt", false),
("otel.exporter", false),
("otel.span_attributes.service.name", false),
("otel.tracestate.managed.tenant", false),
("otel.tracestate.managed.region", true),
("otel.environment", true),
("otel.tool_result.max_bytes", true),
("otel.span_attributes.service.version", true),
("otel.tracestate.local.tenant", true),
("otel.tracestate.empty.tenant", true),
] {
assert_eq!(response.origins.contains_key(path), user_owned, "{path}");
}
}
#[tokio::test]
async fn read_projects_managed_otel_tool_result_and_omits_its_origin() {
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
r#"[otel]
tool_result = { max_bytes = 1234 }"#,
)
.expect("write user config");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"[otel]
tool_result = { max_bytes = 4096 }"#,
),
);
let response = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("config read should succeed");
assert_eq!(
response
.config
.additional
.get("otel")
.and_then(|otel| otel.get("tool_result")),
Some(&serde_json::json!({ "max_bytes": 4096 }))
);
assert!(!response.origins.contains_key("otel.tool_result.max_bytes"));
}
#[tokio::test]
async fn read_keeps_omitted_otel_tool_result_unset() {
let tmp = tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(CONFIG_TOML_FILE),
r#"[otel]
environment = "user""#,
)
.expect("write user config");
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let response = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("config read should succeed");
assert_eq!(
response
.config
.additional
.get("otel")
.and_then(|otel| otel.get("tool_result")),
Some(&serde_json::Value::Null)
);
}
#[tokio::test]
async fn read_materializes_default_allow_login_shell() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap();
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let response = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("config read should succeed");
assert_eq!(
response.config.additional.get("allow_login_shell"),
Some(&serde_json::json!(true))
);
}
#[tokio::test]
async fn write_value_allows_unmanaged_setting_with_exact_requirement() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, "").unwrap();
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement(
r#"
allow_login_shell = false
"#,
),
);
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "windows.sandbox".to_string(),
value: serde_json::json!("elevated"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("unmanaged setting should remain writable");
assert!(
std::fs::read_to_string(path)
.unwrap()
.contains("sandbox = \"elevated\"")
);
}
#[tokio::test]
async fn read_reports_managed_overrides_user_and_session_flags() {
let tmp = tempdir().expect("tempdir");
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&user_path, "model = \"user\"").unwrap();
let user_file = AbsolutePathBuf::try_from(user_path.clone()).expect("user file");
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "model = \"system\"").unwrap();
let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file");
let cli_overrides = vec![(
"model".to_string(),
TomlValue::String("session".to_string()),
)];
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
cli_overrides,
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudConfigBundleLoader::default(),
);
let response = service
.read(ConfigReadParams {
include_layers: true,
cwd: None,
})
.await
.expect("response");
assert_eq!(response.config.model.as_deref(), Some("system"));
assert_eq!(
response.origins.get("model").expect("origin").name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile {
file: managed_file.clone()
},
);
let layers = response.layers.expect("layers");
// Local macOS machines can surface an MDM-managed config layer at the
// top of the stack; ignore it so this test stays focused on file/session/user ordering.
let layers = if matches!(
layers.first().map(|layer| &layer.name),
Some(ApiConfigLayerSource::LegacyManagedConfigTomlFromMdm)
) {
&layers[1..]
} else {
layers.as_slice()
};
assert_eq!(
layers.first().unwrap().name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file }
);
assert_eq!(
layers.get(1).unwrap().name,
ApiConfigLayerSource::SessionFlags
);
assert_eq!(
layers.get(2).unwrap().name,
ApiConfigLayerSource::User {
file: user_file,
profile: None
}
);
}
#[tokio::test]
async fn write_value_reports_managed_override() {
let tmp = tempdir().expect("tempdir");
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "").unwrap();
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, "approval_policy = \"never\"").unwrap();
let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudConfigBundleLoader::default(),
);
let result = service
.write_value(ConfigValueWriteParams {
file_path: Some(tmp.path().join(CONFIG_TOML_FILE).display().to_string()),
key_path: "approval_policy".to_string(),
value: serde_json::json!("on-request"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("result");
assert_eq!(result.status, WriteStatus::OkOverridden);
let overridden = result.overridden_metadata.expect("overridden metadata");
assert_eq!(
overridden.overriding_layer.name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file }
);
assert_eq!(overridden.effective_value, serde_json::json!("never"));
}
#[tokio::test]
async fn write_overrides_redact_managed_otel_exporter_headers() {
let tmp = tempdir().expect("tempdir");
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&user_path, "").expect("write user config");
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(
&managed_path,
r#"[otel]
environment = "managed"
exporter = { otlp-http = { endpoint = "https://collector.example", protocol = "json", headers = { Authorization = "admin-secret", "x.api.key" = "dotted-secret" } } }
trace_exporter = { otlp-grpc = { endpoint = "https://trace.example", headers = { Authorization = "trace-secret" } } }
metrics_exporter = { otlp-http = { endpoint = "https://metrics.example", protocol = "json", headers = { Authorization = "metrics-secret" } } }
span_attributes = { "service.name" = "managed-service" }"#,
)
.expect("write managed config");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path),
CloudConfigBundleLoader::default(),
);
let exporter = serde_json::json!({
"otlp-http": {
"endpoint": "https://collector.example",
"headers": { "Authorization": "[redacted]", "x.api.key": "[redacted]" },
"protocol": "json"
}
});
let otel = serde_json::json!({
"environment": "managed",
"exporter": exporter.clone(),
"trace_exporter": {
"otlp-grpc": {
"endpoint": "https://trace.example",
"headers": { "Authorization": "[redacted]" }
}
},
"metrics_exporter": {
"otlp-http": {
"endpoint": "https://metrics.example",
"protocol": "json",
"headers": { "Authorization": "[redacted]" }
}
},
"span_attributes": { "service.name": "managed-service" }
});
for (key_path, expected) in [
("otel", otel.clone()),
("otel.exporter", exporter.clone()),
("otel.exporter.otlp-http", exporter["otlp-http"].clone()),
(
"otel.exporter.otlp-http.headers",
exporter["otlp-http"]["headers"].clone(),
),
(
"otel.exporter.otlp-http.headers.Authorization",
serde_json::json!("[redacted]"),
),
(
"otel.exporter.otlp-http.headers.\"x.api.key\"",
serde_json::json!("[redacted]"),
),
(
"otel.exporter.otlp-http.endpoint",
serde_json::json!("https://collector.example"),
),
(
"otel.exporter.otlp-http.protocol",
serde_json::json!("json"),
),
("otel.trace_exporter", otel["trace_exporter"].clone()),
("otel.metrics_exporter", otel["metrics_exporter"].clone()),
(
"otel.metrics_exporter.otlp-http.headers.Authorization",
serde_json::json!("[redacted]"),
),
("otel.environment", serde_json::json!("managed")),
(
"otel.span_attributes.\"service.name\"",
serde_json::json!("managed-service"),
),
] {
let response = service
.write_value(ConfigValueWriteParams {
file_path: None,
key_path: key_path.to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("no-op config clear should succeed");
assert_eq!(response.status, WriteStatus::OkOverridden, "{key_path}");
assert_eq!(
response
.overridden_metadata
.expect("managed override")
.effective_value,
expected,
"{key_path}"
);
assert_eq!(std::fs::read_to_string(&user_path).unwrap(), "");
}
let response = service
.batch_write(ConfigBatchWriteParams {
edits: vec![codex_app_server_protocol::ConfigEdit {
key_path: "otel.exporter.otlp-http.headers.Authorization".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
}],
file_path: None,
expected_version: None,
reload_user_config: false,
})
.await
.expect("no-op batch clear should succeed");
assert_eq!(response.status, WriteStatus::OkOverridden);
assert_eq!(
response
.overridden_metadata
.expect("managed batch override")
.effective_value,
serde_json::json!("[redacted]")
);
assert_eq!(std::fs::read_to_string(&user_path).unwrap(), "");
let read = service
.read(ConfigReadParams {
include_layers: true,
cwd: None,
})
.await
.expect("read public config");
let mut effective_exporter = exporter.clone();
effective_exporter["otlp-http"]["tls"] = serde_json::Value::Null;
assert_eq!(
read.config.additional["otel"]["exporter"],
effective_exporter
);
let managed_layer = read
.layers
.expect("config layers")
.into_iter()
.find(|layer| {
matches!(
layer.name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
)
})
.expect("legacy managed layer");
assert_eq!(managed_layer.config["otel"]["exporter"], exporter);
let runtime = service
.load_latest_config(/*fallback_cwd*/ None)
.await
.expect("load runtime config");
let OtelExporterKind::OtlpHttp { headers, .. } = runtime.otel.exporter else {
panic!("unexpected runtime exporter");
};
assert_eq!(
headers.get("Authorization").map(|value| value.as_str()),
Some("admin-secret")
);
assert_eq!(
headers.get("x.api.key").map(|value| value.as_str()),
Some("dotted-secret")
);
}
/// Legacy managed feature toggles own their normalized enabled origin and override metadata.
#[tokio::test]
async fn multi_agent_v2_boolean_layer_owns_enabled_origin_and_overrides() {
let tmp = tempdir().expect("tempdir");
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(
&user_path,
"[features.multi_agent_v2]\nenabled = true\nsubagent_usage_hint_text = \"keep\"\n\n[features.network_proxy]\nenabled = true\ncredential_broker = true\n",
)
.expect("user config");
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(
&managed_path,
"[features]\nmulti_agent_v2 = false\nnetwork_proxy = false\n",
)
.expect("managed config");
let managed_file = AbsolutePathBuf::try_from(managed_path.clone()).expect("managed file");
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path),
CloudConfigBundleLoader::default(),
);
let read = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.expect("read config");
for path in [
"features.multi_agent_v2.enabled",
"features.network_proxy",
"features.network_proxy.enabled",
] {
assert_eq!(
read.origins.get(path).expect("managed feature origin").name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile {
file: managed_file.clone(),
},
);
}
let result = service
.write_value(ConfigValueWriteParams {
file_path: Some(user_path.display().to_string()),
key_path: "features.multi_agent_v2.enabled".to_string(),
value: serde_json::json!(true),
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await
.expect("write config");
assert_eq!(result.status, WriteStatus::OkOverridden);
let overridden = result.overridden_metadata.expect("overridden metadata");
assert_eq!(
overridden.overriding_layer.name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file }
);
assert_eq!(overridden.effective_value, serde_json::json!(false));
}
#[tokio::test]
async fn structured_feature_toggle_ignores_unrelated_managed_settings() -> Result<()> {
let tmp = tempdir()?;
let user_path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(
&user_path,
"[features.network_proxy]\nenabled = false\ncredential_broker = true\n",
)?;
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(
&managed_path,
"[features.network_proxy]\nproxy_url = 'http://127.0.0.1:4321'\n",
)?;
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path.clone()),
CloudConfigBundleLoader::default(),
);
service
.write_value(ConfigValueWriteParams {
file_path: Some(user_path.display().to_string()),
key_path: "features.network_proxy".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await?;
assert_eq!(
toml::from_str::<TomlValue>(&std::fs::read_to_string(&user_path)?)?,
toml::from_str::<TomlValue>(
"[features.network_proxy]\nenabled = false\ncredential_broker = true\n"
)?
);
let result = service
.write_value(ConfigValueWriteParams {
file_path: Some(user_path.display().to_string()),
key_path: "features.network_proxy".to_string(),
value: serde_json::json!(true),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await?;
assert_eq!(result.status, WriteStatus::Ok);
assert_eq!(result.overridden_metadata, None);
let selected_path = tmp.path().join("work.config.toml");
std::fs::write(&selected_path, "")?;
let mut loader_overrides = LoaderOverrides::with_managed_config_path_for_tests(managed_path);
loader_overrides.user_config_path = Some(AbsolutePathBuf::from_absolute_path(&selected_path)?);
loader_overrides.user_config_profile = Some("work".parse()?);
let profile_service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
loader_overrides,
CloudConfigBundleLoader::default(),
);
profile_service
.write_value(ConfigValueWriteParams {
file_path: Some(selected_path.display().to_string()),
key_path: "features.network_proxy".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await?;
assert_eq!(
toml::from_str::<TomlValue>(&std::fs::read_to_string(&selected_path)?)?,
toml::from_str::<TomlValue>("[features]\nnetwork_proxy = false\n")?
);
service
.batch_write(ConfigBatchWriteParams {
edits: vec![
codex_app_server_protocol::ConfigEdit {
key_path: "features.network_proxy.credential_broker".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
},
codex_app_server_protocol::ConfigEdit {
key_path: "features.network_proxy".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
},
],
file_path: Some(user_path.display().to_string()),
expected_version: None,
reload_user_config: false,
})
.await?;
assert!(
toml::from_str::<TomlValue>(&std::fs::read_to_string(&user_path)?)?
.get("features")
.and_then(|features| features.get("network_proxy"))
.is_none()
);
let configured_provider = "[features.network_proxy]\nenabled = true\n\n\
[features.network_proxy.credentials.vendor]\n\
env = ['VENDOR_TOKEN']\n\
patterns = ['token_[a-z]{24}']\n\
url_prefixes = ['https://api.vendor.example']\n";
std::fs::write(&user_path, configured_provider)?;
service
.write_value(ConfigValueWriteParams {
file_path: Some(user_path.display().to_string()),
key_path: "features.network_proxy".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await?;
let updated: TomlValue = toml::from_str(&std::fs::read_to_string(&user_path)?)?;
let feature = &updated["features"]["network_proxy"];
assert_eq!(
feature.get("enabled").and_then(TomlValue::as_bool),
Some(false)
);
assert!(feature.get("credentials").is_some());
Ok(())
}
#[tokio::test]
async fn upsert_merges_tables_replace_overwrites() -> Result<()> {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
let base = r#"[mcp_servers.linear]
bearer_token_env_var = "TOKEN"
name = "linear"
url = "https://linear.example"
[mcp_servers.linear.env_http_headers]
existing = "keep"
[mcp_servers.linear.http_headers]
alpha = "a"
"#;
let overlay = serde_json::json!({
"bearer_token_env_var": "NEW_TOKEN",
"http_headers": {
"alpha": "updated",
"beta": "b"
},
"name": "linear",
"url": "https://linear.example"
});
std::fs::write(&path, base)?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "mcp_servers.linear".to_string(),
value: overlay.clone(),
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await
.expect("upsert succeeds");
let upserted: TomlValue = toml::from_str(&std::fs::read_to_string(&path)?)?;
let expected_upsert: TomlValue = toml::from_str(
r#"[mcp_servers.linear]
bearer_token_env_var = "NEW_TOKEN"
name = "linear"
url = "https://linear.example"
[mcp_servers.linear.env_http_headers]
existing = "keep"
[mcp_servers.linear.http_headers]
alpha = "updated"
beta = "b"
"#,
)?;
assert_eq!(upserted, expected_upsert);
std::fs::write(&path, base)?;
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "mcp_servers.linear".to_string(),
value: overlay,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("replace succeeds");
let replaced: TomlValue = toml::from_str(&std::fs::read_to_string(&path)?)?;
let expected_replace: TomlValue = toml::from_str(
r#"[mcp_servers.linear]
bearer_token_env_var = "NEW_TOKEN"
name = "linear"
url = "https://linear.example"
[mcp_servers.linear.http_headers]
alpha = "updated"
beta = "b"
"#,
)?;
assert_eq!(replaced, expected_replace);
Ok(())
}
#[tokio::test]
async fn config_writes_apply_path_sensitive_merge_rules() -> Result<()> {
let cases = [
(
r#"[shell_environment_policy]
exclude = ["AWS_*"]
"#,
"shell_environment_policy",
serde_json::json!({"filters": {"AWS_*": "include"}}),
r#"[shell_environment_policy.filters]
"AWS_*" = "include"
"#,
),
(
r#"[shell_environment_policy]
inherit = "core"
exclude = ["AWS_*"]
"#,
"shell_environment_policy.filters",
serde_json::json!({"AWS_*": "include"}),
r#"[shell_environment_policy]
inherit = "core"
[shell_environment_policy.filters]
"AWS_*" = "include"
"#,
),
(
r#"[shell_environment_policy.filters]
"AWS_*" = "include"
"#,
"shell_environment_policy.exclude",
serde_json::json!(["AWS_*"]),
r#"[shell_environment_policy]
exclude = ["AWS_*"]
"#,
),
(
r#"[shell_environment_policy]
exclude = ["AWS_*"]
include_only = ["PATH"]
"#,
"shell_environment_policy.filters",
serde_json::json!({}),
r#"[shell_environment_policy.filters]
"#,
),
(
r#"[shell_environment_policy.filters]
"AWS_*" = "include"
"#,
"shell_environment_policy.exclude",
serde_json::json!([]),
r#"[shell_environment_policy]
exclude = []
"#,
),
(
r#"[shell_environment_policy.filters]
"aws_*" = "exclude"
"#,
"shell_environment_policy.filters",
serde_json::json!({"AWS_*": "include"}),
r#"[shell_environment_policy.filters]
"aws_*" = "include"
"#,
),
(
r#"[shell_environment_policy.filters]
"aws_*" = "exclude"
"#,
"shell_environment_policy.filters.AWS_*",
serde_json::json!("include"),
r#"[shell_environment_policy.filters]
"aws_*" = "include"
"#,
),
(
r#"[shell_environment_policy.filters]
"секрет_*" = "exclude"
"#,
"shell_environment_policy.filters.СЕКРЕТ_*",
serde_json::json!("include"),
r#"[shell_environment_policy.filters]
"секрет_*" = "include"
"#,
),
(
r#"[permissions.dev.network.domains]
"example.com" = "deny"
"#,
"permissions.dev.network.domains",
serde_json::json!({"EXAMPLE.COM": "allow"}),
r#"[permissions.dev.network.domains]
"example.com" = "allow"
"#,
),
(
r#"[memories]
no_memories_if_mcp_or_web_search = false
"#,
"memories",
serde_json::json!({"disable_on_external_context": true}),
r#"[memories]
disable_on_external_context = true
"#,
),
(
r#"[features]
multi_agent_v2 = true
"#,
"features.multi_agent_v2.subagent_usage_hint_text",
serde_json::json!("Delegate carefully."),
r#"[features.multi_agent_v2]
enabled = true
subagent_usage_hint_text = "Delegate carefully."
"#,
),
(
r#"[features]
multi_agent_v2 = true
"#,
"features.multi_agent_v2",
serde_json::json!({"subagent_usage_hint_text": "Delegate carefully."}),
r#"[features.multi_agent_v2]
enabled = true
subagent_usage_hint_text = "Delegate carefully."
"#,
),
(
r#"[features.multi_agent_v2]
enabled = true
subagent_usage_hint_text = "Delegate carefully."
"#,
"features.multi_agent_v2",
serde_json::json!(false),
r#"[features.multi_agent_v2]
enabled = false
subagent_usage_hint_text = "Delegate carefully."
"#,
),
(
r#"[features.multi_agent_v2]
enabled = true
subagent_usage_hint_text = "Delegate carefully."
"#,
"features.multi_agent_v2",
serde_json::Value::Null,
"",
),
(
r#"[features]
network_proxy = true
"#,
"features.network_proxy.credential_broker",
serde_json::json!(true),
r#"[features.network_proxy]
enabled = true
credential_broker = true
"#,
),
(
r#"[features.network_proxy]
enabled = true
credential_broker = true
"#,
"features.network_proxy",
serde_json::json!(false),
r#"[features.network_proxy]
enabled = false
credential_broker = true
"#,
),
(
r#"[desktop.features.multi_agent_v2]
custom = true
"#,
"desktop.features.multi_agent_v2",
serde_json::json!(false),
r#"[desktop.features]
multi_agent_v2 = false
"#,
),
(
r#"[desktop.features]
multi_agent_v2 = true
"#,
"desktop.features.multi_agent_v2",
serde_json::json!({"custom": true}),
r#"[desktop.features.multi_agent_v2]
custom = true
"#,
),
];
for (base, key_path, value, expected) in cases {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(&path, base)?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: key_path.to_string(),
value,
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await?;
let updated: TomlValue = toml::from_str(&std::fs::read_to_string(&path)?)?;
let expected: TomlValue = toml::from_str(expected)?;
assert_eq!(updated, expected);
service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await?;
}
Ok(())
}
#[tokio::test]
async fn clear_shell_environment_filter_ignores_ascii_case() -> Result<()> {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(
&path,
r#"[shell_environment_policy.filters]
"aws_*" = "exclude"
"keep_*" = "include"
"#,
)?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let response = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "shell_environment_policy.filters.AWS_*".to_string(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await?;
assert_eq!(response.status, WriteStatus::Ok);
assert_eq!(response.overridden_metadata, None);
assert_eq!(
std::fs::read_to_string(&path)?,
r#"[shell_environment_policy.filters]
"keep_*" = "include"
"#
);
service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await?;
Ok(())
}
#[tokio::test]
async fn upsert_shell_environment_scalar_preserves_unrelated_formatting() -> Result<()> {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(
&path,
r#"[shell_environment_policy]
inherit = "all"
exclude = [
"AWS_*", # keep this comment
]
set = { KEEP = "1", OTHER = "2", GH_HOST = "github.stale.example" } # keep this inline table
"#,
)?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "shell_environment_policy.inherit".to_string(),
value: serde_json::json!("core"),
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await?;
assert_eq!(
std::fs::read_to_string(&path)?,
r#"[shell_environment_policy]
inherit = "core"
exclude = [
"AWS_*", # keep this comment
]
set = { KEEP = "1", OTHER = "2", GH_HOST = "github.stale.example" } # keep this inline table
"#
);
for (value, expected_set) in [
(
serde_json::json!("github.trusted.example"),
"KEEP='1'\nOTHER='2'\nGH_HOST='github.stale.example'\ngh_host='github.trusted.example'",
),
(
serde_json::Value::Null,
"KEEP='1'\nOTHER='2'\nGH_HOST='github.stale.example'",
),
] {
let response = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "shell_environment_policy.set.gh_host".to_string(),
value,
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await?;
assert_eq!(response.status, WriteStatus::Ok);
assert_eq!(response.overridden_metadata, None);
let config: TomlValue = toml::from_str(&std::fs::read_to_string(&path)?)?;
assert_eq!(
config["shell_environment_policy"]["set"],
toml::from_str::<TomlValue>(expected_set)?
);
}
service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await?;
Ok(())
}
#[tokio::test]
async fn upsert_shell_environment_filter_scalar_preserves_formatting_and_version() -> Result<()> {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(
&path,
r#"[shell_environment_policy]
set = { KEEP = "1", OTHER = "2" } # keep this inline table
[shell_environment_policy.filters]
"AWS_*" = "exclude" # keep this edited comment
"KEEP_*" = "include" # keep this untouched comment
"#,
)?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let response = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "shell_environment_policy.filters.aws_*".to_string(),
value: serde_json::json!("include"),
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await?;
assert_eq!(
std::fs::read_to_string(&path)?,
r#"[shell_environment_policy]
set = { KEEP = "1", OTHER = "2" } # keep this inline table
[shell_environment_policy.filters]
"AWS_*" = "include" # keep this edited comment
"KEEP_*" = "include" # keep this untouched comment
"#
);
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "shell_environment_policy.filters.AWS_*".to_string(),
value: serde_json::json!("exclude"),
merge_strategy: MergeStrategy::Upsert,
expected_version: Some(response.version),
})
.await?;
service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await?;
Ok(())
}
#[tokio::test]
async fn shell_environment_upsert_rejects_case_variant_filters_in_one_edit() -> Result<()> {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
let initial = r#"[shell_environment_policy.filters]
"KEEP_*" = "include"
"#;
std::fs::write(&path, initial)?;
let service = ConfigManager::without_managed_config_for_tests(tmp.path().to_path_buf());
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "shell_environment_policy.filters".to_string(),
value: serde_json::json!({"AWS_*": "include", "aws_*": "exclude"}),
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await
.expect_err("one filter-map edit must not contain case-variant keys");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigValidationError)
);
assert!(
error
.to_string()
.contains("duplicate shell environment filter")
);
assert_eq!(std::fs::read_to_string(&path)?, initial);
Ok(())
}
#[tokio::test]
async fn shell_environment_representation_switch_reports_managed_override() -> Result<()> {
let cases = [
(
r#"[shell_environment_policy]
exclude = ["AWS_*"]
"#,
"shell_environment_policy.filters.AWS_*",
serde_json::json!("include"),
Some(serde_json::Value::Null),
),
(
r#"[shell_environment_policy.filters]
"AWS_*" = "include"
"#,
"shell_environment_policy.exclude",
serde_json::json!(["AWS_*"]),
Some(serde_json::Value::Null),
),
(
"[shell_environment_policy.set]\nGH_HOST = 'github.managed.example'\n",
"shell_environment_policy.set.GH_HOST",
serde_json::json!("github.user.example"),
Some(serde_json::json!("github.managed.example")),
),
(
"[shell_environment_policy.set]\nGH_HOST = 'github.managed.example'\n",
"shell_environment_policy.set.gh_host",
serde_json::json!("github.user.example"),
None,
),
(
"[shell_environment_policy.set]\ngh_host = 'github.managed.example'\n",
"shell_environment_policy.set.gh_host",
serde_json::json!("github.user.example"),
Some(serde_json::json!("github.managed.example")),
),
(
"[shell_environment_policy.set]\ngh_host = 'github.managed.example'\n",
"shell_environment_policy.set.GH_HOST",
serde_json::json!("github.user.example"),
None,
),
];
for (managed, key_path, value, expected_effective_value) in cases {
let tmp = tempdir()?;
let path = tmp.path().join(CONFIG_TOML_FILE);
std::fs::write(
&path,
"[shell_environment_policy.set]\n\
GH_HOST = 'github.original.example'\ngh_host = 'github.original-lower.example'\n",
)?;
let managed_path = tmp.path().join("managed_config.toml");
std::fs::write(&managed_path, managed)?;
let managed_file = AbsolutePathBuf::try_from(managed_path.clone())?;
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::with_managed_config_path_for_tests(managed_path),
CloudConfigBundleLoader::default(),
);
let response = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: key_path.to_string(),
value,
merge_strategy: MergeStrategy::Upsert,
expected_version: None,
})
.await?;
if let Some(expected_effective_value) = expected_effective_value {
assert_eq!(response.status, WriteStatus::OkOverridden);
let overridden = response
.overridden_metadata
.expect("managed setting should override the user edit");
assert_eq!(
overridden.overriding_layer.name,
ApiConfigLayerSource::LegacyManagedConfigTomlFromFile { file: managed_file }
);
assert_eq!(overridden.effective_value, expected_effective_value);
} else {
assert_eq!(response.status, WriteStatus::Ok);
assert_eq!(response.overridden_metadata, None);
}
service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await?;
}
Ok(())
}
#[tokio::test]
async fn allowed_login_methods_follow_current_forced_workspaces() -> Result<()> {
use codex_protocol::config_types::ForcedLoginMethod;
let tmp = tempdir()?;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
std::fs::write(
tmp.path().join("requirements.toml"),
"allowed_chatgpt_workspaces = ['managed']",
)?;
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
Vec::new(),
LoaderOverrides::with_managed_config_path_for_tests(tmp.path().join("managed_config.toml")),
CloudConfigBundleLoader::default(),
);
let config = service.load_latest_config(/*fallback_cwd*/ None).await?;
let auth = codex_login::AuthManager::shared_from_config(
&config, /*enable_codex_api_key_env*/ false,
)
.await?;
for (workspaces, expected) in [
(
Some(vec!["managed".to_string()]),
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
),
(
Some(vec!["other".to_string()]),
vec![ForcedLoginMethod::Api],
),
(Some(Vec::new()), vec![ForcedLoginMethod::Api]),
(
None,
vec![ForcedLoginMethod::Api, ForcedLoginMethod::Chatgpt],
),
] {
auth.set_forced_chatgpt_workspace_id(workspaces);
assert_eq!(auth.allowed_login_methods(), expected);
}
Ok(())
}
#[tokio::test]
async fn permission_config_reload_merges_session_layers() -> Result<()> {
use codex_config::ConfigLayerEntry;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
let tmp = tempdir()?;
let service = ConfigManager::new(
tmp.path().to_path_buf(),
Vec::new(),
LoaderOverrides::without_managed_config_for_tests(),
/*strict_config*/ false,
CloudConfigBundleLoader::default(),
codex_arg0::Arg0DispatchPaths::default(),
std::sync::Arc::new(codex_config::NoopThreadConfigLoader),
);
let mut config = service
.load_with_overrides(
/*request_overrides*/ None,
codex_core::config::ConfigOverrides {
cwd: Some(tmp.path().to_path_buf()),
..Default::default()
},
)
.await?;
let mut layers = config
.config_layer_stack
.all_layers_low_to_high()
.cloned()
.collect::<Vec<_>>();
for contents in [
"[permissions.first.filesystem]\n",
"[permissions.second]\nextends = ':workspace'\n",
] {
layers.push(ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
toml::from_str(contents)?,
));
}
layers.push(ConfigLayerEntry::new_disabled(
ConfigLayerSource::SessionFlags,
toml::from_str("[permissions.disabled]\nextends = ':read-only'\n")?,
"disabled for test",
));
config.config_layer_stack =
ConfigLayerStack::new(layers, Default::default(), Default::default())?;
let loaded = service
.load_permission_config_for_thread(&config, config.cwd.clone(), "first".to_string())
.await?;
assert_eq!(
loaded
.custom_permission_profiles
.iter()
.map(|profile| profile.id.as_str())
.collect::<Vec<_>>(),
vec!["first", "second"]
);
Ok(())
}
/// Cloud storage requirements override user config and reject leaf or whole-table writes without touching the file.
#[tokio::test]
async fn cloud_desktop_storage_requirements_control_reads_and_writes() {
let tmp = tempdir().expect("tempdir");
let path = tmp.path().join(CONFIG_TOML_FILE);
let original = "[desktop]\ndefaultProjectParent = 'user'\nprojectlessWorkspaceRoot = 'user'\nlocaleOverride = 'en-US'\n";
std::fs::write(&path, original).unwrap();
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
vec![],
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleFixture::loader_with_enterprise_requirement(format!(
"[application.storage]\ndefault_project_parent = \"{}\"\nprojectless_workspace_root = \"{}\"",
toml_path(tmp.path(), "projects"),
toml_path(tmp.path(), "chats"),
)),
);
let response = service
.read(ConfigReadParams {
include_layers: false,
cwd: None,
})
.await
.unwrap();
assert_eq!(
serde_json::to_value(response.config.desktop).unwrap(),
serde_json::json!({
"defaultProjectParent": tmp.path().join("projects"),
"projectlessWorkspaceRoot": tmp.path().join("chats"),
"localeOverride": "en-US",
})
);
for key in [
"desktop.defaultProjectParent",
"desktop.projectlessWorkspaceRoot",
"desktop",
] {
let error = service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: key.into(),
value: serde_json::Value::Null,
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect_err("managed storage is read-only");
assert_eq!(
error.write_error_code(),
Some(ConfigWriteErrorCode::ConfigRequirementReadonly)
);
}
assert_eq!(std::fs::read_to_string(&path).unwrap(), original);
service
.write_value(ConfigValueWriteParams {
file_path: Some(path.display().to_string()),
key_path: "desktop.localeOverride".into(),
value: serde_json::json!("fr-FR"),
merge_strategy: MergeStrategy::Replace,
expected_version: None,
})
.await
.expect("unmanaged desktop settings stay writable");
}
#[tokio::test]
async fn model_provider_list_allows_user_overrides_of_system_defaults() -> Result<()> {
use codex_app_server_protocol::ModelProviderListParams;
let tmp = tempdir()?;
let system_path = tmp.path().join("system.toml");
std::fs::write(
&system_path,
"model_provider = 'team'\n[model_providers.team]\nname = 'System gateway'\nbase_url = 'https://system.example.test/v1'\nenv_key = 'SYSTEM_KEY'\n",
)?;
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), "")?;
let mut overrides = LoaderOverrides::without_managed_config_for_tests();
overrides.system_config_path = Some(system_path);
for (cli_overrides, ignore_user_config, can_edit, can_change_default, name, auth_mode) in [
(
vec![],
false,
true,
true,
"System gateway",
"environmentVariable",
),
(
vec![],
true,
false,
false,
"System gateway",
"environmentVariable",
),
(
vec![("model_provider", "team")],
false,
true,
false,
"System gateway",
"environmentVariable",
),
(
vec![(
"model_providers.team.base_url",
"https://system.example.test/v1",
)],
false,
true,
true,
"System gateway",
"environmentVariable",
),
(
vec![
("model_providers.team.name", "System gateway"),
(
"model_providers.team.base_url",
"https://system.example.test/v1",
),
("model_providers.team.env_key", "SYSTEM_KEY"),
],
false,
false,
true,
"System gateway",
"environmentVariable",
),
(
vec![("model_providers.team.name", "OpenAI")],
false,
false,
true,
"OpenAI",
"externallyConfigured",
),
(
vec![("model_providers.team.name", " ")],
false,
false,
true,
" ",
"environmentVariable",
),
] {
overrides.ignore_user_config = ignore_user_config;
let cli_overrides = cli_overrides
.into_iter()
.map(|(key, value)| (key.to_owned(), TomlValue::String(value.to_owned())))
.collect();
let service = ConfigManager::new_for_tests(
tmp.path().to_path_buf(),
cli_overrides,
overrides.clone(),
CloudConfigBundleLoader::default(),
);
let response = service
.model_provider_list(ModelProviderListParams {})
.await?;
assert_eq!(response.can_change_default, can_change_default);
assert_eq!(response.default_provider_id, "team");
assert_eq!(
serde_json::to_value(response.data)?,
serde_json::json!([
{"id": "openai", "name": "OpenAI", "canEdit": false, "baseUrl":null, "settings":{"type":"openai"}},
{
"id": "team", "name": name,
"canEdit": can_edit, "baseUrl": "https://system.example.test/v1",
"settings": {"type": "customGateway", "auth": {"mode": auth_mode, "envKey": "SYSTEM_KEY"}},
}
])
);
}
Ok(())
}
#[tokio::test]
async fn model_provider_list_checks_builtin_configuration_fields() -> Result<()> {
use codex_app_server_protocol::ModelProviderListParams;
use codex_app_server_protocol::ModelProviderSummary;
use codex_app_server_protocol::ProviderSettings;
let home = tempdir()?;
std::fs::write(
home.path().join(CONFIG_TOML_FILE),
"[model_providers.amazon-bedrock.aws]\nregion = 'us-east-1'\n[model_providers.amazon-bedrock-runtime.aws]\nregion = 'us-east-1'\n[model_providers.gcp]\nproject_id = 'test-project'\n",
)?;
let credential_file = home.path().join("credentials.json").display().to_string();
for (id, name, settings) in [
(
"amazon-bedrock",
"Amazon Bedrock Mantle",
&[("aws.profile", "default"), ("aws.region", "us-east-1")][..],
),
(
"amazon-bedrock-runtime",
"Amazon Bedrock Runtime",
&[("aws.profile", "default"), ("aws.region", "us-east-1")][..],
),
(
"gcp",
"Google Cloud Vertex AI",
&[
("project_id", "test-project"),
("location", "global"),
("credential_file", credential_file.as_str()),
][..],
),
] {
for pinned_fields in [1, settings.len()] {
let overrides = settings
.iter()
.take(pinned_fields)
.map(|(field, value)| {
(
format!("model_providers.{id}.{field}"),
TomlValue::String((*value).to_owned()),
)
})
.collect();
let service = ConfigManager::new_for_tests(
home.path().to_path_buf(),
overrides,
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleLoader::default(),
);
let list = service
.model_provider_list(ModelProviderListParams {})
.await?;
assert_eq!(
list.data.into_iter().find(|provider| provider.id == id),
Some(ModelProviderSummary {
id: id.to_owned(),
name: name.to_owned(),
can_edit: pinned_fields < settings.len(),
base_url: None,
settings: if id == "gcp" {
ProviderSettings::Gcp {
project_id: Some("test-project".to_owned()),
location: (pinned_fields == settings.len())
.then(|| "global".to_owned()),
credential_file: (pinned_fields == settings.len())
.then(|| credential_file.clone()),
}
} else {
ProviderSettings::Bedrock {
profile: Some("default".to_owned()),
region: Some("us-east-1".to_owned()),
}
},
})
);
}
}
for id in ["amazon-bedrock", "amazon-bedrock-runtime"] {
for (section, overrides) in [
("auth", vec![]),
(
"aws.credential_export",
vec![(
format!("model_providers.{id}.aws.region"),
"us-east-1".into(),
)],
),
] {
std::fs::write(
home.path().join(CONFIG_TOML_FILE),
format!("[model_providers.{id}.{section}]\ncommand = 'helper'\n"),
)?;
let service = ConfigManager::new_for_tests(
home.path().to_path_buf(),
overrides,
LoaderOverrides::without_managed_config_for_tests(),
CloudConfigBundleLoader::default(),
);
let list = service
.model_provider_list(ModelProviderListParams {})
.await?;
assert_eq!(
list.data
.into_iter()
.find(|provider| provider.id == id)
.map(|provider| provider.can_edit),
Some(false)
);
}
}
Ok(())
}