Who can access it
| Role | role_id | is_superuser | Has access? |
|---|---|---|---|
| super admin | 1 | True | ✅ |
| account owner | 2 | — | ✅ |
| Account Owner | 8 | — | ✅ |
| Super Admin | 9 | — | ✅ |
| Super Admin Agency | 11 | — | ✅ |
| Account Owner Agency | 12 | — | ❌ |
| Administrator Agency | 13 | — | ❌ |
| Standard Agency | 14 | — | ❌ |
| administrator | 3 | — | ❌ |
| standard | 4 | — | ❌ |
| (any non-superuser, role_id ∉ ) | — | False | ❌ |
is_superuser || role_id ∈ {1, 2, 8, 9, 11}. Source of truth: shared/auth.py:agent_surface_allowed.
Backend gate
shared/auth.py:agent_surface_allowed(user) -> bool is the single source of truth.
- DRF permission class
shared.permissions.AgentSurfaceAllowed— gates every mutation view inautomations,custom_agents,skills, and the write paths inknowledge_base. - Querset scoping helper
shared.auth.scope_to_accessible_clients(qs, request)— returnsqs.none()for non-allowed users so reads silently return empty rather than 403. - WebSocket consumer
automations.consumers— checks at connect time; rejects the upgrade for non-allowed users. - Backend system check —
python manage.py checksurfacesagent_surface.W001 / W002for missingOPENAI_API_KEY/pgvector.
Frontend gate
The frontend mirrors the backend gate in two places:AgentSurfaceGuard (router)
l5ui/src/app/shared/guards/agent-surface.guard.ts is wired onto every /automations/*, /skills/*, /agent-builder/*, /knowledge-bases/* route via canActivate: [AuthGuard, AgentSurfaceGuard]. Non-allowed users get redirected to /.
Sidebar filter
l5ui/src/app/components/sidebar/sidebar.component.ts filters the “Agents” submenu (Automations / Live Operations / Run History / Agent Builder / Skills / Knowledge Bases) using the same gate — items are stripped before rendering rather than shown-then-403.
JWT claims
The user object inside the JWT (token.payload['user']) is built by useraccount/helpers.py:get_user_details(user) at login. The claims this gate depends on:
| Claim | Source | Purpose |
|---|---|---|
is_superuser | User.is_superuser | Django superuser flag. |
role_id | User.role_id | FK to users.UserRole. |
client_id | User.client_id | Tenant the user belongs to. |
client_type | Client.client_type | 1=B2C, 2=B2B, 3=AGENCY. |
processCompleted | Client.onboarding_complete | Affects sidebar disabled-flag handling. |
feature_flags | FeatureFlag.get_client_flags(client_id) | Per-client feature flags. |
agent_surface_allowed only reads is_superuser and role_id. If either is missing from the JWT, the user is locked out of the surface — this is the intended fail-closed behavior.
Tenant scoping vs. surface gate
The agent surface gate decides whether you can touch the surface at all. Once you’re allowed, every read still tenant-scopes viashared.auth.get_accessible_client_ids(user, token_client_id):
| User type | What they see |
|---|---|
| Superuser | Every tenant. |
Elevated agency role (role_id ∈ {1,2,8,9,11}) at agency level | Their agency + every child client. |
| Elevated agency role at child level | Just that client (+ falls back to virtual_clients if no agency record). |
| Non-elevated | qs.none() — surface gate rejects them earlier. |
?client_id=<id> and that id is in their accessible set.
Common gotchas
Logged in but the agent surface is gone
Logged in but the agent surface is gone
Most common cause: stale JWT from before a frontend refactor.Fix: log out, log back in. The fresh JWT carries the current
is_superuser + role_id claims.If still missing after re-login — open DevTools → Application → Local Storage → decode the access token’s payload and check user.is_superuser and user.role_id. If either is missing, the JWT minting logic in useraccount/helpers.py:get_user_details is the issue.My role_id is correct but I'm still locked out
My role_id is correct but I'm still locked out
Verify the role is in
{1, 2, 8, 9, 11} — not {3, 4, 5, 6, 7, 10, 12, 13, 14}. The 11/12/13/14 series are the agency-side counterparts of the 1/2/3/4 base roles, but only 11 (Super Admin Agency) is in the elevated set. Account Owner / Administrator / Standard at the agency level are intentionally excluded — they’re operational roles, not super-admin equivalents.Custom agent missing for an allowed user
Custom agent missing for an allowed user
Custom agent visibility additionally tenant-scopes via
ai_chat/agent_scope.py. An agent in tenant A is not visible to a super-admin viewing tenant B unless the agent is shared (currently not exposed in the UI — agents are tenant-private by default).Feature flag overrides the gate
Feature flag overrides the gate
Per-client feature flags can disable specific agent-surface features even when the gate allows them. For example, a
disable_automations flag on a client makes /automations route to / even for super admins of that tenant. Check currentUserInfo.user.feature_flags.Adding a new role to the elevated set
If a new role needs agent-surface access:Update the frontend mirror
l5ui/src/app/shared/guards/agent-surface.guard.ts:ELEVATED_AGENCY_ROLE_IDS — add the same id.l5ui/src/app/components/sidebar/sidebar.component.ts — find the [1, 2, 8, 9, 11] literal in the agent-surface filter and add the id.Where the code lives
| Concern | Path |
|---|---|
| Backend gate | shared/auth.py:agent_surface_allowed |
| DRF permission class | shared/permissions.py:AgentSurfaceAllowed |
| Queryset scoping | shared/auth.py:scope_to_accessible_clients |
| WebSocket connect-time check | automations/consumers.py |
| JWT user payload | useraccount/helpers.py:get_user_details |
| Frontend route guard | l5ui/src/app/shared/guards/agent-surface.guard.ts |
| Frontend sidebar filter | l5ui/src/app/components/sidebar/sidebar.component.ts |
Frontend currentUserInfo accessor | l5ui/src/app/shared/services/authentication.service.ts:currentUserInfo |
is_superuser getter | l5ui/src/app/shared/services/authentication.service.ts:isSuperUser |
.png?fit=max&auto=format&n=Frm2GFbmok4D-yJA&q=85&s=93c3ebd47542af65d1cd06d8563a7f6e)