jcp-wire: the endpoint's spend, revocations and memory of decided requests survive a restart #326

Merged
dcharlot merged 1 commit from feat/jcp-wire-durable-endpoint into main 2026-10-18 19:46:48 -04:00
Owner

Stacked on #322. This branch is #322's head plus one commit, a linear stack with no merge commit. Merge #322 first.

The gap

The jcp/1 endpoint held everything it enforces in memory. A restart therefore:

  • gave every grant its budget back;
  • made every revoked grant valid again;
  • forgot which requests it had decided, so a captured request still inside #322's freshness window could be decided again.

JCP §4.11 requires an enforcement point that can restart to record its decisions durably and re-establish them before deciding anything new.

The fix

  • Recording: Endpoint keeps a Journal. Each admitted request is recorded (Decided { at, request }) before it is decided. A request that cannot be recorded is denied unrecorded (a new DenyReason), debiting nothing and remembered as nothing.
  • Revocation: Endpoint::revoke verifies a revocation, records it, then applies it. One that does not verify is neither recorded nor applied.
  • Replay: Endpoint::with_journal replays the records through the gate, each request at the time it was decided. That re-establishes the spend, the revocations, and the memory of requests still in the window. A request whose signature fails, or that was decided twice, makes the journal Inconsistent.
  • File journal: FileJournal uses the wire's own framing (length prefix, canonical CBOR) and syncs per record. A torn final record is dropped and cut from the file.
  • SPEC §11 ties the endpoint to §4.11.

Checked

On the build host:

  • jcp-wire: 14 tests pass, five of them new:
    • a restart keeps the spend;
    • a revocation survives a restart;
    • a request decided before a restart is still refused;
    • a request that can't be recorded isn't decided;
    • the file journal drops a torn record.
  • jcp-rs: 522 tests pass, and the conformance suite passes all checks.
  • Clippy is clean.
  • Mutations: each fails tests:
    • replay skipping decisions
    • a request not recorded
    • a revocation not recorded
    • replay forgetting requests
  • Repository checks: tests-in-CI, spec references, vanity metrics and orphan sources pass locally.

🤖 Generated with Claude Code

> **Stacked on #322.** This branch is #322's head plus one commit, a linear stack with no merge commit. Merge #322 first. ## The gap The `jcp/1` endpoint held everything it enforces in memory. A restart therefore: - **gave every grant its budget back;** - **made every revoked grant valid again;** - **forgot which requests it had decided,** so a captured request still inside #322's freshness window could be decided again. JCP §4.11 requires an enforcement point that can restart to record its decisions durably and re-establish them before deciding anything new. ## The fix - **Recording:** `Endpoint` keeps a `Journal`. Each admitted request is recorded (`Decided { at, request }`) **before** it is decided. A request that cannot be recorded is denied `unrecorded` (a new `DenyReason`), debiting nothing and remembered as nothing. - **Revocation:** `Endpoint::revoke` verifies a revocation, records it, then applies it. One that does not verify is neither recorded nor applied. - **Replay:** `Endpoint::with_journal` replays the records through the gate, each request at the time it was decided. That re-establishes the spend, the revocations, and the memory of requests still in the window. A request whose signature fails, or that was decided twice, makes the journal `Inconsistent`. - **File journal:** `FileJournal` uses the wire's own framing (length prefix, canonical CBOR) and syncs per record. A torn final record is dropped and cut from the file. - **SPEC §11** ties the endpoint to §4.11. ## Checked On the build host: - **jcp-wire:** 14 tests pass, five of them new: - a restart keeps the spend; - a revocation survives a restart; - a request decided before a restart is still refused; - a request that can't be recorded isn't decided; - the file journal drops a torn record. - **jcp-rs:** 522 tests pass, and the conformance suite passes all checks. - **Clippy** is clean. - **Mutations:** each fails tests: - replay skipping decisions - a request not recorded - a revocation not recorded - replay forgetting requests - **Repository checks:** tests-in-CI, spec references, vanity metrics and orphan sources pass locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jcp-wire: the endpoint decides at its own clock, and a captured request is decided once
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 12s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m21s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m44s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m14s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m12s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m37s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m17s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m20s
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 1m6s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 2m9s
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 2m49s
6cf9023940
The `jcp/1` endpoint decided every request at the `now` the request
carried, and debited it every time it arrived. The request is signed by
its caller, the grant's subject, so the caller chose both:
- **Time.** A caller could stamp a request before its grant expired, or
  inside a `NotAfter` it had since left, and be decided as of that
  moment.
