Route curated plugin catalogs by authentication mode (#38429)

## Why

The model provider does not reliably identify which curated plugin catalog is
available. ChatGPT authentication can be used with a custom provider, while an
unauthenticated session should use the API-compatible catalog regardless of its
provider.

## What changed

- Select the ChatGPT curated catalog only for authentication modes that use the
  Codex backend; use the API curated catalog for API-key and unauthenticated
  sessions.
- Initialize standalone plugin managers with the current authentication mode
  across CLI, app-server, MCP, and external-agent migration paths.
- Preserve authentication mode while detecting and importing migrated plugins.

## Testing

- Cover catalog and skill routing across ChatGPT, API-key, unauthenticated,
  Bedrock, and custom-provider configurations.
- Verify authenticated plugin migration uses the ChatGPT curated marketplace.

GitOrigin-RevId: 660a339ee8891c33aad961078d3a979242a6a166
This commit is contained in:
willwang-openai authored and copyberry committed 2026-08-13 20:20:01 +00:00
1 parent 507ef0b371
commit f898ebcafd
28 files changed
+348 -139

No files matched your search

@@ -162,17 +162,17 @@ async fn plugin_list_skips_invalid_marketplace_file_and_reports_error() -> Resul
async fn plugin_installed_includes_installed_plugins_and_explicit_install_suggestions() -> Result<()>
{
let codex_home = TempDir::new()?;
write_openai_curated_marketplace(
write_openai_api_curated_marketplace(
codex_home.path(),
&["linear", "computer-use", "not-mentioned"],
)?;
write_installed_plugin(&codex_home, "openai-curated", "linear")?;
write_installed_plugin(&codex_home, "openai-api-curated", "linear")?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"[features]
plugins = true
[plugins."linear@openai-curated"]
[plugins."linear@openai-api-curated"]
enabled = true
"#,
)?;
@@ -193,7 +193,7 @@ enabled = true
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
assert_eq!(response.marketplaces.len(), 1);
assert_eq!(response.marketplaces[0].name, "openai-curated");
assert_eq!(response.marketplaces[0].name, "openai-api-curated");
assert_eq!(
response.marketplaces[0]
.plugins
@@ -201,8 +201,8 @@ enabled = true
.map(|plugin| (plugin.id.clone(), plugin.installed, plugin.enabled))
.collect::<Vec<_>>(),
vec![
("linear@openai-curated".to_string(), true, true),
("computer-use@openai-curated".to_string(), false, false),
("linear@openai-api-curated".to_string(), true, true),
("computer-use@openai-api-curated".to_string(), false, false),
]
);
assert_eq!(response.marketplace_load_errors, Vec::new());
@@ -4548,16 +4548,19 @@ remote_plugin = true
}
#[tokio::test]
async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Result<()> {
async fn plugin_list_omits_featured_plugin_ids_without_chatgpt_auth() -> Result<()> {
let codex_home = TempDir::new()?;
let server = MockServer::start().await;
write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;
write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;
write_openai_api_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;
Mock::given(method("GET"))
.and(path("/backend-api/plugins/featured"))
.and(query_param("platform", "codex"))
.respond_with(ResponseTemplate::new(200).set_body_string(r#"["linear@openai-curated"]"#))
.respond_with(
ResponseTemplate::new(200).set_body_string(r#"["linear@openai-api-curated"]"#),
)
.expect(0)
.mount(&server)
.await;
@@ -4577,10 +4580,8 @@ async fn plugin_list_fetches_featured_plugin_ids_without_chatgpt_auth() -> Resul
let response: PluginListResponse =
timeout(DEFAULT_TIMEOUT, mcp.read_response(request_id)).await??;
assert_eq!(
response.featured_plugin_ids,
vec!["linear@openai-curated".to_string()]
);
assert_eq!(response.featured_plugin_ids, Vec::<String>::new());
assert_eq!(response.marketplaces[0].name, "openai-api-curated");
Ok(())
}
@@ -4590,6 +4591,7 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() ->
let server = MockServer::start().await;
write_plugin_sync_config(codex_home.path(), &format!("{}/backend-api/", server.uri()))?;
write_openai_curated_marketplace(codex_home.path(), &["linear", "gmail"])?;
write_remote_plugin_test_auth(codex_home.path())?;
Mock::given(method("GET"))
.and(path("/backend-api/plugins/featured"))
@@ -5290,6 +5292,7 @@ chatgpt_base_url = "{base_url}"
[features]
plugins = true
remote_plugin = false
[plugins."linear@openai-curated"]
enabled = false
@@ -142,8 +142,13 @@ fn write_cached_remote_plugin_with_skill(
Ok(skill_path)
}
fn write_cached_local_curated_plugin_with_skill(codex_home: &std::path::Path) -> Result<()> {
let plugin_root = codex_home.join("plugins/cache/openai-curated/google-calendar/local");
fn write_cached_local_curated_plugin_with_skill(
codex_home: &std::path::Path,
marketplace_name: &str,
) -> Result<()> {
let plugin_root = codex_home.join(format!(
"plugins/cache/{marketplace_name}/google-calendar/local"
));
std::fs::create_dir_all(plugin_root.join(".codex-plugin"))?;
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
@@ -397,7 +402,7 @@ async fn runtime_remote_plugin_toggle_updates_local_curated_plugin_skills() -> R
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let server = MockServer::start().await;
write_cached_local_curated_plugin_with_skill(codex_home.path())?;
write_cached_local_curated_plugin_with_skill(codex_home.path(), "openai-curated")?;
std::fs::write(
codex_home.path().join("config.toml"),
format!(
@@ -842,13 +847,13 @@ async fn skills_list_preserves_requested_cwd_order() -> Result<()> {
let first_cwd = TempDir::new()?;
let second_cwd = TempDir::new()?;
write_skill(&codex_home, "shared-skill")?;
write_cached_local_curated_plugin_with_skill(codex_home.path())?;
write_cached_local_curated_plugin_with_skill(codex_home.path(), "openai-api-curated")?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"[features]
plugins = true
[plugins."google-calendar@openai-curated"]
[plugins."google-calendar@openai-api-curated"]
enabled = true
"#,
)?;
@@ -858,7 +863,9 @@ enabled = true
std::fs::create_dir_all(cwd.join(".codex"))?;
std::fs::write(
cwd.join(".codex/config.toml"),
format!("[plugins.\"google-calendar@openai-curated\"]\nenabled = {plugin_enabled}\n"),
format!(
"[plugins.\"google-calendar@openai-api-curated\"]\nenabled = {plugin_enabled}\n"
),
)?;
set_project_trust_level(codex_home.path(), cwd, TrustLevel::Trusted)?;
}
@@ -955,13 +962,13 @@ async fn skills_list_force_reload_refreshes_cached_plugin_roots() -> Result<()>
let codex_home = TempDir::new()?;
let first_cwd = TempDir::new()?;
let second_cwd = TempDir::new()?;
write_cached_local_curated_plugin_with_skill(codex_home.path())?;
write_cached_local_curated_plugin_with_skill(codex_home.path(), "openai-api-curated")?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"[features]
plugins = true
[plugins."google-calendar@openai-curated"]
[plugins."google-calendar@openai-api-curated"]
enabled = true
"#,
)?;
@@ -991,7 +998,7 @@ enabled = true
if force_reload {
let plugin_root = codex_home
.path()
.join("plugins/cache/openai-curated/google-calendar/local");
.join("plugins/cache/openai-api-curated/google-calendar/local");
std::fs::write(
plugin_root.join(".codex-plugin/plugin.json"),
r#"{"name":"google-calendar","skills":"./replacement-skills"}"#,
@@ -4414,7 +4414,7 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<(
let curated_sha = "0123456789abcdef0123456789abcdef01234567";
let plugin_root = codex_home
.path()
.join("plugins/cache/openai-curated/google-calendar/01234567");
.join("plugins/cache/openai-api-curated/google-calendar/01234567");
let script_path = plugin_root.join("scripts/run.sh");
let synced_root = codex_home.path().join(".tmp/plugins");
for path in [
@@ -4437,9 +4437,9 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<(
format!("{curated_sha}\n"),
)?;
std::fs::write(
synced_root.join(".agents/plugins/marketplace.json"),
synced_root.join(".agents/plugins/api_marketplace.json"),
r#"{
"name": "openai-curated",
"name": "openai-api-curated",
"plugins": [{
"name": "google-calendar",
"source": {"source": "local", "path": "./plugins/google-calendar"}
@@ -4464,7 +4464,7 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<(
.with_sandbox_mode("danger-full-access")
.enable_feature(Feature::Plugins)
.disable_feature(Feature::RemotePlugin)
.with_extra_config("[plugins.\"google-calendar@openai-curated\"]\nenabled = true")
.with_extra_config("[plugins.\"google-calendar@openai-api-curated\"]\nenabled = true")
.write(codex_home.path())?;
let mut mcp = TestAppServer::builder()
@@ -4506,7 +4506,7 @@ async fn command_execution_notifications_include_trusted_plugin_id() -> Result<(
.expect("command execution item should include scriptPath");
assert_eq!(
(item_json["pluginId"].as_str(), emitted_script_path),
(Some("google-calendar@openai-curated"), "scripts/run.sh")
(Some("google-calendar@openai-api-curated"), "scripts/run.sh")
);
assert!(
!emitted_script_path.contains(script_path.to_string_lossy().as_ref()),