mesh: a grant is spent only by its issuer, at the steward it names, and its spend survives a restart #301

Merged
dcharlot merged 1 commit from feat/mesh-caller-auth-durable-spend into main 2026-10-18 00:42:18 -04:00
Owner

Stacked on #294 and #300. Merge those first; this branch contains both exact commits, so it merges cleanly after them. Its own change is the last commit, mesh: a grant is spent only by its issuer….

What was left open after #294

Mesh decided a grant with JCP's runtime but honoured it as a bearer token: whoever presented it was taken to be its subject. The steward's spend lived in memory. So:

  • anyone who obtained a grant could spend it and receive the output;
  • the same grant spent its full budget again at every steward it was shown to;
  • a restart gave every grant its budget back.

Caller authentication (SPEC §2.1)

A grant is presented as a Presentation:

  • the delegation chain, root first, leaf last
  • a caller
  • a nonce
  • an Ed25519 signature over the request id, the leaf's id, the caller and the nonce

The steward refuses a presentation:

  • whose caller is not the leaf's issuer;
  • whose signature is not for the request being served;
  • that it has already spent (keyed by leaf + nonce), so a captured presentation cannot be replayed.

The steward holds the grant (SPEC §2.1)

The steward decides the chain as its holder, under the identity of its sealing key, so the leaf's subject must be this steward. A grant shown to another steward is refused NotThisSteward.

One budget over several stewards is sharded with jcp_grant::shard (JCP §4.5): one shard held by each steward, summing to no more than the parent, so the total stays bounded without coordination. Chains are decided with authorize_chained / charge_chained from #300, which check and debit every link.

Durable spend (SPEC §2.2, JCP §4.11)

The steward keeps a decision journal:

  • Before the engine runs, each decision is recorded as an Authorised entry. A decision that cannot be recorded is refused.
  • After the debit, a Debited entry is recorded.
  • On restart, Mesh::with_journal replays the journal through the gate: each authorisation is verified and decided again at its recorded time, and each debit is applied to its serve.
  • Inconsistent journals fail with JournalInconsistent: a replay the gate refuses, a debit with no authorisation, or a presentation spent twice.
  • In-flight serves (authorised, never debited) stay debited at their request's ceiling, are listed in unresolved() with the caller and the backend that held them, and are never retried.
  • FileJournal is append-only JSON lines, synced per append. A torn final line is dropped and cut from the file.

API

Mesh::serve and mesh-openai's complete_chat / complete_responses now take Option<&Presentation>. The demo and the falcon binary present their grants. The HTTP server presents none, as before. SPEC §2, §2.1, §2.2, §7 and §8 and the README describe all of it.

Checked

On the build host:

  • mesh-rs: 73 tests pass, eight of them new. They cover a grant held elsewhere, a wrong or misdirected caller, replay, shards across two stewards, restart resumption, an in-flight serve, a forged journal, and a torn file line.
  • Vectors: every regenerated mesh/1 vector is byte-identical.
  • Builds: the lockfile is current (--locked), the gguf and openai server features build, and the demo runs.
  • Clippy: clean on mesh-core and mesh-openai.
  • Mutations: each fails tests:
    • no caller check
    • no replay check
    • deciding as the grant's subject (bearer)
    • replay dropping debits
    • an in-flight serve not debited
    • replay trusting the journal

Not covered

  • Money ceilings on chains: chained decisions take no money cost, so budget_funds is not enforced on the chained path. Mesh charges no money, so it is unaffected.
  • Journal truncation: a journal is only as durable as its storage. A truncated journal (entries removed from the end) cannot be told apart from one that never had them.

🤖 Generated with Claude Code

