Skip to content

Configuration

This page documents all available environment variables for XTM One. The .env.sample file is the canonical source and includes inline comments for each variable.

SECRET_KEY

This is the most critical value. It encrypts all stored integration credentials. If you lose it or change it, all encrypted data becomes unrecoverable.

ADMIN_PASSWORD

The platform resets the admin password to the value in .env on every startup. This means .env is a standing password reset mechanism — anyone with read access to the file can take over the admin account by restarting the service. Treat .env as a privileged secret (owner-only permissions, never committed to version control). After first login, consider rotating the password via the UI and keeping the .env value in sync.

Security

Variable Required Default Description
SECRET_KEY Yes JWT signing + Fernet encryption key. Generate with python -c "import secrets; print(secrets.token_urlsafe(48))". Never change after deployment.
ADMIN_EMAIL Yes Default admin account email. Created or updated on every startup.
ADMIN_PASSWORD Yes Default admin account password. Reset to this value on every startup.
ADMIN_TOKEN No Programmatic API token for the admin (must start with fcp-, min 16 chars).

URLs

Variable Required Default Description
BASE_URL Yes http://localhost:4000 Public URL of the platform. Used for OAuth/SAML callbacks and email links.
FRONTEND_URL Yes http://localhost:4000 Public URL of the frontend. Usually same as BASE_URL.

Platform mode

Variable Required Default Description
PLATFORM_MODE No xtm_one xtm_one is the open standalone platform. Set ai_disabled to start with AI switched off — see Running without AI.
PLATFORM_REGISTRATION_TOKEN No Shared token for OpenCTI/OpenAEV registration. Must match across services.
ENTERPRISE_LICENSE No Enterprise license PEM. Can also be configured from Settings UI.
PLATFORM_NAME No Mode default Display name override. DB value takes precedence.
PLATFORM_LOGO No Mode default Logo override (data URI). DB value takes precedence.

Database

Variable Required Default Description
DATABASE_URL No Built from DB_PASSWORD Full asyncpg connection string. Override when using external PostgreSQL.
DB_PASSWORD Yes (if bundled) Password for the bundled PostgreSQL container.
DB_POOL_SIZE No 15 Connections per worker process.
DB_MAX_OVERFLOW No 10 Extra connections allowed beyond pool size.
PG_MAX_CONNECTIONS No 200 PostgreSQL max_connections (Docker Compose managed only).

Redis

Variable Required Default Description
REDIS_URL No redis://redis:6379 Redis connection string. Override for external Redis.
REDIS_PASSWORD No Redis password (if required).
REDIS_MODE No standalone standalone or sentinel.
REDIS_SENTINEL_HOSTS No Comma-separated Sentinel addresses (host:port).
REDIS_SENTINEL_SERVICE No mymaster Sentinel service name.

File storage (S3 / MinIO)

Variable Required Default Description
S3_ACCESS_KEY Yes S3/MinIO access key.
S3_SECRET_KEY Yes S3/MinIO secret key.
S3_ENDPOINT No minio:9000 S3-compatible endpoint.
S3_BUCKET No copilot-files Bucket name.
S3_USE_SSL No false Enable TLS for S3 connections.
S3_REGION No AWS region (required for AWS S3).

AI providers

Pre-provision LLM providers via environment variables so the platform is ready without UI configuration.

Variable Required Default Description
LLM_PROVIDER_{NAME}_API_KEY No API key for the named provider. Re-applied on every startup (env wins). The provider shows an Env badge in Settings → AI Models and cannot be deleted from the UI; removing the variable and restarting removes it.
LLM_PROVIDER_{NAME}_MODELS No All available Comma-separated list of models to expose. Either pinned or an editable baseline, depending on LLM_PROVIDER_MODELS_EDITABLE. When unset, the provider's stored model list is never touched by the startup seed.
LLM_PROVIDER_MODELS_EDITABLE No false false: the *_MODELS list is the catalog — it overwrites the provider's models on every startup, the Models editor is hidden for env-seeded providers and the API refuses model changes on them (403). true: the *_MODELS list is a baseline — seeded on first startup, then only its changes are applied on later restarts (models added to the variable are added, models removed from it are removed, every other edit made in Settings → AI Models is kept), and administrators can curate the models of env-seeded providers from the UI. The API key stays env-managed and the provider still cannot be deleted from the UI.
MODELS_ENV_PROVIDING_ONLY No false When true, all LLM provider management from the UI/API is disabled (add, edit, rename, delete return 403); providers are defined only through LLM_PROVIDER_* variables. Overrides LLM_PROVIDER_MODELS_EDITABLE.
DEFAULT_MODEL No First configured Platform-wide default model. Re-applied on every startup when a configured provider still offers it (a warning is logged otherwise and the current default is kept).

