Configuration

All runtime config is centralized in .env and validated via Pydantic (prax/settings.py). Copy .env-example and fill in your values:

Synced from Prax at 35c345e5 View source ↗

← Security

All runtime config is centralized in .env and validated via Pydantic (prax/settings.py). Copy .env-example and fill in your values:

cp .env-example .env

Key fields:

Variable Purpose Default
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN Twilio console credentials (not needed for TeamWork- or Discord-only setups) None
OPENAI_KEY OpenAI API key (required unless using other provider)
ANTHROPIC_KEY Anthropic API key (Claude provider for the main agent) None
LLM_PROVIDER LLM provider: openai, anthropic, openrouter, google (alias google-vertex), ollama (alias local), vllm — the names prax/agent/llm_factory.py matches; google_vertex with an underscore is not recognised openai
BASE_MODEL Model name for the main agent gpt-5.4-nano
AGENT_NAME Display name for the agent across all channels, greetings, and prompts Prax
PHONE_TO_NAME_MAP JSON: {"+15551234567": "Alice"} — whitelists callers None
PHONE_TO_EMAIL_MAP JSON: {"+15551234567": "alice@example.com"} None
NGROK_URL HTTPS base URL from ngrok — Twilio webhooks + opt-in shares only (course/note pages no longer auto-publish here) None
TEAMWORK_BASE_URL User-facing base URL Prax pastes into chat for course/note links http://localhost:8000
TS_AUTHKEY Tailscale auth key; prefer a non-ephemeral identity with persisted state. Pre-approve if tailnet device approval requires it. Current Compose also requires a nonempty value when this profile is disabled; see below. None
TS_HOSTNAME Tailnet hostname for the sidecar (becomes <hostname>.<tailnet>.ts.net) prax
COMPOSE_PROFILES Set to tailscale to activate the sidecar; without this it’s silently skipped (unset)
WORKSPACE_DIR Path to workspace root ../workspaces (prax/settings.py; the compose file overrides it to ./workspaces inside the container)
Sandbox
SANDBOX_IMAGE Docker image for sandbox prax-sandbox:latest
SANDBOX_TIMEOUT Max sandbox session duration (seconds) 1800
SANDBOX_MAX_CONCURRENT Max simultaneous sandbox sessions 5
SANDBOX_DEFAULT_MODEL Legacy — the coding-session feature it configured was removed in 2026-07 (#142); still defined and forwarded to the sandbox client, governs nothing in Prax openai/gpt-5.4
SANDBOX_MAX_ROUNDS Legacy — same as above 10
SANDBOX_MEM_LIMIT Container memory limit 1g
SANDBOX_CPU_LIMIT Container CPU limit (nanocpus) 2000000000
Fine-Tuning (optional)
FINETUNE_ENABLED Enable self-improving fine-tuning false
VLLM_BASE_URL vLLM server URL http://localhost:8000/v1
LOCAL_MODEL Local model name for vLLM inference Qwen/Qwen3-8B
FINETUNE_BASE_MODEL Unsloth model for QLoRA training unsloth/Qwen3-8B-unsloth-bnb-4bit
FINETUNE_OUTPUT_DIR Directory for LoRA adapters ./adapters
FINETUNE_MAX_STEPS Training steps per run 60
FINETUNE_LEARNING_RATE QLoRA learning rate 2e-4
FINETUNE_LORA_RANK LoRA rank (higher = more capacity) 16
Browser (optional)
BROWSER_HEADLESS Run Chromium in headless mode true
BROWSER_TIMEOUT Default page timeout (ms) 30000
SITES_CREDENTIALS_PATH Path to sites.yaml credentials file None
BROWSER_PROFILE_DIR Directory for persistent browser profiles (cookies/sessions); recommended for x.com/Twitter support None
BROWSER_VNC_ENABLED Enable VNC-based manual login sessions false
BROWSER_VNC_BASE_PORT Base port for VNC servers 5900
Self-Improvement (optional)
SELF_IMPROVE_ENABLED Enable self-modification via staging clone + verify + deploy false
SELF_IMPROVE_REPO_PATH Path to the repo (default: cwd) None
Discord (optional)
DISCORD_BOT_TOKEN Discord bot token from Developer Portal None
DISCORD_ALLOWED_USERS JSON: {"123456789": "Alice"} — maps Discord user IDs to names None
DISCORD_ALLOWED_CHANNELS Comma-separated channel IDs the bot responds in (empty = DMs + all visible) None
DISCORD_TO_PHONE_MAP JSON: {"discord_id": "+phone"} — link Discord to Twilio identity None

Channel Setup

You need at least one messaging channel. You can run multiple simultaneously.

Option A: TeamWork Web UI (Included)

TeamWork is bundled inside the prax container image (the Dockerfile builds it from TEAMWORK_PATH, default ../teamwork; there is no separate teamwork service in docker-compose.yml) and starts with it.

docker compose up --build    # TeamWork is at http://localhost:3000 (container :8000 → host :3000)

(Until 2026-09 this command could not bring prax up at all — the sandbox service’s compose healthcheck probed a server no longer in the image; closed, see the note in docker.md.)

TeamWork provides Slack-like chat channels, a Kanban board, an in-browser terminal, browser screencast, and a file browser. Prax connects to it automatically on startup via the TEAMWORK_URL environment variable.

To link TeamWork conversations with your SMS/Discord identity (shared workspace and memory), set TEAMWORK_USER_PHONE in .env to your phone number.

Variable Default Description
TEAMWORK_URL (empty) — inside the bundled image scripts/watchdog-launch.sh defaults it to http://localhost:8000; make run-local-all passes the same TeamWork API URL. This is the on/off switch: set → Prax connects; empty → TeamWork integration off (TEAMWORK_ENABLED is legacy). Neither compose file sets it.
TEAMWORK_API_KEY (empty) Shared key Prax sends (X-API-Key) to TeamWork’s /api/external — required whenever TEAMWORK_URL is set (TeamWork answers 503 without it; see .env-example). Outbound only: Prax does not check it on the requests TeamWork makes to Prax — that is PRAX_API_KEY’s job.
PRAX_API_KEY (empty) — no inbound check Inbound counterpart: when set, Prax’s own /teamwork/*, /plugins/* and /api/users/* routes require a matching X-API-Key header (or Authorization: Bearer) and answer 401 otherwise (prax/blueprints/inbound_auth.py). Set TeamWork’s PRAX_API_KEY to the same value so its proxy routers send the header. Anything else that calls those routes directly does not yet send it (the k8s operator’s /teamwork/health probe, scripts/smoke_test.py, TeamWork’s MCP Library bridge) — add the header there before turning the key on in such a deployment. Twilio routes keep signature validation, /mcp its bearer, /health and /healthz/* stay open.
TEAMWORK_USER_PHONE (empty) Phone number to share workspace with SMS/Discord

Option B: Discord (Free)

No ngrok, no per-message costs. The bot connects to Discord via WebSocket.

Step 1: Create a Discord Application

  1. Go to the Discord Developer Portal.
  2. Click New Application (top right).
  3. When asked “What brings you to the Developer Portal?”, select Build a Bot.
  4. Give it a name (e.g., “Prax”) and click Create.

Step 2: Configure the Bot

  1. In your application, go to the Bot tab (left sidebar).
  2. Click Reset Token and copy the token. You’ll only see it once — save it now.
  3. Scroll down to Privileged Gateway Intents and enable:
    • Message Content Intent (required — the bot needs to read message text)
  4. Under Authorization Flow, keep Public Bot checked (it just means anyone with the invite link can add it — you control who can actually talk to it via DISCORD_ALLOWED_USERS).

Step 3: Create a Discord Server

If you don’t already have a server to add the bot to:

  1. Open Discord (desktop app or browser).
  2. Click the + button at the bottom of the server list (left sidebar).
  3. Select Create My Own → For me and my friends (or any option).
  4. Name it (e.g., “Prax AI”) and click Create.

Step 4: Invite the Bot to Your Server

  1. Back in the Developer Portal, open your application.
  2. Go to the Installation tab (left sidebar).
  3. Under Installation Contexts, uncheck User Install and keep Guild Install checked.
  4. Under Guild Install → Default Install Settings, click the Scopes dropdown and add bot.
  5. A Permissions dropdown appears — add:
    • Send Messages
    • Read Message History
    • Attach Files
    • Add Reactions
    • Embed Links
    • View Channels
  6. Click Save Changes.
  7. Now go to the OAuth2 tab (left sidebar). Copy the Install Link (or use the URL Generator with bot scope if you prefer).
  8. Open the link in your browser, select your server, and click Authorize.

Step 5: Find Your Discord User ID

You need your Discord user ID (a long number) for the allow list:

  1. Open Discord → User Settings (gear icon) → Advanced → enable Developer Mode.
  2. Close settings, then right-click your own name in any chat → Copy User ID.
  3. It’ll be something like 123456789012345678.

Step 6: Configure .env

# Paste the bot token from Step 2
DISCORD_BOT_TOKEN=MTIz...your_token_here

# Map Discord user IDs to display names (JSON)
DISCORD_ALLOWED_USERS={"123456789012345678": "Alice"}

# Optional: restrict to specific channels (comma-separated channel IDs)
# If empty, the bot responds to DMs and all channels it can see
DISCORD_ALLOWED_CHANNELS=

Step 7: Identity Linking (automatic for single users)

If you have one Discord user and one phone user in your config, they are automatically linked — Discord messages share the same conversation history and workspace as SMS. You’ll see this in the logs:

Auto-linking Discord user 123... → +1555... (single user on both channels).

Multiple users? Set the mapping explicitly:

# Maps Discord user IDs to PERSONAL phone numbers (from PHONE_TO_NAME_MAP).
# This is YOUR number that you text/call FROM — NOT the Twilio ROOT_PHONE_NUMBER.
DISCORD_TO_PHONE_MAP={"123456789012345678": "+15551234567", "987654321098765432": "+15559876543"}

Don’t want linking? Opt out explicitly:

DISCORD_TO_PHONE_MAP=false

Without linking, Discord gets its own separate conversation history and workspace.

Step 8: Start

uv run python app.py

The Discord bot starts automatically if DISCORD_BOT_TOKEN is set. You’ll see Discord bot connected as Prax#1234 in the logs. DM the bot or message in a channel to start chatting.

Discord-only setup: If you don’t want Twilio at all, you can skip TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN entirely. The app works with just Discord.

Option C: Twilio (Voice + SMS)

Requires a Twilio account and ngrok for webhook forwarding — Twilio’s servers must reach Prax’s /transcribe and /sms routes from the public internet, so ngrok is the supported tunnel for this flow. (Course/note publishing and per-file shares no longer ride on this tunnel by default — they’re served by TeamWork on the local network unless the user explicitly opts a specific page into the share registry. See docs/infrastructure/content-publishing.md.)

⚠️ Read before you open the tunnel (Known gaps, 2026-09).

  • ngrok http 5001 publishes every Prax route, not just Twilio’s. An ngrok port tunnel is not path-scoped. Behind it sit routes with no inbound authentication unless PRAX_API_KEY is set — POST /teamwork/webhook (starts an agent turn as the configured TeamWork user), the rest of /teamwork/* (schedules, memory, library delete), /plugins/* (git-clones a repo), /api/users/*. PRAX_HOST=127.0.0.1 does not help: ngrok dials localhost. Do not run this shape on the open internet without both setting PRAX_API_KEY (see Option A: TeamWork Web UI above) and restricting the tunnel to the Twilio paths (a path-filtering rule at the tunnel, or a reverse proxy that forwards only /sms, /transcribe, /respond, /reader, /read, /conference, /say, /play, /shared/) — the key guards those three route groups only, not /execution/*, /courses/, /notes/ or anything else on :5001. The same applies to the in-container ngrok started by scripts/ngrok-launch.sh when NGROK_AUTHTOKEN is set.
  • Signature validation is fail-open and does not work behind the HTTPS tunnel. prax/blueprints/twilio_auth.py skips validation entirely when TWILIO_AUTH_TOKEN is empty (one warning, then every request is accepted — the only remaining gate is the spoofable From allow-list). When the token is set, it signs request.url, and Flask sees http://… for a request that arrived at ngrok over https://… (no ProxyFix / X-Forwarded-Proto handling anywhere in the app), while Twilio signed the https:// URL — so every genuine webhook is rejected with 403. Until the app builds the signed URL from NGROK_URL or trusts the forwarded scheme, the Twilio channel is either unauthenticated or non-functional; there is no working configuration in which it is both. Under the keyless (forward-proxy) setup Prax holds only a placeholder token, which cannot validate either.
  1. Start the Flask server locally (see Running below).
  2. In another terminal, run ngrok against the Flask port (default 5001):
    ngrok http 5001
    
  3. Copy the HTTPS forwarding URL from ngrok output and set NGROK_URL in .env.
  4. In the Twilio console open Phone Numbers → Active Numbers → [your number] → Voice & Fax:
    • Set A Call Comes In to Webhook with URL https://<ngrok-domain>/transcribe using POST.
  5. Under Messaging set A Message Comes In to Webhook with URL https://<ngrok-domain>/sms using POST.

Note: US phone numbers require A2P 10DLC registration for SMS. Consider a toll-free number or use Discord to avoid this entirely.

Remote access (Tailscale sidecar)

To access TeamWork and Grafana over a private HTTPS route, opt into the dockerized Tailscale sidecar. It does not remove the host-published application ports; restrict those separately. The sidecar starts only with the tailscale profile. Current Compose files require TS_AUTHKEY during interpolation even when that profile is disabled: for local use with COMPOSE_PROFILES unset, set TS_AUTHKEY=unused. Replace the placeholder with a real key before enabling Tailscale.

  1. Generate a key at https://login.tailscale.com/admin/settings/keys. Use a non-ephemeral identity and persisted state for this long-running service. A reusable key supports re-enrollment; scope and protect it. Select pre-approval only if device approval is enabled. Ephemeral nodes are designed for short-lived workloads; their minute accounting depends on the plan and lifetime, and nodes present for four hours count as standard tagged devices. See Tailscale’s documentation.
  2. Add to .env:
    TS_AUTHKEY=tskey-auth-...
    TS_HOSTNAME=prax
    COMPOSE_PROFILES=tailscale
    TEAMWORK_BASE_URL=https://prax.<your-tailnet>.ts.net
    
  3. docker compose up -d — the sidecar joins the tailnet automatically. Visit https://prax.<tailnet>.ts.net/ for TeamWork and https://prax.<tailnet>.ts.net:3001/ for Grafana. Note that the compose Grafana has anonymous access enabled at the Admin role (GF_AUTH_ANONYMOUS_ENABLED=true, GF_AUTH_ANONYMOUS_ORG_ROLE=Admin in docker-compose.yml), so every tailnet member who can reach :3001 is a Grafana admin without logging in. Neither TeamWork nor Prax has a browser-session login of its own; the tailnet ACL is the access control.

The checked-in sidecar uses kernel TUN mode with NET_ADMIN and /dev/net/tun, and persists state in a Docker volume so the node identity survives restarts — each restart re-registers as the same device, so it doesn’t churn your tailnet’s device count.

If you already run tailscaled directly on the host and don’t want a sidecar, the original make tailscale-up / make tailscale-down / make tailscale-status Makefile targets still work as a fallback.

Database

By default the SQLite conversation-history database lives under the user’s workspace service-state directory:

workspaces/<PRAX_USER_ID>/.services/prax/conversations.db

To start fresh:

rm -f "$WORKSPACE_DIR/$PRAX_USER_ID/.services/prax/conversations.db"
uv run python -c "from prax.services.state_paths import ensure_conversation_db; ensure_conversation_db()"

Running the App

uv run python app.py

The server listens on 127.0.0.1:5001 by default (PRAX_HOST / PORT in .env; see network-exposure.md before changing the bind address). The scheduler starts automatically and loads any existing schedules.yaml files from user workspaces.

Production / Deployment

  • Process manager: gunicorn is not a dependency of this project. The shipped unit, deploy/systemd/prax.service, runs uv run --python 3.13 python app.py under systemd (Restart=always); use that (or your own supervisor around the same command).
  • Environment: copy .env to the server, point LOG_PATH and WORKSPACE_DIR to persistent volumes. DATABASE_NAME is optional; leaving it as conversations.db stores history under the workspace service-state directory.
  • Docker: see Dockerfile and docker-compose.yml in the repo root. The app container needs /var/run/docker.sock mounted for sandbox functionality.
  • TLS / DNS: for inbound from your own laptops, prefer the Tailscale sidecar (HTTPS via MagicDNS, see Remote access above). For inbound from external services that aren’t on your tailnet (e.g. Twilio webhooks), terminate TLS via ngrok (dev) or a reverse proxy (Nginx/Cloudflare/etc.).