Grants — token format, attenuation, verification, budgets
Token
cog1.<base64url(payload JSON)>.<base64url(HMAC-SHA256(GRANT_SIGNING_KEY, payload JSON))>
Opaque to holders. Only the runtime verifies (it is the only party that acts on a grant). Present as Authorization: Bearer cog1... or X-Grant: cog1....
Payload
{
"v": 1,
"id": "gr_…",
"tenant": "t_…",
"parent": "gr_… | null",
"depth": 0,
"sub": "agent:tip-bot-builder",
"iss": "creator-os-runtime",
"iat": 1760000000,
"exp": 1760003600,
"caps": ["deploy.worker", "worker.status"],
"scope": {
"railway": { "projects": ["<project id>"], "environments": ["production"] },
"workers": { "namePrefix": "t-abc123-" },
"memory": { "partitions": ["worker:t-abc123-probe-1a2b", "agent:deployer:probe"] }
},
"budget": { "usd_cents": 200, "calls": 100, "deploys": 3 },
"constraints": { "maxWorkers": 1, "maxReplicas": 1, "allowDelete": false, "requireDryRun": true },
"nonce": "…"
}
Status (active | revoked | expired) and counters live in the DB; the token is checked against the DB on every use so revocation is immediate.
Capability registry (closed set)
| cap | lets the holder |
| `grant.issue` | mint attenuated child grants under its own |
| `grant.read` | list/inspect its own subtree |
| `cog.plan` | ask the COG for a proposal |
| `cog.execute` | submit a proposal to the gatehouse for signing + execution |
| `deploy.worker` | submit a deploy intent (controlled exit) |
| `worker.status` | read deploy/worker status |
| `worker.logs.read` | read worker logs (when the exit supports it) |
| `worker.restart` | restart a worker |
| `worker.delete` | delete a worker — production requires break-glass |
| `ledger.read` | read the tenant ledger |
| `tenant.read` | read tenant info + events |
| `webhook.emit` | post an event onto the bus |
| `memory.read` | search/read tenant memory inside the grant's partition prefixes |
| `memory.write` | remember / tombstone inside the grant's partition prefixes |
Anything else is rejected at issue time. There is no money, wallet, trade, or secret-read capability; none can be created by API.
Attenuation rules (child vs parent) — all must hold, else 403 `attenuation_violation`
caps(child) ⊆ caps(parent)exp(child) ≤ exp(parent) and ttl ≤ tier.maxTtlscope(child) ⊆ scope(parent): every list is a subset; namePrefix(child) starts with namePrefix(parent); every memory partition prefix of the child starts with one of the parent's (root = [""] = whole tenant)budget dimension ≤ parent's remaining (budget − reserved − settled); issuing reserves it from the parentallowDelete true→false ok, false→true rejected; maxWorkers, maxReplicas ≤ parentdepth(child) = depth(parent)+1 ≤ tier.maxDepthactive, not expired, tenant active, kill switch offRevoking a grant revokes every descendant (cascade) and releases their unspent reservations back up the chain.
Verification order (every grant-bearing request)
bad_grantexp → else 401 grant_expired (row marked expired lazily)active → else 401 grant_revokedactive → else 403 tenant_suspendedruntime_pausedcaps → else 403 missing_capout_of_scopebudget_exhaustedAny exception in this chain → 403 verification_error (fail closed).
Budgets and the ledger
Dimensions: usd_cents (compute allowance), calls (exit calls), deploys.
| event | ledger kind | effect |
| child issued | `reserve` | parent.reserved += child.budget |
| exit call succeeds | `settle` | grant.settled += cost |
| grant revoked / expired | `release` | parent.reserved −= (child.budget − child.settled) |
remaining = budget − reserved − settled. Ledger rows: {id, ts, tenant, grant, parent, kind, usd_cents, calls, deploys, ref, note}. Tier period counters (deploys/period) reset on the tenant's period start, driven by Stripe invoice.paid.
Gatehouse policy (deterministic)
Inputs: tier ceilings, hard rules, the proposal. Output: signed grants or a single denied with reasons. Policy version is stamped into every grant.
Hard rules (never overridable): closed cap registry; targets ⊆ tenant allowlist; TTL ≤ tier; budget ≤ tier and ≤ parent remaining; depth ≤ tier; worker.delete on production needs live break-glass; kill switch off; tenant active. Any evaluation error ⇒ deny.
Break-glass: admin-only, ≤ 25 minutes, tenant-scoped, auto-expires, logged. It authorizes destructive worker actions only; it does not create capabilities.