Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ fn selected_capability_fixture(
std::fs::write(
config_path,
format!(
"{config}\n[features]\napps = true\ndeferred_executor = true\n\n[skills]\ninclude_instructions = true\n"
"{config}\n[features]\napps = true\ndeferred_executor = true\nexecutor_capability_discovery = true\n\n[skills]\ninclude_instructions = true\n"
),
)?;
write_chatgpt_auth(
Expand Down
4 changes: 2 additions & 2 deletions codex-rs/core-plugins/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub type PluginManifestMcpServers =
codex_plugin::manifest::PluginManifestMcpServers<AbsolutePathBuf>;
pub type PluginManifestPaths = codex_plugin::manifest::PluginManifestPaths<AbsolutePathBuf>;

pub(crate) type UriPluginManifest = codex_plugin::manifest::PluginManifest<PathUri>;
pub type UriPluginManifest = codex_plugin::manifest::PluginManifest<PathUri>;

#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand Down Expand Up @@ -178,7 +178,7 @@ pub(crate) fn parse_plugin_manifest(
.try_map_resources(|path| path.to_abs_path().map_err(serde_json::Error::io))
}

pub(crate) fn parse_plugin_manifest_uri(
pub fn parse_plugin_manifest_uri(
plugin_root: &PathUri,
manifest_path: &PathUri,
contents: &str,
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core-skills/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ 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;
Expand Down
139 changes: 139 additions & 0 deletions codex-rs/core-skills/src/loader/environment.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::HashMap;
use std::io;

use codex_exec_server::CapabilityRootDiscovery;
use codex_exec_server::ExecutorFileSystem;
use codex_protocol::protocol::Product;
use codex_utils_path_uri::PathUri;
Expand Down Expand Up @@ -45,6 +47,19 @@ pub struct EnvironmentSkillMetadata {
pub 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 EnvironmentSkillMetadata {
pub fn allows_implicit_invocation(&self) -> bool {
self.policy
Expand Down Expand Up @@ -212,6 +227,130 @@ 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,
Expand Down
141 changes: 141 additions & 0 deletions codex-rs/core-skills/tests/environment_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ 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;
Expand All @@ -19,6 +22,7 @@ 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;
Expand Down Expand Up @@ -540,3 +544,140 @@ 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,
}],
},
)
.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,
}],
},
)
.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");
}
6 changes: 6 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,9 @@
"exec_permission_approvals": {
"type": "boolean"
},
"executor_capability_discovery": {
"type": "boolean"
},
"experimental_use_unified_exec_tool": {
"type": "boolean"
},
Expand Down Expand Up @@ -4912,6 +4915,9 @@
"exec_permission_approvals": {
"type": "boolean"
},
"executor_capability_discovery": {
"type": "boolean"
},
"experimental_use_unified_exec_tool": {
"type": "boolean"
},
Expand Down
Loading
Loading