# Handler configuration — copy to .env and fill in. Never commit real secrets. # Database. SQLite fallback (single-node) or Postgres (centralized, default for real deploys). # SQLite: sqlite:////absolute/path/to/handler.db # Postgres: postgresql+psycopg://user:pass@host:5432/handler DATABASE_URL=sqlite:////var/lib/handler/handler.db # Legacy/machine bearer token gating every API route. Human operators now sign in with # email + password (user accounts — first sign-up becomes the admin); this token remains # for scripts/CI and as a break-glass credential. Optional once accounts exist. AUTH_TOKEN=change-me-to-a-long-random-string # ---- User accounts (email + password sign-in for the dashboard/API) ---- # Browser session lifetime and one-shot link validity. Defaults shown. # SESSION_TTL_DAYS=30 # RESET_TOKEN_TTL_HOURS=2 # INVITE_TOKEN_TTL_HOURS=168 # Outbound email for invite + password-reset links. Leave SMTP_HOST unset to run without # email: invite/reset links are then shown to the admin in the dashboard instead of # mailed, and self-serve "forgot password" tells users to ask an admin. # SMTP_HOST=smtp.example.com # SMTP_PORT=587 # SMTP_USERNAME= # SMTP_PASSWORD= # SMTP_FROM=handler@example.com # SMTP_STARTTLS=true # STARTTLS on port 587 (the common setup) # SMTP_SSL=false # implicit TLS on port 465 instead # Base URL the emailed links point at, e.g. https://handler.example.com. Falls back to # each request's own origin when unset (right for the same-origin bundled UI). # PUBLIC_BASE_URL= # Optional higher-trust token gating PUT /shared/context/:key. # Falls back to AUTH_TOKEN if unset. # SHARED_CONTEXT_WRITE_TOKEN= # Optional admin token gating the web control surface: enqueuing control commands # (spawn/kill/resume/approve/reject/forge-init/poll-ci), project CRUD, forge-host CRUD, # and credential-pointer edits. Falls back to AUTH_TOKEN if unset. Give operators this # token in the dashboard to unlock management actions. # ADMIN_TOKEN= # Optional generic webhook target for the Notification hook (ntfy, Pushover, Slack, ...). # Fully bring-your-own; the Notification hook is a no-op when unset. # WEBHOOK_URL=https://ntfy.sh/my-topic # Symmetric key for the encrypted secret store: git-server tokens and SSH private keys # are Fernet-encrypted with it before they reach the database. Set the SAME value on the # API (encrypts on write) and the control container (decrypts at clone/spawn). Generate: # python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # Unset => storing tokens/SSH keys on git servers is refused with a clear error. # HANDLER_SECRET_KEY= # Base directory under which per-project roots and agent worktrees live (isolation). PROJECTS_ROOT=/var/lib/handler/projects # Web search provider for the agents' web_search tool (pi harness). Resolution order: # SearXNG instance -> Brave Search API -> unset = DuckDuckGo HTML fallback (zero-config, # rate-limited). web_fetch needs no provider. # SEARXNG_URL=http://searxng.lan:8080 # BRAVE_SEARCH_API_KEY= # Binary overrides (defaults shown). Point at fakes in tests/CI. # CLAUDE_BIN=claude # PI_BIN=pi # MISE_BIN=mise # TMUX_BIN=tmux # FORGE_BIN=forge # GIT_BIN=git # Phase 2 (forge integration). Pin the forge version your base image installs; spawn # verifies the injected forge matches and warns on drift. Leave unset to skip the check. # FORGE_VERSION=1.2.3 # Branches a direct `git push` may not reach without a standing approval (comma-separated). # Closes the "merge locally, push to main" path around the forge-merge approval gate. # PROTECTED_BRANCHES=main,master # Agent-initiated dispatch (the `dispatch_agent` tool): an agent handing work to a fresh # agent in its own project, so a pipeline advances on a result instead of on a timer. # Both caps bound a confused or looping agent, not a healthy one — a handoff is one call. # MAX_DISPATCH_PER_RUN=3 # MAX_DISPATCH_DEPTH=3 # Phase 3 (web UI). Serve the bundled UI from "/" and "/static". Set false for a # headless, API-only deployment. Applied at process start (restart to change). # UI_ENABLED=true # Extra origins allowed to call the API cross-origin (comma-separated). Only needed if # you host the UI on a DIFFERENT origin than the API; the shipped UI is same-origin and # needs none. Empty => no CORS middleware. # CORS_ORIGINS=https://handler.example.ts.net # Per-project credentials are NOT set here — they live on each project's `credential_ref` # as a POINTER (env:VAR / file:/path / cmd:...), resolved and injected only at spawn. # The database never stores the raw token. Example, when registering a project: # credential_ref = "env:LEEWORKS_TOKEN" (then export LEEWORKS_TOKEN where the control # layer runs; it's injected as FORGE_TOKEN + # the host-specific var, e.g. GITHUB_TOKEN)