Supported provider names: OPENAI, ANTHROPIC, GEMINI, OPENROUTER, OLLAMA, MISTRAL, AZURE_OPENAI, BEDROCK, LITELLM, CUSTOM_OPENAI.

Managed deployments: operator baseline, admin curation

Set the credential and a baseline catalog from the environment, then let the tenant administrator curate which models are actually offered from Settings → AI Models:

LLM_PROVIDER_ANTHROPIC_API_KEY=sk-ant-...
LLM_PROVIDER_ANTHROPIC_MODELS=claude-opus-5,claude-sonnet-5,claude-haiku-4-5
LLM_PROVIDER_MODELS_EDITABLE=true

Adding a model to LLM_PROVIDER_ANTHROPIC_MODELS rolls it out to every install on the next restart; removing one retires it everywhere (agents, user preferences and platform defaults that pointed at it are reset). Models the administrator enabled or disabled on top of the baseline are preserved across restarts.

Performance

Variable Required Default Description
WEB_WORKERS No 2 Uvicorn worker processes. Production: 2 × CPU cores + 1.
WORKER_CONCURRENCY No 20 Concurrent async jobs per worker container.
MAX_CONCURRENT_RUNS No 10 Max parallel assignment executions.
MAX_CONCURRENT_SUB_AGENT_TASKS No 5 Max parallel sub-agent tasks.
MAX_CONCURRENT_AUTONOMOUS_RUNS No 12 Dedicated slots for autonomous attack-path runs, separate from the assignment/sub-agent pool so dozens can run in parallel without starving normal execution. 0 shares the pool.
AUTONOMOUS_BURST_MAX_CYCLES No 8 Max decision cycles an autonomous run chains back-to-back before briefly yielding, so a multi-step engagement doesn't stall between cycles.
AUTONOMOUS_BURST_BUDGET_SECONDS No 180 Wall-clock budget for that back-to-back burst (whichever limit is reached first triggers the yield).
AUTONOMOUS_WAITING_INPUT_RECHECK_SECONDS No 3600 How often a run waiting on operator input re-checks for an answer or stop. 0 waits purely for the operator's reply.
AUTONOMOUS_CANCEL_POLL_SECONDS No 2 How often the worker re-checks whether an in-flight run was stopped, so an operator Stop aborts the running agent within seconds instead of after the current decision cycle finishes. 0 falls back to the slower lease-heartbeat detection.

PostgreSQL connection budget

(DB_POOL_SIZE + DB_MAX_OVERFLOW) × (WEB_WORKERS + SAQ workers) + 20 < PG_MAX_CONNECTIONS

Example with 4 web workers and 1 SAQ worker: (15 + 10) × (4 + 1) + 20 = 145 → fits the default PG_MAX_CONNECTIONS=200.

Agent run budget

Cost controls for a single agent run (one assignment execution). Each agent run gets a budget object bounding it along four dimensions at once; before every model call the run reserves that call's worst-case spend, debits the actual spend afterwards, and refuses the next call once the budget no longer covers it. A slice of the cost/token budget is always held back so the run can still compose a final answer from the work it completed.

Every limit at 0 (the default) means no budget is applied and runs behave exactly as before.

These values do double duty. They are the default every run inherits, and they are the ceiling a non-admin cannot exceed: an agent's Budget tab and a flow's Budget node may tighten any dimension freely, but raising one above the value set here requires an administrator. Set them even on a deployment that mostly relies on per-agent budgets — with a dimension left at 0 there is no ceiling for it, so any user may set any value for that dimension on their own agents. See Run budgets for the per-agent and per-flow surfaces and how the layers resolve.

Variable Required Default Description
AGENT_LOOP_MAX_COST_USD No 0 Hard ceiling, in USD, on the model spend of one run. Requires pricing for the model in the platform's price table (seeded automatically for known models); with no price known, cost is not enforced (a warning is logged) and the other limits still apply. 0 = unlimited.
AGENT_LOOP_MAX_TOTAL_TOKENS No 0 Hard ceiling on the tokens one run may consume. 0 = unlimited.
AGENT_LOOP_MAX_TOOL_CALLS No 0 Hard ceiling on the number of tool calls one run may make. Bounds parallel tool use, which the internal iteration limit cannot. 0 = unlimited.
AGENT_LOOP_MAX_DURATION_SECONDS No 0 Hard ceiling on the wall-clock duration of one run. Combined with the assignment's own timeout — the tighter of the two applies. 0 = unlimited.
AGENT_LOOP_BUDGET_FINAL_ANSWER_RESERVE No 0.1 Fraction of the cost and token budgets reserved for the run's closing answer, so an exhausted budget still yields a written result instead of a raw digest of tool output. Clamped to 00.5.
AGENT_LOOP_BUDGET_FALLBACK_ENABLED No false When the cost budget can no longer pay for the next model call, continue the run on the platform's fallback model (Settings → AI Models → Fallback Model / DEFAULT_FALLBACK_MODEL) instead of stopping. One downgrade per run. Only cost qualifies — a cheaper model buys no extra tokens, tool calls, or time. An individual agent or flow may turn this on for itself even when it is off here: it cannot raise the cost ceiling, so it is not a privileged change.

