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.

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.