Dynamic access controls
Agenta EE ships with code-default plans, entitlements, and role catalogs. Operators can override any of these at runtime by setting JSON environment variables. This page documents the access layer:
AGENTA_ACCESS_PLANS— plan slugs and per-plan entitlement controls (flags, counters, gauges, throttles).AGENTA_ACCESS_ROLES— custom roles per scope on top of the platform minima (owner/viewer).AGENTA_ACCESS_ROLES_OVERLAY— small deployment-wide role-catalog patches.
These env vars are parsed once at process startup. After changing them, restart all API and worker processes — they each load the controls into memory and will otherwise enforce different limits.
If any override var is set, validation runs at startup:
- invalid JSON → fail
- unknown flag / counter / gauge / permission slug → fail
AGENTA_ACCESS_PLANSempty object → fail- plan entry with no entitlement info and no
description→ fail AGENTA_ACCESS_ROLESredefining the reservedownerorviewerslug → fail- empty scope list → fail
- duplicate role slug within a scope → fail
Run staging deploys with the new values before pushing to production.
AGENTA_ACCESS_PLANS
JSON object keyed by plan slug. The set of keys is the effective plan set — runtime plan references must point to one of these slugs.
Top-level shape
{
"<plan_slug>": <PlanEntry>,
...
}
PlanEntry fields
Every entry must define at least one of description, flags,
counters, gauges, or throttles.
| Field | Type | Required | Description |
|---|---|---|---|
description | string | one-of | Operator-facing description (not shown to users). Description-only entries are accepted for display-only/custom plans. |
flags | object | one-of | Map of flag slug → bool. See flag keys. |
counters | object | one-of | Map of counter slug → Quota. See counter keys. |
gauges | object | one-of | Map of gauge slug → Quota. See gauge keys. |
throttles | array | no | List of Throttle entries. See throttles. |
Quota shape
Used by counters and gauges map values.
| Field | Type | Default | Description |
|---|---|---|---|
free | integer | null | null | Free-tier allowance before paid or capped usage applies. |
limit | integer | null | null | Hard cap; null = unlimited. |
strict | boolean | null | null | If true, the request that would cross the limit is itself rejected (no "one free overshoot"). null is treated as false. |
retention | integer | null | null | Retention window in minutes. Must be one of the canonical Retention enum values: 0 (ephemeral), 60 (hourly), 1440 (daily), 44640 (monthly ≈ 31d), 131040 (quarterly ≈ 91d), 525600 (yearly ≈ 365d). Used by traces_ingested and events_ingested for retention flush. |
scope | string | null | null | Granularity of the meter row: "organization" (default when null), "workspace", "project", or "user". |
period | string | null | null | Metering bucket: "daily", "monthly", "yearly". null means non-periodic (used for gauges). Replaces the pre-reshape monthly: true boolean. |
Flag keys
rbac, access, domains, sso. Values are booleans.
Counter keys
evaluations_run, traces_ingested, traces_retrieved, credits_consumed, events_ingested.
traces_ingested and events_ingested are independent retention domains: each has its own
counter, its own retention window, its own admin flush endpoint
(/admin/spans/flush and /admin/events/flush), and its own cron
schedule. Setting one does not affect the other. events_ingested is a
retention-only counter; operators set its retention per plan to bound how
long event rows live.
traces_retrieved is the only counter with a non-default scope and
period in the code defaults: scope=user, period=daily, declared on
every plan with limit=null (unlimited). Operators who want to cap
per-user daily reads set limit via env override.
Gauge keys
users.
Throttle shape
| Field | Type | Description |
|---|---|---|
bucket.capacity | integer | Max tokens in the bucket. |
bucket.rate | integer | Tokens added per minute. |
bucket.algorithm | string | null | Optional algorithm tag. |
mode | string | "include" or "exclude". |
categories | array | null | Endpoint categories the throttle applies to. |
endpoints | array | null | Explicit [method, path] pairs. |
Example — self_hosted_enterprise (the code-default for self-hosted)
When AGENTA_ACCESS_DEFAULT_PLAN is unset, signup onboards new
organizations on self_hosted_enterprise. This is the operative plan for
almost every self-hosted deployment, and the canonical shape to use as a
starting point for any further customization via AGENTA_ACCESS_PLANS:
{
"self_hosted_enterprise": {
"description": "Self-hosted enterprise — full access, no quotas.",
"flags": {
"rbac": true,
"access": true,
"domains": true,
"sso": true
},
"counters": {
"evaluations_run": {"strict": true, "period": "monthly"},
"traces_ingested": {"period": "monthly"},
"traces_retrieved": {"strict": true, "scope": "user", "period": "daily"},
"credits_consumed": {"strict": true, "period": "monthly"},
"events_ingested": {"period": "monthly"}
},
"gauges": {
"users": {"strict": true}
}
}
}
What's notable about the code-default shape:
- All four flags are
true. RBAC, access controls, custom domains, and SSO are all on by default. - No counter has a
limit. Every counter islimit: null(omitted), so the meters layer tracks usage but never blocks a request.strict: trueis structurally present so it kicks in the moment alimitis added. traces_ingestedretention is unset. The tracing-flush job iterates plans and skips this one — traces are kept forever unless you opt in.events_ingestedretention is also unset. Same behavior for events.usersis unlimited. No seat cap;strict: truewould only enforce if you add alimit.
Restating the whole plan via AGENTA_ACCESS_PLANS is necessary only when
you want to define the full effective plan catalog from scratch (or run
several plans side-by-side). For changing one or two fields on the
default plan, AGENTA_ACCESS_DEFAULT_PLAN_OVERLAY
is the right tool — see the worked examples below.
Worked example — per-user, per-day trace-retrieval limit
Counter.TRACES_RETRIEVED ships on self_hosted_enterprise with
scope=user, period=daily, strict=true, and limit=null (unlimited).
The structural plumbing is in place; setting a real number flips the cap
on without any code change.
Use AGENTA_ACCESS_DEFAULT_PLAN_OVERLAY
to cap each user at 1,000 trace retrievals per day. Restate every quota
field explicitly — the overlay is a field-merge, so fields you omit
inherit from the base, but spelling everything out makes the intended
shape obvious in a diff and survives future changes to the base plan:
{"counters": {"traces_retrieved": {"limit": 1000, "strict": true, "period": "daily", "scope": "user"}}}
What it means at runtime:
- The meters DAO persists one row per
(organization_id, workspace_id, project_id, user_id, year, month, day, key)tuple. Different users get different rows; the same user on different days gets different rows. - Every trace/span fetch or query handler runs
check_entitlements(key=Counter.TRACES_RETRIEVED, delta=<distinct traces returned>)in hard-adjust mode. The first request that would push that user's daily counter past 1000 getsHTTP 429 Too Many Requestsand the meter is not bumped — becausestrict=truemakes the DAO predicategreatest(value + delta, 0) <= limit, the request that crosses the line is itself rejected (no "one free overshoot"). - The usage rollup sums every matching row for today across users, so the UI can reflect total org-wide daily retrievals. The per-user cap is enforced; the org-wide display is informational.
- The counter is tracked and surfaced internally for enforcement and usage display.
This is the pattern for any per-scope, per-period counter: the meters
layer handles the per-row bookkeeping automatically based on what the
quota declares for scope and period.
Worked example — seat cap via overlay
To cap users at 50 seats for a single-instance self-hosted deployment,
set the overlay with every gauge field restated explicitly:
{"gauges": {"users": {"limit": 50, "strict": true}}}
The gauge starts blocking the 51st invite immediately after restart.
AGENTA_ACCESS_ROLES
JSON object keyed by scope. Scope values are non-empty arrays of custom
role entries. The owner and viewer minima are platform-managed and
always synthesized for every scope — env can only add roles, never
redefine the minima.
Top-level shape
{
"<scope>": [<RoleEntry>, ...],
...
}
Recognized scopes: organization, workspace, project. Unknown scopes
fail startup. Omitted scopes keep their full code defaults.
RoleEntry fields
| Field | Type | Required | Description |
|---|---|---|---|
role | string | yes | Slug; cannot be owner or viewer (reserved). |
description | string | no | Human-readable description for UIs. |
permissions | string[] | yes | Permission enum slugs, or "*" for full access. |
Platform minima (always present)
The platform always synthesizes owner and viewer in every scope. Their
permission sets are code-defined:
| Scope | owner | viewer |
|---|---|---|
organization | ["*"] | [] (membership marker, no permissions) |
workspace | ["*"] | Read-only set (sourced from the code-default WorkspaceRole.VIEWER) |
project | ["*"] | Same read-only set |
Org-scope viewer having no permissions is intentional: organizations don't
have a permission concept today — viewer is purely a membership marker.
Examples
AGENTA_ACCESS_ROLES is an override, not an overlay. For any scope you
name in the JSON, the parser replaces the default extras
(admin/developer/editor/annotator on the workspace and project
scopes) with whatever you provide. The platform minima (owner and
viewer) are always re-synthesized, but the default extras are not.
This matters because project_members.role rows are populated with
workspace-role slugs at write time. An operator who names only reviewer
in project keeps owner/viewer/reviewer and silently strips every
existing project member of their permissions. If your intent is to
add one role on top of the defaults, use
AGENTA_ACCESS_ROLES_OVERLAY instead.
Override the full project-scope catalog (destructive — restate everything)
{
"project": [
{
"role": "admin",
"description": "Full management of project members and configuration.",
"permissions": ["*"]
},
{
"role": "developer",
"description": "Standard contributor.",
"permissions": ["read_system", "edit_evaluation", "view_evaluation", "edit_testset", "view_testset"]
},
{
"role": "editor",
"description": "Edit-level contributor.",
"permissions": ["read_system", "view_evaluation", "view_testset"]
},
{
"role": "annotator",
"description": "Annotates traces for evaluation.",
"permissions": ["read_system", "view_spans", "edit_annotations"]
},
{
"role": "reviewer",
"description": "Can inspect runs and annotate traces.",
"permissions": ["read_system", "view_evaluation_runs", "edit_annotations"]
}
]
}
After applying this override, /workspace/roles/ and member serialization
return owner, viewer, admin, developer, editor, annotator, and
reviewer for the project scope. Workspace and organization scopes are
untouched. The permissions arrays restate the default-extras' permissions
verbatim because the override does not inherit from them — anything you
don't list is dropped.
Add a single role on top of the defaults (use the overlay)
{
"reviewer": {
"description": "Can inspect runs and annotate traces.",
"permissions": ["read_system", "view_evaluation_runs", "edit_annotations"]
}
}
Set this as AGENTA_ACCESS_ROLES_OVERLAY (not AGENTA_ACCESS_ROLES).
Default extras stay; reviewer is appended. See the overlay
section for full semantics.
The permissions array entries must be valid Permission enum members or
the wildcard "*". Unknown permissions fail startup.
AGENTA_ACCESS_ROLES_OVERLAY
Use this when you want to add one role or patch one existing role without
restating the full AGENTA_ACCESS_ROLES catalog. The overlay is
deployment-wide: after restart, it applies to every organization.
Shape
{
"<role_slug>": {
"description": "string (optional)",
"permissions": ["...permission slugs..."]
},
...
}
If the role already exists, fields you provide replace the existing values.
If the role is new, permissions is required and description is optional.
The platform-managed owner and viewer roles cannot be patched.
Examples
Give the editor role one extra permission on top of its default set
(permissions replaces the array — restate the full list you want):
{"editor": {"permissions": ["edit_annotations", "view_evaluation_runs", "read_system", "view_spans"]}}
Add a new auditor role:
{"auditor": {"description": "Audit-only access.", "permissions": ["read_system"]}}
Rename the annotator description without touching its permissions:
{"annotator": {"description": "Annotates traces for evaluation."}}
Validation
Failures at startup:
- invalid JSON → fail
- empty object → fail
- patching
ownerorviewer→ fail - unknown permission slug → fail
- new role without
permissions→ fail - extra fields on a patch entry → fail
AGENTA_ACCESS_DEFAULT_PLAN
Plan slug assigned to new organizations on signup. Must resolve to one of
the slugs in the effective plan map. For self-hosted deployments, leave it
unset to use self_hosted_enterprise.
The legacy AGENTA_DEFAULT_PLAN env var is still honored; the canonical
form takes precedence when both are set.
AGENTA_ACCESS_DEFAULT_PLAN_OVERLAY
Self-hosted operators often want to tweak one or two entitlement values on
the default plan (trace retention, a throttle rate, a flag) without restating
the whole plan via AGENTA_ACCESS_PLANS. The overlay env var does exactly
that.
This overlay is plan-targeted — it patches only the plan that
AGENTA_ACCESS_DEFAULT_PLAN resolves to (self_hosted_enterprise by
default, or whatever you set explicitly). Organizations on any other plan
are unaffected. This is the opposite of
AGENTA_ACCESS_ROLES_OVERLAY, which is
plan-independent and applies to every organization.
Targeting
The overlay applies only to whatever AGENTA_ACCESS_DEFAULT_PLAN
resolves to. There is no multi-plan overlay; for cross-plan changes use
AGENTA_ACCESS_PLANS.
Shape
Same top-level keys and units as a plan entry in AGENTA_ACCESS_PLANS
(description, flags, counters, gauges, throttles). Every field is
optional; what you set replaces or merges into the base plan field-by-field.
What you omit stays at the base plan's value.
One divergence from the plan-entry shape: throttles is a map keyed by
category slug ("standard", "core_fast", …) instead of a list. That makes
per-category patches ergonomic. Throttles that combine multiple categories
or use endpoints cannot be addressed via the overlay — operators who need
that should use AGENTA_ACCESS_PLANS.
Examples
Bump trace retention to monthly:
{"counters": {"traces_ingested": {"retention": 44640}}}
Raise the STANDARD throttle's rate to 7200 tokens/minute without touching the capacity:
{"throttles": {"standard": {"bucket": {"rate": 7200}}}}
Both at once:
{"counters": {"traces_ingested": {"retention": 44640}}, "throttles": {"standard": {"bucket": {"rate": 7200}}}}
Validation
Failures at startup (same idiom as the other access-controls env vars):
- invalid JSON → fail
- empty object → fail
- unknown flag / counter / gauge / throttle category slug → fail
- target plan slug not in the effective plan set → fail
- patching a single-category throttle that doesn't exist on the base plan
(e.g. overlaying
ai_servicesonself_hosted_enterprise, which has no AI-services throttle by default) → fail
Operational guidance
- Store these JSON values in your deployment's secrets manager. They affect runtime enforcement; don't keep them in source-controlled plain env files.
- Validate every change in staging before pushing to production.
- After changing either env var, restart all API workers and background workers — each process loads controls into memory once.
- Logs at startup show the resolved source (
defaultsvsenv) and a short hash of the effective controls; grep API logs for[access-controls]to verify all processes see the same configuration.