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;