[codex] Move plugin discoverable logic into core-plugins (#25783)

## Summary
- Move plugin discoverable recommendation filtering from `codex-core`
into `codex-core-plugins` behind `ToolSuggestPluginDiscoveryInput`.
- Keep `codex-core` as a thin adapter from `Config` to the core-plugins
API and back to `DiscoverablePluginInfo`.
- Keep the existing discoverable allowlist private to the core-plugins
implementation.

## Validation
- `just fmt`
- `just test -p codex-core list_tool_suggest_discoverable_plugins`
- `git diff --check`
- Read-only subagent review: no findings
This commit is contained in:
xl-openai
2026-06-01 23:25:37 -07:00
committed by GitHub
Unverified
parent f2b725102b
commit 07f04cc3c7
3 changed files with 259 additions and 197 deletions
+218
View File
@@ -0,0 +1,218 @@
use anyhow::Context;
use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_login::CodexAuth;
use codex_plugin::PluginCapabilitySummary;
use std::collections::HashSet;
use tracing::warn;
use crate::OPENAI_BUNDLED_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;
use crate::remote::RemotePluginScope;
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",
];
const TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST: &[&str] = &[
OPENAI_BUNDLED_MARKETPLACE_NAME,
OPENAI_CURATED_MARKETPLACE_NAME,
REMOTE_GLOBAL_MARKETPLACE_NAME,
];
#[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 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, &[])
.context("failed to list plugin marketplaces for tool suggestions")?
.marketplaces;
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 remote_installed_marketplaces = if input.plugins.remote_plugin_enabled {
self.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global])
} else {
None
};
let mut discoverable_plugins = Vec::<ToolSuggestDiscoverablePlugin>::new();
for marketplace in marketplaces {
let marketplace_name = marketplace.name;
if input.plugins.remote_plugin_enabled
&& marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME
{
continue;
}
let is_allowlisted_marketplace = TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST
.contains(&marketplace_name.as_str());
for plugin in marketplace.plugins {
let is_configured_plugin = input.configured_plugin_ids.contains(plugin.id.as_str());
let is_fallback_plugin =
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str());
if plugin.installed
|| plugin.policy.installation == MarketplacePluginInstallPolicy::NotAvailable
|| input.disabled_plugin_ids.contains(plugin.id.as_str())
|| (!is_allowlisted_marketplace && !is_configured_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();
let matches_installed_app =
plugin.app_connector_ids.iter().any(|connector_id| {
installed_app_connector_ids.contains(connector_id.0.as_str())
});
if !is_configured_plugin && !is_fallback_plugin && !matches_installed_app {
continue;
}
discoverable_plugins.push(ToolSuggestDiscoverablePlugin {
id: plugin.config_name,
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 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 =
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.config_id.as_str());
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,
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)
}
}
+3 -33
View File
@@ -1,3 +1,4 @@
mod discoverable;
pub mod installed_marketplaces;
pub mod loader;
mod manager;
@@ -20,42 +21,11 @@ pub mod toggles;
pub const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated";
pub const OPENAI_BUNDLED_MARKETPLACE_NAME: &str = "openai-bundled";
pub 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",
];
pub type LoadedPlugin = codex_plugin::LoadedPlugin<codex_config::McpServerConfig>;
pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome<codex_config::McpServerConfig>;
pub use discoverable::ToolSuggestDiscoverablePlugin;
pub use discoverable::ToolSuggestPluginDiscoveryInput;
pub use manager::ConfiguredMarketplace;
pub use manager::ConfiguredMarketplaceListOutcome;
pub use manager::ConfiguredMarketplacePlugin;
+38 -164
View File
@@ -1,28 +1,10 @@
use anyhow::Context;
use std::collections::HashSet;
use tracing::warn;
use super::PluginCapabilitySummary;
use crate::config::Config;
use codex_app_server_protocol::PluginAvailability;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_config::types::ToolSuggestDiscoverableType;
use codex_core_plugins::OPENAI_BUNDLED_MARKETPLACE_NAME;
use codex_core_plugins::OPENAI_CURATED_MARKETPLACE_NAME;
use codex_core_plugins::PluginsManager;
use codex_core_plugins::TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST;
use codex_core_plugins::marketplace::MarketplacePluginInstallPolicy;
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
use codex_core_plugins::remote::RemotePluginScope;
use codex_features::Feature;
use codex_core_plugins::ToolSuggestPluginDiscoveryInput;
use codex_login::CodexAuth;
use codex_tools::DiscoverablePluginInfo;
const TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST: &[&str] = &[
OPENAI_BUNDLED_MARKETPLACE_NAME,
OPENAI_CURATED_MARKETPLACE_NAME,
REMOTE_GLOBAL_MARKETPLACE_NAME,
];
use std::collections::HashSet;
pub(crate) async fn list_tool_suggest_discoverable_plugins(
config: &Config,
@@ -30,151 +12,43 @@ pub(crate) async fn list_tool_suggest_discoverable_plugins(
auth: Option<&CodexAuth>,
loaded_plugin_app_connector_ids: &[String],
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
if !config.features.enabled(Feature::Plugins) {
return Ok(Vec::new());
}
let plugins_input = config.plugins_config_input();
let configured_plugin_ids = config
.tool_suggest
.discoverables
.iter()
.filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Plugin)
.map(|discoverable| discoverable.id.as_str())
.collect::<HashSet<_>>();
let disabled_plugin_ids = config
.tool_suggest
.disabled_tools
.iter()
.filter(|disabled_tool| disabled_tool.kind == ToolSuggestDiscoverableType::Plugin)
.map(|disabled_tool| disabled_tool.id.as_str())
.collect::<HashSet<_>>();
let marketplaces = plugins_manager
.list_marketplaces_for_config(&plugins_input, &[])
.context("failed to list plugin marketplaces for tool suggestions")?
.marketplaces;
let mut installed_app_connector_ids = plugins_manager
.plugins_for_config(&plugins_input)
.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(loaded_plugin_app_connector_ids.iter().cloned());
let remote_installed_marketplaces = if plugins_input.remote_plugin_enabled {
plugins_manager
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global])
} else {
None
};
let mut discoverable_plugins = Vec::<DiscoverablePluginInfo>::new();
for marketplace in marketplaces {
let marketplace_name = marketplace.name;
if plugins_input.remote_plugin_enabled
&& marketplace_name == OPENAI_CURATED_MARKETPLACE_NAME
{
continue;
}
let is_allowlisted_marketplace =
TOOL_SUGGEST_DISCOVERABLE_MARKETPLACE_ALLOWLIST.contains(&marketplace_name.as_str());
for plugin in marketplace.plugins {
let is_configured_plugin = configured_plugin_ids.contains(plugin.id.as_str());
let is_fallback_plugin =
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.id.as_str());
if plugin.installed
|| plugin.policy.installation == MarketplacePluginInstallPolicy::NotAvailable
|| disabled_plugin_ids.contains(plugin.id.as_str())
|| (!is_allowlisted_marketplace && !is_configured_plugin)
{
continue;
}
let plugin_id = plugin.id.clone();
match plugins_manager
.read_plugin_detail_for_marketplace_plugin(
&plugins_input,
&marketplace_name,
plugin,
)
.await
{
Ok(plugin) => {
let plugin: PluginCapabilitySummary = plugin.into();
let matches_installed_app =
plugin.app_connector_ids.iter().any(|connector_id| {
installed_app_connector_ids.contains(connector_id.0.as_str())
});
if !is_configured_plugin && !is_fallback_plugin && !matches_installed_app {
continue;
}
discoverable_plugins.push(DiscoverablePluginInfo {
id: plugin.config_name,
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 installed_remote_plugin_ids = remote_installed_marketplaces
let input = ToolSuggestPluginDiscoveryInput {
plugins: config.plugins_config_input(),
configured_plugin_ids: config
.tool_suggest
.discoverables
.iter()
.flat_map(|marketplace| marketplace.plugins.iter())
.map(|plugin| plugin.remote_plugin_id.clone())
.collect::<HashSet<_>>();
for plugin in plugins_manager
.cached_global_remote_discoverable_plugins_for_config(&plugins_input, auth)
{
let is_configured_plugin = configured_plugin_ids.contains(plugin.config_id.as_str())
|| configured_plugin_ids.contains(plugin.remote_plugin_id.as_str());
let is_fallback_plugin =
TOOL_SUGGEST_DISCOVERABLE_PLUGIN_ALLOWLIST.contains(&plugin.config_id.as_str());
let matches_installed_app = plugin
.app_ids
.iter()
.any(|app_id| installed_app_connector_ids.contains(app_id.as_str()));
let is_disabled = disabled_plugin_ids.contains(plugin.config_id.as_str())
|| 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(DiscoverablePluginInfo {
id: plugin.config_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)
.filter(|discoverable| discoverable.kind == ToolSuggestDiscoverableType::Plugin)
.map(|discoverable| discoverable.id.clone())
.collect::<HashSet<_>>(),
disabled_plugin_ids: config
.tool_suggest
.disabled_tools
.iter()
.filter(|disabled_tool| disabled_tool.kind == ToolSuggestDiscoverableType::Plugin)
.map(|disabled_tool| disabled_tool.id.clone())
.collect::<HashSet<_>>(),
loaded_plugin_app_connector_ids: loaded_plugin_app_connector_ids
.iter()
.cloned()
.collect::<HashSet<_>>(),
};
plugins_manager
.list_tool_suggest_discoverable_plugins(&input, auth)
.await
.map(|plugins| {
plugins
.into_iter()
.map(|plugin| DiscoverablePluginInfo {
id: plugin.id,
name: plugin.name,
description: plugin.description,
has_skills: plugin.has_skills,
mcp_server_names: plugin.mcp_server_names,
app_connector_ids: plugin.app_connector_ids,
})
.collect()
})
}
#[cfg(test)]