jcp-wire: the endpoint decides at its own clock, and a captured request is decided once #322
Merged
dcharlot
merged 1 commit from 2026-10-18 19:02:35 -04:00
fix/jcp-wire-endpoint-clock-and-replay into main 2026-10-18
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
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]>