Configuration
All runtime config is centralized in .env and validated via Pydantic (prax/settings.py). Copy .env-example and fill in your values:
35c345e5
View source ↗
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
praxup at all — thesandboxservice’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
- Go to the Discord Developer Portal.
- Click New Application (top right).
- When asked “What brings you to the Developer Portal?”, select Build a Bot.
- Give it a name (e.g., “Prax”) and click Create.
Step 2: Configure the Bot
- In your application, go to the Bot tab (left sidebar).
- Click Reset Token and copy the token. You’ll only see it once — save it now.
- Scroll down to Privileged Gateway Intents and enable:
- Message Content Intent (required — the bot needs to read message text)
- 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:
- Open Discord (desktop app or browser).
- Click the + button at the bottom of the server list (left sidebar).
- Select Create My Own → For me and my friends (or any option).
- Name it (e.g., “Prax AI”) and click Create.
Step 4: Invite the Bot to Your Server
- Back in the Developer Portal, open your application.
- Go to the Installation tab (left sidebar).
- Under Installation Contexts, uncheck User Install and keep Guild Install checked.
- Under Guild Install → Default Install Settings, click the Scopes dropdown and add
bot. - A Permissions dropdown appears — add:
- Send Messages
- Read Message History
- Attach Files
- Add Reactions
- Embed Links
- View Channels
- Click Save Changes.
- Now go to the OAuth2 tab (left sidebar). Copy the Install Link (or use the URL Generator with
botscope if you prefer). - 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:
- Open Discord → User Settings (gear icon) → Advanced → enable Developer Mode.
- Close settings, then right-click your own name in any chat → Copy User ID.
- 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_SIDandTWILIO_AUTH_TOKENentirely. 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 5001publishes every Prax route, not just Twilio’s. An ngrok port tunnel is not path-scoped. Behind it sit routes with no inbound authentication unlessPRAX_API_KEYis 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.1does not help: ngrok dials localhost. Do not run this shape on the open internet without both settingPRAX_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 byscripts/ngrok-launch.shwhenNGROK_AUTHTOKENis set.- Signature validation is fail-open and does not work behind the HTTPS tunnel.
prax/blueprints/twilio_auth.pyskips validation entirely whenTWILIO_AUTH_TOKENis empty (one warning, then every request is accepted — the only remaining gate is the spoofableFromallow-list). When the token is set, it signsrequest.url, and Flask seeshttp://…for a request that arrived at ngrok overhttps://…(noProxyFix/X-Forwarded-Protohandling anywhere in the app), while Twilio signed thehttps://URL — so every genuine webhook is rejected with 403. Until the app builds the signed URL fromNGROK_URLor 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.
- Start the Flask server locally (see Running below).
- In another terminal, run ngrok against the Flask port (default 5001):
ngrok http 5001 - Copy the HTTPS forwarding URL from ngrok output and set
NGROK_URLin.env. - In the Twilio console open Phone Numbers → Active Numbers → [your number] → Voice & Fax:
- Set A Call Comes In to
Webhookwith URLhttps://<ngrok-domain>/transcribeusing POST.
- Set A Call Comes In to
- Under Messaging set A Message Comes In to
Webhookwith URLhttps://<ngrok-domain>/smsusing 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.
- 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.
- Add to
.env:TS_AUTHKEY=tskey-auth-... TS_HOSTNAME=prax COMPOSE_PROFILES=tailscale TEAMWORK_BASE_URL=https://prax.<your-tailnet>.ts.net docker compose up -d— the sidecar joins the tailnet automatically. Visithttps://prax.<tailnet>.ts.net/for TeamWork andhttps://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=Adminindocker-compose.yml), so every tailnet member who can reach:3001is 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:
gunicornis not a dependency of this project. The shipped unit,deploy/systemd/prax.service, runsuv run --python 3.13 python app.pyunder systemd (Restart=always); use that (or your own supervisor around the same command). - Environment: copy
.envto the server, pointLOG_PATHandWORKSPACE_DIRto persistent volumes.DATABASE_NAMEis optional; leaving it asconversations.dbstores history under the workspace service-state directory. - Docker: see
Dockerfileanddocker-compose.ymlin the repo root. The app container needs/var/run/docker.sockmounted 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.).