Who can access it
The gate is
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:
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):
The tenant scoping is enforced separately on every endpoint — even an authenticated super-admin doesn’t see another tenant’s data unless they pass
?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:1
Update the backend constant
shared/auth.py:ELEVATED_AGENCY_ROLE_IDS — add the new role id.2
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.3
Add to the docs table
The role table at the top of this doc — add the new row.
.png?fit=max&auto=format&n=Frm2GFbmok4D-yJA&q=85&s=93c3ebd47542af65d1cd06d8563a7f6e)