mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
c73296a0f0
## What changed - retain the parsed plugin manifest namespace on loaded plugins - carry that namespace through `PluginSkillRoot` and `SkillRoot` - use the provided namespace when qualifying plugin skill names - include the namespace in the skills cache key ## Why Plugin loading has already parsed `plugin.json`, but skill parsing currently walks every `SKILL.md` ancestor and probes/reads the manifest again to reconstruct the same namespace. Passing the parsed namespace removes those repeated filesystem calls, which are particularly costly on remote filesystems. Context: https://openai.slack.com/archives/C0ARA9GF5D4/p1781639496496439?thread_ts=1781202444.891669&cid=C0ARA9GF5D4 ## Impact Plugin skill names remain unchanged. A regression test uses a deliberately different on-disk manifest name to verify that plugin roots use the provided parsed namespace. ## Validation - `just test -p codex-core-skills -p codex-core-plugins -p codex-plugin -p codex-utils-plugins` (352 passed) - `just fix -p codex-core-skills -p codex-core-plugins -p codex-plugin -p codex-utils-plugins` - `just fmt`
265 lines
8.3 KiB
Rust
265 lines
8.3 KiB
Rust
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
|
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
|
use codex_utils_plugins::PluginSkillRoot;
|
|
|
|
use crate::AppConnectorId;
|
|
use crate::AppDeclaration;
|
|
use crate::PluginCapabilitySummary;
|
|
use crate::PluginHookSource;
|
|
use crate::app_connector_ids_from_declarations;
|
|
|
|
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<M> {
|
|
pub config_name: String,
|
|
pub manifest_name: Option<String>,
|
|
pub plugin_namespace: Option<String>,
|
|
pub manifest_description: Option<String>,
|
|
pub root: AbsolutePathBuf,
|
|
pub enabled: bool,
|
|
pub skill_roots: Vec<AbsolutePathBuf>,
|
|
pub disabled_skill_paths: HashSet<AbsolutePathBuf>,
|
|
pub has_enabled_skills: bool,
|
|
pub mcp_servers: HashMap<String, M>,
|
|
pub apps: Vec<AppDeclaration>,
|
|
pub hook_sources: Vec<PluginHookSource>,
|
|
pub hook_load_warnings: Vec<String>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
impl<M> LoadedPlugin<M> {
|
|
pub fn is_active(&self) -> bool {
|
|
self.enabled && self.error.is_none()
|
|
}
|
|
|
|
pub fn display_name(&self) -> &str {
|
|
self.manifest_name.as_deref().unwrap_or(&self.config_name)
|
|
}
|
|
}
|
|
|
|
fn plugin_capability_summary_from_loaded<M>(
|
|
plugin: &LoadedPlugin<M>,
|
|
) -> Option<PluginCapabilitySummary> {
|
|
if !plugin.is_active() {
|
|
return None;
|
|
}
|
|
|
|
let mut mcp_server_names: Vec<String> = plugin.mcp_servers.keys().cloned().collect();
|
|
mcp_server_names.sort_unstable();
|
|
|
|
let summary = PluginCapabilitySummary {
|
|
config_name: plugin.config_name.clone(),
|
|
display_name: plugin.display_name().to_string(),
|
|
description: prompt_safe_plugin_description(plugin.manifest_description.as_deref()),
|
|
has_skills: plugin.has_enabled_skills,
|
|
mcp_server_names,
|
|
app_connector_ids: app_connector_ids_from_declarations(&plugin.apps),
|
|
};
|
|
|
|
(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<String> {
|
|
let description = description?
|
|
.split_whitespace()
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
if description.is_empty() {
|
|
return None;
|
|
}
|
|
|
|
Some(
|
|
description
|
|
.chars()
|
|
.take(MAX_CAPABILITY_SUMMARY_DESCRIPTION_LEN)
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
/// Runtime view of loaded plugins and their derived capability summaries.
|
|
///
|
|
/// Callers must apply any runtime capability policies before constructing this outcome.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct PluginLoadOutcome<M> {
|
|
plugins: Vec<LoadedPlugin<M>>,
|
|
capability_summaries: Vec<PluginCapabilitySummary>,
|
|
}
|
|
|
|
impl<M: Clone> Default for PluginLoadOutcome<M> {
|
|
fn default() -> Self {
|
|
Self::from_plugins(Vec::new())
|
|
}
|
|
}
|
|
|
|
impl<M: Clone> PluginLoadOutcome<M> {
|
|
pub fn from_plugins(plugins: Vec<LoadedPlugin<M>>) -> Self {
|
|
let capability_summaries = plugins
|
|
.iter()
|
|
.filter_map(plugin_capability_summary_from_loaded)
|
|
.collect::<Vec<_>>();
|
|
Self {
|
|
plugins,
|
|
capability_summaries,
|
|
}
|
|
}
|
|
|
|
pub fn effective_skill_roots(&self) -> Vec<AbsolutePathBuf> {
|
|
let mut skill_roots: Vec<AbsolutePathBuf> = 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_plugin_skill_roots(&self) -> Vec<PluginSkillRoot> {
|
|
let mut skill_roots = Vec::new();
|
|
let mut seen_paths = HashSet::new();
|
|
for plugin in self.plugins.iter().filter(|plugin| plugin.is_active()) {
|
|
let Some(plugin_namespace) = &plugin.plugin_namespace else {
|
|
continue;
|
|
};
|
|
for path in &plugin.skill_roots {
|
|
if seen_paths.insert(path.clone()) {
|
|
skill_roots.push(PluginSkillRoot {
|
|
path: path.clone(),
|
|
plugin_id: plugin.config_name.clone(),
|
|
plugin_namespace: plugin_namespace.clone(),
|
|
plugin_root: plugin.root.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
skill_roots.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
|
skill_roots
|
|
}
|
|
|
|
pub fn effective_mcp_servers(&self) -> HashMap<String, M> {
|
|
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<AppConnectorId> {
|
|
app_connector_ids_from_declarations(
|
|
self.plugins
|
|
.iter()
|
|
.filter(|plugin| plugin.is_active())
|
|
.flat_map(|plugin| plugin.apps.iter()),
|
|
)
|
|
}
|
|
|
|
pub fn effective_plugin_hook_sources(&self) -> Vec<PluginHookSource> {
|
|
self.plugins
|
|
.iter()
|
|
.filter(|plugin| plugin.is_active())
|
|
.flat_map(|plugin| plugin.hook_sources.iter().cloned())
|
|
.collect()
|
|
}
|
|
|
|
pub fn effective_plugin_hook_warnings(&self) -> Vec<String> {
|
|
self.plugins
|
|
.iter()
|
|
.filter(|plugin| plugin.is_active())
|
|
.flat_map(|plugin| plugin.hook_load_warnings.iter().cloned())
|
|
.collect()
|
|
}
|
|
|
|
pub fn capability_summaries(&self) -> &[PluginCapabilitySummary] {
|
|
&self.capability_summaries
|
|
}
|
|
|
|
pub fn plugins(&self) -> &[LoadedPlugin<M>] {
|
|
&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<AbsolutePathBuf>;
|
|
|
|
fn effective_plugin_skill_roots(&self) -> Vec<PluginSkillRoot>;
|
|
}
|
|
|
|
impl<M: Clone> EffectiveSkillRoots for PluginLoadOutcome<M> {
|
|
fn effective_skill_roots(&self) -> Vec<AbsolutePathBuf> {
|
|
PluginLoadOutcome::effective_skill_roots(self)
|
|
}
|
|
|
|
fn effective_plugin_skill_roots(&self) -> Vec<PluginSkillRoot> {
|
|
PluginLoadOutcome::effective_plugin_skill_roots(self)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn test_path(name: &str) -> AbsolutePathBuf {
|
|
AbsolutePathBuf::from_absolute_path_checked(std::env::temp_dir().join(name))
|
|
.expect("absolute temp path")
|
|
}
|
|
|
|
fn loaded_plugin(config_name: &str, skill_roots: Vec<AbsolutePathBuf>) -> LoadedPlugin<()> {
|
|
LoadedPlugin {
|
|
config_name: config_name.to_string(),
|
|
manifest_name: None,
|
|
plugin_namespace: Some(
|
|
config_name
|
|
.split_once('@')
|
|
.map_or(config_name, |(name, _)| name)
|
|
.to_string(),
|
|
),
|
|
manifest_description: None,
|
|
root: test_path(config_name),
|
|
enabled: true,
|
|
skill_roots,
|
|
disabled_skill_paths: HashSet::new(),
|
|
has_enabled_skills: true,
|
|
mcp_servers: HashMap::new(),
|
|
apps: Vec::new(),
|
|
hook_sources: Vec::new(),
|
|
hook_load_warnings: Vec::new(),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn effective_plugin_skill_roots_preserves_first_plugin_for_shared_root() {
|
|
let shared_root = test_path("shared-skills");
|
|
let outcome = PluginLoadOutcome::from_plugins(vec![
|
|
loaded_plugin("zeta@test", vec![shared_root.clone()]),
|
|
loaded_plugin("alpha@test", vec![shared_root.clone()]),
|
|
]);
|
|
|
|
assert_eq!(
|
|
outcome.effective_plugin_skill_roots(),
|
|
vec![PluginSkillRoot {
|
|
path: shared_root,
|
|
plugin_id: "zeta@test".to_string(),
|
|
plugin_namespace: "zeta".to_string(),
|
|
plugin_root: test_path("zeta@test"),
|
|
}]
|
|
);
|
|
}
|
|
}
|