open-standards/jcp/jcp-rs/crates
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science ec2292174b
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 12s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 4m13s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 1m20s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 27s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m48s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m19s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, -p sandbox-tee --features amd-kds,intel-pcs, sandbox-rs, linux) (pull_request) Successful in 1m22s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 19m53s
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 4m56s
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 1m7s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 7m18s
jcp §4.13: a retry is not a second call, where the caller names the call
§4.11 makes a restart give no budget back. This is the case one layer out: the
same call delivered twice.

An endpoint already recognised a repeat within its freshness window, by the
request's content address, and answered it from the record without debiting
again — so the mechanism was there, and this narrows it rather than inventing
it. The gap is what that key is made of. The content address covers the
caller's stamp, so it catches a redelivery that reproduced the request's bytes
exactly, and misses the one that matters: a transport whose retry is slow
enough that the first stamp has aged out, where the caller MUST re-stamp to be
admitted at all. That retry has a different address, is admitted as a new call,
and is debited again. The overspend §4.11 exists to prevent happens anyway, for
a reason §4.11 does not reach.

The cause is that identity was incidental — derived from the bytes — rather
than declared.

What changes

- `WireRequest::call_id`: 32 bytes the caller chose, the same across every
  delivery of one call and different between calls. Signed with the rest of the
  request, `Option` with `serde(default, skip_serializing_if)`, so a request
  without one has the bytes it always had and still verifies against the old
  signable bytes. A test pins both.
- Where a request carries one, `admit` recognises a repeat by `(caller,
  call_id)` before it looks at the bytes, and answers what the first delivery
  was answered without debiting the grant again.
- The index is re-established by the replay `with_journal` already performs, so
  a restart does not forget which calls it decided and debit their retries.
- `Endpoint::with_replay_horizon` sets how long a call id is remembered,
  defaulting to `DEFAULT_SKEW_SECS` so an endpoint that sets nothing behaves as
  it did. A `Called` entry is retained by **when it was decided**, not by the
  request's stamp: an entry pruned by the original stamp would already be gone
  when the re-stamped retry arrives, which is the whole case.

Three things the spec says that the code holds

**The id is scoped to its caller.** The key is the pair, never the id alone. A
global id space would let one caller suppress another's call by guessing its
id — a denial with no trace, since a suppressed call looks exactly like a
recognised repeat.

**An absent id is not a promise of uniqueness.** Without one, every delivery is
a distinct call. A host must not infer an identity the caller did not declare:
two byte-identical calls may be two calls the caller meant, and a test holds
that two ids that differ both happen.

**The guarantee is bounded, and says so.** Past the horizon a repeat is no
longer recognised. Remembering every call forever would need state forever; a
bounded guarantee that names its bound is honest where an unbounded one would
not be true. The horizon MUST be at least the longest retry of the transport in
front of it, or the mechanism does not cover the case it was added for.

What this deliberately does not add

Whether an action *may* be re-dispatched is a property of the action, not of the
id, and §4.11 already governs the unresolved case well: no silent retry, no
reporting an in-flight action as done or failed, surfaced naming the party that
held it. A declared id is what makes a re-dispatch *decidable* for a dispatcher
that does classify its actions — "safe to re-dispatch" means nothing until the
re-dispatch can be recognised as the same call. So no second replay class is
introduced here beside the one the dispatching layer already has. The spec
states the authority instead: the classification is the implementing party's,
the decision to act on it is the enforcement point's, and a model is never
asked whether an action already ran, because it would be guessing about state it
cannot observe and a guess is not evidence.

Incidentally, the signable-bytes variants collapse into one. `signed_bytes(&self)`
builds the canonical bytes from whichever optionals a request carries, and
`verify_caller` reads it instead of branching on `challenge`; `sign_parts` takes
both optionals and `sign`/`sign_challenged` delegate. Adding the third optional
field would otherwise have doubled the number of ways to ask the same question.

Conformance class C29.

Evidence, on the Framework box (nothing on the CI host, which is mid-matrix for
another PR): the pack reports `✓ C29 a retry is not a second call` and all
checks passed; 66 test suites green across the workspace; clippy `-D warnings
--no-deps` clean on both crates touched. The two `unused_mut` warnings clippy
reports in `jcp-energy` are pre-existing and in a crate this change does not
touch, so they are left for their own change.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-11-02 19:14:53 -05:00
..
jcp-a2a jcp-a2a: an unverified AP2 mandate could set the ceiling, and name any payee 2026-10-05 07:22:11 -04:00
jcp-action-receipt mesh/1 + AR-1: receipts commit to the output; deterministic serves replay 2026-10-21 04:35:32 -04:00
jcp-bridge jcp: WithinWindow, a sliding-window caveat over calls, payloads and measured joules 2026-10-21 07:30:20 -04:00
jcp-channel jcp: a ModelIn caveat says which model a grant authorizes, and the frame names the model it runs 2026-10-17 04:21:21 -04:00
jcp-cite jcp: a ModelIn caveat says which model a grant authorizes, and the frame names the model it runs 2026-10-17 04:21:21 -04:00
jcp-cli jcp: a ModelIn caveat says which model a grant authorizes, and the frame names the model it runs 2026-10-17 04:21:21 -04:00
jcp-conformance jcp §4.13: a retry is not a second call, where the caller names the call 2026-11-02 19:14:53 -05:00
jcp-efficiency efficiency/1: a partial energy figure is sealed as partial and never headlines as a task total 2026-10-20 13:58:36 -04:00
jcp-energy energy meters: a GPU counter reading low is refuted in sandbox-meter, eoc-meter and jcp-energy too 2026-10-20 04:15:36 -04:00
jcp-flow jcp-flow: Compute propagated provenance from tool bytes 2026-07-22 02:09:29 -04:00
jcp-gateway jcp-gateway: everything a gated request carries to the server is sealed and flow-checked 2026-10-19 11:23:23 -04:00
jcp-grant jcp: a chained decision enforces caveats, and a chain cannot drop one 2026-10-17 10:44:16 -04:00
jcp-intercept jcp-intercept: a reading meter meters a seam as one span, divided among its steps by CPU time 2026-10-13 02:41:41 -04:00
jcp-orchestrate jcp: a ModelIn caveat says which model a grant authorizes, and the frame names the model it runs 2026-10-17 04:21:21 -04:00
jcp-policy jcp-policy: a receipt sealed the policy's name, so the rules could be swapped under it 2026-10-05 07:22:12 -04:00
jcp-prompt jcp-prompt: a signed, content-addressed prompt registry 2026-06-27 19:57:46 -04:00
jcp-receipt jcp-gateway: replay our own receipt stream at startup (SPEC §8.1) 2026-10-17 11:42:06 -04:00
jcp-runtime jcp: WithinWindow, a sliding-window caveat over calls, payloads and measured joules 2026-10-21 07:30:20 -04:00
jcp-schema jcp: WithinWindow, a sliding-window caveat over calls, payloads and measured joules 2026-10-21 07:30:20 -04:00
jcp-threshold jcp-wire: the endpoint decides at its own clock, and a captured request is decided once 2026-10-18 13:40:16 -04:00
jcp-trace jcp-trace: an undefined wedge metric is not zero 2026-10-17 04:54:47 -04:00
jcp-wire jcp §4.13: a retry is not a second call, where the caller names the call 2026-11-02 19:14:53 -05:00
jcp-x402 jcp-x402: refuse a payment scheme this crate does not implement 2026-10-22 07:27:44 -04:00