Skip to main content

Environment Variable Reference

The complete, exhaustive list of every environment variable AIFactory reads — required vars, config, feature flags, tuning knobs, and credentials. If a fresh install "works here but breaks there", the cause is almost always a variable set in one environment and missing in the other. This page is the single source of truth for finding them.

For a shorter, opinionated tour of the feature flags that matter most, see the Configuration Reference. This page is the full enumeration; that page is the highlights.

How variables are read​

AIFactory reads configuration from four distinct surfaces:

SurfaceNamingWhere it livesRead by
Pydantic SettingsAPP_-prefixedapps/web-server/.env or pod/Helm envapps/web-server/server/config.py
Direct os.environunprefixed (e.g. AIFACTORY_*, provider keys)process env, apps/backend/.env, apps/web-server/.env (loaded into os.environ at startup by server/env_bootstrap.py), or Helm envmodules across apps/backend/ and apps/web-server/server/
Per-project .aifactory/.envAIFACTORY_* / DELEGATE_*the target repo being builtPR-endgame + deploy flags; override the host per project
Frontend build envVITE_-prefixedapps/frontend-web build (Vite)baked into the static bundle at build time

A field named FOO on the pydantic Settings class is set with APP_FOO. Some variables (e.g. DATABASE_URL, DEBUG) are read both as an APP_ pydantic field and directly from os.environ elsewhere — set the unprefixed form and, for the pydantic field, the APP_-prefixed form; both are noted below.

Booleans are truthy on 1 / true / yes / on (case-insensitive) unless noted. Empty / unset means off / default.

:::note Authoritative source Defaults and behaviour are verified against the code at time of writing. The code is always the final authority — file paths are given for every variable so you can confirm. :::

Control plane (web server)​

Set via APP_-prefixed env (pydantic Settings). All read in apps/web-server/server/config.py unless noted.

