Skip to main content
The agent surface is a single permission boundary. Either the user can touch all of it (automations + agent builder + skills + knowledge bases + ops dashboard + run history) or none of it. There’s no per-feature mix. This page documents the gate end-to-end: backend, frontend, and the JWT claims it relies on.

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.
Used by:
  • DRF permission class shared.permissions.AgentSurfaceAllowed — gates every mutation view in automations, custom_agents, skills, and the write paths in knowledge_base.
  • Querset scoping helper shared.auth.scope_to_accessible_clients(qs, request) — returns qs.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 checkpython manage.py check surfaces agent_surface.W001 / W002 for missing OPENAI_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 /.
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.
Both gates read from currentUserInfo (the parsed JWT payload), not from currentUserValue (the raw login response).An earlier bug pointed both gates at currentUserValue?.user, but currentUserValue is the raw login response — it has access / refresh / is_agency / client_type but no user property. Reading tokens.user.is_superuser always returned undefined, locking even super admins out of the agent surface.If you find yourself accessing user claims from a route guard, sidebar filter, or component, always go through currentUserInfo?.user.

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 via shared.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

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.
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.
The frontend bundle may be stale. Hard-refresh the browser (Cmd+Shift+R) or restart ng serve. The sidebar filter is evaluated client-side; if your bundle pre-dates the gate change, the gate behavior will lag.
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).
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.
The two literals (backend + frontend) being separate is intentional — the backend is the source of truth, the frontend is a UX optimization to hide what the backend would reject anyway.

Where the code lives