From 0acf302db5ffedea4b8ef0112f4cbcddd65cff57 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 18 Aug 2026 07:51:09 +0000 Subject: [PATCH] Prevent marketplace identity spoofing (#39165) ## Why Repository manifests and user-configured sources must not be able to claim names reserved for managed or remote marketplaces. ## What changed - Reject reserved marketplace names from unmanaged sources during discovery, installation, configuration projection, and configured marketplace upgrades, even when source restrictions are disabled. - Recognize managed marketplaces only at their expected paths. Preserve the required macOS and Windows path equivalences without allowing symlink aliases to inherit managed provenance. ## Testing - Cover reserved-name rejection for plugin list and install RPCs, configured upgrades, unrestricted policy, and remote marketplace names. - Cover symlink spoofing and Windows managed-path normalization. GitOrigin-RevId: a610bdc382857f306dd3e244f0992de2ec279673 --- .../app-server/tests/suite/v2/plugin_list.rs | 63 +++++++++++ .../app-server/tests/suite/v2/plugin_read.rs | 17 +-- .../core-plugins/src/discoverable_tests.rs | 16 +-- codex-rs/core-plugins/src/manager.rs | 18 ++- .../core-plugins/src/marketplace_policy.rs | 100 +++++++++++----- .../src/marketplace_policy_tests.rs | 107 ++++++++++++++++-- .../core-plugins/src/marketplace_upgrade.rs | 6 +- 7 files changed, 262 insertions(+), 65 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/plugin_list.rs b/codex-rs/app-server/tests/suite/v2/plugin_list.rs index 2b16b6cc8d..80ec20880d 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -19,6 +19,7 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginAccountResponse; use codex_app_server_protocol::PluginAuthPolicy; use codex_app_server_protocol::PluginDisabledReason; +use codex_app_server_protocol::PluginInstallParams; use codex_app_server_protocol::PluginInstallPolicy; use codex_app_server_protocol::PluginInstallPolicySource; use codex_app_server_protocol::PluginInstalledParams; @@ -158,6 +159,68 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul Ok(()) } +#[tokio::test] +async fn plugin_rpcs_reject_repository_spoofing_openai_curated() -> Result<()> { + let codex_home = TempDir::new()?; + let repository = TempDir::new()?; + std::fs::create_dir_all(repository.path().join(".git"))?; + std::fs::create_dir_all(repository.path().join(".agents/plugins"))?; + std::fs::create_dir_all(repository.path().join("attacker/.codex-plugin"))?; + std::fs::write( + repository.path().join("attacker/.codex-plugin/plugin.json"), + r#"{"name":"attacker"}"#, + )?; + let marketplace_path = + AbsolutePathBuf::try_from(repository.path().join(".agents/plugins/marketplace.json"))?; + std::fs::write( + marketplace_path.as_path(), + r#"{"name":"openai-curated","plugins":[{"name":"attacker","source":{"source":"local","path":"./attacker"}}]}"#, + )?; + write_plugins_enabled_config(codex_home.path())?; + let original_config = std::fs::read(codex_home.path().join("config.toml"))?; + let mut mcp = TestAppServer::builder() + .with_codex_home(codex_home.path()) + .build_initialized_with_timeout(DEFAULT_TIMEOUT) + .await?; + + let request_id = mcp + .send_plugin_list_request(PluginListParams { + cwds: Some(vec![AbsolutePathBuf::try_from(repository.path())?]), + marketplace_kinds: None, + force_refetch: false, + }) + .await?; + let response: PluginListResponse = + timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??; + assert!( + response + .marketplaces + .iter() + .all(|marketplace| marketplace.path.as_ref() != Some(&marketplace_path)) + ); + + let request_id = mcp + .send_plugin_install_request(PluginInstallParams { + marketplace_path: Some(marketplace_path), + remote_marketplace_name: None, + install_attempt_id: None, + plugin_name: "attacker".to_string(), + }) + .await?; + let error = timeout( + DEFAULT_TIMEOUT, + mcp.read_stream_until_error_message(RequestId::Integer(request_id)), + ) + .await??; + assert!(error.error.message.contains("reserved")); + assert!(!codex_home.path().join("plugins/cache").exists()); + assert_eq!( + std::fs::read(codex_home.path().join("config.toml"))?, + original_config + ); + Ok(()) +} + #[tokio::test] async fn plugin_installed_includes_installed_plugins_and_explicit_install_suggestions() -> Result<()> { diff --git a/codex-rs/app-server/tests/suite/v2/plugin_read.rs b/codex-rs/app-server/tests/suite/v2/plugin_read.rs index f49bbd9f96..73098ae5c7 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_read.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_read.rs @@ -959,18 +959,11 @@ async fn plugin_read_rejects_invalid_remote_plugin_name() -> Result<()> { #[tokio::test] async fn plugin_read_returns_canonical_openai_curated_marketplace_name() -> Result<()> { let codex_home = TempDir::new()?; - let repo_root = TempDir::new()?; - write_plugin_marketplace( - repo_root.path(), - "openai-curated", - "demo-plugin", - "./demo-plugin", - )?; - std::fs::create_dir_all(repo_root.path().join("demo-plugin/.codex-plugin"))?; + let repo_root = codex_home.path().join(".tmp/plugins"); + write_plugin_marketplace(&repo_root, "openai-curated", "demo-plugin", "./demo-plugin")?; + std::fs::create_dir_all(repo_root.join("demo-plugin/.codex-plugin"))?; std::fs::write( - repo_root - .path() - .join("demo-plugin/.codex-plugin/plugin.json"), + repo_root.join("demo-plugin/.codex-plugin/plugin.json"), r#"{ "name": "demo-plugin", "description": "OpenAI curated plugin" @@ -994,7 +987,7 @@ enabled = true .await?; let marketplace_path = - AbsolutePathBuf::try_from(repo_root.path().join(".agents/plugins/marketplace.json"))?; + AbsolutePathBuf::try_from(repo_root.join(".agents/plugins/marketplace.json"))?; let request_id = mcp .send_plugin_read_request(PluginReadParams { marketplace_path: Some(marketplace_path.clone()), diff --git a/codex-rs/core-plugins/src/discoverable_tests.rs b/codex-rs/core-plugins/src/discoverable_tests.rs index 6e09347552..3c2ca25bff 100644 --- a/codex-rs/core-plugins/src/discoverable_tests.rs +++ b/codex-rs/core-plugins/src/discoverable_tests.rs @@ -156,9 +156,9 @@ async fn omits_openai_curated_but_keeps_configured_marketplaces_for_remote_codex write_openai_curated_marketplace(&curated_root, &["slack"]); let bundled_marketplace_name = OPENAI_BUNDLED_MARKETPLACE_NAME; - let bundled_marketplace_root = codex_home - .path() - .join(format!(".tmp/marketplaces/{bundled_marketplace_name}")); + let bundled_marketplace_root = codex_home.path().join(format!( + ".tmp/bundled-marketplaces/{bundled_marketplace_name}" + )); write_file( &bundled_marketplace_root.join(".agents/plugins/marketplace.json"), &format!( @@ -179,8 +179,8 @@ async fn omits_openai_curated_but_keeps_configured_marketplaces_for_remote_codex plugins = true [marketplaces.{bundled_marketplace_name}] -source_type = "git" -source = "/tmp/{bundled_marketplace_name}" +source_type = "local" +source = {bundled_marketplace_root:?} "# ), ); @@ -240,7 +240,7 @@ async fn deduplicates_and_reprojects_cached_configured_marketplace_plugin() { let plugin_id = format!("{plugin_name}@{marketplace_name}"); let marketplace_root = codex_home .path() - .join(format!(".tmp/marketplaces/{marketplace_name}")); + .join(format!(".tmp/bundled-marketplaces/{marketplace_name}")); write_file( &marketplace_root.join(".agents/plugins/marketplace.json"), &format!( @@ -267,8 +267,8 @@ async fn deduplicates_and_reprojects_cached_configured_marketplace_plugin() { plugins = true [marketplaces.{marketplace_name}] -source_type = "git" -source = "/tmp/{marketplace_name}" +source_type = "local" +source = {marketplace_root:?} "# ), ); diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index c23a3b6b53..0fd577ea9b 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -40,7 +40,6 @@ use crate::marketplace::home_dir; use crate::marketplace::list_marketplaces_with_home; use crate::marketplace::plugin_interface_with_marketplace_category; use crate::marketplace_policy::MarketplacePolicy; -use crate::marketplace_policy::allowed_configured_marketplace_names; use crate::marketplace_policy::configured_plugins_from_stack; use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeError; use crate::marketplace_upgrade::ConfiguredMarketplaceUpgradeOutcome; @@ -2921,16 +2920,15 @@ impl PluginsManager { ) -> Result { let mut outcome = list_marketplaces_with_home(roots, home_dir().as_deref())?; let policy = MarketplacePolicy::from_requirements(config.config_layer_stack.requirements()); - if !policy.is_restricted() { - return Ok(outcome); - } - let allowed_marketplace_names = allowed_configured_marketplace_names( - &config.config_layer_stack, - self.codex_home.as_path(), - ); outcome.marketplaces.retain(|marketplace| { - is_openai_curated_marketplace_name(&marketplace.name) - || allowed_marketplace_names.contains(&marketplace.name) + policy + .validate_install( + &config.config_layer_stack, + self.codex_home.as_path(), + &marketplace.path, + &marketplace.name, + ) + .is_ok() }); Ok(outcome) } diff --git a/codex-rs/core-plugins/src/marketplace_policy.rs b/codex-rs/core-plugins/src/marketplace_policy.rs index 5c13a4059d..8e8ae238ca 100644 --- a/codex-rs/core-plugins/src/marketplace_policy.rs +++ b/codex-rs/core-plugins/src/marketplace_policy.rs @@ -9,6 +9,7 @@ use crate::is_openai_curated_marketplace_name; use crate::marketplace::marketplace_root_dir; use crate::marketplace_add::MarketplaceSource; use crate::marketplace_add::parse_marketplace_source; +use crate::remote::RemotePluginScope; use crate::startup_sync::curated_plugins_api_marketplace_path; use crate::startup_sync::curated_plugins_repo_path; use codex_config::ConfigLayerStack; @@ -107,14 +108,18 @@ impl MarketplacePolicy { marketplace_path: &AbsolutePathBuf, marketplace_name: &str, ) -> Result<(), String> { - if !self.is_restricted() { - return Ok(()); - } - let root = marketplace_root_dir(marketplace_path).map_err(|err| err.to_string())?; if let Some(expected_name) = managed_marketplace_name(codex_home, marketplace_path, &root) { return validate_expected_marketplace_name(expected_name, marketplace_name); } + if is_reserved_marketplace_name(marketplace_name) { + return Err(format!( + "marketplace `{marketplace_name}` is reserved and cannot be loaded from this source" + )); + } + if !self.is_restricted() { + return Ok(()); + } let user_config = config_layer_stack.effective_user_config().ok_or_else(|| { format!( @@ -208,9 +213,6 @@ pub(crate) fn project_effective_user_config( ) -> Option { let mut user_config = config_layer_stack.effective_user_config()?; let policy = MarketplacePolicy::from_requirements(config_layer_stack.requirements()); - if !policy.is_restricted() { - return Some(user_config); - } let allowed_marketplace_names = allowed_configured_marketplace_names_with_policy(&user_config, &policy, codex_home); let configured_marketplace_names = user_config @@ -232,11 +234,13 @@ pub(crate) fn project_effective_user_config( { plugins.retain(|plugin_key, _| { let Ok(plugin_id) = PluginId::parse(plugin_key) else { - return false; + return !policy.is_restricted(); }; - (is_openai_curated_marketplace_name(&plugin_id.marketplace_name) - && !configured_marketplace_names.contains(&plugin_id.marketplace_name)) - || allowed_marketplace_names.contains(&plugin_id.marketplace_name) + let marketplace_name = &plugin_id.marketplace_name; + if configured_marketplace_names.contains(marketplace_name) { + return allowed_marketplace_names.contains(marketplace_name); + } + is_openai_curated_marketplace_name(marketplace_name) || !policy.is_restricted() }); } Some(user_config) @@ -264,14 +268,13 @@ fn allowed_configured_marketplace_names_with_policy( else { return HashSet::new(); }; - if !policy.is_restricted() { - return marketplaces.keys().cloned().collect(); - } marketplaces .iter() .filter_map(|(marketplace_name, marketplace)| { let allowed = match managed_marketplace_config_name(codex_home, marketplace) { Some(expected_name) => expected_name == marketplace_name, + None if is_reserved_marketplace_name(marketplace_name) => false, + None if !policy.is_restricted() => true, None => policy .validate_configured_marketplace(marketplace_name, marketplace) .is_ok(), @@ -306,14 +309,14 @@ pub(crate) fn validate_marketplace_source_for_add( source: &MarketplaceSource, ) -> Result, String> { let policy = MarketplacePolicy::from_requirements(requirements); - if !policy.is_restricted() { - return Ok(None); - } if let MarketplaceSource::Local { path } = source && let Some(expected_name) = managed_local_marketplace_name(codex_home, path) { return Ok(Some(expected_name)); } + if !policy.is_restricted() { + return Ok(None); + } policy.validate_source(source)?; Ok(None) } @@ -325,7 +328,7 @@ pub(crate) fn validate_marketplace_name_for_add( if let Some(expected_name) = expected_name { return validate_expected_marketplace_name(expected_name, marketplace_name); } - if is_openai_curated_marketplace_name(marketplace_name) { + if is_reserved_marketplace_name(marketplace_name) { return Err(format!( "marketplace `{marketplace_name}` is reserved and cannot be added from this source" )); @@ -443,18 +446,30 @@ fn validate_expected_marketplace_name( }) } +fn is_reserved_marketplace_name(marketplace_name: &str) -> bool { + matches!( + marketplace_name, + OPENAI_CURATED_MARKETPLACE_NAME + | OPENAI_API_CURATED_MARKETPLACE_NAME + | OPENAI_BUNDLED_MARKETPLACE_NAME + | OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME + | OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME + ) || RemotePluginScope::from_marketplace_name(marketplace_name).is_some() +} + fn managed_marketplace_name( codex_home: &Path, marketplace_path: &AbsolutePathBuf, root: &AbsolutePathBuf, ) -> Option<&'static str> { - if paths_match_after_normalization( + if is_expected_managed_path( marketplace_path.as_path(), - curated_plugins_api_marketplace_path(codex_home), - ) { + &curated_plugins_api_marketplace_path(codex_home), + ) && is_expected_managed_path(root.as_path(), &curated_plugins_repo_path(codex_home)) + { return Some(OPENAI_API_CURATED_MARKETPLACE_NAME); } - if paths_match_after_normalization(root.as_path(), curated_plugins_repo_path(codex_home)) { + if is_expected_managed_path(root.as_path(), &curated_plugins_repo_path(codex_home)) { return Some(OPENAI_CURATED_MARKETPLACE_NAME); } managed_local_marketplace_name(codex_home, root.as_path()) @@ -483,14 +498,42 @@ fn managed_local_marketplace_name(codex_home: &Path, root: &Path) -> Option<&'st let expected_root = codex_home .join(".tmp/bundled-marketplaces") .join(marketplace_name); - if paths_match_after_normalization(root, &expected_root) { + if is_expected_managed_path(root, &expected_root) { return Some(marketplace_name); } } let runtime_root = primary_runtime_marketplace_root()?; - paths_match_after_normalization(root, &runtime_root) - .then_some(OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME) + is_expected_managed_path(root, &runtime_root).then_some(OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME) +} + +fn is_expected_managed_path(path: &Path, expected: &Path) -> bool { + if path == expected { + return true; + } + #[cfg(target_os = "macos")] + { + path.strip_prefix("/private/var") + .is_ok_and(|suffix| Path::new("/var").join(suffix) == expected) + || expected + .strip_prefix("/private/var") + .is_ok_and(|suffix| Path::new("/var").join(suffix) == path) + } + #[cfg(windows)] + { + let path = path.to_string_lossy(); + let expected = expected.to_string_lossy(); + let path = codex_utils_absolute_path::normalize_windows_device_path(&path) + .unwrap_or_else(|| path.into_owned()); + let expected = codex_utils_absolute_path::normalize_windows_device_path(&expected) + .unwrap_or_else(|| expected.into_owned()); + path.replace('/', "\\") + .eq_ignore_ascii_case(&expected.replace('/', "\\")) + } + #[cfg(not(any(target_os = "macos", windows)))] + { + false + } } pub(crate) fn primary_runtime_marketplace_root() -> Option { @@ -511,7 +554,12 @@ fn primary_runtime_cache_dir_from_user_profile(user_profile: Option) -> user_profile.map(|profile| profile.join(".cache")) } -#[cfg(not(target_os = "windows"))] +#[cfg(target_os = "macos")] +fn primary_runtime_cache_dir() -> Option { + dirs::home_dir().map(|home| home.join(".cache")) +} + +#[cfg(not(any(target_os = "macos", target_os = "windows")))] fn primary_runtime_cache_dir() -> Option { dirs::cache_dir() } diff --git a/codex-rs/core-plugins/src/marketplace_policy_tests.rs b/codex-rs/core-plugins/src/marketplace_policy_tests.rs index 16d1e0d9c5..3c3277f390 100644 --- a/codex-rs/core-plugins/src/marketplace_policy_tests.rs +++ b/codex-rs/core-plugins/src/marketplace_policy_tests.rs @@ -22,6 +22,10 @@ fn primary_runtime_cache_uses_user_profile_on_windows() { primary_runtime_cache_dir_from_user_profile(Some(PathBuf::from(r"C:\Users\user"))), Some(PathBuf::from(r"C:\Users\user\.cache")) ); + assert!(is_expected_managed_path( + Path::new(r"\\?\c:\users\USER\.cache"), + Path::new(r"C:\Users/user\.cache"), + )); } fn config_layer_stack_with_user_config( @@ -363,6 +367,81 @@ restrict_to_allowed_sources = true ) .is_err() ); + + let repository_manifest = AbsolutePathBuf::try_from( + codex_home + .path() + .join("repository/.agents/plugins/marketplace.json"), + ) + .expect("absolute repository manifest"); + let unrestricted = ConfigLayerStack::default(); + for config in [&stack, &unrestricted] { + for marketplace in [ + OPENAI_CURATED_MARKETPLACE_NAME, + OPENAI_API_CURATED_MARKETPLACE_NAME, + OPENAI_BUNDLED_MARKETPLACE_NAME, + OPENAI_BUNDLED_ALPHA_MARKETPLACE_NAME, + OPENAI_PRIMARY_RUNTIME_MARKETPLACE_NAME, + crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME, + crate::remote::REMOTE_CREATED_BY_ME_MARKETPLACE_NAME, + crate::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME, + crate::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME, + crate::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME, + crate::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME, + ] { + assert!( + MarketplacePolicy::from_requirements(config.requirements()) + .validate_install(config, codex_home.path(), &repository_manifest, marketplace) + .is_err() + ); + } + } +} + +#[cfg(unix)] +#[test] +fn symlinked_marketplaces_cannot_borrow_managed_provenance() { + let codex_home = TempDir::new().expect("create Codex home"); + let official_root = curated_plugins_repo_path(codex_home.path()); + let manifest = official_root.join(".agents/plugins/marketplace.json"); + fs::create_dir_all(manifest.parent().expect("manifest directory")) + .expect("create manifest directory"); + fs::write(&manifest, "{}").expect("write official manifest"); + + let root_alias = codex_home.path().join("repository"); + std::os::unix::fs::symlink(&official_root, &root_alias).expect("symlink official root"); + let parent_alias = codex_home.path().join("parent-alias"); + std::os::unix::fs::symlink( + official_root.parent().expect("official root parent"), + &parent_alias, + ) + .expect("symlink official root parent"); + let manifest_alias = codex_home + .path() + .join("manifest-alias/.agents/plugins/api_marketplace.json"); + fs::create_dir_all(manifest_alias.parent().expect("manifest alias parent")) + .expect("create manifest alias directory"); + std::os::unix::fs::symlink(&manifest, &manifest_alias).expect("symlink official manifest"); + let stack = ConfigLayerStack::default(); + + for (path, marketplace) in [ + ( + root_alias.join(".agents/plugins/marketplace.json"), + OPENAI_CURATED_MARKETPLACE_NAME, + ), + ( + parent_alias.join("plugins/.agents/plugins/marketplace.json"), + OPENAI_CURATED_MARKETPLACE_NAME, + ), + (manifest_alias, OPENAI_API_CURATED_MARKETPLACE_NAME), + ] { + let manifest = AbsolutePathBuf::try_from(path).expect("absolute repository manifest"); + assert!( + MarketplacePolicy::from_requirements(stack.requirements()) + .validate_install(&stack, codex_home.path(), &manifest, marketplace) + .is_err() + ); + } } #[test] @@ -518,7 +597,7 @@ enabled = true } #[test] -fn allowlisted_config_names_are_not_globally_reserved() { +fn allowlisted_sources_cannot_claim_reserved_marketplace_names() { let codex_home = TempDir::new().expect("create Codex home"); let source_root = TempDir::new().expect("create marketplace root"); let source_root = source_root @@ -569,7 +648,7 @@ enabled = true .keys() .cloned() .collect::>(), - vec!["openai-bundled".to_string(), "openai-curated".to_string()] + Vec::::new() ); assert_eq!( projected["plugins"] @@ -578,15 +657,12 @@ enabled = true .keys() .cloned() .collect::>(), - vec![ - "sample@openai-bundled".to_string(), - "sample@openai-curated".to_string() - ] + Vec::::new() ); } #[test] -fn blocked_upgrade_is_rejected_before_marketplace_installation() { +fn blocked_or_reserved_upgrade_is_rejected_before_marketplace_installation() { let codex_home = TempDir::new().expect("create Codex home"); let config_file = AbsolutePathBuf::try_from(codex_home.path().join("config.toml")) .expect("absolute config path"); @@ -601,7 +677,7 @@ restrict_to_allowed_sources = true source_type = "git" source = "https://github.com/example/blocked.git" "#, - config_file, + config_file.clone(), )), ); @@ -616,6 +692,21 @@ source = "https://github.com/example/blocked.git" .contains("is not allowed by requirements") ); assert!(!marketplace_install_root(codex_home.path()).exists()); + + let stack = config_layer_stack_with_user_config( + "[marketplaces]\nrestrict_to_allowed_sources = false\n", + Some(( + "[marketplaces.openai-bundled]\nsource_type = \"git\"\nsource = \"https://github.com/example/blocked.git\"\n", + config_file, + )), + ); + let outcome = upgrade_configured_git_marketplaces( + codex_home.path(), + &stack, + Some(crate::OPENAI_BUNDLED_MARKETPLACE_NAME), + ); + assert!(outcome.errors[0].message.contains("is reserved")); + assert!(!marketplace_install_root(codex_home.path()).exists()); } #[test] diff --git a/codex-rs/core-plugins/src/marketplace_upgrade.rs b/codex-rs/core-plugins/src/marketplace_upgrade.rs index 01daa90f24..fc60349a5c 100644 --- a/codex-rs/core-plugins/src/marketplace_upgrade.rs +++ b/codex-rs/core-plugins/src/marketplace_upgrade.rs @@ -10,6 +10,7 @@ use crate::installed_marketplaces::marketplace_install_root; use crate::marketplace::validate_marketplace_root; use crate::marketplace_add::MarketplaceSource; use crate::marketplace_policy::MarketplacePolicy; +use crate::marketplace_policy::validate_marketplace_name_for_add; use codex_config::CONFIG_TOML_FILE; use codex_config::ConfigLayerStack; use codex_config::MarketplaceConfigUpdate; @@ -99,7 +100,10 @@ pub fn upgrade_configured_git_marketplaces( let policy = MarketplacePolicy::from_requirements(config_layer_stack.requirements()); for marketplace in marketplaces { let normalized_source = - match policy.validate_git_source(&marketplace.source, marketplace.ref_name.clone()) { + match validate_marketplace_name_for_add(/*expected_name*/ None, &marketplace.name) + .and_then(|()| { + policy.validate_git_source(&marketplace.source, marketplace.ref_name.clone()) + }) { Ok(normalized_source) => normalized_source, Err(message) => { errors.push(ConfiguredMarketplaceUpgradeError {