← Back to docs

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)

caplets 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`

1. caps(child) ⊆ caps(parent)
2. exp(child) ≤ exp(parent) and ttl ≤ tier.maxTtl
3. scope(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)
4. each budget dimension ≤ parent's remaining (budget − reserved − settled); issuing reserves it from the parent
5. constraints only tighten: allowDelete true→false ok, false→true rejected; maxWorkers, maxReplicas ≤ parent
6. depth(child) = depth(parent)+1 ≤ tier.maxDepth
7. parent must be active, not expired, tenant active, kill switch off

Revoking a grant revokes every descendant (cascade) and releases their unspent reservations back up the chain.

Verification order (every grant-bearing request)

1. parse + signature (constant-time compare) → else 401 bad_grant
2. exp → else 401 grant_expired (row marked expired lazily)
3. DB row active → else 401 grant_revoked
4. tenant active → else 403 tenant_suspended
5. kill switch off → else 503 runtime_paused
6. route cap ∈ caps → else 403 missing_cap
7. scope covers the request target → else 403 out_of_scope
8. budget available for this action (reserve) → else 402 budget_exhausted
9. per-grant rate limit → else 429

Any exception in this chain → 403 verification_error (fail closed).

Budgets and the ledger

Dimensions: usd_cents (compute allowance), calls (exit calls), deploys.

eventledger kindeffect
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.