Extract codex-plugin crate (#15747)

## Summary
- extract plugin identifiers and load-outcome types into codex-plugin
- update codex-core to consume the new plugin crate

## Testing
- CI

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Ahmed Ibrahim
2026-03-25 11:07:31 -07:00
committed by GitHub
parent ad74543a6f
commit 2bb1027e37
14 changed files with 394 additions and 297 deletions

View File

@@ -0,0 +1,55 @@
//! Shared plugin identifiers and telemetry-facing summaries.
pub use codex_utils_plugins::PLUGIN_MANIFEST_PATH;
pub use codex_utils_plugins::mention_syntax;
pub use codex_utils_plugins::plugin_namespace_for_skill_path;
mod load_outcome;
mod plugin_id;
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;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AppConnectorId(pub String);
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PluginCapabilitySummary {
pub config_name: String,
pub display_name: String,
pub description: Option<String>,
pub has_skills: bool,
pub mcp_server_names: Vec<String>,
pub app_connector_ids: Vec<AppConnectorId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginTelemetryMetadata {
pub plugin_id: PluginId,
pub capability_summary: Option<PluginCapabilitySummary>,
}
impl PluginTelemetryMetadata {
pub fn from_plugin_id(plugin_id: &PluginId) -> Self {
Self {
plugin_id: plugin_id.clone(),
capability_summary: None,
}
}
}
impl PluginCapabilitySummary {
pub fn telemetry_metadata(&self) -> Option<PluginTelemetryMetadata> {
PluginId::parse(&self.config_name)
.ok()
.map(|plugin_id| PluginTelemetryMetadata {
plugin_id,
capability_summary: Some(self.clone()),
})
}
}

View File

@@ -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<M> {
pub config_name: String,
pub manifest_name: Option<String>,
pub manifest_description: Option<String>,
pub root: AbsolutePathBuf,
pub enabled: bool,
pub skill_roots: Vec<PathBuf>,
pub disabled_skill_paths: HashSet<PathBuf>,
pub has_enabled_skills: bool,
pub mcp_servers: HashMap<String, M>,
pub apps: Vec<AppConnectorId>,
pub error: Option<String>,
}
impl<M> LoadedPlugin<M> {
pub fn is_active(&self) -> bool {
self.enabled && self.error.is_none()
}
}
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
.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<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(),
)
}
/// Outcome of loading configured plugins (skills roots, MCP, apps, errors).
#[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<PathBuf> {
let mut skill_roots: Vec<PathBuf> = 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<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> {
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<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<PathBuf>;
}
impl<M: Clone> EffectiveSkillRoots for PluginLoadOutcome<M> {
fn effective_skill_roots(&self) -> Vec<PathBuf> {
PluginLoadOutcome::effective_skill_roots(self)
}
}

View File

@@ -0,0 +1,64 @@
//! Stable plugin identifier parsing and validation shared with the plugin cache.
#[derive(Debug, thiserror::Error)]
pub enum PluginIdError {
#[error("{0}")]
Invalid(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginId {
pub plugin_name: String,
pub marketplace_name: String,
}
impl PluginId {
pub fn new(plugin_name: String, marketplace_name: String) -> Result<Self, PluginIdError> {
validate_plugin_segment(&plugin_name, "plugin name").map_err(PluginIdError::Invalid)?;
validate_plugin_segment(&marketplace_name, "marketplace name")
.map_err(PluginIdError::Invalid)?;
Ok(Self {
plugin_name,
marketplace_name,
})
}
pub fn parse(plugin_key: &str) -> Result<Self, PluginIdError> {
let Some((plugin_name, marketplace_name)) = plugin_key.rsplit_once('@') else {
return Err(PluginIdError::Invalid(format!(
"invalid plugin key `{plugin_key}`; expected <plugin>@<marketplace>"
)));
};
if plugin_name.is_empty() || marketplace_name.is_empty() {
return Err(PluginIdError::Invalid(format!(
"invalid plugin key `{plugin_key}`; expected <plugin>@<marketplace>"
)));
}
Self::new(plugin_name.to_string(), marketplace_name.to_string()).map_err(|err| match err {
PluginIdError::Invalid(message) => {
PluginIdError::Invalid(format!("{message} in `{plugin_key}`"))
}
})
}
pub fn as_key(&self) -> String {
format!("{}@{}", self.plugin_name, self.marketplace_name)
}
}
/// Validates a single path segment used in plugin IDs and cache layout.
pub fn validate_plugin_segment(segment: &str, kind: &str) -> Result<(), String> {
if segment.is_empty() {
return Err(format!("invalid {kind}: must not be empty"));
}
if !segment
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
{
return Err(format!(
"invalid {kind}: only ASCII letters, digits, `_`, and `-` are allowed"
));
}
Ok(())
}