Plans vs Grants vs Delegation
Three authority primitives — single-use signed plans, durable revocable capability grants, and verified multi-hop delegation — and when to reach for each.
> Normative spec: /specs/grant-chain-v1 — the canonical envelope, canonicalization scheme, chain verification rules, and reproducible test vectors. This page is the narrative companion.
BrainstormRouter has three ways to express "this identity may do this much, no more". They look similar — all three are BR-signed and all three feed the same deny-wins scope composer — but they answer different questions. Picking the wrong one is the most common modeling mistake, so start here.
The one-paragraph version
A plan is a single-use, prompt-pinned ceiling for one request — "this exact prompt may cost at most $X against this model". A grant is durable, standing authority for an identity — "this agent may use these models for the next 24 hours, derived from this parent, revocable". Delegation is what happens when a grant's subject mints a narrower child grant: a signed, chain-bound, monotone-narrowing hop that the verifier walks link-by-link.
Side by side
| Dimension | Plan | Grant | Delegation (attenuation) |
|---|---|---|---|
| Lifetime | One request (single-use, TTL window) | Hours to 30 days, multi-use | Same as the child grant |
| Signature | HMAC (shared secret) → Ed25519 (later) | Ed25519 (dedicated grant key ring) | Ed25519, per link |
| Anchored in | In-memory LruMap (deploy-mortal) | Postgres row (durable, RLS-scoped) | Postgres rows, one per hop |
| Revocation | Burns at commit | Online, zero-lag by default (revoke the row) | Cascade to the whole subtree |
| Pins a prompt? | Yes (prompt_fingerprint) | No — standing authority | No |
| Who mints | A liaison session | An api_key holder (root) | The parent grant's subject |
| Carries cost? | Yes (ceiling_usd, enforced) | No (spend routes through budget slices) | No |
| Evidence | Liaison close summary | Tamper-evident audit chain + grant_events | Each hop is its own chain link |
| Header | X-BR-Plan-Id (+ X-BR-Liaison) | X-BR-Grant-Id | X-BR-Grant-Id (the leaf id) |
Why a grant — when a plan already exists
Plans are perfect for a bounded, one-shot, cost-pinned task. They fall short the moment you want standing authority that survives a deploy, can be used many times, and can be revoked out-of-band. A plan lives in a per-replica in-memory map that evicts under pressure and dies on deploy; revoking it means "don't mint another one". A grant is a durable Postgres row — revoking it flips the row, and the next request that presents the grant id is denied with no per-replica cache lag.
The subtractive trap (and grant-bound credentials)
A grant that is only an intersection source grants nothing and its revocation revokes nothing: a holder with a broad api key can simply omit the X-BR-Grant-Id header and revert to broad base authority. The composer only ever intersects — it never widens — so an unbound grant is purely _subtractive_.
To make a grant the positive unit of authority, mark the credential grant-bound (api_keys.grantBound). A grant-bound key contributes deny-all ([]) as its own api_key composer source unless a verified grant is present on the request. Now the grant is required: revoke it and the holder loses access. The tenant dial security.grants.require ∈ {off, warn, enforce} controls whether a bound key with no verified grant gets would_deny (warn) or a 403 grant_required (enforce).
How delegation stays honest
When a grant's subject calls POST /v1/grants/{id}/attenuate, BR mints a child grant under a double subset bound:
child ⊆ parent— the child can only narrow the parent's scope.child ⊆ the delegator's live effective scope— an over-minted parent or a
since-narrowed delegator cannot launder stale breadth.
Both bounds are expressed through the same composeModelScope intersection the request-time composer uses, so mint-time ⊆ and enforcement-time ⊆ can never drift. Every hop is Ed25519-signed and chain-bound (issuer == parent.subject, depth == parent.depth + 1, expires_at ≤ parent.expires_at), and the verifier walks the whole chain root→leaf on every request. A stored widening is structurally inert — the running intersection ignores it and the composer emits delegation_scope_exceeded.
BR does not hold caller-side signing keys today, so "A delegated to B" means _BR's Ed25519 signature over A's authenticated attenuation request_, with A's auth method recorded inside the signed envelope. The chain never claims more assurance than it verified — holder-of-key / DID is forward-compatible (deferred), not faked.
Decision guide
- One bounded, cost-pinned request? → Plan (via a liaison session).
- Standing, revocable authority for an identity? → Grant.
- An agent needs to hand a narrower slice to a sub-agent? → Delegation
(attenuate the grant).
- You want revocation to actually remove access? → Grant + grant-bound
credential + security.grants.require=enforce.
- A non-BR gateway needs to consult the same authority? → the standalone PDP,
POST /v1/grants/{id}/check — it returns a signed allow/deny verdict computed with the identical algebra.
Convergence
The two systems converge over time: the plan signer moves HMAC → Ed25519, plans become mintable _under_ a grant, and budget slices accept a grant_id so spend bounds and scope delegation share one identity spine. Until then, operators carry both mental models — plans for prompt-pinned ceilings, grants for standing revocable authority.
See also
- Grant Chain spec (
/specs/grant-chain-v1) — normative envelope, canonicalization, verification, test vectors. - Agent Delegation & M2M Hiring — budget-sliced sub-agent provisioning (the spend half).
- Trust Envelope — the per-request signed decision contract grants compose into.