vtcode_core/skills/
system.rs1use 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";
16const SYSTEM_SKILLS_MARKER_SALT: &str = "v2";
19
20pub(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
29pub(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
123fn 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}