diff --git a/codex-rs/app-server/src/external_agent_migration/processor.rs b/codex-rs/app-server/src/external_agent_migration/processor.rs index fb0d9b2d5a..1e7def61d4 100644 --- a/codex-rs/app-server/src/external_agent_migration/processor.rs +++ b/codex-rs/app-server/src/external_agent_migration/processor.rs @@ -98,6 +98,7 @@ impl ExternalAgentConfigRequestProcessor { } = args; let migration_service = ExternalAgentConfigService::new( codex_home.clone(), + thread_manager.plugins_manager().auth_mode(), analytics_events_client.clone(), state_db.clone(), ); @@ -127,6 +128,7 @@ impl ExternalAgentConfigRequestProcessor { ) -> Result { let migration_service = self .migration_service + .with_auth_mode(self.thread_manager.plugins_manager().auth_mode()) .with_migration_source(params.migration_source.as_deref()); let default_session_import_limits = ExternalAgentSessionImportLimits::default(); let migration_service = @@ -198,6 +200,7 @@ impl ExternalAgentConfigRequestProcessor { let provider_id = params.provider_id.clone(); let migration_service = self .migration_service + .with_auth_mode(self.thread_manager.plugins_manager().auth_mode()) .with_migration_source(params.migration_source.as_deref()); let needs_runtime_refresh = migration_items_need_runtime_refresh(¶ms.migration_items); let has_migration_items = !params.migration_items.is_empty(); 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 927902d365..e224c9c237 100644 --- a/codex-rs/app-server/tests/suite/v2/plugin_list.rs +++ b/codex-rs/app-server/tests/suite/v2/plugin_list.rs @@ -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![ - ("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::::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 diff --git a/codex-rs/app-server/tests/suite/v2/skills_list.rs b/codex-rs/app-server/tests/suite/v2/skills_list.rs index 6a8253a4f2..63142396f2 100644 --- a/codex-rs/app-server/tests/suite/v2/skills_list.rs +++ b/codex-rs/app-server/tests/suite/v2/skills_list.rs @@ -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"}"#, diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index c9709eef06..06bf7966fa 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -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()), diff --git a/codex-rs/cli/src/marketplace_cmd.rs b/codex-rs/cli/src/marketplace_cmd.rs index d319e9af93..c243fc447c 100644 --- a/codex-rs/cli/src/marketplace_cmd.rs +++ b/codex-rs/cli/src/marketplace_cmd.rs @@ -211,8 +211,7 @@ async fn run_list(overrides: Vec<(String, toml::Value)>, args: ListMarketplaceAr let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; - let manager = plugins_manager_for_config(&config); - manager.set_auth_mode(load_cli_auth_mode(&config).await?); + let manager = plugins_manager_for_config(&config, load_cli_auth_mode(&config).await?); let plugins_input = config.plugins_config_input(); let marketplace_listing = manager .discover_marketplaces_for_config(&plugins_input, &[]) @@ -378,7 +377,7 @@ async fn run_upgrade( let config = Config::load_with_cli_overrides(overrides) .await .context("failed to load configuration")?; - let manager = plugins_manager_for_config(&config); + let manager = plugins_manager_for_config(&config, load_cli_auth_mode(&config).await?); let plugins_input = config.plugins_config_input(); let outcome = manager .upgrade_configured_marketplaces_for_config(&plugins_input, marketplace_name.as_deref()) diff --git a/codex-rs/cli/src/mcp_cmd.rs b/codex-rs/cli/src/mcp_cmd.rs index e434a0a9d1..b2296d96df 100644 --- a/codex-rs/cli/src/mcp_cmd.rs +++ b/codex-rs/cli/src/mcp_cmd.rs @@ -524,8 +524,10 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr } async fn load_mcp_manager(config: &Config) -> Result { - let plugins_manager = Arc::new(plugins_manager_for_config(config)); - plugins_manager.set_auth_mode(load_cli_auth_mode(config).await?); + let plugins_manager = Arc::new(plugins_manager_for_config( + config, + load_cli_auth_mode(config).await?, + )); Ok(McpManager::new(plugins_manager)) } diff --git a/codex-rs/cli/src/plugin_cmd.rs b/codex-rs/cli/src/plugin_cmd.rs index 48dae16bdf..27e5a09e73 100644 --- a/codex-rs/cli/src/plugin_cmd.rs +++ b/codex-rs/cli/src/plugin_cmd.rs @@ -590,8 +590,7 @@ async fn load_plugin_command_context( .await .context("failed to load configuration")?; let plugins_input = config.plugins_config_input(); - let manager = plugins_manager_for_config(&config); - manager.set_auth_mode(load_cli_auth_mode(&config).await?); + let manager = plugins_manager_for_config(&config, load_cli_auth_mode(&config).await?); Ok(PluginCommandContext { codex_home: codex_home.to_path_buf(), plugins_input, diff --git a/codex-rs/core-plugins/src/discoverable_tests.rs b/codex-rs/core-plugins/src/discoverable_tests.rs index d95eaa6eaf..ccd4c8644e 100644 --- a/codex-rs/core-plugins/src/discoverable_tests.rs +++ b/codex-rs/core-plugins/src/discoverable_tests.rs @@ -113,6 +113,7 @@ async fn returns_microsoft_fallback_plugins() { let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[], &[], &[]), @@ -190,10 +191,10 @@ source = "/tmp/{bundled_marketplace_name}" } #[tokio::test] -async fn includes_openai_curated_when_remote_enabled_without_auth() { +async fn includes_openai_api_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"]); + write_openai_api_curated_marketplace(&curated_root, &["slack"]); let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); @@ -209,7 +210,7 @@ async fn includes_openai_curated_when_remote_enabled_without_auth() { .into_iter() .map(|plugin| plugin.id) .collect::>(), - vec!["slack@openai-curated".to_string()] + vec!["slack@openai-api-curated".to_string()] ); } @@ -300,6 +301,7 @@ async fn reprojects_cached_skill_availability_for_current_config() { let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let expected = ToolSuggestDiscoverablePlugin { id: "slack@openai-curated".to_string(), remote_plugin_id: None, @@ -354,6 +356,7 @@ async fn does_not_advertise_skills_when_skill_loading_fails() { let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[], &[], &[]), @@ -393,6 +396,7 @@ async fn clear_cache_invalidates_cached_tool_suggest_metadata() { let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let input = discovery_input(plugins, &[], &[], &[]); let expected_cached = vec![ToolSuggestDiscoverablePlugin { id: "slack@openai-curated".to_string(), @@ -465,6 +469,7 @@ source = "/tmp/{marketplace_name}" let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[], &[], &[]), @@ -492,6 +497,7 @@ async fn normalizes_description() { let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[], &[], &[]), @@ -576,6 +582,7 @@ async fn omits_not_available_curated_plugins() { let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let discoverable_plugins = list_discoverable_plugins( &plugins_manager, discovery_input(plugins, &[], &[], &[]), @@ -617,6 +624,7 @@ async fn does_not_reload_marketplace_per_plugin() { let plugins = load_plugins_config(codex_home.path(), codex_home.path()).await; let plugins_manager = test_plugins_manager(codex_home.path().to_path_buf()); + plugins_manager.set_auth_mode(Some(AuthMode::Chatgpt)); let buffer: &'static std::sync::Mutex> = Box::leak(Box::new(std::sync::Mutex::new(Vec::new()))); let subscriber = tracing_subscriber::fmt() diff --git a/codex-rs/core-plugins/src/manager.rs b/codex-rs/core-plugins/src/manager.rs index 8ad903dc86..5656de77d7 100644 --- a/codex-rs/core-plugins/src/manager.rs +++ b/codex-rs/core-plugins/src/manager.rs @@ -82,7 +82,6 @@ use codex_hooks::plugin_hook_declarations; use codex_http_client::HttpClientFactory; use codex_login::AuthManager; use codex_login::CodexAuth; -use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID; use codex_plugin::AppConnectorId; use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; @@ -491,35 +490,24 @@ impl PluginLoadCacheKey { } } -fn target_curated_marketplace( - auth_mode: Option, - model_provider_id: &str, -) -> TargetCuratedMarketplace { - match auth_mode { - Some(auth_mode) => { - if auth_mode.uses_codex_backend() { - TargetCuratedMarketplace::OpenAiWithRemote - } else { - TargetCuratedMarketplace::OpenAiApi - } - } - // Bedrock can use ambient AWS credentials without producing a stored auth mode. - None if model_provider_id == AMAZON_BEDROCK_PROVIDER_ID => { - TargetCuratedMarketplace::OpenAiApi - } - None => TargetCuratedMarketplace::OpenAi, +fn target_curated_marketplace(auth_mode: Option) -> TargetCuratedMarketplace { + if auth_mode.is_some_and(AuthMode::uses_codex_backend) { + TargetCuratedMarketplace::OpenAiWithRemote + } else { + TargetCuratedMarketplace::OpenAiApi } } impl PluginsManager { pub fn new( codex_home: PathBuf, + auth_mode: Option, skill_root_loader: Arc>, ) -> Self { Self::new_with_options( codex_home, Some(Product::Codex), - /*auth_mode*/ None, + auth_mode, skill_root_loader, ) } @@ -683,7 +671,7 @@ impl PluginsManager { remote_global_catalog_active, ); if !force_reload && let Some(plugins) = self.cached_loaded_plugins(&cache_key) { - return self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id); + return self.resolve_loaded_plugins_for_auth(plugins); } let Ok(_load_permit) = self.loaded_plugins_load_semaphore.acquire().await else { @@ -691,7 +679,7 @@ impl PluginsManager { return PluginLoadOutcome::default(); }; if !force_reload && let Some(plugins) = self.cached_loaded_plugins(&cache_key) { - return self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id); + return self.resolve_loaded_plugins_for_auth(plugins); } let cache_generation = self.loaded_plugins_cache_generation(); let plugin_skill_snapshots = new_plugin_skill_snapshots(); @@ -712,16 +700,12 @@ impl PluginsManager { plugins.clone(), plugin_skill_snapshots, ); - self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id) + self.resolve_loaded_plugins_for_auth(plugins) } - fn resolve_loaded_plugins_for_auth( - &self, - mut plugins: Vec, - model_provider_id: &str, - ) -> PluginLoadOutcome { + fn resolve_loaded_plugins_for_auth(&self, mut plugins: Vec) -> PluginLoadOutcome { let auth_mode = self.auth_mode(); - let target_curated_marketplace = target_curated_marketplace(auth_mode, model_provider_id); + let target_curated_marketplace = target_curated_marketplace(auth_mode); plugins.retain(|plugin| { plugin_is_eligible_for_target_marketplace( &plugin.config_name, @@ -796,8 +780,7 @@ impl PluginsManager { if !config.plugins_enabled { return PluginHookLoadOutcome::default(); } - let target_curated_marketplace = - target_curated_marketplace(self.auth_mode(), &config.model_provider_id); + let target_curated_marketplace = target_curated_marketplace(self.auth_mode()); load_plugin_hooks_from_layer_stack( config_layer_stack, self.remote_installed_plugin_configs(), @@ -2909,7 +2892,7 @@ impl PluginsManager { self.codex_home.as_path(), )); let curated_marketplace_path = if include_openai_curated { - match target_curated_marketplace(self.auth_mode(), &config.model_provider_id) { + match target_curated_marketplace(self.auth_mode()) { TargetCuratedMarketplace::OpenAi | TargetCuratedMarketplace::OpenAiWithRemote => { let curated_repo_root = curated_plugins_repo_path(self.codex_home.as_path()); curated_repo_root.is_dir().then_some(curated_repo_root) diff --git a/codex-rs/core-plugins/src/manager_tests.rs b/codex-rs/core-plugins/src/manager_tests.rs index 61cce20b58..9dbada513f 100644 --- a/codex-rs/core-plugins/src/manager_tests.rs +++ b/codex-rs/core-plugins/src/manager_tests.rs @@ -45,6 +45,7 @@ use codex_config::SkillConfigRules; use codex_config::compose_requirements; use codex_config::types::McpServerTransportConfig; use codex_login::CodexAuth; +use codex_model_provider::AMAZON_BEDROCK_PROVIDER_ID; use codex_plugin::AppDeclaration; use codex_plugin::PluginId; use codex_protocol::auth::AuthMode; @@ -1637,7 +1638,7 @@ enabled = true manager.set_auth_mode(/*auth_mode*/ None); assert_eq!( loaded_plugin_names(&manager, &config).await, - vec!["linear@openai-curated".to_string()] + vec!["linear@openai-api-curated".to_string()] ); for auth_mode in [AuthMode::ApiKey, AuthMode::BedrockApiKey] { @@ -1650,11 +1651,13 @@ enabled = true } manager.set_auth_mode(/*auth_mode*/ None); - config.model_provider_id = AMAZON_BEDROCK_PROVIDER_ID.to_string(); - assert_eq!( - loaded_plugin_names(&manager, &config).await, - vec!["linear@openai-api-curated".to_string()] - ); + for model_provider_id in ["openai", AMAZON_BEDROCK_PROVIDER_ID, "ollama"] { + config.model_provider_id = model_provider_id.to_string(); + assert_eq!( + loaded_plugin_names(&manager, &config).await, + vec!["linear@openai-api-curated".to_string()] + ); + } manager.set_auth_mode(Some(AuthMode::Chatgpt)); assert_eq!( @@ -4679,10 +4682,14 @@ plugins = true .unwrap(); let config = load_config(tmp.path(), tmp.path()).await; - let marketplaces = test_plugins_manager(tmp.path().to_path_buf()) - .list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true) - .unwrap() - .marketplaces; + let marketplaces = test_plugins_manager_with_options( + tmp.path().to_path_buf(), + Some(Product::Codex), + Some(AuthMode::Chatgpt), + ) + .list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true) + .unwrap() + .marketplaces; let curated_marketplace = marketplaces .into_iter() @@ -4843,16 +4850,36 @@ plugins = true } #[tokio::test] -async fn list_marketplaces_uses_resolved_provider_instead_of_configured_default() { - for (configured_provider, resolved_provider, expected_marketplace) in [ +async fn list_marketplaces_selects_curated_catalog_only_from_authentication() { + for (configured_provider, resolved_provider, auth_mode, expected_marketplace) in [ ( "openai", AMAZON_BEDROCK_PROVIDER_ID, + None, OPENAI_API_CURATED_MARKETPLACE_NAME, ), ( AMAZON_BEDROCK_PROVIDER_ID, "openai", + None, + OPENAI_API_CURATED_MARKETPLACE_NAME, + ), + ( + "openai", + "ollama", + None, + OPENAI_API_CURATED_MARKETPLACE_NAME, + ), + ( + "openai", + "ollama", + Some(AuthMode::ApiKey), + OPENAI_API_CURATED_MARKETPLACE_NAME, + ), + ( + "openai", + "ollama", + Some(AuthMode::Chatgpt), OPENAI_CURATED_MARKETPLACE_NAME, ), ] { @@ -4874,16 +4901,22 @@ plugins = true let mut config = load_config(tmp.path(), tmp.path()).await; config.model_provider_id = resolved_provider.to_string(); - let marketplaces = test_plugins_manager(tmp.path().to_path_buf()) - .list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true) - .unwrap() - .marketplaces; + let marketplaces = test_plugins_manager_with_options( + tmp.path().to_path_buf(), + Some(Product::Codex), + auth_mode, + ) + .list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true) + .unwrap() + .marketplaces; - assert!( + assert_eq!( marketplaces .iter() - .any(|marketplace| marketplace.name == expected_marketplace), - "expected `{expected_marketplace}` for resolved provider `{resolved_provider}`" + .map(|marketplace| marketplace.name.as_str()) + .collect::>(), + vec![expected_marketplace], + "unexpected curated catalog for provider `{resolved_provider}` with auth {auth_mode:?}" ); } } diff --git a/codex-rs/core-plugins/src/test_support.rs b/codex-rs/core-plugins/src/test_support.rs index c182bfb2d4..13b69c9c42 100644 --- a/codex-rs/core-plugins/src/test_support.rs +++ b/codex-rs/core-plugins/src/test_support.rs @@ -43,7 +43,11 @@ pub(crate) const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcd pub(crate) const TEST_CURATED_PLUGIN_CACHE_VERSION: &str = "01234567"; pub(crate) fn test_plugins_manager(codex_home: PathBuf) -> PluginsManager { - PluginsManager::new(codex_home, test_skill_root_loader()) + PluginsManager::new( + codex_home, + /*auth_mode*/ None, + test_skill_root_loader(), + ) } pub(crate) fn test_plugins_manager_with_options( diff --git a/codex-rs/core/src/agent/role_tests.rs b/codex-rs/core/src/agent/role_tests.rs index 16e16bb56d..32db1a3af8 100644 --- a/codex-rs/core/src/agent/role_tests.rs +++ b/codex-rs/core/src/agent/role_tests.rs @@ -463,7 +463,7 @@ enabled = false .await .expect("custom role should apply"); - let plugins_manager = Arc::new(plugins_manager_for_config(&config)); + let plugins_manager = Arc::new(plugins_manager_for_config(&config, /*auth_mode*/ None)); let skills_service = HostSkillsService::new(home.path().abs(), /*bundled_skills_enabled*/ true); let plugins_input = config.plugins_config_input(); diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 1e35618276..da3555d01c 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -522,7 +522,7 @@ async fn load_config_resolves_non_prefixed_mcp_tool_servers() -> std::io::Result assert_eq!(config.non_prefixed_mcp_tool_servers, expected_servers); assert_eq!(config.prefix_mcp_tool_names(), expected_prefix); - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; assert_eq!(mcp_config.prefix_mcp_tool_names, expected_prefix); assert_eq!( @@ -5234,7 +5234,7 @@ async fn rebuild_preserving_session_layers_refreshes_plugin_derived_mcp_config() let config = thread_config .rebuild_preserving_session_layers(&refreshed_config) .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; let configured_servers = mcp_config.mcp_server_catalog.configured_servers(); @@ -5296,7 +5296,7 @@ enabled = true .codex_home(codex_home.path().to_path_buf()) .build() .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; let configured_servers = mcp_config.mcp_server_catalog.configured_servers(); @@ -5364,7 +5364,7 @@ url = "https://sample.example/mcp" ) .build() .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; let configured_servers = mcp_config.mcp_server_catalog.configured_servers(); @@ -5466,7 +5466,7 @@ enabled = true ) .build() .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; let configured_servers = mcp_config.mcp_server_catalog.configured_servers(); @@ -6483,7 +6483,7 @@ async fn to_mcp_config_preserves_apps_feature_from_config() -> std::io::Result<( codex_home.abs(), ) .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); config.apps_mcp_product_sku = Some("tpp".to_string()); let mcp_config = config.to_mcp_config(&plugins_manager).await; @@ -6510,7 +6510,7 @@ async fn to_mcp_config_flows_mcp_tool_prefix_from_feature() -> std::io::Result<( codex_home.abs(), ) .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; assert!(mcp_config.prefix_mcp_tool_names); @@ -6546,7 +6546,7 @@ async fn to_mcp_config_flows_mcp_2026_feature_from_config() -> std::io::Result<( codex_home.abs(), ) .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; assert_eq!(mcp_config.protocol_mode, codex_mcp::McpProtocolMode::Legacy); @@ -6570,7 +6570,7 @@ async fn to_mcp_config_preserves_auth_elicitation_feature_from_config() -> std:: codex_home.abs(), ) .await?; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); let mcp_config = config.to_mcp_config(&plugins_manager).await; assert_eq!( diff --git a/codex-rs/core/src/connectors.rs b/codex-rs/core/src/connectors.rs index 118033a752..d99ada63f0 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -189,7 +189,13 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager( force_refetch: bool, environment_manager: Arc, ) -> anyhow::Result { - let plugins_manager = Arc::new(plugins_manager_for_config(config)); + let auth_manager = + AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await?; + let auth = auth_manager.auth().await; + let plugins_manager = Arc::new(plugins_manager_for_config( + config, + auth.as_ref().map(CodexAuth::api_auth_mode), + )); let mcp_manager = Arc::new(McpManager::new(plugins_manager)); list_accessible_connectors_from_mcp_tools_with_mcp_manager( config, diff --git a/codex-rs/core/src/connectors_tests.rs b/codex-rs/core/src/connectors_tests.rs index 8258f44763..8cdab1ecd1 100644 --- a/codex-rs/core/src/connectors_tests.rs +++ b/codex-rs/core/src/connectors_tests.rs @@ -537,7 +537,7 @@ discoverables = [ .await .expect("config should load"); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, Some(auth.api_auth_mode())); let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth( &config, @@ -575,7 +575,7 @@ apps = true .expect("config should load"); let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); let loaded_plugin_app_connector_ids = vec!["asdk_app_databricks_workspace".to_string()]; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, Some(auth.api_auth_mode())); let discoverable_tools = list_tool_suggest_discoverable_tools_with_auth( &config, diff --git a/codex-rs/core/src/plugins/discoverable_tests.rs b/codex-rs/core/src/plugins/discoverable_tests.rs index 0e11d7e832..89af2e9cc5 100644 --- a/codex-rs/core/src/plugins/discoverable_tests.rs +++ b/codex-rs/core/src/plugins/discoverable_tests.rs @@ -1,7 +1,7 @@ use crate::plugins::plugins_manager_for_config; use crate::plugins::test_support::load_plugins_config; use crate::plugins::test_support::write_file; -use crate::plugins::test_support::write_openai_curated_marketplace; +use crate::plugins::test_support::write_openai_api_curated_marketplace; use codex_core_plugins::startup_sync::curated_plugins_repo_path; use codex_tools::DiscoverablePluginInfo; use pretty_assertions::assert_eq; @@ -11,7 +11,7 @@ async fn list_discoverable_plugins( config: &crate::config::Config, loaded_plugin_app_connector_ids: &[String], ) -> anyhow::Result> { - let plugins_manager = plugins_manager_for_config(config); + let plugins_manager = plugins_manager_for_config(config, /*auth_mode*/ None); super::list_tool_suggest_discoverable_plugins( config, &plugins_manager, @@ -25,7 +25,7 @@ async fn list_discoverable_plugins( async fn list_tool_suggest_discoverable_plugins_returns_empty_when_plugins_feature_disabled() { 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"]); + write_openai_api_curated_marketplace(&curated_root, &["slack"]); write_file( &codex_home.path().join(crate::config::CONFIG_TOML_FILE), r#"[features] @@ -43,7 +43,7 @@ plugins = false async fn list_tool_suggest_discoverable_plugins_omits_disabled_tool_suggestions() { 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"]); + write_openai_api_curated_marketplace(&curated_root, &["slack"]); write_file( &codex_home.path().join(crate::config::CONFIG_TOML_FILE), r#"[features] @@ -51,7 +51,7 @@ plugins = true [tool_suggest] disabled_tools = [ - { type = "plugin", id = "slack@openai-curated" } + { type = "plugin", id = "slack@openai-api-curated" } ] "#, ); @@ -66,14 +66,14 @@ disabled_tools = [ async fn list_tool_suggest_discoverable_plugins_includes_configured_plugin_ids() { 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"]); + write_openai_api_curated_marketplace(&curated_root, &["sample"]); write_file( &codex_home.path().join(crate::config::CONFIG_TOML_FILE), r#"[features] plugins = true [tool_suggest] -discoverables = [{ type = "plugin", id = "sample@openai-curated" }] +discoverables = [{ type = "plugin", id = "sample@openai-api-curated" }] "#, ); @@ -83,7 +83,7 @@ discoverables = [{ type = "plugin", id = "sample@openai-curated" }] assert_eq!( discoverable_plugins, vec![DiscoverablePluginInfo { - id: "sample@openai-curated".to_string(), + id: "sample@openai-api-curated".to_string(), remote_plugin_id: None, name: "sample".to_string(), description: Some( diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index 0a399b4525..ad6616de99 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -11,6 +11,7 @@ pub(crate) mod test_support; use crate::config::Config; use codex_core_plugins::PluginsManager; +use codex_protocol::auth::AuthMode; use codex_skills_extension::HostSkillsService; use std::sync::Arc; @@ -28,10 +29,14 @@ pub(crate) use mentions::collect_tool_mentions_from_messages; /// Constructs a standalone plugin manager with extension-owned plugin skill loading. /// /// Callers that already own a host skills service should inject that existing service instead. -pub fn plugins_manager_for_config(config: &Config) -> PluginsManager { +pub fn plugins_manager_for_config(config: &Config, auth_mode: Option) -> PluginsManager { let skill_root_loader = Arc::new(HostSkillsService::new( config.codex_home.clone(), /*bundled_skills_enabled*/ false, )); - PluginsManager::new(config.codex_home.to_path_buf(), skill_root_loader) + PluginsManager::new( + config.codex_home.to_path_buf(), + auth_mode, + skill_root_loader, + ) } diff --git a/codex-rs/core/src/plugins/test_support.rs b/codex-rs/core/src/plugins/test_support.rs index 8fbaebb803..bf54e40815 100644 --- a/codex-rs/core/src/plugins/test_support.rs +++ b/codex-rs/core/src/plugins/test_support.rs @@ -3,7 +3,7 @@ use crate::config::ConfigBuilder; use std::fs; use std::path::Path; -use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME; +use codex_core_plugins::OPENAI_API_CURATED_MARKETPLACE_NAME; pub(crate) const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567"; @@ -50,7 +50,7 @@ pub(crate) fn write_curated_plugin(root: &Path, plugin_name: &str) { ); } -pub(crate) fn write_openai_curated_marketplace(root: &Path, plugin_names: &[&str]) { +pub(crate) fn write_openai_api_curated_marketplace(root: &Path, plugin_names: &[&str]) { let plugins = plugin_names .iter() .map(|plugin_name| { @@ -67,10 +67,10 @@ pub(crate) fn write_openai_curated_marketplace(root: &Path, plugin_names: &[&str .collect::>() .join(",\n"); write_file( - &root.join(".agents/plugins/marketplace.json"), + &root.join(".agents/plugins/api_marketplace.json"), &format!( r#"{{ - "name": "{OPENAI_CURATED_MARKETPLACE_NAME}", + "name": "{OPENAI_API_CURATED_MARKETPLACE_NAME}", "plugins": [ {plugins} ] diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 1f5fccf9db..0d231921c2 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -5653,7 +5653,10 @@ async fn session_new_fails_when_zsh_fork_enabled_without_packaged_zsh() { let (tx_event, _rx_event) = async_channel::unbounded(); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); - let plugins_manager = Arc::new(plugins_manager_for_config(&config)); + let plugins_manager = Arc::new(plugins_manager_for_config( + &config, + auth_manager.get_api_auth_mode(), + )); let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager))); let skills_service = Arc::new(HostSkillsService::new( config.codex_home.clone(), @@ -5818,7 +5821,10 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) { .expect("primary environment") .environment, ); - let plugins_manager = Arc::new(plugins_manager_for_config(&config)); + let plugins_manager = Arc::new(plugins_manager_for_config( + &config, + auth_manager.get_api_auth_mode(), + )); let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager))); let skills_service = Arc::new(HostSkillsService::new( config.codex_home.clone(), @@ -6073,7 +6079,10 @@ async fn make_session_with_config_and_rx( let (tx_event, rx_event) = async_channel::unbounded(); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); - let plugins_manager = Arc::new(plugins_manager_for_config(&config)); + let plugins_manager = Arc::new(plugins_manager_for_config( + &config, + auth_manager.get_api_auth_mode(), + )); let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager))); let skills_service = Arc::new(HostSkillsService::new( config.codex_home.clone(), @@ -6188,7 +6197,10 @@ async fn make_session_with_history_source_and_agent_control_and_rx( let (tx_event, rx_event) = async_channel::unbounded(); let (agent_status_tx, _agent_status_rx) = watch::channel(AgentStatus::PendingInit); - let plugins_manager = Arc::new(plugins_manager_for_config(&config)); + let plugins_manager = Arc::new(plugins_manager_for_config( + &config, + auth_manager.get_api_auth_mode(), + )); let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager))); let skills_service = Arc::new(HostSkillsService::new( config.codex_home.clone(), @@ -7997,7 +8009,10 @@ where .expect("primary environment") .environment, ); - let plugins_manager = Arc::new(plugins_manager_for_config(&config)); + let plugins_manager = Arc::new(plugins_manager_for_config( + &config, + auth_manager.get_api_auth_mode(), + )); let mcp_manager = Arc::new(McpManager::new(Arc::clone(&plugins_manager))); let skills_service = Arc::new(HostSkillsService::new( config.codex_home.clone(), @@ -8524,6 +8539,10 @@ async fn mcp_refresh_updates_plugin_auth_mode_before_checking_pending_state() { CodexAuth::from_api_key("old-api-key"), codex_home.path().to_path_buf(), ); + session + .services + .plugins_manager + .set_auth_mode(/*auth_mode*/ None); let session = Arc::new(session); let auth_mode = session.services.auth_manager.get_api_auth_mode(); diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index c4d31e951f..6eb6a35b75 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -750,7 +750,10 @@ async fn guardian_subagent_does_not_inherit_parent_exec_policy_rules() { auth_manager.clone(), config.model_provider.clone(), ); - let plugins_manager = Arc::new(plugins_manager_for_config(&config)); + let plugins_manager = Arc::new(plugins_manager_for_config( + &config, + auth_manager.get_api_auth_mode(), + )); let skills_service = Arc::new(HostSkillsService::new( config.codex_home.clone(), /*bundled_skills_enabled*/ true, diff --git a/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs b/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs index 846ae38d13..c505b8b2ea 100644 --- a/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs +++ b/codex-rs/core/src/tools/handlers/request_plugin_install_tests.rs @@ -2,7 +2,7 @@ use super::*; use crate::plugins::plugins_manager_for_config; use crate::plugins::test_support::load_plugins_config; use crate::plugins::test_support::write_curated_plugin_sha; -use crate::plugins::test_support::write_openai_curated_marketplace; +use crate::plugins::test_support::write_openai_api_curated_marketplace; use crate::plugins::test_support::write_plugins_feature_config; use codex_config::CONFIG_TOML_FILE; use codex_config::config_toml::ConfigToml; @@ -35,15 +35,15 @@ fn request_plugin_install_does_not_support_parallel_tool_calls() { async fn verified_plugin_install_completed_requires_installed_plugin() { 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"]); + write_openai_api_curated_marketplace(&curated_root, &["sample"]); write_curated_plugin_sha(codex_home.path()); write_plugins_feature_config(codex_home.path()); let config = load_plugins_config(codex_home.path()).await; - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, /*auth_mode*/ None); assert!(!verified_plugin_install_completed( - "sample@openai-curated", + "sample@openai-api-curated", &config, &plugins_manager, )); @@ -54,7 +54,7 @@ async fn verified_plugin_install_completed_requires_installed_plugin() { PluginInstallRequest { plugin_name: "sample".to_string(), marketplace_path: AbsolutePathBuf::try_from( - curated_root.join(".agents/plugins/marketplace.json"), + curated_root.join(".agents/plugins/api_marketplace.json"), ) .expect("marketplace path"), }, @@ -64,7 +64,7 @@ async fn verified_plugin_install_completed_requires_installed_plugin() { let refreshed_config = load_plugins_config(codex_home.path()).await; assert!(verified_plugin_install_completed( - "sample@openai-curated", + "sample@openai-api-curated", &refreshed_config, &plugins_manager, )); diff --git a/codex-rs/core/tests/suite/mcp_auth_refresh.rs b/codex-rs/core/tests/suite/mcp_auth_refresh.rs index 4fa1f437a5..ffa80cb8bc 100644 --- a/codex-rs/core/tests/suite/mcp_auth_refresh.rs +++ b/codex-rs/core/tests/suite/mcp_auth_refresh.rs @@ -88,7 +88,7 @@ async fn hosted_plugin_runtime_ps_mcp_tool_calls_use_current_auth_manager_token( .build() .await?; config.permissions.approval_policy = Constrained::allow_any(AskForApproval::Never); - let plugins_manager = plugins_manager_for_config(&config); + let plugins_manager = plugins_manager_for_config(&config, auth_manager.get_api_auth_mode()); let mcp_config = Arc::new(config.to_mcp_config(&plugins_manager).await); let runtime = McpRuntime::new(McpRuntimeInput { startup_policy: McpStartupPolicy::Eager, diff --git a/codex-rs/core/tests/suite/plugins.rs b/codex-rs/core/tests/suite/plugins.rs index a7654e2f9a..a77c7f726b 100644 --- a/codex-rs/core/tests/suite/plugins.rs +++ b/codex-rs/core/tests/suite/plugins.rs @@ -826,6 +826,14 @@ async fn agent_turns_route_curated_plugin_skills_after_auth_switch() -> Result<( expected_target_loaded_plugin_skills: &[CHATGPT_CURATED_PLUGIN_SKILL], expected_target_skill_description: "chatgpt description", }, + Fixture { + name: "ChatGPT with a custom provider", + target_auth: TargetAuth::Chatgpt, + target_model_provider_id: "ollama", + target_prompt: "chatgpt custom provider target turn", + expected_target_loaded_plugin_skills: &[CHATGPT_CURATED_PLUGIN_SKILL], + expected_target_skill_description: "chatgpt description", + }, Fixture { name: "API key", target_auth: TargetAuth::ApiKey, @@ -850,6 +858,22 @@ async fn agent_turns_route_curated_plugin_skills_after_auth_switch() -> Result<( expected_target_loaded_plugin_skills: &[API_CURATED_PLUGIN_SKILL], expected_target_skill_description: "api description before", }, + Fixture { + name: "unauthenticated OpenAI", + target_auth: TargetAuth::NoCodexAuth, + target_model_provider_id: OPENAI_PROVIDER_ID, + target_prompt: "unauthenticated openai target turn", + expected_target_loaded_plugin_skills: &[API_CURATED_PLUGIN_SKILL], + expected_target_skill_description: "api description before", + }, + Fixture { + name: "unauthenticated custom provider", + target_auth: TargetAuth::NoCodexAuth, + target_model_provider_id: "ollama", + target_prompt: "unauthenticated custom provider target turn", + expected_target_loaded_plugin_skills: &[API_CURATED_PLUGIN_SKILL], + expected_target_skill_description: "api description before", + }, ]; async fn skills_for_agent_turn( diff --git a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs index 6f15958f4d..c2b86c5864 100644 --- a/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs +++ b/codex-rs/ext/mcp/tests/hosted_apps_mcp.rs @@ -28,7 +28,7 @@ async fn contributes_hosted_plugin_runtime_without_an_executor() -> TestResult { .build() .await?; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let manager = installed_manager(&config); + let manager = installed_manager(&config, Some(auth.api_auth_mode())); let servers = manager.effective_servers(&config, Some(&auth)).await; let server = servers @@ -60,7 +60,7 @@ async fn runtime_overlay_preserves_disabled_server() -> TestResult { .build() .await?; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let manager = installed_manager(&config); + let manager = installed_manager(&config, Some(auth.api_auth_mode())); let servers = manager.effective_servers(&config, Some(&auth)).await; let server = servers @@ -87,7 +87,10 @@ async fn default_fallback_overwrites_reserved_config_without_an_extension() -> T .build() .await?; let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing(); - let manager = McpManager::new(Arc::new(plugins_manager_for_config(&config))); + let manager = McpManager::new(Arc::new(plugins_manager_for_config( + &config, + Some(auth.api_auth_mode()), + ))); let servers = manager.effective_servers(&config, Some(&auth)).await; let server = servers @@ -116,7 +119,10 @@ async fn later_extension_can_remove_same_name_registration() -> TestResult { codex_mcp_extension::install(&mut builder); builder.mcp_server_contributor(Arc::new(RemoveCodexApps)); let manager = McpManager::new_with_extensions( - Arc::new(plugins_manager_for_config(&config)), + Arc::new(plugins_manager_for_config( + &config, + Some(auth.api_auth_mode()), + )), Arc::new(builder.build()), codex_core::CodexAppsToolsCache::default(), ); @@ -137,7 +143,7 @@ async fn hosted_apps_mcp_requires_chatgpt_auth() -> TestResult { .build() .await?; let auth = CodexAuth::from_api_key("test"); - let manager = installed_manager(&config); + let manager = installed_manager(&config, Some(auth.api_auth_mode())); let servers = manager.effective_servers(&config, Some(&auth)).await; assert!(!servers.contains_key(CODEX_APPS_MCP_SERVER_NAME)); @@ -161,8 +167,10 @@ async fn disabled_apps_remove_reserved_server_config_for_all_hosts() -> TestResu .build() .await?; let managers = [ - installed_manager(&config), - McpManager::new(Arc::new(plugins_manager_for_config(&config))), + installed_manager(&config, /*auth_mode*/ None), + McpManager::new(Arc::new(plugins_manager_for_config( + &config, /*auth_mode*/ None, + ))), ]; for manager in managers { let servers = manager.runtime_servers(&config).await; @@ -171,11 +179,14 @@ async fn disabled_apps_remove_reserved_server_config_for_all_hosts() -> TestResu Ok(()) } -fn installed_manager(config: &Config) -> McpManager { +fn installed_manager( + config: &Config, + auth_mode: Option, +) -> McpManager { let mut builder = ExtensionRegistryBuilder::new(); codex_mcp_extension::install(&mut builder); McpManager::new_with_extensions( - Arc::new(plugins_manager_for_config(config)), + Arc::new(plugins_manager_for_config(config, auth_mode)), Arc::new(builder.build()), codex_core::CodexAppsToolsCache::default(), ) diff --git a/codex-rs/external-agent-migration/src/detect/mod.rs b/codex-rs/external-agent-migration/src/detect/mod.rs index 461e793eda..b7f6a7b479 100644 --- a/codex-rs/external-agent-migration/src/detect/mod.rs +++ b/codex-rs/external-agent-migration/src/detect/mod.rs @@ -351,7 +351,7 @@ impl ExternalAgentConfigService { .unwrap_or_default(); let configured_marketplace_plugins = configured_marketplace_plugins( &config, - &plugins_manager_for_config(&config), + &plugins_manager_for_config(&config, self.auth_mode), )?; let source_root = repo_root.unwrap_or(self.external_agent_home.as_path()); if let Some(detected) = diff --git a/codex-rs/external-agent-migration/src/plugins.rs b/codex-rs/external-agent-migration/src/plugins.rs index 46524e7482..003ea796dd 100644 --- a/codex-rs/external-agent-migration/src/plugins.rs +++ b/codex-rs/external-agent-migration/src/plugins.rs @@ -101,7 +101,7 @@ impl ExternalAgentConfigService { .map_err(|err| io::Error::other(format!("failed to load config: {err}")))?; let requirements = config.config_layer_stack.requirements().clone(); let mut outcome = PluginImportOutcome::default(); - let plugins_manager = plugins_manager_for_config(&config) + let plugins_manager = plugins_manager_for_config(&config, self.auth_mode) .with_plugin_install_source(PluginInstallSource::ExternalAgentMigration); if let Some(analytics_events_client) = self.analytics_events_client.clone() { plugins_manager.set_analytics_events_client(analytics_events_client); diff --git a/codex-rs/external-agent-migration/src/service.rs b/codex-rs/external-agent-migration/src/service.rs index e206a638a0..93b16ef137 100644 --- a/codex-rs/external-agent-migration/src/service.rs +++ b/codex-rs/external-agent-migration/src/service.rs @@ -43,6 +43,7 @@ use codex_analytics::AnalyticsEventsClient; use codex_core::config::Config; use codex_core_plugins::PluginsManager; use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy; +use codex_protocol::auth::AuthMode; use codex_protocol::protocol::Product; use codex_rollout::StateDbHandle; use serde_json::Value as JsonValue; @@ -68,6 +69,7 @@ pub struct ExternalAgentConfigService { pub(super) connector_metadata_roots: Vec, pub(crate) external_agent_home: PathBuf, pub(crate) analytics_events_client: Option, + pub(crate) auth_mode: Option, pub(crate) source: ExternalAgentSource, pub(crate) session_import_limits: ExternalAgentSessionImportLimits, state_db: Option, @@ -76,6 +78,7 @@ pub struct ExternalAgentConfigService { impl ExternalAgentConfigService { pub fn new( codex_home: PathBuf, + auth_mode: Option, analytics_events_client: AnalyticsEventsClient, state_db: Option, ) -> Self { @@ -87,6 +90,7 @@ impl ExternalAgentConfigService { connector_metadata_roots, external_agent_home, analytics_events_client: Some(analytics_events_client), + auth_mode, source, session_import_limits: ExternalAgentSessionImportLimits::default(), state_db, @@ -102,6 +106,7 @@ impl ExternalAgentConfigService { connector_metadata_roots, external_agent_home, analytics_events_client: self.analytics_events_client.clone(), + auth_mode: self.auth_mode, source, session_import_limits: self.session_import_limits, state_db: self.state_db.clone(), @@ -114,6 +119,12 @@ impl ExternalAgentConfigService { service } + pub fn with_auth_mode(&self, auth_mode: Option) -> Self { + let mut service = self.clone(); + service.auth_mode = auth_mode; + service + } + pub fn session_metadata_mode(&self) -> SessionMetadataMode { self.source.session_metadata_mode() } @@ -157,6 +168,7 @@ impl ExternalAgentConfigService { connector_metadata_roots, external_agent_home, analytics_events_client: None, + auth_mode: None, source, session_import_limits: ExternalAgentSessionImportLimits::default(), state_db: None, diff --git a/codex-rs/external-agent-migration/src/service_tests/plugins/basics.rs b/codex-rs/external-agent-migration/src/service_tests/plugins/basics.rs index 8625534e61..8a4656717f 100644 --- a/codex-rs/external-agent-migration/src/service_tests/plugins/basics.rs +++ b/codex-rs/external-agent-migration/src/service_tests/plugins/basics.rs @@ -1,8 +1,96 @@ use super::super::*; use crate::migration_source::MarketplaceImportSource; use crate::source_cla; +use codex_protocol::auth::AuthMode; use pretty_assertions::assert_eq; +#[tokio::test] +async fn authenticated_plugin_migration_uses_chatgpt_curated_marketplace() { + let (_root, external_agent_home, codex_home) = fixture_paths(); + let curated_root = codex_home.join(".tmp/plugins"); + let plugin_root = curated_root.join("plugins/sample"); + fs::create_dir_all(&external_agent_home).expect("create external agent home"); + fs::create_dir_all(curated_root.join(".agents/plugins")) + .expect("create curated marketplace directory"); + fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("create curated plugin directory"); + fs::write( + external_agent_home.join("settings.json"), + r#"{"enabledPlugins":{"sample@openai-curated":true}}"#, + ) + .expect("write external agent settings"); + fs::write( + codex_home.join("config.toml"), + "[features]\nplugins = true\n", + ) + .expect("write Codex config"); + fs::write( + codex_home.join(".tmp/plugins.sha"), + "0123456789abcdef0123456789abcdef01234567\n", + ) + .expect("write curated plugin version"); + fs::write( + curated_root.join(".agents/plugins/marketplace.json"), + r#"{ + "name": "openai-curated", + "plugins": [{ + "name": "sample", + "source": {"source": "local", "path": "./plugins/sample"} + }] +}"#, + ) + .expect("write curated marketplace"); + fs::write( + plugin_root.join(".codex-plugin/plugin.json"), + r#"{"name":"sample","version":"0.1.0"}"#, + ) + .expect("write curated plugin manifest"); + + let service = service_for_paths(external_agent_home.clone(), codex_home) + .with_auth_mode(Some(AuthMode::Chatgpt)); + let items = service + .detect(ExternalAgentConfigDetectOptions { + include_home: true, + include_memory: false, + cwds: None, + }) + .await + .expect("detect authenticated curated plugin"); + let details = Some(MigrationDetails { + plugins: vec![PluginsMigration { + marketplace_name: "openai-curated".to_string(), + plugin_names: vec!["sample".to_string()], + }], + ..Default::default() + }); + assert_eq!( + items, + vec![ExternalAgentConfigMigrationItem { + item_type: ExternalAgentConfigMigrationItemType::Plugins, + description: format!( + "Migrate enabled plugins from {}", + external_agent_home.join("settings.json").display() + ), + cwd: None, + details: details.clone(), + }] + ); + + let outcome = service + .import_plugins(/*cwd*/ None, details) + .await + .expect("import authenticated curated plugin"); + assert_eq!( + outcome, + PluginImportOutcome { + succeeded_marketplaces: vec!["openai-curated".to_string()], + succeeded_plugin_ids: vec!["sample@openai-curated".to_string()], + failed_marketplaces: Vec::new(), + failed_plugin_ids: Vec::new(), + raw_errors: Vec::new(), + } + ); +} + #[tokio::test] async fn detect_home_lists_enabled_plugins_from_settings() { let (_root, external_agent_home, codex_home) = fixture_paths();