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
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]>
230 lines
11 KiB
Markdown
230 lines
11 KiB
Markdown
# 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.
|