Files
codex/codex-rs/app-server/src/config_manager_service_tests.rs
T
Abhinav c0cc9fc820 Add refreshable headers for OTLP HTTP exporters (#61028)
## Why

Allow collector credentials to refresh without rebuilding OpenTelemetry providers.

## What changed

Add `otel.headers_helper`, a command run from `CODEX_HOME` that returns a JSON object of header strings. Share its results across explicitly configured HTTP log, trace, and metric exporters. Refresh every 29 minutes by default, configurable through `otel.headers_helper_refresh_interval_ms`.

Helper headers override matching static headers. Failed refreshes block affected exports until the helper recovers, and HTTP redirects are disabled when helper headers are used. Repository-local configuration cannot set the helper; exporters set by admin requirements, gRPC exporters, and built-in analytics do not use it. Helper execution respects network policy.

## Testing

Add tests for shared execution, credential refresh and recovery, blocked exports after failure, network restrictions, and configuration validation. Collector tests cover header precedence across all three signals using JSON and binary HTTP encodings, including exclusion of an admin-managed exporter.

GitOrigin-RevId: 89018ac09c2c99523d3f92bf77048e7ed9dae5f2
2026-11-12 05:21:30 +00:00

3682 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]
headers_helper = "get-otel-headers"
headers_helper_refresh_interval_ms = 60000
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!({
"headers_helper": "get-otel-headers",
"headers_helper_refresh_interval_ms": 60000,
"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.headers_helper", true),
("otel.headers_helper_refresh_interval_ms", 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(())
}