diff --git a/crates/cli/src/agents/codex/host.rs b/crates/cli/src/agents/codex/host.rs index 1e8fb6f96..185121ffb 100644 --- a/crates/cli/src/agents/codex/host.rs +++ b/crates/cli/src/agents/codex/host.rs @@ -749,6 +749,7 @@ pub(crate) fn install_codex_config(path: &Path, gateway_url: &str) -> Result<(), } doc["model_provider"] = value("nemo-relay-openai"); ensure_table(&mut doc, "features")["hooks"] = value(true); + set_multi_agent_v2_enabled(&mut doc, false); let providers = ensure_table(&mut doc, "model_providers"); let mut provider = Table::new(); @@ -932,6 +933,7 @@ fn sanitize_codex_backup_doc( restore_codex_client_proof_from_backup(&mut backup, &empty, challenge); } remove_table_item_if_bool(&mut backup, "features", "hooks", true); + remove_managed_multi_agent_v2_enabled(&mut backup); remove_empty_table(&mut backup, "model_providers"); remove_empty_table(&mut backup, "features"); backup @@ -1192,6 +1194,7 @@ fn restore_codex_config_from_backup( if !preserve_hooks || feature_hooks_enabled(doc) != Some(true) { restore_table_item_if_bool(doc, backup_doc, "features", "hooks", true); } + restore_multi_agent_v2_enabled(doc, backup_doc); } fn remove_codex_config_without_backup( @@ -1211,6 +1214,7 @@ fn remove_codex_config_without_backup( if !preserve_hooks { remove_table_item_if_bool(doc, "features", "hooks", true); } + remove_managed_multi_agent_v2_enabled(doc); } pub(crate) fn remove_legacy_codex_hooks(path: &Path) -> Result<(), String> { @@ -1379,6 +1383,7 @@ pub(crate) fn codex_config_doc_has_managed_install(doc: &DocumentMut, gateway_ur == Some("nemo-relay-openai") && codex_provider_item_is_managed(doc, gateway_url) && feature_hooks_enabled(doc) == Some(true) + && feature_multi_agent_v2_enabled(doc) == Some(false) } #[cfg(test)] @@ -1504,6 +1509,74 @@ pub(crate) fn feature_hooks_enabled(doc: &DocumentMut) -> Option { .and_then(|value| value.as_bool()) } +fn feature_multi_agent_v2_enabled(doc: &DocumentMut) -> Option { + let item = doc + .get("features") + .and_then(Item::as_table) + .and_then(|features| features.get("multi_agent_v2"))?; + if let Some(table) = item.as_table() { + return table + .get("enabled") + .and_then(Item::as_value) + .and_then(TomlValue::as_bool); + } + item.as_inline_table() + .and_then(|table| table.get("enabled")) + .and_then(TomlValue::as_bool) +} + +fn set_multi_agent_v2_enabled(doc: &mut DocumentMut, enabled: bool) { + let features = ensure_table(doc, "features"); + if let Some(item) = features.get_mut("multi_agent_v2") { + if let Some(table) = item.as_table_mut() { + table["enabled"] = value(enabled); + return; + } + if let Some(table) = item.as_inline_table_mut() { + table.insert("enabled", TomlValue::from(enabled)); + return; + } + } + let mut table = Table::new(); + table["enabled"] = value(enabled); + features["multi_agent_v2"] = Item::Table(table); +} + +fn restore_multi_agent_v2_enabled(doc: &mut DocumentMut, backup: &DocumentMut) { + if feature_multi_agent_v2_enabled(doc) != Some(false) { + return; + } + match feature_multi_agent_v2_enabled(backup) { + Some(enabled) => set_multi_agent_v2_enabled(doc, enabled), + None => remove_managed_multi_agent_v2_enabled(doc), + } +} + +fn remove_managed_multi_agent_v2_enabled(doc: &mut DocumentMut) { + if feature_multi_agent_v2_enabled(doc) != Some(false) { + return; + } + let Some(features) = doc.get_mut("features").and_then(Item::as_table_mut) else { + return; + }; + let remove_feature = if let Some(item) = features.get_mut("multi_agent_v2") { + if let Some(table) = item.as_table_mut() { + table.remove("enabled"); + table.is_empty() + } else if let Some(table) = item.as_inline_table_mut() { + table.remove("enabled"); + table.is_empty() + } else { + false + } + } else { + false + }; + if remove_feature { + features.remove("multi_agent_v2"); + } +} + pub(crate) fn remove_empty_table(doc: &mut DocumentMut, key: &str) { let is_empty = doc .get(key) diff --git a/crates/cli/src/agents/codex/launch.rs b/crates/cli/src/agents/codex/launch.rs index 0fcd0978b..2bbd79927 100644 --- a/crates/cli/src/agents/codex/launch.rs +++ b/crates/cli/src/agents/codex/launch.rs @@ -37,6 +37,8 @@ pub(crate) fn prepare(launch: &mut PreparedAgentLaunch, gateway_url: &str) -> Re "--config".to_string(), "features.hooks=true".to_string(), "--config".to_string(), + "features.multi_agent_v2.enabled=false".to_string(), + "--config".to_string(), "model_provider=\"nemo-relay-openai\"".to_string(), "--config".to_string(), gateway_provider_config(gateway_url), diff --git a/crates/cli/tests/coverage/agents/launcher_tests.rs b/crates/cli/tests/coverage/agents/launcher_tests.rs index e9aa1e1d4..c3715e8d9 100644 --- a/crates/cli/tests/coverage/agents/launcher_tests.rs +++ b/crates/cli/tests/coverage/agents/launcher_tests.rs @@ -266,6 +266,11 @@ fn prepares_codex_config_overrides() { assert!(!prepared.argv.iter().any(|arg| arg == "--profile")); assert!(prepared.argv.contains(&"features.hooks=true".into())); + assert!( + prepared + .argv + .contains(&"features.multi_agent_v2.enabled=false".into()) + ); assert!( prepared .argv diff --git a/crates/cli/tests/coverage/agents/plugin_host_tests.rs b/crates/cli/tests/coverage/agents/plugin_host_tests.rs index c9e4233c4..c44f13e99 100644 --- a/crates/cli/tests/coverage/agents/plugin_host_tests.rs +++ b/crates/cli/tests/coverage/agents/plugin_host_tests.rs @@ -1161,6 +1161,10 @@ fn repeated_codex_install_does_not_overwrite_original_backup() { .unwrap() .parse::() .unwrap(); + assert_eq!( + doc["features"]["multi_agent_v2"]["enabled"].as_bool(), + Some(false) + ); let token = codex_provider_client_token(&doc).unwrap(); assert!( BootstrapChallengeKey::load() @@ -1169,6 +1173,75 @@ fn repeated_codex_install_does_not_overwrite_original_backup() { ); } +#[test] +fn codex_uninstall_restores_multi_agent_v2_setting() { + let dir = tempdir().unwrap(); + let _home = HomeScope::enter(dir.path()); + let path = dir.path().join(".codex").join("config.toml"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + let original = r#"model_provider = "openai" + +[features.multi_agent_v2] +enabled = true +tool_namespace = "agents" +"#; + fs::write(&path, original).unwrap(); + + install_codex_config(&path, DEFAULT_URL).unwrap(); + let installed = fs::read_to_string(&path) + .unwrap() + .parse::() + .unwrap(); + assert_eq!( + installed["features"]["multi_agent_v2"]["enabled"].as_bool(), + Some(false) + ); + assert_eq!( + installed["features"]["multi_agent_v2"]["tool_namespace"].as_str(), + Some("agents") + ); + + uninstall_codex_config(&path, DEFAULT_URL, false).unwrap(); + + let restored = fs::read_to_string(&path) + .unwrap() + .parse::() + .unwrap(); + assert_eq!(restored["model_provider"].as_str(), Some("openai")); + assert_eq!( + restored["features"]["multi_agent_v2"]["enabled"].as_bool(), + Some(true) + ); + assert_eq!( + restored["features"]["multi_agent_v2"]["tool_namespace"].as_str(), + Some("agents") + ); + assert!(restored.get("model_providers").is_none()); + assert!(!backup_path(&path).exists()); +} + +#[test] +fn codex_uninstall_removes_multi_agent_v2_absent_from_backup() { + let dir = tempdir().unwrap(); + let _home = HomeScope::enter(dir.path()); + let path = dir.path().join(".codex").join("config.toml"); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(&path, "model_provider = \"openai\"\n").unwrap(); + + install_codex_config(&path, DEFAULT_URL).unwrap(); + assert!(backup_path(&path).exists()); + + uninstall_codex_config(&path, DEFAULT_URL, false).unwrap(); + + let restored = fs::read_to_string(&path) + .unwrap() + .parse::() + .unwrap(); + assert_eq!(restored["model_provider"].as_str(), Some("openai")); + assert!(restored.get("features").is_none()); + assert!(!backup_path(&path).exists()); +} + #[test] fn codex_client_token_supports_regular_header_tables() { let document = r#" @@ -1992,6 +2065,9 @@ model_provider = "nemo-relay-openai" [features] hooks = true +[features.multi_agent_v2] +enabled = false + [model_providers.nemo-relay-openai] name = "NeMo Relay" base_url = "http://127.0.0.1:47632" @@ -2008,6 +2084,7 @@ supports_websockets = false assert!(!updated.contains("model_provider")); assert!(!updated.contains("nemo-relay-openai")); assert!(!updated.contains("hooks = true")); + assert!(!updated.contains("multi_agent_v2")); } #[test]