The gate's decision logic was reachable only through wrap()/GatedStage —
fine for a Stage-based cascade, but a consumer whose tiers are async
(a model call, a remote ACP turn) can't express them as the sync Stage
trait. Two small additions open the same gate to them:
- JcpGate::authorize is now public: `if gate.authorize(tier, &req) { run }`
— the exact per-consultation decision (decide_chained against the chain,
debit on allow, seal the JCR-1 receipt either way) that GatedStage runs,
callable directly.
- JcpGate::local(resource, actions, budget_uj) issues a self-signed,
single-link grant naming the tier verbs the consumer is willing to use,
under a joule ceiling. The air-gapped, own-issuer case: omit "l4" and the
remote frontier agent is structurally unreachable — no policy server
required. Deterministic local keys; receipts still seal and aggregate.
Two tests: granted tiers allow / ungranted l4 denies with all three
consultations sealed; budget caps total spend at the third L3 attempt.
Co-Authored-By: Claude Opus 4.8 <[email protected]>