mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Preserve discovery paths for symlinked skills (#37144)
## Why Symlinked skills have a canonical `SKILL.md` path that can differ from the path under the configured skill root. A catalog entry that advertises the discovery path must still select the canonical skill. ## What changed - Track each loaded skill's discovery path alongside its canonical identity. - Render discovery paths in the skill catalog and accept either form for structured selections and linked skill mentions. - Preserve enabled-state checks, deduplication, and product filtering when resolving discovery paths. ## Testing Added loader, catalog, mention-resolution, and end-to-end turn coverage for skills discovered through directory symlinks. GitOrigin-RevId: bae539b766cc3782c7c121a4276c337699cbb509
This commit is contained in:
1 parent
fa5d5ae047
commit
72d937ed4d
11 files changed
+392
-49
No files matched your search
@@ -106,6 +106,11 @@ fn catalog_from_outcome(outcome: &SkillLoadOutcome) -> SkillCatalog {
|
||||
|
||||
for (skill, enabled) in outcome.skills_with_enabled() {
|
||||
let mut entry = catalog_entry_from_skill(skill, enabled);
|
||||
if let Some(discovery_path) =
|
||||
outcome.skill_discovery_path_for_path(&skill.path_to_skills_md)
|
||||
{
|
||||
entry = entry.with_display_path(discovery_path.to_string_lossy().replace('\\', "/"));
|
||||
}
|
||||
if let Some(root) = outcome.skill_root_for_path(&skill.path_to_skills_md) {
|
||||
entry = entry.with_display_path_root(root.to_string_lossy().replace('\\', "/"));
|
||||
if let Some(root_order) = root_order_by_path.get(root.as_path()) {
|
||||
|
||||
@@ -63,3 +63,53 @@ async fn host_catalog_entries_carry_their_render_metadata() -> Result<(), Box<dy
|
||||
std::fs::remove_dir_all(root.as_path())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn host_catalog_preserves_symlinked_skill_discovery_paths()
|
||||
-> Result<(), Box<dyn std::error::Error>> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let source = tempfile::tempdir()?;
|
||||
let source_skill_dir = source.path().join("linked-skill");
|
||||
std::fs::create_dir_all(&source_skill_dir)?;
|
||||
std::fs::write(
|
||||
source_skill_dir.join("SKILL.md"),
|
||||
"---\nname: linked-skill\ndescription: Linked skill.\n---\n# Linked skill\n",
|
||||
)?;
|
||||
std::os::unix::fs::symlink(&source_skill_dir, root.path().join("linked-skill"))?;
|
||||
|
||||
let root = AbsolutePathBuf::try_from(std::fs::canonicalize(root.path())?)?;
|
||||
let outcome = load_skills_from_roots(
|
||||
[SkillRoot {
|
||||
path: root.clone(),
|
||||
scope: SkillScope::User,
|
||||
file_system: Arc::clone(&LOCAL_FS),
|
||||
plugin_identity: None,
|
||||
plugin_namespace: None,
|
||||
plugin_root: None,
|
||||
discovery_mode: Default::default(),
|
||||
}],
|
||||
/*plugin_skill_snapshots*/ None,
|
||||
Arc::new(Semaphore::new(1)),
|
||||
)
|
||||
.await;
|
||||
let catalog = catalog_from_outcome(&outcome);
|
||||
let canonical_path = std::fs::canonicalize(source_skill_dir.join("SKILL.md"))?;
|
||||
let discovery_path = root.join("linked-skill/SKILL.md");
|
||||
|
||||
assert_eq!(catalog.entries.len(), 1);
|
||||
assert_eq!(
|
||||
(
|
||||
catalog.entries[0].main_prompt.as_str(),
|
||||
catalog.entries[0].display_path.as_deref(),
|
||||
catalog.entries[0].display_path_root(),
|
||||
),
|
||||
(
|
||||
canonical_path.to_string_lossy().as_ref(),
|
||||
Some(discovery_path.to_string_lossy().as_ref()),
|
||||
Some(root.to_string_lossy().as_ref()),
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user