Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:12:55.293Z 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

receipt.rs

1535 lines · 60.2 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.