Configuration
All runtime config is centralized in and validated via Pydantic ( ). Copy and fill in your values:
f62d7985
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, google_vertex, ollama, vllm |
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 reusable, non-ephemeral, pre-approved auth key — enables the Tailscale sidecar | 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 |
| 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 |
Default model for sandbox coding | openai/gpt-5.4 |
SANDBOX_MAX_ROUNDS |
Max message rounds per sandbox session | 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 included in docker-compose.yml and starts automatically. No extra configuration needed.
docker compose up --build # TeamWork is at http://localhost:3000
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 |
http://teamwork:8000 |
TeamWork API URL (set by docker-compose) |
TEAMWORK_API_KEY |
(empty) | API key for authentication (optional) |
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.)
- 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 from another machine without exposing the
host’s network, opt into the dockerized Tailscale sidecar. Without
TS_AUTHKEY + COMPOSE_PROFILES=tailscale in .env, the sidecar is
silently skipped — there’s no opt-out flag to set.
- Generate a key at https://login.tailscale.com/admin/settings/keys. Pick Reusable ✓, Ephemeral ✗, Pre-approved ✓ — non-ephemeral keys avoid the free tier’s 1,000-min/month minute budget that would otherwise count container restarts as fresh ephemeral nodes.
- 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.
The sidecar runs in userspace mode (no /dev/net/tun on the host) 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 0.0.0.0:5001 (configurable via .env). The scheduler starts automatically and loads any existing schedules.yaml files from user workspaces.
Production / Deployment
- Gunicorn:
uv run gunicorn 'app:app' --bind 0.0.0.0:5001 --workers 2 --threads 4 - 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.).