Support admin scope skills. (#8296)

a new scope reads from /etc/codex
This commit is contained in:
xl-openai
2025-12-18 18:28:56 -08:00
committed by GitHub
parent d35337227a
commit 8120c8765b
3 changed files with 61 additions and 2 deletions

View File

@@ -1081,6 +1081,7 @@ pub enum SkillScope {
User,
Repo,
System,
Admin,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -1131,6 +1132,7 @@ impl From<CoreSkillScope> for SkillScope {
CoreSkillScope::User => Self::User,
CoreSkillScope::Repo => Self::Repo,
CoreSkillScope::System => Self::System,
CoreSkillScope::Admin => Self::Admin,
}
}
}

View File

@@ -33,6 +33,7 @@ struct SkillFrontmatterMetadata {
const SKILLS_FILENAME: &str = "SKILL.md";
const SKILLS_DIR_NAME: &str = "skills";
const REPO_ROOT_CONFIG_DIR_NAME: &str = ".codex";
const ADMIN_SKILLS_ROOT: &str = "/etc/codex/skills";
const MAX_NAME_LEN: usize = 64;
const MAX_DESCRIPTION_LEN: usize = 1024;
const MAX_SHORT_DESCRIPTION_LEN: usize = MAX_DESCRIPTION_LEN;
@@ -108,6 +109,13 @@ pub(crate) fn system_skills_root(codex_home: &Path) -> SkillRoot {
}
}
pub(crate) fn admin_skills_root() -> SkillRoot {
SkillRoot {
path: PathBuf::from(ADMIN_SKILLS_ROOT),
scope: SkillScope::Admin,
}
}
pub(crate) fn repo_skills_root(cwd: &Path) -> Option<SkillRoot> {
let base = if cwd.is_dir() { cwd } else { cwd.parent()? };
let base = normalize_path(base).unwrap_or_else(|_| base.to_path_buf());
@@ -148,9 +156,12 @@ fn skill_roots(config: &Config) -> Vec<SkillRoot> {
}
// Load order matters: we dedupe by name, keeping the first occurrence.
// This makes repo/user skills win over system skills.
// Priority order: repo, user, system, then admin.
roots.push(user_skills_root(&config.codex_home));
roots.push(system_skills_root(&config.codex_home));
if cfg!(unix) {
roots.push(admin_skills_root());
}
roots
}
@@ -622,7 +633,7 @@ mod tests {
}
#[tokio::test]
async fn loads_system_skills_with_lowest_priority() {
async fn loads_system_skills_when_present() {
let codex_home = tempfile::tempdir().expect("tempdir");
write_system_skill(&codex_home, "system", "dupe-skill", "from system");
@@ -764,6 +775,51 @@ mod tests {
assert_eq!(outcome.skills[0].scope, SkillScope::System);
}
#[tokio::test]
async fn skill_roots_include_admin_with_lowest_priority_on_unix() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cfg = make_config(&codex_home).await;
let scopes: Vec<SkillScope> = skill_roots(&cfg)
.into_iter()
.map(|root| root.scope)
.collect();
let mut expected = vec![SkillScope::User, SkillScope::System];
if cfg!(unix) {
expected.push(SkillScope::Admin);
}
assert_eq!(scopes, expected);
}
#[tokio::test]
async fn deduplicates_by_name_preferring_system_over_admin() {
let system_dir = tempfile::tempdir().expect("tempdir");
let admin_dir = tempfile::tempdir().expect("tempdir");
write_skill_at(system_dir.path(), "system", "dupe-skill", "from system");
write_skill_at(admin_dir.path(), "admin", "dupe-skill", "from admin");
let outcome = load_skills_from_roots([
SkillRoot {
path: system_dir.path().to_path_buf(),
scope: SkillScope::System,
},
SkillRoot {
path: admin_dir.path().to_path_buf(),
scope: SkillScope::Admin,
},
]);
assert!(
outcome.errors.is_empty(),
"unexpected errors: {:?}",
outcome.errors
);
assert_eq!(outcome.skills.len(), 1);
assert_eq!(outcome.skills[0].name, "dupe-skill");
assert_eq!(outcome.skills[0].scope, SkillScope::System);
}
#[tokio::test]
async fn deduplicates_by_name_preferring_user_over_system() {
let codex_home = tempfile::tempdir().expect("tempdir");

View File

@@ -1721,6 +1721,7 @@ pub enum SkillScope {
User,
Repo,
System,
Admin,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]