mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
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:
@@ -76,6 +76,14 @@ pub struct PluginHookLoadOutcome {
|
||||
pub hook_load_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// The built-in curated marketplace selection for the current runtime.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TargetCuratedMarketplace {
|
||||
OpenAi,
|
||||
OpenAiWithRemote,
|
||||
OpenAiApi,
|
||||
}
|
||||
|
||||
enum PluginLoadScope<'a> {
|
||||
AllCapabilities {
|
||||
restriction_product: Option<Product>,
|
||||
@@ -192,9 +200,10 @@ pub async fn load_plugin_hooks_from_layer_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
extra_plugins: HashMap<String, PluginConfig>,
|
||||
store: &PluginStore,
|
||||
target_curated_marketplace: TargetCuratedMarketplace,
|
||||
remote_global_catalog_active: bool,
|
||||
) -> PluginHookLoadOutcome {
|
||||
let plugins = load_plugins_from_layer_stack_with_scope(
|
||||
let mut plugins = load_plugins_from_layer_stack_with_scope(
|
||||
config_layer_stack,
|
||||
extra_plugins,
|
||||
store,
|
||||
@@ -202,6 +211,9 @@ pub async fn load_plugin_hooks_from_layer_stack(
|
||||
PluginLoadScope::HooksOnly,
|
||||
)
|
||||
.await;
|
||||
plugins.retain(|plugin| {
|
||||
plugin_is_eligible_for_target_marketplace(&plugin.config_name, target_curated_marketplace)
|
||||
});
|
||||
PluginHookLoadOutcome {
|
||||
hook_sources: plugins
|
||||
.iter()
|
||||
@@ -236,7 +248,7 @@ fn merge_configured_plugins_with_remote_installed(
|
||||
let Ok(plugin_id) = PluginId::parse(plugin_key) else {
|
||||
continue;
|
||||
};
|
||||
if !is_openai_curated_marketplace_name(&plugin_id.marketplace_name)
|
||||
if plugin_id.marketplace_name != crate::OPENAI_CURATED_MARKETPLACE_NAME
|
||||
|| store.active_plugin_version(&plugin_id).is_none()
|
||||
{
|
||||
continue;
|
||||
@@ -267,6 +279,28 @@ fn merge_configured_plugins_with_remote_installed(
|
||||
configured_plugins
|
||||
}
|
||||
|
||||
pub(crate) fn plugin_is_eligible_for_target_marketplace(
|
||||
plugin_key: &str,
|
||||
target_curated_marketplace: TargetCuratedMarketplace,
|
||||
) -> bool {
|
||||
let Ok(plugin_id) = PluginId::parse(plugin_key) else {
|
||||
return true;
|
||||
};
|
||||
match target_curated_marketplace {
|
||||
TargetCuratedMarketplace::OpenAi => {
|
||||
plugin_id.marketplace_name != crate::OPENAI_API_CURATED_MARKETPLACE_NAME
|
||||
&& plugin_id.marketplace_name != REMOTE_GLOBAL_MARKETPLACE_NAME
|
||||
}
|
||||
TargetCuratedMarketplace::OpenAiWithRemote => {
|
||||
plugin_id.marketplace_name != crate::OPENAI_API_CURATED_MARKETPLACE_NAME
|
||||
}
|
||||
TargetCuratedMarketplace::OpenAiApi => {
|
||||
plugin_id.marketplace_name != crate::OPENAI_CURATED_MARKETPLACE_NAME
|
||||
&& plugin_id.marketplace_name != REMOTE_GLOBAL_MARKETPLACE_NAME
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn installed_plugin_name_for_marketplace(
|
||||
plugin_key: &str,
|
||||
marketplace_name: &str,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -138,6 +138,54 @@ fn plugins_manager_tracks_auth_mode() {
|
||||
assert_eq!(manager_with_auth.auth_mode(), Some(AuthMode::Chatgpt));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curated_repo_sync_stays_deferred_for_remote_chatgpt_catalog() {
|
||||
CURATED_REPO_SYNC_STARTED.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = PluginsConfigInput::new(
|
||||
unrestricted_config_layer_stack(),
|
||||
"openai".to_string(),
|
||||
/*plugins_enabled*/ true,
|
||||
/*remote_plugin_enabled*/ true,
|
||||
"https://chatgpt.com".to_string(),
|
||||
test_http_client_factory(),
|
||||
);
|
||||
let manager = Arc::new(PluginsManager::new_with_options(
|
||||
tmp.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
));
|
||||
|
||||
manager.maybe_start_curated_repo_sync_for_config(
|
||||
&config, /*on_effective_plugins_changed*/ None,
|
||||
);
|
||||
|
||||
assert!(!CURATED_REPO_SYNC_STARTED.load(std::sync::atomic::Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn marketplace_source_refresh_notifies_only_after_installed_cache_changes() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let callback_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
let callback_count_for_callback = Arc::clone(&callback_count);
|
||||
let callback: EffectivePluginsChangedCallback = Arc::new(move |_change| {
|
||||
callback_count_for_callback.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
});
|
||||
|
||||
manager.clear_caches_after_marketplace_source_refresh(
|
||||
/*installed_plugin_cache_refreshed*/ false,
|
||||
Some(&callback),
|
||||
);
|
||||
assert_eq!(callback_count.load(std::sync::atomic::Ordering::Relaxed), 0);
|
||||
|
||||
manager.clear_caches_after_marketplace_source_refresh(
|
||||
/*installed_plugin_cache_refreshed*/ true,
|
||||
Some(&callback),
|
||||
);
|
||||
assert_eq!(callback_count.load(std::sync::atomic::Ordering::Relaxed), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn marketplace_policy_projection_disables_installed_plugin_and_invalidates_cache() {
|
||||
let codex_home = TempDir::new().expect("create Codex home");
|
||||
@@ -805,6 +853,16 @@ fn write_cached_plugin(codex_home: &Path, marketplace_name: &str, plugin_name: &
|
||||
);
|
||||
}
|
||||
|
||||
async fn loaded_plugin_names(manager: &PluginsManager, config: &PluginsConfigInput) -> Vec<String> {
|
||||
manager
|
||||
.plugins_for_config(config)
|
||||
.await
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|plugin| plugin.config_name.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_plugins_loads_default_skills_and_mcp_servers() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -1286,27 +1344,30 @@ enabled = true
|
||||
|
||||
[plugins."calendar@openai-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."linear@openai-api-curated"]
|
||||
enabled = true
|
||||
"#,
|
||||
);
|
||||
write_cached_plugin(codex_home.path(), "openai-curated", "linear");
|
||||
write_cached_plugin(codex_home.path(), "openai-curated", "calendar");
|
||||
write_cached_plugin(codex_home.path(), "openai-api-curated", "linear");
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only");
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
);
|
||||
manager.write_remote_installed_plugins_cache(vec![
|
||||
remote_installed_plugin("linear"),
|
||||
remote_installed_plugin("remote-only"),
|
||||
]);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
assert_eq!(
|
||||
outcome
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|plugin| plugin.config_name.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
loaded_plugin_names(&manager, &config).await,
|
||||
vec![
|
||||
"calendar@openai-curated".to_string(),
|
||||
"linear@openai-curated".to_string(),
|
||||
@@ -1315,6 +1376,36 @@ enabled = true
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn api_curated_plugin_does_not_suppress_remote_curated_conflict_for_chatgpt() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_file(
|
||||
&codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = false
|
||||
|
||||
[plugins."linear@openai-api-curated"]
|
||||
enabled = true
|
||||
"#,
|
||||
);
|
||||
write_cached_plugin(codex_home.path(), "openai-api-curated", "linear");
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "linear");
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
);
|
||||
manager.write_remote_installed_plugins_cache(vec![remote_installed_plugin("linear")]);
|
||||
|
||||
assert_eq!(
|
||||
loaded_plugin_names(&manager, &config).await,
|
||||
vec!["linear@openai-curated-remote".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_global_catalog_ignores_local_curated_plugins() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
@@ -1350,15 +1441,9 @@ enabled = true
|
||||
remote_installed_plugin("remote-only"),
|
||||
]);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
assert_eq!(
|
||||
outcome
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|plugin| plugin.config_name.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
loaded_plugin_names(&manager, &config).await,
|
||||
vec![
|
||||
"linear@openai-api-curated".to_string(),
|
||||
"linear@openai-curated-remote".to_string(),
|
||||
"remote-only@openai-curated-remote".to_string(),
|
||||
]
|
||||
@@ -1366,7 +1451,7 @@ enabled = true
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_plugin_feature_keeps_local_curated_without_codex_backend() {
|
||||
async fn non_chatgpt_auth_rejects_cached_remote_curated_plugins_after_auth_switch() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_file(
|
||||
&codex_home.path().join(CONFIG_TOML_FILE),
|
||||
@@ -1382,26 +1467,46 @@ enabled = true
|
||||
);
|
||||
write_cached_plugin(codex_home.path(), "openai-curated", "linear");
|
||||
write_cached_plugin(codex_home.path(), "openai-api-curated", "linear");
|
||||
write_cached_plugin(codex_home.path(), "openai-curated-remote", "remote-only");
|
||||
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let mut config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::ApiKey),
|
||||
Some(AuthMode::Chatgpt),
|
||||
);
|
||||
manager.write_remote_installed_plugins_cache(vec![remote_installed_plugin("remote-only")]);
|
||||
assert_eq!(
|
||||
loaded_plugin_names(&manager, &config).await,
|
||||
vec!["remote-only@openai-curated-remote".to_string()]
|
||||
);
|
||||
|
||||
let outcome = manager.plugins_for_config(&config).await;
|
||||
|
||||
manager.set_auth_mode(/*auth_mode*/ None);
|
||||
assert_eq!(
|
||||
outcome
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|plugin| plugin.config_name.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"linear@openai-api-curated".to_string(),
|
||||
"linear@openai-curated".to_string(),
|
||||
]
|
||||
loaded_plugin_names(&manager, &config).await,
|
||||
vec!["linear@openai-curated".to_string()]
|
||||
);
|
||||
|
||||
for auth_mode in [AuthMode::ApiKey, AuthMode::BedrockApiKey] {
|
||||
manager.set_auth_mode(Some(auth_mode));
|
||||
|
||||
assert_eq!(
|
||||
loaded_plugin_names(&manager, &config).await,
|
||||
vec!["linear@openai-api-curated".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
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()]
|
||||
);
|
||||
|
||||
manager.set_auth_mode(Some(AuthMode::Chatgpt));
|
||||
assert_eq!(
|
||||
loaded_plugin_names(&manager, &config).await,
|
||||
vec!["remote-only@openai-curated-remote".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2715,7 +2820,11 @@ enabled = true
|
||||
.write_remote_plugin_id(&plugin_id, "plugins~Plugin_persisted")
|
||||
.expect("persist remote plugin id");
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new(codex_home.path().to_path_buf());
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
);
|
||||
if let Some(installed_plugins) = installed_plugins {
|
||||
manager.write_remote_installed_plugins_cache(installed_plugins);
|
||||
}
|
||||
@@ -4527,6 +4636,42 @@ plugins = true
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_marketplaces_uses_chatgpt_curated_manifest_for_bedrock_with_chatgpt_auth() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let curated_root = curated_plugins_repo_path(tmp.path());
|
||||
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"model_provider = "amazon-bedrock"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
"#,
|
||||
);
|
||||
write_openai_curated_marketplace(&curated_root, &["chatgpt-plugin"]);
|
||||
write_openai_api_curated_marketplace(&curated_root, &["api-plugin"]);
|
||||
|
||||
let config = load_config(tmp.path(), tmp.path()).await;
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
manager.set_auth_mode(Some(AuthMode::Chatgpt));
|
||||
let marketplaces = manager
|
||||
.list_marketplaces_for_config(&config, &[], /*include_openai_curated*/ true)
|
||||
.unwrap()
|
||||
.marketplaces;
|
||||
|
||||
assert!(
|
||||
marketplaces
|
||||
.iter()
|
||||
.any(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME)
|
||||
);
|
||||
assert!(
|
||||
marketplaces
|
||||
.iter()
|
||||
.all(|marketplace| marketplace.name != OPENAI_API_CURATED_MARKETPLACE_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_marketplaces_skips_missing_api_curated_manifest() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
@@ -6158,6 +6303,97 @@ async fn plugin_hooks_for_layer_stack_loads_configured_plugin_hooks() {
|
||||
assert_eq!(outcome.hook_load_warnings, Vec::<String>::new());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_hooks_for_layer_stack_follow_auth_mode_and_provider() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
write_file(
|
||||
&codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"model_provider = "amazon-bedrock"
|
||||
|
||||
[features]
|
||||
plugins = true
|
||||
remote_plugin = false
|
||||
|
||||
[plugins."linear@openai-curated"]
|
||||
enabled = true
|
||||
|
||||
[plugins."linear@openai-api-curated"]
|
||||
enabled = true
|
||||
"#,
|
||||
);
|
||||
for marketplace_name in [
|
||||
OPENAI_CURATED_MARKETPLACE_NAME,
|
||||
OPENAI_API_CURATED_MARKETPLACE_NAME,
|
||||
] {
|
||||
write_cached_plugin(codex_home.path(), marketplace_name, "linear");
|
||||
write_file(
|
||||
&codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join(marketplace_name)
|
||||
.join("linear/local/hooks/hooks.json"),
|
||||
r#"{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "echo startup"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
}
|
||||
let config = load_config(codex_home.path(), codex_home.path()).await;
|
||||
let manager = PluginsManager::new_with_options(
|
||||
codex_home.path().to_path_buf(),
|
||||
Some(Product::Codex),
|
||||
Some(AuthMode::Chatgpt),
|
||||
);
|
||||
|
||||
let chatgpt_hooks = manager
|
||||
.plugin_hooks_for_layer_stack(&config.config_layer_stack, &config)
|
||||
.await;
|
||||
assert_eq!(
|
||||
chatgpt_hooks
|
||||
.hook_sources
|
||||
.iter()
|
||||
.map(|source| source.plugin_id.as_key())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["linear@openai-curated"]
|
||||
);
|
||||
|
||||
assert!(manager.set_auth_mode(Some(AuthMode::ApiKey)));
|
||||
let api_hooks = manager
|
||||
.plugin_hooks_for_layer_stack(&config.config_layer_stack, &config)
|
||||
.await;
|
||||
assert_eq!(
|
||||
api_hooks
|
||||
.hook_sources
|
||||
.iter()
|
||||
.map(|source| source.plugin_id.as_key())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["linear@openai-api-curated"]
|
||||
);
|
||||
|
||||
assert!(manager.set_auth_mode(/*auth_mode*/ None));
|
||||
let bedrock_hooks = manager
|
||||
.plugin_hooks_for_layer_stack(&config.config_layer_stack, &config)
|
||||
.await;
|
||||
assert_eq!(
|
||||
bedrock_hooks
|
||||
.hook_sources
|
||||
.iter()
|
||||
.map(|source| source.plugin_id.as_key())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["linear@openai-api-curated"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_installed_plugins_cache_refresh_coalesces_materializations() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user