Files
codex/codex-rs/core-plugins/src/discoverable.rs
T
felixxia-oai 02dce8eb8d [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.
2026-06-16 01:16:11 +00:00

225 lines
8.6 KiB
Rust

use anyhow::Context;
use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_login::CodexAuth;
use codex_plugin::PluginCapabilitySummary;
use codex_plugin::PluginId;
use std::collections::HashSet;
use tracing::warn;
use crate::OPENAI_API_CURATED_MARKETPLACE_NAME;
use crate::OPENAI_CURATED_MARKETPLACE_NAME;
use crate::PluginsConfigInput;
use crate::PluginsManager;
use crate::marketplace::MarketplacePluginInstallPolicy;
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
const TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST: &[&str] = &[
"github@openai-curated",
"notion@openai-curated",
"slack@openai-curated",
"gmail@openai-curated",
"google-calendar@openai-curated",
"google-drive@openai-curated",
"openai-developers@openai-curated",
"canva@openai-curated",
"teams@openai-curated",
"sharepoint@openai-curated",
"outlook-email@openai-curated",
"outlook-calendar@openai-curated",
"linear@openai-curated",
"figma@openai-curated",
"github@openai-curated-remote",
"notion@openai-curated-remote",
"slack@openai-curated-remote",
"gmail@openai-curated-remote",
"google-calendar@openai-curated-remote",
"google-drive@openai-curated-remote",
"openai-developers@openai-curated-remote",
"canva@openai-curated-remote",
"teams@openai-curated-remote",
"sharepoint@openai-curated-remote",
"outlook-email@openai-curated-remote",
"outlook-calendar@openai-curated-remote",
"linear@openai-curated-remote",
"figma@openai-curated-remote",
"chrome@openai-bundled",
"computer-use@openai-bundled",
];
#[derive(Debug, Clone)]
pub struct ToolSuggestPluginDiscoveryInput {
pub plugins: PluginsConfigInput,
pub configured_plugin_ids: HashSet<String>,
pub disabled_plugin_ids: HashSet<String>,
pub loaded_plugin_app_connector_ids: HashSet<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolSuggestDiscoverablePlugin {
pub id: String,
pub remote_plugin_id: Option<String>,
pub name: String,
pub description: Option<String>,
pub has_skills: bool,
pub mcp_server_names: Vec<String>,
pub app_connector_ids: Vec<String>,
}
impl PluginsManager {
pub async fn list_tool_suggest_discoverable_plugins(
&self,
input: &ToolSuggestPluginDiscoveryInput,
auth: Option<&CodexAuth>,
) -> anyhow::Result<Vec<ToolSuggestDiscoverablePlugin>> {
if !input.plugins.plugins_enabled {
return Ok(Vec::new());
}
let marketplaces = self
.list_marketplaces_for_config(&input.plugins, &[], /*include_openai_curated*/ true)
.context("failed to list plugin marketplaces for tool suggestions")?
.marketplaces;
let remote_installed_marketplaces = if input.plugins.remote_plugin_enabled {
self.build_remote_installed_plugin_marketplaces_from_cache(&[
REMOTE_GLOBAL_MARKETPLACE_NAME,
])
} else {
None
};
let mut discoverable_plugins = Vec::<ToolSuggestDiscoverablePlugin>::new();
for marketplace in marketplaces {
let marketplace_name = marketplace.name;
for plugin in marketplace.plugins {
let is_configured_plugin = input.configured_plugin_ids.contains(plugin.id.as_str());
let is_fallback_plugin = is_tool_suggest_fallback_plugin(&plugin.id);
if plugin.installed
|| plugin.policy.installation == MarketplacePluginInstallPolicy::NotAvailable
|| input.disabled_plugin_ids.contains(plugin.id.as_str())
|| (!is_configured_plugin && !is_fallback_plugin)
{
continue;
}
let plugin_id = plugin.id.clone();
match self
.read_plugin_detail_for_marketplace_plugin(
&input.plugins,
&marketplace_name,
plugin,
)
.await
{
Ok(plugin) => {
let plugin: PluginCapabilitySummary = plugin.into();
discoverable_plugins.push(ToolSuggestDiscoverablePlugin {
id: plugin.config_name,
remote_plugin_id: None,
name: plugin.display_name,
description: plugin.description,
has_skills: plugin.has_skills,
mcp_server_names: plugin.mcp_server_names,
app_connector_ids: plugin
.app_connector_ids
.into_iter()
.map(|connector_id| connector_id.0)
.collect(),
});
}
Err(err) => {
warn!("failed to load discoverable plugin suggestion {plugin_id}: {err:#}")
}
}
}
}
if let Some(remote_installed_marketplaces) = remote_installed_marketplaces.as_ref() {
let mut installed_app_connector_ids = self
.plugins_for_config(&input.plugins)
.await
.capability_summaries()
.iter()
.flat_map(|plugin| plugin.app_connector_ids.iter())
.map(|connector_id| connector_id.0.clone())
.collect::<HashSet<_>>();
installed_app_connector_ids
.extend(input.loaded_plugin_app_connector_ids.iter().cloned());
let installed_remote_plugin_ids = remote_installed_marketplaces
.iter()
.flat_map(|marketplace| marketplace.plugins.iter())
.map(|plugin| plugin.remote_plugin_id.clone())
.collect::<HashSet<_>>();
for plugin in
self.cached_global_remote_discoverable_plugins_for_config(&input.plugins, auth)
{
let is_configured_plugin = input
.configured_plugin_ids
.contains(plugin.config_id.as_str())
|| input
.configured_plugin_ids
.contains(plugin.remote_plugin_id.as_str());
let is_fallback_plugin = is_tool_suggest_fallback_plugin(&plugin.config_id);
let matches_installed_app = plugin
.app_ids
.iter()
.any(|app_id| installed_app_connector_ids.contains(app_id.as_str()));
let is_disabled = input
.disabled_plugin_ids
.contains(plugin.config_id.as_str())
|| input
.disabled_plugin_ids
.contains(plugin.remote_plugin_id.as_str());
if installed_remote_plugin_ids.contains(&plugin.remote_plugin_id)
|| plugin.install_policy == PluginInstallPolicy::NotAvailable
|| plugin.availability == PluginAvailability::DisabledByAdmin
|| is_disabled
|| (!is_configured_plugin && !is_fallback_plugin && !matches_installed_app)
{
continue;
}
discoverable_plugins.push(ToolSuggestDiscoverablePlugin {
id: plugin.config_id,
remote_plugin_id: Some(plugin.remote_plugin_id),
name: plugin.name,
description: plugin.description,
has_skills: plugin.has_skills,
mcp_server_names: Vec::new(),
app_connector_ids: plugin.app_ids,
});
}
}
discoverable_plugins.sort_by(|left, right| {
left.name
.cmp(&right.name)
.then_with(|| left.id.cmp(&right.id))
});
Ok(discoverable_plugins)
}
}
fn is_tool_suggest_fallback_plugin(plugin_id: &str) -> bool {
if TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin_id) {
return true;
}
let Ok(plugin_id) = PluginId::parse(plugin_id) else {
return false;
};
if plugin_id.marketplace_name != OPENAI_API_CURATED_MARKETPLACE_NAME {
return false;
}
let default_curated_plugin_id = format!(
"{}@{}",
plugin_id.plugin_name, OPENAI_CURATED_MARKETPLACE_NAME
);
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&default_curated_plugin_id.as_str())
}
#[cfg(test)]
#[path = "discoverable_tests.rs"]
mod tests;