| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
Some checks failed
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 13s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Failing after 2m24s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 1m46s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 6m7s
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 59s
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 2m38s
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 1m39s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m12s
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 2m26s
SPEC §3.3 is normative: "If the action carries an estimable cost, the
grant's joule budget MUST be ≥ the estimate; over budget is `deny`. On
`allow`, the spent amount is debited."
The debit lived in the `LocalToolGate` that `gate()` constructs, and
`gate()` runs once per process — this binary is a PreToolUse command the
host spawns per action. So the spend total died with the process on every
single call and the next action started again at the full
`JOULEHOOK_BUDGET_UJ`. **There was no sequence of actions that could
exhaust the budget**, so the deny branch could never be taken, and
`conformance/v1/receipt.json` records the symptom: `budget_remaining_uj:
1000000000`, the untouched ceiling.
Worse, the deny branch that *was* reachable reported the wrong thing: a
single message, "is not in the granted capability set", covered every
denial — so a budget denial would have been recorded as a capability
denial if one could ever have happened.
The hook already wrote the evidence and never read it: sealed receipts
are appended to `JOULEHOOK_RECEIPT_LOG`, hex per line.
- `LocalToolGate::resume` replays a sealed stream into the gate's spend
ledger via `jcp_receipt::replay_spend`, so only verified allow-receipts
of this gate's own steward contribute debits. `Bridge` gains
`steward()`, `steward_pubkey()` and `restore()` to make that possible.
The grants are deterministic (fixed issuer key, `nonce: [0; 16]`,
`expiry: 0`), which is exactly why a later process rebuilds
byte-identical grant ids for the debits to land on — and exactly why
the budget refilled without them.
- `gate()` replays the stream before deciding, and the allow reason now
records the spend and how many receipts it replayed.
- A budget denial says it is a budget denial.
- **No stream configured** ⇒ the ceiling covers one call only. That is
now disclosed in the allow reason rather than asserted: §3.3 states
the budget MUST hold, and per-call it cannot. The crate's env doc
previously carried the disclosure ("Cross-call budget accounting needs
a persistent gate") while the SPEC's MUST did not; the doc is updated
to point at the stream that now provides it.
- **A stream that does not decode denies.** The prior spend is then
unknown, the budget cannot be asserted, and allowing anyway is the
refill the replay exists to prevent — fail closed, as SPEC §8 has this
binary do for an unparseable payload and an unrecognised shape.
A missing file is simply the first call.
The headline test runs three separate `run()` calls — each a fresh gate,
which is what a per-action process is — persisting between them as
`main` does: 2 mJ, 2 mJ, 2 mJ against a 5 mJ ceiling gives allow, allow,
**deny**, with the denial naming the budget. Previously that third call
allowed, and so would the three-hundredth.
Co-Authored-By: Claude Opus 5 <[email protected]>
|
||
| .. | ||
| joulehook-hook | ||
| joulehook-mcp | ||
| joulehook-meter | ||
| joulehook-proxy | ||