mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Load API curated marketplace by auth (#28383)
## Summary - choose the local OpenAI curated marketplace manifest based on auth: Codex backend auth gets the existing marketplace, direct provider auth gets `api_marketplace.json` - include Bedrock API key auth in the direct-provider API marketplace path - safely skip the API marketplace when `api_marketplace.json` is absent ## Validation - `just fmt` - `git diff --check origin/main...HEAD` - CI should run the full validation ## Manual Testing ### - New api marketplace not available for API key sign 1. Safely not display anything from api marketplace <img width="1161" height="289" alt="Screenshot 2026-06-15 at 21 37 43" src="https://github.com/user-attachments/assets/a5f16642-8a20-4ac1-a0de-1274a4c7b5b2" /> ### - New api marketplace for API key sign in 1. Setup api_marketplace.json ``` { "name": "openai-curated", "interface": { "displayName": "Codex official" }, "plugins": [ { "name": "linear", "source": { "source": "local", "path": "./plugins/linear" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, "category": "Productivity" } ] } ``` 2. Log in with API key, observe that only the defined plugin from api_marketplace.json is available from "Codex Official" (outside of local testing marketplaces) <img width="1167" height="446" alt="Screenshot 2026-06-15 at 21 16 53" src="https://github.com/user-attachments/assets/7cf61477-d826-4ef6-bc05-0a23ac1c0259" /> also checked functionality on codex app ### - SiWC users Still uses 'default' marketplace.json and renders all plugins <img width="1171" height="502" alt="Screenshot 2026-06-15 at 21 40 25" src="https://github.com/user-attachments/assets/d212ea9b-0aa5-470b-8ea4-450efe65bb2b" /> also checked functionality on codex app ## Notes - `just test -p codex-core-plugins` was started locally before splitting branches, but I stopped relying on local tests per follow-up and left final validation to PR CI.
This commit is contained in:
@@ -8,6 +8,7 @@ use codex_app_server_protocol::PluginShareTargetRole;
|
||||
use codex_config::types::McpServerConfig;
|
||||
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::PluginListBackgroundTaskOptions;
|
||||
use codex_core_plugins::is_openai_curated_marketplace_name;
|
||||
use codex_core_plugins::remote::REMOTE_CREATED_BY_ME_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use codex_core_plugins::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
||||
@@ -174,9 +175,9 @@ fn filter_openai_curated_installed_conflicts(
|
||||
) {
|
||||
let local_installed_plugin_names = marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME)
|
||||
.map(|marketplace| installed_plugin_names(&marketplace.plugins))
|
||||
.unwrap_or_default();
|
||||
.filter(|marketplace| is_openai_curated_marketplace_name(&marketplace.name))
|
||||
.flat_map(|marketplace| installed_plugin_names(&marketplace.plugins))
|
||||
.collect::<HashSet<_>>();
|
||||
let remote_installed_plugin_names = marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == REMOTE_GLOBAL_MARKETPLACE_NAME)
|
||||
@@ -190,13 +191,12 @@ fn filter_openai_curated_installed_conflicts(
|
||||
return;
|
||||
}
|
||||
|
||||
let marketplace_to_filter = if prefer_remote_curated_conflicts {
|
||||
OPENAI_CURATED_MARKETPLACE_NAME
|
||||
} else {
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME
|
||||
};
|
||||
for marketplace in marketplaces.iter_mut() {
|
||||
if marketplace.name != marketplace_to_filter {
|
||||
if prefer_remote_curated_conflicts {
|
||||
if !is_openai_curated_marketplace_name(&marketplace.name) {
|
||||
continue;
|
||||
}
|
||||
} else if marketplace.name != REMOTE_GLOBAL_MARKETPLACE_NAME {
|
||||
continue;
|
||||
}
|
||||
marketplace
|
||||
@@ -551,6 +551,8 @@ impl PluginRequestProcessor {
|
||||
{
|
||||
return Ok(empty_response());
|
||||
}
|
||||
let auth_mode = auth.as_ref().map(CodexAuth::api_auth_mode);
|
||||
plugins_manager.set_auth_mode(auth_mode);
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let include_shared_with_me =
|
||||
marketplace_kinds.contains(&PluginListMarketplaceKind::SharedWithMe);
|
||||
@@ -559,10 +561,12 @@ impl PluginRequestProcessor {
|
||||
&& config.features.enabled(Feature::RemotePlugin);
|
||||
let include_global_remote =
|
||||
!explicit_marketplace_kinds && config.features.enabled(Feature::RemotePlugin);
|
||||
let use_remote_global_catalog =
|
||||
include_global_remote && auth_mode.is_some_and(AuthMode::uses_codex_backend);
|
||||
let remote_plugin_service_config = RemotePluginServiceConfig {
|
||||
chatgpt_base_url: config.chatgpt_base_url.clone(),
|
||||
};
|
||||
let refresh_global_remote_catalog_cache = include_global_remote
|
||||
let refresh_global_remote_catalog_cache = use_remote_global_catalog
|
||||
&& codex_core_plugins::remote::has_cached_global_remote_plugin_catalog(
|
||||
config.codex_home.as_path(),
|
||||
&remote_plugin_service_config,
|
||||
@@ -578,7 +582,7 @@ impl PluginRequestProcessor {
|
||||
.list_marketplaces_for_config(
|
||||
&config_for_marketplace_listing,
|
||||
&roots_for_marketplace_listing,
|
||||
/*include_openai_curated*/ true,
|
||||
/*include_openai_curated*/ !use_remote_global_catalog,
|
||||
)?;
|
||||
Ok::<
|
||||
(
|
||||
@@ -649,16 +653,14 @@ impl PluginRequestProcessor {
|
||||
data.push(remote_marketplace_to_info(remote_marketplace));
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(RemotePluginCatalogError::UnsupportedAuthMode) => {}
|
||||
Err(err) if explicit_marketplace_kinds => {
|
||||
return Err(remote_plugin_catalog_error_to_jsonrpc(
|
||||
err,
|
||||
"list OpenAI Curated remote plugin catalog",
|
||||
));
|
||||
}
|
||||
Err(
|
||||
RemotePluginCatalogError::AuthRequired
|
||||
| RemotePluginCatalogError::UnsupportedAuthMode,
|
||||
) => {}
|
||||
Err(RemotePluginCatalogError::AuthRequired) => {}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
error = %err,
|
||||
@@ -669,7 +671,7 @@ impl PluginRequestProcessor {
|
||||
}
|
||||
|
||||
let mut remote_sources = Vec::new();
|
||||
if include_global_remote {
|
||||
if use_remote_global_catalog {
|
||||
remote_sources.push(RemoteMarketplaceSource::Global);
|
||||
}
|
||||
if include_created_by_me_remote {
|
||||
@@ -741,9 +743,10 @@ impl PluginRequestProcessor {
|
||||
);
|
||||
}
|
||||
|
||||
let featured_plugin_ids = if data
|
||||
.iter()
|
||||
.any(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME)
|
||||
let featured_plugin_ids = if !plugins_input.remote_plugin_enabled
|
||||
&& data
|
||||
.iter()
|
||||
.any(|marketplace| marketplace.name == OPENAI_CURATED_MARKETPLACE_NAME)
|
||||
{
|
||||
match plugins_manager
|
||||
.featured_plugin_ids_for_config(&plugins_input, auth.as_ref())
|
||||
@@ -799,6 +802,7 @@ impl PluginRequestProcessor {
|
||||
{
|
||||
return Ok(empty_response());
|
||||
}
|
||||
plugins_manager.set_auth_mode(auth.as_ref().map(CodexAuth::api_auth_mode));
|
||||
|
||||
let plugins_input = config.plugins_config_input();
|
||||
let remote_installed_plugin_visible_marketplaces =
|
||||
|
||||
@@ -21,6 +21,8 @@ use codex_app_server_protocol::PluginSummary;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use codex_core::config::set_project_trust_level;
|
||||
use codex_login::AuthKeyringBackendKind;
|
||||
use codex_login::login_with_api_key;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use flate2::Compression;
|
||||
@@ -2126,6 +2128,98 @@ async fn plugin_list_propagates_explicit_openai_curated_remote_collection_errors
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_skips_explicit_openai_curated_remote_collection_for_api_auth() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_plugins_enabled_config_with_base_url(
|
||||
codex_home.path(),
|
||||
&format!("{}/backend-api/", server.uri()),
|
||||
)?;
|
||||
login_with_api_key(
|
||||
codex_home.path(),
|
||||
"sk-test-key",
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
marketplace_kinds: Some(vec![PluginListMarketplaceKind::Vertical]),
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
assert!(response.marketplaces.is_empty());
|
||||
assert!(response.marketplace_load_errors.is_empty());
|
||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_includes_api_curated_marketplace_for_api_auth_when_remote_plugin_enabled()
|
||||
-> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let server = MockServer::start().await;
|
||||
write_remote_plugin_catalog_config(
|
||||
codex_home.path(),
|
||||
&format!("{}/backend-api/", server.uri()),
|
||||
)?;
|
||||
write_openai_api_curated_marketplace(codex_home.path(), &["api-plugin"])?;
|
||||
login_with_api_key(
|
||||
codex_home.path(),
|
||||
"sk-test-key",
|
||||
AuthCredentialsStoreMode::File,
|
||||
AuthKeyringBackendKind::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_plugin_list_request(PluginListParams {
|
||||
cwds: None,
|
||||
marketplace_kinds: None,
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
let api_curated_marketplace = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.find(|marketplace| marketplace.name == "openai-api-curated")
|
||||
.expect("expected API curated marketplace");
|
||||
assert_eq!(
|
||||
api_curated_marketplace
|
||||
.interface
|
||||
.as_ref()
|
||||
.and_then(|interface| interface.display_name.as_deref()),
|
||||
Some("OpenAI Curated")
|
||||
);
|
||||
assert_eq!(api_curated_marketplace.plugins.len(), 1);
|
||||
assert_eq!(
|
||||
api_curated_marketplace.plugins[0].id,
|
||||
"api-plugin@openai-api-curated"
|
||||
);
|
||||
assert!(response.marketplace_load_errors.is_empty());
|
||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_does_not_query_openai_curated_remote_collection_by_default() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
@@ -3928,6 +4022,35 @@ remote_plugin = true
|
||||
fn write_openai_curated_marketplace(
|
||||
codex_home: &std::path::Path,
|
||||
plugin_names: &[&str],
|
||||
) -> std::io::Result<()> {
|
||||
write_curated_marketplace(
|
||||
codex_home,
|
||||
"marketplace.json",
|
||||
"openai-curated",
|
||||
/*display_name*/ None,
|
||||
plugin_names,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_openai_api_curated_marketplace(
|
||||
codex_home: &std::path::Path,
|
||||
plugin_names: &[&str],
|
||||
) -> std::io::Result<()> {
|
||||
write_curated_marketplace(
|
||||
codex_home,
|
||||
"api_marketplace.json",
|
||||
"openai-api-curated",
|
||||
Some("OpenAI Curated"),
|
||||
plugin_names,
|
||||
)
|
||||
}
|
||||
|
||||
fn write_curated_marketplace(
|
||||
codex_home: &std::path::Path,
|
||||
manifest_name: &str,
|
||||
marketplace_name: &str,
|
||||
display_name: Option<&str>,
|
||||
plugin_names: &[&str],
|
||||
) -> std::io::Result<()> {
|
||||
let curated_root = codex_home.join(".tmp/plugins");
|
||||
std::fs::create_dir_all(curated_root.join(".git"))?;
|
||||
@@ -3947,11 +4070,21 @@ fn write_openai_curated_marketplace(
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(",\n");
|
||||
let interface = display_name
|
||||
.map(|display_name| {
|
||||
format!(
|
||||
r#"
|
||||
"interface": {{
|
||||
"displayName": "{display_name}"
|
||||
}},"#
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
std::fs::write(
|
||||
curated_root.join(".agents/plugins/marketplace.json"),
|
||||
curated_root.join(".agents/plugins").join(manifest_name),
|
||||
format!(
|
||||
r#"{{
|
||||
"name": "openai-curated",
|
||||
"name": "{marketplace_name}",{interface}
|
||||
"plugins": [
|
||||
{plugins}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user