mesh: a grant is decided by JCP's runtime, so expiry, caveats, the model and spend bind #294

Merged
dcharlot merged 2 commits from fix/mesh-grant-decided-by-jcp into main 2026-10-17 17:07:15 -04:00
Owner

The defect

Mesh honoured a JCP grant by checking three things: the issuer signature, that the capability permits infer on the request, and that the grant's budget covered the request's ceiling. Nothing else was checked:

  • Expiry: an expired grant was served.
  • Caveats: none was evaluated, so NotAfter, NotBefore, PayloadIs, MaxCostUj and the new ModelIn (#285) were ignored at Mesh.
  • Spend: nothing was ever debited, so one grant paid for any number of serves.

mesh-core had no unit test of its grant path.

The fix

The steward holds a jcp-runtime Runtime and decides every grant in JCP's two-phase metered path, the same code every other JCP enforcement point uses:

  1. 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).
  2. Authorise, before any work. The frame names infer on the request's content id, 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.
  3. 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, which is omitted when absent.
  4. Model check. An engine that runs other weights than the ones it named before serving is refused a receipt, because the grant was decided over the named ones.

Engines name their weights through a new ServingEngine::weights_digest(), which defaults to None, so existing engines are unchanged.

The refusals that existed keep their errors (GrantInvalid, Unauthorized, BudgetTooLow). BudgetTooLow now reports what the grant has left. The spec (§2, §6.1, §7, §8) and the README describe the new phase order.

Stated, not fixed

These are now written into SPEC §8:

  • Bearer grants. Mesh does not authenticate the caller, so a grant works as a bearer token of its subject.
  • Spend is per steward. The debit ledger is in memory, so a restart or a second steward spends the grant again.

Checked

On the build host:

  • mesh-rs: 65 tests pass, six of them new. They cover expiry and time caveats, spend across serves, ModelIn routing, a swapped model, a per-call cost breach, and the refusals that already existed.
  • Vectors: every regenerated mesh/1 vector is byte-identical to mesh/conformance/v1.
  • Features: mesh-engine-gguf/gguf and mesh-openai/server build.
  • Mutations: removing the debit, a router that ignores the filter, ignoring the other denials, and skipping the model check each fail tests.
  • Clippy: clean on mesh-core, mesh-router, mesh-engine and mesh-receipt.

🤖 Generated with Claude Code

## The defect Mesh honoured a JCP grant by checking three things: the issuer signature, that the capability permits `infer` on the request, and that the grant's budget covered the request's ceiling. Nothing else was checked: - **Expiry:** an expired grant was served. - **Caveats:** none was evaluated, so `NotAfter`, `NotBefore`, `PayloadIs`, `MaxCostUj` and the new `ModelIn` (#285) were ignored at Mesh. - **Spend:** nothing was ever debited, so one grant paid for any number of serves. mesh-core had no unit test of its grant path. ## The fix The steward holds a jcp-runtime `Runtime` and decides every grant in JCP's two-phase metered path, the same code every other JCP enforcement point uses: 1. **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`). 2. **Authorise,** before any work. The frame names `infer` on the request's content id, 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. 3. **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`, which is omitted when absent. 4. **Model check.** An engine that runs other weights than the ones it named before serving is refused a receipt, because the grant was decided over the named ones. Engines name their weights through a new `ServingEngine::weights_digest()`, which defaults to `None`, so existing engines are unchanged. The refusals that existed keep their errors (`GrantInvalid`, `Unauthorized`, `BudgetTooLow`). `BudgetTooLow` now reports what the grant has left. The spec (§2, §6.1, §7, §8) and the README describe the new phase order. ## Stated, not fixed These are now written into SPEC §8: - **Bearer grants.** Mesh does not authenticate the caller, so a grant works as a bearer token of its subject. - **Spend is per steward.** The debit ledger is in memory, so a restart or a second steward spends the grant again. ## Checked On the build host: - **mesh-rs:** 65 tests pass, six of them new. They cover expiry and time caveats, spend across serves, `ModelIn` routing, a swapped model, a per-call cost breach, and the refusals that already existed. - **Vectors:** every regenerated `mesh/1` vector is byte-identical to `mesh/conformance/v1`. - **Features:** `mesh-engine-gguf/gguf` and `mesh-openai/server` build. - **Mutations:** removing the debit, a router that ignores the filter, ignoring the other denials, and skipping the model check each fail tests. - **Clippy:** clean on mesh-core, mesh-router, mesh-engine and mesh-receipt. 🤖 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]>
joulehook-rs: lock mesh-core's new dependency on jcp-runtime
All checks were successful
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 57s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
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) Successful in 1m24s
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 2m49s
3e2e3f85f3
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!294
No description provided.