- **Replay.** A captured request presented again was decided again and
  debited again. SPEC §11 described that as intended ("replays into the
  same deterministic decision and the same budget accounting").

The fix:
- **Clock.** `Endpoint` decides at its own clock (`with_clock`; the
  system clock by default). A request's `now` is a freshness stamp: one
  more than the window (`with_skew`, default 300 s) from the clock is
  denied `stale-request` and sealed like any other denial.
- **Replay.** Within the window the endpoint remembers each request it
  decided, by content id. One presented again is denied `replayed`,
  neither decided nor debited. Once a stamp leaves the window it is
  forgotten, and a replay of it is stale, so memory stays bounded.
- **Deny reasons.** `DenyReason::StaleRequest` and `DenyReason::Replayed`
  are new, with their labels.
- **Spec.** SPEC §11 says the endpoint's clock decides, a request is
  decided once, and the cost is the holder's measurement. The endpoint
  debits the declared cost as given, so the requester must be the party
  that metered the action, the grant's holder at the enforcement point
  (as a Mesh steward is), never the governed agent reporting its own
  consumption.
- **Conformance.** C8 covers a replay, a stale request, and a grant that
  expires between a request's stamp and the endpoint's clock.

jcp-wire's only users are the conformance suite and jcp-threshold's test.
Both now pin the endpoint's clock to the stamps their requests carry.

Checked on the build host:
- jcp-rs: 517 tests pass, three of them new in jcp-wire. The conformance
  suite passes all checks, including C8 and C13.
- Every vector is byte-identical, and clippy is clean.
- Every workspace depending on jcp-schema builds: mesh, jouleclaw,
  joule-code, sandbox, joule-ui, joulehook, jouledesk.
- Mutations each fail tests: deciding at the caller's `now` (also fails
  C8), deciding a replay again (also fails C8), and no freshness window
  (also fails C8).
- jcp-gateway's process-tree energy tests flaked twice under host load
  during full runs; alone they pass every time, and this change does not
  touch that crate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
jcp-wire: the endpoint's spend, revocations and memory of decided requests survive a restart
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 5s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 48s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m17s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m32s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m11s
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 6m47s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m11s
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 1m51s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 4m31s
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 6m11s
2d293b1e24
The `jcp/1` endpoint held everything it enforces in memory. A restart
therefore:
- gave every grant its budget back,
- made every revoked grant valid again, and
- forgot which requests it had decided, so a captured request still inside
  the freshness window (#322) could be decided again.

JCP §4.11 requires an enforcement point that can restart to record its
decisions durably and re-establish them before deciding anything new.

- **Recording.** `Endpoint` keeps a `Journal`. Each admitted request is
  recorded (`Decided { at, request }`) before it is decided. A request that
  cannot be recorded is denied `unrecorded` (new `DenyReason`), debiting
  nothing and remembered as nothing.
- **Revocation.** `Endpoint::revoke` verifies a revocation, records it,
  then applies it. A revocation that does not verify is neither recorded
  nor applied.
- **Replay.** `Endpoint::with_journal` replays the records through the
  gate, each request at the time it was decided. That re-establishes the
  spend, the revocations, and the memory of requests still in the window.
  A request whose signature fails, or that was decided twice, makes the
  journal `Inconsistent`.
- **File journal.** `FileJournal` uses the wire's own framing (length
  prefix, canonical CBOR) and syncs per record. A torn final record is
  dropped and cut from the file.
- **Spec.** SPEC §11 ties the endpoint to §4.11.

Checked on the build host:
- jcp-wire: 14 tests pass, five of them new. They cover a restart keeping
  spend, a revocation surviving, a decided request still refused after a
  restart, an unrecordable request, and a torn file record.
- jcp-rs: 522 tests pass, and the conformance suite passes all checks.
  Clippy is clean.
- Mutations each fail tests: replay skipping decisions, a request not
  recorded, a revocation not recorded, and replay forgetting requests.

Stacked linearly on #322 (its head plus this commit).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!326
No description provided.