Skip to main content

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.
Restart required

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.

Docker Compose vs Kubernetes

Docker Compose users set these as environment variables in the env file passed with --env-file. Kubernetes users should set the matching Helm values under agenta.access in values.yaml; the chart renders them into backend environment variables for the API, workers, cron, and Alembic job.

Helm valueEnvironment variable
agenta.access.plansAGENTA_ACCESS_PLANS
agenta.access.rolesAGENTA_ACCESS_ROLES
agenta.access.rolesOverlayAGENTA_ACCESS_ROLES_OVERLAY
agenta.access.defaultPlanAGENTA_ACCESS_DEFAULT_PLAN
agenta.access.defaultPlanOverlayAGENTA_ACCESS_DEFAULT_PLAN_OVERLAY
Validation is strict

If any override var is set, validation runs at startup:

  • invalid JSON → fail
  • unknown flag / counter / gauge / permission slug → fail
  • AGENTA_ACCESS_PLANS empty object → fail
  • plan entry with no entitlement info and no description → fail
  • AGENTA_ACCESS_ROLES redefining the reserved owner or viewer slug → 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.

FieldTypeRequiredDescription
descriptionstringone-ofOperator-facing description (not shown to users). Description-only entries are accepted for display-only/custom plans.
flagsobjectone-ofMap of flag slug → bool. See flag keys.
countersobjectone-ofMap of counter slug → Quota. See counter keys.
gaugesobjectone-ofMap of gauge slug → Quota. See gauge keys.
throttlesarraynoList of Throttle entries. See throttles.

Quota shape

Used by counters and gauges map values.

FieldTypeDefaultDescription
freeinteger | nullnullFree-tier allowance before paid or capped usage applies.
limitinteger | nullnullHard cap; null = unlimited.
strictboolean | nullnullIf true, the request that would cross the limit is itself rejected (no "one free overshoot"). null is treated as false.
retentioninteger | nullnullRetention 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.
scopestring | nullnullGranularity of the meter row: "organization" (default when null), "workspace", "project", or "user".
periodstring | nullnullMetering bucket: "daily", "monthly", "yearly". null means non-periodic (used for gauges). Replaces the pre-reshape monthly: true boolean.

Flag keys

rbac, audit, 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 also an enforced usage counter: event publishing performs a soft quota check before queueing, the events worker applies the authoritative meter adjust, and operators can set limit as well as retention per plan.

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

FieldTypeDescription
bucket.capacityintegerMax tokens in the bucket.
bucket.rateintegerTokens added per minute.
bucket.algorithmstring | nullOptional algorithm tag.
modestring"include" or "exclude".
categoriesarray | nullEndpoint categories the throttle applies to.
endpointsarray | nullExplicit [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,
"audit": 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 five flags are true. RBAC, audit-log access, access controls, custom domains, and SSO are all on by default.
  • No counter has a limit. Every counter is limit: null (omitted), so the meters layer tracks usage but never blocks a request. strict: true is structurally present so it kicks in the moment a limit is added.
  • traces_ingested retention is unset. The tracing-flush job iterates plans and skips this one — traces are kept forever unless you opt in.
  • events_ingested retention is also unset. Same behavior for events.
  • users is unlimited. No seat cap; strict: true would only enforce if you add a limit.
Prefer the overlay for one-knob tweaks

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 gets HTTP 429 Too Many Requests and the meter is not bumped — because strict=true makes the DAO predicate greatest(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

FieldTypeRequiredDescription
rolestringyesSlug; cannot be owner or viewer (reserved).
descriptionstringnoHuman-readable description for UIs.
permissionsstring[]yesPermission 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:

Scopeownerviewer
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

Override semantics: replace, not merge

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 owner or viewer → 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.

Scope of effect

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_services on self_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 (defaults vs env) and a short hash of the effective controls; grep API logs for [access-controls] to verify all processes see the same configuration.