Skip to main content

codex_skills/
lib.rs

1mod model;
2
3pub use model::EnvironmentSkillMetadata;
4pub use model::SkillConfigRule;
5pub use model::SkillConfigRuleSelector;
6pub use model::SkillConfigRules;
7pub use model::SkillDependencies;
8pub use model::SkillInterface;
9pub use model::SkillMetadata;
10pub use model::SkillPolicy;
11pub use model::SkillToolDependency;
12
13use codex_utils_absolute_path::AbsolutePathBuf;
14use include_dir::Dir;
15use std::collections::hash_map::DefaultHasher;
16use std::fs;
17use std::hash::Hash;
18use std::hash::Hasher;
19
20use thiserror::Error;
21
22const SYSTEM_SKILLS_DIR: Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/src/assets/samples");
23
24const SYSTEM_SKILLS_DIR_NAME: &str = ".system";
25const SKILLS_DIR_NAME: &str = "skills";
26const SYSTEM_SKILLS_MARKER_FILENAME: &str = ".codex-system-skills.marker";
27const SYSTEM_SKILLS_MARKER_SALT: &str = "v1";
28
29/// Returns the on-disk cache location for embedded system skills from an absolute CODEX_HOME.
30pub fn system_cache_root_dir(codex_home: &AbsolutePathBuf) -> AbsolutePathBuf {
31    codex_home
32        .join(SKILLS_DIR_NAME)
33        .join(SYSTEM_SKILLS_DIR_NAME)
34}
35
36/// Installs embedded system skills into `CODEX_HOME/skills/.system`.
37///
38/// Clears any existing system skills directory first and then writes the embedded
39/// skills directory into place.
40///
41/// To avoid doing unnecessary work on every startup, a marker file is written
42/// with a fingerprint of the embedded directory. When the marker matches, the
43/// install is skipped.
44pub fn install_system_skills(codex_home: &AbsolutePathBuf) -> Result<(), SystemSkillsError> {
45    let skills_root_dir = codex_home.join(SKILLS_DIR_NAME);
46    fs::create_dir_all(skills_root_dir.as_path())
47        .map_err(|source| SystemSkillsError::io("create skills root dir", source))?;
48
49    let dest_system = system_cache_root_dir(codex_home);
50
51    let marker_path = dest_system.join(SYSTEM_SKILLS_MARKER_FILENAME);
52    let expected_fingerprint = embedded_system_skills_fingerprint();
53    if dest_system.as_path().is_dir()
54        && read_marker(&marker_path).is_ok_and(|marker| marker == expected_fingerprint)
55    {
56        return Ok(());
57    }
58
59    if dest_system.as_path().exists() {
60        fs::remove_dir_all(dest_system.as_path())
61            .map_err(|source| SystemSkillsError::io("remove existing system skills dir", source))?;
62    }
63
64    write_embedded_dir(&SYSTEM_SKILLS_DIR, &dest_system)?;
65    fs::write(marker_path.as_path(), format!("{expected_fingerprint}\n"))
66        .map_err(|source| SystemSkillsError::io("write system skills marker", source))?;
67    Ok(())
68}
69
70fn read_marker(path: &AbsolutePathBuf) -> Result<String, SystemSkillsError> {
71    Ok(fs::read_to_string(path.as_path())
72        .map_err(|source| SystemSkillsError::io("read system skills marker", source))?
73        .trim()
74        .to_string())
75}
76
77fn embedded_system_skills_fingerprint() -> String {
78    let mut items = Vec::new();
79    collect_fingerprint_items(&SYSTEM_SKILLS_DIR, &mut items);
80    items.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
81
82    let mut hasher = DefaultHasher::new();
83    SYSTEM_SKILLS_MARKER_SALT.hash(&mut hasher);
84    for (path, contents_hash) in items {
85        path.hash(&mut hasher);
86        contents_hash.hash(&mut hasher);
87    }
88    format!("{:x}", hasher.finish())
89}
90
91fn collect_fingerprint_items(dir: &Dir<'_>, items: &mut Vec<(String, Option<u64>)>) {
92    for entry in dir.entries() {
93        match entry {
94            include_dir::DirEntry::Dir(subdir) => {
95                items.push((subdir.path().to_string_lossy().to_string(), None));
96                collect_fingerprint_items(subdir, items);
97            }
98            include_dir::DirEntry::File(file) => {
99                let mut file_hasher = DefaultHasher::new();
100                file.contents().hash(&mut file_hasher);
101                items.push((
102                    file.path().to_string_lossy().to_string(),
103                    Some(file_hasher.finish()),
104                ));
105            }
106        }
107    }
108}
109
110/// Writes the embedded `include_dir::Dir` to disk under `dest`.
111///
112/// Preserves the embedded directory structure.
113fn write_embedded_dir(dir: &Dir<'_>, dest: &AbsolutePathBuf) -> Result<(), SystemSkillsError> {
114    fs::create_dir_all(dest.as_path())
115        .map_err(|source| SystemSkillsError::io("create system skills dir", source))?;
116
117    for entry in dir.entries() {
118        match entry {
119            include_dir::DirEntry::Dir(subdir) => {
120                let subdir_dest = dest.join(subdir.path());
121                fs::create_dir_all(subdir_dest.as_path()).map_err(|source| {
122                    SystemSkillsError::io("create system skills subdir", source)
123                })?;
124                write_embedded_dir(subdir, dest)?;
125            }
126            include_dir::DirEntry::File(file) => {
127                let path = dest.join(file.path());
128                if let Some(parent) = path.as_path().parent() {
129                    fs::create_dir_all(parent).map_err(|source| {
130                        SystemSkillsError::io("create system skills file parent", source)
131                    })?;
132                }
133                fs::write(path.as_path(), file.contents())
134                    .map_err(|source| SystemSkillsError::io("write system skill file", source))?;
135            }
136        }
137    }
138
139    Ok(())
140}
141
142#[derive(Debug, Error)]
143pub enum SystemSkillsError {
144    #[error("io error while {action}: {source}")]
145    Io {
146        action: &'static str,
147        #[source]
148        source: std::io::Error,
149    },
150}
151
152impl SystemSkillsError {
153    fn io(action: &'static str, source: std::io::Error) -> Self {
154        Self::Io { action, source }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::SYSTEM_SKILLS_DIR;
161    use super::collect_fingerprint_items;
162
163    #[test]
164    fn fingerprint_traverses_nested_entries() {
165        let mut items = Vec::new();
166        collect_fingerprint_items(&SYSTEM_SKILLS_DIR, &mut items);
167        let mut paths: Vec<String> = items.into_iter().map(|(path, _)| path).collect();
168        paths.sort_unstable();
169
170        assert!(
171            paths
172                .binary_search_by(|probe| probe.as_str().cmp("skill-creator/SKILL.md"))
173                .is_ok()
174        );
175        assert!(
176            paths
177                .binary_search_by(|probe| probe.as_str().cmp("skill-creator/scripts/init_skill.py"))
178                .is_ok()
179        );
180    }
181}