Authorization — who may do what, and how you prove it
Control Center answers two authorization questions, and they are genuinely different. “May this person call this operation?” is about humans, roles and repositories. “May this agent cause this effect?” is about actions, arguments and blast radius — that one is Guardrails.
This page is the first question, plus the record both of them leave behind.
One decision point
Section titled “One decision point”Every human-side authorization decision resolves through a single function:
can(principal, permission, resource) → allow | deny + whyA permission is <domain>:<tier> — tickets:write,
members:administer, repos:read. The catalog is derived, not
hand-written: it is computed from the operation surface itself (the op’s name
prefix gives the domain, the role floor it already enforces gives the tier),
so all ~550 operations carry a permission without anyone typing one.
That derivation is the property worth protecting. It means a NEW operation is gated by default — the failure mode of a hand-annotated model is that somebody forgets, and the thing they forget is the gate. A ratchet test pins the resulting domain set, so adding an operation under a new prefix is a naming decision a human makes rather than an accident.
The four tiers map exactly onto the built-in roles:
| Tier | Role floor | Example |
|---|---|---|
read |
guest | tickets:read |
write |
member | tickets:write |
administer |
admin | members:administer |
own |
owner | workspace:own |
Five built-in roles — owner, admin, member, viewer, guest — are presets over that catalog. Ownership is a single seat per workspace, handed over explicitly with Transfer ownership (an owner who leaves is otherwise unremovable: SCIM deprovisioning refuses to delete the last owner).
Custom roles are subtractive. A custom role names a base preset and a set
of permissions REMOVED from it — “admin, but cannot manage members”. It can
never grant more than its base, which is what makes them safe to introduce
into an existing install: every existing check remains a sound upper bound.
Members hold them as custom:<id>, and a client too old to understand that
value falls back to the lowest privilege rather than guessing.
Underneath roles sit per-repo grants (none / read / review /
write): workspace membership never silently out-privileges the forge. Every
surface that exposes code checks them — including the ones that reach code
sideways, like a terminal or an editor session opened on a space’s worktree.
Server authority
Section titled “Server authority”Some operations are not about a workspace at all: install-wide settings, SSO configuration, provider credentials, model management, whole-install backups. Those declare server authority and are refused for everyone except the install’s operator (the recorded server owner). There is deliberately one operator identity and one definition of it.
The audit trail
Section titled “The audit trail”Every authorization decision — allow and deny — is appended to a per-workspace, hash-chained audit trail.
A deny-only log answers “what did we block?” but not “what did this agent actually do, and who authorized it?”, which is the question an incident review starts from. So each entry records the full attribution chain: the acting principal, the human it acted on behalf of, the delegation it arrived through, the action and its arguments (as a digest), the decision, and the rule that decided.
Entries are chained: each one’s hash covers the previous one’s, so editing, deleting or reordering any row breaks every hash after it. Verify chain re-derives the whole thing and names the first link that does not hold. An audit log you cannot check is a log; one you can re-derive is evidence.
Two consequences worth knowing:
- The audit table is exempt from age-based pruning. A hash chain cannot be trimmed from the middle. Retention is export-then-truncate, and the truncation leaves a checkpoint carrying the removed segment’s terminal hash, so what survives still verifies back to the beginning.
- It sits beside the activity timeline, not on top of it. The timeline is what people did; this is what was authorized and refused. A shared correlation id reads one call across both.
It exports as newline-delimited JSON and can stream to a SIEM (Splunk, Elastic, Sentinel — an HTTP endpoint with a bearer token). Streaming is best-effort by design: a SIEM being down must never block an agent’s tool call, and the durable, verifiable copy is on disk either way.
Sessions
Section titled “Sessions”An install can set a maximum session age and an idle timeout. Both are enforced at the device-credential check every authenticated lane funnels through — a session bound a client could ignore answers a questionnaire, not an attacker. An install can also require SSO, which disables manual pairing for new devices (with a lockout guard: it refuses while no working SSO connection exists).
What this is not
Section titled “What this is not”- Not per-object ACLs. There is no per-ticket or per-message permission, deliberately. Repository-level separation is what teams actually ask for and it exists; per-object sharing is a combinatorial surface with no demonstrated demand here.
- Not a policy engine you configure in a DSL. The vocabulary is closed on purpose, so the whole policy can be rendered, explained and enumerated.
- Not enforced anywhere but the server. Client-side checks exist only to avoid offering a control that will be refused. The authoritative decision is always the server’s.