> **Stacked on #294 and #300.** Merge those first; this branch contains both exact commits, so it merges cleanly after them. Its own change is the last commit, `mesh: a grant is spent only by its issuer…`. ## What was left open after #294 Mesh decided a grant with JCP's runtime but honoured it as a **bearer token**: whoever presented it was taken to be its subject. The steward's spend lived **in memory**. So: - anyone who obtained a grant could spend it and receive the output; - the same grant spent its full budget again at every steward it was shown to; - a restart gave every grant its budget back. ## Caller authentication (SPEC §2.1) A grant is presented as a `Presentation`: - the delegation chain, root first, leaf last - a caller - a nonce - an Ed25519 signature over the request id, the leaf's id, the caller and the nonce The steward refuses a presentation: - whose caller is not the leaf's issuer; - whose signature is not for the request being served; - that it has already spent (keyed by leaf + nonce), so a captured presentation cannot be replayed. ## The steward holds the grant (SPEC §2.1) The steward decides the chain **as its holder**, under the identity of its sealing key, so the leaf's subject must be this steward. A grant shown to another steward is refused `NotThisSteward`. One budget over several stewards is **sharded** with `jcp_grant::shard` (JCP §4.5): one shard held by each steward, summing to no more than the parent, so the total stays bounded without coordination. Chains are decided with `authorize_chained` / `charge_chained` from #300, which check and debit every link. ## Durable spend (SPEC §2.2, JCP §4.11) The steward keeps a decision journal: - **Before the engine runs,** each decision is recorded as an `Authorised` entry. A decision that cannot be recorded is refused. - **After the debit,** a `Debited` entry is recorded. - **On restart,** `Mesh::with_journal` replays the journal **through the gate**: each authorisation is verified and decided again at its recorded time, and each debit is applied to its serve. - **Inconsistent journals** fail with `JournalInconsistent`: a replay the gate refuses, a debit with no authorisation, or a presentation spent twice. - **In-flight serves** (authorised, never debited) stay debited at their request's ceiling, are listed in `unresolved()` with the caller and the backend that held them, and are never retried. - **`FileJournal`** is append-only JSON lines, synced per append. A torn final line is dropped and cut from the file. ## API `Mesh::serve` and mesh-openai's `complete_chat` / `complete_responses` now take `Option<&Presentation>`. The demo and the falcon binary present their grants. The HTTP server presents none, as before. SPEC §2, §2.1, §2.2, §7 and §8 and the README describe all of it. ## Checked On the build host: - **mesh-rs:** 73 tests pass, eight of them new. They cover a grant held elsewhere, a wrong or misdirected caller, replay, shards across two stewards, restart resumption, an in-flight serve, a forged journal, and a torn file line. - **Vectors:** every regenerated `mesh/1` vector is byte-identical. - **Builds:** the lockfile is current (`--locked`), the gguf and openai server features build, and the demo runs. - **Clippy:** clean on mesh-core and mesh-openai. - **Mutations:** each fails tests: - no caller check - no replay check - deciding as the grant's subject (bearer) - replay dropping debits - an in-flight serve not debited - replay trusting the journal ## Not covered - **Money ceilings on chains:** chained decisions take no money cost, so `budget_funds` is not enforced on the chained path. Mesh charges no money, so it is unaffected. - **Journal truncation:** a journal is only as durable as its storage. A truncated journal (entries removed from the end) cannot be told apart from one that never had them. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
mesh: a grant is decided by JCP's runtime, so expiry, caveats, the model and spend bind
Some checks failed
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Failing after 1m55s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 13s
standards workspaces build + test / macOS energy backends (pull_request) Has been skipped
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Has been cancelled
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) Has been cancelled
afde4f18ec
Mesh honoured a JCP grant by checking three things: the issuer signature,
that the capability permits `infer` on the request, and that the budget
covered the request's ceiling. Nothing else was checked:
- An expired grant was served.
- No caveat was evaluated, so `NotAfter`, `NotBefore`, `PayloadIs`,
  `MaxCostUj` and `ModelIn` were ignored.
- Nothing was ever debited, so one grant paid for any number of serves.
mesh-core had no unit test of the grant path.

The steward now holds a jcp-runtime `Runtime` and decides every grant in
JCP's two-phase metered path:
- **Route.** With a grant, a backend whose model the grant's `ModelIn`
  does not permit is inadmissible, and so is one that names no model. The
  route is the cheapest permitted backend, and the candidates record why
  the others were refused (`Router::route_where`).
- **Authorise,** before any work. The frame names `infer` on the request,
  the prompt hash as payload, and the chosen backend's weights as `model`.
  Signature, expiry, revocation and every cost-independent caveat apply,
  and what the grant has left must cover the request's ceiling.
- **Debit,** after metering. The metered joules are charged whatever
  happens next. A per-call cost caveat binds here, and a breach is sealed on
  the receipt as `grant_breach`.
- **Model check.** An engine that runs other weights than the ones it named
  before serving is refused a receipt.

Engines name their weights before serving through a new
`ServingEngine::weights_digest()`, which defaults to `None`.

The refusals that existed keep their errors (`GrantInvalid`,
`Unauthorized`, `BudgetTooLow`). `BudgetTooLow` now reports what the grant
has left. SPEC §2, §6.1, §7 and §8 and the README describe the new order.
§8 states that Mesh does not authenticate the caller, honouring a grant
as a bearer token of its subject, and that spend is per steward.

Checked on the build host:
- mesh-rs: 65 tests pass, six of them new, and every regenerated `mesh/1`
  vector is byte-identical.
- The gguf and openai server features build.
- clippy is clean on the changed crates.
- Mutations: no debit, a router that ignores the filter, ignoring other
  denials, and skipping the model check each fail tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
