Attenuation's axes all narrow by set-subtraction: remove actions, lower a
ceiling, shorten an expiry. They cannot express a condition the issuer did not
anticipate. `jcp-policy` can, but it is service-side — evaluated against a
PolicyContext the verifier must be told about, not carried in the capability.
A caveat is a condition embedded INSIDE the signed grant. Any enforcement point
holding it checks offline: no call back to the issuer, no shared policy config.
That is the distinction — policy is something a verifier must be told; a caveat
is something the token carries.
A design constraint found in the code rather than assumed: the predicate set is
bounded by what `decide` actually holds. jcp-policy's `Match::ContentContains`
CANNOT become a caveat, because the runtime sees a frame's `payload_hash` and
never its bytes. So a condition on content is expressible only as `PayloadIs`,
binding an exact payload rather than inspecting one. SPEC §4.9 states that
constraint normatively so implementations don't invent undecidable predicates.
Two variants express authority the fixed axes cannot:
- `PayloadIs` makes a grant ONE-SHOT — it authorizes one specific call, so a
leaked grant cannot be replayed against different arguments. `capability`
narrows to an action; it can never narrow to a call.
- `MaxCostUj` bounds a SINGLE invocation, where `budget_uj` is cumulative — so a
delegate cannot burn the whole remaining budget in one call.
Plus ActionIn (may narrow below the capability), CallerIs, NotBefore, NotAfter.
Enforcement is in decide/decide_priced with the real cost. The two-phase
`authorize` path skips only the cost-dependent caveat — the cost is unknowable
there — and enforces every other; that binds at decide time and is documented
rather than silently ignored.
Attenuation is monotone: `attenuate` INHERITS the parent's caveats (dropping
them would widen a delegate's authority — the same amplification the dual-ceiling
work had to guard), `attenuate_caveated` appends, and `caveats_narrow` refuses a
child that lost one. Adding the field with no default made the compiler flag
every Grant construction site, which is how three bad auto-patches into function
parameter lists surfaced as build errors instead of silent breakage.
Empty caveat set ⇒ omitted from the canonical encoding, so a grant without
caveats is byte-identical to one minted before they existed — content id and
signature unchanged, asserted by test and by C24.
42 suites green, clippy clean, all conformance checks pass including C24.
Co-Authored-By: Claude Opus 4.8 <[email protected]>