Skip to repository content205 lines · 8.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:19:13.250Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
measured.rs
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