Load host skill roots through the skills extension (#37162)

## What changed

- Load ordinary recursive host skill roots with the skills extension's host loader.
- Keep plugin-specific roots on the existing loader so plugin snapshot caching and namespacing continue to work.
- Merge both kinds of root snapshots through the existing precedence and deduplication logic, including discovery paths and source filesystem mappings.
- Move `HostSkillsSnapshot` into the skills extension as the owner of host skill loading.

## Testing

- Cover merged host and plugin catalogs, precedence for symlinked roots, discovery paths, and end-to-end rendering and invocation of host, repository, and plugin skills.

GitOrigin-RevId: c18080afb917c33c57bc17e36b40dd0f3c72d1d7
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-08-05 21:17:54 +00:00
1 parent c38a60ded2
commit bd36d69aae
21 files changed
+515 -162

No files matched your search

-1
View File
@@ -17,7 +17,6 @@ pub const MAX_SKILL_PROMPT_BYTES: usize = 8_000;
pub(crate) use invocation_utils::build_implicit_skill_path_indexes;
pub use invocation_utils::detect_implicit_skill_invocation_for_command;
pub use mention_counts::build_skill_name_counts;
pub use model::HostSkillsSnapshot;
pub use model::SkillError;
pub use model::SkillLoadOutcome;
pub use model::SkillMetadata;
+25 -1
View File
@@ -2,6 +2,7 @@ mod discovery;
mod environment;
mod namespace;
pub use crate::root_loader::load_skill_root_snapshot;
pub use environment::EnvironmentSkillLoadOutcome;
pub use environment::EnvironmentSkillMetadata;
pub use environment::load_environment_skills_from_root;
@@ -161,8 +162,12 @@ where
.await
}
/// One loaded skill root and the filesystem that supplied it.
///
/// This is exposed temporarily while host discovery moves out of `core-skills`; callers should
/// combine snapshots through [`SkillLoadOutcome::from_root_snapshots`].
#[derive(Clone)]
pub(crate) struct SkillRootSnapshot {
pub struct SkillRootSnapshot {
pub(crate) root: AbsolutePathBuf,
pub(crate) is_agent_plugin: bool,
pub(crate) skills: Vec<SkillMetadata>,
@@ -171,6 +176,25 @@ pub(crate) struct SkillRootSnapshot {
pub(crate) file_system: Arc<dyn ExecutorFileSystem>,
}
impl SkillRootSnapshot {
pub fn new(
root: AbsolutePathBuf,
skills: Vec<SkillMetadata>,
skill_discovery_path_by_path: Arc<HashMap<AbsolutePathBuf, AbsolutePathBuf>>,
errors: Vec<SkillError>,
file_system: Arc<dyn ExecutorFileSystem>,
) -> Self {
Self {
root,
is_agent_plugin: false,
skills,
skill_discovery_path_by_path,
errors,
file_system,
}
}
}
pub(crate) async fn load_skill_root(root: SkillRoot) -> SkillRootSnapshot {
let canonical_root =
canonicalize_for_skill_identity(root.file_system.as_ref(), &root.path).await;
+7 -16
View File
@@ -102,27 +102,18 @@ impl SkillLoadOutcome {
self.file_systems_by_skill_path
.get(&skill.path_to_skills_md)
}
}
/// Immutable snapshot of host-owned skills and the filesystem mapping needed
/// to read each skill through the environment that discovered it.
#[derive(Debug, Clone)]
pub struct HostSkillsSnapshot {
outcome: Arc<SkillLoadOutcome>,
}
impl HostSkillsSnapshot {
pub fn new(outcome: Arc<SkillLoadOutcome>) -> Self {
Self { outcome }
}
pub fn outcome(&self) -> &SkillLoadOutcome {
self.outcome.as_ref()
/// Builds the legacy aggregate from independently loaded roots.
///
/// This is a temporary migration boundary while host root loading moves to the skills
/// extension. It deliberately reuses the existing merge, precedence, and deduplication logic.
pub fn from_root_snapshots(snapshots: Vec<crate::loader::SkillRootSnapshot>) -> Self {
crate::root_loader::merge_skill_root_snapshots(snapshots)
}
/// Reads one loaded skill through the filesystem that discovered it.
pub async fn read_skill_text(&self, skill: &SkillMetadata) -> io::Result<String> {
let fs = self
.outcome
.file_system_for_skill(skill)
.unwrap_or_else(|| Arc::clone(&LOCAL_FS));
let path = PathUri::from_abs_path(&skill.path_to_skills_md);
+54 -43
View File
@@ -71,48 +71,7 @@ where
.acquire()
.await
.unwrap_or_else(|_| unreachable!());
let cache_key = match (
root.plugin_identity.clone(),
root.plugin_namespace.clone(),
root.plugin_root.clone(),
) {
(Some(plugin_identity), Some(plugin_namespace), Some(plugin_root)) => {
Some(PluginSkillRoot {
path: root.path.clone(),
plugin_identity,
plugin_namespace,
plugin_root,
discovery_mode: root.discovery_mode,
})
}
_ => None,
};
let cached_snapshot = cache_key.as_ref().and_then(|cache_key| {
let plugin_skill_snapshots = plugin_skill_snapshots?;
plugin_skill_snapshots
.snapshots_by_root
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(cache_key)
.cloned()
});
let snapshot = match cached_snapshot {
Some(snapshot) => snapshot,
None => {
let snapshot = load_skill_root(root).await;
if let Some(plugin_skill_snapshots) = plugin_skill_snapshots
&& let Some(cache_key) = cache_key
{
plugin_skill_snapshots
.snapshots_by_root
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(cache_key, snapshot.clone());
}
snapshot
}
};
let snapshot = load_skill_root_snapshot(root, plugin_skill_snapshots).await;
(root_index, snapshot)
})
// Keep each load's scan queue bounded while avoiding head-of-line blocking.
@@ -129,7 +88,59 @@ where
merge_skill_root_snapshots(root_snapshots)
}
fn merge_skill_root_snapshots(snapshots: Vec<SkillRootSnapshot>) -> SkillLoadOutcome {
/// Loads one root while preserving the existing plugin-loader snapshot reuse.
///
/// This is a temporary migration boundary for callers that load non-plugin roots elsewhere but
/// still need plugin roots to use the legacy preload cache.
pub async fn load_skill_root_snapshot(
root: SkillRoot,
plugin_skill_snapshots: Option<&PluginSkillSnapshots>,
) -> SkillRootSnapshot {
let cache_key = match (
root.plugin_identity.clone(),
root.plugin_namespace.clone(),
root.plugin_root.clone(),
) {
(Some(plugin_identity), Some(plugin_namespace), Some(plugin_root)) => {
Some(PluginSkillRoot {
path: root.path.clone(),
plugin_identity,
plugin_namespace,
plugin_root,
discovery_mode: root.discovery_mode,
})
}
_ => None,
};
let cached_snapshot = cache_key.as_ref().and_then(|cache_key| {
let plugin_skill_snapshots = plugin_skill_snapshots?;
plugin_skill_snapshots
.snapshots_by_root
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(cache_key)
.cloned()
});
match cached_snapshot {
Some(snapshot) => snapshot,
None => {
let snapshot = load_skill_root(root).await;
if let Some(plugin_skill_snapshots) = plugin_skill_snapshots
&& let Some(cache_key) = cache_key
{
plugin_skill_snapshots
.snapshots_by_root
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(cache_key, snapshot.clone());
}
snapshot
}
}
}
pub(crate) fn merge_skill_root_snapshots(snapshots: Vec<SkillRootSnapshot>) -> SkillLoadOutcome {
fn scope_rank(scope: codex_protocol::protocol::SkillScope) -> u8 {
use codex_protocol::protocol::SkillScope;