Skip to main content

vtcode_core/skills/
system.rs

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