A budget that cannot cover one model call

A limit smaller than the cost or tokens of a single call to the configured model makes every run stop immediately and return a summary of tool output. The backend logs an explicit warning at the start of such a run — raise the budget, or lower the model's maximum output tokens.

Finishing on a cheaper model instead of stopping

A cost cap of, say, $0.50 on an expensive model usually means "don't spend more than $0.50 at this quality", not "abandon the task" — and the same budget buys several times more work on a cheaper model. With AGENT_LOOP_BUDGET_FALLBACK_ENABLED=true, a run that reaches its cost budget switches to the fallback model and carries on, once. The rest of the run — including the closing answer — is then charged at the fallback's rates, so the cap still holds.

It is off by default: finishing a run on a weaker model is a quality decision, so it has to be one you made. Requirements and limits:

  • A fallback model must be configured, and the platform must know its price (pricing is seeded automatically for known models). Without a price there is no way to know the next call is actually cheaper, so the run stops as before.
  • The switch is visible: the run's timeline shows a model-downgrade step, the agent is told its model changed, and every call in the run records which model produced it.
  • The switch is skipped — and the run stops instead — when the fallback would make things worse: its context window cannot hold the agent's instructions, its provider allows fewer tools than the run is using, or the run requires structured JSON output and the fallback is on a different provider. Each case is logged with the reason.

Agentic quota

One agentic execution — a chat turn, an assignment run, a background sub-agent task, a flow's agent step, a browser-extension task — spends one agentic action from the user's action quota. The action is deducted before the agent runs, which is what lets the platform refuse a request instead of discovering mid-run that it was never allowed. The consequence is that a run which ends in failure has already spent an action, on work the user never received.

The agent failure pool gives that unit back, up to a bounded number of failures per agent:

Variable Required Default Description
AGENT_FAILURE_QUOTA_REFUND_ENABLED No true Master switch for the failure pool. On by default — spending an action on a run the platform failed to deliver is the wrong default, and the pool is what keeps that from becoming free unlimited retries. Set it to false to deduct an action for every failure, like a success.
AGENT_FAILURE_QUOTA_POOL No 5 Failures credited back per agent, per user, per pool window. The N+1th failure in the same window spends its action normally. 0 closes the pool without touching the switch above.

How it behaves:

  • The first AGENT_FAILURE_QUOTA_POOL failed runs of a given agent, for a given user, in the current window have their action credited back. Every failure after that spends an action like a successful run — so a provider outage or a broken tool is forgiven, while an agent that fails every time keeps consuming the action quota.
  • The window is one year, on the anniversary of your XTM license. With no license installed it falls back to the anniversary of this platform instance, and if neither is readable, to the calendar year. Settings → Quotas shows which anchor is in use and the exact renewal date.
  • It is scoped per agent and per user: on a company-managed agent shared by a whole company, one user's failures never spend another user's pool.
  • Failed runs still count as work performed: the execution telemetry and the run's own record are untouched, and the run is still visible as failed. Only the action is credited back.

When no model was ever called

A run that could not call a model at all — the provider was unreachable, it was down or overloaded, this platform is not configured / authorised to call it, or the platform abandoned the run itself (the worker was lost, its lease expired, its deadline blew mid-run) — is a different case: nothing was produced, and nothing on the user's side caused it. Those runs have their action credited back for free, outside the pool: they never consume a pool slot, and they are never refused, however many of them happen. One provider outage must not be able to exhaust an agent's whole allowance and leave real failures charged for the rest of the year.

The free credit-back is not gated by AGENT_FAILURE_QUOTA_REFUND_ENABLED: that variable sizes the pool, and an unreachable provider is not a pool question. Turning the pool off still credits back a run that never reached a model.

What counts as "never reached a model" (is_provider_unavailable_error in app/llm/router.py, the single source of truth shared by chat, assignment runs and extension tasks):

Failure Credited back Paid by
No LLM provider configured at all Yes Free
Connection error, DNS failure, timeout Yes Free
Provider 5xx / 529 overload, gateway 502-504 Yes Free
Every provider in the failover chain failed for one of the above Yes Free
Run abandoned by the orchestration engine (worker lost, lease expired, deadline blown mid-run) Yes Free
Background sub-agent task abandoned the same way Yes Free
Rate limit / 429 Yes The agent's pool
Invalid API key, provider not authorised (401/403) Yes The agent's pool
Provider account out of credit (insufficient_quota, billing) Yes The agent's pool
Context overflow, request too large (413) Yes The agent's pool
Tool error, malformed model output, any other crash Yes The agent's pool

