jcp: hand the caller its receipt, in MCP's namespaced _meta slot #69

Merged
dcharlot merged 1 commit from jcp/mcp-meta-receipt into main 2026-09-03 15:12:27 -04:00

2026-08-29

David Charlot
jcp: hand the caller its receipt, in MCP's namespaced _meta slot
The gateway authorized every `tools/call`, metered it on real hardware, debited
the grant and sealed allow and deny alike into a JCR-1 chain — and the client
never saw any of it. The receipt lived in `gateway.receipts()`: the enforcing
party keeping an artifact about itself, which the subject of the decision could
not present, dispute, or forward. That is most of what a receipt is for.

MCP's 2026-07-28 revision supplies the place to put it. `_meta` is now a
first-class namespaced extension point with a 12-month deprecation policy behind
it, so a policy object carried there has a defined home rather than an invented
one, and the home stops moving.

- An allowed call returns its sealed receipt in `result._meta` under
  `science.transaction/jcp`, beside the upstream's own untouched payload, with
  the measured joules and the remaining budget.
- A denial returns its sealed refusal too, in `error.data._meta` — JSON-RPC
  reserves `data` for exactly this. A refusal the caller cannot see is an
  enforcement nobody can audit, and a receipt's value does not depend on the
  verdict going the caller's way.
- The witness moves from the hand-rolled flat `params._jcp_witness` into the
  namespaced slot, and the flat key keeps working. A silent behaviour change at
  an enforcement point is worse than a legacy key, so the fallback stays until
  something forces it out.

The returned bytes are the same sealed envelope the gateway logged, not a
summary of it — asserted directly rather than described. Attaching is defensive:
a response that is not a JSON-RPC result object comes back untouched, because
this must never mangle an upstream payload it does not understand.

Base64 is hand-rolled (no new dependency for one encode) and pinned against the
RFC 4648 §10 vectors, since a receipt that does not decode is not a receipt.

Found by the 2026-08-29 sweep, whose repo-check phase also refuted two things I
had queued as gaps — the IETF agent-audit-trail draft and a hardware-rooted
attestation paper are both already shipped, the draft being a strictly weaker
subset with an optional signature and no energy field.

Not in this change, and worth its own: MCP's new `CacheableResult`/`ttlMs` makes
a cache hit into joules avoided, and nothing in the family binds a TTL'd result
to an energy receipt.

Verified: 245 jcp-rs tests pass (5 new), conformance passes all classes,
published vectors unchanged on disk, no new clippy warnings.
2026-08-29 20:52:05 -04:00