Environment Variables
Every environment variable the Cat Factory backends read, grouped by purpose and annotated with the deployment modes each applies to. This page is generated from the canonical list in the code repository, which a CI guard reads on every change, so the two cannot drift.
That canonical list is the backend's own, and a deployment needs a few things it does not carry — frontend build variables, and integration credentials that are entered in the UI rather than read from the environment. For the narrative on how to configure a deployment (what to set first, what refuses to boot without it), read Configuration, which is the authoritative page for standing a deployment up.
These names are RESERVED
Every variable in this reference belongs to the platform, and none of them can be the KEY a capability credential is looked up by. A tool server (MCP) or a generative binary integration declares the credential it needs by name, and the default resolver reads that name off this same environment before the value is injected into an agent process: a declaration of ENCRYPTION_KEY would hand a prompt-injectable agent the key every stored credential is sealed with. Such a declaration is refused at boot and again at dispatch. Look the credential up under a name of the integration's own (ACME_IMAGE_API_KEY) and set it beside these.
The rule is enforced by isReservedPlatformEnvKey (backend/packages/contracts/src/reserved-env-keys.ts), which reserves the platform's prefix families (AUTH_, GITHUB_, LOCAL_, …) plus the remaining exact names, case-insensitively, because process.env lookup is case-insensitive on Windows. scripts/check-reserved-env-keys.mjs fails CI when a variable documented below is not covered, so adding a row here is also how the reserved set stays current.
The model-provider keys are reserved too. That looks like over-reach and is not: OPENAI_API_KEY is billable and exfiltratable, and an integration that wants to call OpenAI on the deployment's account should say so in its own variable rather than silently inherit the one the model router spends.
A credential's OTHER name is not reserved
The floor above binds the LOOKUP name, because that is the one that can read this environment. It does not bind the variable a resolved value is INJECTED under in the agent's or the MCP server's process, which reads nothing at all. A declaration keeps them apart with envName:
secretKeys: [{ key: 'ACME_GITHUB_TOKEN', envName: 'GITHUB_PERSONAL_ACCESS_TOKEN' }]
That escape is why the prefix families can be as broad as they are. The GitHub MCP server's client reads GITHUB_PERSONAL_ACCESS_TOKEN, the Slack one reads SLACK_BOT_TOKEN, and an AWS one reads AWS_ACCESS_KEY_ID: the platform reads none of those, but each falls inside a family it does own, and no deployment can rename what a vendor's own SDK looks for. Injection names have their own, narrower rule instead (isToolchainEnvName): not PATH, NODE_OPTIONS, npm_config_* or the other names that would reconfigure the process rather than authenticate a call.
Name a capability credential under a prefix of your own
The lookup name is yours to pick, so pick a family and stay in it. The convention this repo's own registrations follow is MCP_… for a tool server's credentials and GEN_… for a generative binary integration's, or a house prefix (ACME_…) where one deployment owns both. Two reasons, and the second is the one that bites:
- Every family the platform uses is reserved (
AUTH_,GITHUB_,LOCAL_,SLACK_, the model-provider keys, …), so a name that reads naturally is quite often refused.MCP_SLACK_TOKENis not, andenvNameputs the value into the variable the vendor's own client insists on. allowKeysis set PER DEPLOYMENT, not per capability, and it gates every subject the resolver serves. An allow-list holding onlyMCP_…silently resolves nothing for a registered image or music generator, and the failure surfaces as the agent reporting that integration unavailable with nothing pointing back here. List a prefix per family, or the exact keys your registrations declare.
Whether a name resolves at all is answerable without starting a run: Infrastructure → Capability credentials lists every key the deployment's capabilities declare, and the tool-server rows above it carry a Test button that resolves the credential through the real chain and speaks MCP to a remote server. See custom-agents.md.
The environment is the FALLBACK, not the primary home
Capability credentials are resolved from the per-workspace capability-credential store first, falling back to this environment per key, so a multi-tenant deployment gives each workspace its own vendor account instead of sharing one variable. See ADR 0041. Setting a variable still works and is the right mechanism for a single-tenant or local install.
Deployment modes
The same @cat-factory/server app ships to several targets. "Mode" is which facade boots plus a few switches:
| Mode | Facade | Meaning |
|---|---|---|
| Cloudflare | runtimes/cloudflare (@cat-factory/worker) | The Worker: D1 + Durable Objects + Workflows + Containers. Config comes from wrangler.toml [vars] + secrets + bindings. |
| Node | runtimes/node (@cat-factory/node-server) | The hosted Node service: Postgres (Drizzle) + pg-boss. Config comes from process.env. Also called "remote node". |
| Local | runtimes/local (@cat-factory/local-server) | The Node facade a single developer runs on their machine: per-run local containers + a GitHub PAT. Reuses every Node variable plus the LOCAL_* extras and some local-friendly defaults. |
| Mothership | a Node/Cloudflare deployment acting as the hosted org backend, plus a local laptop that delegates persistence to it over RPC | The hosted side reads the Node/Cloudflare variables; the laptop reads the Local variables plus LOCAL_MOTHERSHIP_*. |
In the tables below the Modes column uses: CF (Cloudflare), Node, Local, MS (mothership-specific). A variable marked Node is also read by Local and by a mothership-mode laptop, because Local reuses the Node config loader; the tables call out Local/MS only when a variable is exclusive to those modes.
Spend budgets
Budgets are tiered: a per-workspace monthly limit (configured in the UI), a per-account limit, and a per-user limit. The two variables below are operator hard ceilings on the account and user tiers. When set, a UI user cannot configure a value above the cap (it is also enforced server-side), the cap is shown on the budget configuration screen, and it acts as the effective tier limit when nothing is configured. How budgets behave for a user is on the website: Budgets. Amounts are in the base pricing currency (EUR by default).
| Variable | Modes | Default | Description |
|---|---|---|---|
BUDGET_MAX_MONTHLY_PER_ACCOUNT | CF, Node, MS | none (uncapped) | Hard ceiling on the account-tier monthly budget any account may configure. |
BUDGET_MAX_MONTHLY_PER_USER | CF, Node, MS | none (uncapped) | Hard ceiling on the user-tier monthly budget any user may configure. |
Notes: these are read by the Node and Cloudflare config loaders, so they apply in the Node, Cloudflare, and mothership-hosted deployments. A single-user local deployment reads them too (Local reuses the Node loader) but they are rarely meaningful there. The per-workspace budget itself is not an env variable: it is configured per workspace in the UI (Workspace settings -> Budget) and defaults to about 100 EUR/month.
Core service & networking
| Variable | Modes | Default | Description |
|---|---|---|---|
DATABASE_URL | Node, Local | required (Node) | Postgres connection string. Prefer 127.0.0.1 over localhost for a local DB: on Windows + Docker Desktop localhost resolves to IPv6 ::1 first and the connection RESETS at boot (ECONNRESET). |
DB_SCHEMA | Node | public | Schema for the app's unqualified tables (relocated via the connection search_path); set when sharing a Postgres with other services. Plain lowercase identifier. |
DB_MIGRATIONS_SCHEMA | Node | drizzle | Schema for the Drizzle migration ledger, so it can't collide with another Drizzle service's drizzle.__drizzle_migrations. Plain lowercase identifier. |
DB_PGBOSS_SCHEMA | Node | pgboss | Schema for pg-boss's durable-job queue tables. Plain lowercase identifier. |
PORT | Node, Local | 8080 | HTTP listen port. |
HOST | Node, Local | all interfaces | Bind address. |
PUBLIC_URL / WORKER_PUBLIC_URL / APP_BASE_URL | Node / CF | derived | Public base URL used to build callback/redirect URLs. |
CORS_ALLOWED_ORIGINS | CF, Node | none | Comma-separated allowed CORS origins. |
ENVIRONMENT | CF, Node | development | Deployment environment label (production, local, ...). |
Realtime (Node horizontal scaling)
| Variable | Modes | Default | Description |
|---|---|---|---|
REDIS_URL | Node | none (single node) | Enables the Redis pub/sub cross-node WebSocket propagator. ioredis is imported only when set. |
REDIS_REALTIME_CHANNEL | Node | default channel | Redis channel for realtime fan-out. |
REALTIME_NODE_ID | Node | generated | Stable id for this replica in the propagator. |
Authentication
| Variable | Modes | Default | Description |
|---|---|---|---|
AUTH_SESSION_SECRET | CF, Node | required (Node/Local) | HMAC secret for session tokens (>= 32 chars). |
HARNESS_SHARED_SECRET | CF, Node | required (executor) | Shared secret the orchestrator sends on every agent-container harness call (x-harness-secret) so a job container only trusts this service (>= 16 chars, stable across restarts). |
AUTH_SESSION_TTL_HOURS | Node | default TTL | Session lifetime. |
AUTH_DEV_OPEN | Node, Local | false (Local true) | Dev-open auth (no sign-in). |
AUTH_PASSWORD_ENABLED | Node, Local | false (Local true) | Enable password auth. |
AUTH_OPEN_SIGNUP | Local | true (Local) | Allow open sign-up. |
AUTH_ALLOWED_LOGINS / AUTH_ALLOWED_ORGS / AUTH_ALLOWED_EMAIL_DOMAINS | CF, Node | none | Allow-lists gating who may sign in. |
AUTH_ALLOWED_REDIRECT_ORIGINS / AUTH_SUCCESS_REDIRECT_URL / AUTH_CALLBACK_URL | CF, Node | none | OAuth redirect configuration. |
AUTH_MACHINE_TOKEN_TTL_MS | CF, Node | 30 days | Lifetime of a machine token minted for a mothership-mode node. |
AUTH_TRUST_PROXY | Node, Local | false | Let the password throttle read the client address from x-forwarded-for instead of the socket peer. Set ONLY when a proxy you control terminates every request: the header is attacker-supplied otherwise, and a client-chosen address defeats the throttle. cf-connecting-ip is deliberately NOT consulted on Node (a generic reverse proxy forwards it untouched); the Worker reads that header alone, because its edge injects and overwrites it. |
AUTH_TRUST_PROXY_HOPS | Node, Local | 1 | How many trusted proxies sit in front of this process, used to pick the client hop out of an x-forwarded-for chain. The rightmost entry is the one the nearest proxy appended, so one proxy needs no change; a CDN plus a load balancer is 2. A chain shorter than this is discarded in favour of the socket peer. |
GITHUB_OAUTH_CLIENT_ID / GITHUB_OAUTH_CLIENT_SECRET | CF, Node | none | "Login with GitHub" OAuth app. |
GOOGLE_OAUTH_CLIENT_ID / GOOGLE_OAUTH_CLIENT_SECRET / GOOGLE_OAUTH_REDIRECT_URL | Node | none | "Login with Google" OAuth app. |
AUTH_SSO_ISSUER_URL / AUTH_SSO_CLIENT_ID / AUTH_SSO_CLIENT_SECRET | CF, Node | none | Enterprise SSO through the deployment's own OpenID Connect provider (Okta, Entra ID, Auth0, Keycloak, PingFederate, a Shibboleth OP …). ONE generic adapter: the issuer's discovery document supplies every endpoint. All three are required together: a partial set REFUSES to boot, as does a non-https issuer, a weak AUTH_SESSION_SECRET, or AUTH_DEV_OPEN alongside SSO. See auth.md. |
AUTH_SSO_LABEL / AUTH_SSO_SCOPES / AUTH_SSO_REDIRECT_URL | CF, Node | see auth.md | SSO presentation + request shaping: the sign-in button label, the space-separated scopes (openid is added when absent), and an explicit redirect_uri for a deployment whose public URL differs from the request origin. |
AUTH_SSO_GROUPS_CLAIM / AUTH_SSO_REQUIRED_GROUPS / AUTH_SSO_ALLOWED_EMAIL_DOMAINS | CF, Node | groups / none / none | Optional narrowings on SSO admission. By default the IdP's own app assignment is the whole allowlist (which is the point of SSO); these restrict it further to named directory groups and/or verified email domains, checked on EVERY sign-in. |
VCS integration (GitHub / GitLab)
| Variable | Modes | Default | Description |
|---|---|---|---|
GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY | CF, Node | none | The GitHub App (installation-based repo access + CI/merge gates). |
GITHUB_APP_SLUG / GITHUB_API_BASE / GITHUB_SETUP_REDIRECT_URL | CF, Node | defaults | GitHub App metadata + API base (GitHub Enterprise). The API base also decides the WEB host the SPA links repositories, pull requests and issues to (/api/v3 stripped, api.github.com mapped to github.com); a base with neither shape names no host and those links are withheld. |
GITHUB_PAT | Local, MS | none | Personal access token local mode uses instead of a GitHub App (push token + CI/merge client). OPTIONAL: without it a developer can sign in with a token on the sign-in screen and it becomes the deployment's credential (sealed on the machine, no restart). Setting it here WINS over an installed one, and closes that browser flow. In mothership mode, neither ⇒ GitHub runs on installation tokens the mothership's GitHub App mints over the machine API. |
GITLAB_PAT / GITLAB_API_BASE | Local | none | GitLab personal access token + API base for a GitLab local deployment. The API base also decides the WEB host the SPA links projects, merge requests and issues to (/api/v4 stripped); a base with no such suffix names no host and those links are withheld. |
LOCAL_VCS_CREDENTIAL_DB | Local, MS | ~/.cat-factory/vcs-credential.sqlite | Where the sign-in-screen-installed source-control token is sealed. Only consulted when neither PAT above is set. |
Model providers
| Variable | Modes | Default | Description |
|---|---|---|---|
OPENAI_API_KEY / ANTHROPIC_API_KEY / QWEN_API_KEY / DEEPSEEK_API_KEY / MOONSHOT_API_KEY | CF, Node | none | Direct vendor API keys. |
OPENROUTER_BASE_URL | CF, Node | public gateway | OpenRouter gateway base URL. |
LITELLM_BASE_URL | CF, Node | required to enable | Operator-hosted LiteLLM gateway (no public default). |
CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN / CLOUDFLARE_AI_GATEWAY | CF, Node | none | Cloudflare Workers AI over REST (Node) + AI Gateway. |
BEDROCK_REGION / AWS_* / BEDROCK_MODELS | CF, Node | none | Opt-in AWS Bedrock. BEDROCK_MODELS is both the resolver's allow-list and the per-model picker enablement (unset ⇒ routing-default only). |
AGENT_DEFAULT_PROVIDER / AGENT_DEFAULT_MODEL / AGENT_DEFAULT_TEMPERATURE / AGENT_MAX_OUTPUT_TOKENS / AGENT_MODELS | CF, Node | built-in routing | Default agent routing + per-kind model overrides. |
Web search
| Variable | Modes | Default | Description |
|---|---|---|---|
WEB_SEARCH_SEARXNG_URL / WEB_SEARCH_SEARXNG_API_KEY | CF, Node, Local | none | SearXNG upstream. |
WEB_SEARCH_BRAVE_API_KEY | CF, Node | none | Brave search upstream (wins when set). |
INLINE_WEB_SEARCH_ENABLED / INLINE_WEB_SEARCH_KINDS / INLINE_WEB_SEARCH_MAX_USES | Node | off | Inline web-search tool for non-container agents. |
LOCAL_WEB_SEARCH | Local | on | Set off to disable the local SearXNG default. |
Execution tuning
| Variable | Modes | Default | Description |
|---|---|---|---|
DECISION_TIMEOUT | CF, Node | default | Human-decision wait timeout. |
JOB_POLL_INTERVAL / JOB_MAX_POLLS / JOB_POLL_FAILURE_TOLERANCE | Node | defaults | Container job polling cadence + limits. |
CI_POLL_INTERVAL / CI_MAX_POLLS | Node | defaults | CI gate polling cadence + limits. |
ADVANCE_TIMEOUT | CF, Node | 30 minutes | Hang ceiling on one pipeline-step advance or status read. |
CONTAINER_MAX_AGE_MINUTES | Node | 90 | Max age of a per-run container before eviction. |
EXECUTION_CONCURRENCY / EXECUTION_HEARTBEAT_SECONDS / EXECUTION_MAX_DRIVE_STEPS / EXECUTION_DRIVE_EXPIRE_MINUTES | Node | defaults | pg-boss execution worker tuning. |
STALE_RUN_SWEEP_MINUTES / STALE_RUN_LEASE_MINUTES | Node | defaults | Stale-run sweeper cadence + lease. |
Storage & retention
| Variable | Modes | Default | Description |
|---|---|---|---|
ENCRYPTION_KEY | CF, Node, Local | required for sealed stores | Base64 system key (>= 32 bytes) for sealed credentials. |
TOKEN_USAGE_RETENTION_DAYS | CF, Node | 395 | Retention for the token_usage ledger. |
LLM_CALL_METRICS_RETENTION_DAYS | CF, Node | 14 | Retention for the LLM-call telemetry store. Long enough that a post-mortem started days after the run can still read the calls; lower it to shrink the store's footprint. |
GITHUB_RATE_LIMIT_RETENTION_DAYS | CF, Node | 7 | Retention for GitHub rate-limit rows. |
GITHUB_COMMIT_RETENTION_DAYS | CF, Node | 90 | Retention for commit-projection rows. |
GATE_OUTCOME_RETENTION_DAYS | CF, Node | 90 | Retention for the settled-gate projection behind the dashboard's gate attempt statistics. |
PLATFORM_RUN_DAY_RETENTION_DAYS | CF, Node | 400 | Retention for the daily run rollup behind the dashboard's 30d/90d windows. |
AUDIT_EVENT_RETENTION_DAYS | CF, Node | 730 | Retention for the account audit log. The longest window here by design: it answers a compliance question, not an operational one. 0 disables the prune entirely. |
NOTIFICATION_RETENTION_DAYS | CF, Node | 90 | Retention for RESOLVED (acted or dismissed) notifications. Open cards are the actionable inbox and are never pruned. |
PROVISIONING_LOG_RETENTION_DAYS | CF, Node | 14 | Retention for the infrastructure provisioning event log (high churn). |
LLM_RECORD_PROMPTS | CF, Node | false | Deployment switch that (with the per-workspace toggle) enables storing prompts/agent context. |
LOG_LEVEL | CF, Node, Local | info | Emit threshold for the structured logger: debug/info/warn/error. An unrecognised value falls back to info. See backend/docs/logging.md. |
Integrations & observability
The *_ALLOW_URL_HOSTS variables below are all read by one SSRF guard, so their entry format is shared: a comma-separated list whose entries match the URL host case-insensitively, either exactly (envs.corp, 10.1.2.3) or as a dot suffix when the entry begins with . (.internal matches internal and a.b.internal). There is no glob syntax, so an entry like *.internal is not a wildcard and matches nothing. Each integration resolves its own list: a host allowed to one is not thereby allowed to another.
| Variable | Modes | Default | Description |
|---|---|---|---|
SLACK_ENABLED | CF, Node | false | Enable the Slack notification channel. |
OBSERVABILITY_ENABLED | CF, Node | false | Enable the post-release-health observability providers. |
CONSENSUS_ENABLED | Node | false | Enable the consensus-orchestration mechanism. |
ENVIRONMENTS_ALLOW_HTTP_URLS / ENVIRONMENTS_ALLOW_URL_HOSTS | Node, Local | off | Relax environment URL restrictions (local defaults on). |
LOCAL_MODELS_ALLOW_LAN | CF, Node, Local | false | Permit private-LAN hosts (RFC1918 / ULA / mDNS .local) for user-registered locally-run model endpoints, beside the always-allowed loopback. Off by default because the endpoint URL is fetched server-side, so on a shared deployment LAN reach is an internal-network SSRF grant. Local mode defaults it on (single tenant). |
RUNNERS_ENABLED | CF, Node | false | Enable self-hosted runner pools. |
MCP_OAUTH_REDIRECT_URL | CF, Node, Local | none | This deployment's public app URL followed by /mcp-oauth-callback, where a vendor's authorization server redirects an operator's browser after they connect an OAuth-protected remote MCP tool server, and the same string registered as the OAuth client's redirect URI at the vendor. It points at the SPA rather than the backend: that page re-presents the vendor's code and state over the authenticated API, which is what lets the completion be session-gated at all (a vendor's redirect carries no bearer token). Operator-set rather than derived from the request, because a Host-derived value differs behind every proxy, preview URL and private hostname a deployment sits behind and the exchange then fails at the vendor with redirect_uri_mismatch. Unset ⇒ the interactive grant refuses with a 503 naming this variable; the client_credentials grant needs no redirect and works without it. Grants are sealed, so ENCRYPTION_KEY is required either way. See backend/docs/mcp-tool-servers.md. |
NOTIFICATION_WEBHOOK_ALLOW_HTTP_URLS / NOTIFICATION_WEBHOOK_ALLOW_URL_HOSTS | CF, Node, Local | off | Relax the strict public-https guard on a workspace's outbound notification-webhook endpoint (e.g. a receiver on an internal host, or a developer's localhost). Scoped to webhooks alone: this is the one integration whose target URL a workspace chooses, so it never rides the operator-set runner/environment allow-lists. The webhook feature itself needs no flag: it assembles wherever ENCRYPTION_KEY is set. |
LANGFUSE_* | CF, Node | none | Langfuse trace sink credentials. |
OTEL_ENABLED / OTEL_EXPORTER_OTLP_* / OTEL_SERVICE_NAME | CF, Node | false | OpenTelemetry OTLP trace + metrics exporter. |
OTEL_PLATFORM_METRICS (+ _WINDOW, _INTERVAL_MS) | CF, Node | false | Push per-account platform-health aggregates as OTLP gauge metrics (opt-in on top of OTEL_ENABLED). |
OTEL_LOGS | CF, Node, Local | false | Export the platform's own structured log lines to the OTLP endpoint as log records (opt-in on top of OTEL_ENABLED). LOG_LEVEL governs what is exported, exactly as it governs what is written locally. |
OTEL_LOGS_MAX_BATCH_SIZE | CF, Node, Local | 128 | Lines per OTLP log POST; also bounds the exporter's in-memory buffer (8 batches), beyond which the oldest lines are dropped and the drop count is reported on the next batch. |
OTEL_LOGS_FLUSH_INTERVAL_MS | Node, Local | 5000 | Node flush cadence (the Worker flushes at the end of every invocation, and a workflow wake also flushes at each durable suspension, since a per-isolate buffer has no later tick guaranteed to reach it). |
PLATFORM_ALERTS | CF, Node | false | Enable platform-health threshold alerting: a periodic sweep raises a platform_health notification (in-app + Slack) when the deployment's own run health crosses a threshold, auto-clearing on recovery. |
PLATFORM_ALERTS_WINDOW | CF, Node | 1h | Window each evaluation aggregates over (1h/24h/7d). |
PLATFORM_ALERTS_INTERVAL_MS | Node | 300000 | Node sweep interval (the Worker is cron-driven). |
PLATFORM_ALERTS_MIN_RUNS | CF, Node | 5 | Minimum terminal runs in the window before the failure-rate alert can fire. |
PLATFORM_ALERTS_MAX_FAILURE_RATE | CF, Node | 0.5 | Failure rate (0..1) at or above which the failure-rate alert fires. |
PLATFORM_ALERTS_MAX_P99_MINUTES | CF, Node | 60 | p99 run duration (minutes) at or above which the slow-run alert fires. |
PLATFORM_ALERTS_MAX_BACKLOG | CF, Node | 50 | Live running/blocked/paused/pending depth at or above which the backlog alert fires. |
PLATFORM_ALERTS_STALLED_BUCKETS | CF, Node | 3 | Trailing trend buckets that must be completely empty before the zero-throughput alert fires. In buckets, not hours, so it scales with the window. Every other condition divides by runs and goes silent at zero, which is what made a dead deployment read as a quiet healthy one. |
PLATFORM_ALERTS_MIN_STALLED_PRIOR_RUNS | CF, Node | 5 | Runs the EARLIER part of the same window must have created before a silence counts as a stall, so a genuinely idle deployment stays quiet instead of paging every night. 0 alerts on silence unconditionally. |
PLATFORM_ALERTS_MAX_FAILURE_KIND_SHARE | CF, Node | 0.8 | Share (0..1) of the window's failures ONE kind must account for before the dominant-failure alert fires. 100% evicted and 100% agent produce an identical failure rate and need opposite fixes. |
PLATFORM_ALERTS_MAX_SWEEP_FAILURES | CF, Node | 3 | Consecutive failed passes of one background sweeper before the sweep-degraded alert fires. A wedged sweeper makes every other signal stale without making any of them fire. |
PLATFORM_ALERTS_FAILURE_KIND_RATES | CF, Node | (none) | Per-failure-kind alert rules: kind=share[:minCount], comma-separated (e.g. evicted=0.05:3,timeout=0.2). The dominant-kind ceiling above asks whether one cause is swamping the rest; these ask whether a NAMED cause reached what this deployment tolerates from it (the share is the trigger point, fired at or above it), which no single ceiling can express (5% evictions is the substrate failing, 40% rejected is the product working). minCount is the per-rule minimum number of failures of that kind, defaulting to 1: without it a low share is a hair trigger, since five terminal runs with one eviction is already 20%. A rule that cannot be read is reported and dropped on its own, never clamped into range. A rule naming a kind this build does not produce is reported and KEPT, since a typo and a retired kind are the same string here and only a human can tell them apart. Unset means no per-kind rules. |
INFRA_REACHABILITY_WATCH | CF, Node | false | Enable the infrastructure-reachability watcher: a periodic sweep probes each workspace's CONFIGURED infrastructure connections (ephemeral-environment provider, self-hosted runner pool) and reports a dead one as unreachable; an infra_unreachable notification (in-app + Slack) plus a live infraSetup push that raises the setup banner. Off by default: it is the one sweep making an OUTBOUND call per workspace per pass. |
INFRA_REACHABILITY_INTERVAL_MS | CF, Node | 300000 | Sweep interval, floored at 30s. Honoured on both facades: Node times it directly, and the Worker (whose cron ticks every 2 min) runs the sweep only on the tick that opens a new interval window, so the cadence of the one sweep that calls OUT per workspace is the operator's to set on Cloudflare too. |
INFRA_REACHABILITY_PROBE_TIMEOUT_MS | CF, Node | 5000 | Per-probe timeout, clamped to 1s..60s. A probe that does not answer inside the budget counts as unreachable. |
EMAIL_SYSTEM_PROVIDER / EMAIL_SYSTEM_FROM / EMAIL_SYSTEM_API_KEY | Node | none | System email sender. |
DOC_SOURCE_<SOURCE>_<FIELD> | CF, Node, Local | none | The DEPLOYMENT's own credentials for a document source, which is what lets a code-registered prompt fragment name a LIVING document (documentRef) instead of a frozen body. <SOURCE> is the source id (CONFLUENCE, NOTION, LINEAR, FIGMA, ZEPLIN) and <FIELD> is that provider's own credential field in SCREAMING_SNAKE, so Confluence reads DOC_SOURCE_CONFLUENCE_BASE_URL / _ACCOUNT_EMAIL / _API_TOKEN and Notion reads DOC_SOURCE_NOTION_API_TOKEN. Distinct from a tenant's connected source: these authenticate reads made on the whole deployment's behalf, cached once deployment-wide. github is NOT configurable this way (its credential is a workspace's App installation). Setting some but not all of a source's variables is REPORTED at boot and leaves the source unconfigured, which then makes any fragment naming it fail validation. See reusable-operations.md. |
Local mode
| Variable | Modes | Default | Description |
|---|---|---|---|
LOCAL_HARNESS_IMAGE | Local | recommended pin | The executor-harness image local mode pulls + runs. |
LOCAL_HARNESS_IMAGE_REFRESH | Local | off | Re-pull the harness image at boot. |
LOCAL_CONTAINER_RUNTIME | Local | docker | Container runtime adapter (docker/podman/orbstack/colima/apple). |
LOCAL_DOCKER_BINARY / LOCAL_DOCKER_NETWORK / LOCAL_HARNESS_HOST_ALIAS / LOCAL_DOCKER_ADD_HOST_GATEWAY / LOCAL_DOCKER_PRIVILEGED_TEST_JOBS | Local | defaults | Docker-CLI adapter tuning. |
LOCAL_NATIVE_AGENTS / LOCAL_HARNESS_ENTRY | Local | off | Run CONTAINER agents natively (no container) on the developer's claude/codex CLI. |
LOCAL_NATIVE_INLINE | Local | on (both) | Which subscription harnesses (claude-code/codex) may serve INLINE steps (reviewer/brainstorm/estimator) via the local CLI; off to disable. |
LOCAL_INLINE_CLI_IDLE_TIMEOUT_MS | Local | 300000 | Kill an inline host-CLI run after this long with NO output. Bounds how long it may be STUCK, not how long it may work (every chunk re-arms it). Whole ms, 1..2147483647; anything else is reported at boot and ignored. |
LOCAL_INLINE_CLI_MAX_TIMEOUT_MS | Local | 3600000 | Absolute wall-clock ceiling for one inline host-CLI run, however busy it looks: the backstop for a run that narrates forever. Same bounds; a ceiling below the idle window is reported, since it makes that watchdog unreachable. |
LOCAL_HARNESS_ENV_ALLOW | Local | none | Extra variable names (comma-separated) the sanitized native-harness child env may inherit on top of the built-in allow-list. |
LOCAL_HARNESS_NODE_ARGS | Local | none | Extra node arguments for the spawned native harness process. |
Mothership mode
| Variable | Modes | Default | Description |
|---|---|---|---|
LOCAL_MOTHERSHIP_URL | MS | none (off) | Enables mothership mode: the local node delegates persistence to this hosted backend. |
LOCAL_MOTHERSHIP_TOKEN | MS | minted via login | Headless/CI machine-token override. |
LOCAL_MOTHERSHIP_TOKEN_DB / LOCAL_MOTHERSHIP_CREDENTIAL_DB / LOCAL_MOTHERSHIP_SETTINGS_DB / LOCAL_MOTHERSHIP_TELEMETRY_DB / LOCAL_MOTHERSHIP_WORK_DB | MS | default paths | Override paths for the laptop's local node:sqlite stores. |
Next: Configuration for what to set first, or Upgrades & Data Retention for the retention windows in context.