diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index a0d713e0b9..d67ac2f4e6 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2018,6 +2018,7 @@ dependencies = [ "tokio", "toml 0.9.11+spec-1.1.0", "tracing", + "zip", ] [[package]] @@ -2445,6 +2446,7 @@ dependencies = [ name = "codex-plugin" version = "0.0.0" dependencies = [ + "codex-utils-absolute-path", "serde", "serde_json", "tempfile", diff --git a/codex-rs/core-skills/Cargo.toml b/codex-rs/core-skills/Cargo.toml index 4f4d23cc19..80f6d43daa 100644 --- a/codex-rs/core-skills/Cargo.toml +++ b/codex-rs/core-skills/Cargo.toml @@ -32,6 +32,7 @@ serde_json = { workspace = true } tokio = { workspace = true, features = ["fs", "macros", "rt"] } toml = { workspace = true } tracing = { workspace = true } +zip = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/core-skills/src/skills/manager.rs b/codex-rs/core-skills/src/skills/manager.rs index 6810356a2b..5352f18d01 100644 --- a/codex-rs/core-skills/src/skills/manager.rs +++ b/codex-rs/core-skills/src/skills/manager.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use std::sync::Arc; use std::sync::RwLock; +use codex_config::ConfigLayerStack; use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; @@ -12,11 +13,9 @@ use toml::Value as TomlValue; use tracing::info; use tracing::warn; -use crate::config::Config; use crate::config_loader::CloudRequirementsLoader; use crate::config_loader::LoaderOverrides; use crate::config_loader::load_config_layers_state; -use crate::plugins::PluginsManager; use crate::skills::SkillLoadOutcome; use crate::skills::build_implicit_skill_path_indexes; use crate::skills::config_rules::SkillConfigRules; @@ -28,38 +27,27 @@ use crate::skills::loader::skill_roots; use crate::skills::system::install_system_skills; use crate::skills::system::uninstall_system_skills; use codex_config::SkillsConfig; +use codex_plugin::EffectiveSkillRoots; pub struct SkillsManager { codex_home: PathBuf, - plugins_manager: Arc, restriction_product: Option, cache_by_cwd: RwLock>, cache_by_config: RwLock>, } impl SkillsManager { - pub fn new( - codex_home: PathBuf, - plugins_manager: Arc, - bundled_skills_enabled: bool, - ) -> Self { - Self::new_with_restriction_product( - codex_home, - plugins_manager, - bundled_skills_enabled, - Some(Product::Codex), - ) + pub fn new(codex_home: PathBuf, bundled_skills_enabled: bool) -> Self { + Self::new_with_restriction_product(codex_home, bundled_skills_enabled, Some(Product::Codex)) } pub fn new_with_restriction_product( codex_home: PathBuf, - plugins_manager: Arc, bundled_skills_enabled: bool, restriction_product: Option, ) -> Self { let manager = Self { codex_home, - plugins_manager, restriction_product, cache_by_cwd: RwLock::new(HashMap::new()), cache_by_config: RwLock::new(HashMap::new()), @@ -80,9 +68,20 @@ impl SkillsManager { /// This path uses a cache keyed by the effective skill-relevant config state rather than just /// cwd so role-local and session-local skill overrides cannot bleed across sessions that happen /// to share a directory. - pub fn skills_for_config(&self, config: &Config) -> SkillLoadOutcome { - let roots = self.skill_roots_for_config(config); - let skill_config_rules = skill_config_rules_from_stack(&config.config_layer_stack); + pub fn skills_for_config( + &self, + cwd: &Path, + effective_skill_roots: &[PathBuf], + config_layer_stack: &ConfigLayerStack, + bundled_skills_enabled: bool, + ) -> SkillLoadOutcome { + let roots = self.skill_roots_for_config( + cwd, + effective_skill_roots, + config_layer_stack, + bundled_skills_enabled, + ); + let skill_config_rules = skill_config_rules_from_stack(&config_layer_stack); let cache_key = config_skills_cache_key(&roots, &skill_config_rules); if let Some(outcome) = self.cached_outcome_for_config(&cache_key) { return outcome; @@ -97,14 +96,19 @@ impl SkillsManager { outcome } - pub(crate) fn skill_roots_for_config(&self, config: &Config) -> Vec { - let loaded_plugins = self.plugins_manager.plugins_for_config(config); + pub(crate) fn skill_roots_for_config( + &self, + cwd: &Path, + effective_skill_roots: &[PathBuf], + config_layer_stack: &ConfigLayerStack, + bundled_skills_enabled: bool, + ) -> Vec { let mut roots = skill_roots( - &config.config_layer_stack, - &config.cwd, - loaded_plugins.effective_skill_roots(), + &config_layer_stack, + &cwd, + effective_skill_roots.to_vec(), ); - if !config.bundled_skills_enabled() { + if !bundled_skills_enabled { roots.retain(|root| root.scope != SkillScope::System); } roots @@ -127,7 +131,7 @@ impl SkillsManager { pub async fn skills_for_cwd_with_extra_user_roots( &self, cwd: &Path, - config: &Config, + effective_skill_roots: &[PathBuf], force_reload: bool, extra_user_roots: &[PathBuf], ) -> SkillLoadOutcome { @@ -171,13 +175,10 @@ impl SkillsManager { } }; - let loaded_plugins = self - .plugins_manager - .plugins_for_config_with_force_reload(config, force_reload); let mut roots = skill_roots( &config_layer_stack, cwd, - loaded_plugins.effective_skill_roots(), + effective_skill_roots.to_vec(), ); if !bundled_skills_enabled_from_stack(&config_layer_stack) { roots.retain(|root| root.scope != SkillScope::System); diff --git a/codex-rs/core/src/plugins/manager.rs b/codex-rs/core/src/plugins/manager.rs index 15ca0b573c..c732e94808 100644 --- a/codex-rs/core/src/plugins/manager.rs +++ b/codex-rs/core/src/plugins/manager.rs @@ -1,3 +1,5 @@ +use super::LoadedPlugin; +use super::PluginLoadOutcome; use super::PluginManifestPaths; use super::curated_plugins_repo_path; use super::load_plugin_manifest; @@ -49,6 +51,7 @@ use codex_plugin::PluginCapabilitySummary; use codex_plugin::PluginId; use codex_plugin::PluginIdError; use codex_plugin::PluginTelemetryMetadata; +use codex_plugin::prompt_safe_plugin_description; use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; @@ -76,7 +79,6 @@ const DEFAULT_MCP_CONFIG_FILE: &str = ".mcp.json"; const DEFAULT_APP_CONFIG_FILE: &str = ".app.json"; pub const OPENAI_CURATED_MARKETPLACE_NAME: &str = "openai-curated"; static CURATED_REPO_SYNC_STARTED: AtomicBool = AtomicBool::new(false); -const MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN: usize = 1024; const FEATURED_PLUGIN_IDS_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(60 * 60 * 3); @@ -185,53 +187,6 @@ pub struct ConfiguredMarketplaceListOutcome { pub errors: Vec, } -#[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 { - 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) -} - impl From for PluginCapabilitySummary { fn from(value: PluginDetail) -> Self { let has_skills = value.skills.iter().any(|skill| { @@ -250,95 +205,6 @@ impl From for PluginCapabilitySummary { } } -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(), - ) -} - -#[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 { - 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 = std::collections::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 - } -} - #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct RemotePluginSyncResult { /// Plugin ids newly installed into the local plugin cache. diff --git a/codex-rs/core/src/plugins/manager_tests.rs b/codex-rs/core/src/plugins/manager_tests.rs index cf16e5b5fb..7c7ef57c50 100644 --- a/codex-rs/core/src/plugins/manager_tests.rs +++ b/codex-rs/core/src/plugins/manager_tests.rs @@ -7,7 +7,9 @@ use crate::config_loader::ConfigLayerEntry; use crate::config_loader::ConfigLayerStack; use crate::config_loader::ConfigRequirements; use crate::config_loader::ConfigRequirementsToml; +use crate::plugins::LoadedPlugin; use crate::plugins::MarketplacePluginInstallPolicy; +use crate::plugins::PluginLoadOutcome; use crate::plugins::test_support::TEST_CURATED_PLUGIN_SHA; use crate::plugins::test_support::write_curated_plugin_sha_with as write_curated_plugin_sha; use crate::plugins::test_support::write_file; diff --git a/codex-rs/core/src/plugins/mod.rs b/codex-rs/core/src/plugins/mod.rs index e3acc7dde7..4501e44ac7 100644 --- a/codex-rs/core/src/plugins/mod.rs +++ b/codex-rs/core/src/plugins/mod.rs @@ -1,3 +1,5 @@ +use crate::config::types::McpServerConfig; + mod discoverable; mod injection; mod manager; @@ -13,22 +15,25 @@ pub(crate) mod test_support; mod toggles; pub use codex_plugin::AppConnectorId; +pub use codex_plugin::EffectiveSkillRoots; pub use codex_plugin::PluginCapabilitySummary; pub use codex_plugin::PluginId; pub use codex_plugin::PluginIdError; pub use codex_plugin::PluginTelemetryMetadata; + +pub type LoadedPlugin = codex_plugin::LoadedPlugin; +pub type PluginLoadOutcome = codex_plugin::PluginLoadOutcome; + pub(crate) use discoverable::list_tool_suggest_discoverable_plugins; pub(crate) use injection::build_plugin_injections; pub use manager::ConfiguredMarketplace; pub use manager::ConfiguredMarketplaceListOutcome; pub use manager::ConfiguredMarketplacePlugin; -pub use manager::LoadedPlugin; pub use manager::OPENAI_CURATED_MARKETPLACE_NAME; pub use manager::PluginDetail; pub use manager::PluginInstallError; pub use manager::PluginInstallOutcome; pub use manager::PluginInstallRequest; -pub use manager::PluginLoadOutcome; pub use manager::PluginReadOutcome; pub use manager::PluginReadRequest; pub use manager::PluginRemoteSyncError; diff --git a/codex-rs/plugin/Cargo.toml b/codex-rs/plugin/Cargo.toml index 7caccc496d..339e04d278 100644 --- a/codex-rs/plugin/Cargo.toml +++ b/codex-rs/plugin/Cargo.toml @@ -13,6 +13,7 @@ path = "src/lib.rs" workspace = true [dependencies] +codex-utils-absolute-path = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/codex-rs/plugin/src/lib.rs b/codex-rs/plugin/src/lib.rs index d789f64d04..c387044365 100644 --- a/codex-rs/plugin/src/lib.rs +++ b/codex-rs/plugin/src/lib.rs @@ -2,9 +2,14 @@ pub mod mention_syntax; +mod load_outcome; mod plugin_id; mod plugin_namespace; +pub use load_outcome::EffectiveSkillRoots; +pub use load_outcome::LoadedPlugin; +pub use load_outcome::PluginLoadOutcome; +pub use load_outcome::prompt_safe_plugin_description; pub use plugin_id::PluginId; pub use plugin_id::PluginIdError; pub use plugin_id::validate_plugin_segment;