jcp: a chained decision enforces caveats, and a chain cannot drop one
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 9s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m21s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m57s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (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 1m34s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m9s
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 2m36s
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 1m9s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 2m1s
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 3m13s
f0952309c1
`decide_chained` evaluated no caveat at all: not the leaf's, not any
ancestor's. It is the decision path behind jcp-wire's endpoint (§11),
jcp-channel, jouleclaw-jcp and joule-code-jcp. Those always present a
chain, even a one-link chain for an undelegated grant. So `PayloadIs`,
`NotBefore`, `NotAfter`, `CallerIs`, `ActionIn`, `MaxCostUj` and
`ModelIn` were enforced only on the single-grant `decide`/`authorize`
path, which the conformance suite exercises.

Separately, `verify_chain` checked capability, budget, expiry and depth
at each hop, but not that a child carries its parent's caveats or no
wider money ceiling. `attenuate` refuses both when it mints. A chain
minted by hand verified anyway and conveyed the authority the caveat
withheld.

- `verify_chain` refuses a hop that drops a parent caveat
  (`caveats_narrow`) or widens or drops its money ceiling
  (`funds_narrows`). Both are denied `attenuation-violation`.
- The chained decision's checks move into `check_chained`, which now
  evaluates every link's caveats. Cost-dependent ones are evaluated when
  the cost is known.
- `authorize_chained` / `charge_chained` add the two-phase metered path
  for a chain. Authorize debits nothing and reports the least any link
  has left. Charge debits every link, so siblings share their parent as
  in `decide_chained`, and a per-call breach is reported with the joules
  still debited.
- SPEC §4.2 states both rules and the chained metered path. C7's
  conformance check covers an inherited caveat and a dropped one.

Checked on the build host:
- jcp-rs: 501 tests pass, and the conformance suite passes all checks.
- Every vector gen-vectors writes is byte-identical.
- jouleclaw-jcp, joule-code-jcp and sandbox-jcp pass their tests.
- mesh, joule-ui, joulehook and jouledesk compile.
- Mutations each fail tests: chains ignoring caveats, `verify_chain`
  allowing a dropped caveat, and a charge debiting only the leaf.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
dcharlot force-pushed feat/mesh-caller-auth-durable-spend from 18a3efda3b
Some checks failed
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) Failing after 1m39s
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
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) Has been cancelled
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Has been cancelled
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Has been cancelled
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Has been cancelled
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Has been cancelled
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Has been cancelled
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) Has been cancelled
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Has been cancelled
to 736548be93
All checks were successful
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m16s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 16s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 10m10s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m52s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m34s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m15s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m14s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m31s
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 3m6s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 4m29s
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 6m21s
2026-10-17 11:48:10 -04:00
Compare
dcharlot force-pushed feat/mesh-caller-auth-durable-spend from 736548be93
All checks were successful
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m16s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 16s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 10m10s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m52s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m34s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m15s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m14s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m31s
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 3m6s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 4m29s
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 6m21s
to f79fffd9fa
All checks were successful
wai-quantum tensor-engine member / does this change touch it (pull_request) Successful in 4s
wai-quantum portable tensor program / does this change touch it (pull_request) Successful in 4s
wai-quantum tensor-engine member / member · tensor engine, the kernel toolchain's simulator (linux) (pull_request) Has been skipped
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Has been skipped
wai-quantum portable tensor program / tensor program · CPU, mutants, GPU builds (pull_request) Has been skipped
wai-quantum portable tensor program / tensor program · GPUs (jetson-hub) (pull_request) Has been skipped
wai-quantum portable tensor program / tensor program · integrated GPU (framework-128gb) (pull_request) Has been skipped
wai-quantum portable tensor program / tensor program · Metal and the arm64 CPU (macos) (pull_request) Has been skipped
wai-quantum CPU vector units / does this change touch it (pull_request) Successful in 4s
wai-quantum CPU vector units / member · vector unit, rvv 1024-bit (riscv64, emulated) (pull_request) Has been skipped
wai-quantum CPU vector units / member · vector unit, sve 128-bit (aarch64, emulated) (pull_request) Has been skipped
wai-quantum CPU vector units / member · vector unit, rvv 128-bit (riscv64, emulated) (pull_request) Has been skipped
wai-quantum CPU vector units / member · vector unit, sve 2048-bit (aarch64, emulated) (pull_request) Has been skipped
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 4s
wai-quantum CPU vector units / member · vector unit, avx2 (x86_64 silicon) (pull_request) Has been skipped
wai-quantum CPU vector units / member · vector unit, sve 512-bit (aarch64, emulated) (pull_request) Has been skipped
wai-quantum CPU vector units / member · vector units, avx2 and avx512 (framework-128gb silicon) (pull_request) Has been skipped
wai-quantum CPU vector units / member · vector unit absent (macos) (pull_request) Has been skipped
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Has been skipped
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Has been skipped
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Has been skipped
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Has been skipped
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Has been skipped
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Has been skipped
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Successful in 4s
wai-web wasm32 / does this change touch it (pull_request) Successful in 4s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Has been skipped
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Has been skipped
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m34s
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 56s
2026-10-17 17:14:49 -04:00
Compare
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!301
No description provided.