VariableDefaultRequiredPurpose
APP_HOST0.0.0.0noBind address for the web server.
APP_PORT3101noListen port.
APP_DEBUGfalseno (on=debug)Debug mode; also relaxes the APP_DISABLE_AUTH / wildcard-CORS boot guards to warn instead of refuse.
APP_API_TOKENauto-generatednoAdmin bearer token. Auto-generated to ~/.aifactory/.token (0600) on first run if unset. Also read directly as APP_API_TOKEN by intake/handoff clients.
APP_DISABLE_AUTHfalseno (on=bypass)Bypass auth — every request treated as admin. Local dev only; the server refuses to bind a non-loopback host with this on unless APP_DEBUG.
APP_JWT_SECRETauto-generatednoHMAC secret for JWTs. Persisted to ~/.aifactory/.jwt_secret if unset so tokens survive restarts.
APP_JWT_ACCESS_TOKEN_EXPIRE_MINUTES15noAccess-token lifetime.
APP_JWT_REFRESH_TOKEN_EXPIRE_DAYS7noRefresh-token lifetime.
APP_JWT_ALGORITHMHS256noJWT signing algorithm.
APP_SSL_ENABLEDfalsenoServe HTTPS; generates a self-signed cert if no paths given.
APP_SSL_CERTFILE""noPath to SSL certificate (with APP_SSL_ENABLED).
APP_SSL_KEYFILE""noPath to SSL private key (with APP_SSL_ENABLED).
DATABASE_URL / APP_DATABASE_URLsqlite+aiosqlite:///<data>/data.dbnoPrimary database URL. Set Postgres here for multi-replica. Read directly in server/database/engine.py, database/alembic/env.py, services/tenant_teardown.py, services/job_state_store.py, crypto/__main__.py.
APP_MIGRATIONS_AUTO_APPLYtruenoOn boot run alembic upgrade head. Set false in K8s where a Helm Job migrates out-of-band.
APP_PROJECTS_DATA_DIR / APP_PROJECTS_DATA_DIRdata dirnoDirectory for project metadata + SQLite. Also read directly by routes/copilot_mcp.py.
APP_BACKEND_PATH../backendnoPath to apps/backend. Also the build-Job backend path (build_backend.py).
APP_CORS_ORIGINSlocalhost:3000/3100/3101noAllowed CORS origins (comma-list or JSON array). A wildcard * disables credentialed CORS (refuses boot unless APP_DEBUG).
APP_DEFAULT_SHELL/bin/bashnoShell for the built-in terminal.
APP_MAX_TERMINALS20noMax concurrent terminal sessions.
APP_MAX_CONCURRENT_TASKS5noGlobal cap on concurrently running builds (RFC-0016 admission control); <= 0 = unlimited.
APP_AGENT_MONITOR_SYNC_INTERVAL3.0noSeconds between agent-monitor poll/sync ticks.
APP_AGENT_MAX_CONTINUATION_ROUNDS10noMax auto-continue rounds for a finished-but-incomplete build.
APP_REDIS_URL""noRedis for cross-replica WebSocket fan-out. Empty = in-process only (single replica).
APP_REDIS_CHANNELaifactory:eventsnoRedis pub/sub channel (override for shared Redis).
APP_WORKSPACE_S3_URI_BASE""nofsspec URI base (s3://, gs://, azure://) enabling workspace snapshot/restore. Empty = local PVC only.
APP_CFACTORY_SEARCH_URLhttp://cfactory.factory.svc.cluster.local:3111noCockpit base for federated command-palette (Cmd-K) search proxy (#149).
APP_CFACTORY_READ_KEY""noRead-scoped cockpit key for federated search. Empty = feature off.
APP_SKILLS_PATH(built-in)noOverride path to the agent skills directory. Read in services/skills_service.py.
APP_FILE_BROWSE_ROOTS""noExtra os.pathsep-separated roots the file browser may serve. Read in routes/files.py.
APP_PROJECTS_DATA_DIR(data dir)noCopilot-MCP project data root fallback (routes/copilot_mcp.py).

Authentication & provider credentials​

AIFactory is OAuth-only by default for Claude: any ANTHROPIC_API_KEY in the environment is scrubbed from agents to prevent silent API billing. Read in apps/backend/core/auth.py, core/client.py, services/claude_token_pool.py.

VariableDefaultRequiredPurpose
CLAUDE_CODE_OAUTH_TOKEN(none)yes (Claude)Claude Code OAuth token — the primary credential. Set via claude setup-token, the portal OAuth flow, or explicitly. Without it a build dies No OAuth token found.
CLAUDE_CODE_OAUTH_TOKEN_POOL(none)noNewline/comma list of OAuth tokens for rotation/failover across replicas (claude_token_pool.py).
ANTHROPIC_AUTH_TOKEN(none)conditionalCCR/proxy bearer token for enterprise gateway setups.
ANTHROPIC_BASE_URLAnthropic APInoOverride the Anthropic endpoint (local proxy / gateway / self-hosted). Read in cli/utils.py, core/auth.py.
AIFACTORY_ALLOW_API_KEYoffnoOpt into direct ANTHROPIC_API_KEY auth (accepts the direct-billing path). Default OAuth-only.
AIFACTORY_HEADLESS_PREFER_API_KEYoffnoIn headless mode prefer an API key over OAuth when both present.
ANTHROPIC_API_KEY(none)conditionalDirect Anthropic key. Only honoured with AIFACTORY_ALLOW_API_KEY; also used by the Batch API helper (core/batch.py) and Graphiti. Otherwise scrubbed from agents.
ANTHROPIC_MODEL(SDK default)noModel-name override passed through to the agent SDK.
ANTHROPIC_DEFAULT_HAIKU_MODEL(SDK default)noOverride the concrete model the haiku alias resolves to (phase_config.py).
ANTHROPIC_DEFAULT_SONNET_MODEL(SDK default)noOverride the sonnet alias model ID.
ANTHROPIC_DEFAULT_OPUS_MODEL(SDK default)noOverride the opus / opus-1m alias model ID.
NO_PROXY(none)noStandard no-proxy list; SDK passthrough (usually with ANTHROPIC_BASE_URL).
DISABLE_TELEMETRY(none)noDisable Claude SDK telemetry; SDK passthrough.
DISABLE_COST_WARNINGS(none)noSuppress SDK cost warnings; SDK passthrough.
API_TIMEOUT_MS(SDK default)noAgent SDK request timeout (ms); SDK passthrough.

Non-Claude provider credentials​

Resolved by phase routing / byo_llm and forwarded to build Jobs only when present. Read mainly in apps/backend/phase_config.py, providers/*, integrations/graphiti/config.py.

VariableDefaultRequiredPurpose
OPENAI_API_KEY(none)conditionalOpenAI key (QA/planner routing, Graphiti, changelog fallback).
OPENAI_MODELgpt-5-mini (Graphiti)noOpenAI model name (Graphiti LLM / OpenAI-compatible).
OPENAI_EMBEDDING_MODELtext-embedding-3-smallnoOpenAI embedding model (Graphiti).
OPENAI_COMPATIBLE_API_KEY(none)conditionalKey for a generic OpenAI-compatible endpoint.
OPENAI_COMPATIBLE_BASE_URL(none)conditionalBase URL for the OpenAI-compatible endpoint.
OPENAI_COMPATIBLE_MAX_TOKENS(provider default)noMax-tokens override for the OpenAI-compatible agent (providers/openai_compatible_agentic.py).
OPENAI_COMPATIBLE_REASONING_EFFORT(provider default)noReasoning-effort override for the OpenAI-compatible agent.
AZURE_OPENAI_API_KEY(none)conditionalAzure OpenAI key (Graphiti).
AZURE_OPENAI_BASE_URL(none)conditionalAzure OpenAI endpoint.
AZURE_OPENAI_LLM_DEPLOYMENT""noAzure OpenAI LLM deployment name.
AZURE_OPENAI_EMBEDDING_DEPLOYMENT""noAzure OpenAI embedding deployment name.
GEMINI_API_KEY(none)conditionalGoogle Gemini key (phase routing, CLI accounts).
GEMINI_CLI_TRUST_WORKSPACE(none)noGemini CLI workspace-trust passthrough for build Jobs.
GOOGLE_API_KEY(none)conditionalGoogle AI key (Graphiti, phase routing).
GOOGLE_LLM_MODELgemini-2.0-flashnoGoogle LLM model (Graphiti).
GOOGLE_EMBEDDING_MODELtext-embedding-004noGoogle embedding model (Graphiti).
OPENROUTER_API_KEY(none)conditionalOpenRouter aggregator key (Graphiti).
OPENROUTER_BASE_URLhttps://openrouter.ai/api/v1noOpenRouter base URL.
OPENROUTER_LLM_MODELanthropic/claude-3.5-sonnetnoOpenRouter LLM model.
OPENROUTER_EMBEDDING_MODELopenai/text-embedding-3-smallnoOpenRouter embedding model.
VOYAGE_API_KEY(none)conditionalVoyage AI embeddings key (Graphiti).
VOYAGE_EMBEDDING_MODELvoyage-3noVoyage embedding model.
OLLAMA_BASE_URLhttp://localhost:11434noOllama server URL (local/offline models).
OLLAMA_API_URL(fallback of OLLAMA_BASE_URL)noAlternate Ollama URL var (pfactory/tiers.py).
OLLAMA_API_KEY(none)noOllama Cloud key; build-Job passthrough.
OLLAMA_CLOUD_BASE_URL(none)noOllama Cloud base URL; build-Job passthrough.
OLLAMA_LLM_MODEL""conditionalOllama LLM model (required for Ollama LLM).
OLLAMA_EMBEDDING_MODEL""conditionalOllama embedding model (required for Ollama embeddings).
OLLAMA_EMBEDDING_DIM0conditionalOllama embedding dimension; must match the model.
OPENCODE_DEFAULT_MODEL""conditionalModel for the OpenCode agentic provider (providers/opencode_agentic.py).
GH_TOKEN / GITHUB_TOKEN(none)conditionalGitHub token for PR endgame, workspace fetch, testing (providers/factory.py, runners/github/runner.py). Required to open/merge PRs. With the GitHub App on, the server sets both itself and refuses to start if a PAT is present at boot.
AIFACTORY_GITHUB_APP_ID(none)noGitHub App ID (#1671). All three AIFACTORY_GITHUB_APP_* vars or none.
AIFACTORY_GITHUB_APP_INSTALLATION_ID(none)noInstallation ID of the App on the target repos.
AIFACTORY_GITHUB_APP_PRIVATE_KEY(none)noApp PEM private key. Secret only; popped from the environment at boot.
GITHUB_BOT_TOKEN(none)noSeparate bot identity for GitHub actions (runners/github/runner.py).
GITHUB_REPO(none)conditionalTarget repo for the GitHub runner CLI.

Recognized / scrubbed credentials​

These are recognized as secrets and blanked from the agent environment (core/auth.py::_AGENT_ENV_DENY_EXACT) so a prompt-injected build can't exfiltrate them. Set them on the control plane only if a component genuinely needs them (cloud/KMS SDKs); they never reach agents.

VariablePurpose
GROQ_API_KEY, TOGETHER_API_KEY, MISTRAL_API_KEY, DEEPSEEK_API_KEY, LINEAR_API_KEYAdditional provider/integration keys; recognized and scrubbed.
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKENAWS credentials (boto3 / workspace store / deploy); scrubbed from agents.
GOOGLE_APPLICATION_CREDENTIALSGCP service-account path; scrubbed from agents.
AZURE_CLIENT_SECRETAzure app secret; scrubbed from agents.
APP_CFACTORY_READ_KEY, CONTEXT7_KEY, RAPIDAPI_KEY, LANGCHAIN_API_KEY, OPENAI_COMPATIBLE_API_KEY, S3_ACCESS_KEYHost keys scrubbed from agents (#1674). OPENAI_COMPATIBLE_API_KEY is restored for the runner via RUNNER_KEEP; S3_ACCESS_KEY is denied to agents and the in-pod runner (kubejob Jobs still receive both for run.py).

Any host var whose name matches SECRET|PASSWORD|PRIVATE_KEY|CREDENTIAL|_KMS|PASSPHRASE is also blanked generically.

LiteLLM gateway & audit​

Read in providers/_gateway.py, phase_config.py, core/enforcement.py, services/litellm_admin_client.py, services/llm_audit_hook.py, providers/openai_compatible.py.

VariableDefaultRequiredPurpose
LITELLM_GATEWAY_URL""conditionalRoute model calls through a LiteLLM gateway. Empty = direct providers.
LITELLM_API_KEY""conditionalAuth key for the LiteLLM gateway.
LITELLM_MASTER_KEY""conditionalMaster key for LiteLLM admin operations.
LITELLM_MASTER_KEY_WRAPPED""noKMS-wrapped master key (base64) unwrapped at runtime.
LITELLM_AUDIT_SCRUB_OUTBOUNDonnoScrub high-precision PII (SSN, email, phone, Luhn-checked CC) from the prompt BEFORE it is sent to the LLM provider (#320). On by default; kill-switch — set false/0/no/off to disable and restore audit-row-only redaction.
LITELLM_AUDIT_EXTRA_PATTERNS""noExtra comma-separated regex patterns for the audit scrubber.

Model, agent & routing tuning​

Read across apps/backend/ (routing_policy.py, core/model_config.py, agents/*, prompts_pkg/prompts.py, spec/pipeline/agent_runner.py, solo_mode.py, integrations/bmad/session_config.py, core/enforcement.py).

VariableDefaultRequiredPurpose
AIFACTORY_ROUTING_POLICY""noNamed per-phase model routing policy (routing_policy.py).
AIFACTORY_RUNTIMES"" (only claude)no (spend)RFC-0014 operator allowlist of non-default runtimes — comma-separated (codex, antigravity, ollama, ollama-cloud), or all. claude is always on; the fan-out runtimes claude-subagents / dynamic-workflow multiply spend and must be named explicitly (never implied by all). A runtime also needs the contract's execution.runtime opt-in (core/runtime_gating.py).
AIFACTORY_PROVIDER_FAILOVER(built-in chain)noComma-separated provider failover order tried when the primary provider errors (core/provider_failover.py). Empty = default chain.
AIFACTORY_PROVIDER_DEADLINE_S900noTotal wall-clock budget (seconds) for a phase across all failover attempts (core/provider_failover.py).
AUTO_BUILD_MODEL(built-in)noDefault model for CLI auto-builds (cli/main.py).
UTILITY_MODEL_ID(built-in)noModel for small utility completions (core/model_config.py).
UTILITY_THINKING_BUDGET""noThinking-token budget for the utility model.
QA_LLM_PROVIDER""noForce the QA phase to a specific provider (phase_config.py).
INSIGHT_EXTRACTION_ENABLEDtrueno (off disables)Enable session-insight extraction (analysis/insight_extractor.py).
INSIGHT_EXTRACTOR_MODEL(built-in)noModel used for insight extraction.
AIFACTORY_SOLO_MODEoffnoSingle self-directed agent for small jobs (token-saving).
QUICK_MODEoffnoShorter prompts / quick path (prompts_pkg/prompts.py, spec/pipeline/agent_runner.py).
BMAD_SESSION_SEGMENTATIONoffnoPer-story session segmentation for large tasks.
USE_CLAUDE_MDoffnoInject the repo CLAUDE.md into the agent context (core/client.py).
DELEGATE_BY_DEFAULToffnoDelegate the coder phase by default (per-project .aifactory/.env).
AIFACTORY_AGENT_STALL_TIMEOUT600noSeconds before an agent with no output is considered stalled.
AIFACTORY_FIRST_TOKEN_TIMEOUT120noSeconds to wait for the agent's first token (agents/session.py).
AIFACTORY_MAX_RATE_LIMIT_WAIT_SECONDS3300noCap on backoff waiting out a provider rate limit (agents/base.py).
AIFACTORY_GUARDRAIL_REPEAT_FAIL5noRepeated-failure count that trips the guardrail (agents/guardrails.py).
AIFACTORY_GUARDRAIL_TOOL_FAIL_HALT8noTool-failure count that halts the agent.
AIFACTORY_GUARDRAIL_NOPROGRESS5noNo-progress count that trips the guardrail.
AIFACTORY_ACT_GUARDRAILoffnoEnable the act-loop guardrail hook (agents/act_loop_hooks.py).
AIFACTORY_CONTEXT_SUMMARYoffnoEnable rolling context summarization (agents/context_summary.py).
AIFACTORY_MUTATION_LEDGERoffnoRecord agent file mutations to a ledger (agents/mutation_ledger.py).
AIFACTORY_TEST_EVIDENCE_GATEon (set to off/0/false/no to disable)noHonesty gate (agents/test_evidence.py). Two checks, both at update_subtask_status(... "completed"): (1) #851 — a test/verification subtask needs a real, non-failing test command recorded this build; (2) #1111 — a subtask naming an HTTP path needs at least one Python test mentioning that path which reaches the shipped app (imports an application entrypoint without building its own, or calls a live server), so a suite asserting against a FastAPI() built inside the test file no longer counts. Inert when the subtask names no HTTP path or no Python test mentions it. (3) #1123 — one post-merge gate (agents/route_wiring.py), run once at the trailing-gate step when every wave has merged, that imports the application entrypoint the repo's own tests name and asserts every HTTP path the plan promised resolves on the real app. No gate at all when the plan promises no HTTP path or no test names an entrypoint; reported skipped (never passed) when the app cannot be imported.
AIFACTORY_SELF_HEALoffnoOff in all deployments today — merges are not security-scanned. When on, the pre-merge gate blocks a merge with a high-severity finding or one that could not be scanned (scanner failure, unreadable diff), and the flag also enables review-tier gating and plan-artifact emission (agents/self_heal_integration.py, core/worktree.py, #1454).
AIFACTORY_CLAUDE_ENFORCEMENT_ENABLEDoffnoEnable per-org Claude enforcement (core/enforcement.py).
AIFACTORY_CLAUDE_ENFORCEMENT_FAILURE_MODEopennoopen (fail-open) or closed (fail-closed) when enforcement can't decide.
AIFACTORY_GRAPHIFY_ENABLEDoffnoOpt into the Graphify code-graph MCP tool for the coder (core/client.py); forwarded to build Jobs.
AIFACTORY_TEST_AGENT_CMD""noOverride the agent command (test hook, services/agent_service.py).
AIFACTORY_GIT_LOCK_TIMEOUT120noSeconds to wait for the per-repo git lock (core/worktree.py).
DEFAULT_BRANCHauto-detectnoBase branch for worktree creation (core/worktree.py, cli/workspace_commands.py).
PROJECT_WORKSPACE_ROOT(data dir)noRoot for project workspaces (services/project_workspace_service.py).
CIruntime-detectednotrue selects non-interactive / CI reviewer behaviour (cli/spec_commands.py, review/reviewer.py). Also set in the fixed build-Job env.
ENABLE_FANCY_UItruenoRich terminal UI vs plain text (ui/capabilities.py).
FACTORY_SERVICE_NAMEthe FactorynoService name string in GitLab-provider messages.
EMAIL_OAUTH_REDIRECT_URI(derived)noOverride the email-integration OAuth redirect (routes/email.py).

Build backend & job-native scheduling​

How run.py executes and what lets a build schedule on any node. Read in apps/web-server/server/services/build_backend.py, core/nix_env.py, core/workspace_fetch.py, agents/gate_runner.py.

VariableDefaultRequiredPurpose
AIFACTORY_BUILD_BACKENDsubprocessnosubprocess (in-pod asyncio) or kubejob (each build as its own k8s Job). Unknown values fall back to subprocess. Auto-merge is held for every task unless this is kubejob and DATABASE_URL is set.
AIFACTORY_IMAGE(built-in)noRunning image; fallback build-Job image when AIFACTORY_BUILD_IMAGE unset.
AIFACTORY_BUILD_IMAGE(unset)noOverride the image for the kubejob build Job only. Point at a :sha-<short>-nix tag for in-image Nix.
AIFACTORY_PACK_WORKSPACEoffnoPack /work to object storage and unpack in the Job (removes the workspace node-pin).
AIFACTORY_PACKED_NIX_IN_IMAGEoffnoGate Jobs resolve /nix from the image instead of the Nix-store PVC (removes the last node-pin). Forwarded into the build Job (#1736).
WORKSPACE_URI""conditionalObject-storage URI the Job unpacks /work from (set by the pack path; core/workspace_fetch.py).
AIFACTORY_DATA_ROOT/home/nonroot/.aifactorynoData PVC mount root; used to compute worktree subPaths and sandbox mounts.
AIFACTORY_SANDBOX_NAMESPACEfactorynoK8s namespace the build Job runs in.
AIFACTORY_BUILD_SAaifactory-sandboxnoServiceAccount for the build Job.
AIFACTORY_BUILD_DEADLINE_SECONDS21600 (6h)noActive-deadline for the build Job.
AIFACTORY_SANDBOX_REPO_PVCaifactory-datanoPVC holding the repo/worktree (co-mounted into the Job).
AIFACTORY_NIX_STORE_PVC""noWarm Nix-store PVC that gate Jobs mount (seeded from the image). Forwarded into the build Job; the build Job itself never mounts it (#1736).
AIFACTORY_CLI_CREDS_SECRET""noK8s Secret name holding CLI credentials mounted into the build Job.
APP_BACKEND_PATH/home/projects/MagesticAI/apps/backendnoBackend path inside the build Job.

Build-Job env allowlist (_PASSTHROUGH_BUILD_ENV)​

A kubejob build runs in a fresh pod. build_job_env() seeds it with a fixed non-interactive env plus a passthrough allowlist — control-plane vars are forwarded only when present, in env (never argv). Anything not on the allowlist (notably ANTHROPIC_API_KEY and control-plane secrets) never reaches the Job.

Fixed env always set: PYTHONUNBUFFERED=1, PYTHONIOENCODING=utf-8, CLAUDE_CODE_ENTRYPOINT=cli, CI=true, plus CLAUDE_CODE_OAUTH_TOKEN (the pooled token).

Forwarded when present: ANTHROPIC_BASE_URL, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_MODEL, ANTHROPIC_DEFAULT_HAIKU_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL, ANTHROPIC_DEFAULT_OPUS_MODEL, NO_PROXY, DISABLE_TELEMETRY, DISABLE_COST_WARNINGS, API_TIMEOUT_MS, GITHUB_TOKEN, GH_TOKEN, OPENAI_API_KEY, OPENAI_COMPATIBLE_API_KEY, OPENAI_COMPATIBLE_BASE_URL, GEMINI_API_KEY, GEMINI_CLI_TRUST_WORKSPACE, GOOGLE_API_KEY, OLLAMA_API_KEY, OLLAMA_CLOUD_BASE_URL, S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY, S3_REGION, AIFACTORY_GRAPHIFY_ENABLED.

The S3_* namespace is what the Job-side core/artifact_store reads to reconstitute a packed workspace (distinct from the chart's fsspec AIFACTORY_S3_* / AWS_* WorkspaceStore vars below). All forwarded only when non-empty.

Execution mode & parallelism​

VariableDefaultRequiredPurpose
AIFACTORY_INTAKE_PARALLELoffnoFleet default: unlabelled issue-intake builds run parallel waves. Per-issue labels win.
AIFACTORY_INTAKE_WORKERS(unset)noWorker cap for parallel intake builds; unset uses the coder default (3).

run.py --parallel / --workers N are CLI flags, not env vars. Per-issue labels (factory:parallel, factory:serial, factory:workers=N) are covered in the Configuration Reference.

Intake poller​

Read in apps/web-server/server/services/intake_poller.py, apps/backend/intake/*, routes/from_issue.py.

VariableDefaultRequiredPurpose
AIFACTORY_INTAKE_POLLERoffnoEnable the background issue-intake poller.
AIFACTORY_INTAKE_REPOS""conditionalComma-separated repos the poller watches.
AIFACTORY_INTAKE_INTERVAL_S30noPoll interval (min 5s).
AIFACTORY_INTAKE_REQUEUE_AFTER_S600noRequeue a stuck in-flight issue after N seconds (min 60).
AIFACTORY_INTAKE_DB(data dir)noOverride the processed-issue store path (intake/processed_store.py).
AIFACTORY_INTAKE_AUTO_HANDOFFoffnoAuto-hand off to the next stage after an intake build (routes/from_issue.py).
AIFACTORY_TOKEN(none)conditionalAIFactory API token for the poller's own calls (falls back to APP_API_TOKEN).
AIFACTORY_URL""conditionalAIFactory base URL for poller callbacks.
PFACTORY_TOKEN(none)noPFactory token for ingest (falls back to APP_API_TOKEN).
PFACTORY_INGEST_URL""noPFactory ingest endpoint the poller posts to.

PR endgame, deploy & Copilot​

Read in apps/web-server/server/services/pr_endgame.py, services/merger.py, server/main.py (merger sweep startup), services/copilot_dispatch_service.py, routes/github.py. These are commonly set per-project in .aifactory/.env.

VariableDefaultRequiredPurpose
AIFACTORY_AUTO_PRoffnoAuto-open a PR on a clean build.
AIFACTORY_AUTO_MERGEoffnoAuto-merge after the reviewer approves (requires AIFACTORY_AUTO_PR).
AIFACTORY_PR_REVIEWERaifactorynoWhich review gates merge: aifactory, copilot, or any.
AIFACTORY_AUTO_DEPLOYoffnoDeploy-then-verify to AWS App Runner on a clean build, then tear down.
AIFACTORY_COPILOT_DISPATCH_ENABLEDoffnoEnable GitHub Copilot dispatch for reviews.
AIFACTORY_MERGER_SWEEPoffnoPeriodic merger sweep: sync task status to PR state and, where AIFACTORY_AUTO_PR is on for the project, push and open PRs for finished work with no PR. See guides/pr-endgame.md.
AIFACTORY_MERGER_SWEEP_DRY_RUNtruenoReport-only unless exactly false.
AIFACTORY_MERGER_SWEEP_INTERVAL_S900noSeconds between sweep ticks.

Completion events & outbox​

RFC-0001 PARR spine. Read in apps/web-server/server/services/completion.py, services/outbox.py. Best-effort — a missing target never breaks a build.

VariableDefaultRequiredPurpose
AIFACTORY_COMPLETION_WEBHOOK(none)noURL to POST normalized completion events (cockpit ingress :3111/api/events).
AIFACTORY_COMPLETION_WEBHOOK_TIMEOUT5noTimeout (seconds) for the completion webhook.
AIFACTORY_COMPLETION_SENTINELoffnoAlso write <spec_dir>/COMPLETED.json on completion.
AIFACTORY_COMPLETION_OUTBOXoffnoEnable the durable completion outbox (retry queue).
AIFACTORY_COMPLETION_OUTBOX_DB(data dir)noOverride the outbox DB path.
AIFACTORY_EVENT_SOURCE/aifactorynoCloudEvents source for emitted events.
AIFACTORY_WORKER_PROGRESS_INTERVAL_S(built-in)noInterval for worker progress heartbeat events.

MCP control plane​

Read in apps/web-server/server/mcp_remote/*, routes/copilot_mcp.py, apps/backend/mcp_server/*, agents/tools_pkg/http_client.py, core/mcp_credentials.py, agents/tools_pkg/mcp_catalog.py.

VariableDefaultRequiredPurpose
AIFACTORY_MCP_REMOTE_ENABLEDoffnoExpose the HTTP+SSE MCP server at /api/mcp-remote/sse.
AIFACTORY_MCP_LOOPBACK_URLhttp://localhost:3101noLoopback URL MCP tools call back into.
AIFACTORY_MCP_SECRET""conditionalShared secret for the Copilot MCP endpoint.
AIFACTORY_MCP_KEY""noScoped MCP key (acw_) the agent HTTP client presents.
AIFACTORY_API_URLhttp://localhost:3101noBase URL the agent tools call the web server on.
AIFACTORY_API_TOKEN_FILE~/.aifactory/.tokennoFile the agent tools read the API token from.
AIFACTORY_SPEC_DIR(none)noSpec directory the MCP server serves (mcp_server/aifactory_server.py).
AIFACTORY_PROJECT_DIR(cwd)noProject directory for the MCP server (falls back to CLAUDE_PROJECT_DIR).
GCP_MCP_ENDPOINT(built-in default)noEndpoint for the GCP MCP catalog tool.
GRAPHITI_MCP_URL(none)conditionalEnables the Graphiti MCP memory tool when set (agents/tools_pkg/models.py, core/client.py).

Live Console (rmux)​

Read in apps/web-server/server/rmux/*.

VariableDefaultRequiredPurpose
AIFACTORY_RMUX_ENABLED / APP_RMUX_ENABLEDoffnoEnable the streaming rmux Live Agent Console.
AIFACTORY_RMUX_PANES_DIR(data dir)noWritable directory for rmux FIFO panes.

Security & sandbox​

Read in apps/web-server/server/services/sandbox.py, apps/backend/agents/gate_runner.py, agents/coder.py, core/client.py, runners/ai_analyzer/claude_client.py, project/models.py, security/egress.py.

VariableDefaultRequiredPurpose
AIFACTORY_BASH_SANDBOXonnoGate the bubblewrap syscall sandbox; set off where bwrap can't mount /proc.
AIFACTORY_AGENT_SANDBOXoffnoOS-level agent sandbox mode (services/sandbox.py).
AIFACTORY_AGENT_SANDBOX_PIDNSoffnoOpt into a private PID namespace for the sandbox. It does not make a host count as isolated for auto-merge while run.py in the sandbox carries the server's environment (#1680).
AIFACTORY_SANDBOX_BACKENDdockernoGate-runner sandbox backend (docker / nixjob).
AIFACTORY_SANDBOX_GATESoffnoRun agent gates inside the sandbox.
AIFACTORY_SANDBOX_IMAGE""noImage for sandboxed gate runs.
AIFACTORY_SANDBOX_NETWORKnonenoNetwork mode for the sandbox.
AIFACTORY_SANDBOX_REPO_PVCaifactory-datanoRepo PVC for sandboxed gate runs.
AIFACTORY_REPO_LINT_GATEoffnoRun the target repo's CI linters (ruff, flake8, ktlint) as blocking trailing gates and name them in the planner prompt (#1742).
AIFACTORY_EXTRA_ALLOWED_COMMANDS""noExtend the dynamic command allowlist (project/models.py).
AIFACTORY_EGRESS_POLICYoffnoAgent egress policy mode (security/egress.py).
AIFACTORY_EGRESS_ALLOWED_HOSTS""noAllowed egress hosts when the egress policy is on.
AIFACTORY_INJECTION_SCANonno (fail-closed)Prompt-injection content-scan gate mode: on / warn / off. Unrecognised values resolve to on so a typo can't silently disable it (security/content_scan.py).
AIFACTORY_AUTH_PREFLIGHTwarnnoProvider-credential pre-flight check before a run: off / warn / enforce (core/auth_preflight.py).
METRICS_SCRAPE_TOKEN""noBearer token gating the /metrics scrape endpoint (observability/metrics.py).

Identity providers (OIDC / SAML / SCIM / email OAuth)​

Read in apps/web-server/server/oidc/*, saml/*, scim/*, identity_providers.py, routes/oidc_routes.py, _get_email_oauth_credentials.py.

OIDC​

VariableDefaultRequiredPurpose
APP_OIDC_ENABLEDoffnoEnable OIDC login.
APP_OIDC_ISSUER_URL(none)conditionalOIDC issuer URL (required when enabled).
APP_OIDC_CLIENT_ID(none)conditionalOIDC client ID.
APP_OIDC_CLIENT_SECRET(none)conditionalOIDC client secret.
APP_OIDC_PROVIDERkeycloaknoProvider preset (oidc/presets.py).
APP_OIDC_SCOPEpreset defaultnoOIDC scopes.
APP_OIDC_REDIRECT_URI(derived)noOverride the OIDC redirect URI.
APP_OIDC_POST_LOGIN_REDIRECT/noPost-login redirect path.
APP_OIDC_POST_LOGOUT_REDIRECT/noPost-logout redirect path.
APP_OIDC_DEFAULT_ROLEmembernoRole assigned to new OIDC users.
APP_OIDC_GROUP_TO_ROLE""noJSON map of IdP groups to roles.
APP_OIDC_DEFAULT_ORG_SLUGdefaultnoDefault org slug for provisioned users.
APP_OIDC_DEFAULT_ORG_NAMEDefault OrganizationnoDefault org display name.
APP_OIDC_USERINFO_CACHE_TTL_S(built-in)noTTL for cached OIDC userinfo.
OIDC_DISPLAY_NAMEOIDC (default)noLogin-button label (identity_providers.py).

SAML​

VariableDefaultRequiredPurpose
SAML_ENABLEDoffnoEnable SAML SSO.
SAML_IDP_NAMEsamlnoInternal IdP identifier.
SAML_IDP_DISPLAY_NAME(= name)noLogin-button label.
SAML_SP_ENTITY_ID""conditionalService-provider entity ID.
SAML_ACS_URL""conditionalAssertion Consumer Service URL.
SAML_IDP_METADATA_URL(none)conditionalIdP metadata URL (or use the file form).
SAML_IDP_METADATA_FILE(none)conditionalPath to IdP metadata XML.
SAML_SP_CERT_FILE(none)noSP certificate file path.
SAML_SP_KEY_FILE(none)noSP private-key file path.
SAML_SP_CERT_PREVIOUS_FILE(none)noPrevious SP cert (rotation).
SAML_REQUIRE_ENCRYPTED_ASSERTIONoffnoRequire encrypted assertions.
SAML_IDP_INIT_DEFAULT_RETURN_TO""noDefault return-to for IdP-initiated login.
SAML_SLO_ENABLEDoffnoEnable Single Logout.
SAML_SLO_URL(none)noSP SLO endpoint.
SAML_IDP_SLO_URL(none)noIdP SLO endpoint.

SCIM & email OAuth​

VariableDefaultRequiredPurpose
SCIM_ENABLEDoffnoEnable the SCIM provisioning endpoint (main.py).
SCIM_BEARER_TOKEN""conditionalBearer token gating SCIM (scim/auth.py).
APP_EMAIL_GOOGLE_CLIENT_ID(none)conditionalGoogle email-integration OAuth client ID.
APP_EMAIL_GOOGLE_CLIENT_SECRET(none)conditionalGoogle email-integration OAuth secret.
APP_EMAIL_MICROSOFT_CLIENT_ID(none)conditionalMicrosoft email-integration OAuth client ID.
APP_EMAIL_MICROSOFT_CLIENT_SECRET(none)conditionalMicrosoft email-integration OAuth secret.

Multi-tenant, crypto (KMS) & audit anchor​

Read in apps/web-server/server/tenancy.py, services/tenant_reconciler.py, services/tenant_teardown.py, crypto/kms/*, services/audit_anchor.py, jobs/audit_anchor_cron.py, services/vault_client.py, crypto/kms/vault.py.

VariableDefaultRequiredPurpose
AIFACTORY_MULTI_TENANToffnoEnable multi-tenant isolation (tenancy.py).
TENANT_ISOLATION_ENABLEDoffnoGate tenant-teardown isolation logic.
TENANT_NAMESPACE_PREFIXaifactory-tenantnoNamespace prefix for per-tenant namespaces.
TENANT_DELETION_GRACE_DAYS30noGrace period before tenant deletion.
TENANT_TEARDOWN_DRY_RUN_HOURS24noDry-run window before teardown executes.
TENANT_CLOUD_BACKENDawsnoCloud backend for tenant reconciliation.
TENANT_CNI_BACKENDautonoCNI backend for tenant network policy.
TENANT_S3_BUCKET""noS3 bucket for per-tenant prefixes (empty = skip).
TENANT_OIDC_PROVIDER_ARN""noIRSA OIDC provider ARN (empty = skip IAM role).
TENANT_ALLOWED_FQDNSapi.anthropic.comnoAllowed-FQDN egress list for tenants.
TENANT_QUOTA_PODS50noPer-tenant pod quota.
TENANT_QUOTA_PVCS10noPer-tenant PVC quota.
TENANT_LIMIT_CPU500mnoDefault per-tenant CPU limit.
TENANT_LIMIT_MEMORY512MinoDefault per-tenant memory limit.
KMS_BACKEND / APP_KMS_BACKENDfernetnoEnvelope-encryption backend: fernet, aws, gcp, azure, vault.
KMS_FERNET_KEY / APP_KMS_FERNET_KEY(none)conditionalFernet key for the fernet KMS backend.
AWS_KMS_KEY_ID(none)conditionalKMS key ID (AWS backend).
AWS_REGION(none)noAWS region for KMS/services.
AWS_ENDPOINT_URL(none)noCustom AWS endpoint (e.g. LocalStack).
GCP_KMS_KEY_NAME(none)conditionalKMS key name (GCP backend).
AZURE_KEYVAULT_URL(none)conditionalKey Vault URL (Azure backend).
AZURE_KEYVAULT_KEY(none)conditionalKey Vault key name (Azure backend).
VAULT_ADDR(none)conditionalHashiCorp Vault address (Vault backend / vault client).
VAULT_TOKEN(none)conditionalVault token.
VAULT_NAMESPACE(none)noVault namespace (Enterprise).
VAULT_TRANSIT_KEY(default)noVault transit key name.
VAULT_TRANSIT_MOUNT(default)noVault transit mount point.
AUDIT_ANCHOR_PER_TENANTfalsenoAnchor the audit chain per tenant.
AUDIT_ANCHOR_BATCH_SIZE100noRows per anchor batch.
AUDIT_ANCHOR_KEY_CACHE_SIZE1000noPer-tenant anchor-key cache size.
AUDIT_ANCHOR_GENESISGENESISnoGenesis sentinel for an empty audit chain.

Graphiti memory​

Enable/disable and provider selection for the persistent memory layer. Read in apps/backend/integrations/graphiti/config.py, query_memory.py. Provider credential/model vars (OPENAI_*, AZURE_OPENAI_*, VOYAGE_*, GOOGLE_*, OPENROUTER_*, OLLAMA_*, ANTHROPIC_API_KEY, GRAPHITI_ANTHROPIC_MODEL) are listed under provider credentials.

VariableDefaultRequiredPurpose
GRAPHITI_ENABLEDtrueno (off disables)Enable the Graphiti memory layer.
GRAPHITI_LLM_PROVIDERopenainoLLM provider: openai/anthropic/azure_openai/ollama/google/openrouter.
GRAPHITI_EMBEDDER_PROVIDERopenainoEmbedder provider: openai/voyage/azure_openai/ollama/google/openrouter.
GRAPHITI_DATABASEmagestic_ai_memorynoLadybugDB database name.
GRAPHITI_DB_PATH~/.magestic-ai/memoriesnoLadybugDB storage path.
GRAPHITI_ANTHROPIC_MODELclaude-sonnet-4-5noAnthropic model when Graphiti LLM = anthropic.

Observability & tracing​

Read in apps/web-server/server/observability/*, apps/backend/core/job_tracing.py.

VariableDefaultRequiredPurpose
OTEL_EXPORTER_OTLP_ENDPOINT""noOTLP endpoint; set enables OpenTelemetry export.
OTEL_EXPORTER_OTLP_PROTOCOLgrpcnoOTLP protocol (grpc/http).
OTEL_SERVICE_NAMEaifactory-webnoService name for web-server traces/metrics.
OTEL_SERVICE_NAME_AGENTaifactory-agentnoService name for agent traces.

TFactory / PFactory handoff​

Read in apps/backend/pfactory/tfactory_client.py, pfactory/tiers.py, services/agent_service.py. (Poller-side PFACTORY_* / AIFACTORY_URL vars are under Intake poller.)

VariableDefaultRequiredPurpose
TFACTORY_BASE_URL""conditionalTFactory base URL; unset disables the handoff (a local marker is still written).
TFACTORY_TOKEN(none)noBearer token for TFactory (falls back to APP_API_TOKEN).
TFACTORY_HANDOFF_PATH/api/handoffnoTFactory handoff endpoint path.
TFACTORY_PROJECT_ID(derived)noTFactory project ID for the handoff (tfactory_client.py).

Workspace object storage (fsspec WorkspaceStore)​

Read in apps/web-server/server/services/workspace_store.py. Distinct from the Job-side S3_* namespace in the build allowlist.

VariableDefaultRequiredPurpose
AIFACTORY_S3_ENDPOINT_URL(none)noCustom S3 endpoint for the fsspec workspace store (MinIO etc.).
AIFACTORY_S3_ADDRESSING_STYLE(provider default)noS3 addressing style (path/virtual).

Batch API (Anthropic Message Batches)​

Read in apps/backend/analysis/insight_extractor.py (helper: core/batch.py).

VariableDefaultRequiredPurpose
AIFACTORY_BATCH_MIN_JOBS2noBelow this many concurrent completions, skip the batch path.
AIFACTORY_BATCH_TIMEOUT120noHard timeout (seconds) for the batch poll loop.
AIFACTORY_BATCH_DISABLEoffnoKill-switch forcing the sequential path.

Frontend build (Vite)​

Baked into the static bundle at build time. Read in apps/frontend-web/src/*.

VariableDefaultRequiredPurpose
VITE_API_URL(same-origin)noBackend base URL for integration settings.
VITE_API_BASE_URL/apinoAPI base path for the API client.
VITE_WS_BASE_URL(derived)noWebSocket base URL.
VITE_DEBUGoffnoEnable verbose frontend debug logging (with DEV).
VITE_PFACTORY_URLhttps://pfactory.freundcloud.org.uknoPlan-portal link in the portal switcher.
VITE_AIFACTORY_URLhttps://aifactory.freundcloud.org.uknoBuild-portal link in the portal switcher.
VITE_TFACTORY_URLhttps://tfactory.freundcloud.org.uknoTest-portal link in the portal switcher.
VITE_CFACTORY_URLhttps://cfactory.freundcloud.org.uknoCockpit link in the portal switcher.

Trusted-plan ingest​

VariableDefaultRequiredPurpose
AIFACTORY_TRUSTED_PLAN_KEY_<AUTHORITY>(none)noHMAC key verifying a signed Task Contract v2 from an upstream authority (e.g. AIFACTORY_TRUSTED_PLAN_KEY_PFACTORY). Legacy single-key entry; matches envelopes with no kid. See Task Contract. Read in apps/backend/trusted_plan.py.
AIFACTORY_TRUSTED_PLAN_KEY_<AUTHORITY>__<KID>(none)noKeyed rotation entry: registers one verification key under authority/kid (kid case-insensitive). Multiple kids can be active at once for zero-downtime rotation. See Trusted-plan Key Rotation.
AIFACTORY_TRUSTED_PLAN_RETIRED_KIDS(none)noComma-separated authority/kid (or bare kid) to revoke; a retired kid is rejected at verify time even if its key material is still configured.
AIFACTORY_BASELINE_DRIFTblast-radiusnoBaseline-freshness policy on the trusted-plan path (#1109). blast-radius rejects with 422 only when a file in baseline.blast_radius.files changed between the approved commit and the target repo's HEAD, and records unrelated drift as a warning; reject rejects any divergence from baseline.commit; warn never rejects but still records the verdict on the spec's provenance; off skips the check. An unrecognised value falls back to blast-radius — a typo must not disable a supply-chain control. Read in apps/backend/trusted_plan.py.

Baseline freshness (#1109)​

User story. As a reviewer who approved a plan against the repo as it stood last Tuesday, I want the build to refuse — or at minimum say so on the record — when the code the plan is about has moved since, so my approval does not silently attach to a tree nobody reviewed.

The signature gate answers did anyone edit the instructions. Baseline freshness answers are the instructions still about this codebase. A contract carries the approved commit twice (provenance.baseline_commit and baseline.commit), both inside the signed bytes, so both are already tamper-evident; this gate is what reads them back.

Verdicts recorded on requirements.json under provenance.baseline_drift:

VerdictMeaningRejected under…
absentThe contract carries no baseline (greenfield, or a v1 plan). Nothing recorded.never
currentHEAD is the approved commit (a short SHA still counts as the same commit).never
unknownHEAD could not be resolved, or the approved commit is not in this clone (e.g. a shallow clone).never
driftedThe tree moved, but no blast-radius file was touched.reject
blast_radius_changedA file the plan intends to touch changed since approval.reject, blast-radius

unknown never rejects, deliberately: a control that cannot see must say so rather than guess. Guessing "current" would hide the very drift the gate exists to catch; guessing "drifted" would turn every shallow clone into an outage. Every non-absent verdict is written to the spec's provenance even when it does not reject — "we proceeded" is a decision, and an auditor should not have to reconstruct it from a log line.

Approved-model registry​

Read in apps/backend/model_registry.py. See Approved-model Registry.

VariableDefaultRequiredPurpose
AIFACTORY_MODEL_REGISTRY_ENFORCEwarnnoRegistry assertion mode: warn (log unregistered/mis-staged model, never block), deny (raise and fail the stage), off (skip). Default is advisory — routing behaviour is unchanged.
AIFACTORY_MODEL_REGISTRY(built-in)noOverride the approved-model allowlist: inline JSON or a path to a JSON file. Invalid/unreadable value fails safe to the built-in DEFAULT_REGISTRY.

Debug helpers​

Read in apps/backend/core/debug.py, ui/status.py, and various runners.

VariableDefaultRequiredPurpose
DEBUG / APP_DEBUGoffnoEnable debug logging across backend + web server.
DEBUG_LEVEL1noDebug verbosity: 1=basic, 2=detailed, 3=verbose.
DEBUG_LOG_FILE(stdout)noWrite debug logs to a file instead of stdout.

Intentionally excluded (incidental)​

These are read by the code but are pure OS / runtime / toolchain noise an operator does not configure for AIFactory. They are listed here so the reconciliation below accounts for every grepped name.

  • OS / shell / terminal: PATH, HOME, PWD, TERM, SHELL, EDITOR, VISUAL, NO_COLOR, FORCE_COLOR, XDG_RUNTIME_DIR, XDG_CACHE_HOME.
  • Python / Node runtime: PYTHONPATH, PYTHONUNBUFFERED, PYTHONIOENCODING, NODE_ENV, DEV (Vite build-mode flag).
  • Injected by the Claude Code CLI / k8s, not set by operators: CLAUDE_CODE_ENTRYPOINT, CLAUDE_PROJECT_DIR, KUBECONFIG, TRACEPARENT (W3C trace-context propagation).
  • Analyzer example strings / test fixtures (not AIFactory config — they are literals inside code that scans other repos, or pytest fixtures): VAR, VAR_NAME, API_KEY, QUOTED, ALREADY_SET, and the bare PORT example in analysis/analyzers/port_detector.py (the real port is APP_PORT).

Completeness​

This reference was reconciled against an exhaustive grep of the codebase (os.environ, os.getenv, getenv(, env.get(, process.env, import.meta.env, every APP_-prefixed pydantic Settings field, the _PASSTHROUGH_BUILD_ENV allowlist, and every indirected _ENV_* constant).

  • 303 unique variable names found in code.
  • 288 documented above (including the _PASSTHROUGH_BUILD_ENV allowlist and recognized/scrubbed credentials).
  • 15 intentionally excluded as incidental (OS/runtime noise, CLI-injected vars, the analyzer PORT example) — a subset of the fuller excluded list above, which also names OS vars read only via env.get/fixtures.
  • 0 unaccounted for.