[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.
This commit is contained in:
xl-openai
2026-06-16 17:24:48 -07:00
committed by GitHub
Unverified
parent bfe90188ad
commit 69bc0645ac
2 changed files with 45 additions and 10 deletions
+8 -2
View File
@@ -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,
])
@@ -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<_>>(),
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<_>>(),
vec![
"chrome@openai-bundled".to_string(),
"slack@openai-curated".to_string(),
]
vec!["slack@openai-curated".to_string()]
);
}