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
2026-08-13 20:20:01 +00:00
committed by copyberry
parent 507ef0b371
commit f898ebcafd
28 changed files with 348 additions and 139 deletions
@@ -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<ExternalAgentConfigDetectResponse, JSONRPCErrorError> {
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(&params.migration_items);
let has_migration_items = !params.migration_items.is_empty();
@@ -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()),
+2 -3
View File
@@ -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())
+4 -2
View File
@@ -524,8 +524,10 @@ async fn run_remove(config_overrides: &CliConfigOverrides, remove_args: RemoveAr
}
async fn load_mcp_manager(config: &Config) -> Result<McpManager> {
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))
}
+1 -2
View File
@@ -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,
@@ -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<_>>(),
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<Vec<u8>> =
Box::leak(Box::new(std::sync::Mutex::new(Vec::new())));
let subscriber = tracing_subscriber::fmt()
+14 -31
View File
@@ -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<AuthMode>,
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<AuthMode>) -> 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<AuthMode>,
skill_root_loader: Arc<dyn SkillRootLoader<PluginSkillRoot>>,
) -> 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<LoadedPlugin>,
model_provider_id: &str,
) -> PluginLoadOutcome {
fn resolve_loaded_plugins_for_auth(&self, mut plugins: Vec<LoadedPlugin>) -> 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)
+52 -19
View File
@@ -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<_>>(),
vec![expected_marketplace],
"unexpected curated catalog for provider `{resolved_provider}` with auth {auth_mode:?}"
);
}
}
+5 -1
View File
@@ -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(
+1 -1
View File
@@ -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();
+9 -9
View File
@@ -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!(
+7 -1
View File
@@ -189,7 +189,13 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
force_refetch: bool,
environment_manager: Arc<EnvironmentManager>,
) -> anyhow::Result<AccessibleConnectorsStatus> {
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,
+2 -2
View File
@@ -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,
@@ -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<Vec<DiscoverablePluginInfo>> {
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(
+7 -2
View File
@@ -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<AuthMode>) -> 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,
)
}
+4 -4
View File
@@ -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::<Vec<_>>()
.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}
]
+24 -5
View File
@@ -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();
@@ -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,
@@ -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,
));
@@ -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,
+24
View File
@@ -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(
+20 -9
View File
@@ -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<codex_protocol::auth::AuthMode>,
) -> 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(),
)
@@ -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) =
@@ -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);
@@ -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<PathBuf>,
pub(crate) external_agent_home: PathBuf,
pub(crate) analytics_events_client: Option<AnalyticsEventsClient>,
pub(crate) auth_mode: Option<AuthMode>,
pub(crate) source: ExternalAgentSource,
pub(crate) session_import_limits: ExternalAgentSessionImportLimits,
state_db: Option<StateDbHandle>,
@@ -76,6 +78,7 @@ pub struct ExternalAgentConfigService {
impl ExternalAgentConfigService {
pub fn new(
codex_home: PathBuf,
auth_mode: Option<AuthMode>,
analytics_events_client: AnalyticsEventsClient,
state_db: Option<StateDbHandle>,
) -> 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<AuthMode>) -> 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,
@@ -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();