Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:15:02.103Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

measured.rs

205 lines · 8.4 KB · rust
1//! A digest that can only exist because the host hashed bytes.
2//!
3//! The whole packet turns on this file. A maintenance receipt is worth
4//! something only if the digest in it is a *measurement* — a number the host
5//! computed from the bytes that are actually going to run — rather than a
6//! *claim* copied out of a registry document, a release tag, or a request that
7//! arrived over a wire.
8//!
9//! Rust cannot tell those apart at the type level unless the type refuses to be
10//! built from a string. So [`MeasuredDigest`] has exactly one primitive
11//! constructor, [`MeasuredDigest::measure`], and it takes bytes. There is no
12//! `From<String>`, no `new(&str)`, no `FromStr`, and deliberately no
13//! `Deserialize`: a value that arrived as text is not a measurement this host
14//! made, and giving it the same type would erase the only distinction that
15//! matters.
16//!
17//! [`MeasuredDigest::measure_tree`] is the second constructor and it is not an
18//! exception: its inputs are themselves `MeasuredDigest` values, so the only
19//! way into the type is still through bytes.
20//!
21//! `OMEGA-DELTA-0025` pins that shape mechanically, because it is the kind of
22//! invariant a convenience constructor added in good faith would silently
23//! undo.
24
25use sha2::{Digest as _, Sha256};
26
27/// A SHA-256 digest of bytes this host read.
28///
29/// Compared against a pin and written into a receipt. It is never parsed from
30/// one: see the module documentation.
31#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub struct MeasuredDigest(String);
33
34impl MeasuredDigest {
35    /// Hash bytes the host read.
36    ///
37    /// The only way to obtain a `MeasuredDigest` from anything that is not
38    /// already one.
39    #[must_use]
40    pub fn measure(bytes: &[u8]) -> Self {
41        let mut hasher = Sha256::new();
42        hasher.update(bytes);
43        Self(format!("{:x}", hasher.finalize()))
44    }
45
46    /// Fold a set of already-measured files into one digest for the whole
47    /// installed payload.
48    ///
49    /// An installed harness is a directory, not a file. Measuring only the
50    /// executable named by `cmd` would leave every sidecar in the same
51    /// directory unattested, and a swap that replaced one of those would
52    /// produce an unchanged digest. So the attested unit is the tree.
53    ///
54    /// The canonical form sorts by path and separates the path from its digest
55    /// with a NUL, which no path component can contain, so two different trees
56    /// cannot fold to the same input string.
57    #[must_use]
58    pub fn measure_tree(files: &mut [(String, MeasuredDigest)]) -> Self {
59        files.sort();
60        let mut canonical = String::new();
61        for (path, digest) in files.iter() {
62            canonical.push_str(path);
63            canonical.push('\0');
64            canonical.push_str(digest.as_str());
65            canonical.push('\n');
66        }
67        Self::measure(canonical.as_bytes())
68    }
69
70    /// The lowercase hexadecimal digest.
71    #[must_use]
72    pub fn as_str(&self) -> &str {
73        &self.0
74    }
75
76    /// Whether a recorded digest — a pin, or a digest decoded from an older
77    /// receipt — describes the same bytes this host just measured.
78    ///
79    /// Takes the claim as a `&str` on purpose. Comparing a measurement to a
80    /// claim is exactly what a pin check is; *becoming* one is what
81    /// `MeasuredDigest` refuses.
82    #[must_use]
83    pub fn matches_recorded(&self, recorded: &str) -> bool {
84        self.0.eq_ignore_ascii_case(recorded.trim())
85    }
86}
87
88impl std::fmt::Display for MeasuredDigest {
89    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        formatter.write_str(&self.0)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    /// Pinned against an independently computed value, so a change of hash
99    /// function or of encoding is caught rather than merely staying
100    /// self-consistent. Produced by `printf 'omega' | shasum -a 256`.
101    #[test]
102    fn a_digest_is_the_sha256_of_the_bytes() {
103        assert_eq!(
104            MeasuredDigest::measure(b"omega").as_str(),
105            "304b4a90a76a1cbe4c112e074b30e75181f54df43d60f883597457844293b341"
106        );
107    }
108
109    #[test]
110    fn measuring_the_same_bytes_twice_agrees_and_different_bytes_do_not() {
111        let first = MeasuredDigest::measure(b"codex-acp v0.9.4");
112        let second = MeasuredDigest::measure(b"codex-acp v0.9.4");
113        let other = MeasuredDigest::measure(b"codex-acp v0.9.5");
114        assert_eq!(first, second);
115        assert_ne!(first, other);
116        assert_eq!(first.as_str().len(), 64);
117        assert!(first.as_str().chars().all(|c| c.is_ascii_hexdigit()));
118    }
119
120    /// The fold has to be sensitive to *where* a byte moved, not only to the
121    /// multiset of file contents. Two trees whose contents are swapped between
122    /// two paths are different trees.
123    #[test]
124    fn the_tree_digest_binds_content_to_its_path() {
125        let alpha = MeasuredDigest::measure(b"alpha");
126        let beta = MeasuredDigest::measure(b"beta");
127        let forward = MeasuredDigest::measure_tree(&mut [
128            ("bin/agent".to_string(), alpha.clone()),
129            ("lib/support".to_string(), beta.clone()),
130        ]);
131        let swapped = MeasuredDigest::measure_tree(&mut [
132            ("bin/agent".to_string(), beta),
133            ("lib/support".to_string(), alpha),
134        ]);
135        assert_ne!(forward, swapped);
136    }
137
138    #[test]
139    fn the_tree_digest_does_not_depend_on_the_order_the_host_walked_the_directory() {
140        let alpha = MeasuredDigest::measure(b"alpha");
141        let beta = MeasuredDigest::measure(b"beta");
142        let one = MeasuredDigest::measure_tree(&mut [
143            ("bin/agent".to_string(), alpha.clone()),
144            ("lib/support".to_string(), beta.clone()),
145        ]);
146        let two = MeasuredDigest::measure_tree(&mut [
147            ("lib/support".to_string(), beta),
148            ("bin/agent".to_string(), alpha),
149        ]);
150        assert_eq!(one, two);
151    }
152
153    /// An extra file in the installed tree changes the tree digest even when
154    /// every previously-present file is byte-identical.
155    ///
156    /// The added path sorts *after* the existing one on purpose. An earlier
157    /// version of this test added `bin/.hidden`, which sorts first, so a fold
158    /// that only consumed the first entry still produced a different digest and
159    /// the test passed while covering nothing. That mutation was found by
160    /// falsifying it.
161    #[test]
162    fn adding_a_file_to_the_tree_changes_the_tree_digest() {
163        let alpha = MeasuredDigest::measure(b"alpha");
164        let before = MeasuredDigest::measure_tree(&mut [("bin/agent".to_string(), alpha.clone())]);
165        let after = MeasuredDigest::measure_tree(&mut [
166            ("bin/agent".to_string(), alpha),
167            ("lib/plugin.so".to_string(), MeasuredDigest::measure(b"")),
168        ]);
169        assert_ne!(before, after);
170    }
171
172    /// The whole tree reaches the digest, not a prefix of it.
173    ///
174    /// This is the assertion the three tests above could not make: every one of
175    /// them compares trees that already differ in their first entry, so a fold
176    /// that stopped after the first file passed all three. Here only the *last*
177    /// file changes, so a truncated fold produces two equal digests and the
178    /// test goes red.
179    #[test]
180    fn changing_only_the_last_file_in_the_tree_changes_the_digest() {
181        let first = MeasuredDigest::measure(b"first");
182        let one = MeasuredDigest::measure_tree(&mut [
183            ("a/first".to_string(), first.clone()),
184            ("z/last".to_string(), MeasuredDigest::measure(b"before")),
185        ]);
186        let two = MeasuredDigest::measure_tree(&mut [
187            ("a/first".to_string(), first),
188            ("z/last".to_string(), MeasuredDigest::measure(b"after")),
189        ]);
190        assert_ne!(
191            one, two,
192            "a file the fold never reached is a file nothing attests"
193        );
194    }
195
196    #[test]
197    fn a_recorded_digest_is_compared_case_and_whitespace_insensitively() {
198        let measured = MeasuredDigest::measure(b"omega");
199        assert!(measured.matches_recorded(&measured.as_str().to_uppercase()));
200        assert!(measured.matches_recorded(&format!("  {}  ", measured.as_str())));
201        assert!(!measured.matches_recorded("not-a-digest"));
202        assert!(!measured.matches_recorded(""));
203    }
204}
205
Served at tenant.openagents/omega Member data and write actions are omitted.