diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index dc2492995..0aec959b9 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -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 for SkillScope { CoreSkillScope::User => Self::User, CoreSkillScope::Repo => Self::Repo, CoreSkillScope::System => Self::System, + CoreSkillScope::Admin => Self::Admin, } } } diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index ca330a0e5..2a2fc0e87 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -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 { 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 { } // 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 = 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"); diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6417e1bce..1e03f5ce1 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1721,6 +1721,7 @@ pub enum SkillScope { User, Repo, System, + Admin, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]