diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index dc2492995f..0aec959b9a 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/features.rs b/codex-rs/core/src/features.rs index 98cfca74a3..22fd310b99 100644 --- a/codex-rs/core/src/features.rs +++ b/codex-rs/core/src/features.rs @@ -395,7 +395,7 @@ pub const FEATURES: &[FeatureSpec] = &[ id: Feature::Skills, key: "skills", stage: Stage::Experimental, - default_enabled: false, + default_enabled: true, }, FeatureSpec { id: Feature::Tui2, diff --git a/codex-rs/core/src/skills/assets/samples/plan/SKILL.md b/codex-rs/core/src/skills/assets/samples/plan/SKILL.md index a515fa659d..5d49c33945 100644 --- a/codex-rs/core/src/skills/assets/samples/plan/SKILL.md +++ b/codex-rs/core/src/skills/assets/samples/plan/SKILL.md @@ -2,7 +2,7 @@ name: plan description: Generate a plan for how an agent should accomplish a complex coding task. Use when a user asks for a plan, and optionally when they want to save, find, read, update, or delete plan files in $CODEX_HOME/plans (default ~/.codex/plans). metadata: - short-description: Create and manage plan markdown files under $CODEX_HOME/plans. + short-description: Generate a plan for a complex task --- # Plan diff --git a/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md b/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md index 23836e5d85..f061c96e3b 100644 --- a/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md +++ b/codex-rs/core/src/skills/assets/samples/skill-creator/SKILL.md @@ -1,6 +1,8 @@ --- name: skill-creator description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. +metadata: + short-description: Create or update a skill --- # Skill Creator diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index ca330a0e5e..bce13fbb05 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()); @@ -140,21 +148,28 @@ pub(crate) fn repo_skills_root(cwd: &Path) -> Option { }) } -fn skill_roots(config: &Config) -> Vec { +pub(crate) fn skill_roots_for_cwd(codex_home: &Path, cwd: &Path) -> Vec { let mut roots = Vec::new(); - if let Some(repo_root) = repo_skills_root(&config.cwd) { + if let Some(repo_root) = repo_skills_root(cwd) { roots.push(repo_root); } // Load order matters: we dedupe by name, keeping the first occurrence. - // This makes repo/user skills win over system skills. - roots.push(user_skills_root(&config.codex_home)); - roots.push(system_skills_root(&config.codex_home)); + // Priority order: repo, user, system, then admin. + roots.push(user_skills_root(codex_home)); + roots.push(system_skills_root(codex_home)); + if cfg!(unix) { + roots.push(admin_skills_root()); + } roots } +fn skill_roots(config: &Config) -> Vec { + skill_roots_for_cwd(&config.codex_home, &config.cwd) +} + fn discover_skills_under_root(root: &Path, scope: SkillScope, outcome: &mut SkillLoadOutcome) { let Ok(root) = normalize_path(root) else { return; @@ -622,7 +637,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 +779,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/core/src/skills/manager.rs b/codex-rs/core/src/skills/manager.rs index 5ce174e4f7..8cc93d05bc 100644 --- a/codex-rs/core/src/skills/manager.rs +++ b/codex-rs/core/src/skills/manager.rs @@ -5,9 +5,7 @@ use std::sync::RwLock; use crate::skills::SkillLoadOutcome; use crate::skills::loader::load_skills_from_roots; -use crate::skills::loader::repo_skills_root; -use crate::skills::loader::system_skills_root; -use crate::skills::loader::user_skills_root; +use crate::skills::loader::skill_roots_for_cwd; use crate::skills::system::install_system_skills; pub struct SkillsManager { codex_home: PathBuf, @@ -39,12 +37,7 @@ impl SkillsManager { return outcome; } - let mut roots = Vec::new(); - if let Some(repo_root) = repo_skills_root(cwd) { - roots.push(repo_root); - } - roots.push(user_skills_root(&self.codex_home)); - roots.push(system_skills_root(&self.codex_home)); + let roots = skill_roots_for_cwd(&self.codex_home, cwd); let outcome = load_skills_from_roots(roots); match self.cache_by_cwd.write() { Ok(mut cache) => { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6417e1bce7..1e03f5ce11 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)] diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 377b34175e..fe96b5f970 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1284,9 +1284,9 @@ async fn unified_exec_end_after_task_complete_is_suppressed() { ); } -#[test] -fn unified_exec_waiting_multiple_empty_snapshots() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); +#[tokio::test] +async fn unified_exec_waiting_multiple_empty_snapshots() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; begin_unified_exec_startup(&mut chat, "call-wait-1", "proc-1", "just fix"); terminal_interaction(&mut chat, "call-wait-1a", "proc-1", ""); @@ -1311,9 +1311,9 @@ fn unified_exec_waiting_multiple_empty_snapshots() { assert_snapshot!("unified_exec_waiting_multiple_empty_after", combined); } -#[test] -fn unified_exec_empty_then_non_empty_snapshot() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); +#[tokio::test] +async fn unified_exec_empty_then_non_empty_snapshot() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; begin_unified_exec_startup(&mut chat, "call-wait-2", "proc-2", "just fix"); terminal_interaction(&mut chat, "call-wait-2a", "proc-2", ""); @@ -1327,9 +1327,9 @@ fn unified_exec_empty_then_non_empty_snapshot() { assert_snapshot!("unified_exec_empty_then_non_empty_after", combined); } -#[test] -fn unified_exec_non_empty_then_empty_snapshots() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None); +#[tokio::test] +async fn unified_exec_non_empty_then_empty_snapshots() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; begin_unified_exec_startup(&mut chat, "call-wait-3", "proc-3", "just fix"); terminal_interaction(&mut chat, "call-wait-3a", "proc-3", "pwd\n");