codex/capabilities/
cache.rs1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::env;
4use std::ffi::OsString;
5use std::fs as std_fs;
6use std::path::{Path, PathBuf};
7use std::sync::{Mutex, OnceLock};
8use std::time::SystemTime;
9
10#[cfg(unix)]
11use std::os::unix::fs::PermissionsExt;
12
13use super::{
14 CapabilityFeatureOverrides, CapabilityOverrides, CapabilityProbeStep, CodexCapabilities,
15 CodexFeatureFlags,
16};
17
18#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
20pub enum CapabilityCachePolicy {
21 #[default]
24 PreferCache,
25 Refresh,
27 Bypass,
29}
30
31#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
36pub struct CapabilityCacheKey {
37 pub binary_path: PathBuf,
39}
40
41#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
43pub struct BinaryFingerprint {
44 pub canonical_path: Option<PathBuf>,
46 pub modified: Option<SystemTime>,
48 pub len: Option<u64>,
50}
51
52const PATH_ENV: &str = "PATH";
53
54pub(crate) fn capability_cache() -> &'static Mutex<HashMap<CapabilityCacheKey, CodexCapabilities>> {
55 static CACHE: OnceLock<Mutex<HashMap<CapabilityCacheKey, CodexCapabilities>>> = OnceLock::new();
56 CACHE.get_or_init(|| Mutex::new(HashMap::new()))
57}
58
59pub(crate) fn effective_path_env(env_overrides: &[(String, String)]) -> Option<OsString> {
60 env_overrides
61 .iter()
62 .rev()
63 .find_map(|(key, value)| path_env_key_matches(key).then(|| OsString::from(value)))
64 .or_else(|| env::var_os(PATH_ENV))
65}
66
67#[cfg(windows)]
68fn path_env_key_matches(key: &str) -> bool {
69 key.eq_ignore_ascii_case(PATH_ENV)
70}
71
72#[cfg(not(windows))]
73fn path_env_key_matches(key: &str) -> bool {
74 key == PATH_ENV
75}
76
77pub(crate) fn resolve_binary_path(
78 binary: &Path,
79 current_dir: Option<&Path>,
80 path_env: Option<OsString>,
81) -> PathBuf {
82 if is_path_qualified(binary) {
83 return resolve_path_qualified_binary(binary.to_path_buf(), current_dir);
84 }
85
86 find_binary_on_path(binary, path_env, current_dir).unwrap_or_else(|| binary.to_path_buf())
87}
88
89pub(crate) fn capability_cache_key(binary: &Path) -> CapabilityCacheKey {
90 capability_cache_key_for_current_dir(binary, None)
91}
92
93pub(crate) fn capability_cache_key_for_current_dir(
94 binary: &Path,
95 current_dir: Option<&Path>,
96) -> CapabilityCacheKey {
97 capability_cache_key_for_current_dir_with_env(binary, current_dir, &[])
98}
99
100pub(crate) fn capability_cache_key_for_current_dir_with_env(
101 binary: &Path,
102 current_dir: Option<&Path>,
103 env_overrides: &[(String, String)],
104) -> CapabilityCacheKey {
105 let resolved = resolve_binary_path(binary, current_dir, effective_path_env(env_overrides));
106 let canonical = std_fs::canonicalize(&resolved).unwrap_or(resolved);
107 CapabilityCacheKey {
108 binary_path: canonical,
109 }
110}
111
112fn is_path_qualified(path: &Path) -> bool {
113 path.is_absolute()
114 || path
115 .parent()
116 .is_some_and(|parent| !parent.as_os_str().is_empty())
117}
118
119fn resolve_path_qualified_binary(binary_path: PathBuf, current_dir: Option<&Path>) -> PathBuf {
120 if binary_path.is_absolute() {
121 return binary_path;
122 }
123
124 let joined = effective_base_dir(current_dir)
125 .map(|cwd| cwd.join(&binary_path))
126 .unwrap_or(binary_path);
127
128 std_fs::canonicalize(&joined).unwrap_or(joined)
129}
130
131fn effective_base_dir(base_dir: Option<&Path>) -> Option<PathBuf> {
132 match base_dir {
133 Some(path) if path.is_absolute() => Some(path.to_path_buf()),
134 Some(path) => env::current_dir().ok().map(|cwd| cwd.join(path)),
135 None => env::current_dir().ok(),
136 }
137}
138
139fn find_binary_on_path(
140 binary_name: &Path,
141 path_env: Option<OsString>,
142 current_dir: Option<&Path>,
143) -> Option<PathBuf> {
144 let path_env = path_env?;
145 let effective_cwd = effective_base_dir(current_dir);
146 env::split_paths(&path_env)
147 .find_map(|directory| {
148 let search_dir = if directory.is_absolute() {
149 directory
150 } else if let Some(cwd) = effective_cwd.as_deref() {
151 cwd.join(directory)
152 } else {
153 directory
154 };
155 candidate_binary_path(&search_dir, binary_name)
156 })
157 .map(|candidate| std_fs::canonicalize(&candidate).unwrap_or(candidate))
158}
159
160fn candidate_binary_path(directory: &Path, binary_name: &Path) -> Option<PathBuf> {
161 let candidate = directory.join(binary_name);
162 if is_runnable_path_candidate(&candidate) {
163 return Some(candidate);
164 }
165
166 #[cfg(windows)]
167 {
168 if candidate.extension().is_some() {
169 return None;
170 }
171
172 let pathext =
173 env::var_os("PATHEXT").unwrap_or_else(|| OsString::from(".COM;.EXE;.BAT;.CMD"));
174 for extension in pathext.to_string_lossy().split(';') {
175 let extension = extension.trim();
176 if extension.is_empty() {
177 continue;
178 }
179
180 let suffixed = candidate.with_extension(extension.trim_start_matches('.'));
181 if is_runnable_path_candidate(&suffixed) {
182 return Some(suffixed);
183 }
184 }
185 }
186
187 None
188}
189
190fn is_runnable_path_candidate(candidate: &Path) -> bool {
191 let Ok(metadata) = std_fs::metadata(candidate) else {
192 return false;
193 };
194
195 if !metadata.is_file() {
196 return false;
197 }
198
199 #[cfg(unix)]
200 {
201 metadata.permissions().mode() & 0o111 != 0
202 }
203
204 #[cfg(not(unix))]
205 {
206 true
207 }
208}
209
210pub(crate) fn has_fingerprint_metadata(fingerprint: &Option<BinaryFingerprint>) -> bool {
211 fingerprint.is_some()
212}
213
214pub(crate) fn cached_capabilities(
215 key: &CapabilityCacheKey,
216 fingerprint: &Option<BinaryFingerprint>,
217) -> Option<CodexCapabilities> {
218 let cache = capability_cache().lock().ok()?;
219 let cached = cache.get(key)?;
220 if !has_fingerprint_metadata(&cached.fingerprint) || !has_fingerprint_metadata(fingerprint) {
221 return None;
222 }
223 if fingerprints_match(&cached.fingerprint, fingerprint) {
224 Some(cached.clone())
225 } else {
226 None
227 }
228}
229
230pub(crate) fn update_capability_cache(capabilities: CodexCapabilities) {
231 if !has_fingerprint_metadata(&capabilities.fingerprint) {
232 return;
233 }
234 if let Ok(mut cache) = capability_cache().lock() {
235 cache.insert(capabilities.cache_key.clone(), capabilities);
236 }
237}
238
239pub fn capability_cache_entries() -> Vec<CodexCapabilities> {
241 capability_cache()
242 .lock()
243 .map(|cache| cache.values().cloned().collect())
244 .unwrap_or_default()
245}
246
247pub fn capability_cache_entry(binary: &Path) -> Option<CodexCapabilities> {
249 let key = capability_cache_key(binary);
250 capability_cache()
251 .lock()
252 .ok()
253 .and_then(|cache| cache.get(&key).cloned())
254}
255
256pub fn clear_capability_cache_entry(binary: &Path) -> bool {
258 let key = capability_cache_key(binary);
259 capability_cache()
260 .lock()
261 .ok()
262 .map(|mut cache| cache.remove(&key).is_some())
263 .unwrap_or(false)
264}
265
266pub fn clear_capability_cache() {
268 if let Ok(mut cache) = capability_cache().lock() {
269 cache.clear();
270 }
271}
272
273pub(crate) fn current_fingerprint(key: &CapabilityCacheKey) -> Option<BinaryFingerprint> {
274 let canonical = std_fs::canonicalize(&key.binary_path).ok();
275 let metadata_path = canonical.as_deref().unwrap_or(key.binary_path.as_path());
276 let metadata = std_fs::metadata(metadata_path).ok()?;
277 Some(BinaryFingerprint {
278 canonical_path: canonical,
279 modified: metadata.modified().ok(),
280 len: Some(metadata.len()),
281 })
282}
283
284pub(crate) fn fingerprints_match(
285 cached: &Option<BinaryFingerprint>,
286 fresh: &Option<BinaryFingerprint>,
287) -> bool {
288 cached == fresh
289}
290
291pub(crate) fn finalize_capabilities_with_overrides(
292 mut capabilities: CodexCapabilities,
293 overrides: &CapabilityOverrides,
294 cache_key: CapabilityCacheKey,
295 fingerprint: Option<BinaryFingerprint>,
296 manual_source: bool,
297) -> CodexCapabilities {
298 capabilities.cache_key = cache_key;
299 capabilities.fingerprint = fingerprint;
300
301 let mut applied = manual_source;
302
303 if let Some(version) = overrides.version.clone() {
304 capabilities.version = Some(version);
305 applied = true;
306 }
307
308 if apply_feature_overrides(&mut capabilities.features, &overrides.features) {
309 applied = true;
310 }
311
312 if applied
313 && !capabilities
314 .probe_plan
315 .steps
316 .contains(&CapabilityProbeStep::ManualOverride)
317 {
318 capabilities
319 .probe_plan
320 .steps
321 .push(CapabilityProbeStep::ManualOverride);
322 }
323
324 capabilities
325}
326
327fn apply_feature_overrides(
328 features: &mut CodexFeatureFlags,
329 overrides: &CapabilityFeatureOverrides,
330) -> bool {
331 let mut applied = false;
332
333 if let Some(value) = overrides.supports_features_list {
334 features.supports_features_list = value;
335 applied = true;
336 }
337
338 if let Some(value) = overrides.supports_output_schema {
339 features.supports_output_schema = value;
340 applied = true;
341 }
342
343 if let Some(value) = overrides.supports_add_dir {
344 features.supports_add_dir = value;
345 applied = true;
346 }
347
348 if let Some(value) = overrides.supports_mcp_login {
349 features.supports_mcp_login = value;
350 applied = true;
351 }
352
353 applied
354}