mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
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>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -176,6 +176,7 @@ enabled = false
|
||||
cfg.skills,
|
||||
Some(SkillsConfig {
|
||||
bundled: Some(BundledSkillsConfig { enabled: false }),
|
||||
roots: Vec::new(),
|
||||
config: Vec::new(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
Reference in New Issue
Block a user