Skip to main content

codex/capabilities/
snapshot.rs

1use serde::{de::DeserializeOwned, Serialize};
2use std::fs as std_fs;
3use std::path::Path;
4
5use super::{
6    capability_cache_key, current_fingerprint, fingerprints_match, has_fingerprint_metadata,
7    CapabilityOverrides, CapabilitySnapshotError, CapabilitySnapshotFormat, CodexCapabilities,
8};
9
10/// Serializes a capability snapshot to a JSON or TOML string.
11pub fn serialize_capabilities_snapshot(
12    snapshot: &CodexCapabilities,
13    format: CapabilitySnapshotFormat,
14) -> Result<String, CapabilitySnapshotError> {
15    serialize_snapshot(snapshot, format)
16}
17
18/// Parses a capability snapshot from serialized JSON or TOML.
19pub fn deserialize_capabilities_snapshot(
20    input: &str,
21    format: CapabilitySnapshotFormat,
22) -> Result<CodexCapabilities, CapabilitySnapshotError> {
23    deserialize_snapshot(input, format)
24}
25
26/// Writes a capability snapshot to disk, inferring format from the file extension when absent.
27pub fn write_capabilities_snapshot(
28    path: impl AsRef<Path>,
29    snapshot: &CodexCapabilities,
30    format: Option<CapabilitySnapshotFormat>,
31) -> Result<(), CapabilitySnapshotError> {
32    let path = path.as_ref();
33    let resolved_format = resolve_snapshot_format(format, path)?;
34    let contents = serialize_capabilities_snapshot(snapshot, resolved_format)?;
35    std_fs::write(path, contents).map_err(|source| CapabilitySnapshotError::WriteSnapshot {
36        path: path.to_path_buf(),
37        source,
38    })
39}
40
41/// Loads a capability snapshot from disk, inferring format from the file extension when absent.
42pub fn read_capabilities_snapshot(
43    path: impl AsRef<Path>,
44    format: Option<CapabilitySnapshotFormat>,
45) -> Result<CodexCapabilities, CapabilitySnapshotError> {
46    let path = path.as_ref();
47    let resolved_format = resolve_snapshot_format(format, path)?;
48    let contents =
49        std_fs::read_to_string(path).map_err(|source| CapabilitySnapshotError::ReadSnapshot {
50            path: path.to_path_buf(),
51            source,
52        })?;
53    deserialize_capabilities_snapshot(&contents, resolved_format)
54}
55
56/// Serializes capability overrides (snapshot, version, feature flags) to a JSON or TOML string.
57pub fn serialize_capability_overrides(
58    overrides: &CapabilityOverrides,
59    format: CapabilitySnapshotFormat,
60) -> Result<String, CapabilitySnapshotError> {
61    serialize_snapshot(overrides, format)
62}
63
64/// Parses capability overrides from serialized JSON or TOML.
65pub fn deserialize_capability_overrides(
66    input: &str,
67    format: CapabilitySnapshotFormat,
68) -> Result<CapabilityOverrides, CapabilitySnapshotError> {
69    deserialize_snapshot(input, format)
70}
71
72/// Writes capability overrides to disk, inferring format from the file extension when absent.
73pub fn write_capability_overrides(
74    path: impl AsRef<Path>,
75    overrides: &CapabilityOverrides,
76    format: Option<CapabilitySnapshotFormat>,
77) -> Result<(), CapabilitySnapshotError> {
78    let path = path.as_ref();
79    let resolved_format = resolve_snapshot_format(format, path)?;
80    let contents = serialize_capability_overrides(overrides, resolved_format)?;
81    std_fs::write(path, contents).map_err(|source| CapabilitySnapshotError::WriteSnapshot {
82        path: path.to_path_buf(),
83        source,
84    })
85}
86
87/// Reads capability overrides from disk, inferring format from the file extension when absent.
88pub fn read_capability_overrides(
89    path: impl AsRef<Path>,
90    format: Option<CapabilitySnapshotFormat>,
91) -> Result<CapabilityOverrides, CapabilitySnapshotError> {
92    let path = path.as_ref();
93    let resolved_format = resolve_snapshot_format(format, path)?;
94    let contents =
95        std_fs::read_to_string(path).map_err(|source| CapabilitySnapshotError::ReadSnapshot {
96            path: path.to_path_buf(),
97            source,
98        })?;
99    deserialize_capability_overrides(&contents, resolved_format)
100}
101
102/// True when the snapshot was captured for the same binary path and fingerprint.
103///
104/// Hosts can consult this before applying a serialized snapshot to avoid
105/// reusing stale capability data after binary upgrades.
106pub fn capability_snapshot_matches_binary(snapshot: &CodexCapabilities, binary: &Path) -> bool {
107    let cache_key = capability_cache_key(binary);
108    if snapshot.cache_key != cache_key {
109        return false;
110    }
111    let current = current_fingerprint(&cache_key);
112    has_fingerprint_metadata(&snapshot.fingerprint)
113        && has_fingerprint_metadata(&current)
114        && fingerprints_match(&snapshot.fingerprint, &current)
115}
116
117fn serialize_snapshot<T: Serialize>(
118    value: &T,
119    format: CapabilitySnapshotFormat,
120) -> Result<String, CapabilitySnapshotError> {
121    match format {
122        CapabilitySnapshotFormat::Json => serde_json::to_string_pretty(value)
123            .map_err(|source| CapabilitySnapshotError::JsonEncode { source }),
124        CapabilitySnapshotFormat::Toml => toml::to_string_pretty(value)
125            .map_err(|source| CapabilitySnapshotError::TomlEncode { source }),
126    }
127}
128
129fn deserialize_snapshot<T: DeserializeOwned>(
130    input: &str,
131    format: CapabilitySnapshotFormat,
132) -> Result<T, CapabilitySnapshotError> {
133    match format {
134        CapabilitySnapshotFormat::Json => serde_json::from_str(input)
135            .map_err(|source| CapabilitySnapshotError::JsonDecode { source }),
136        CapabilitySnapshotFormat::Toml => {
137            toml::from_str(input).map_err(|source| CapabilitySnapshotError::TomlDecode { source })
138        }
139    }
140}
141
142fn resolve_snapshot_format(
143    format: Option<CapabilitySnapshotFormat>,
144    path: &Path,
145) -> Result<CapabilitySnapshotFormat, CapabilitySnapshotError> {
146    format
147        .or_else(|| CapabilitySnapshotFormat::from_path(path))
148        .ok_or_else(|| CapabilitySnapshotError::UnsupportedFormat {
149            path: path.to_path_buf(),
150        })
151}