mirror of
https://github.com/openai/codex.git
synced 2026-10-17 17:08:55 +00:00
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:
committed by
copyberry
parent
507ef0b371
commit
f898ebcafd
@@ -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(¶ms.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()),
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user