From 69bc0645acc474452e28f31a227b14b3a3f302cc Mon Sep 17 00:00:00 2001 From: xl-openai Date: Tue, 16 Jun 2026 17:24:48 -0700 Subject: [PATCH] [codex] Gate remote plugin catalog by auth (#28625) ## Summary - Treat the remote global plugin catalog as active only when `remote_plugin` is enabled and the current auth uses the Codex backend. - Skip the local OpenAI curated marketplace for remote-enabled ChatGPT users while preserving configured marketplaces. - Keep the local curated marketplace for API-key users, unauthenticated fallback, and ChatGPT users with `remote_plugin` disabled. - Apply the same effective-remote gate to the remote installed-marketplace cache. ## Root cause The tool-suggestion discovery path unconditionally included the local OpenAI curated marketplace. For remote-enabled ChatGPT users, that made remote discovery additive: Codex parsed every local curated `plugin.json` before also loading the remote catalog. ## Validation - `just fmt` - `cargo build -p codex-cli --bin codex` - Targeted auth/feature matrix tests pass, including API-key auth with `remote_plugin` enabled. - Manual CLI validation confirmed: - ChatGPT + remote off includes local curated. - ChatGPT + remote on excludes local curated. - API-key auth keeps local curated when remote is enabled. - `just test -p codex-core-plugins`: 235 passed; one unrelated existing marketplace test failed because it loaded the developer's home marketplace configuration. --- codex-rs/core-plugins/src/discoverable.rs | 10 ++++- .../core-plugins/src/discoverable_tests.rs | 45 +++++++++++++++---- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/codex-rs/core-plugins/src/discoverable.rs b/codex-rs/core-plugins/src/discoverable.rs index c6a7146a8..0379819c4 100644 --- a/codex-rs/core-plugins/src/discoverable.rs +++ b/codex-rs/core-plugins/src/discoverable.rs @@ -76,11 +76,17 @@ impl PluginsManager { return Ok(Vec::new()); } + let use_remote_global_catalog = + input.plugins.remote_plugin_enabled && auth.is_some_and(CodexAuth::uses_codex_backend); let marketplaces = self - .list_marketplaces_for_config(&input.plugins, &[], /*include_openai_curated*/ true) + .list_marketplaces_for_config( + &input.plugins, + &[], + /*include_openai_curated*/ !use_remote_global_catalog, + ) .context("failed to list plugin marketplaces for tool suggestions")? .marketplaces; - let remote_installed_marketplaces = if input.plugins.remote_plugin_enabled { + let remote_installed_marketplaces = if use_remote_global_catalog { self.build_remote_installed_plugin_marketplaces_from_cache(&[ REMOTE_GLOBAL_MARKETPLACE_NAME, ]) diff --git a/codex-rs/core-plugins/src/discoverable_tests.rs b/codex-rs/core-plugins/src/discoverable_tests.rs index c1fb04f92..8465d2431 100644 --- a/codex-rs/core-plugins/src/discoverable_tests.rs +++ b/codex-rs/core-plugins/src/discoverable_tests.rs @@ -15,6 +15,7 @@ use crate::test_support::write_curated_plugin_sha_with; use crate::test_support::write_file; use crate::test_support::write_openai_api_curated_marketplace; use crate::test_support::write_openai_curated_marketplace; +use codex_app_server_protocol::AuthMode; use codex_config::CONFIG_TOML_FILE; use codex_login::CodexAuth; use codex_utils_absolute_path::AbsolutePathBuf; @@ -34,17 +35,19 @@ use wiremock::matchers::path; use wiremock::matchers::query_param; #[tokio::test] -async fn returns_fallback_plugins_without_installed_apps() { +async fn returns_fallback_plugins_when_remote_disabled_for_codex_auth() { let codex_home = tempdir().expect("tempdir should succeed"); let curated_root = curated_plugins_repo_path(codex_home.path()); write_openai_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]); let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[], &[], &[]), - /*auth*/ None, + Some(&auth), ) .await; @@ -66,8 +69,10 @@ async fn returns_api_curated_fallback_plugins_for_direct_provider_auth() { let curated_root = curated_plugins_repo_path(codex_home.path()); write_openai_api_curated_marketplace(&curated_root, &["sample", "slack", "openai-developers"]); - let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; + let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; + plugins.remote_plugin_enabled = true; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::ApiKey)); let auth = CodexAuth::from_api_key("test-api-key"); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, @@ -121,7 +126,7 @@ async fn returns_microsoft_fallback_plugins() { } #[tokio::test] -async fn includes_openai_curated_when_remote_enabled() { +async fn omits_openai_curated_but_keeps_configured_marketplaces_for_remote_codex_auth() { let codex_home = tempdir().expect("tempdir should succeed"); let curated_root = curated_plugins_repo_path(codex_home.path()); write_openai_curated_marketplace(&curated_root, &["slack"]); @@ -159,6 +164,33 @@ source = "/tmp/{bundled_marketplace_name}" let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); + let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); + let discoverable_plugins = list_discoverable_plugins( + &plugins_manager, + discovery_input(plugins, &[], &[], &[]), + Some(&auth), + ) + .await; + + assert_eq!( + discoverable_plugins + .into_iter() + .map(|plugin| plugin.id) + .collect::>(), + vec!["chrome@openai-bundled".to_string()] + ); +} + +#[tokio::test] +async fn includes_openai_curated_when_remote_enabled_without_auth() { + let codex_home = tempdir().expect("tempdir should succeed"); + let curated_root = curated_plugins_repo_path(codex_home.path()); + write_openai_curated_marketplace(&curated_root, &["slack"]); + + let mut plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; + plugins.remote_plugin_enabled = true; + let plugins_manager = PluginsManager::new(codex_home.path().to_path_buf()); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[], &[], &[]), @@ -171,10 +203,7 @@ source = "/tmp/{bundled_marketplace_name}" .into_iter() .map(|plugin| plugin.id) .collect::>(), - vec![ - "chrome@openai-bundled".to_string(), - "slack@openai-curated".to_string(), - ] + vec!["slack@openai-curated".to_string()] ); }