A mesh receipt bound what was asked (request_id) and what it cost, and
nothing about what came back, so no one could later show which answer a
receipt was for. AR-1 had the same gap beside input_hash.
mesh/1 (SPEC §6.4):
- output_commitment: a salted BLAKE3 commitment over the request id, the
digest of the prompt payload the engine was handed, and the output tokens.
It is a commitment, not a hash, because a short output is reversible from
its hash. The opening is derived per request from a key drawn from the
steward's signing key, so a restarted steward can still disclose it.
The field is absent for a typed decision, whose answer the engine
interface does not carry, and omitted when absent, so older receipts keep
their bytes.
- The payload is bound because prompt_hash addresses the prompt's bytes
while an engine is handed token ids, so request_id alone cannot confirm
what a replay runs on.
- ServingEngine::deterministic (default false; the reference engine true).
- mesh_core::replay re-executes a sealed serve. Its verdicts:
- Verified: the re-run opens the commitment.
- Diverged { first_difference }: only when a disclosed output opens the
commitment on the same payload and opening, so the mismatch is the
serve's and not the replayer's input.
- NotReplayable(why): the engine samples; another backend, recipe or
weights; another request; no commitment; or a mismatch nothing
attributes.
- The conformance pack is regenerated (the reference receipt gains the
field; the envelope grows from 663 to 747 bytes).
AR-1: output_hash, an optional field after input_hash, sealed only when
bound (§2.3). It recommends a commitment over a plain hash and names
mesh's output_commitment as one. The published AR-1 vectors regenerate
byte-identical.
Also: ar-verify's `len() % 2 == 0` becomes `is_multiple_of(2)`, a clippy
error that predates this change, so the crate is clippy-clean.
Tests:
- mesh: the commitment binds request, opening, payload and tokens; a
generation commits and a decision does not; deterministic replay
verifies and locates a drift; replay refuses what it cannot settle,
including a wrong payload with the honest output disclosed; a restarted
steward reopens.
- AR-1: output_hash sealed when bound and absent otherwise.
- Mutation testing: 20 mutants, all killed (13, then 7 more after
binding the payload; one of the 13 targets code since replaced).
Verified on the build host:
- mesh-rs: 94 tests plus 19 behind features; clippy and rustdoc clean.
- jcp-rs: 565 tests and the conformance binary; jcp-action-receipt
clippy-clean.
- Repo checks pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
11 KiB
Action Receipt Specification (AR-1) — Draft
Status: Draft. Profile tag ar/1-draft. Field names are settled against the
existing canonical projections; the tag becomes ar/1 when the vector pack has
been reviewed.
Conformance pack: jcp/conformance/v1/action_receipt.json,
action_receipt_meta.json — 7 positive and 3 negative vectors, signed with a
published fixture key.
0. Why this exists
Three receipt families already ship in this tree. Each records one quarter of what an audit actually asks for, and none of them binds to the others:
| Family | Records | Carries |
|---|---|---|
jcp-receipt |
authority | grant chain, verdict, budget drawn |
sandbox-proof |
environment | attestation, isolation tier, substrate |
| JCR-1 | work | input hash, joules, and how they were obtained (energy provenance) |
| (nothing) | effect | what changed, and whether it can be undone |
So no single artifact answers: under what authority did this agent do that, where did it run, what did it cost, and what did it change?
The receipt encoding is implemented six independent times across the tree. That is evidence the encoding is portable, and evidence the composition was never specified. AR-1 specifies it.
Two of the three families already gesture at the chain — the sandbox proof cell
carries a receipt_id, the authority receipt carries a payload_hash. The
intent was there. The binding was not.
1. Design invariants
- Envelope, not replacement. Each family keeps its own canonical encoding and its own signer. AR-1 reaches into them by content-id; they never reach back. Every existing verifier continues to work unchanged.
- No new cryptography. Canonical det-CBOR (RFC 8949 §4.2.1) and the tagged COSE_Sign1 envelope are reused verbatim from JCR-1. If AR-1 ever appears to need a novel primitive, the design has gone wrong.
- Refusal over record-keeping. A format that merely records claims is a log. AR-1 defines three shapes that MUST be refused at verification, so a violation is detectable rather than a matter of trust.
- Never claim beyond the evidence. Every field that asserts a property must be falsifiable from the receipt itself or from an artifact it binds.
2. The signed core
Canonical projection, det-CBOR encoded, signed as COSE_Sign1 with EdDSA
(COSE alg −8). Absent optional fields are omitted, not nulled — JCR-1 §3.1
drops nulls from the signed core, so an emitted null would not round-trip.
ActionReceipt {
ar_receipt: "ar/1-draft" // profile tag
closed_at: Timestamp // RFC 3339
// binding 1 — authority
grant_id: ContentId
chain_ids: [ContentId]? // root first; omitted when empty
authority_receipt: ContentId
verdict: "allow" | "deny"
// binding 2 — environment
attestation_id: ContentId
proof_cell_id: ContentId?
tier: "S0".."S7"
grounding: "hardware" | "software"
// binding 3 — work
work_receipt: ContentId
input_hash: Hash
output_hash: Hash? // a commitment to what the work returned (§2.3)
joules_uj: u64 // the work's cost, graded by energy_provenance
energy_provenance: "HwShunt" | "ModelBased" | "Estimator"
// binding 4 — effect (new; nothing else records this)
effect: {
class: "none" | "reversible" | "compensable" | "irreversible"
target: ContentId
before: Hash?
after: Hash?
compensator: ContentId?
plan_receipt: ContentId?
}
// binding 5 — egress (the router's per-request privacy contract)
egress: {
class: "none" | "attested_enclave" | "third_party"
bytes: u64
peer: ContentId?
}
steward: String // REQUIRED, as in JCR-1 §2.1
}
Receipt id = BLAKE3 over the canonical signed core.
2.1 grounding is carried, never upgraded
grounding reports whether the bound attestation is rooted in silicon or in
software. It is copied verbatim from that attestation. An implementation MUST
NOT promote software to hardware under any circumstance — the field exists
precisely so that a software-rooted claim stays legible as one.
2.2 energy_provenance never collapses
HwShunt, ModelBased and Estimator are three different claims about how a
joule figure was obtained. An implementation MUST NOT sum or average across
them into a single undifferentiated number. A measured figure and an estimated
one are not interchangeable, and a receipt that treats them as such is lying by
arithmetic.
joules_uj is the work's cost however it was obtained. It is a hardware
reading only when energy_provenance is HwShunt, and a verifier or display
MUST NOT present it as a measurement otherwise.
2.3 output_hash commits to what came back
Without it a receipt shows what went in (input_hash) and what changed in the
world (effect), and not what the work returned. It therefore cannot show
which answer it was for. Where the producer binds the output, it seals
output_hash. It is omitted when absent, so a receipt sealed before the field
existed keeps its bytes.
A producer SHOULD seal a commitment rather than a plain hash wherever the
output can be short. A plain hash of a one-token answer can be reversed by
trying every answer, and a receipt is shown to parties who were not owed the
output. mesh/1's output_commitment (mesh SPEC §6.4), which commits under
an opening its steward discloses per receipt, is such a commitment and can be
carried here as it is.
3. The three rules
These MUST be enforced at verification, re-derived from the wire projection — not from a caller-supplied struct. A receipt hand-forged to violate them signs correctly and must be rejected anyway. The negative vectors in the pack exist to prove an implementation does this.
| # | Rule | Rationale |
|---|---|---|
| 1 | effect.class == "irreversible" REQUIRES effect.plan_receipt |
The cascade's body-tier design already separates plan from commit for actions that touch the world. AR-1 binds that discipline into the evidence: an irreversible commit with no preceding plan is a protocol violation, not a missing nicety. |
| 2 | egress.class == "none" REQUIRES egress.bytes == 0 |
A claim that nothing left the machine is falsified by its own byte count. |
| 3 | egress.class == "attested_enclave" REQUIRES egress.peer |
The router may never claim a stronger class than it can bind evidence for. An unattested destination is third_party even when the operator believes it trustworthy. Trust is not an egress class. |
4. Effect classes
| Class | Contract | Enforcement |
|---|---|---|
none |
Read-only. Nothing changed. | Trivially safe to replay |
reversible |
before recorded; prior state restorable. |
Undo is a state transition |
compensable |
Cannot be undone, but a compensating action exists and its grant is named at the time of acting — not discovered afterwards. | compensator bound in the receipt |
irreversible |
Once committed, permanent. | Rule 1: plan/commit separation |
AR-1 does not solve effect revocation — nothing does, in general. Revoking authority does not un-send an email or un-move funds. What AR-1 does is make the distinction explicit and machine-readable at the moment of action, which is the precondition for any system that wants to reason about it afterwards.
5. Egress classes
| Class | Means | Verifiable by |
|---|---|---|
none |
Resolved on-device. | Rule 2, plus tier and attestation |
attested_enclave |
Left the device to somewhere that proved what it was running. | Rule 3 — the peer attestation id |
third_party |
Left the device to an unattested destination. | Named and counted, honestly marked unverifiable |
6. Verification flow
- Parse the tagged COSE_Sign1 envelope; reject any
algother than EdDSA (−8). - Check
kidagainst the expected steward. - Verify the signature over the canonical signed core.
- Decode the core as canonical det-CBOR. Reject trailing bytes — a signed core is exactly one item, and anything appended is corruption or an attempt to smuggle data past a verifier that stops at the first complete value.
- Check the profile tag.
- Apply the three rules to the decoded projection.
- Optionally resolve each bound content-id and verify the underlying receipt in its own family's verifier. AR-1 binds; it does not re-verify on their behalf.
Steps 1–6 require only the envelope and a public key. That is what makes a standalone verifier possible, and shipping that verifier is what turns an internal convention into something a third party can adopt.
7. Conformance
An implementation conforms at AR-1/basic when, given the published pack:
- every positive vector verifies, and the re-derived receipt id and canonical
core match the published
id_hexandcore_hexbyte-for-byte; - every negative vector is rejected, despite carrying a genuine signature.
The negatives are the substance. An implementation that checks signatures and skips the structural rules passes the positives and fails the pack — which is exactly the failure mode the pack is designed to catch.
8. What AR-1 deliberately does not do
- Key custody and recovery. Unsolved everywhere; AR-1 must not be built in a way that presumes a solution.
- Re-verifying bound receipts. Each family owns its own verification. AR-1 binds content-ids and stops there.
- Proving the energy figure is honest. It carries provenance so a reader can judge; it does not adjudicate.
- Undoing effects. See §4.
9. Security considerations
A valid signature is not a valid receipt. The three rules exist because signature verification alone accepts well-formed lies. The negative vectors demonstrate this concretely: each is signed with the real fixture key and each must be refused.
Content-id binding is only as strong as the bound artifacts. AR-1 proves which authority receipt, attestation and work receipt were referenced. It does not prove those artifacts are themselves sound — that is their own verifiers' job, and a complete audit runs all of them.
The energy cross-check is a consistency check, not a proof. An attestation
asserts; joules are metered, and where energy_provenance is HwShunt they are
read off an independently sampled counter. Binding both means a forged
attestation must also maintain a plausible energy profile across that counter,
and that the failure is visible rather than silent. An Estimator figure offers
no such cross-check, which is why it is labelled. An adversary with physical
access who can forge a quote can also influence measurement. Claiming more than
this would be exactly the overreach the design exists to avoid.