Download codex-rs/app-server/src/config_manager_service_tests.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 89.3 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server/src/config_manager_service_tests.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/app-server/src/config_manager_service_tests.rs
-
curl -L -o config_manager_service_tests.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/app-server/src/config_manager_service_tests.rs
89.3 kB
| 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_http_client::HttpClientFactory; | |
| use codex_http_client::OutboundProxyPolicy; | |
| use codex_utils_absolute_path::AbsolutePathBuf; | |
| use pretty_assertions::assert_eq; | |
| use tempfile::tempdir; | |
| 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}" | |
| ); | |
| } | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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") | |
| ); | |
| } | |
| 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(()) | |
| } | |
| 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_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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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, | |
| }, | |
| }), | |
| 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, | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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 { .. } | |
| )); | |
| } | |
| 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(()) | |
| } | |
| 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()); | |
| } | |
| 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) | |
| ); | |
| } | |
| 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" | |
| ); | |
| } | |
| 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\"" | |
| ); | |
| } | |
| 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()]), | |
| } | |
| ); | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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\""); | |
| } | |
| 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"); | |
| } | |
| 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(()) | |
| } | |
| 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(), | |
| "" | |
| ); | |
| } | |
| 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" | |
| ); | |
| } | |
| fn toml_path(tmp: &Path, name: &str) -> String { | |
| tmp.join(name).to_string_lossy().replace('\\', "\\\\") | |
| } | |
| 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")); | |
| } | |
| } | |
| 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)) | |
| ); | |
| } | |
| async fn write_value_allows_unmanaged_sibling_of_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#" | |
| [windows] | |
| sandbox_private_desktop = 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 sibling should remain writable"); | |
| assert!( | |
| std::fs::read_to_string(path) | |
| .unwrap() | |
| .contains("sandbox = \"elevated\"") | |
| ); | |
| } | |
| 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 | |
| } | |
| ); | |
| } | |
| 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")); | |
| } | |
| /// Legacy managed feature toggles own their normalized enabled origin and override metadata. | |
| 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)); | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |
| 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(()) | |
| } | |