Route curated plugins by authentication mode (#35671)

## Why

Curated plugin capabilities need to follow the active authentication mode, including after an account switch and when the configured model provider differs from the authentication source.

## What changed

- Select the ChatGPT, remote, or API curated marketplace from the current authentication mode, with an API marketplace fallback for ambient Amazon Bedrock credentials.
- Apply that selection consistently to plugin loading, hooks, skills, installed-plugin conflict filtering, marketplace listing, and `codex mcp` discovery.
- Start the local curated repository sync when an account change makes the remote catalog unavailable, and refresh existing thread MCP runtimes when the effective plugin cache changes.

## Testing

Added coverage for account switches, ChatGPT-authenticated Bedrock sessions, API-key MCP discovery, curated marketplace filtering, hook and skill routing, and existing-thread MCP refreshes.

GitOrigin-RevId: dbefdba3a3ea7281e7b6013e057a418770ccfc95
This commit is contained in:
felixxia-oai
2026-07-27 21:17:42 +00:00
committed by copyberry
parent fd41e813cb
commit 294d813263
12 changed files with 1258 additions and 88 deletions
+98 -27
View File
@@ -4,6 +4,7 @@ use crate::app_mcp_routing::apply_app_mcp_routing_policy;
use crate::installed_marketplaces::installed_marketplace_roots_from_layer_stack;
use crate::is_openai_curated_marketplace_name;
use crate::loader::PluginHookLoadOutcome;
use crate::loader::TargetCuratedMarketplace;
use crate::loader::configured_curated_plugin_ids_from_codex_home;
use crate::loader::curated_plugin_cache_version;
use crate::loader::load_plugin_apps_from_manifest;
@@ -15,6 +16,7 @@ use crate::loader::load_plugins_from_layer_stack;
use crate::loader::log_plugin_load_errors;
use crate::loader::materialize_marketplace_plugin_source;
use crate::loader::plugin_capability_summary_from_root;
use crate::loader::plugin_is_eligible_for_target_marketplace;
use crate::loader::refresh_curated_plugin_cache;
use crate::loader::refresh_non_curated_plugin_cache_detailed;
use crate::loader::refresh_non_curated_plugin_cache_force_reinstall_detailed;
@@ -484,6 +486,26 @@ 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,
}
}
impl PluginsManager {
pub fn new(codex_home: PathBuf) -> Self {
Self::new_with_options(codex_home, Some(Product::Codex), /*auth_mode*/ None)
@@ -562,6 +584,20 @@ impl PluginsManager {
config.remote_plugin_enabled && self.auth_mode().is_some_and(AuthMode::uses_codex_backend)
}
/// Starts the local curated marketplace sync when the remote catalog is unavailable.
pub fn maybe_start_curated_repo_sync_for_config(
self: &Arc<Self>,
config: &PluginsConfigInput,
on_effective_plugins_changed: Option<EffectivePluginsChangedCallback>,
) {
if config.plugins_enabled && !self.remote_global_catalog_active(config) {
self.start_curated_repo_sync(
config.http_client_factory.clone(),
on_effective_plugins_changed,
);
}
}
pub fn set_analytics_events_client(&self, analytics_events_client: AnalyticsEventsClient) {
let mut stored_client = match self.analytics_events_client.write() {
Ok(client_guard) => client_guard,
@@ -633,7 +669,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);
return self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id);
}
let Ok(_load_permit) = self.loaded_plugins_load_semaphore.acquire().await else {
@@ -641,7 +677,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);
return self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id);
}
let cache_generation = self.loaded_plugins_cache_generation();
let plugin_skill_snapshots = PluginSkillSnapshots::for_plugin_load();
@@ -662,11 +698,22 @@ impl PluginsManager {
plugins.clone(),
plugin_skill_snapshots,
);
self.resolve_loaded_plugins_for_auth(plugins)
self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id)
}
fn resolve_loaded_plugins_for_auth(&self, mut plugins: Vec<LoadedPlugin>) -> PluginLoadOutcome {
fn resolve_loaded_plugins_for_auth(
&self,
mut plugins: Vec<LoadedPlugin>,
model_provider_id: &str,
) -> PluginLoadOutcome {
let auth_mode = self.auth_mode();
let target_curated_marketplace = target_curated_marketplace(auth_mode, model_provider_id);
plugins.retain(|plugin| {
plugin_is_eligible_for_target_marketplace(
&plugin.config_name,
target_curated_marketplace,
)
});
for plugin in &mut plugins {
let plugin_active = plugin.is_active();
apply_app_mcp_routing_policy(
@@ -714,9 +761,13 @@ impl PluginsManager {
fn clear_caches_after_marketplace_source_refresh(
&self,
installed_plugin_cache_refreshed: bool,
on_effective_plugins_changed: Option<&EffectivePluginsChangedCallback>,
) {
if installed_plugin_cache_refreshed {
self.clear_cache();
if let Some(on_effective_plugins_changed) = on_effective_plugins_changed {
on_effective_plugins_changed(EffectivePluginsChange::default());
}
} else {
self.tool_suggest_metadata_cache.clear();
}
@@ -741,7 +792,7 @@ impl PluginsManager {
Arc::clone(&self.skill_root_scan_slots),
)
.await;
self.resolve_loaded_plugins_for_auth(plugins)
self.resolve_loaded_plugins_for_auth(plugins, &config.model_provider_id)
}
/// Resolve plugin hooks for a config layer stack without loading other plugin capabilities.
@@ -753,10 +804,13 @@ impl PluginsManager {
if !config.plugins_enabled {
return PluginHookLoadOutcome::default();
}
let target_curated_marketplace =
target_curated_marketplace(self.auth_mode(), &config.model_provider_id);
load_plugin_hooks_from_layer_stack(
config_layer_stack,
self.remote_installed_plugin_configs(),
&self.store,
target_curated_marketplace,
self.remote_global_catalog_active(config),
)
.await
@@ -2049,11 +2103,10 @@ impl PluginsManager {
on_effective_plugins_changed: Option<EffectivePluginsChangedCallback>,
) {
if config.plugins_enabled {
let use_remote_global_catalog =
config.remote_plugin_enabled && auth_manager.current_auth_uses_codex_backend();
if !use_remote_global_catalog {
self.start_curated_repo_sync(config.http_client_factory.clone());
}
self.maybe_start_curated_repo_sync_for_config(
config,
on_effective_plugins_changed.clone(),
);
let should_spawn_marketplace_auto_upgrade = {
let mut state = match self.configured_marketplace_upgrade_state.write() {
Ok(state) => state,
@@ -2189,6 +2242,7 @@ impl PluginsManager {
Ok(refresh_outcome) => {
self.clear_caches_after_marketplace_source_refresh(
refresh_outcome.cache_refreshed,
/*on_effective_plugins_changed*/ None,
);
outcome
.errors
@@ -2517,10 +2571,27 @@ impl PluginsManager {
}
}
fn start_curated_repo_sync(self: &Arc<Self>, http_client_factory: HttpClientFactory) {
fn start_curated_repo_sync(
self: &Arc<Self>,
http_client_factory: HttpClientFactory,
on_effective_plugins_changed: Option<EffectivePluginsChangedCallback>,
) {
if CURATED_REPO_SYNC_STARTED.swap(true, Ordering::SeqCst) {
return;
}
let on_effective_plugins_changed =
on_effective_plugins_changed.map(|on_effective_plugins_changed| {
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
return on_effective_plugins_changed;
};
let callback: EffectivePluginsChangedCallback = Arc::new(move |change| {
let on_effective_plugins_changed = Arc::clone(&on_effective_plugins_changed);
runtime.spawn(async move {
on_effective_plugins_changed(change);
});
});
callback
});
let manager = Arc::clone(self);
let codex_home = self.codex_home.clone();
if let Err(err) = std::thread::Builder::new()
@@ -2536,8 +2607,10 @@ impl PluginsManager {
&configured_curated_plugin_ids,
) {
Ok(cache_refreshed) => {
manager
.clear_caches_after_marketplace_source_refresh(cache_refreshed);
manager.clear_caches_after_marketplace_source_refresh(
cache_refreshed,
on_effective_plugins_changed.as_ref(),
);
}
Err(err) => {
manager.clear_cache();
@@ -2800,20 +2873,18 @@ impl PluginsManager {
self.codex_home.as_path(),
));
let curated_marketplace_path = if include_openai_curated {
if config.model_provider_id == AMAZON_BEDROCK_PROVIDER_ID
|| matches!(
self.auth_mode(),
Some(AuthMode::ApiKey | AuthMode::BedrockApiKey)
)
{
let api_marketplace_path =
curated_plugins_api_marketplace_path(self.codex_home.as_path());
api_marketplace_path
.is_file()
.then_some(api_marketplace_path)
} else {
let curated_repo_root = curated_plugins_repo_path(self.codex_home.as_path());
curated_repo_root.is_dir().then_some(curated_repo_root)
match target_curated_marketplace(self.auth_mode(), &config.model_provider_id) {
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)
}
TargetCuratedMarketplace::OpenAiApi => {
let api_marketplace_path =
curated_plugins_api_marketplace_path(self.codex_home.as_path());
api_marketplace_path
.is_file()
.then_some(api_marketplace_path)
}
}
} else {
None