From 0f832b68676103eedee4e9eb4c66e6683ac3cde3 Mon Sep 17 00:00:00 2001 From: Xin Lin Date: Mon, 1 Jun 2026 22:50:43 -0700 Subject: [PATCH] Move plugin discoverable logic into core-plugins --- codex-rs/core-plugins/src/discoverable.rs | 218 ++++++++++++++++++++++ codex-rs/core-plugins/src/lib.rs | 36 +--- codex-rs/core/src/plugins/discoverable.rs | 202 ++++---------------- 3 files changed, 259 insertions(+), 197 deletions(-) create mode 100644 codex-rs/core-plugins/src/discoverable.rs diff --git a/codex-rs/core-plugins/src/discoverable.rs b/codex-rs/core-plugins/src/discoverable.rs new file mode 100644 index 000000000000..ccf093d8166f --- /dev/null +++ b/codex-rs/core-plugins/src/discoverable.rs @@ -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, + pub disabled_plugin_ids: HashSet, + pub loaded_plugin_app_connector_ids: HashSet, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ToolSuggestDiscoverablePlugin { + pub id: String, + pub name: String, + pub description: Option, + pub has_skills: bool, + pub mcp_server_names: Vec, + pub app_connector_ids: Vec, +} + +impl PluginsManager { + pub async fn list_tool_suggest_discoverable_plugins( + &self, + input: &ToolSuggestPluginDiscoveryInput, + auth: Option<&CodexAuth>, + ) -> anyhow::Result> { + 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::>(); + 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::::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::>(); + 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) + } +} diff --git a/codex-rs/core-plugins/src/lib.rs b/codex-rs/core-plugins/src/lib.rs index 60cb50a88588..4728a8c6f89c 100644 --- a/codex-rs/core-plugins/src/lib.rs +++ b/codex-rs/core-plugins/src/lib.rs @@ -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; pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome; +pub use discoverable::ToolSuggestDiscoverablePlugin; +pub use discoverable::ToolSuggestPluginDiscoveryInput; pub use manager::ConfiguredMarketplace; pub use manager::ConfiguredMarketplaceListOutcome; pub use manager::ConfiguredMarketplacePlugin; diff --git a/codex-rs/core/src/plugins/discoverable.rs b/codex-rs/core/src/plugins/discoverable.rs index 933b30dc1d6e..8ddec7056e7c 100644 --- a/codex-rs/core/src/plugins/discoverable.rs +++ b/codex-rs/core/src/plugins/discoverable.rs @@ -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> { - 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::>(); - 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::>(); - 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::>(); - 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::::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::>(); - 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::>(), + 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::>(), + loaded_plugin_app_connector_ids: loaded_plugin_app_connector_ids + .iter() + .cloned() + .collect::>(), + }; + 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)]