Roles and Permissions
AgentCore uses a hybrid RBAC model. The legacy Role enum stays for compatibility, while SystemRole holds editable permission sets and department scope. User-level overrides handle exceptions — no full permission join table yet.
System Roles
The four built-in roles are seeded as SystemRole rows with isSystem: true. They can be assigned to users and updated through migrations or seed data, but the product UI should not let tenants delete them.
| Role | Department Scope | Main Permissions |
|---|---|---|
admin | all departments | all known permissions |
dept_head | own and assigned departments | department users, namespaces, persona, approvals, knowledge, templates, employee profiles |
approver | own and assigned departments | approvals, escalation, all approvals view, knowledge read, document generation |
employee | own and assigned departments | persona read, knowledge read, document generation, plugin read, self profile |
Legacy enum values are still on User.role. When roleId is set, User.systemRole is the primary source of permissions. Otherwise the backend falls back to the legacy enum mapping.
Custom Roles
Custom roles are rows in system_roles with isSystem: false.
Fields:
| Field | Purpose |
|---|---|
name | Display name. |
slug | Stable lowercase key. Generated from name when omitted. |
isSystem | Protects built-ins from user deletion. |
allDepartments | Grants cross-department visibility when true. |
departmentIds | Explicit department scope when allDepartments is false. |
permissions | Array of permission keys, or * for all permissions. |
Custom role routes:
GET /api/v1/permissions
GET /api/v1/roles
GET /api/v1/roles/:id
POST /api/v1/roles
PATCH /api/v1/roles/:id
DELETE /api/v1/roles/:id
Viewing roles requires canViewRoles. Creating, updating, and deleting roles
requires canManageRoles. Deleting system roles is rejected.
Permission Catalog
Permissions are string keys grouped for UI display:
| Group | Keys |
|---|---|
| Users | canViewAllUsers, canCreateUsers, canManageUsers, canManageDepartmentUsers |
| Roles | canViewRoles, canManageRoles |
| Departments | canManageDepartments, canArchiveDepartments |
| Namespaces & Personas | canManageNamespaces, canEditPersona, canViewPersona |
| Approvals | canApprove, canEscalate, canViewAllApprovals |
| Knowledge | canViewKnowledge, canUploadDocuments, canDeleteDocuments |
| Document Generator | canGenerateDocuments, canManageTemplates |
| Plugins | canViewPlugins, canManagePlugins |
| Administration | canViewAudit, canManageEmployeeProfiles, canEditSettings, canEditSelfProfile |
| System | * |
The API validates every custom role and override against this catalog.
Permission Matrix
The seeded built-in matrix is:
| Permission | Admin | Dept Head | Approver | Employee |
|---|---|---|---|---|
canCreateUsers | yes | no | no | no |
canManageUsers | yes | no | no | no |
canManageDepartmentUsers | yes | yes | no | no |
canViewAllUsers | yes | yes | yes | no |
canViewRoles | yes | no | no | no |
canManageRoles | yes | no | no | no |
canManageDepartments | yes | no | no | no |
canArchiveDepartments | yes | no | no | no |
canManageNamespaces | yes | yes | no | no |
canEditPersona | yes | yes | no | no |
canViewPersona | yes | yes | yes | yes |
canApprove | yes | yes | yes | no |
canEscalate | yes | yes | yes | no |
canViewAllApprovals | yes | no | yes | no |
canUploadDocuments | yes | yes | no | no |
canDeleteDocuments | yes | yes | no | no |
canViewKnowledge | yes | yes | yes | yes |
canManageTemplates | yes | yes | no | no |
canGenerateDocuments | yes | yes | yes | yes |
canManagePlugins | yes | no | no | no |
canViewPlugins | yes | yes | yes | yes |
canViewAudit | yes | no | no | no |
canManageEmployeeProfiles | yes | yes | no | no |
canEditSelfProfile | yes | yes | yes | yes |
canEditSettings | yes | no | no | no |
Custom roles can grant any subset of these permissions.
User Overrides
Users carry four override arrays:
| Field | Effect |
|---|---|
extraPermissions | Adds permission keys after the role is evaluated. |
revokedPermissions | Removes permission keys after extras are applied. |
extraDepartmentIds | Adds visible departments after role scope is evaluated. |
revokedDepartmentIds | Removes departments after extras are applied. |
Override routes:
POST /api/v1/users/:id/permissions/grant
POST /api/v1/users/:id/permissions/revoke
DELETE /api/v1/users/:id/permissions/grant/:permission
DELETE /api/v1/users/:id/permissions/revoke/:permission
GET /api/v1/users/:id/effective-access
Grant and revoke requests can include permission, departmentId, or both.
Each mutation writes an audit entry.
Effective Access Formula
Permissions:
rolePermissions = user.systemRole?.permissions ?? legacyPermissions(user.role)
effectivePermissions = (rolePermissions + user.extraPermissions) - user.revokedPermissions
If extraPermissions includes *, it expands to every known permission key.
Revokes are applied last, so a revoked permission wins over the base role and
over extras.
Departments:
if role.allDepartments or legacy role is admin:
effectiveDepartments = all
else:
effectiveDepartments =
role.departmentIds
+ user.primaryDepartmentId
+ user.departmentId
+ user.extraDepartmentIds
- user.revokedDepartmentIds
The forUser() helper wraps this computation for route code. It exposes:
directWhere()for models with a directdepartmentId;nestedWhere('namespace')for models scoped through a relation;canSeeDept(id)for resource checks;hasPermission(key)for RBAC checks.
Development Rules
- Use
requirePermission()on privileged routes. - Use
forUser(request.user)in new code — don't checkrole === 'admin'directly. - Prefer custom roles for durable tenant policy, overrides for individual exceptions.
- Keep
revokedPermissionssmall. If many users need a permission removed, make a different custom role. - Hit
GET /users/:id/effective-accesswhen debugging access tickets.