mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
1 parent
d1fb77d692
commit
1a7519fa07
13 files changed
+1077
-1109
No files matched your search
@@ -27,7 +27,6 @@ codex-skills = { workspace = true }
|
||||
codex-utils-absolute-path = { workspace = true }
|
||||
codex-utils-path-uri = { workspace = true }
|
||||
codex-utils-plugins = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -35,10 +34,10 @@ serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
tokio = { workspace = true, features = ["fs", "macros", "rt"] }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
@@ -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;
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
pub(crate) use codex_skills::system_cache_root_dir;
|
||||
Reference in new issue
Block a user