Everything the provider replies sits on the pool side deliberately — a rate limit, a rejected credential, an account out of credit. They are persistent and indefinitely reproducible: forgiving them without a bound would stop the action quota metering entirely for as long as they last, which is the free-unlimited-retries outcome the pool exists to prevent. They are still forgiven — out of the bounded pool, which is what bounded is for.

Settings → Quotas reports the two reasons apart — "credited back · agent failure" (from the pools) and "credited back · provider unavailable" (free) — with a per-agent breakdown, so a large number can be attributed before anyone acts on it.

A run the engine abandons is failed from a different process than the one that charged it, so the credit-back cannot rely on anything held in memory: the charge is recorded on the run itself when it happens, and the abandonment path reads it back. A run that was never claimed — nobody ever executed it, which is what a scheduled run does when no worker is running — was never charged, so there is nothing to credit and nothing appears.

Background sub-agent tasks are covered too, by a different mechanism: their action is charged inside the agent turn, where nothing knows about the task, so the charge writes a short-lived metering receipt keyed by the task. The abandonment path redeems that receipt — atomically, so two sweeps cannot credit twice — which means it credits back only what was really charged rather than assuming a charge happened.

Not yet covered (#3128)

Flow runs still are not. A flow can meter several actions (one per agent node), so a single receipt per run does not describe it; the mechanism extends — one receipt per step — but the abandonment path has to enumerate the steps it charged. A worker lost mid-flow therefore still leaves those actions charged.

Bounded on the generous side

A run whose first model calls succeeded and which then died on a provider outage is also credited back for free — charging a user for an outage they could not see is the worse error. Removing that approximation is part of

3128, which also gives failures a classifiable cause instead of the

free-text error string they carry today.

  • The General Assistant is not covered — there is no agent to pool against.
  • Resetting a user's quota usage from the admin UI also restores their pools.

Recharging a pool before its renewal date

An administrator can restore every pool on the platform without waiting for the anniversary, by redeeming a support code issued by Filigran in Settings → Quotas → Agent Failure Pool.

  • A code is a single signed token (XTMP-…) that the admin pastes in. It is verified offline, against the same Filigran trust anchor the platform already uses for license certificates — an air-gapped deployment can redeem one, and no secret is added to the platform.
  • Each code carries an expiry and an identifier, and is single-use: the identifier is recorded in support_code_redemptions on redemption, so a second attempt (or a code shared between two admins) is refused. Settings → Quotas lists the codes already redeemed, when, and by whom.
  • A code is normally bound to one PLATFORM_ID and refused elsewhere; Filigran can also issue a global code, redeemable on any platform.
  • Because verification is offline, a code cannot be revoked once issued — it can only expire. Ask support for a short validity when that matters.

Refusals are shown inline with the reason: not signed by Filigran, altered, expired on a date, issued for another platform, or already used on a date.

Why a pool instead of refunding every failure

Nothing available at the point of failure reliably separates "the provider was down" from "this prompt can never succeed". Crediting back every failure would make an endlessly retried, hopeless run free; crediting none spends a user's action quota on outages they did not cause. A small pool per agent covers the accidental case and charges the systematic one to the quota, with no classification to get wrong.

Sizing is environment-only

Both variables are environment-only, by design: what consumes the action quota belongs to whoever operates the deployment, not to whoever administers the tenant. Changing either one requires a restart. The admin screen is read-only apart from redeeming a support code.

Logging

Variable Required Default Description
LOG_LEVEL No info debug, info, warning, error, critical.
LOG_FORMAT No json json (structured) or console (human-readable). When unset, defaults to json in production or console when DEBUG=true.

Email (SMTP)

Variable Required Default Description
SMTP_HOSTNAME No SMTP server hostname.
SMTP_PORT No 587 SMTP port.
SMTP_USERNAME No SMTP authentication username.
SMTP_PASSWORD No SMTP authentication password.
SMTP_FROM No Sender address for outgoing emails.
SMTP_STARTTLS No true Use STARTTLS.

Observability

Variable Required Default Description
LANGFUSE_HOST No https://cloud.langfuse.com Langfuse server URL.
LANGFUSE_PUBLIC_KEY No Langfuse public key.
LANGFUSE_SECRET_KEY No Langfuse secret key.
PROMETHEUS_ENABLED No false Enable Prometheus metrics endpoint.
PROMETHEUS_BEARER_TOKEN No Bearer token for metrics endpoint (min 16 chars).

Security feature flags

Variable Required Default Description
DISABLE_CODE_INTERPRETER No true (xtm_one mode) Disable code interpreter at infra level.
DISABLE_CUSTOM_TOOLS No true (xtm_one mode) Disable custom tools at infra level.
CUSTOM_TOOL_SANDBOX No auto Custom tool execution mode: auto, docker, subprocess, opensandbox. opensandbox delegates to an OpenSandbox-managed container (also used by the code interpreter) and requires the OpenSandbox connection variables below.
DISABLE_SANDBOX_QUOTA_USAGE No false Opt out of per-user sandbox usage counting (quota_usage rows with usage_type="sandbox", alongside the existing LLM-proxy/agentic usage). Counted by default; set true for self-hosted/on-prem deployments that manage their own sandbox infrastructure independently of hosted usage-based billing. Restart required to take effect.

XTM Hub sign-in gate

Restricts sign-in to people XTM Hub has entitled to this XTM One instance, and lets the Hub decide who is an administrator here. OIDC only — XTM Hub does not provide SAML.

Variable Required Default Description
XTM_HUB_ADMIN_GROUP No The role, in this instance's XTM Hub claim, that means administrator. Matched exactly, including case. Setting it enables the gate; empty leaves OIDC behaving exactly as it does without this feature.

The gate reads a claim named after this instance — {PLATFORM_ID}_groups — so PLATFORM_ID must be set when you enable it. It is the switch that is XTM_HUB_ADMIN_GROUP rather than PLATFORM_ID, because forcing a platform identifier is an ordinary thing to do for clone/restore identity continuity and must not enable a sign-in gate as a side effect. Enabling the gate without PLATFORM_ID refuses to start, rather than silently refusing every sign-in.

Enabling it changes three things at once:

  • Every sign-in is checked, not only the first. Someone who already has an account and has since lost their Hub entitlement is turned away too.
  • A refused sign-in creates no account and lands on the login page with a neutral message. The reason is in the server log, not on the page — the person is unauthenticated, and the criteria describe how your IdP is configured.
  • Administrator follows the Hub in both directions: granted when the Hub asserts it, and removed when it stops. The account named by ADMIN_EMAIL is exempt from removal, so a Hub-side group change cannot lock you out of your own instance.

If a misconfigured gate refuses everyone, sign in with a local account — that route stays available and is the way back in.

MCP OAuth

MCP OAuth runs in two independent directions, and each has its own switch.

Inbound — an external MCP client (Claude Desktop, Cursor, …) connecting to XTM One. The platform acts as the authorization server and lets clients self-register, which the MCP specification requires because a client has no credentials before any user session exists.

Outbound — XTM One connecting to a remote MCP server. With no client ID configured, the platform discovers the remote authorization server and registers itself, so a server can be added from its URL alone. This is the MCP specification's intended onboarding path: modern remote MCP servers do not issue static credentials to paste into a form.

Variable Required Default Description
MCP_DCR_ENABLED No true Inbound. Allow external MCP clients to self-register against this platform (POST /mcp/oauth/register, RFC 7591). The endpoint is unauthenticated by specification, so it is protected by a per-IP rate limit and a bounded client table rather than a login. Set false to turn registration off entirely (the endpoint then returns 403).
MCP_DCR_MAX_CLIENTS No 500 Inbound. Upper bound on dynamically registered clients. At the cap the oldest registrations are evicted; RFC 7591 clients re-register transparently. Values below 1 are clamped to 1.
MCP_OUTBOUND_DCR_ENABLED No true Outbound. Let the platform register itself with a remote MCP server that advertises a registration endpoint, so remote servers can be connected without pre-provisioned credentials. Set false to require every outbound OAuth client to be provisioned by hand — connecting a remote server then needs an explicitly configured client ID.

OpenSandbox

Only used when CUSTOM_TOOL_SANDBOX=opensandbox. Governs the OpenSandbox-managed sandbox used by the code interpreter and by custom tools.

Variable Required Default Description
OPENSANDBOX_DOMAIN Yes (if opensandbox mode) localhost:8090 Host (and optional port) of the OpenSandbox service. The default matches local dev (see dev.sh) and isn't enforced at startup — a production deployment that forgets to set this won't fail loudly, it will just try to reach localhost:8090 and fail to connect.
OPENSANDBOX_PROTOCOL No http Protocol used to reach OpenSandbox (http or https).
OPENSANDBOX_API_KEY No API key for authenticating to OpenSandbox.
OPENSANDBOX_IMAGES No python:sandbox-python:0.1,nodejs:sandbox-nodejs:0.1 Per-key container image, as comma-separated key:image pairs (split on the first : per entry, so a registry:port-qualified image survives intact). Overrides merge on top of the defaults, so overriding one key leaves the other on its default.
OPENSANDBOX_K8S_POOL_NAMES No — (cold start, k8s-only) Per-key Kubernetes Pool binding, as comma-separated key:pool-name pairs, e.g. python:my-python-pool,nodejs:my-js-pool. A Pool's warm pods run one fixed image, so one name can't back both sandbox kinds — a key missing from the mapping (or the whole variable unset) cold-starts for that key.
OPENSANDBOX_NETWORK_ALLOW No — (all egress denied) Comma-separated list of domains/IPs to allow as egress from every OpenSandbox container. Empty means all egress is denied.
SANDBOX_CPU No 1 CPU requested for each OpenSandbox container (code interpreter + custom tools). Passed through as-is to the OpenSandbox SDK's resource argument on the cold-start path. When OPENSANDBOX_K8S_POOL_NAMES binds that sandbox kind to a k8s Pool, this is not read at all — the Pool manifest's own resources.requests governs warm-pod sizing instead, so there's no sync requirement between the two - see dev-docs/opensandbox-pool-management.md.
SANDBOX_RAM No 128Mi Memory requested for each OpenSandbox container. Same cold-start-only pass-through as SANDBOX_CPU.
OPENSANDBOX_USE_SERVER_PROXY No false Route sandbox traffic through the OpenSandbox server instead of connecting directly to each sandbox's endpoint. Useful for any deployment topology where this process can reach the OpenSandbox server but not the individual sandbox endpoints it hands back — e.g. a native dev.sh backend against a containerized server, where the server advertises endpoints via a hostname like host.docker.internal that only resolves from inside other containers (dev.sh sets this automatically in that case), or a production/k8s deployment where sandbox pods aren't directly routable from this process but the server's service is.

Running the OpenSandbox server itself

The variables above configure how the platform connects to an OpenSandbox server. These configure how that server itself runs:

Variable Required Default Description
OPENSANDBOX_SERVER_DOCKER No false Local dev only (dev.sh / dev.ps1 / dev-podman.sh). By default the launcher runs opensandbox-server natively; set true to run it as the opensandbox Docker Compose service (docker-compose.dev.yml, plain bridge networking) instead. The native default remains preferred on WSL2, where Docker Desktop's port-publish relay has been observed to fail, or hang unpredictably, for this container. dev.sh also sets OPENSANDBOX_USE_SERVER_PROXY=true automatically when this is enabled, since the backend runs natively but the server does not — see that variable above.
KUBECONFIG_PATH Yes (if the opensandbox-k8s Compose profile is enabled) Production only (docker-compose.yml). Path to a kubeconfig, authorized against the target cluster, mounted into the opensandbox-k8s service so it can manage sandbox pods there instead of local Docker containers. That cluster must already have the BatchSandbox CRD, opensandbox-controller, and the kata-fc RuntimeClass installed.

docker-compose.yml (production) exposes two mutually exclusive, opt-in profiles for the OpenSandbox server, matching where sandbox containers/pods should run: docker compose --profile opensandbox-docker up -d (Docker containers on the same host) or docker compose --profile opensandbox-k8s up -d (pods on an external Kubernetes cluster).

  • Target namespace for the opensandbox-k8s profile: set via the namespace key in [kubernetes] in backend/sandbox/sandbox.k8s.firecracker.toml (mounted into that service) — there's no env var for this specific setting, it's TOML-only. (opensandbox-server itself does read two env vars: OPENSANDBOX_SERVER_API_KEY for its API key, and SANDBOX_CONFIG_PATH as a fallback for --config when no CLI flag is given — dev.ps1 relies on the latter — but neither substitutes for TOML settings like namespace.) Defaults to null (server default) if omitted. See the OpenSandbox server configuration reference for the full [kubernetes] section.

Sandbox call log fields

Every OpenSandbox-routed call (code interpreter, custom JS tool, custom Python tool) emits one structured sandbox_call log event to stdout (JSON in production — see Logging above), independent of the quota_usage counters. These fields are stable across releases so a log pipeline (e.g. shipping stdout into Grafana/Loki) can filter and aggregate on them; a field not listed here (e.g. level, timestamp, logger) is generic structlog metadata, not part of this schema.

Field Type Present when Description
usage_type string Always One of code_interpreter, custom_tool_js, custom_tool_python.
user_id string (UUID) Only when the call had a known user context The caller's user ID.
outcome string Always success or failure.
failure_reason string Only when outcome=failure One of sandbox_ready_timeout (the sandbox never became ready in time), sandbox_create_failed (other sandbox-creation error), sandbox_create_error (unexpected error creating the sandbox), nonzero_exit (the executed code/tool exited with a non-zero status), execution_error (an unexpected error while running the code/tool).
cpu_used_percentage float Successful calls and nonzero_exit failures, when metrics could be retrieved CPU used, as a percentage.
cpu_count float Same as cpu_used_percentage Number of CPU cores allocated to the sandbox.
memory_used_in_mib float Same as cpu_used_percentage Memory used, in MiB.
memory_total_in_mib float Same as cpu_used_percentage Memory allocated to the sandbox, in MiB.
tte_ms integer Successful calls and nonzero_exit failures, when the sandbox backend reported a completed execution Time-to-execute: the sandbox's own reported execution duration, in milliseconds (not overall call time, which also includes sandbox creation/setup). Can be absent even on a nonzero_exit failure if the process errored before an execution-complete event was ever emitted (e.g. an immediate crash).
egress_default_action string Always Always "deny" today.
egress_allow_list array of strings Always (may be empty) The egress allow-list (OPENSANDBOX_NETWORK_ALLOW) applied when the sandbox was created.

CPU/RAM/TTE are omitted (not emitted as null) whenever they aren't available — e.g. no sandbox was ever created (sandbox_ready_timeout, sandbox_create_failed, sandbox_create_error), or the metrics lookup itself failed or timed out. A slow or unreachable metrics lookup is bounded to a few seconds and never delays or fails the underlying sandbox call.


Webhook delivery

Governs POST /api/hooks/assignments/{assignment_id} — the endpoint an external system (SIEM/SOAR, ticketing, CI) posts to in order to drive an agent.

An assignment runs one execution at a time, because parallel runs of the same assignment race the same tools and database writes. A delivery arriving while a run is active is therefore queued, not discarded: it becomes a run in the queued status and starts as soon as the assignment goes idle. An accepted delivery produces exactly one run, eventually.

Variable Required Default Description
ASSIGNMENT_QUEUE_ENABLED No true Queue deliveries that arrive while the assignment is busy. Set false only to restore the legacy behaviour, where such a delivery was discarded and still answered 200.
ASSIGNMENT_QUEUE_MAX_DEPTH No 20 Maximum deliveries held per assignment. Bounds the backlog a slow or stuck assignment can accumulate.
ASSIGNMENT_QUEUE_OVERFLOW_POLICY No reject What happens to a delivery arriving at a full queue. reject: HTTP 429 + Retry-After, so a sender with retry logic loses nothing. drop_oldest: the oldest waiting delivery is finalized as a skipped run (visible, payload preserved) and the new one takes its slot. drop_newest: the incoming delivery is discarded with a 200 and a warning — no run row is created.
ASSIGNMENT_QUEUE_RETRY_AFTER_SECONDS No 30 Retry-After value returned with the reject policy's 429.
ASSIGNMENT_QUEUE_HOLD_ON_AWAITING_HUMAN No true Whether a run paused for human input holds its place in the queue. true keeps deliveries waiting behind the unanswered approval — bounded and visible, unlike the old silent drop. false lets the queue drain past a paused run; only safe when a HITL pause leaves no half-written shared state.
ASSIGNMENT_QUEUE_DRAIN_BATCH No 20 Maximum assignments drained per reconciler cycle.
WEBHOOK_REQUESTS_PER_MINUTE No 60 Per-assignment delivery throttle, applied before the request body is read. Over-limit deliveries get 429 + Retry-After. Raise it for a high-cadence alert feed — the queue still bounds how many accepted deliveries become runs.
WEBHOOK_DEDUP_TTL_SECONDS No 300 Idempotency window. A delivery whose key was already seen within the window replays the first delivery's response instead of firing a second run.

Response statuses

status Meaning
ok A run started. triggered[0].run_id is its id.
queued Accepted, waiting behind the active run. run_id is the queued run, queue_depth the current backlog.
duplicate (with deduplicated: true) The same idempotency key already arrived within the window. The original response is replayed; nothing new was triggered.
dropped The queue was full under drop_newest. This delivery will never run.
ignored The target does not resolve (unknown, disabled, or not a webhook trigger), or an event filter excluded it.
HTTP 429 Not accepted — retry after the Retry-After delay. Either the per-assignment throttle or a full queue under reject.

Idempotency

Duplicate deliveries — proxy retries, sender at-least-once redelivery, a double-submitted test POST — are deduplicated so a retry returns the original run_id without re-running the assignment. The key is derived in this order:

  1. An explicit header: Idempotency-Key, X-Idempotency-Key, X-GitHub-Delivery, Webhook-Id / Svix-Id, or X-Hook-Signature-Id.
  2. A provider-specific id in the payload (OpenCTI notification_id, Slack event_id, HubSpot eventId, or a JSON-API type + id pair).
  3. A SHA-256 of the URL scope + raw request body.

Send an Idempotency-Key

Step 3 cannot tell a retry apart from a genuine recurrence. Two distinct events carrying byte-identical bodies inside the window — a repeated detection with no timestamp, a state notification with no counter — collapse into a single run, and the second delivery is answered with deduplicated: true. Sending an Idempotency-Key that is unique per logical event removes the ambiguity entirely. Failing that, shorten WEBHOOK_DEDUP_TTL_SECONDS below the sender's event cadence.


Runners

A Runner is a downloadable native daemon a user installs on a machine they control, enrolls once from Profile > Runners, and leaves running. It makes only outbound connections and pulls commands from the platform (the platform never opens a connection to the machine), giving agents genuine full-desktop screenshots, real-browser control, shell + file operations, and synthetic input through the computer_use built-in tool. Enrollment is a single-use, short-lived token exchanged once for a durable credential stored in the machine's OS keychain.

The runner installers, bare-binary archives, and self-update artifacts are built by the release pipeline and embedded in the platform image — every instance serves its own version-matched downloads directly from Profile > Runners and answers the runners' self-update checks from the same embedded set. There is no external download URL, CDN, or GitHub Releases dependency to configure, and an air-gapped deployment is fully self-sufficient. A source/dev build (no embedded distribution) simply reports downloads as unavailable while everything else — enrollment, transport, computer_use — keeps working with runners you built yourself (apps/runner/build.ps1 / build.sh).

Variable Required Default Description
RUNNERS_ENABLED No true Master switch for the whole Runner subsystem: enrollment, the runner transport (WebSocket + long-poll), and the computer_use agent tools. Set false to remove it entirely (every runner endpoint then returns 404 and the Profile tab is hidden).
RUNNER_DIST_DIR No runner_dist Directory of the embedded runner distribution inside the image, populated by CI at image build time. Only needs changing if you repackage the image and relocate the artifacts. Empty/absent (source builds) reports downloads as unavailable.
RUNNER_MIN_SUPPORTED_VERSION No Minimum runner binary version accepted at connect (protocol gate). Empty means no floor; a runner below it is told to self-update. Format MAJOR.YYMMDD.SEQ.
RUNNER_MAX_PER_USER No 200 Cap on active runners a single user (or the company-managed fleet) may have enrolled at once, enforced at enrollment so an unattended roll-out cannot create unbounded records. 0 = unlimited.
RUNNER_MAX_INFLIGHT_COMMANDS No 10 Cap on concurrent in-flight commands per runner; a burst past this is refused with a transient "busy" so parallel tool calls cannot swamp one machine. 0 = unlimited.

Tuning knobs

The transport cadence and timeouts (RUNNER_HEARTBEAT_INTERVAL_SECONDS, RUNNER_OFFLINE_AFTER_SECONDS, RUNNER_LONG_POLL_TIMEOUT_SECONDS, RUNNER_RESULT_WAIT_TIMEOUT_SECONDS, RUNNER_COMMAND_DEFAULT_TIMEOUT_SECONDS, RUNNER_MAX_RESULT_BYTES, RUNNER_ENROLLMENT_TTL_SECONDS) have sensible defaults and rarely need changing. See .env.sample for the full list.

Feature flags

Variable Required Default Description
ENABLED_FEATURES No — (none enabled) Comma-separated list of feature flag ids to turn on experimental/in-development features, e.g. MY_FLAG,OTHER_FLAG. Use * to enable all of them. Leaving this unset enables none of them. Changing it requires a restart — there is no toggle in the Settings UI. An unrecognized flag id (e.g. left over from another branch) is logged as a warning and ignored — it never enables anything and never prevents the platform from starting.

The resolved list of enabled flags is also sent to the frontend (as part of the public, boot-time GET /auth/config response), so a flag can control both backend behavior and whether a UI element is shown — with the backend independently enforcing the same flag on any route it gates.


Skill loading

An agent's prompt carries a catalog of its attached skills (name + description) and the agent pulls a skill's full content in on demand with the load_skill tool, so prompt size scales with the number of attached skills rather than with the size of their content. A skill that must stay in the prompt at all times can be pinned per agent on the agent's Configuration tab.

These caps bound the worst case and rarely need changing:

Variable Required Default Description
SKILL_LOAD_MAX_CHARS No 60000 Maximum characters returned by one load_skill call. Longer content is truncated with an explicit marker naming how much was cut.
SKILL_LOAD_BUDGET No 8 Maximum number of distinct skills one run may load. Re-loading an already-loaded skill never counts again. 0 = unlimited.
SKILL_RESOURCE_MAX_BYTES No 40000 Maximum bytes returned by one read_skill_resource call (files bundled with a packaged skill).