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";
13const SYSTEM_SKILLS_MARKER_SALT: &str = "v2";
16
17pub fn system_cache_root_dir(codex_home: &Path) -> PathBuf {
21 codex_home.join(SKILLS_DIR_NAME).join(SYSTEM_SKILLS_DIR_NAME)
22}
23
24pub 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
111fn 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}