Move executor skill bundle loading into the skills extension (#36877)

## What changed

- Move parsing of pre-discovered executor skill bundles from `core-skills` to
  the skills extension.
- Expose shared `SKILL.md` frontmatter parsing from `codex-skills` so direct
  and pre-discovered loading use the same validation and repair behavior.

## Testing

- Add parity coverage for direct and pre-discovered executor skill catalogs,
  including plugin namespaces, metadata, product policy, and warnings.
- Add a snapshot for the resulting pre-discovered executor catalog.

GitOrigin-RevId: c5f888226fa5600bd8b90f5682400da39a5db5ff
This commit is contained in:
felixxia-oai
2026-08-04 10:41:01 +00:00
committed by copyberry
parent 2a16af8234
commit 4c25d6cc5c
17 changed files with 1138 additions and 488 deletions
+7
View File
@@ -4087,6 +4087,9 @@ dependencies = [
"codex-utils-absolute-path",
"codex-utils-path-uri",
"include_dir",
"pretty_assertions",
"serde",
"serde_yaml",
"thiserror 2.0.18",
]
@@ -4104,14 +4107,18 @@ dependencies = [
"codex-skills",
"codex-tools",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-path-uri",
"codex-utils-string",
"futures",
"insta",
"opentelemetry_sdk",
"pretty_assertions",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_yaml",
"tempfile",
"tokio",
"tracing",
"url",
+18 -202
View File
@@ -4,9 +4,6 @@ mod namespace;
pub use environment::EnvironmentSkillLoadOutcome;
pub use environment::EnvironmentSkillMetadata;
pub use environment::EnvironmentSkillSnapshot;
pub use environment::EnvironmentSkillSnapshotOutcome;
pub use environment::load_environment_skills_from_discovery;
pub use environment::load_environment_skills_from_root;
use crate::model::SkillDependencies;
@@ -26,6 +23,9 @@ use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::LOCAL_FS;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_skills::ParsedSkillFrontmatter;
use codex_skills::SkillParseError;
use codex_skills::parse_skill_frontmatter_metadata;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathUri;
@@ -60,22 +60,6 @@ use tracing::error;
// TODO(anp): Tune this eight-scan limit after revisiting byte-based backpressure.
pub const MAX_CONCURRENT_ROOT_SCANS: usize = 8;
#[derive(Debug, Deserialize)]
struct SkillFrontmatter {
#[serde(default)]
name: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
metadata: SkillFrontmatterMetadata,
}
#[derive(Debug, Default, Deserialize)]
struct SkillFrontmatterMetadata {
#[serde(default, rename = "short-description")]
short_description: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
struct SkillMetadataFile {
#[serde(default)]
@@ -128,13 +112,6 @@ struct DependencyTool {
url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ParsedSkillFrontmatter {
name: String,
description: String,
short_description: Option<String>,
}
const SKILLS_FILENAME: &str = "SKILL.md";
const AGENTS_DIR_NAME: &str = ".agents";
const SKILLS_METADATA_DIR: &str = "agents";
@@ -165,31 +142,27 @@ struct ResolvedDiscoveredSkill {
}
#[derive(Debug)]
enum SkillParseError {
enum SkillLoadError {
Read(std::io::Error),
MissingFrontmatter,
InvalidYaml(serde_yaml::Error),
MissingField(&'static str),
InvalidField { field: &'static str, reason: String },
Parse(SkillParseError),
}
impl fmt::Display for SkillParseError {
impl fmt::Display for SkillLoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SkillParseError::Read(e) => write!(f, "failed to read file: {e}"),
SkillParseError::MissingFrontmatter => {
write!(f, "missing YAML frontmatter delimited by ---")
}
SkillParseError::InvalidYaml(e) => write!(f, "invalid YAML: {e}"),
SkillParseError::MissingField(field) => write!(f, "missing field `{field}`"),
SkillParseError::InvalidField { field, reason } => {
write!(f, "invalid {field}: {reason}")
}
Self::Read(error) => write!(f, "failed to read file: {error}"),
Self::Parse(error) => write!(f, "{error}"),
}
}
}
impl Error for SkillParseError {}
impl Error for SkillLoadError {}
impl From<SkillParseError> for SkillLoadError {
fn from(error: SkillParseError) -> Self {
Self::Parse(error)
}
}
pub struct SkillRoot {
pub path: AbsolutePathBuf,
@@ -693,7 +666,7 @@ async fn parse_skill_file(
scope: SkillScope,
plugin_identity: Option<&PluginIdentity>,
plugin_root: Option<&AbsolutePathBuf>,
) -> Result<SkillMetadata, SkillParseError> {
) -> Result<SkillMetadata, SkillLoadError> {
let metadata_path = path_uri
.parent()
.and_then(|parent| parent.join(SKILLS_METADATA_DIR).ok())
@@ -708,12 +681,12 @@ async fn parse_skill_file(
fs.read_file_text(path_uri, /*sandbox*/ None),
load_skill_metadata(fs, path, &metadata, plugin_root),
);
let contents = contents.map_err(SkillParseError::Read)?;
let contents = contents.map_err(SkillLoadError::Read)?;
let ParsedSkillFrontmatter {
name: base_name,
description,
short_description,
} = parse_skill_frontmatter_metadata_inner(&contents, || default_skill_name(path))?;
} = parse_skill_frontmatter_metadata(&contents, || default_skill_name(path))?;
let LoadedSkillMetadata {
interface,
dependencies,
@@ -734,56 +707,6 @@ async fn parse_skill_file(
})
}
fn parse_skill_frontmatter_metadata_inner(
contents: &str,
default_name: impl FnOnce() -> String,
) -> Result<ParsedSkillFrontmatter, SkillParseError> {
let frontmatter = extract_frontmatter(contents).ok_or(SkillParseError::MissingFrontmatter)?;
let parsed: SkillFrontmatter = match serde_yaml::from_str(&frontmatter) {
Ok(parsed) => Ok(parsed),
Err(original_error) => match repair_frontmatter_scalar_fields(&frontmatter) {
// Some third-party skills use prose like `description: Build for AWS: ECS`
// or `argument-hint: <duration: e.g. 7d>`. Keep the repair line-oriented
// so unrelated invalid YAML still surfaces.
Some(repaired_frontmatter) => {
serde_yaml::from_str(&repaired_frontmatter).map_err(|_| original_error)
}
None => Err(original_error),
},
}
.map_err(SkillParseError::InvalidYaml)?;
let name = parsed
.name
.as_deref()
.map(sanitize_single_line)
.filter(|value| !value.is_empty())
.unwrap_or_else(default_name);
let description = parsed
.description
.as_deref()
.map(sanitize_single_line)
.unwrap_or_default();
let short_description = parsed
.metadata
.short_description
.as_deref()
.map(sanitize_single_line)
.filter(|value| !value.is_empty());
validate_len(&name, MAX_NAME_LEN, "name")?;
if description.is_empty() {
return Err(SkillParseError::MissingField("description"));
}
Ok(ParsedSkillFrontmatter {
name,
description,
short_description,
})
}
fn default_skill_name(path: &AbsolutePathBuf) -> String {
path.parent()
.and_then(|parent| {
@@ -1067,91 +990,6 @@ fn sanitize_single_line(raw: &str) -> String {
raw.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn repair_frontmatter_scalar_fields(frontmatter: &str) -> Option<String> {
let mut changed = false;
let mut block_scalar_indent: Option<usize> = None;
let mut repaired_lines: Vec<String> = Vec::new();
for line in frontmatter.lines() {
let indent = line
.chars()
.take_while(|character| *character == ' ')
.count();
if let Some(block_indent) = block_scalar_indent {
if line.trim().is_empty() || indent > block_indent {
repaired_lines.push(line.to_string());
continue;
}
block_scalar_indent = None;
}
let Some((key, value)) = line.split_once(':') else {
repaired_lines.push(line.to_string());
continue;
};
if key.trim().is_empty() || !value.chars().next().is_none_or(char::is_whitespace) {
repaired_lines.push(line.to_string());
continue;
}
let trimmed_start = value.trim_start();
let leading_whitespace = &value[..value.len() - trimmed_start.len()];
let mut scalar = trimmed_start;
let mut comment = "";
for (index, character) in trimmed_start.char_indices() {
if character == '#'
&& (index == 0
|| trimmed_start[..index]
.chars()
.next_back()
.is_some_and(char::is_whitespace))
{
let comment_start = trimmed_start[..index].trim_end().len();
scalar = &trimmed_start[..comment_start];
comment = &trimmed_start[comment_start..];
break;
}
}
let scalar = scalar.trim_end();
let Some(first_char) = scalar.chars().next() else {
repaired_lines.push(line.to_string());
continue;
};
if matches!(first_char, '|' | '>') {
block_scalar_indent = Some(indent);
repaired_lines.push(line.to_string());
continue;
}
if matches!(first_char, '\'' | '"') {
repaired_lines.push(line.to_string());
continue;
}
let mut has_colon_separator = false;
let mut chars = scalar.chars().peekable();
while let Some(character) = chars.next() {
if character == ':'
&& matches!(chars.peek(), Some(next_character) if next_character.is_whitespace())
{
has_colon_separator = true;
break;
}
}
let invalid_flow_like_scalar = matches!(first_char, '[' | '{' | '@' | '`')
&& serde_yaml::from_str::<serde_yaml::Value>(scalar).is_err();
if !has_colon_separator && !invalid_flow_like_scalar {
repaired_lines.push(line.to_string());
continue;
}
let quoted_scalar = format!("'{}'", scalar.replace('\'', "''"));
repaired_lines.push(format!(
"{key}:{leading_whitespace}{quoted_scalar}{comment}"
));
changed = true;
}
changed.then(|| repaired_lines.join("\n"))
}
fn validate_len(
value: &str,
max_len: usize,
@@ -1211,28 +1049,6 @@ fn resolve_color_str(value: Option<String>, field: &'static str) -> Option<Strin
}
}
fn extract_frontmatter(contents: &str) -> Option<String> {
let mut lines = contents.lines();
if !matches!(lines.next(), Some(line) if line.trim() == "---") {
return None;
}
let mut frontmatter_lines: Vec<&str> = Vec::new();
let mut found_closing = false;
for line in lines.by_ref() {
if line.trim() == "---" {
found_closing = true;
break;
}
frontmatter_lines.push(line);
}
if frontmatter_lines.is_empty() || !found_closing {
return None;
}
Some(frontmatter_lines.join("\n"))
}
#[cfg(test)]
pub(crate) async fn skill_roots_from_layer_stack(
fs: Arc<dyn ExecutorFileSystem>,
+3 -142
View File
@@ -1,10 +1,10 @@
use std::collections::HashMap;
use std::io;
use codex_exec_server::CapabilityRootDiscovery;
use codex_exec_server::ExecutorFileSystem;
use codex_protocol::protocol::Product;
pub use codex_skills::EnvironmentSkillMetadata;
use codex_skills::ParsedSkillFrontmatter;
use codex_skills::parse_skill_frontmatter_metadata;
use codex_utils_path_uri::PathUri;
use futures::StreamExt;
@@ -12,7 +12,6 @@ use crate::model::SkillDependencies;
use crate::model::SkillPolicy;
use super::MAX_QUALIFIED_NAME_LEN;
use super::ParsedSkillFrontmatter;
use super::SkillMetadataFile;
use super::discovery::DirectorySymlinkPolicy;
use super::discovery::DiscoveredSkill;
@@ -22,7 +21,6 @@ use super::discovery::SkillDiscoveryOptions;
use super::discovery::SkillMetadataDiscovery;
use super::discovery::discover_skills;
use super::namespace::SkillNamespaceResolver;
use super::parse_skill_frontmatter_metadata_inner;
use super::resolve_dependencies;
use super::resolve_policy;
use super::sanitize_single_line;
@@ -37,19 +35,6 @@ struct ParsedEnvironmentSkill {
policy: Option<SkillPolicy>,
}
/// Parsed executor skill plus the instructions already materialized by capability discovery.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnvironmentSkillSnapshot {
pub metadata: EnvironmentSkillMetadata,
pub instructions: String,
}
#[derive(Debug, Default)]
pub struct EnvironmentSkillSnapshotOutcome {
pub skills: Vec<EnvironmentSkillSnapshot>,
pub warnings: Vec<String>,
}
impl ParsedEnvironmentSkill {
async fn load(
file_system: &dyn ExecutorFileSystem,
@@ -72,7 +57,7 @@ impl ParsedEnvironmentSkill {
name: base_name,
description,
short_description,
} = parse_skill_frontmatter_metadata_inner(&contents, || default_skill_name(&skill.path))
} = parse_skill_frontmatter_metadata(&contents, || default_skill_name(&skill.path))
.map_err(|err| err.to_string())?;
let (dependencies, policy) = match &skill.metadata {
SkillMetadataDiscovery::Present(_) | SkillMetadataDiscovery::Absent => {
@@ -197,130 +182,6 @@ pub async fn load_environment_skills_from_root(
outcome
}
/// Parses an executor-produced manifest bundle without issuing additional filesystem requests.
pub fn load_environment_skills_from_discovery(
discovery: &CapabilityRootDiscovery,
restriction_product: Option<Product>,
) -> EnvironmentSkillSnapshotOutcome {
let mut outcome = EnvironmentSkillSnapshotOutcome {
warnings: discovery.warnings.clone(),
..Default::default()
};
if let Some(error) = &discovery.error {
outcome.warnings.push(error.clone());
return outcome;
}
let mut plugin_namespaces = HashMap::new();
for (plugin_root, name) in discovery.namespace_manifests.iter().filter_map(|manifest| {
#[derive(serde::Deserialize)]
struct ManifestName {
#[serde(default)]
name: String,
}
let plugin_root = manifest.path.parent()?.parent()?;
let parsed = serde_json::from_str::<ManifestName>(&manifest.contents).ok()?;
let name = if parsed.name.trim().is_empty() {
plugin_root.basename()?
} else {
parsed.name
};
Some((plugin_root, name))
}) {
// Exec-server orders manifests by the same precedence as local discovery. Preserve the
// first manifest if an older or alternate server returns duplicates for one plugin root.
plugin_namespaces.entry(plugin_root).or_insert(name);
}
for skill in &discovery.skills {
let plugin_namespace =
nearest_plugin_namespace(&skill.instructions.path, &plugin_namespaces);
let ParsedSkillFrontmatter {
name: base_name,
description,
short_description,
} = match parse_skill_frontmatter_metadata_inner(&skill.instructions.contents, || {
default_skill_name(&skill.instructions.path)
}) {
Ok(frontmatter) => frontmatter,
Err(error) => {
outcome.warnings.push(format!(
"Failed to load environment skill at {}: {error}",
skill.instructions.path
));
continue;
}
};
let name = plugin_namespace
.map(|namespace| format!("{namespace}:{base_name}"))
.unwrap_or(base_name);
if let Err(error) = validate_len(&name, MAX_QUALIFIED_NAME_LEN, "qualified name") {
outcome.warnings.push(format!(
"Failed to load environment skill at {}: {error}",
skill.instructions.path
));
continue;
}
let (dependencies, policy) = skill
.metadata
.as_ref()
.and_then(|metadata| {
serde_yaml::from_str::<SkillMetadataFile>(&metadata.contents)
.map_err(|error| {
tracing::warn!(
path = %metadata.path,
"ignoring invalid discovered skill metadata: {error}"
);
})
.ok()
})
.map(|metadata| {
(
resolve_dependencies(metadata.dependencies),
resolve_policy(metadata.policy),
)
})
.unwrap_or((None, None));
let metadata = EnvironmentSkillMetadata {
path_to_skills_md: skill.instructions.path.clone(),
name,
description,
short_description,
dependencies,
policy,
};
if metadata.matches_product_restriction(restriction_product) {
outcome.skills.push(EnvironmentSkillSnapshot {
metadata,
instructions: skill.instructions.contents.clone(),
});
}
}
outcome.skills.sort_by(|left, right| {
left.metadata.name.cmp(&right.metadata.name).then_with(|| {
left.metadata
.path_to_skills_md
.to_string()
.cmp(&right.metadata.path_to_skills_md.to_string())
})
});
outcome
}
fn nearest_plugin_namespace<'a>(
skill_path: &PathUri,
plugin_namespaces: &'a HashMap<PathUri, String>,
) -> Option<&'a str> {
let mut ancestor = skill_path.parent();
while let Some(path) = ancestor {
if let Some(namespace) = plugin_namespaces.get(&path) {
return Some(namespace);
}
ancestor = path.parent();
}
None
}
async fn read_skill_contents(
file_system: &dyn ExecutorFileSystem,
skill_path: &PathUri,
@@ -6,10 +6,7 @@ use std::sync::atomic::Ordering;
use std::time::Duration;
use codex_core_skills::loader::EnvironmentSkillMetadata;
use codex_core_skills::loader::load_environment_skills_from_discovery;
use codex_core_skills::loader::load_environment_skills_from_root;
use codex_exec_server::CapabilityRootDiscoverRequest;
use codex_exec_server::CapabilityRootsDiscoverParams;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::ExecutorFileSystem;
@@ -22,7 +19,6 @@ use codex_exec_server::ReadDirectoryEntry;
use codex_exec_server::RemoveOptions;
use codex_exec_server::WalkOptions;
use codex_exec_server::WalkOutcome;
use codex_exec_server::discover_capability_roots;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
@@ -547,142 +543,3 @@ async fn host_loading_reuses_walk_inventory_for_symlinked_skill_pack() {
1
);
}
#[tokio::test]
async fn executor_bundle_parser_matches_the_existing_environment_loader() {
let root = tempdir().expect("tempdir");
let plugin_manifest = root.path().join(".codex-plugin/plugin.json");
let nested_manifest = root.path().join("nested/.claude-plugin/plugin.json");
let deploy_skill = root.path().join("skills/deploy/SKILL.md");
let deploy_metadata = root.path().join("skills/deploy/agents/openai.yaml");
let audit_skill = root.path().join("nested/skills/audit/SKILL.md");
for (path, contents) in [
(&plugin_manifest, r#"{"name":"demo"}"#),
(&nested_manifest, r#"{"name":"nested"}"#),
(
&deploy_skill,
"---\nname: deploy\ndescription: Deploy the service.\n---\n\nDeploy.\n",
),
(
&deploy_metadata,
"policy:\n allow_implicit_invocation: false\n",
),
(
&audit_skill,
"---\nname: audit\ndescription: Audit the service.\n---\n\nAudit.\n",
),
] {
fs::create_dir_all(path.parent().expect("test file parent")).expect("test directory");
fs::write(path, contents).expect("test file");
}
let root_uri = PathUri::from_host_native_path(root.path()).expect("root URI");
let existing = load_environment_skills_from_root(
LOCAL_FS.as_ref(),
&root_uri,
/*restriction_product*/ None,
)
.await;
let response = discover_capability_roots(
LOCAL_FS.as_ref(),
CapabilityRootsDiscoverParams {
roots: vec![CapabilityRootDiscoverRequest {
id: "demo@1".to_string(),
path: root_uri,
sandbox: None,
}],
},
)
.await
.expect("capability discovery");
let bundled = load_environment_skills_from_discovery(
response.roots.first().expect("discovered root"),
/*restriction_product*/ None,
);
assert_eq!(bundled.warnings, existing.warnings);
assert_eq!(
bundled
.skills
.iter()
.map(|skill| skill.metadata.clone())
.collect::<Vec<_>>(),
existing.skills
);
assert_eq!(
bundled
.skills
.iter()
.map(|skill| skill.instructions.as_str())
.collect::<Vec<_>>(),
vec![
"---\nname: deploy\ndescription: Deploy the service.\n---\n\nDeploy.\n",
"---\nname: audit\ndescription: Audit the service.\n---\n\nAudit.\n",
]
);
}
#[tokio::test]
async fn executor_bundle_preserves_parent_namespace_and_manifest_precedence() {
let plugin = tempdir().expect("tempdir");
for (relative_path, name) in [
(".codex-plugin/plugin.json", "codex-name"),
(".claude-plugin/plugin.json", "claude-name"),
(".cursor-plugin/plugin.json", "cursor-name"),
] {
let manifest = plugin.path().join(relative_path);
fs::create_dir_all(manifest.parent().expect("manifest parent"))
.expect("manifest directory");
fs::write(&manifest, format!(r#"{{"name":"{name}"}}"#)).expect("manifest");
}
let skills_root = plugin.path().join("skills");
let skill_path = skills_root.join("search/SKILL.md");
fs::create_dir_all(skill_path.parent().expect("skill parent")).expect("skill directory");
fs::write(
&skill_path,
"---\nname: search\ndescription: Search the project.\n---\n\nSearch.\n",
)
.expect("skill");
let root_uri = PathUri::from_host_native_path(&skills_root).expect("skills root URI");
let existing = load_environment_skills_from_root(
LOCAL_FS.as_ref(),
&root_uri,
/*restriction_product*/ None,
)
.await;
let response = discover_capability_roots(
LOCAL_FS.as_ref(),
CapabilityRootsDiscoverParams {
roots: vec![CapabilityRootDiscoverRequest {
id: "skills-only".to_string(),
path: root_uri,
sandbox: None,
}],
},
)
.await
.expect("capability discovery");
let discovery = response.roots.first().expect("discovered root");
let bundled =
load_environment_skills_from_discovery(discovery, /*restriction_product*/ None);
assert_eq!(discovery.plugin, None);
assert_eq!(discovery.namespace_manifests.len(), 1);
assert!(
discovery.namespace_manifests[0]
.path
.to_string()
.ends_with("/.codex-plugin/plugin.json")
);
assert_eq!(bundled.warnings, existing.warnings);
assert_eq!(
bundled
.skills
.iter()
.map(|skill| skill.metadata.clone())
.collect::<Vec<_>>(),
existing.skills
);
assert_eq!(bundled.skills[0].metadata.name, "codex-name:search");
}
+4
View File
@@ -27,6 +27,7 @@ futures = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
serde_yaml = { workspace = true }
tokio = { workspace = true, features = ["sync", "time"] }
tracing = { workspace = true }
url = { workspace = true }
@@ -34,6 +35,9 @@ url = { workspace = true }
[dev-dependencies]
codex-models-manager = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
insta = { workspace = true }
opentelemetry_sdk = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
+1
View File
@@ -4,6 +4,7 @@ mod config;
mod dynamic_skill_selector;
mod extension;
mod fragments;
mod loader;
pub mod provider;
mod render;
mod render_observability;
@@ -0,0 +1,165 @@
use std::collections::HashMap;
use codex_exec_server::CapabilityRootDiscovery;
use codex_protocol::protocol::Product;
use codex_skills::EnvironmentSkillMetadata;
use codex_skills::ParsedSkillFrontmatter;
use codex_skills::parse_skill_frontmatter_metadata;
use codex_utils_path_uri::PathUri;
use super::MAX_QUALIFIED_NAME_LEN;
use super::metadata::SkillMetadataFile;
use super::metadata::resolve_dependencies;
use super::metadata::resolve_policy;
use super::metadata::sanitize_single_line;
use super::metadata::validate_len;
/// Parsed executor skill plus the instructions already materialized by capability discovery.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnvironmentSkillSnapshot {
pub metadata: EnvironmentSkillMetadata,
pub instructions: String,
}
#[derive(Debug, Default)]
pub struct EnvironmentSkillSnapshotOutcome {
pub skills: Vec<EnvironmentSkillSnapshot>,
pub warnings: Vec<String>,
}
/// Parses an executor-produced manifest bundle without issuing additional filesystem requests.
pub fn load_environment_skills_from_discovery(
discovery: &CapabilityRootDiscovery,
restriction_product: Option<Product>,
) -> EnvironmentSkillSnapshotOutcome {
let mut outcome = EnvironmentSkillSnapshotOutcome {
warnings: discovery.warnings.clone(),
..Default::default()
};
if let Some(error) = &discovery.error {
outcome.warnings.push(error.clone());
return outcome;
}
let mut plugin_namespaces = HashMap::new();
for (plugin_root, name) in discovery.namespace_manifests.iter().filter_map(|manifest| {
#[derive(serde::Deserialize)]
struct ManifestName {
#[serde(default)]
name: String,
}
let plugin_root = manifest.path.parent()?.parent()?;
let parsed = serde_json::from_str::<ManifestName>(&manifest.contents).ok()?;
let name = if parsed.name.trim().is_empty() {
plugin_root.basename()?
} else {
parsed.name
};
Some((plugin_root, name))
}) {
// Exec-server orders manifests by the same precedence as local discovery. Preserve the
// first manifest if an older or alternate server returns duplicates for one plugin root.
plugin_namespaces.entry(plugin_root).or_insert(name);
}
for skill in &discovery.skills {
let plugin_namespace =
nearest_plugin_namespace(&skill.instructions.path, &plugin_namespaces);
let ParsedSkillFrontmatter {
name: base_name,
description,
short_description,
} = match parse_skill_frontmatter_metadata(&skill.instructions.contents, || {
default_skill_name(&skill.instructions.path)
}) {
Ok(frontmatter) => frontmatter,
Err(error) => {
outcome.warnings.push(format!(
"Failed to load environment skill at {}: {error}",
skill.instructions.path
));
continue;
}
};
let name = plugin_namespace
.map(|namespace| format!("{namespace}:{base_name}"))
.unwrap_or(base_name);
if let Err(error) = validate_len(&name, MAX_QUALIFIED_NAME_LEN, "qualified name") {
outcome.warnings.push(format!(
"Failed to load environment skill at {}: {error}",
skill.instructions.path
));
continue;
}
let (dependencies, policy) = skill
.metadata
.as_ref()
.and_then(|metadata| {
serde_yaml::from_str::<SkillMetadataFile>(&metadata.contents)
.map_err(|error| {
tracing::warn!(
path = %metadata.path,
"ignoring invalid discovered skill metadata: {error}"
);
})
.ok()
})
.map(|metadata| {
(
resolve_dependencies(metadata.dependencies),
resolve_policy(metadata.policy),
)
})
.unwrap_or((None, None));
let metadata = EnvironmentSkillMetadata {
path_to_skills_md: skill.instructions.path.clone(),
name,
description,
short_description,
dependencies,
policy,
};
if metadata.matches_product_restriction(restriction_product) {
outcome.skills.push(EnvironmentSkillSnapshot {
metadata,
instructions: skill.instructions.contents.clone(),
});
}
}
outcome.skills.sort_by(|left, right| {
left.metadata.name.cmp(&right.metadata.name).then_with(|| {
left.metadata
.path_to_skills_md
.to_string()
.cmp(&right.metadata.path_to_skills_md.to_string())
})
});
outcome
}
fn nearest_plugin_namespace<'a>(
skill_path: &PathUri,
plugin_namespaces: &'a HashMap<PathUri, String>,
) -> Option<&'a str> {
let mut ancestor = skill_path.parent();
while let Some(path) = ancestor {
if let Some(namespace) = plugin_namespaces.get(&path) {
return Some(namespace);
}
ancestor = path.parent();
}
None
}
fn default_skill_name(path: &PathUri) -> String {
path.parent()
.and_then(|parent| parent.basename())
.map(|name| sanitize_single_line(&name))
.filter(|name| !name.is_empty())
.unwrap_or_else(|| "skill".to_string())
}
#[cfg(test)]
#[path = "environment_tests.rs"]
mod tests;
@@ -0,0 +1,151 @@
use std::fs;
use codex_core_skills::loader::load_environment_skills_from_root;
use codex_exec_server::CapabilityRootDiscoverRequest;
use codex_exec_server::CapabilityRootsDiscoverParams;
use codex_exec_server::LOCAL_FS;
use codex_exec_server::discover_capability_roots;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use tempfile::tempdir;
use super::load_environment_skills_from_discovery;
#[tokio::test]
async fn executor_bundle_parser_matches_the_existing_environment_loader() {
let root = tempdir().expect("tempdir");
let plugin_manifest = root.path().join(".codex-plugin/plugin.json");
let nested_manifest = root.path().join("nested/.claude-plugin/plugin.json");
let deploy_skill = root.path().join("skills/deploy/SKILL.md");
let deploy_metadata = root.path().join("skills/deploy/agents/openai.yaml");
let audit_skill = root.path().join("nested/skills/audit/SKILL.md");
for (path, contents) in [
(&plugin_manifest, r#"{"name":"demo"}"#),
(&nested_manifest, r#"{"name":"nested"}"#),
(
&deploy_skill,
"---\nname: deploy\ndescription: Deploy the service.\n---\n\nDeploy.\n",
),
(
&deploy_metadata,
"policy:\n allow_implicit_invocation: false\n",
),
(
&audit_skill,
"---\nname: audit\ndescription: Audit the service.\n---\n\nAudit.\n",
),
] {
fs::create_dir_all(path.parent().expect("test file parent")).expect("test directory");
fs::write(path, contents).expect("test file");
}
let root_uri = PathUri::from_host_native_path(root.path()).expect("root URI");
let existing = load_environment_skills_from_root(
LOCAL_FS.as_ref(),
&root_uri,
/*restriction_product*/ None,
)
.await;
let response = discover_capability_roots(
LOCAL_FS.as_ref(),
CapabilityRootsDiscoverParams {
roots: vec![CapabilityRootDiscoverRequest {
id: "demo@1".to_string(),
path: root_uri,
sandbox: None,
}],
},
)
.await
.expect("capability discovery");
let bundled = load_environment_skills_from_discovery(
response.roots.first().expect("discovered root"),
/*restriction_product*/ None,
);
assert_eq!(bundled.warnings, existing.warnings);
assert_eq!(
bundled
.skills
.iter()
.map(|skill| skill.metadata.clone())
.collect::<Vec<_>>(),
existing.skills
);
assert_eq!(
bundled
.skills
.iter()
.map(|skill| skill.instructions.as_str())
.collect::<Vec<_>>(),
vec![
"---\nname: deploy\ndescription: Deploy the service.\n---\n\nDeploy.\n",
"---\nname: audit\ndescription: Audit the service.\n---\n\nAudit.\n",
]
);
}
#[tokio::test]
async fn executor_bundle_preserves_parent_namespace_and_manifest_precedence() {
let plugin = tempdir().expect("tempdir");
for (relative_path, name) in [
(".codex-plugin/plugin.json", "codex-name"),
(".claude-plugin/plugin.json", "claude-name"),
(".cursor-plugin/plugin.json", "cursor-name"),
] {
let manifest = plugin.path().join(relative_path);
fs::create_dir_all(manifest.parent().expect("manifest parent"))
.expect("manifest directory");
fs::write(&manifest, format!(r#"{{"name":"{name}"}}"#)).expect("manifest");
}
let skills_root = plugin.path().join("skills");
let skill_path = skills_root.join("search/SKILL.md");
fs::create_dir_all(skill_path.parent().expect("skill parent")).expect("skill directory");
fs::write(
&skill_path,
"---\nname: search\ndescription: Search the project.\n---\n\nSearch.\n",
)
.expect("skill");
let root_uri = PathUri::from_host_native_path(&skills_root).expect("skills root URI");
let existing = load_environment_skills_from_root(
LOCAL_FS.as_ref(),
&root_uri,
/*restriction_product*/ None,
)
.await;
let response = discover_capability_roots(
LOCAL_FS.as_ref(),
CapabilityRootsDiscoverParams {
roots: vec![CapabilityRootDiscoverRequest {
id: "skills-only".to_string(),
path: root_uri,
sandbox: None,
}],
},
)
.await
.expect("capability discovery");
let discovery = response.roots.first().expect("discovered root");
let bundled =
load_environment_skills_from_discovery(discovery, /*restriction_product*/ None);
assert_eq!(discovery.plugin, None);
assert_eq!(discovery.namespace_manifests.len(), 1);
assert!(
discovery.namespace_manifests[0]
.path
.to_string()
.ends_with("/.codex-plugin/plugin.json")
);
assert_eq!(bundled.warnings, existing.warnings);
assert_eq!(
bundled
.skills
.iter()
.map(|skill| skill.metadata.clone())
.collect::<Vec<_>>(),
existing.skills
);
assert_eq!(bundled.skills[0].metadata.name, "codex-name:search");
}
+176
View File
@@ -0,0 +1,176 @@
use std::path::PathBuf;
use codex_protocol::protocol::Product;
use codex_skills::SkillDependencies;
use codex_skills::SkillParseError;
use codex_skills::SkillPolicy;
use codex_skills::SkillToolDependency;
use serde::Deserialize;
use super::MAX_DEPENDENCY_COMMAND_LEN;
use super::MAX_DEPENDENCY_DESCRIPTION_LEN;
use super::MAX_DEPENDENCY_TRANSPORT_LEN;
use super::MAX_DEPENDENCY_TYPE_LEN;
use super::MAX_DEPENDENCY_URL_LEN;
use super::MAX_DEPENDENCY_VALUE_LEN;
#[derive(Debug, Default, Deserialize)]
pub(super) struct SkillMetadataFile {
// Keep parsing the host-only interface fields so malformed interface metadata
// invalidates the file exactly as it does in the legacy loader.
#[serde(default, rename = "interface")]
_interface: Option<Interface>,
#[serde(default)]
pub(super) dependencies: Option<Dependencies>,
#[serde(default)]
pub(super) policy: Option<Policy>,
}
#[derive(Debug, Default, Deserialize)]
struct Interface {
#[serde(rename = "display_name")]
_display_name: Option<String>,
#[serde(rename = "short_description")]
_short_description: Option<String>,
#[serde(rename = "icon_small")]
_icon_small: Option<PathBuf>,
#[serde(rename = "icon_large")]
_icon_large: Option<PathBuf>,
#[serde(rename = "brand_color")]
_brand_color: Option<String>,
#[serde(rename = "default_prompt")]
_default_prompt: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
pub(super) struct Dependencies {
#[serde(default)]
tools: Vec<DependencyTool>,
}
#[derive(Debug, Deserialize)]
pub(super) struct Policy {
#[serde(default)]
allow_implicit_invocation: Option<bool>,
#[serde(default)]
products: Vec<Product>,
}
#[derive(Debug, Default, Deserialize)]
struct DependencyTool {
#[serde(rename = "type")]
kind: Option<String>,
value: Option<String>,
description: Option<String>,
transport: Option<String>,
command: Option<String>,
url: Option<String>,
}
pub(super) fn resolve_dependencies(
dependencies: Option<Dependencies>,
) -> Option<SkillDependencies> {
let dependencies = dependencies?;
let tools = dependencies
.tools
.into_iter()
.filter_map(resolve_dependency_tool)
.collect::<Vec<_>>();
if tools.is_empty() {
None
} else {
Some(SkillDependencies { tools })
}
}
pub(super) fn resolve_policy(policy: Option<Policy>) -> Option<SkillPolicy> {
policy.map(|policy| SkillPolicy {
allow_implicit_invocation: policy.allow_implicit_invocation,
products: policy.products,
})
}
fn resolve_dependency_tool(tool: DependencyTool) -> Option<SkillToolDependency> {
let r#type = resolve_required_str(
tool.kind,
MAX_DEPENDENCY_TYPE_LEN,
"dependencies.tools.type",
)?;
let value = resolve_required_str(
tool.value,
MAX_DEPENDENCY_VALUE_LEN,
"dependencies.tools.value",
)?;
let description = resolve_str(
tool.description,
MAX_DEPENDENCY_DESCRIPTION_LEN,
"dependencies.tools.description",
);
let transport = resolve_str(
tool.transport,
MAX_DEPENDENCY_TRANSPORT_LEN,
"dependencies.tools.transport",
);
let command = resolve_str(
tool.command,
MAX_DEPENDENCY_COMMAND_LEN,
"dependencies.tools.command",
);
let url = resolve_str(tool.url, MAX_DEPENDENCY_URL_LEN, "dependencies.tools.url");
Some(SkillToolDependency {
r#type,
value,
description,
transport,
command,
url,
})
}
pub(super) fn sanitize_single_line(raw: &str) -> String {
raw.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub(super) fn validate_len(
value: &str,
max_len: usize,
field_name: &'static str,
) -> Result<(), SkillParseError> {
if value.is_empty() {
return Err(SkillParseError::MissingField(field_name));
}
if value.chars().count() > max_len {
return Err(SkillParseError::InvalidField {
field: field_name,
reason: format!("exceeds maximum length of {max_len} characters"),
});
}
Ok(())
}
fn resolve_str(value: Option<String>, max_len: usize, field: &'static str) -> Option<String> {
let value = value?;
let value = sanitize_single_line(&value);
if value.is_empty() {
tracing::warn!("ignoring {field}: value is empty");
return None;
}
if value.chars().count() > max_len {
tracing::warn!("ignoring {field}: exceeds maximum length of {max_len} characters");
return None;
}
Some(value)
}
fn resolve_required_str(
value: Option<String>,
max_len: usize,
field: &'static str,
) -> Option<String> {
let Some(value) = value else {
tracing::warn!("ignoring {field}: value is missing");
return None;
};
resolve_str(Some(value), max_len, field)
}
+14
View File
@@ -0,0 +1,14 @@
mod environment;
mod metadata;
pub(crate) use environment::load_environment_skills_from_discovery;
pub(super) const MAX_NAME_LEN: usize = 64;
pub(super) const MAX_QUALIFIED_NAME_LEN: usize = 128;
pub(super) const MAX_DESCRIPTION_LEN: usize = 1024;
pub(super) const MAX_DEPENDENCY_TYPE_LEN: usize = MAX_NAME_LEN;
pub(super) const MAX_DEPENDENCY_TRANSPORT_LEN: usize = MAX_NAME_LEN;
pub(super) const MAX_DEPENDENCY_VALUE_LEN: usize = MAX_DESCRIPTION_LEN;
pub(super) const MAX_DEPENDENCY_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN;
pub(super) const MAX_DEPENDENCY_COMMAND_LEN: usize = MAX_DESCRIPTION_LEN;
pub(super) const MAX_DEPENDENCY_URL_LEN: usize = MAX_DESCRIPTION_LEN;
+1 -1
View File
@@ -1,6 +1,5 @@
use std::sync::Arc;
use codex_core_skills::loader::load_environment_skills_from_discovery;
use codex_core_skills::loader::load_environment_skills_from_root;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::FileSystemSandboxContext;
@@ -20,6 +19,7 @@ use crate::catalog::SkillReadResult;
use crate::catalog::SkillResourceId;
use crate::catalog::SkillSearchResult;
use crate::catalog::SkillSourceKind;
use crate::loader::load_environment_skills_from_discovery;
use crate::provider::MAX_SKILL_RESOURCE_CONTENT_BYTES;
use crate::provider::SkillListQuery;
use crate::provider::SkillProvider;
@@ -7,6 +7,7 @@ use std::sync::atomic::Ordering;
use codex_core_skills::HostSkillsSnapshot;
use codex_core_skills::loader::MAX_CONCURRENT_ROOT_SCANS;
use codex_core_skills::loader::SkillRoot;
use codex_core_skills::loader::load_environment_skills_from_root as load_legacy_environment_skills_from_root;
use codex_core_skills::loader::load_skills_from_roots;
use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
@@ -24,6 +25,7 @@ use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::Product;
use codex_protocol::protocol::SkillScope;
use codex_skills_extension::ExecutorSkillProvider;
use codex_skills_extension::catalog::SkillAuthority;
@@ -350,6 +352,295 @@ async fn selected_root_id_distinguishes_identical_executor_paths() {
std::fs::remove_dir_all(test_root).expect("remove skill directory");
}
#[tokio::test]
async fn executor_discovery_routes_produce_equivalent_catalog_metadata() {
let id = NEXT_TEST_ROOT_ID.fetch_add(1, Ordering::Relaxed);
let test_root = std::env::temp_dir().join(format!(
"codex-executor-skill-parity-{}-{id}",
std::process::id()
));
let plugin_manifest = test_root.join(".codex-plugin/plugin.json");
let deploy_skill = test_root.join("skills/deploy/SKILL.md");
let deploy_metadata = test_root.join("skills/deploy/agents/openai.yaml");
let excluded_skill = test_root.join("skills/excluded/SKILL.md");
let excluded_metadata = test_root.join("skills/excluded/agents/openai.yaml");
let repaired_skill = test_root.join("skills/repaired/SKILL.md");
let invalid_skill = test_root.join("skills/invalid/SKILL.md");
let invalid_metadata_skill = test_root.join("skills/invalid-metadata/SKILL.md");
let invalid_metadata = test_root.join("skills/invalid-metadata/agents/openai.yaml");
for path in [
&plugin_manifest,
&deploy_skill,
&deploy_metadata,
&excluded_skill,
&excluded_metadata,
&repaired_skill,
&invalid_skill,
&invalid_metadata_skill,
&invalid_metadata,
] {
std::fs::create_dir_all(path.parent().expect("test file parent"))
.expect("create test directory");
}
std::fs::write(&plugin_manifest, r#"{"name":"catalog"}"#).expect("write plugin manifest");
std::fs::write(
&deploy_skill,
"---\nname: deploy\ndescription: Deploy the service.\nmetadata:\n short-description: Deploy safely.\n---\n\nDeploy.\n",
)
.expect("write deploy skill");
std::fs::write(
&deploy_metadata,
"dependencies:\n tools:\n - type: mcp\n value: deployer\n description: Deployment server.\npolicy:\n allow_implicit_invocation: false\n products:\n - codex\n",
)
.expect("write deploy metadata");
std::fs::write(
&excluded_skill,
"---\nname: excluded\ndescription: Excluded skill.\n---\n",
)
.expect("write excluded skill");
std::fs::write(&excluded_metadata, "policy:\n products:\n - chatgpt\n")
.expect("write excluded metadata");
std::fs::write(
&repaired_skill,
"---\ndescription: Build for AWS: ECS\n---\n",
)
.expect("write repaired skill");
std::fs::write(&invalid_skill, "---\nname: invalid\n---\n").expect("write invalid skill");
std::fs::write(
&invalid_metadata_skill,
"---\nname: invalid-metadata\ndescription: Invalid optional metadata.\n---\n",
)
.expect("write invalid metadata skill");
std::fs::write(
&invalid_metadata,
"interface: []\ndependencies:\n tools:\n - type: mcp\n value: must-be-ignored\n",
)
.expect("write invalid metadata");
let manager = Arc::new(EnvironmentManager::default_for_tests());
let provider = ExecutorSkillProvider::new_with_restriction_product(
Arc::clone(&manager),
Some(Product::Codex),
);
let executor_roots = vec![SelectedCapabilityRoot {
id: "parity-root".to_string(),
location: CapabilityRootLocation::Environment {
environment_id: "local".to_string(),
path: PathUri::from_host_native_path(&test_root).expect("skill root URI"),
},
}];
let query = |executor_capability_discovery| SkillListQuery {
turn_id: "turn-1".to_string(),
executor_roots: executor_roots.clone(),
resolved_executor_roots: Vec::new(),
host_snapshot: None,
include_host_skills: false,
include_bundled_skills: true,
include_orchestrator_skills: false,
mcp_resources: None,
executor_capability_discovery,
};
let legacy_file_system = manager
.get_environment("local")
.expect("local environment")
.get_filesystem();
let legacy = load_legacy_environment_skills_from_root(
legacy_file_system.as_ref(),
&PathUri::from_host_native_path(&test_root).expect("skill root URI"),
Some(Product::Codex),
)
.await;
let direct = provider
.list(query(None))
.await
.expect("list directly discovered executor skills");
let discovery = ExecutorCapabilityDiscoveryCache::new(manager)
.snapshot(&executor_roots, &Default::default())
.await;
let bundled = provider
.list(query(Some(discovery)))
.await
.expect("list bundled executor skills");
assert_eq!(bundled.warnings, direct.warnings);
let comparable_entries = |catalog: &codex_skills_extension::catalog::SkillCatalog| {
catalog
.entries
.iter()
.map(|entry| {
(
entry.id.clone(),
entry.authority.clone(),
entry.name.clone(),
entry.description.clone(),
entry.short_description.clone(),
entry.display_path.clone(),
entry.dependencies.clone(),
entry.enabled,
entry.prompt_visible,
)
})
.collect::<Vec<_>>()
};
assert_eq!(comparable_entries(&bundled), comparable_entries(&direct));
assert_eq!(legacy.warnings, direct.warnings);
assert_eq!(direct.warnings.len(), 1);
assert!(direct.warnings[0].contains("missing field `description`"));
let legacy_metadata = legacy
.skills
.iter()
.map(|skill| {
(
skill.name.clone(),
skill.description.clone(),
skill.short_description.clone(),
skill.dependencies.clone(),
skill.allows_implicit_invocation(),
)
})
.collect::<Vec<_>>();
let catalog_metadata = direct
.entries
.iter()
.map(|entry| {
(
entry.name.clone(),
entry.description.clone(),
entry.short_description.clone(),
entry.dependencies.clone(),
entry.prompt_visible,
)
})
.collect::<Vec<_>>();
assert_eq!(catalog_metadata, legacy_metadata);
let entry = direct
.entries
.iter()
.find(|entry| entry.name == "catalog:deploy")
.expect("deploy skill");
assert_eq!(
entry
.dependencies
.as_ref()
.expect("skill dependencies")
.tools[0]
.value,
"deployer"
);
assert!(!entry.prompt_visible);
let repaired = direct
.entries
.iter()
.find(|entry| entry.name == "catalog:repaired")
.expect("repaired skill");
assert_eq!(repaired.description, "Build for AWS: ECS");
let invalid_metadata = direct
.entries
.iter()
.find(|entry| entry.name == "catalog:invalid-metadata")
.expect("invalid metadata skill");
assert_eq!(invalid_metadata.dependencies, None);
std::fs::remove_dir_all(test_root).expect("remove skill directory");
}
#[tokio::test]
async fn pre_discovered_executor_catalog_snapshot() {
let test_root = create_local_skill_root("snapshot").expect("create local skill root");
let manifest_dir = test_root.join(".codex-plugin");
let metadata_dir = test_root.join("skill/agents");
std::fs::create_dir_all(&manifest_dir).expect("create plugin manifest directory");
std::fs::create_dir_all(&metadata_dir).expect("create skill metadata directory");
std::fs::write(
manifest_dir.join("plugin.json"),
r#"{"name":"snapshot-plugin"}"#,
)
.expect("write plugin manifest");
std::fs::write(
metadata_dir.join("openai.yaml"),
"dependencies:\n tools:\n - type: mcp\n value: deployer\n description: Deployment server.\npolicy:\n allow_implicit_invocation: false\n",
)
.expect("write skill metadata");
let manager = Arc::new(EnvironmentManager::default_for_tests());
let provider = ExecutorSkillProvider::new_with_restriction_product(
Arc::clone(&manager),
/*restriction_product*/ None,
);
let executor_roots = vec![SelectedCapabilityRoot {
id: "snapshot-root".to_string(),
location: CapabilityRootLocation::Environment {
environment_id: "local".to_string(),
path: PathUri::from_host_native_path(&test_root).expect("skill root URI"),
},
}];
let executor_capability_discovery = ExecutorCapabilityDiscoveryCache::new(manager)
.snapshot(&executor_roots, &Default::default())
.await;
let catalog = provider
.list(SkillListQuery {
turn_id: "turn-1".to_string(),
executor_roots,
resolved_executor_roots: Vec::new(),
host_snapshot: None,
include_host_skills: false,
include_bundled_skills: true,
include_orchestrator_skills: false,
mcp_resources: None,
executor_capability_discovery: Some(executor_capability_discovery),
})
.await
.expect("list pre-discovered executor skills");
let mut snapshot = serde_json::json!({
"warnings": &catalog.warnings,
"entries": catalog
.entries
.iter()
.map(|entry| serde_json::json!({
"authority": {
"kind": entry.authority.kind.to_string(),
"id": &entry.authority.id,
},
"name": &entry.name,
"description": &entry.description,
"short_description": entry.short_description.as_deref(),
"dependencies": entry.dependencies.as_ref().map(|dependencies| {
dependencies.tools.iter().map(|tool| serde_json::json!({
"type": &tool.r#type,
"value": &tool.value,
"description": tool.description.as_deref(),
"transport": tool.transport.as_deref(),
"command": tool.command.as_deref(),
"url": tool.url.as_deref(),
})).collect::<Vec<_>>()
}),
"enabled": entry.enabled,
"prompt_visible": entry.prompt_visible,
}))
.collect::<Vec<_>>(),
});
snapshot.sort_all_objects();
let snapshot_file = codex_utils_cargo_bin::find_resource!(
"tests/snapshots/executor_file_system_authority__pre_discovered_executor_catalog.snap"
)
.expect("resolve catalog snapshot");
let snapshot_dir = snapshot_file
.parent()
.unwrap_or_else(|| panic!("snapshot file has no parent: {}", snapshot_file.display()));
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(snapshot_dir);
settings.bind(|| {
insta::assert_snapshot!(
"pre_discovered_executor_catalog",
serde_json::to_string_pretty(&snapshot).expect("serialize catalog snapshot")
);
});
std::fs::remove_dir_all(test_root).expect("remove skill directory");
}
#[tokio::test]
async fn high_level_discovery_reuses_materialized_skill_contents_for_reads() {
let test_root = create_local_skill_root("materialized").expect("create local skill root");
@@ -0,0 +1,30 @@
---
source: ext/skills/tests/executor_file_system_authority.rs
expression: "serde_json::to_string_pretty(&snapshot).expect(\"serialize catalog snapshot\")"
---
{
"entries": [
{
"authority": {
"id": "snapshot-root",
"kind": "executor"
},
"dependencies": [
{
"command": null,
"description": "Deployment server.",
"transport": null,
"type": "mcp",
"url": null,
"value": "deployer"
}
],
"description": "Synthetic executor skill.",
"enabled": true,
"name": "snapshot-plugin:synthetic",
"prompt_visible": false,
"short_description": null
}
],
"warnings": []
}
+5
View File
@@ -18,4 +18,9 @@ codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-path-uri = { workspace = true }
include_dir = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_yaml = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
+4
View File
@@ -1,4 +1,5 @@
mod model;
mod parser;
pub use model::EnvironmentSkillMetadata;
pub use model::SkillConfigRule;
@@ -9,6 +10,9 @@ pub use model::SkillInterface;
pub use model::SkillMetadata;
pub use model::SkillPolicy;
pub use model::SkillToolDependency;
pub use parser::ParsedSkillFrontmatter;
pub use parser::SkillParseError;
pub use parser::parse_skill_frontmatter_metadata;
use codex_utils_absolute_path::AbsolutePathBuf;
use include_dir::Dir;
+225
View File
@@ -0,0 +1,225 @@
use serde::Deserialize;
use thiserror::Error;
const MAX_NAME_LEN: usize = 64;
#[derive(Debug, Deserialize)]
struct SkillFrontmatter {
#[serde(default)]
name: Option<String>,
#[serde(default)]
description: Option<String>,
#[serde(default)]
metadata: SkillFrontmatterMetadata,
}
#[derive(Debug, Default, Deserialize)]
struct SkillFrontmatterMetadata {
#[serde(default, rename = "short-description")]
short_description: Option<String>,
}
/// Validated metadata parsed from a `SKILL.md` frontmatter block.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedSkillFrontmatter {
pub name: String,
pub description: String,
pub short_description: Option<String>,
}
/// Error produced while parsing or validating `SKILL.md` metadata.
#[derive(Debug, Error)]
pub enum SkillParseError {
#[error("missing YAML frontmatter delimited by ---")]
MissingFrontmatter,
#[error("invalid YAML: {0}")]
InvalidYaml(#[source] serde_yaml::Error),
#[error("missing field `{0}`")]
MissingField(&'static str),
#[error("invalid {field}: {reason}")]
InvalidField { field: &'static str, reason: String },
}
/// Parses and validates the metadata frontmatter from `SKILL.md` contents.
pub fn parse_skill_frontmatter_metadata(
contents: &str,
default_name: impl FnOnce() -> String,
) -> Result<ParsedSkillFrontmatter, SkillParseError> {
let frontmatter = extract_frontmatter(contents).ok_or(SkillParseError::MissingFrontmatter)?;
let parsed: SkillFrontmatter = match serde_yaml::from_str(&frontmatter) {
Ok(parsed) => Ok(parsed),
Err(original_error) => match repair_frontmatter_scalar_fields(&frontmatter) {
// Some third-party skills use prose like `description: Build for AWS: ECS`
// or `argument-hint: <duration: e.g. 7d>`. Keep the repair line-oriented
// so unrelated invalid YAML still surfaces.
Some(repaired_frontmatter) => {
serde_yaml::from_str(&repaired_frontmatter).map_err(|_| original_error)
}
None => Err(original_error),
},
}
.map_err(SkillParseError::InvalidYaml)?;
let name = parsed
.name
.as_deref()
.map(sanitize_single_line)
.filter(|value| !value.is_empty())
.unwrap_or_else(default_name);
let description = parsed
.description
.as_deref()
.map(sanitize_single_line)
.unwrap_or_default();
let short_description = parsed
.metadata
.short_description
.as_deref()
.map(sanitize_single_line)
.filter(|value| !value.is_empty());
validate_len(&name, MAX_NAME_LEN, "name")?;
if description.is_empty() {
return Err(SkillParseError::MissingField("description"));
}
Ok(ParsedSkillFrontmatter {
name,
description,
short_description,
})
}
fn sanitize_single_line(raw: &str) -> String {
raw.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn repair_frontmatter_scalar_fields(frontmatter: &str) -> Option<String> {
let mut changed = false;
let mut block_scalar_indent: Option<usize> = None;
let mut repaired_lines = Vec::new();
for line in frontmatter.lines() {
let indent = line
.chars()
.take_while(|character| *character == ' ')
.count();
if let Some(block_indent) = block_scalar_indent {
if line.trim().is_empty() || indent > block_indent {
repaired_lines.push(line.to_string());
continue;
}
block_scalar_indent = None;
}
let Some((key, value)) = line.split_once(':') else {
repaired_lines.push(line.to_string());
continue;
};
if key.trim().is_empty() || !value.chars().next().is_none_or(char::is_whitespace) {
repaired_lines.push(line.to_string());
continue;
}
let trimmed_start = value.trim_start();
let leading_whitespace = &value[..value.len() - trimmed_start.len()];
let mut scalar = trimmed_start;
let mut comment = "";
for (index, character) in trimmed_start.char_indices() {
if character == '#'
&& (index == 0
|| trimmed_start[..index]
.chars()
.next_back()
.is_some_and(char::is_whitespace))
{
let comment_start = trimmed_start[..index].trim_end().len();
scalar = &trimmed_start[..comment_start];
comment = &trimmed_start[comment_start..];
break;
}
}
let scalar = scalar.trim_end();
let Some(first_char) = scalar.chars().next() else {
repaired_lines.push(line.to_string());
continue;
};
if matches!(first_char, '|' | '>') {
block_scalar_indent = Some(indent);
repaired_lines.push(line.to_string());
continue;
}
if matches!(first_char, '\'' | '"') {
repaired_lines.push(line.to_string());
continue;
}
let mut has_colon_separator = false;
let mut chars = scalar.chars().peekable();
while let Some(character) = chars.next() {
if character == ':'
&& matches!(chars.peek(), Some(next_character) if next_character.is_whitespace())
{
has_colon_separator = true;
break;
}
}
let invalid_flow_like_scalar = matches!(first_char, '[' | '{' | '@' | '`')
&& serde_yaml::from_str::<serde_yaml::Value>(scalar).is_err();
if !has_colon_separator && !invalid_flow_like_scalar {
repaired_lines.push(line.to_string());
continue;
}
let quoted_scalar = format!("'{}'", scalar.replace('\'', "''"));
repaired_lines.push(format!(
"{key}:{leading_whitespace}{quoted_scalar}{comment}"
));
changed = true;
}
changed.then(|| repaired_lines.join("\n"))
}
fn validate_len(
value: &str,
max_len: usize,
field_name: &'static str,
) -> Result<(), SkillParseError> {
if value.is_empty() {
return Err(SkillParseError::MissingField(field_name));
}
if value.chars().count() > max_len {
return Err(SkillParseError::InvalidField {
field: field_name,
reason: format!("exceeds maximum length of {max_len} characters"),
});
}
Ok(())
}
fn extract_frontmatter(contents: &str) -> Option<String> {
let mut lines = contents.lines();
if !matches!(lines.next(), Some(line) if line.trim() == "---") {
return None;
}
let mut frontmatter_lines = Vec::new();
let mut found_closing = false;
for line in lines.by_ref() {
if line.trim() == "---" {
found_closing = true;
break;
}
frontmatter_lines.push(line);
}
if frontmatter_lines.is_empty() || !found_closing {
return None;
}
Some(frontmatter_lines.join("\n"))
}
#[cfg(test)]
#[path = "parser_tests.rs"]
mod tests;
+43
View File
@@ -0,0 +1,43 @@
use pretty_assertions::assert_eq;
use super::ParsedSkillFrontmatter;
use super::parse_skill_frontmatter_metadata;
#[test]
fn parses_repairs_and_sanitizes_frontmatter() {
let parsed = parse_skill_frontmatter_metadata(
"---\nname: deploy service\ndescription: Build for AWS: ECS\nmetadata:\n short-description: Deploy safely\n---\n",
|| "fallback".to_string(),
)
.expect("valid frontmatter");
assert_eq!(
parsed,
ParsedSkillFrontmatter {
name: "deploy service".to_string(),
description: "Build for AWS: ECS".to_string(),
short_description: Some("Deploy safely".to_string()),
}
);
}
#[test]
fn uses_default_name_and_requires_description() {
let parsed = parse_skill_frontmatter_metadata("---\ndescription: Demo skill\n---\n", || {
"demo".to_string()
})
.expect("valid frontmatter");
assert_eq!(
parsed,
ParsedSkillFrontmatter {
name: "demo".to_string(),
description: "Demo skill".to_string(),
short_description: None,
}
);
let error =
parse_skill_frontmatter_metadata("---\nname: demo\n---\n", || "fallback".to_string())
.expect_err("description should be required");
assert_eq!(error.to_string(), "missing field `description`");
}