From 09d4ce896dbf580b42259b18b7b4c60002f71ddf Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Wed, 25 Mar 2026 00:15:59 -0700 Subject: [PATCH] fix --- codex-rs/plugin/src/load_outcome.rs | 163 ++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 codex-rs/plugin/src/load_outcome.rs diff --git a/codex-rs/plugin/src/load_outcome.rs b/codex-rs/plugin/src/load_outcome.rs new file mode 100644 index 0000000000..9faebaebe9 --- /dev/null +++ b/codex-rs/plugin/src/load_outcome.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::path::PathBuf; + +use codex_utils_absolute_path::AbsolutePathBuf; + +use crate::AppConnectorId; +use crate::PluginCapabilitySummary; + +const MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN: usize = 1024; + +/// A plugin that was loaded from disk, including merged MCP server definitions. +#[derive(Debug, Clone, PartialEq)] +pub struct LoadedPlugin { + pub config_name: String, + pub manifest_name: Option, + pub manifest_description: Option, + pub root: AbsolutePathBuf, + pub enabled: bool, + pub skill_roots: Vec, + pub disabled_skill_paths: HashSet, + pub has_enabled_skills: bool, + pub mcp_servers: HashMap, + pub apps: Vec, + pub error: Option, +} + +impl LoadedPlugin { + pub fn is_active(&self) -> bool { + self.enabled && self.error.is_none() + } +} + +fn plugin_capability_summary_from_loaded( + plugin: &LoadedPlugin, +) -> Option { + if !plugin.is_active() { + return None; + } + + let mut mcp_server_names: Vec = plugin.mcp_servers.keys().cloned().collect(); + mcp_server_names.sort_unstable(); + + let summary = PluginCapabilitySummary { + config_name: plugin.config_name.clone(), + display_name: plugin + .manifest_name + .clone() + .unwrap_or_else(|| plugin.config_name.clone()), + description: prompt_safe_plugin_description(plugin.manifest_description.as_deref()), + has_skills: plugin.has_enabled_skills, + mcp_server_names, + app_connector_ids: plugin.apps.clone(), + }; + + (summary.has_skills + || !summary.mcp_server_names.is_empty() + || !summary.app_connector_ids.is_empty()) + .then_some(summary) +} + +/// Normalizes plugin descriptions for inclusion in model-facing capability summaries. +pub fn prompt_safe_plugin_description(description: Option<&str>) -> Option { + let description = description? + .split_whitespace() + .collect::>() + .join(" "); + if description.is_empty() { + return None; + } + + Some( + description + .chars() + .take(MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN) + .collect(), + ) +} + +/// Outcome of loading configured plugins (skills roots, MCP, apps, errors). +#[derive(Debug, Clone, PartialEq)] +pub struct PluginLoadOutcome { + plugins: Vec>, + capability_summaries: Vec, +} + +impl Default for PluginLoadOutcome { + fn default() -> Self { + Self::from_plugins(Vec::new()) + } +} + +impl PluginLoadOutcome { + pub fn from_plugins(plugins: Vec>) -> Self { + let capability_summaries = plugins + .iter() + .filter_map(plugin_capability_summary_from_loaded) + .collect::>(); + Self { + plugins, + capability_summaries, + } + } + + pub fn effective_skill_roots(&self) -> Vec { + let mut skill_roots: Vec = self + .plugins + .iter() + .filter(|plugin| plugin.is_active()) + .flat_map(|plugin| plugin.skill_roots.iter().cloned()) + .collect(); + skill_roots.sort_unstable(); + skill_roots.dedup(); + skill_roots + } + + pub fn effective_mcp_servers(&self) -> HashMap { + let mut mcp_servers = HashMap::new(); + for plugin in self.plugins.iter().filter(|plugin| plugin.is_active()) { + for (name, config) in &plugin.mcp_servers { + mcp_servers + .entry(name.clone()) + .or_insert_with(|| config.clone()); + } + } + mcp_servers + } + + pub fn effective_apps(&self) -> Vec { + let mut apps = Vec::new(); + let mut seen_connector_ids = HashSet::new(); + + for plugin in self.plugins.iter().filter(|plugin| plugin.is_active()) { + for connector_id in &plugin.apps { + if seen_connector_ids.insert(connector_id.clone()) { + apps.push(connector_id.clone()); + } + } + } + + apps + } + + pub fn capability_summaries(&self) -> &[PluginCapabilitySummary] { + &self.capability_summaries + } + + pub fn plugins(&self) -> &[LoadedPlugin] { + &self.plugins + } +} + +/// Implemented by [`PluginLoadOutcome`] so callers (e.g. skills) can depend on `codex-plugin` +/// without naming the MCP config type parameter. +pub trait EffectiveSkillRoots { + fn effective_skill_roots(&self) -> Vec; +} + +impl EffectiveSkillRoots for PluginLoadOutcome { + fn effective_skill_roots(&self) -> Vec { + PluginLoadOutcome::effective_skill_roots(self) + } +}