Move host skill root resolution into the skills extension (#36943)

## What changed

- Move config-layer, user, system, plugin, extra, and repository skill-root
  resolution from `core-skills` into the host skills extension.
- Keep the core loader focused on loading explicit `SkillRoot` values.
- Relocate and expand tests for root precedence, deduplication, repository
  ancestry, plugin metadata, and concurrent probing.

GitOrigin-RevId: 3b95cf28101b8b4d64d54079d202154dad560aab
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-08-04 18:41:55 +00:00
1 parent d1fb77d692
commit 1a7519fa07
13 files changed
+1077 -1109

No files matched your search

-1
View File
@@ -7,7 +7,6 @@ pub mod model;
pub mod remote;
mod root_loader;
mod skill_instructions;
pub mod system;
pub(crate) use invocation_utils::build_implicit_skill_path_indexes;
pub use invocation_utils::detect_implicit_skill_invocation_for_command;
-305
View File
@@ -13,14 +13,7 @@ use crate::model::SkillLoadOutcome;
use crate::model::SkillMetadata;
use crate::model::SkillPolicy;
use crate::model::SkillToolDependency;
use crate::system::system_cache_root_dir;
use codex_config::ConfigLayerSource;
use codex_config::ConfigLayerStack;
use codex_config::default_project_root_markers;
use codex_config::merge_toml_values;
use codex_config::project_root_markers_from_config;
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;
@@ -33,9 +26,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::AbsolutePathBufGuard;
use codex_utils_path_uri::PathUri;
use codex_utils_plugins::PluginIdentity;
use codex_utils_plugins::PluginSkillRoot;
use codex_utils_plugins::SkillDiscoveryMode;
use dirs::home_dir;
use discovery::DirectorySymlinkPolicy;
use discovery::DiscoveredSkill;
use discovery::HiddenDirectoryPolicy;
@@ -48,13 +39,11 @@ use futures::FutureExt;
use futures::StreamExt;
use namespace::SkillNamespaceResolver;
use serde::Deserialize;
use std::collections::HashSet;
use std::error::Error;
use std::fmt;
use std::io;
use std::sync::Arc;
use tokio::sync::Semaphore;
use toml::Value as TomlValue;
use tracing::error;
// TODO(anp): Tune this eight-scan limit after revisiting byte-based backpressure.
@@ -103,10 +92,8 @@ struct DependencyTool {
}
const SKILLS_FILENAME: &str = "SKILL.md";
const AGENTS_DIR_NAME: &str = ".agents";
const SKILLS_METADATA_DIR: &str = "agents";
const SKILLS_METADATA_FILENAME: &str = "openai.yaml";
const SKILLS_DIR_NAME: &str = "skills";
const MAX_NAME_LEN: usize = 64;
const MAX_QUALIFIED_NAME_LEN: usize = 128;
const MAX_DESCRIPTION_LEN: usize = 1024;
@@ -119,9 +106,6 @@ const MAX_DEPENDENCY_URL_LEN: usize = MAX_DESCRIPTION_LEN;
// Traversal depth from the skills root.
const MAX_SCAN_DEPTH: usize = 6;
const MAX_SKILLS_DIRS_PER_ROOT: usize = 2000;
// Keep ancestor metadata probes within one remote round trip for typical project hierarchies while
// leaving room for other startup discovery on the shared exec-server transport.
const MAX_CONCURRENT_ANCESTOR_PROBES: usize = 256;
struct ResolvedDiscoveredSkill {
skill: DiscoveredSkill,
@@ -197,277 +181,6 @@ pub(crate) async fn load_skill_root(root: SkillRoot) -> SkillRootSnapshot {
}
}
pub async fn skill_roots(
fs: Option<Arc<dyn ExecutorFileSystem>>,
config_layer_stack: &ConfigLayerStack,
cwd: &AbsolutePathBuf,
plugin_skill_roots: Vec<PluginSkillRoot>,
extra_skill_roots: Vec<AbsolutePathBuf>,
) -> Vec<SkillRoot> {
let home_dir =
home_dir().and_then(|path| AbsolutePathBuf::from_absolute_path_checked(path).ok());
skill_roots_with_home_dir(
fs,
config_layer_stack,
cwd,
home_dir.as_ref(),
plugin_skill_roots,
extra_skill_roots,
)
.await
}
async fn skill_roots_with_home_dir(
fs: Option<Arc<dyn ExecutorFileSystem>>,
config_layer_stack: &ConfigLayerStack,
cwd: &AbsolutePathBuf,
home_dir: Option<&AbsolutePathBuf>,
plugin_skill_roots: Vec<PluginSkillRoot>,
extra_skill_roots: Vec<AbsolutePathBuf>,
) -> Vec<SkillRoot> {
let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir, fs.clone());
roots.extend(plugin_skill_roots.into_iter().map(|root| SkillRoot {
path: root.path,
scope: SkillScope::User,
file_system: Arc::clone(&LOCAL_FS),
plugin_identity: Some(root.plugin_identity),
plugin_namespace: Some(root.plugin_namespace),
plugin_root: Some(root.plugin_root),
discovery_mode: root.discovery_mode,
}));
roots.extend(extra_skill_roots.into_iter().map(|path| SkillRoot {
path,
scope: SkillScope::User,
file_system: Arc::clone(&LOCAL_FS),
plugin_identity: None,
plugin_namespace: None,
plugin_root: None,
discovery_mode: SkillDiscoveryMode::Recursive,
}));
roots.extend(repo_agents_skill_roots(fs, config_layer_stack, cwd).await);
dedupe_skill_roots_by_path(&mut roots);
roots
}
fn skill_roots_from_layer_stack_inner(
config_layer_stack: &ConfigLayerStack,
home_dir: Option<&AbsolutePathBuf>,
repo_fs: Option<Arc<dyn ExecutorFileSystem>>,
) -> Vec<SkillRoot> {
let mut roots = Vec::new();
for layer in config_layer_stack.all_layers_high_to_low() {
let Some(config_folder) = layer.config_folder() else {
continue;
};
match &layer.name {
ConfigLayerSource::Project { .. } => {
if let Some(repo_fs) = &repo_fs {
roots.push(SkillRoot {
path: config_folder.join(SKILLS_DIR_NAME),
scope: SkillScope::Repo,
file_system: Arc::clone(repo_fs),
plugin_identity: None,
plugin_namespace: None,
plugin_root: None,
discovery_mode: SkillDiscoveryMode::Recursive,
});
}
}
ConfigLayerSource::User { .. } => {
// Deprecated user skills location (`$CODEX_HOME/skills`), kept for backward
// compatibility.
roots.push(SkillRoot {
path: config_folder.join(SKILLS_DIR_NAME),
scope: SkillScope::User,
file_system: Arc::clone(&LOCAL_FS),
plugin_identity: None,
plugin_namespace: None,
plugin_root: None,
discovery_mode: SkillDiscoveryMode::Recursive,
});
// `$HOME/.agents/skills` (user-installed skills).
if let Some(home_dir) = home_dir {
roots.push(SkillRoot {
path: home_dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME),
scope: SkillScope::User,
file_system: Arc::clone(&LOCAL_FS),
plugin_identity: None,
plugin_namespace: None,
plugin_root: None,
discovery_mode: SkillDiscoveryMode::Recursive,
});
}
// Embedded system skills are cached under `$CODEX_HOME/skills/.system` and are a
// special case (not a config layer).
roots.push(SkillRoot {
path: system_cache_root_dir(&config_folder),
scope: SkillScope::System,
file_system: Arc::clone(&LOCAL_FS),
plugin_identity: None,
plugin_namespace: None,
plugin_root: None,
discovery_mode: SkillDiscoveryMode::Recursive,
});
}
ConfigLayerSource::System { .. } => {
// The system config layer lives under `/etc/codex/` on Unix, so treat
// `/etc/codex/skills` as admin-scoped skills.
roots.push(SkillRoot {
path: config_folder.join(SKILLS_DIR_NAME),
scope: SkillScope::Admin,
file_system: Arc::clone(&LOCAL_FS),
plugin_identity: None,
plugin_namespace: None,
plugin_root: None,
discovery_mode: SkillDiscoveryMode::Recursive,
});
}
ConfigLayerSource::Mdm { .. }
| ConfigLayerSource::EnterpriseManaged { .. }
| ConfigLayerSource::SessionFlags
| ConfigLayerSource::LegacyManagedConfigTomlFromFile { .. }
| ConfigLayerSource::LegacyManagedConfigTomlFromMdm => {}
}
}
roots
}
async fn repo_agents_skill_roots(
fs: Option<Arc<dyn ExecutorFileSystem>>,
config_layer_stack: &ConfigLayerStack,
cwd: &AbsolutePathBuf,
) -> Vec<SkillRoot> {
let Some(fs) = fs else {
return Vec::new();
};
let project_root_markers = project_root_markers_from_stack(config_layer_stack);
let project_root = find_project_root(fs.as_ref(), cwd, &project_root_markers).await;
let dirs = dirs_between_project_root_and_cwd(cwd, &project_root);
let mut roots = Vec::new();
let mut results = futures::stream::iter(dirs)
.map(|dir| {
let fs = Arc::clone(&fs);
async move {
let agents_skills = dir.join(AGENTS_DIR_NAME).join(SKILLS_DIR_NAME);
let agents_skills_uri = PathUri::from_abs_path(&agents_skills);
let result = fs.get_metadata(&agents_skills_uri, /*sandbox*/ None).await;
(agents_skills, result)
}
})
.buffered(MAX_CONCURRENT_ANCESTOR_PROBES);
while let Some((agents_skills, result)) = results.next().await {
match result {
Ok(metadata) if metadata.is_directory => roots.push(SkillRoot {
path: agents_skills,
scope: SkillScope::Repo,
file_system: Arc::clone(&fs),
plugin_identity: None,
plugin_namespace: None,
plugin_root: None,
discovery_mode: SkillDiscoveryMode::Recursive,
}),
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
tracing::warn!(
"failed to stat repo skills root {}: {err:#}",
agents_skills.display()
);
}
}
}
roots
}
fn project_root_markers_from_stack(config_layer_stack: &ConfigLayerStack) -> Vec<String> {
let mut merged = TomlValue::Table(toml::map::Map::new());
for layer in config_layer_stack.layers_low_to_high() {
if matches!(layer.name, ConfigLayerSource::Project { .. }) {
continue;
}
merge_toml_values(&mut merged, &layer.config);
}
match project_root_markers_from_config(&merged) {
Ok(Some(markers)) => markers,
Ok(None) => default_project_root_markers(),
Err(err) => {
tracing::warn!("invalid project_root_markers: {err}");
default_project_root_markers()
}
}
}
async fn find_project_root(
fs: &dyn ExecutorFileSystem,
cwd: &AbsolutePathBuf,
project_root_markers: &[String],
) -> AbsolutePathBuf {
if project_root_markers.is_empty() {
return cwd.clone();
}
let mut probes = Vec::new();
for ancestor in cwd.ancestors() {
for marker in project_root_markers {
let marker_path = ancestor.join(marker);
probes.push((ancestor.clone(), marker_path));
}
}
let mut results = futures::stream::iter(probes)
.map(|(ancestor, marker_path)| async move {
let marker_path_uri = PathUri::from_abs_path(&marker_path);
let result = fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await;
(ancestor, marker_path, result)
})
.buffered(MAX_CONCURRENT_ANCESTOR_PROBES);
while let Some((ancestor, marker_path, result)) = results.next().await {
match result {
Ok(_) => return ancestor,
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
tracing::warn!(
"failed to stat project root marker {}: {err:#}",
marker_path.display()
);
}
}
}
cwd.clone()
}
fn dirs_between_project_root_and_cwd(
cwd: &AbsolutePathBuf,
project_root: &AbsolutePathBuf,
) -> Vec<AbsolutePathBuf> {
let mut dirs = cwd
.ancestors()
.scan(false, |done, dir| {
if *done {
None
} else {
if &dir == project_root {
*done = true;
}
Some(dir)
}
})
.collect::<Vec<_>>();
dirs.reverse();
dirs
}
fn dedupe_skill_roots_by_path(roots: &mut Vec<SkillRoot>) {
let mut seen: HashSet<AbsolutePathBuf> = HashSet::new();
roots.retain(|root| seen.insert(root.path.clone()));
}
async fn canonicalize_for_skill_identity(
fs: &dyn ExecutorFileSystem,
path: &AbsolutePathBuf,
@@ -889,24 +602,6 @@ fn resolve_required_str(
resolve_str(Some(value), max_len, field)
}
#[cfg(test)]
pub(crate) async fn skill_roots_from_layer_stack(
fs: Arc<dyn ExecutorFileSystem>,
config_layer_stack: &ConfigLayerStack,
cwd: &AbsolutePathBuf,
home_dir: Option<&AbsolutePathBuf>,
) -> Vec<SkillRoot> {
skill_roots_with_home_dir(
Some(fs),
config_layer_stack,
cwd,
home_dir,
Vec::new(),
Vec::new(),
)
.await
}
#[cfg(test)]
#[path = "loader_tests.rs"]
mod tests;
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
pub(crate) use codex_skills::system_cache_root_dir;