open-standards/jcp/jcp-rs/crates/jcp-action-receipt/docs/AR-1-spec.md
Transaction Science 62a81b3472
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 44s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m22s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m41s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m23s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m11s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m7s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m54s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m16s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m37s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m42s
mesh/1 + AR-1: receipts commit to the output; deterministic serves replay
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]>
2026-10-21 04:35:32 -04:00

230 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
1. **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.
2. **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.
3. **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.
4. **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
1. Parse the tagged COSE_Sign1 envelope; reject any `alg` other than EdDSA (−8).
2. Check `kid` against the expected steward.
3. Verify the signature over the canonical signed core.
4. 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.
5. Check the profile tag.
6. **Apply the three rules to the decoded projection.**
7. 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_hex` and `core_hex` byte-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.