Compare commits

...

1 Commits

Author SHA1 Message Date
Prabhat Agarwal
2a30816095 Add skills.roots config for explicit skill roots
Add skills.roots to config so users can specify absolute skill folders.
Use configured roots as the local root set when provided; otherwise keep default local roots.
Add loader/config tests and regenerate the committed config schema fixture.

Co-authored-by: Codex <noreply@openai.com>
2026-03-21 09:09:42 -07:00
5 changed files with 99 additions and 2 deletions

View File

@@ -1545,6 +1545,13 @@
"$ref": "#/definitions/SkillConfig"
},
"type": "array"
},
"roots": {
"description": "Additional absolute skill root directories to scan for `SKILL.md`.\n\nWhen non-empty, these roots override default local skill roots.",
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"type": "object"

View File

@@ -176,6 +176,7 @@ enabled = false
cfg.skills,
Some(SkillsConfig {
bundled: Some(BundledSkillsConfig { enabled: false }),
roots: Vec::new(),
config: Vec::new(),
})
);

View File

@@ -821,6 +821,12 @@ pub struct SkillsConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bundled: Option<BundledSkillsConfig>,
/// Additional absolute skill root directories to scan for `SKILL.md`.
///
/// When non-empty, these roots override default local skill roots.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub roots: Vec<AbsolutePathBuf>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<SkillConfig>,
}

View File

@@ -3,6 +3,7 @@ use crate::config_loader::ConfigLayerStackOrdering;
use crate::config_loader::default_project_root_markers;
use crate::config_loader::merge_toml_values;
use crate::config_loader::project_root_markers_from_config;
use crate::config::types::SkillsConfig;
use crate::plugins::plugin_namespace_for_skill_path;
use crate::skills::model::SkillDependencies;
use crate::skills::model::SkillError;
@@ -34,6 +35,7 @@ use std::path::Path;
use std::path::PathBuf;
use toml::Value as TomlValue;
use tracing::error;
use tracing::warn;
#[cfg(test)]
use crate::config::Config;
@@ -234,16 +236,49 @@ fn skill_roots_with_home_dir(
home_dir: Option<&Path>,
plugin_skill_roots: Vec<PathBuf>,
) -> Vec<SkillRoot> {
let mut roots = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir);
let configured_roots = configured_skill_roots_from_stack(config_layer_stack);
let mut roots = if configured_roots.is_empty() {
let mut defaults = skill_roots_from_layer_stack_inner(config_layer_stack, home_dir);
defaults.extend(repo_agents_skill_roots(config_layer_stack, cwd));
defaults
} else {
configured_roots
};
roots.extend(plugin_skill_roots.into_iter().map(|path| SkillRoot {
path,
scope: SkillScope::User,
}));
roots.extend(repo_agents_skill_roots(config_layer_stack, cwd));
dedupe_skill_roots_by_path(&mut roots);
roots
}
fn configured_skill_roots_from_stack(config_layer_stack: &ConfigLayerStack) -> Vec<SkillRoot> {
let effective_config = config_layer_stack.effective_config();
let Some(skills_value) = effective_config
.as_table()
.and_then(|table| table.get("skills"))
else {
return Vec::new();
};
let skills: SkillsConfig = match skills_value.clone().try_into() {
Ok(skills) => skills,
Err(err) => {
warn!("invalid skills config: {err}");
return Vec::new();
}
};
skills
.roots
.into_iter()
.map(|path| SkillRoot {
path: path.as_path().to_path_buf(),
scope: SkillScope::User,
})
.collect()
}
fn skill_roots_from_layer_stack_inner(
config_layer_stack: &ConfigLayerStack,
home_dir: Option<&Path>,

View File

@@ -198,6 +198,54 @@ fn skill_roots_from_layer_stack_includes_disabled_project_layers() -> anyhow::Re
Ok(())
}
#[test]
fn skill_roots_from_layer_stack_can_override_defaults_with_explicit_roots() -> anyhow::Result<()> {
let tmp = tempfile::tempdir()?;
let system_folder = tmp.path().join("etc/codex");
let home_folder = tmp.path().join("home");
let user_folder = home_folder.join("codex");
let explicit_root = tmp.path().join("custom-skills");
fs::create_dir_all(&system_folder)?;
fs::create_dir_all(&user_folder)?;
fs::create_dir_all(&explicit_root)?;
let system_file = AbsolutePathBuf::from_absolute_path(system_folder.join("config.toml"))?;
let user_file = AbsolutePathBuf::from_absolute_path(user_folder.join("config.toml"))?;
let mut skills_table = toml::map::Map::new();
skills_table.insert(
"roots".to_string(),
TomlValue::Array(vec![TomlValue::String(
explicit_root.to_string_lossy().to_string(),
)]),
);
let mut user_table = toml::map::Map::new();
user_table.insert("skills".to_string(), TomlValue::Table(skills_table));
let user_config = TomlValue::Table(user_table);
let layers = vec![
ConfigLayerEntry::new(
ConfigLayerSource::System { file: system_file },
TomlValue::Table(toml::map::Map::new()),
),
ConfigLayerEntry::new(ConfigLayerSource::User { file: user_file }, user_config),
];
let stack = ConfigLayerStack::new(
layers,
ConfigRequirements::default(),
ConfigRequirementsToml::default(),
)?;
let got = skill_roots_from_layer_stack(&stack, Some(&home_folder))
.into_iter()
.map(|root| (root.scope, root.path))
.collect::<Vec<_>>();
assert_eq!(got, vec![(SkillScope::User, explicit_root)]);
Ok(())
}
#[test]
fn loads_skills_from_home_agents_dir_for_user_scope() -> anyhow::Result<()> {
let tmp = tempfile::tempdir()?;