Skip to main content

vtcode_skills/
system.rs

1use include_dir::Dir;
2use std::fs;
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5use vtcode_commons::fs::{ensure_dir_exists_sync, read_file_with_context_sync, write_file_with_context_sync};
6use vtcode_commons::utils::calculate_sha256;
7
8const SYSTEM_SKILLS_DIR: Dir = include_dir::include_dir!("$CARGO_MANIFEST_DIR/src/skills/assets/samples");
9
10const SYSTEM_SKILLS_DIR_NAME: &str = ".system";
11const SKILLS_DIR_NAME: &str = "skills";
12const SYSTEM_SKILLS_MARKER_FILENAME: &str = ".codex-system-skills.marker";
13/// Bump this version to force reinstallation of system skills.
14/// v2: Updated skills with Codex-compatible patterns (XML wrapping, workflows, scripts).
15const SYSTEM_SKILLS_MARKER_SALT: &str = "v2";
16
17/// Returns the on-disk cache location for embedded system skills.
18///
19/// This is typically located at `CODEX_HOME/skills/.system`.
20pub fn system_cache_root_dir(codex_home: &Path) -> PathBuf {
21    codex_home.join(SKILLS_DIR_NAME).join(SYSTEM_SKILLS_DIR_NAME)
22}
23
24/// Installs embedded system skills into `CODEX_HOME/skills/.system`.
25///
26/// Clears any existing system skills directory first and then writes the embedded
27/// skills directory into place.
28///
29/// To avoid doing unnecessary work on every startup, a marker file is written
30/// with a fingerprint of the embedded directory. When the marker matches, the
31/// install is skipped.
32pub fn install_system_skills(codex_home: &Path) -> Result<(), SystemSkillsError> {
33    let skills_root_dir = codex_home.join(SKILLS_DIR_NAME);
34    ensure_dir_exists_sync(&skills_root_dir)
35        .map_err(|source| SystemSkillsError::io("create skills root dir", anyhow_to_io(source)))?;
36
37    let dest_system = system_cache_root_dir(codex_home);
38
39    let marker_path = dest_system.join(SYSTEM_SKILLS_MARKER_FILENAME);
40    let expected_fingerprint = embedded_system_skills_fingerprint();
41    if dest_system.is_dir() && read_marker(&marker_path).is_ok_and(|marker| marker == expected_fingerprint) {
42        return Ok(());
43    }
44
45    if dest_system.exists() {
46        fs::remove_dir_all(&dest_system)
47            .map_err(|source| SystemSkillsError::io("remove existing system skills dir", source))?;
48    }
49
50    write_embedded_dir(&SYSTEM_SKILLS_DIR, &dest_system)?;
51    write_file_with_context_sync(&marker_path, &format!("{expected_fingerprint}\n"), "system skills marker")
52        .map_err(|source| SystemSkillsError::io("write system skills marker", anyhow_to_io(source)))?;
53    Ok(())
54}
55
56pub fn uninstall_system_skills(codex_home: &Path) {
57    let _ = fs::remove_dir_all(system_cache_root_dir(codex_home));
58}
59
60fn anyhow_to_io(err: anyhow::Error) -> std::io::Error {
61    std::io::Error::other(err.to_string())
62}
63
64fn read_marker(path: &Path) -> Result<String, SystemSkillsError> {
65    Ok(read_file_with_context_sync(path, "system skills marker")
66        .map_err(|source| SystemSkillsError::io("read system skills marker", anyhow_to_io(source)))?
67        .trim()
68        .to_string())
69}
70
71fn stable_manifest_fingerprint<'a, I>(salt: &str, items: I) -> String
72where
73    I: IntoIterator<Item = (&'a str, Option<&'a str>)>,
74{
75    let mut manifest = String::new();
76    manifest.push_str("salt\t");
77    manifest.push_str(salt);
78    manifest.push('\n');
79
80    for (path, contents_hash) in items {
81        manifest.push_str(path);
82        manifest.push('\t');
83        manifest.push_str(contents_hash.unwrap_or("<dir>"));
84        manifest.push('\n');
85    }
86
87    calculate_sha256(manifest.as_bytes())
88}
89
90fn embedded_system_skills_fingerprint() -> String {
91    let mut items: Vec<(String, Option<String>)> = SYSTEM_SKILLS_DIR
92        .entries()
93        .iter()
94        .map(|entry| match entry {
95            include_dir::DirEntry::Dir(dir) => (dir.path().to_string_lossy().to_string(), None),
96            include_dir::DirEntry::File(file) => {
97                (file.path().to_string_lossy().to_string(), Some(calculate_sha256(file.contents())))
98            }
99        })
100        .collect();
101    items.sort_unstable_by(|(a, _), (b, _)| a.cmp(b));
102
103    stable_manifest_fingerprint(
104        SYSTEM_SKILLS_MARKER_SALT,
105        items
106            .iter()
107            .map(|(path, contents_hash)| (path.as_str(), contents_hash.as_deref())),
108    )
109}
110
111/// Writes the embedded `include_dir::Dir` to disk under `dest`.
112///
113/// Preserves the embedded directory structure.
114fn write_embedded_dir(dir: &Dir<'_>, dest: &Path) -> Result<(), SystemSkillsError> {
115    ensure_dir_exists_sync(dest)
116        .map_err(|source| SystemSkillsError::io("create system skills dir", anyhow_to_io(source)))?;
117
118    for entry in dir.entries() {
119        match entry {
120            include_dir::DirEntry::Dir(subdir) => {
121                let subdir_dest = dest.join(subdir.path());
122                ensure_dir_exists_sync(&subdir_dest)
123                    .map_err(|source| SystemSkillsError::io("create system skills subdir", anyhow_to_io(source)))?;
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.parent() {
129                    ensure_dir_exists_sync(parent).map_err(|source| {
130                        SystemSkillsError::io("create system skills file parent", anyhow_to_io(source))
131                    })?;
132                }
133                let contents = std::str::from_utf8(file.contents())
134                    .map_err(|source| SystemSkillsError::Utf8 { path: file.path().to_path_buf(), source })?;
135                write_file_with_context_sync(&path, contents, "system skill file")
136                    .map_err(|source| SystemSkillsError::io("write system skill file", anyhow_to_io(source)))?;
137            }
138        }
139    }
140
141    Ok(())
142}
143
144#[derive(Debug, Error)]
145pub enum SystemSkillsError {
146    #[error("io error while {action}: {source}")]
147    Io {
148        action: &'static str,
149        #[source]
150        source: std::io::Error,
151    },
152    #[error("embedded system skill {path} is not valid UTF-8: {source}")]
153    Utf8 {
154        path: PathBuf,
155        #[source]
156        source: std::str::Utf8Error,
157    },
158}
159
160impl SystemSkillsError {
161    fn io(action: &'static str, source: std::io::Error) -> Self {
162        Self::Io { action, source }
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::{SYSTEM_SKILLS_MARKER_SALT, embedded_system_skills_fingerprint, stable_manifest_fingerprint};
169
170    #[test]
171    fn stable_manifest_fingerprint_matches_known_digest() {
172        let fingerprint = stable_manifest_fingerprint(
173            SYSTEM_SKILLS_MARKER_SALT,
174            [
175                ("alpha", None),
176                ("alpha/config.json", Some("77c7ce9a5d86bb386d443bb96390dcf0ecf6afb7b74f84a88d64e6d4e8dcb5e0")),
177                ("beta.md", Some("3f89f6a04a1a1f8cb46fc4b356f7b81b8d18102fdb8b795f5b2f89e7cfefb3af")),
178            ],
179        );
180
181        assert_eq!(fingerprint, "218ca69badb47208804e293074956e23ed68d6946dde5d36e6fe8d56df170170");
182    }
183
184    #[test]
185    fn embedded_system_skills_fingerprint_uses_full_sha256_digest() {
186        assert_eq!(embedded_system_skills_fingerprint().len(), 64);
187    }
188}