jcp: a chain is bounded in money on every link, and a delegated grant is never decided alone #304

Merged
dcharlot merged 1 commit from fix/jcp-chained-money into main 2026-10-17 17:03:10 -04:00
Owner

Stacked on #300. Merge #300 first. This branch is #300's exact commit plus one, so it merges cleanly after it.

Two gaps in deciding a delegated grant

A chain could not be priced. The single-grant path has decide_priced, which enforces the money ceiling beside the joule ceiling (§4.8). Chains had no priced decision at all, so a delegated grant's money ceiling was never consulted: not the leaf's, and not the parent's whose budget the leaf draws on.

A delegated grant could be decided on its own. decide, decide_priced, authorize and the flow variants accepted a grant with a parent, decided it as a root, and never saw the chain. §4.2 says a delegated grant MUST be decided with its full chain. Alone, nothing shows that it narrows what its issuer held, so its budget, caveats and expiry were taken at their word.

The fix

  • decide_chained_priced applies both ceilings to every link:
    • joules first, on every link;
    • then the money ceiling of every link that has one, refusing a cost in another currency.
    • An allow debits both dimensions from every link, so siblings share their parent's money as they share its joules. A deny debits neither. A chain with no money ceiling ignores the price, as a single grant does.
  • Single-grant decisions deny a grant with a parent as chain-broken. Nothing in jcp-rs or its consumers relied on the old behaviour.
  • SPEC §4.2 states both rules, and C22 covers a priced chain and a delegated grant decided alone.

Checked

On the build host:

  • jcp-rs: 504 tests pass, three of them new. The conformance suite passes all checks.
  • Vectors: every vector gen-vectors writes is byte-identical.
  • Consumers: jouleclaw-jcp, joule-code-jcp, sandbox-jcp, mesh-core and joule-ui-interaction pass their tests.
  • Mutations: each fails tests:
    • allowing a lone delegated grant (also fails C22)
    • skipping the money check on chains (also fails C22)
    • debiting money on the leaf only
  • Clippy: nothing new.

🤖 Generated with Claude Code

> **Stacked on #300.** Merge #300 first. This branch is #300's exact commit plus one, so it merges cleanly after it. ## Two gaps in deciding a delegated grant **A chain could not be priced.** The single-grant path has `decide_priced`, which enforces the money ceiling beside the joule ceiling (§4.8). Chains had no priced decision at all, so a delegated grant's money ceiling was never consulted: not the leaf's, and not the parent's whose budget the leaf draws on. **A delegated grant could be decided on its own.** `decide`, `decide_priced`, `authorize` and the flow variants accepted a grant with a `parent`, decided it as a root, and never saw the chain. §4.2 says a delegated grant MUST be decided with its full chain. Alone, nothing shows that it narrows what its issuer held, so its budget, caveats and expiry were taken at their word. ## The fix - **`decide_chained_priced`** applies both ceilings to every link: - joules first, on every link; - then the money ceiling of every link that has one, refusing a cost in another currency. - An allow debits both dimensions from every link, so siblings share their parent's money as they share its joules. A deny debits neither. A chain with no money ceiling ignores the price, as a single grant does. - **Single-grant decisions deny a grant with a `parent`** as `chain-broken`. Nothing in jcp-rs or its consumers relied on the old behaviour. - **SPEC §4.2** states both rules, and **C22** covers a priced chain and a delegated grant decided alone. ## Checked On the build host: - **jcp-rs:** 504 tests pass, three of them new. The conformance suite passes all checks. - **Vectors:** every vector `gen-vectors` writes is byte-identical. - **Consumers:** jouleclaw-jcp, joule-code-jcp, sandbox-jcp, mesh-core and joule-ui-interaction pass their tests. - **Mutations:** each fails tests: - allowing a lone delegated grant (also fails C22) - skipping the money check on chains (also fails C22) - debiting money on the leaf only - **Clippy:** nothing new. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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]>
jcp: a chain is bounded in money on every link, and a delegated grant is never decided alone
All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m1s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m13s
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 1m14s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m19s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m18s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 1m4s
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 3m53s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 5m19s
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 9m23s
cedf49da69
Two gaps in how JCP decides a delegated grant.

A chain could not be priced. The single-grant path has `decide_priced`,
which enforces the money ceiling beside the joule ceiling (§4.8). Chains
had no priced decision at all, so a delegated grant's money ceiling was
never consulted: not the leaf's, and not the parent's whose budget the
leaf draws on.

A delegated grant could be decided on its own. `decide`, `decide_priced`,
`authorize` and the flow variants accepted a grant with a `parent`,
decided it as a root, and never saw the chain. §4.2 says a delegated grant
MUST be decided with its full chain. Alone, nothing shows it narrows what
its issuer held, so its budget, caveats and expiry were taken at their
word.

- `decide_chained_priced` applies both ceilings to every link. Joules are
  checked first on every link, then the money ceiling of every link that
  has one, refusing a foreign currency. An allow debits both dimensions
  from every link, so siblings share their parent's money as they share
  its joules. A deny debits neither. A chain with no money ceiling ignores
  the price.
- The single-grant checks deny a grant with a `parent` `chain-broken`.
  Nothing in jcp-rs or its consumers relied on the old behaviour.
- SPEC §4.2 states both rules, and C22 covers a priced chain and a
  delegated grant decided alone.

Checked on the build host:
- jcp-rs: 504 tests pass, three of them new, and the conformance suite
  passes all checks.
- Every vector is byte-identical.
- jouleclaw-jcp, joule-code-jcp, sandbox-jcp, mesh-core and
  joule-ui-interaction pass their tests.
- Mutations each fail tests and C22: allowing a lone delegated grant,
  skipping the money check on chains, and debiting money on the leaf
  only (that one fails tests).

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!304
No description provided.