Roles and permissions¶
Who can do what in Axon: seven canonical roles, their permissions, scope (instance vs project) and how they're assigned. Plus the
engineer/"architect" persona, which is not in RBAC. The source of truth iscore/models/auth.py; if this manual disagrees with the code — the code wins.
1. What it is and why¶
Every write command (CommandEnvelope) in Axon is gated by four checks before any side effect (invariant 4 "security before execution"):
- permission — does the actor's role have the right to this action (the
ROLE_PERMISSIONStable incore/models/auth.py); - object-scope — does the target object belong to a zone the actor may touch;
- project-scope — for project roles: is the object's
project_idinactor.project_scopes; - policy pre-check — the Policy Engine; for side effects also a budget check.
A role defines what you can do; scope — in which projects. Seven roles: owner, admin, manager, operator, reviewer, read_only, system.
engineer / "architect" (the programmer) is not an RBAC role: it's not in core/models/auth.py. It's a persona that works in code (modules/, git) and at deploy time. For Console/API actions (e.g. publish_definition, create_credential) it needs an ordinary manager+ role. Manuals describing a programmer's work have a dedicated section "what's done in code vs in Console" (example — Connectors-Credentials.md §2.1).
2. Roles and access¶
2.1 Roles and scope¶
| Role | Type | Scope | Who it is |
|---|---|---|---|
owner |
instance-scoped | all projects of the instance | the instance owner; the only role with breakglass |
admin |
instance-scoped | all projects of the instance | the instance admin; everything owner has, minus breakglass |
manager |
project-scoped | only projects in project_scopes |
sets up and runs a project: connectors, credentials, binding profiles, publishing workflows, budgets, approvals, cases |
operator |
project-scoped | only projects in project_scopes |
runs processes, manages tasks/conversations, uses credentials (but does not create them) |
reviewer |
project-scoped | only projects in project_scopes |
approvals (approve/reject); reads the rest |
read_only |
project-scoped | only projects in project_scopes |
read only |
system |
service actor (actor_type=system) |
any project (is_system() → access granted) |
auto-refresh OAuth, health snapshots, retention sweepers, pre-dispatch hooks; not assigned to humans (invariant I-9) |
INSTANCE_SCOPED_ROLES = {owner, admin}. PROJECT_SCOPED_ROLES = {manager, operator, reviewer, read_only}. For instance-scoped roles project_scopes equals "all" (in ActorContext that's project_scopes=None); has_project_access(project_id) is always True for them. For project-scoped roles it's True only if project_id ∈ project_scopes.
actor_type ∈ {user, system, service}: user — a human; system — internal platform processes; service — external integrations (if used). is_system() grants cross-project access (but it's still gated by the system role's permissions + a handler-level allowlist (command_type, actor_id, source) for sensitive commands).
2.2 The "permission × role" matrix¶
From core/models/auth.py. owner = all "human" permissions (HUMAN_PERMISSIONS); admin = the same minus breakglass. ✅ — yes, ❌ — no, — — not applicable.
Projects
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Create a project | create_project |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Edit a project | edit_project |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Archive a project | archive_project |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Suspend / resume a project | suspend_project / resume_project |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
edit_projectis also the permission for the internalupdate_connector_healthcommand (dispatched only by an internal/system actor).
Workflows
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Create / edit a workflow | create_workflow / edit_workflow |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Start a workflow | start_workflow |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | — |
| Pause / resume / cancel a workflow | pause_workflow / resume_workflow / cancel_workflow |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Retry / replan a workflow | retry_workflow / replan_workflow |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Publish a definition | publish_definition |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Configure a binding profile | workflow:configure_bindings |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Classification (collections/labels — UI navigation) | manage_workflow_classification |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Pin a run's binding snapshot | workflow_run:pin_bindings |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅* |
| Invalidate bindings | workflow_binding:invalidate |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅* |
* system-only (in
SYSTEM_ONLY_PERMISSIONS).pin_workflow_run_bindingsruns as a sub-step ofstart_workflow(actorworkflow_start_dispatcher).workflow_binding:invalidateis reserved (no command yet).
Tasks / conversations
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Create / edit a task | create_task / edit_task |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Cancel a task | cancel_task |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Create a conversation | create_conversation |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Send a message | send_message |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
Approvals
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Approve / reject | approve / reject_approval |
✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
Cases (case registry)
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Create / edit / archive a case | create_case / edit_case / archive_case |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Attach an external ref | attach_case_external_ref |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Delete an external ref | delete_case_external_ref |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
A manager can create/edit/archive cases and attach external refs, but not delete them — that's admin/owner.
Definitions, agents, prompts, templates, catalog
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Manage agent definitions | agent_definition:manage |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅† |
| Manage prompts | prompt:manage |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅† |
| Manage templates | manage_templates |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Catalog: publish/archive | catalog:manage |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Catalog: install into a project | catalog:install |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
†
systemhasagent_definition:manageandprompt:managefor publishing built-in definitions/prompts (origin='system'/'code'); handler-level scope checks still gate the actual action.
Read, admin, replay
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| Read (dashboards, statuses, audit) | read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Manage users | manage_users |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Breakglass | breakglass |
✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Replay | replay |
✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
breakglass—owneronly (admin = owner − breakglass). It's an emergency override; every use is written toaudit.audit_log.
Credentials and connectors (details — Connectors-Credentials.md §2)
| Action | Permission | owner | admin | manager | operator | reviewer | read_only | system |
|---|---|---|---|---|---|---|---|---|
| View credentials (redacted) | credential:read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| Create / update / revoke a credential | credential:create / credential:update / credential:revoke |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Test connection | credential:test |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Use in a run | credential:use |
✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Rotate / OAuth refresh-reauthorize | credential:rotate |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| Maintain (auto-healing) | credential:maintain |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅* |
| Purge (hard GDPR erasure) | credential:purge |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌‡ |
| Allowlist toggle | project_connector:manage |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Connector instance CRUD | connector:manage |
✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Webhook ingress verifier upsert/disable | ingress_verifier:manage |
❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅* |
* in
SYSTEM_ONLY_PERMISSIONS— not granted to humans (I-9). ‡credential:purgeis reserved: it's in no role inROLE_PERMISSIONS(includingsystem) — it's a contract surface for Stage 3 retention sweepers, which get the right via a special grant. Thedelete_credential/purge_credentialcommands don't exist yet (see Connectors-Credentials.md §6.4).
2.3 System-only permissions (invariant I-9)¶
SYSTEM_ONLY_PERMISSIONS = {credential:maintain, credential:purge, ingress_verifier:manage, workflow_binding:invalidate, workflow_run:pin_bindings}. HUMAN_PERMISSIONS = all Permission − SYSTEM_ONLY_PERMISSIONS. Invariant: HUMAN ∩ SYSTEM_ONLY = ∅ (there's a blocking pytest). So owner/admin, even having "everything human", do not get these — auto-healing and hard-purge are by design executed only by system actors.
3. Where it is in Console¶
The Users sidebar section (under the Administration group; instance-level + per-project view). Available to owner/admin (manage_users).
| Element | What it does |
|---|---|
| User list | actor_id, name/email, role, project_scopes (or "all projects" for owner/admin), status (active/deactivated) |
Invite / New user |
create a user, assign a role and (for project-scoped roles) project scopes |
Edit role |
change a user's role |
Edit project scopes |
for manager/operator/reviewer/read_only — the list of projects they can access |
Deactivate |
disable a user (new commands/approvals from them are no longer accepted) |
| Role indicator in the header | the current user sees their role (e.g. "Administrator owner") |
The operational side (how exactly to create/assign, the breakglass flow) — Users-And-Access-Management.md.
4. Concepts (mental model)¶
ActorContext (propagated through all layers)
├─ actor_id — stable actor id
├─ actor_type — user | system | service
├─ actor_role — owner | admin | manager | operator | reviewer | read_only | system
└─ project_scopes — list[str] OR None (= all projects, instance-scoped)
has_project_access(project_id):
instance-scoped role → True
actor_type == system → True
project_scopes is None → False (a project-scoped actor with no explicit scopes — no access)
else → project_id in project_scopes
Permission — StrEnum (~45 values), grouped by domain (see §2.2)
ROLE_PERMISSIONS: dict[Role, frozenset[Permission]]
owner = HUMAN_PERMISSIONS (including breakglass)
admin = HUMAN_PERMISSIONS − {breakglass}
manager = <explicit set> | _CREDENTIAL_MANAGER_PERMISSIONS
operator/reviewer/read_only/system = <explicit narrow sets>
SYSTEM_ONLY_PERMISSIONS ⊂ Permission — never for humans (I-9)
HUMAN_PERMISSIONS = Permission − SYSTEM_ONLY_PERMISSIONS
Key ideas:
- instance-scoped vs project-scoped — owner/admin see/touch all projects; the rest are limited to project_scopes. This is orthogonal to role permissions: even if a role has edit_workflow, a project-scoped actor can only edit in their projects.
- system is separated from humans — a system actor has a narrow set of rights (rotate/maintain/ingress/agent/prompt + pin/invalidate) and is additionally gated by a handler-level allowlist (command_type, actor_id, source) for sensitive commands. Humans don't get these system rights at all.
- credential:use ≠ the right to decrypt — it's a "project membership capability"; secret plaintext is decrypted only in the worker.
- breakglass ≠ bypassing the audit — it's an emergency owner-only override that is itself written to the audit (invariant 9: transparency).
5. Flows: step-by-step scenarios¶
Flow 1 — Provision a manager for a project (owner/admin)¶
- Console → Users →
Invite/New user. - Enter
actor_id/email, name. - Role =
manager. - Project scopes =
[<project_id>](one or several projects). - Save → the user can configure those projects (connectors, credentials, binding profiles, publish workflows, budgets, approvals, cases). They have no access to
manage_users,create_project, templates, agent/prompt definitions, classification.
Flow 2 — Give an operator another project (owner/admin)¶
- Console → Users → pick the user (
operator) →Edit project scopes. - Add
<new_project_id>to the list. - Save → the operator can run workflows / manage tasks / use credentials in the new project (but not create credentials/instances and not touch the allowlist — that's
manager+).
Flow 3 — Emergency breakglass (owner only)¶
- Used when the normal path is blocked (an incident). Available only to
owner(breakglass). - Every use is automatically recorded in
audit.audit_log— it's not a "silent root" but an audited override (invariant 9). More — Security-And-Audit.md.
Flow 4 — Deactivate a user (owner/admin)¶
- Console → Users → pick the user →
Deactivate. - Effect: new commands/approvals from them are not accepted. Already-performed actions stay in the audit. If they had pending approvals — reassign them to another approver with access.
6. Options reference¶
6.1 Roles (the Role enum)¶
| Role | Scope | Base permission set |
|---|---|---|
owner |
instance | HUMAN_PERMISSIONS (everything human, including breakglass) |
admin |
instance | HUMAN_PERMISSIONS − {breakglass} |
manager |
project | an explicit set (workflow CRUD/lifecycle except retry/replan, publish_definition, tasks create/edit, approve/reject, conversations, read, cases create/edit/archive/attach, project_connector:manage, connector:manage, workflow:configure_bindings, catalog:install) + _CREDENTIAL_MANAGER_PERMISSIONS (credential read/create/update/rotate/revoke/test/use) |
operator |
project | create_task, edit_task, send_message, start_workflow, read, credential:read, credential:test, credential:use |
reviewer |
project | approve, reject_approval, read, credential:read |
read_only |
project | read, credential:read |
system |
actor_type=system | rotate_credential, maintain_credential, ingress_verifier:manage, workflow_binding:invalidate, workflow_run:pin_bindings, agent_definition:manage, prompt:manage |
6.2 ActorContext (fields)¶
| Field | Type | What it does |
|---|---|---|
actor_id |
str | a stable actor identifier (lands in the audit) |
actor_type |
user / system / service |
the type; system grants cross-project access |
actor_role |
Role |
determines the permission set |
project_scopes |
list[str] | None |
the projects accessible; None = all (instance-scoped) |
6.3 The full Permission list¶
See §2.2 (grouped by domain) and core/models/auth.py. Every write command maps to a single Permission (_COMMAND_PERMISSION_MAP in core/api/commands/execution_paths.py).
7. Lifecycle and maintenance¶
- Changing a role —
Edit rolein the Console (via CommandEnvelope, written to the audit). The user's pending actions stay as they are; new ones are gated by the new role. - Deactivation — the user can no longer run commands; data/audit are preserved. Pending approvals on them — reassign.
- Rotating admins / the owner — an operational procedure; the owner is the only one with
breakglass, so changing the owner needs special care (usually: a new owner is assigned by the acting owner). - Audit of access changes — all role/scope/deactivation changes are in
audit.audit_log. credential:purge(reserved) — will be held by the retention-sweeper actor in beyond-Stage-3 (GDPR hard-purge); no role has it now.
8. Troubleshooting¶
| Symptom | Cause | What to do |
|---|---|---|
403 / "permission denied" on a command |
the actor's role doesn't have the corresponding Permission |
Check §2.2; if needed — elevate the role (owner/admin) or grant the right via the role that has it. Don't "invent" new permissions. |
| "No access to the project" / object not visible | a project-scoped role, and project_id is not in project_scopes |
Edit project scopes (owner/admin) — add the project. |
The owner can't find breakglass on an admin |
by design: admin = owner − breakglass |
breakglass is owner-only. |
A system actor "can't" run a command despite the system role |
the command isn't in its permission set, or it didn't pass the handler-level allowlist (command_type, actor_id, source) |
Check that this action is even done by a system actor (not all are); for sensitive commands — the right actor_id/source is needed. |
| A manager can't delete an external ref / publish a prompt / create a project | manager doesn't have those rights (see §2.2) |
Those actions are admin/owner (and prompt:manage / agent_definition:manage — owner/admin/system). |
9. Constraints and invariants¶
HUMAN ∩ SYSTEM_ONLY = ∅(I-9).credential:maintain,credential:purge,ingress_verifier:manage,workflow_binding:invalidate,workflow_run:pin_bindings— never for humans; there's a blocking test.admin = owner − breakglass. Admin never getsbreakglass, under any condition.- Security before execution (invariant 4): permission + object-scope + project-scope + policy pre-check (+ budget) — before the side effect, not after.
- All mutations go via CommandEnvelope. RBAC is checked server-side, not in the UI. The UI merely hides unavailable actions — it is not a security boundary.
- Optimistic concurrency. Where there's a
version— mutating commands requireexpected_version. - Transparency (invariant 9): any access change and any use of
breakglassis inaudit.audit_log. engineer/"architect" is not an RBAC role. For Console/API actions a realmanager+role is required.
10. Related manuals and canon¶
- Users-And-Access-Management.md — the operational side: how to create a user, grant a role/scopes, the breakglass flow.
- Connectors-Credentials.md §2 — the detailed RBAC picture for credentials/connectors.
- Security-And-Audit.md — object/project scope, policy pre-check, audit log, explainability.
- Workflows.md / Cases.md / Approvals.md / Templates-And-Catalog.md — domain rights in context.
- Canon:
core/models/auth.py(the source of truth),ARCHITECTURE-V6.md(Security),RULES-CODDING-DEVOPS.md(roles, write commands),VISION.md§7 (invariants 4 and 9).