Skip to repository content328 lines · 11.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:12:00.406Z 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
pins.rs
1//! The component ledger: which harness versions the owner froze, and at which
2//! bytes.
3//!
4//! A pin that lives in memory is not a pin. Omega restarts, the registry
5//! document refreshes, and the next launch resolves whatever version the
6//! registry now advertises. So the ledger is a file, it is re-read on every
7//! maintenance decision rather than cached, and it is the *only* thing the
8//! decision consults. Nothing in the enforcement path can hold a stale copy,
9//! because nothing in the enforcement path holds a copy at all.
10//!
11//! A pin names two things:
12//!
13//! * a **version**, which is what the owner typed, and
14//! * a **digest**, which is what the bytes were when the pin was taken.
15//!
16//! Both are checked. A version alone would be satisfied by a re-tagged release
17//! — the exact substitution the falsifier on omega#81 describes — and a digest
18//! alone would produce a refusal message nobody could act on.
19
20use std::collections::BTreeMap;
21
22use serde::{Deserialize, Serialize};
23
24/// The schema every pin ledger on disk carries.
25pub const HARNESS_PIN_LEDGER_SCHEMA: &str = "openagents.omega.harness.pins.v1";
26
27/// The file name of the ledger, relative to the external agents directory.
28pub const HARNESS_PIN_LEDGER_FILE_NAME: &str = "omega-harness-pins.json";
29
30/// The most pins one ledger may hold. A bound, so a corrupt or hostile file
31/// cannot make the launch path do unbounded work.
32pub const MAX_HARNESS_PINS: usize = 128;
33
34/// One frozen harness.
35#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "camelCase", deny_unknown_fields)]
37pub struct HarnessPin {
38 /// The registry id of the harness, for example `codex-acp`.
39 pub harness_id: String,
40 /// The version the owner froze.
41 pub version: String,
42 /// The digest of the installed tree when the pin was taken.
43 ///
44 /// A `String` rather than a
45 /// [`MeasuredDigest`](crate::MeasuredDigest): a pin read back from disk is
46 /// a recorded claim about a past measurement, not a measurement this
47 /// process made, and the two must not share a type. It is only ever
48 /// compared against a live measurement, never written into a receipt as
49 /// one.
50 pub digest: String,
51}
52
53/// The owner's frozen set.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct HarnessPinLedger {
56 pins: BTreeMap<String, HarnessPin>,
57}
58
59/// Every way a ledger file can fail to be a ledger.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum HarnessPinLedgerError {
62 InvalidJson,
63 InvalidSchema,
64 PinBoundExceeded,
65 DuplicatePin,
66 InvalidPin,
67}
68
69impl std::fmt::Display for HarnessPinLedgerError {
70 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 formatter.write_str(match self {
72 Self::InvalidJson => "harness pin ledger is not valid contract JSON",
73 Self::InvalidSchema => "harness pin ledger schema is not supported",
74 Self::PinBoundExceeded => "harness pin ledger holds more pins than are admitted",
75 Self::DuplicatePin => "harness pin ledger pins one harness twice",
76 Self::InvalidPin => "harness pin ledger holds a pin that names no version or no bytes",
77 })
78 }
79}
80
81impl std::error::Error for HarnessPinLedgerError {}
82
83#[derive(Serialize, Deserialize)]
84#[serde(rename_all = "camelCase", deny_unknown_fields)]
85struct RawHarnessPinLedger {
86 schema: String,
87 pins: Vec<HarnessPin>,
88}
89
90impl HarnessPinLedger {
91 /// A ledger with nothing frozen. What a machine with no pin file has.
92 #[must_use]
93 pub fn empty() -> Self {
94 Self {
95 pins: BTreeMap::new(),
96 }
97 }
98
99 /// The pin covering one harness, if the owner froze it.
100 #[must_use]
101 pub fn pin(&self, harness_id: &str) -> Option<&HarnessPin> {
102 self.pins.get(harness_id)
103 }
104
105 /// Every pin, in a stable order.
106 pub fn pins(&self) -> impl Iterator<Item = &HarnessPin> {
107 self.pins.values()
108 }
109
110 #[must_use]
111 pub fn is_empty(&self) -> bool {
112 self.pins.is_empty()
113 }
114
115 /// Freeze one harness at the bytes the host just measured.
116 ///
117 /// Takes a [`MeasuredDigest`](crate::MeasuredDigest) rather than a string,
118 /// so a pin cannot be created against bytes nobody read. That is what makes
119 /// the pin worth checking later: it was a measurement once.
120 pub fn set_pin(&mut self, harness_id: &str, version: &str, digest: &crate::MeasuredDigest) {
121 self.pins.insert(
122 harness_id.to_string(),
123 HarnessPin {
124 harness_id: harness_id.to_string(),
125 version: version.to_string(),
126 digest: digest.as_str().to_string(),
127 },
128 );
129 }
130
131 /// Unfreeze one harness. Returns whether anything was frozen.
132 pub fn remove_pin(&mut self, harness_id: &str) -> bool {
133 self.pins.remove(harness_id).is_some()
134 }
135}
136
137/// Read a ledger file.
138///
139/// Fails closed on every shape it does not recognise. A ledger that cannot be
140/// read is not an empty ledger — see
141/// [`decide_maintenance`](crate::decide_maintenance) for what the caller does
142/// with the error, which is refuse rather than proceed unpinned.
143pub fn decode_harness_pin_ledger(input: &str) -> Result<HarnessPinLedger, HarnessPinLedgerError> {
144 let raw: RawHarnessPinLedger =
145 serde_json::from_str(input).map_err(|_| HarnessPinLedgerError::InvalidJson)?;
146 if raw.schema != HARNESS_PIN_LEDGER_SCHEMA {
147 return Err(HarnessPinLedgerError::InvalidSchema);
148 }
149 if raw.pins.len() > MAX_HARNESS_PINS {
150 return Err(HarnessPinLedgerError::PinBoundExceeded);
151 }
152
153 let mut pins = BTreeMap::new();
154 for pin in raw.pins {
155 if pin.harness_id.trim().is_empty()
156 || pin.version.trim().is_empty()
157 || pin.digest.trim().is_empty()
158 {
159 return Err(HarnessPinLedgerError::InvalidPin);
160 }
161 if pins.insert(pin.harness_id.clone(), pin).is_some() {
162 return Err(HarnessPinLedgerError::DuplicatePin);
163 }
164 }
165 Ok(HarnessPinLedger { pins })
166}
167
168/// Write a ledger file.
169///
170/// Routes back through [`decode_harness_pin_ledger`] before returning, for the
171/// same reason the receipt emitter does: a writer that can produce a file its
172/// own reader refuses turns the next restart into the moment the pins vanish.
173pub fn encode_harness_pin_ledger(
174 ledger: &HarnessPinLedger,
175) -> Result<String, HarnessPinLedgerError> {
176 let raw = RawHarnessPinLedger {
177 schema: HARNESS_PIN_LEDGER_SCHEMA.to_string(),
178 pins: ledger.pins.values().cloned().collect(),
179 };
180 let serialized =
181 serde_json::to_string_pretty(&raw).map_err(|_| HarnessPinLedgerError::InvalidJson)?;
182 decode_harness_pin_ledger(&serialized)?;
183 Ok(serialized)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use crate::MeasuredDigest;
190
191 fn ledger_with_codex() -> HarnessPinLedger {
192 let mut ledger = HarnessPinLedger::empty();
193 ledger.set_pin(
194 "codex-acp",
195 "0.9.4",
196 &MeasuredDigest::measure(b"codex-acp 0.9.4 tree"),
197 );
198 ledger
199 }
200
201 /// The restart law, expressed without a filesystem: whatever the process
202 /// held becomes bytes, and those bytes become the same thing again. The
203 /// on-disk half of this is
204 /// `a_pin_taken_before_a_restart_is_read_back_after_one` in
205 /// `crates/omega_harness/src/omega_harness.rs`.
206 #[test]
207 fn a_ledger_survives_a_round_trip_through_its_own_file_format() {
208 let ledger = ledger_with_codex();
209 let encoded = encode_harness_pin_ledger(&ledger).expect("a ledger encodes");
210 let decoded = decode_harness_pin_ledger(&encoded).expect("its own bytes decode");
211 assert_eq!(decoded, ledger);
212 assert_eq!(decoded.pin("codex-acp").expect("pinned").version, "0.9.4");
213 }
214
215 #[test]
216 fn an_unpinned_harness_has_no_pin() {
217 assert!(ledger_with_codex().pin("gemini-cli").is_none());
218 }
219
220 #[test]
221 fn a_pin_records_the_bytes_that_were_measured_not_a_supplied_string() {
222 let digest = MeasuredDigest::measure(b"codex-acp 0.9.4 tree");
223 let ledger = ledger_with_codex();
224 assert_eq!(
225 ledger.pin("codex-acp").expect("pinned").digest,
226 digest.as_str()
227 );
228 }
229
230 #[test]
231 fn an_unknown_schema_is_refused_rather_than_read_as_an_empty_ledger() {
232 let file = serde_json::json!({
233 "schema": "openagents.omega.harness.pins.v0",
234 "pins": [],
235 })
236 .to_string();
237 assert_eq!(
238 decode_harness_pin_ledger(&file),
239 Err(HarnessPinLedgerError::InvalidSchema)
240 );
241 }
242
243 /// A ledger that pins one harness twice has no single answer to
244 /// "what is `codex-acp` frozen at", and silently keeping the last one would
245 /// make the effective pin depend on file order.
246 #[test]
247 fn a_duplicated_harness_is_refused_rather_than_last_write_wins() {
248 let file = serde_json::json!({
249 "schema": HARNESS_PIN_LEDGER_SCHEMA,
250 "pins": [
251 { "harnessId": "codex-acp", "version": "0.9.4", "digest": "aa" },
252 { "harnessId": "codex-acp", "version": "0.9.5", "digest": "bb" },
253 ],
254 })
255 .to_string();
256 assert_eq!(
257 decode_harness_pin_ledger(&file),
258 Err(HarnessPinLedgerError::DuplicatePin)
259 );
260 }
261
262 #[test]
263 fn a_pin_that_names_no_bytes_is_refused() {
264 for (version, digest) in [("0.9.4", " "), ("", "aa")] {
265 let file = serde_json::json!({
266 "schema": HARNESS_PIN_LEDGER_SCHEMA,
267 "pins": [
268 { "harnessId": "codex-acp", "version": version, "digest": digest },
269 ],
270 })
271 .to_string();
272 assert_eq!(
273 decode_harness_pin_ledger(&file),
274 Err(HarnessPinLedgerError::InvalidPin)
275 );
276 }
277 }
278
279 #[test]
280 fn an_unadmitted_field_is_refused_rather_than_ignored() {
281 let file = serde_json::json!({
282 "schema": HARNESS_PIN_LEDGER_SCHEMA,
283 "pins": [
284 {
285 "harnessId": "codex-acp",
286 "version": "0.9.4",
287 "digest": "aa",
288 "allowUnpinnedUpdate": true,
289 },
290 ],
291 })
292 .to_string();
293 assert_eq!(
294 decode_harness_pin_ledger(&file),
295 Err(HarnessPinLedgerError::InvalidJson)
296 );
297 }
298
299 #[test]
300 fn the_pin_bound_holds() {
301 let pins: Vec<serde_json::Value> = (0..=MAX_HARNESS_PINS)
302 .map(|index| {
303 serde_json::json!({
304 "harnessId": format!("harness-{index}"),
305 "version": "1.0.0",
306 "digest": "aa",
307 })
308 })
309 .collect();
310 let file = serde_json::json!({ "schema": HARNESS_PIN_LEDGER_SCHEMA, "pins": pins })
311 .to_string();
312 assert_eq!(
313 decode_harness_pin_ledger(&file),
314 Err(HarnessPinLedgerError::PinBoundExceeded)
315 );
316 }
317
318 #[test]
319 fn removing_a_pin_unfreezes_exactly_one_harness() {
320 let mut ledger = ledger_with_codex();
321 ledger.set_pin("gemini-cli", "1.2.3", &MeasuredDigest::measure(b"gemini"));
322 assert!(ledger.remove_pin("codex-acp"));
323 assert!(!ledger.remove_pin("codex-acp"));
324 assert!(ledger.pin("codex-acp").is_none());
325 assert!(ledger.pin("gemini-cli").is_some());
326 }
327}
328