Skip to repository content1535 lines · 60.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:17:23.899Z 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
receipt.rs
1//! The maintenance decision, and the receipt that records it.
2//!
3//! Two rules shape this file.
4//!
5//! **One decision, one receipt.** [`decide_maintenance`] is the only thing that
6//! says yes, and [`receipt_for_decision`] is the only thing that writes a
7//! receipt, and it takes that decision as its input. A receipt therefore cannot
8//! describe an outcome different from the one the enforcement path acted on,
9//! because there is no second place where either is computed.
10//! `every_decision_produces_a_receipt_that_agrees_with_it` holds that shut.
11//!
12//! **The producer routes through its own reader.** [`build_harness_maintenance_receipt`]
13//! serialises a JSON document and hands it to [`decode_harness_maintenance_receipt`]
14//! rather than constructing the typed value, exactly as
15//! `build_issue31_host_adjunct` does. Every refusal a later reader could raise
16//! against a stored receipt is raised here first, against the same bytes.
17//!
18//! On top of those, the input types make the incoherent states unwritable
19//! rather than merely refused: an `Applied` outcome carries a
20//! [`MeasuredDigest`](crate::MeasuredDigest) and not a string, so a receipt
21//! saying an update was applied structurally cannot exist unless this host
22//! hashed the bytes it applied.
23
24use serde::{Deserialize, Serialize};
25
26use crate::{HarnessPin, MeasuredDigest};
27
28/// The schema every receipt this Omega writes carries.
29pub const HARNESS_MAINTENANCE_RECEIPT_SCHEMA: &str = "openagents.omega.harness.maintenance.v1";
30
31/// The file name of the append-only receipt log, relative to the external
32/// agents directory. One JSON document per line.
33pub const HARNESS_MAINTENANCE_LOG_FILE_NAME: &str = "omega-harness-receipts.jsonl";
34
35/// The longest a reference may be. Bounded so a corrupt log line cannot make a
36/// reader do unbounded work.
37pub const MAX_HARNESS_REF_LEN: usize = 128;
38
39/// The largest admitted millisecond timestamp, shared with the other Omega
40/// receipt contracts: the JavaScript `Date` bound.
41pub const MAX_HARNESS_TIMESTAMP_MS: u64 = 8_640_000_000_000_000;
42
43// ---------------------------------------------------------------------------
44// Actions
45// ---------------------------------------------------------------------------
46
47/// The maintenance actions Omega performs on a wrapped harness.
48///
49/// A closed set. A new kind of maintenance is a schema change, not a new
50/// string, because a reader that cannot name an action cannot judge it.
51#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum MaintenanceAction {
54 /// First install on a machine that had none.
55 Install,
56 /// Replace the installed tree with a different version.
57 Update,
58 /// Re-measure an already-installed tree without changing it. This is what
59 /// runs on every launch, and it is the reason a pin cannot be defeated by
60 /// swapping the bytes after the install receipt was written.
61 Verify,
62 /// Turn what the registry channel currently advertises into a concrete
63 /// candidate version.
64 ///
65 /// Separate from [`Self::Update`] because it happens when no bytes move and
66 /// no launch is pending: the registry document refreshes in the background
67 /// and the store decides whether to *offer* the version it now names. A
68 /// harness frozen at another version must not be offered an update Omega
69 /// would refuse to apply, and the resolution that decided so is the thing
70 /// worth recording — the update that never started leaves no other trace.
71 ResolveChannel,
72 /// Re-establish what an installed tree is after it changed, rather than
73 /// carrying the previous answer forward.
74 ///
75 /// Separate from [`Self::Verify`] because `Verify` is what the launch path
76 /// does on the way to spawning a harness, while this is what the owner's
77 /// front door does on demand with nothing about to run. Collapsing them
78 /// would make the log unable to say whether a measurement was taken because
79 /// something was about to execute or because a person asked.
80 ReprobeCapability,
81}
82
83/// Every admitted action, in declaration order.
84///
85/// Named rather than derived, so adding a variant without a wire token that
86/// decodes fails `every_admitted_action_round_trips_through_a_receipt`.
87pub const ADMITTED_MAINTENANCE_ACTIONS: &[MaintenanceAction] = &[
88 MaintenanceAction::Install,
89 MaintenanceAction::Update,
90 MaintenanceAction::Verify,
91 MaintenanceAction::ResolveChannel,
92 MaintenanceAction::ReprobeCapability,
93];
94
95// ---------------------------------------------------------------------------
96// Decision
97// ---------------------------------------------------------------------------
98
99/// What the ledger says about one harness.
100///
101/// `Unreadable` is not `Unpinned`. A ledger that fails to parse is a machine
102/// whose pins are unknown, and treating unknown as unpinned would turn
103/// corrupting one file into a way to unfreeze every harness on the machine.
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum PinState<'a> {
106 /// The ledger was read and this harness is frozen.
107 Pinned(&'a HarnessPin),
108 /// The ledger was read and this harness is not frozen.
109 Unpinned,
110 /// The ledger could not be read. Fails closed.
111 Unreadable,
112}
113
114/// The artifact a maintenance action is about to make runnable.
115///
116/// `version` is what the registry document *claims*. `digest` is what this host
117/// *measured*. Only the second one authorises anything: the version is compared
118/// against the pin's version so a refusal can name something an owner
119/// recognises, but a matching version with different bytes is still refused.
120/// See `a_retagged_release_does_not_satisfy_a_pin`.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub enum CandidateArtifact<'a> {
123 Measured {
124 version: &'a str,
125 digest: &'a MeasuredDigest,
126 },
127 /// The host could not read the installed tree. There is no digest field
128 /// here, so nothing downstream can invent one.
129 Unmeasured { version: &'a str },
130}
131
132/// Why a maintenance action was refused.
133///
134/// A closed set with stable wire tokens. Free text would make the reason
135/// unreadable to anything but a human, and this is the value the front door
136/// renders and the receipt records.
137#[derive(Clone, Debug, PartialEq, Eq)]
138pub enum MaintenanceRefusal {
139 /// The owner froze this harness at a different version.
140 PinnedVersion {
141 pinned_version: String,
142 candidate_version: String,
143 },
144 /// The version matches the pin but the bytes do not. This is the
145 /// re-tagged-release case, and the one a version-only pin would miss.
146 PinnedDigest {
147 pinned_version: String,
148 pinned_digest: String,
149 measured_digest: String,
150 },
151 /// The host could not measure the bytes that would run.
152 ProvenanceUnavailable { version: String },
153 /// The pin ledger exists but could not be read.
154 PinLedgerUnreadable,
155 /// The owner froze this harness, and this harness is distributed in a way
156 /// that gives Omega nothing to freeze.
157 ///
158 /// A package manager resolves the bytes at exec time into a cache Omega
159 /// owns no directory for, so there is no tree to hash and the version the
160 /// registry names is a ceiling rather than an exact request. A pin on such
161 /// a harness cannot be enforced. Launching anyway would make the pin a
162 /// decoration — the owner would have said *not that one* and Omega would
163 /// have run whatever `npm` chose — so the pin refuses the launch instead.
164 UnpinnableDistribution {
165 pinned_version: String,
166 /// The resolver that owns the bytes, e.g. `npx`.
167 resolver: String,
168 },
169}
170
171impl MaintenanceRefusal {
172 /// The stable wire token. Persisted and compared, never shown alone.
173 #[must_use]
174 pub const fn reason_class(&self) -> &'static str {
175 match self {
176 Self::PinnedVersion { .. } => "pinned_version",
177 Self::PinnedDigest { .. } => "pinned_digest",
178 Self::ProvenanceUnavailable { .. } => "provenance_unavailable",
179 Self::PinLedgerUnreadable => "pin_ledger_unreadable",
180 Self::UnpinnableDistribution { .. } => "unpinnable_distribution",
181 }
182 }
183
184 /// The sentence the front door shows next to the disabled control.
185 ///
186 /// A refusal the owner cannot see is the same defect as a refusal that
187 /// never happened, so every class has a sentence and the sentence names
188 /// what to do about it.
189 #[must_use]
190 pub fn reason(&self) -> String {
191 match self {
192 Self::PinnedVersion {
193 pinned_version,
194 candidate_version,
195 } => format!(
196 "Pinned to {pinned_version}. The registry now offers {candidate_version}; \
197 remove the pin to take it."
198 ),
199 Self::PinnedDigest {
200 pinned_version,
201 pinned_digest,
202 measured_digest,
203 } => format!(
204 "Pinned to {pinned_version} at {}, but the installed files hash to {}. \
205 The pinned release was replaced; re-pin only if you meant to.",
206 short_digest(pinned_digest),
207 short_digest(measured_digest),
208 ),
209 Self::ProvenanceUnavailable { version } => format!(
210 "Omega could not read the installed files for {version}, so it cannot \
211 verify what would run. Reinstall this agent."
212 ),
213 Self::PinLedgerUnreadable => {
214 "Omega could not read the pin ledger, so it cannot tell which versions \
215 you froze. Every maintenance action is held until it can."
216 .to_string()
217 }
218 Self::UnpinnableDistribution {
219 pinned_version,
220 resolver,
221 } => format!(
222 "Pinned to {pinned_version}, but this agent runs through {resolver}, which \
223 resolves its own bytes at launch. Omega cannot hold it at a version, so it \
224 will not run it while the pin stands. Remove the pin to run it unpinned."
225 ),
226 }
227 }
228}
229
230fn short_digest(digest: &str) -> String {
231 digest.chars().take(12).collect()
232}
233
234/// The outcome of a maintenance decision.
235///
236/// `Permitted` carries the measurement that permitted it, so the receipt writer
237/// cannot be handed a permission without also being handed its evidence.
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub enum MaintenanceDecision<'a> {
240 Permitted {
241 version: &'a str,
242 digest: &'a MeasuredDigest,
243 },
244 Refused(MaintenanceRefusal),
245}
246
247impl MaintenanceDecision<'_> {
248 #[must_use]
249 pub fn is_permitted(&self) -> bool {
250 matches!(self, Self::Permitted { .. })
251 }
252
253 /// The refusal, if this decision is one.
254 #[must_use]
255 pub fn refusal(&self) -> Option<&MaintenanceRefusal> {
256 match self {
257 Self::Permitted { .. } => None,
258 Self::Refused(refusal) => Some(refusal),
259 }
260 }
261}
262
263/// Decide whether a harness may be made runnable.
264///
265/// The single gate. Everything the enforcement path knows arrives here, and
266/// nothing downstream re-derives an answer.
267///
268/// Both non-`Permitted` inputs fail closed:
269///
270/// * an unreadable ledger refuses everything, so corrupting one file unfreezes
271/// nothing;
272/// * an unmeasured artifact refuses **whether or not a pin exists**, because
273/// the falsifier on omega#81 is precisely a binary that runs with full tool
274/// permissions without a verifiable provenance record — a machine with no pin
275/// is not a machine that consented to running unread bytes.
276#[must_use]
277pub fn decide_maintenance<'a>(
278 pin_state: PinState<'_>,
279 candidate: CandidateArtifact<'a>,
280) -> MaintenanceDecision<'a> {
281 if pin_state == PinState::Unreadable {
282 return MaintenanceDecision::Refused(MaintenanceRefusal::PinLedgerUnreadable);
283 }
284
285 let (version, digest) = match candidate {
286 CandidateArtifact::Unmeasured { version } => {
287 return MaintenanceDecision::Refused(MaintenanceRefusal::ProvenanceUnavailable {
288 version: version.to_string(),
289 });
290 }
291 CandidateArtifact::Measured { version, digest } => (version, digest),
292 };
293
294 let PinState::Pinned(pin) = pin_state else {
295 return MaintenanceDecision::Permitted { version, digest };
296 };
297
298 if pin.version != version {
299 return MaintenanceDecision::Refused(MaintenanceRefusal::PinnedVersion {
300 pinned_version: pin.version.clone(),
301 candidate_version: version.to_string(),
302 });
303 }
304 if !digest.matches_recorded(&pin.digest) {
305 return MaintenanceDecision::Refused(MaintenanceRefusal::PinnedDigest {
306 pinned_version: pin.version.clone(),
307 pinned_digest: pin.digest.clone(),
308 measured_digest: digest.as_str().to_string(),
309 });
310 }
311 MaintenanceDecision::Permitted { version, digest }
312}
313
314/// Whether the host may *fetch* a version at all, before any bytes move.
315///
316/// [`decide_maintenance`] is the authority, and it needs a measurement, which
317/// means it can only run after a download. That is too late to stop Omega
318/// pulling a release the owner froze out — the refusal would still hold, but
319/// the network transfer and the disk write would already have happened.
320///
321/// So this is a prefilter, and it is deliberately weaker: it may refuse only
322/// what [`decide_maintenance`] would also refuse, and it may never permit
323/// anything on its own. `the_prefilter_never_admits_what_the_gate_refuses`
324/// holds both halves.
325#[must_use]
326pub fn admits_version(pin_state: PinState<'_>, candidate_version: &str) -> Option<MaintenanceRefusal> {
327 match pin_state {
328 PinState::Unreadable => Some(MaintenanceRefusal::PinLedgerUnreadable),
329 PinState::Unpinned => None,
330 PinState::Pinned(pin) if pin.version != candidate_version => {
331 Some(MaintenanceRefusal::PinnedVersion {
332 pinned_version: pin.version.clone(),
333 candidate_version: candidate_version.to_string(),
334 })
335 }
336 PinState::Pinned(_) => None,
337 }
338}
339
340/// Whether a harness whose bytes no directory of Omega's holds may launch.
341///
342/// [`decide_maintenance`] cannot answer this: it needs a
343/// [`CandidateArtifact`], and for a package-manager-resolved harness there is
344/// no tree to build one from. Routing such a harness through the
345/// `Unmeasured` arm would refuse **every** npx agent on every machine, which
346/// is not what the pin ledger says and not what this issue asks for. So the
347/// question is narrowed to the one the ledger can actually answer:
348///
349/// * an unreadable ledger refuses, exactly as everywhere else — a machine whose
350/// pins are unknown does not launch a harness it cannot attest;
351/// * a **pinned** harness refuses, because the pin cannot be enforced and a pin
352/// that is silently ignored is worse than no pin at all;
353/// * an unpinned harness launches, unattested, and the front door says so.
354///
355/// The last line is the honest limit of this gate and is stated rather than
356/// hidden: it raises no bar on an unpinned npx harness. What it removes is the
357/// state where an owner froze one and Omega ran whatever `npm` resolved.
358#[must_use]
359pub fn admits_package_manager_launch(
360 pin_state: PinState<'_>,
361 resolver: &str,
362) -> Option<MaintenanceRefusal> {
363 match pin_state {
364 PinState::Unreadable => Some(MaintenanceRefusal::PinLedgerUnreadable),
365 PinState::Unpinned => None,
366 PinState::Pinned(pin) => Some(MaintenanceRefusal::UnpinnableDistribution {
367 pinned_version: pin.version.clone(),
368 resolver: resolver.to_string(),
369 }),
370 }
371}
372
373// ---------------------------------------------------------------------------
374// Receipt
375// ---------------------------------------------------------------------------
376
377/// What a receipt says happened.
378#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
379#[serde(tag = "kind", rename_all = "snake_case")]
380pub enum MaintenanceOutcome {
381 Applied {
382 version: String,
383 #[serde(rename = "artifactDigest")]
384 artifact_digest: String,
385 },
386 Refused {
387 #[serde(rename = "reasonClass")]
388 reason_class: String,
389 #[serde(rename = "reasonDetail")]
390 reason_detail: serde_json::Value,
391 },
392}
393
394/// One maintenance action, recorded.
395#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
396#[serde(rename_all = "camelCase")]
397pub struct HarnessMaintenanceReceipt {
398 pub schema: &'static str,
399 pub host_ref: String,
400 pub harness_ref: String,
401 pub action: MaintenanceAction,
402 /// The host's clock, read once by the caller and stamped here.
403 ///
404 /// No input to [`build_harness_maintenance_receipt`] other than this
405 /// parameter carries a time, so there is no path by which a registry
406 /// document, a settings file, or a remote request can supply one. It is a
407 /// measurement of when this host acted, not a claim about when something
408 /// happened.
409 pub observed_at_ms: u64,
410 pub outcome: MaintenanceOutcome,
411}
412
413impl HarnessMaintenanceReceipt {
414 /// The digest this receipt binds to, when it binds to one.
415 ///
416 /// A refused action applied no bytes, so it has none. There is no default.
417 #[must_use]
418 pub fn artifact_digest(&self) -> Option<&str> {
419 match &self.outcome {
420 MaintenanceOutcome::Applied {
421 artifact_digest, ..
422 } => Some(artifact_digest.as_str()),
423 MaintenanceOutcome::Refused { .. } => None,
424 }
425 }
426
427 #[must_use]
428 pub fn reason_class(&self) -> Option<&str> {
429 match &self.outcome {
430 MaintenanceOutcome::Applied { .. } => None,
431 MaintenanceOutcome::Refused { reason_class, .. } => Some(reason_class.as_str()),
432 }
433 }
434}
435
436/// Every way a receipt can fail to be a receipt.
437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
438pub enum HarnessMaintenanceReceiptError {
439 InvalidJson,
440 InvalidSchema,
441 UnsafeReference,
442 InvalidTimestamp,
443 InvalidOutcome,
444}
445
446impl std::fmt::Display for HarnessMaintenanceReceiptError {
447 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 formatter.write_str(match self {
449 Self::InvalidJson => "harness maintenance receipt is not valid contract JSON",
450 Self::InvalidSchema => "harness maintenance receipt schema is not supported",
451 Self::UnsafeReference => "harness maintenance receipt contains an unsafe reference",
452 Self::InvalidTimestamp => "harness maintenance receipt timestamp is not admitted",
453 Self::InvalidOutcome => "harness maintenance receipt outcome is not coherent",
454 })
455 }
456}
457
458impl std::error::Error for HarnessMaintenanceReceiptError {}
459
460#[derive(Deserialize)]
461#[serde(rename_all = "camelCase", deny_unknown_fields)]
462struct RawHarnessMaintenanceReceipt {
463 schema: String,
464 host_ref: String,
465 harness_ref: String,
466 action: MaintenanceAction,
467 observed_at_ms: u64,
468 outcome: RawMaintenanceOutcome,
469}
470
471#[derive(Deserialize)]
472#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
473enum RawMaintenanceOutcome {
474 Applied {
475 version: String,
476 #[serde(rename = "artifactDigest")]
477 artifact_digest: String,
478 },
479 Refused {
480 #[serde(rename = "reasonClass")]
481 reason_class: String,
482 #[serde(rename = "reasonDetail")]
483 reason_detail: serde_json::Value,
484 },
485}
486
487/// The refusal classes a receipt may carry. Kept beside
488/// [`MaintenanceRefusal::reason_class`] so a decoder cannot admit a class no
489/// producer can mean.
490const ADMITTED_REASON_CLASSES: &[&str] = &[
491 "pinned_version",
492 "pinned_digest",
493 "provenance_unavailable",
494 "pin_ledger_unreadable",
495 "unpinnable_distribution",
496];
497
498/// Read a receipt.
499pub fn decode_harness_maintenance_receipt(
500 input: &str,
501) -> Result<HarnessMaintenanceReceipt, HarnessMaintenanceReceiptError> {
502 let raw: RawHarnessMaintenanceReceipt =
503 serde_json::from_str(input).map_err(|_| HarnessMaintenanceReceiptError::InvalidJson)?;
504 if raw.schema != HARNESS_MAINTENANCE_RECEIPT_SCHEMA {
505 return Err(HarnessMaintenanceReceiptError::InvalidSchema);
506 }
507 if raw.observed_at_ms == 0 || raw.observed_at_ms > MAX_HARNESS_TIMESTAMP_MS {
508 return Err(HarnessMaintenanceReceiptError::InvalidTimestamp);
509 }
510 let host_ref = safe_ref(&raw.host_ref)?;
511 let harness_ref = safe_ref(&raw.harness_ref)?;
512
513 let outcome = match raw.outcome {
514 RawMaintenanceOutcome::Applied {
515 version,
516 artifact_digest,
517 } => {
518 if version.trim().is_empty() || !is_sha256_hex(&artifact_digest) {
519 return Err(HarnessMaintenanceReceiptError::InvalidOutcome);
520 }
521 MaintenanceOutcome::Applied {
522 version,
523 artifact_digest: artifact_digest.to_ascii_lowercase(),
524 }
525 }
526 RawMaintenanceOutcome::Refused {
527 reason_class,
528 reason_detail,
529 } => {
530 if !ADMITTED_REASON_CLASSES.contains(&reason_class.as_str())
531 || !reason_detail.is_object()
532 {
533 return Err(HarnessMaintenanceReceiptError::InvalidOutcome);
534 }
535 for value in reason_detail.as_object().expect("checked above").values() {
536 if let Some(text) = value.as_str() {
537 safe_detail(text)?;
538 }
539 }
540 MaintenanceOutcome::Refused {
541 reason_class,
542 reason_detail,
543 }
544 }
545 };
546
547 Ok(HarnessMaintenanceReceipt {
548 schema: HARNESS_MAINTENANCE_RECEIPT_SCHEMA,
549 host_ref,
550 harness_ref,
551 action: raw.action,
552 observed_at_ms: raw.observed_at_ms,
553 outcome,
554 })
555}
556
557fn is_sha256_hex(value: &str) -> bool {
558 value.len() == 64 && value.chars().all(|c| c.is_ascii_hexdigit())
559}
560
561fn safe_ref(raw: &str) -> Result<String, HarnessMaintenanceReceiptError> {
562 if raw != raw.trim() || raw.is_empty() || raw.len() > MAX_HARNESS_REF_LEN {
563 return Err(HarnessMaintenanceReceiptError::UnsafeReference);
564 }
565 if !raw
566 .chars()
567 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-' | ':'))
568 {
569 return Err(HarnessMaintenanceReceiptError::UnsafeReference);
570 }
571 Ok(raw.to_string())
572}
573
574/// A refusal detail is shown to a person, so it is prose rather than a
575/// reference — but it must still not be able to carry a filesystem path out of
576/// the machine, which is how a receipt turns into a disclosure.
577fn safe_detail(raw: &str) -> Result<(), HarnessMaintenanceReceiptError> {
578 if raw.len() > MAX_HARNESS_REF_LEN
579 || raw.contains('/')
580 || raw.contains('\\')
581 || raw.contains('\0')
582 {
583 return Err(HarnessMaintenanceReceiptError::UnsafeReference);
584 }
585 Ok(())
586}
587
588// ---------------------------------------------------------------------------
589// Emitter
590// ---------------------------------------------------------------------------
591
592/// What a caller may ask the receipt writer to record.
593///
594/// `Applied` takes a [`MeasuredDigest`](crate::MeasuredDigest), not a string.
595/// That is the compile-time half of the provenance rule: a receipt claiming an
596/// update was applied cannot be written by a caller that never hashed the
597/// bytes, because there is no way to build the argument without hashing
598/// something.
599#[derive(Clone, Debug, PartialEq, Eq)]
600pub enum MaintenanceOutcomeInput<'a> {
601 Applied {
602 version: &'a str,
603 digest: &'a MeasuredDigest,
604 },
605 Refused(&'a MaintenanceRefusal),
606}
607
608/// Write a receipt for a maintenance action.
609///
610/// `harness_id` is the registry id; the reference is built here rather than
611/// accepted, so a caller cannot name the harness something other than what the
612/// store calls it.
613///
614/// The document is serialised and handed to
615/// [`decode_harness_maintenance_receipt`], so this cannot emit a record its own
616/// reader would refuse.
617pub fn build_harness_maintenance_receipt(
618 host_ref: &str,
619 harness_id: &str,
620 observed_at_ms: u64,
621 action: MaintenanceAction,
622 outcome: &MaintenanceOutcomeInput<'_>,
623) -> Result<HarnessMaintenanceReceipt, HarnessMaintenanceReceiptError> {
624 let document = serde_json::json!({
625 "schema": HARNESS_MAINTENANCE_RECEIPT_SCHEMA,
626 "hostRef": host_ref,
627 "harnessRef": format!("harness.{harness_id}"),
628 "action": action,
629 "observedAtMs": observed_at_ms,
630 "outcome": match outcome {
631 MaintenanceOutcomeInput::Applied { version, digest } => serde_json::json!({
632 "kind": "applied",
633 "version": version,
634 "artifactDigest": digest.as_str(),
635 }),
636 MaintenanceOutcomeInput::Refused(refusal) => serde_json::json!({
637 "kind": "refused",
638 "reasonClass": refusal.reason_class(),
639 "reasonDetail": refusal_detail(refusal),
640 }),
641 },
642 });
643 let serialized = serde_json::to_string(&document)
644 .map_err(|_| HarnessMaintenanceReceiptError::InvalidJson)?;
645 decode_harness_maintenance_receipt(&serialized)
646}
647
648fn refusal_detail(refusal: &MaintenanceRefusal) -> serde_json::Value {
649 match refusal {
650 MaintenanceRefusal::PinnedVersion {
651 pinned_version,
652 candidate_version,
653 } => serde_json::json!({
654 "pinnedVersion": pinned_version,
655 "candidateVersion": candidate_version,
656 }),
657 MaintenanceRefusal::PinnedDigest {
658 pinned_version,
659 pinned_digest,
660 measured_digest,
661 } => serde_json::json!({
662 "pinnedVersion": pinned_version,
663 "pinnedDigest": pinned_digest,
664 "measuredDigest": measured_digest,
665 }),
666 MaintenanceRefusal::ProvenanceUnavailable { version } => serde_json::json!({
667 "version": version,
668 }),
669 MaintenanceRefusal::PinLedgerUnreadable => serde_json::json!({}),
670 MaintenanceRefusal::UnpinnableDistribution {
671 pinned_version,
672 resolver,
673 } => serde_json::json!({
674 "pinnedVersion": pinned_version,
675 "resolver": resolver,
676 }),
677 }
678}
679
680/// Write the receipt for a decision.
681///
682/// The only receipt writer the enforcement path uses. Because the decision is
683/// the input, the receipt cannot say "applied" for an action that was refused,
684/// or the reverse: there is no branch here that re-decides anything.
685pub fn receipt_for_decision(
686 host_ref: &str,
687 harness_id: &str,
688 observed_at_ms: u64,
689 action: MaintenanceAction,
690 decision: &MaintenanceDecision<'_>,
691) -> Result<HarnessMaintenanceReceipt, HarnessMaintenanceReceiptError> {
692 let outcome = match decision {
693 MaintenanceDecision::Permitted { version, digest } => {
694 MaintenanceOutcomeInput::Applied { version, digest }
695 }
696 MaintenanceDecision::Refused(refusal) => MaintenanceOutcomeInput::Refused(refusal),
697 };
698 build_harness_maintenance_receipt(host_ref, harness_id, observed_at_ms, action, &outcome)
699}
700
701// ---------------------------------------------------------------------------
702// Reading a log written by another version of Omega
703// ---------------------------------------------------------------------------
704
705/// One line of the receipt log, as this Omega can read it.
706///
707/// The receipt log is append-only across Omega versions, so a reader meets
708/// lines it did not write: records from before omega#81 added a digest, and
709/// records from a future schema it does not know. Both decode. Neither is
710/// backfilled.
711///
712/// `ProvenanceUnavailable` has no digest, no version, and no outcome — there is
713/// nowhere to put an invented one. A record whose provenance a reader cannot
714/// establish is reported as exactly that and refused, never rendered beside the
715/// attested ones as though it carried the same evidence.
716#[derive(Clone, Debug, PartialEq, Eq)]
717pub enum HarnessMaintenanceRecord {
718 Attested(HarnessMaintenanceReceipt),
719 ProvenanceUnavailable { schema: String },
720}
721
722/// Read one line of the receipt log.
723///
724/// A line that is not JSON at all is an error, not a record: there is nothing
725/// to report the provenance *of*.
726pub fn decode_harness_maintenance_record(
727 input: &str,
728) -> Result<HarnessMaintenanceRecord, HarnessMaintenanceReceiptError> {
729 // The schema is read before the strict decoder runs, not after it fails.
730 // `deny_unknown_fields` rejects a future schema's extra fields as invalid
731 // JSON, so a decoder that only reached this branch on `InvalidSchema` would
732 // classify every genuinely-newer record as garbage — which is a silent
733 // skip, and a silent skip is the backfill this contract refuses.
734 let value: serde_json::Value =
735 serde_json::from_str(input).map_err(|_| HarnessMaintenanceReceiptError::InvalidJson)?;
736 let schema = value
737 .get("schema")
738 .and_then(serde_json::Value::as_str)
739 .ok_or(HarnessMaintenanceReceiptError::InvalidJson)?;
740 if schema != HARNESS_MAINTENANCE_RECEIPT_SCHEMA {
741 safe_ref(schema)?;
742 return Ok(HarnessMaintenanceRecord::ProvenanceUnavailable {
743 schema: schema.to_string(),
744 });
745 }
746 decode_harness_maintenance_receipt(input).map(HarnessMaintenanceRecord::Attested)
747}
748
749/// What a reader may conclude about an installed harness.
750#[derive(Clone, Debug, PartialEq, Eq)]
751pub enum ProvenanceVerdict {
752 /// A receipt this Omega can read binds the bytes now on disk.
753 Verified { digest: String },
754 Refused(ProvenanceGap),
755}
756
757/// Why provenance could not be established.
758#[derive(Clone, Copy, Debug, PartialEq, Eq)]
759pub enum ProvenanceGap {
760 /// No receipt covers this installation. Every harness installed before
761 /// omega#81 is in this state, and stays in it until a maintenance action
762 /// measures it. It is not upgraded by assumption.
763 Unattested,
764 /// A receipt exists, but the bytes on disk are not the bytes it recorded.
765 DigestMismatch,
766 /// The most recent receipt records a refusal, so nothing was applied.
767 LastActionRefused,
768 /// The most recent record is one this Omega cannot read the provenance of.
769 RecordUnreadable,
770 /// The installed tree could not be read, so there is no measurement to
771 /// judge a record against. Distinct from [`Self::Unattested`]: that one is
772 /// "nobody recorded these bytes", this one is "this host cannot read them".
773 TreeUnreadable,
774 /// The harness has no tree on disk to attest, by construction — a package
775 /// manager resolves its bytes at launch into a cache Omega owns no
776 /// directory for. Not a defect in this installation; a property of how the
777 /// harness is distributed.
778 ResolvedAtLaunch,
779}
780
781impl ProvenanceGap {
782 #[must_use]
783 pub const fn reason(self) -> &'static str {
784 match self {
785 Self::Unattested => {
786 "This agent was installed before Omega recorded provenance. Update it \
787 to bind it to a verified build."
788 }
789 Self::DigestMismatch => {
790 "The installed files are not the files Omega recorded. Reinstall this \
791 agent before running it."
792 }
793 Self::LastActionRefused => {
794 "The last maintenance action was refused, so nothing was installed."
795 }
796 Self::RecordUnreadable => {
797 "Omega cannot read the provenance record for this agent. It was written \
798 by a different version."
799 }
800 Self::TreeUnreadable => {
801 "Omega could not read this agent's installed files, so it cannot say what \
802 would run. Reinstall it."
803 }
804 Self::ResolvedAtLaunch => {
805 "This agent's package manager resolves its own bytes when it starts, so \
806 there is nothing installed for Omega to attest."
807 }
808 }
809 }
810}
811
812/// What the host knows about the bytes an installed harness will run.
813#[derive(Clone, Debug, PartialEq, Eq)]
814pub enum InstallationProvenance {
815 /// The most recent record covering this harness.
816 Recorded(HarnessMaintenanceRecord),
817 /// No record covers this installation.
818 Unattested,
819}
820
821/// Judge an installation against the bytes now on disk.
822#[must_use]
823pub fn verify_installation(
824 provenance: &InstallationProvenance,
825 measured: &MeasuredDigest,
826) -> ProvenanceVerdict {
827 match provenance {
828 InstallationProvenance::Unattested => ProvenanceVerdict::Refused(ProvenanceGap::Unattested),
829 InstallationProvenance::Recorded(HarnessMaintenanceRecord::ProvenanceUnavailable {
830 ..
831 }) => ProvenanceVerdict::Refused(ProvenanceGap::RecordUnreadable),
832 InstallationProvenance::Recorded(HarnessMaintenanceRecord::Attested(receipt)) => {
833 match receipt.artifact_digest() {
834 None => ProvenanceVerdict::Refused(ProvenanceGap::LastActionRefused),
835 Some(recorded) if measured.matches_recorded(recorded) => {
836 ProvenanceVerdict::Verified {
837 digest: recorded.to_string(),
838 }
839 }
840 Some(_) => ProvenanceVerdict::Refused(ProvenanceGap::DigestMismatch),
841 }
842 }
843 }
844}
845
846// ---------------------------------------------------------------------------
847// The front-door affordance
848// ---------------------------------------------------------------------------
849
850/// Whether the one-click control is live, and if not, why.
851///
852/// The `reason` is not optional decoration. omega 0.2.0-rc11 bound
853/// `appendSystemNote` to `() => {}` on the framed provider path, so a handoff
854/// was refused and the thread said nothing; a different model then spent the
855/// owner's budget with no trace. A blocked update nobody can see is the same
856/// defect. So a disabled affordance structurally carries its sentence: there is
857/// no `Disabled` without a reason to put in it.
858#[derive(Clone, Debug, PartialEq, Eq)]
859pub enum MaintenanceAffordance {
860 Enabled,
861 Disabled { reason: String },
862}
863
864impl MaintenanceAffordance {
865 #[must_use]
866 pub fn is_enabled(&self) -> bool {
867 matches!(self, Self::Enabled)
868 }
869
870 /// The sentence to render beside a disabled control.
871 #[must_use]
872 pub fn reason(&self) -> Option<&str> {
873 match self {
874 Self::Enabled => None,
875 Self::Disabled { reason } => Some(reason.as_str()),
876 }
877 }
878}
879
880/// The state of the one-click update control for one harness.
881#[must_use]
882pub fn update_affordance(decision: &MaintenanceDecision<'_>) -> MaintenanceAffordance {
883 match decision {
884 MaintenanceDecision::Permitted { .. } => MaintenanceAffordance::Enabled,
885 MaintenanceDecision::Refused(refusal) => MaintenanceAffordance::Disabled {
886 reason: refusal.reason(),
887 },
888 }
889}
890
891#[cfg(test)]
892mod tests {
893 use super::*;
894 use crate::HarnessPinLedger;
895
896 const HOST: &str = "host.omega.device-alpha";
897 const NOW: u64 = 1_784_894_400_000;
898
899 fn pinned_ledger() -> HarnessPinLedger {
900 let mut ledger = HarnessPinLedger::empty();
901 ledger.set_pin("codex-acp", "0.9.4", &MeasuredDigest::measure(RELEASE_0_9_4));
902 ledger
903 }
904
905 const RELEASE_0_9_4: &[u8] = b"codex-acp 0.9.4 tree";
906 const RELEASE_0_9_5: &[u8] = b"codex-acp 0.9.5 tree";
907
908 // -------------------------------------------------------------------
909 // The decision
910 // -------------------------------------------------------------------
911
912 #[test]
913 fn an_unpinned_harness_may_be_updated() {
914 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
915 let decision = decide_maintenance(
916 PinState::Unpinned,
917 CandidateArtifact::Measured {
918 version: "0.9.5",
919 digest: &digest,
920 },
921 );
922 assert!(decision.is_permitted());
923 assert!(update_affordance(&decision).is_enabled());
924 }
925
926 #[test]
927 fn a_pinned_harness_is_not_updated_past_its_pin() {
928 let ledger = pinned_ledger();
929 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
930 let decision = decide_maintenance(
931 PinState::Pinned(ledger.pin("codex-acp").expect("pinned")),
932 CandidateArtifact::Measured {
933 version: "0.9.5",
934 digest: &digest,
935 },
936 );
937 assert_eq!(
938 decision.refusal().map(MaintenanceRefusal::reason_class),
939 Some("pinned_version")
940 );
941 }
942
943 #[test]
944 fn a_pinned_harness_still_runs_at_the_version_it_is_pinned_to() {
945 let ledger = pinned_ledger();
946 let digest = MeasuredDigest::measure(RELEASE_0_9_4);
947 let decision = decide_maintenance(
948 PinState::Pinned(ledger.pin("codex-acp").expect("pinned")),
949 CandidateArtifact::Measured {
950 version: "0.9.4",
951 digest: &digest,
952 },
953 );
954 assert!(
955 decision.is_permitted(),
956 "a pin freezes a harness, it does not disable it"
957 );
958 }
959
960 /// The reason the pin is not a version string. A release re-tagged in place
961 /// carries the pinned version and different bytes; a version-only pin
962 /// admits it, and this is exactly the substitution omega#81's falsifier
963 /// describes.
964 #[test]
965 fn a_retagged_release_does_not_satisfy_a_pin() {
966 let ledger = pinned_ledger();
967 let substituted = MeasuredDigest::measure(b"codex-acp 0.9.4 tree, rebuilt");
968 let decision = decide_maintenance(
969 PinState::Pinned(ledger.pin("codex-acp").expect("pinned")),
970 CandidateArtifact::Measured {
971 version: "0.9.4",
972 digest: &substituted,
973 },
974 );
975 assert_eq!(
976 decision.refusal().map(MaintenanceRefusal::reason_class),
977 Some("pinned_digest")
978 );
979 }
980
981 /// The falsifier, directly: bytes nobody hashed never become runnable, pin
982 /// or no pin.
983 #[test]
984 fn bytes_the_host_could_not_read_are_refused_even_with_no_pin() {
985 for pin_state in [PinState::Unpinned, PinState::Unreadable] {
986 let decision =
987 decide_maintenance(pin_state, CandidateArtifact::Unmeasured { version: "0.9.5" });
988 assert!(!decision.is_permitted(), "{pin_state:?}");
989 }
990 }
991
992 /// Corrupting the ledger must not be a way to unfreeze every harness.
993 #[test]
994 fn an_unreadable_ledger_refuses_everything_rather_than_reading_as_unpinned() {
995 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
996 let decision = decide_maintenance(
997 PinState::Unreadable,
998 CandidateArtifact::Measured {
999 version: "0.9.5",
1000 digest: &digest,
1001 },
1002 );
1003 assert_eq!(
1004 decision.refusal().map(MaintenanceRefusal::reason_class),
1005 Some("pin_ledger_unreadable")
1006 );
1007 }
1008
1009 /// A package-manager harness nobody froze is not this gate's business. It
1010 /// launches unattested, and the front door says so — refusing every npx
1011 /// agent on every machine is a different change than omega#81 asks for, and
1012 /// pretending otherwise here would hide which bar this actually raises.
1013 #[test]
1014 fn an_unpinned_package_manager_harness_is_admitted() {
1015 assert_eq!(
1016 admits_package_manager_launch(PinState::Unpinned, "npx"),
1017 None
1018 );
1019 }
1020
1021 /// The gap this closes: before it, pinning an npx harness did nothing at
1022 /// all. A pin Omega cannot enforce must refuse rather than be ignored,
1023 /// because an ignored pin tells the owner their "not that one" was heard.
1024 #[test]
1025 fn a_pin_on_a_package_manager_harness_refuses_rather_than_being_ignored() {
1026 let ledger = pinned_ledger();
1027 let pin = ledger.pin("codex-acp").expect("pinned");
1028 let refusal = admits_package_manager_launch(PinState::Pinned(pin), "npx")
1029 .expect("a pinned package-manager harness refuses");
1030 assert_eq!(refusal.reason_class(), "unpinnable_distribution");
1031 assert!(refusal.reason().contains("npx"));
1032
1033 // And the refusal is recordable: a gate whose refusal could not be
1034 // written would enforce without leaving evidence it enforced.
1035 let receipt = build_harness_maintenance_receipt(
1036 HOST,
1037 "codex-acp",
1038 NOW,
1039 MaintenanceAction::Verify,
1040 &MaintenanceOutcomeInput::Refused(&refusal),
1041 )
1042 .expect("the refusal is a receipt");
1043 assert_eq!(receipt.reason_class(), Some("unpinnable_distribution"));
1044 assert_eq!(receipt.artifact_digest(), None);
1045 }
1046
1047 /// An unreadable ledger refuses the package-manager path too. The gate that
1048 /// fails closed everywhere else must not be the one place a corrupt file
1049 /// buys a launch.
1050 #[test]
1051 fn an_unreadable_ledger_refuses_a_package_manager_launch_as_well() {
1052 assert_eq!(
1053 admits_package_manager_launch(PinState::Unreadable, "npx")
1054 .map(|refusal| refusal.reason_class()),
1055 Some("pin_ledger_unreadable")
1056 );
1057 }
1058
1059 /// Every refusal a person can hit has a sentence, and the sentence says
1060 /// something. An empty or generic reason is the rc11 defect wearing a
1061 /// different shape.
1062 #[test]
1063 fn every_refusal_class_renders_a_reason_that_names_what_to_do() {
1064 let refusals = [
1065 MaintenanceRefusal::PinnedVersion {
1066 pinned_version: "0.9.4".into(),
1067 candidate_version: "0.9.5".into(),
1068 },
1069 MaintenanceRefusal::PinnedDigest {
1070 pinned_version: "0.9.4".into(),
1071 pinned_digest: "a".repeat(64),
1072 measured_digest: "b".repeat(64),
1073 },
1074 MaintenanceRefusal::ProvenanceUnavailable {
1075 version: "0.9.5".into(),
1076 },
1077 MaintenanceRefusal::PinLedgerUnreadable,
1078 MaintenanceRefusal::UnpinnableDistribution {
1079 pinned_version: "1.2.3".into(),
1080 resolver: "npx".into(),
1081 },
1082 ];
1083 let mut classes = std::collections::BTreeSet::new();
1084 for refusal in &refusals {
1085 let reason = refusal.reason();
1086 assert!(reason.len() > 40, "{reason:?} is not a sentence");
1087 assert!(!reason.contains('/'), "{reason:?} carries a path");
1088 classes.insert(refusal.reason_class());
1089 }
1090 assert_eq!(
1091 classes.len(),
1092 ADMITTED_REASON_CLASSES.len(),
1093 "a refusal class exists that the receipt contract does not admit"
1094 );
1095 for class in ADMITTED_REASON_CLASSES {
1096 assert!(classes.contains(class), "{class} has no refusal that means it");
1097 }
1098 }
1099
1100 /// The prefilter runs before a download and the gate runs after one, so a
1101 /// disagreement between them is either a release Omega fetches and then
1102 /// refuses to run (waste) or, worse, one it declines to fetch that the gate
1103 /// would have permitted (a pin that blocks more than it says).
1104 ///
1105 /// Neither is allowed: over every combination of pin state and candidate,
1106 /// a prefilter refusal implies a gate refusal *of the same class*, and a
1107 /// prefilter pass never turns a gate refusal into a permit.
1108 #[test]
1109 fn the_prefilter_never_admits_what_the_gate_refuses() {
1110 let ledger = pinned_ledger();
1111 let pinned = ledger.pin("codex-acp").expect("pinned");
1112 let matching = MeasuredDigest::measure(RELEASE_0_9_4);
1113 let substituted = MeasuredDigest::measure(b"codex-acp 0.9.4 tree, rebuilt");
1114 let newer = MeasuredDigest::measure(RELEASE_0_9_5);
1115
1116 let cases: Vec<(PinState<'_>, &str, &MeasuredDigest)> = vec![
1117 (PinState::Unpinned, "0.9.5", &newer),
1118 (PinState::Unreadable, "0.9.5", &newer),
1119 (PinState::Pinned(pinned), "0.9.4", &matching),
1120 (PinState::Pinned(pinned), "0.9.4", &substituted),
1121 (PinState::Pinned(pinned), "0.9.5", &newer),
1122 ];
1123
1124 for (pin_state, version, digest) in cases {
1125 let prefiltered = admits_version(pin_state, version);
1126 let decided = decide_maintenance(
1127 pin_state,
1128 CandidateArtifact::Measured { version, digest },
1129 );
1130 match (&prefiltered, &decided) {
1131 (Some(refusal), MaintenanceDecision::Refused(gate)) => {
1132 assert_eq!(
1133 refusal.reason_class(),
1134 gate.reason_class(),
1135 "{pin_state:?} {version}: the prefilter and the gate refuse differently"
1136 );
1137 }
1138 (Some(refusal), MaintenanceDecision::Permitted { .. }) => panic!(
1139 "{pin_state:?} {version}: the prefilter refused {} where the gate permits",
1140 refusal.reason_class()
1141 ),
1142 (None, _) => {}
1143 }
1144 }
1145 }
1146
1147 #[test]
1148 fn a_refused_decision_disables_the_control_with_that_refusals_reason() {
1149 let ledger = pinned_ledger();
1150 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
1151 let decision = decide_maintenance(
1152 PinState::Pinned(ledger.pin("codex-acp").expect("pinned")),
1153 CandidateArtifact::Measured {
1154 version: "0.9.5",
1155 digest: &digest,
1156 },
1157 );
1158 let affordance = update_affordance(&decision);
1159 assert!(!affordance.is_enabled());
1160 let reason = affordance.reason().expect("a disabled control carries a reason");
1161 assert!(reason.contains("0.9.4"), "{reason:?}");
1162 assert!(reason.contains("0.9.5"), "{reason:?}");
1163 }
1164
1165 // -------------------------------------------------------------------
1166 // The receipt
1167 // -------------------------------------------------------------------
1168
1169 #[test]
1170 fn a_permitted_update_produces_a_receipt_bound_to_the_measured_digest() {
1171 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
1172 let decision = decide_maintenance(
1173 PinState::Unpinned,
1174 CandidateArtifact::Measured {
1175 version: "0.9.5",
1176 digest: &digest,
1177 },
1178 );
1179 let receipt = receipt_for_decision(HOST, "codex-acp", NOW, MaintenanceAction::Update, &decision)
1180 .expect("a permitted update writes a receipt");
1181 assert_eq!(receipt.harness_ref, "harness.codex-acp");
1182 assert_eq!(receipt.action, MaintenanceAction::Update);
1183 assert_eq!(receipt.observed_at_ms, NOW);
1184 assert_eq!(receipt.artifact_digest(), Some(digest.as_str()));
1185 }
1186
1187 /// The binding between the gate and the record. A receipt cannot describe
1188 /// an outcome other than the one the gate reached, because the gate's
1189 /// output is the writer's input.
1190 #[test]
1191 fn every_decision_produces_a_receipt_that_agrees_with_it() {
1192 let ledger = pinned_ledger();
1193 let pinned = ledger.pin("codex-acp").expect("pinned");
1194 let matching = MeasuredDigest::measure(RELEASE_0_9_4);
1195 let newer = MeasuredDigest::measure(RELEASE_0_9_5);
1196 let substituted = MeasuredDigest::measure(b"codex-acp 0.9.4 tree, rebuilt");
1197
1198 let cases: Vec<(PinState<'_>, CandidateArtifact<'_>)> = vec![
1199 (
1200 PinState::Unpinned,
1201 CandidateArtifact::Measured {
1202 version: "0.9.5",
1203 digest: &newer,
1204 },
1205 ),
1206 (
1207 PinState::Pinned(pinned),
1208 CandidateArtifact::Measured {
1209 version: "0.9.4",
1210 digest: &matching,
1211 },
1212 ),
1213 (
1214 PinState::Pinned(pinned),
1215 CandidateArtifact::Measured {
1216 version: "0.9.5",
1217 digest: &newer,
1218 },
1219 ),
1220 (
1221 PinState::Pinned(pinned),
1222 CandidateArtifact::Measured {
1223 version: "0.9.4",
1224 digest: &substituted,
1225 },
1226 ),
1227 (
1228 PinState::Pinned(pinned),
1229 CandidateArtifact::Unmeasured { version: "0.9.5" },
1230 ),
1231 (
1232 PinState::Unreadable,
1233 CandidateArtifact::Measured {
1234 version: "0.9.5",
1235 digest: &newer,
1236 },
1237 ),
1238 ];
1239
1240 for (pin_state, candidate) in cases {
1241 let decision = decide_maintenance(pin_state, candidate);
1242 let receipt =
1243 receipt_for_decision(HOST, "codex-acp", NOW, MaintenanceAction::Update, &decision)
1244 .expect("every decision is recordable");
1245 match &decision {
1246 MaintenanceDecision::Permitted { digest, .. } => {
1247 assert_eq!(receipt.artifact_digest(), Some(digest.as_str()));
1248 assert_eq!(receipt.reason_class(), None);
1249 }
1250 MaintenanceDecision::Refused(refusal) => {
1251 assert_eq!(receipt.reason_class(), Some(refusal.reason_class()));
1252 assert_eq!(
1253 receipt.artifact_digest(),
1254 None,
1255 "a refused action applied no bytes and must bind to none"
1256 );
1257 }
1258 }
1259 }
1260 }
1261
1262 /// The producer routes through its own reader, so what it writes is what a
1263 /// later reader gets back.
1264 #[test]
1265 fn an_emitted_receipt_decodes_to_itself() {
1266 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
1267 let receipt = build_harness_maintenance_receipt(
1268 HOST,
1269 "codex-acp",
1270 NOW,
1271 MaintenanceAction::Install,
1272 &MaintenanceOutcomeInput::Applied {
1273 version: "0.9.5",
1274 digest: &digest,
1275 },
1276 )
1277 .expect("emits");
1278 let encoded = serde_json::to_string(&receipt).expect("a receipt serialises");
1279 assert_eq!(
1280 decode_harness_maintenance_receipt(&encoded).expect("its own bytes decode"),
1281 receipt
1282 );
1283 }
1284
1285 #[test]
1286 fn a_receipt_carries_no_filesystem_path() {
1287 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
1288 let receipt = build_harness_maintenance_receipt(
1289 HOST,
1290 "codex-acp",
1291 NOW,
1292 MaintenanceAction::Install,
1293 &MaintenanceOutcomeInput::Applied {
1294 version: "0.9.5",
1295 digest: &digest,
1296 },
1297 )
1298 .expect("emits");
1299 let encoded = serde_json::to_string(&receipt).expect("serialises");
1300 assert!(!encoded.contains("/Users/"));
1301 assert!(!encoded.contains("credential"));
1302 assert!(!encoded.contains("Bearer "));
1303 }
1304
1305 #[test]
1306 fn a_private_path_cannot_be_emitted_as_a_host_reference() {
1307 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
1308 let error = build_harness_maintenance_receipt(
1309 "/Users/owner/.codex/auth.json",
1310 "codex-acp",
1311 NOW,
1312 MaintenanceAction::Install,
1313 &MaintenanceOutcomeInput::Applied {
1314 version: "0.9.5",
1315 digest: &digest,
1316 },
1317 )
1318 .expect_err("a private path must fail closed");
1319 assert_eq!(error, HarnessMaintenanceReceiptError::UnsafeReference);
1320 assert!(!error.to_string().contains("/Users/"));
1321 }
1322
1323 /// The digest is the whole receipt. A stored record that lost it does not
1324 /// decode with a blank, an empty string, or the version standing in.
1325 #[test]
1326 fn an_applied_receipt_without_a_digest_is_refused_rather_than_defaulted() {
1327 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
1328 let receipt = build_harness_maintenance_receipt(
1329 HOST,
1330 "codex-acp",
1331 NOW,
1332 MaintenanceAction::Update,
1333 &MaintenanceOutcomeInput::Applied {
1334 version: "0.9.5",
1335 digest: &digest,
1336 },
1337 )
1338 .expect("emits");
1339 let mut value = serde_json::to_value(&receipt).expect("value");
1340 value["outcome"]
1341 .as_object_mut()
1342 .expect("outcome object")
1343 .remove("artifactDigest");
1344 assert_eq!(
1345 decode_harness_maintenance_receipt(&value.to_string()),
1346 Err(HarnessMaintenanceReceiptError::InvalidJson)
1347 );
1348
1349 let mut value = serde_json::to_value(&receipt).expect("value");
1350 value["outcome"]["artifactDigest"] = serde_json::Value::String(String::new());
1351 assert_eq!(
1352 decode_harness_maintenance_receipt(&value.to_string()),
1353 Err(HarnessMaintenanceReceiptError::InvalidOutcome)
1354 );
1355
1356 let mut value = serde_json::to_value(&receipt).expect("value");
1357 value["outcome"]["artifactDigest"] = serde_json::Value::String("0.9.5".into());
1358 assert_eq!(
1359 decode_harness_maintenance_receipt(&value.to_string()),
1360 Err(HarnessMaintenanceReceiptError::InvalidOutcome)
1361 );
1362 }
1363
1364 #[test]
1365 fn a_refusal_class_no_producer_can_mean_is_refused() {
1366 let receipt = receipt_for_decision(
1367 HOST,
1368 "codex-acp",
1369 NOW,
1370 MaintenanceAction::Update,
1371 &MaintenanceDecision::Refused(MaintenanceRefusal::PinLedgerUnreadable),
1372 )
1373 .expect("emits");
1374 let mut value = serde_json::to_value(&receipt).expect("value");
1375 value["outcome"]["reasonClass"] = serde_json::Value::String("owner_waived_the_pin".into());
1376 assert_eq!(
1377 decode_harness_maintenance_receipt(&value.to_string()),
1378 Err(HarnessMaintenanceReceiptError::InvalidOutcome)
1379 );
1380 }
1381
1382 #[test]
1383 fn an_absent_or_out_of_range_timestamp_is_refused() {
1384 let digest = MeasuredDigest::measure(RELEASE_0_9_5);
1385 for stamp in [0, MAX_HARNESS_TIMESTAMP_MS + 1] {
1386 let error = build_harness_maintenance_receipt(
1387 HOST,
1388 "codex-acp",
1389 stamp,
1390 MaintenanceAction::Update,
1391 &MaintenanceOutcomeInput::Applied {
1392 version: "0.9.5",
1393 digest: &digest,
1394 },
1395 )
1396 .expect_err("an unstamped receipt is not a measurement");
1397 assert_eq!(error, HarnessMaintenanceReceiptError::InvalidTimestamp);
1398 }
1399 }
1400
1401 #[test]
1402 fn an_unadmitted_receipt_field_is_refused_rather_than_ignored() {
1403 let receipt = receipt_for_decision(
1404 HOST,
1405 "codex-acp",
1406 NOW,
1407 MaintenanceAction::Verify,
1408 &MaintenanceDecision::Refused(MaintenanceRefusal::PinLedgerUnreadable),
1409 )
1410 .expect("emits");
1411 let mut value = serde_json::to_value(&receipt).expect("value");
1412 value
1413 .as_object_mut()
1414 .expect("object")
1415 .insert("verified".into(), serde_json::Value::Bool(true));
1416 assert_eq!(
1417 decode_harness_maintenance_receipt(&value.to_string()),
1418 Err(HarnessMaintenanceReceiptError::InvalidJson)
1419 );
1420 }
1421
1422 // -------------------------------------------------------------------
1423 // Reading a log this Omega did not write
1424 // -------------------------------------------------------------------
1425
1426 /// The no-backfill law. A record from a schema this reader does not know
1427 /// decodes, reports its provenance as unavailable, and is refused. It is
1428 /// never given a digest, because the value it decodes to has no field for
1429 /// one.
1430 #[test]
1431 fn a_record_from_another_schema_reports_unavailable_and_is_refused() {
1432 let line = serde_json::json!({
1433 "schema": "openagents.omega.harness.maintenance.v2",
1434 "hostRef": HOST,
1435 "harnessRef": "harness.codex-acp",
1436 "action": "update",
1437 "observedAtMs": NOW,
1438 "outcome": { "kind": "applied", "version": "1.0.0", "artifactDigest": "c".repeat(64) },
1439 "attestationChain": ["signature"],
1440 })
1441 .to_string();
1442 let record = decode_harness_maintenance_record(&line).expect("an unknown schema decodes");
1443 assert_eq!(
1444 record,
1445 HarnessMaintenanceRecord::ProvenanceUnavailable {
1446 schema: "openagents.omega.harness.maintenance.v2".into(),
1447 }
1448 );
1449
1450 let verdict = verify_installation(
1451 &InstallationProvenance::Recorded(record),
1452 &MeasuredDigest::measure(RELEASE_0_9_5),
1453 );
1454 assert_eq!(
1455 verdict,
1456 ProvenanceVerdict::Refused(ProvenanceGap::RecordUnreadable)
1457 );
1458 }
1459
1460 /// An installation that predates this packet has no receipt at all. It
1461 /// stays unattested until a maintenance action measures it; nothing
1462 /// promotes it by assumption.
1463 #[test]
1464 fn an_installation_from_before_this_packet_is_refused_not_assumed_good() {
1465 let verdict = verify_installation(
1466 &InstallationProvenance::Unattested,
1467 &MeasuredDigest::measure(RELEASE_0_9_4),
1468 );
1469 assert_eq!(verdict, ProvenanceVerdict::Refused(ProvenanceGap::Unattested));
1470 assert!(ProvenanceGap::Unattested.reason().contains("provenance"));
1471 }
1472
1473 #[test]
1474 fn a_receipt_whose_bytes_were_swapped_afterwards_is_refused() {
1475 let installed = MeasuredDigest::measure(RELEASE_0_9_5);
1476 let receipt = build_harness_maintenance_receipt(
1477 HOST,
1478 "codex-acp",
1479 NOW,
1480 MaintenanceAction::Install,
1481 &MaintenanceOutcomeInput::Applied {
1482 version: "0.9.5",
1483 digest: &installed,
1484 },
1485 )
1486 .expect("emits");
1487 let provenance =
1488 InstallationProvenance::Recorded(HarnessMaintenanceRecord::Attested(receipt));
1489
1490 assert_eq!(
1491 verify_installation(&provenance, &installed),
1492 ProvenanceVerdict::Verified {
1493 digest: installed.as_str().to_string()
1494 }
1495 );
1496 assert_eq!(
1497 verify_installation(&provenance, &MeasuredDigest::measure(b"swapped afterwards")),
1498 ProvenanceVerdict::Refused(ProvenanceGap::DigestMismatch)
1499 );
1500 }
1501
1502 /// A refused action installed nothing, so the last record covering a
1503 /// harness being a refusal does not verify anything.
1504 #[test]
1505 fn a_refusal_receipt_never_verifies_an_installation() {
1506 let receipt = receipt_for_decision(
1507 HOST,
1508 "codex-acp",
1509 NOW,
1510 MaintenanceAction::Update,
1511 &MaintenanceDecision::Refused(MaintenanceRefusal::PinLedgerUnreadable),
1512 )
1513 .expect("emits");
1514 assert_eq!(
1515 verify_installation(
1516 &InstallationProvenance::Recorded(HarnessMaintenanceRecord::Attested(receipt)),
1517 &MeasuredDigest::measure(RELEASE_0_9_5),
1518 ),
1519 ProvenanceVerdict::Refused(ProvenanceGap::LastActionRefused)
1520 );
1521 }
1522
1523 #[test]
1524 fn a_log_line_that_is_not_json_is_an_error_rather_than_a_record() {
1525 assert_eq!(
1526 decode_harness_maintenance_record("not json"),
1527 Err(HarnessMaintenanceReceiptError::InvalidJson)
1528 );
1529 assert_eq!(
1530 decode_harness_maintenance_record("{}"),
1531 Err(HarnessMaintenanceReceiptError::InvalidJson)
1532 );
1533 }
1534}
1535