Skip to content

Developer Setup

This page collects development commands and repo conventions for contributors.

Terminal window
uv sync --all-extras --dev

Frontend dependencies:

Terminal window
cd frontend
npm install

The WhatsApp bridge requires Node.js 20 or newer and uses its own locked dependency tree:

Terminal window
make whatsapp-bridge-install
Terminal window
docker compose up -d pgvector livekit
uv run alembic upgrade head
make run
make frontend

Root Compose lets the API and durable run worker reach a model server on the host as host.docker.internal. Host-mode development can use localhost or another local address. Add the complete base URL to OUTBOUND_ADDITIONAL_TRUSTED_ENDPOINTS_JSON; trust is exact by endpoint purpose, host, port, and path. See Self-Hosted Models.

Host-mode chat images default to .voxos/attachments, which is gitignored and persistent across API restarts. Root Compose sets ATTACHMENT_LOCAL_PATH=/var/lib/voxos/attachments and mounts the same named volume into the API and durable run worker. Do not use /tmp when separate processes must read the same staged image.

The main attachment settings are:

Variable Default Purpose
ATTACHMENT_STORAGE_BACKEND local local for host/Compose development or s3 for deployed tasks.
CHAT_IMAGE_INPUT_ENABLED true Shared web/Flutter image-input rollout switch; model capability and attachment authorization still apply.
ATTACHMENT_LOCAL_PATH .voxos/attachments Persistent private local object root.
ATTACHMENT_S3_BUCKET unset Required private bucket when the backend is s3.
ATTACHMENT_S3_PREFIX chat-images Object-key prefix covered by task-role IAM.
ATTACHMENT_MAX_SOURCE_BYTES 26214400 Maximum streamed source upload.
ATTACHMENT_MAX_IMAGE_BYTES 5242880 Maximum normalized full image.
ATTACHMENT_MAX_TURN_BYTES 20971520 Maximum normalized bytes in one turn.
ATTACHMENT_MAX_IMAGES_PER_TURN 4 Maximum ordered images in one user turn.
ATTACHMENT_STAGING_TTL_SECONDS 86400 Cleanup age for unclaimed staging records.
ATTACHMENT_MAX_DELETION_ATTEMPTS 5 Maximum physical-deletion attempts before operator review.
ATTACHMENT_DELETION_RETRY_INTERVAL_SECONDS 60 Minimum interval between durable-run worker scans for incomplete object deletion.
ATTACHMENT_MAX_HISTORY_IMAGES 12 Image count retained in provider history.
ATTACHMENT_MAX_HISTORY_BYTES 41943040 Normalized media-byte history budget.

When deployed containers do not share durable storage, configure the supported S3 backend with a private bucket and grant the API/run-worker identity only ListBucket on the configured prefix plus GetObject, PutObject, and DeleteObject below it.

When New Relic is active, image ingestion emits privacy-safe custom metrics and events for success count, client source, normalized-size bucket, validation failure code, object-store latency, and provider image count. These signals do not include tenant IDs, filenames, hashes, captions, object keys, or image content.

Staged rows older than the configured TTL are processed when that tenant next uploads an image. Logical deletion records physical-deletion completion separately, and the durable run worker scans incomplete object deletions at the configured interval across tenants through an attachment-only RLS service scope. API access is revoked as soon as the attachment or its parent chat is marked deleted. Failed object deletion is bounded by ATTACHMENT_MAX_DELETION_ATTEMPTS; records left incomplete after that threshold require operator review.

Configure these settings in the API environment:

Variable Default Purpose
API_DOCS_ENABLED true Serves interactive documentation and the OpenAPI schema. Set to false for a public deployment.
API_RATE_LIMIT_PER_MINUTE 60 Per-user refill rate for authenticated write and stream requests.
API_RATE_LIMIT_BURST 20 Maximum immediately available write/stream admissions per user.
API_MAX_IN_FLIGHT 64 Maximum simultaneous HTTP requests per API process, excluding /health.
API_MAX_USER_IN_FLIGHT 4 Maximum simultaneous admitted write/stream requests for one verified local user.
FIREBASE_AUTH_CHECK_REVOKED false Adds Firebase Admin checks for revoked sessions and disabled or deleted accounts.

User admission failures return 429; exhausted process capacity returns 503. Both include Retry-After. Streaming responses hold their admission for the response lifetime. Budgets are per process, so capacity planning must account for the number of API replicas and worker processes.

Enable revocation checks only after validating Application Default Credentials with the project’s firebaseauth.users.get permission. A mounted credential file uses GOOGLE_APPLICATION_CREDENTIALS; the Helm chart can mount a Secret through application.firebaseAdminCredentialsSecret. Keep this credential outside source control. See Account And Auth.

Root Compose starts a private Stagehand task service and Steel Chromium. Configure separate random BROWSER_AGENT_SERVICE_TOKEN and STEEL_SERVICE_TOKEN values in the ignored .env file. Compose supplies the browser-agent token to API and worker callers as BROWSER_ACCOUNTS_STAGEHAND_SERVICE_TOKEN; host-mode callers must set that variable to the same token themselves. The task service requires both credentials at startup. Steel’s proxy uses its separate credential for HTTP and DevTools access.

The services have no published ports. A task creates an ephemeral browser, and execution is serialized to match Steel’s single-session self-hosted runtime. The queue accepts at most four waiting tasks by default, expires a queued request after 30 seconds, and includes queue time in the 570-second service task deadline. Excess or expired admissions return 429 with Retry-After. MAX_QUEUED_TASKS, QUEUE_WAIT_TIMEOUT_MS, and TASK_TIMEOUT_MS override those service settings. A canceled queued request is removed before a browser starts.

Voxos passes the agent’s instruction, public start boundary, action policy, and the requesting user’s currently selected text model transport for each task. Do not add a separate model key or website workflow to the service environment.

Public browsing uses this runtime without a saved profile. A Browser Account is an optional server-side profile that contributes authorized origins and either vault credentials or a hosted managed-profile reference to the same general task contract.

Browserless and Browser Use Cloud remain optional hosted providers. They require an enabled, trusted, connected MCP connection with static bearer or API-key header authentication respectively. The Browser Account service reuses that encrypted MCP provider token rather than storing a second copy. Define the variables below only when overriding an application default. Browser Use accounts have a two-letter browser country that defaults to GB; this is application data, not an environment setting. Browserless hosted-agent accounts use the configured fleet endpoint and do not expose a per-account country.

Variable Default Purpose
BROWSER_ACCOUNTS_ENABLED true Optional saved-profile rollout or incident kill switch. Set to false to disable Browser Account metadata, setup, authorization, reconciliation, and signed-in execution.
BROWSER_ACCOUNTS_SETUP_TTL_SECONDS 900 Maximum manual sign-in/setup lease and live-session duration.
BROWSER_ACCOUNTS_AUTHORIZATION_TTL_SECONDS 600 Lifetime of one unconsumed chat authorization.
BROWSER_ACCOUNTS_TASK_TIMEOUT_SECONDS 600 Overall provider task wait limit enforced by Voxos.
BROWSER_ACCOUNTS_CLEANUP_TIMEOUT_SECONDS 15 Bounded provider-session stop window used in cleanup.
BROWSER_ACCOUNTS_TASK_LEASE_SECONDS 720 Cross-replica exclusive task lease. Validation requires it to exceed the task timeout, cleanup timeout, and two polling intervals; execution heartbeats extend it.
BROWSER_ACCOUNTS_MAX_TASK_STEPS 40 Provider step and cost budget for one browser task.
BROWSER_ACCOUNTS_MAX_OUTPUT_CHARACTERS 12000 Maximum provider text copied into the tool result.
BROWSER_ACCOUNTS_POLL_INTERVAL_SECONDS 1.0 Provider task-status polling interval.
BROWSER_ACCOUNTS_BROWSERLESS_BASE_URL https://production-lon.browserless.io Optional Browserless hosted-agent fleet origin override. Voxos connects to its /chromium/cli endpoint using only the configured API token.
BROWSER_ACCOUNTS_STAGEHAND_BASE_URL http://127.0.0.1:3100 Private local Stagehand adapter URL for host-mode development. Root Compose supplies http://browser-agent:3100 internally.
BROWSER_ACCOUNTS_STAGEHAND_SERVICE_TOKEN unset Required bearer credential matching the private task service’s BROWSER_AGENT_SERVICE_TOKEN.

Provider execution heartbeats its exact task lease and observes durable account revocation. Browserless revocation waits for the executor that owns the WebSocket to stop it and acknowledge lease release before credentials are deleted. Expired leases are fenced and reconciled before another task can take ownership. The API also performs best-effort reconciliation during Browser Account list operations, marks stale setups, retries remote deletion, and records privacy-safe New Relic lifecycle signals and deletion-backlog counts. At larger volume the list-triggered maintenance should move to a dedicated bounded scheduler job without adding a cross-tenant vault bypass.

The browser runtimes cannot enforce an approval immediately before an agent submits a consequential external action. Browserless receives reviewed origins as instructions rather than a provider-enforced network policy. Browser tasks default to read_only; state_changes_allowed is reserved for actions the user explicitly requested, but it is not a secure transaction approval mechanism. Follow the rollout and incident requirements in Browser Accounts.

The local adapter pins Stagehand 3.7 because it is the Stagehand release line that exposes the autonomous agent() API used by this workflow. Build Steel from helm/images/steel and pin deployed images by digest. Its entrypoint requires a non-root user and the Chromium sandbox enabled. On Linux, configure host seccomp and AppArmor profiles that permit Chromium’s user-namespace sandbox operations; Kubernetes references them through browser.seccompProfile and browser.appArmorProfile. See the repository’s helm/README.md deployment guide for host profile configuration.

Update either component deliberately, review its browser/session and agent API contracts, then rebuild and run the Browser Account integration tests. Local account isolation depends on ephemeral sessions and vault-backed credential injection. The adapter’s CDP guard constrains top-level page and popup navigation to the account’s reviewed origins while allowing the page’s required cross-origin subresources. Steel logging is kept at warning level because its browser instrumentation can otherwise include full page and request URLs.

If the target local database reports that the vector extension is missing during migration, run this once with the configured Postgres admin role before retrying Alembic:

Terminal window
make db-enable-vector

The helper reads DB_HOST, DB_PORT, DB_NAME, POSTGRES_ADMIN_USER, and POSTGRES_ADMIN_PASSWORD from .env or the shell environment.

Optional workers:

Terminal window
make worker
make worker-runs
make worker-whatsapp
make scheduler

make worker runs the demand-driven LiveKit supervisor against the RLS-safe root Postgres/pgvector service. The minimal docker-compose.livekit.yml helper uses a plain Postgres bootstrap user and is not suitable for tenant-isolation or current migration testing.

Alternatively, docker compose --profile voice up --build starts the opt-in containerized supervisor. Use a saved LiveKit hostname reachable from both the caller and worker container.

make worker-whatsapp runs the multi-tenant WhatsApp supervisor. It first builds whatsapp-bridge/dist/index.js, then points the Python worker at the host’s node executable and that compiled script. The worker must share the API’s Postgres and secret-encryption configuration. The bridge is a private stdio child; do not run it as a public service or add a filesystem auth store. For host mode, the Make target defaults its health file to ${TMPDIR:-/tmp}/voxos-whatsapp-worker-<uid>.json unless WHATSAPP_HEALTH_PATH is already set.

The container image includes a pinned Node runtime plus production-only bridge dependencies. Start and verify the complete Compose stack with:

Terminal window
make up

This enables the voice and WhatsApp profiles, rebuilds changed images, runs migrations, recreates changed services, and waits for health checks. The WhatsApp container exits after a sustained unhealthy polling window so its Compose restart policy can replace the process. For a complete host-mode WhatsApp test, run the API, make worker-runs, make worker-whatsapp, and the frontend.

Local MCP services use the separate docker-compose.mcp.yml file and the root voxos_default network:

Terminal window
make mcp-stack-config
make mcp-stack-up
make mcp-stack-logs

The Flightradar24 service wraps its stdio package. The Monzo service builds ../monzo-mcp; that image exposes only authenticated native Streamable HTTP. It mounts only an owner-only MCP endpoint bearer. Provider OAuth, encrypted token storage, refresh, and refresh locking stay in Voxos. The MCP calls the private Voxos credential-broker route with a short-lived signed delegation. The MCP port remains private to the Compose network.

WhatsApp setup has four operational stages with different dependencies:

Stage Required runtime
Register the Voxos number by SMS API, Postgres/vault, public signed Twilio or Telnyx SMS webhook.
Register the Voxos number by call API, Postgres/vault, public signed Telnyx voice webhook and Telnyx transcription events. This call bypasses LiveKit and the normal voice agent.
Link and restore the companion device API plus whatsapp-worker and its private Node bridge.
Receive a message and generate a reply Connected whatsapp-worker, agent-run-worker, and an active tenant text provider.

The API must be running when the SMS or call arrives. Start the registration window in Voxos before requesting the code in WhatsApp Business so the signed provider event is correlated to the exact tenant integration and resource. A Telnyx verification call can contain a preliminary “press digit” prompt. The webhook handler derives the requested digit from each final transcript instead of configuring a fixed DTMF value, then captures the later variable-format spoken code. Keep Telnyx call.transcription events enabled on the Voxos-owned voice route.

Defaults are suitable for local development. Override only when deployment capacity or lifecycle timing requires it.

Variable Default Purpose
WHATSAPP_ENABLED true Enables new link, challenge, and relink operations and worker processing.
WHATSAPP_BRIDGE_EXECUTABLE node Node executable used for the private bridge child. Compose sets /usr/local/bin/node.
WHATSAPP_BRIDGE_SCRIPT /app/whatsapp-bridge/dist/index.js Compiled bridge entrypoint. make worker-whatsapp overrides this with the repository path.
WHATSAPP_WORKER_POLL_INTERVAL_SECONDS 0.5 Demand and outbox reconciliation interval and independent lease-renewal cadence.
WHATSAPP_WORKER_LEASE_TTL_SECONDS 30 Database ownership lease for a linked session.
WHATSAPP_WORKER_SHUTDOWN_TIMEOUT_SECONDS 10 Graceful bridge/session shutdown deadline.
WHATSAPP_BRIDGE_START_TIMEOUT_SECONDS 15 Bridge process and protocol-handshake startup deadline.
WHATSAPP_BRIDGE_REQUEST_TIMEOUT_SECONDS 20 Command and durable event RPC deadline.
WHATSAPP_BRIDGE_RESTART_GRACE_SECONDS 5 Delay between bridge/session recovery attempts.
WHATSAPP_MAX_SESSIONS_PER_WORKER 25 Maximum concurrent tenant sockets multiplexed by one worker.
WHATSAPP_CHALLENGE_TTL_SECONDS 120 Maximum QR or pairing-code lifetime accepted from the bridge.
WHATSAPP_API_CHALLENGE_WAIT_SECONDS 10 How long a challenge request waits for the worker before returning challenge_not_ready.
WHATSAPP_IPC_MAX_FRAME_BYTES 1048576 Exact maximum newline-delimited bridge frame size; must match the bridge handshake.
WHATSAPP_HEALTH_PATH /var/run/voxos/whatsapp-worker-health.json Mode-0600 worker health file used by the container healthcheck; the host Make target chooses a writable temp path by default.
WHATSAPP_HEALTH_FRESHNESS_SECONDS 10 Maximum age of worker health and successful poll timestamps.
WHATSAPP_WORKER_RESTART_AFTER_SECONDS 60 Sustained failed or stalled polling window before the worker exits for orchestration restart.

Settings validation requires the lease TTL to exceed the larger of the bridge request and shutdown timeouts plus two poll intervals. Restart grace must be shorter than the lease TTL, health freshness must exceed the poll interval, and the worker restart window must exceed both health freshness and the bridge request timeout. The configured session cap must also fit the bridge’s handshake capacity.

The root helm/ chart installs PostgreSQL/pgvector and the application as separate releases. Copy the operator values and environment templates from helm/examples/ into the ignored private/ directory; the repository’s helm/README.md installation guide describes domains, Firebase registrations, database roles, TLS, storage and ingress settings. Its migration hook runs after the database and referenced Secrets are ready. Configure private ingress/TLS values and encrypted application settings outside the chart. A Flux installation can load private chart values with HelmRelease.valuesFrom and SOPS decryption.

Keep production promotion workflows in the private GitOps repository and use GitHub Actions environment secrets for private URLs and credentials. Promote application images by digest after validation. Set application.env.DEPLOYMENT_REVISION to the source commit to verify the API and frontend release through their X-Voxos-Revision response headers.

Configure networkPolicy.enabled for both releases with the ingress controller and DNS pod/namespace selectors for the cluster. Set matching application and database release names. Public egress is an explicit setting; exclude node public addresses and other installation-specific infrastructure through private networkPolicy.publicEgress.additionalDeniedCIDRs values. Kubernetes NetworkPolicy enforcement must be available in the cluster.

Create the browser service Secret before installing the application. Its service-token authenticates API/worker callers to the task service and its separate steel-api-key authenticates the task service to Steel. Application containers use non-root users, read-only root filesystems, and ephemeral writable temporary directories. Database data and attachments use their configured persistent volumes.

For macOS Kubernetes tests, k3d runs K3s inside a Docker-compatible runtime. Choose the same K3s version as the deployment, keep kubeconfigs separate, and use localhost port forwarding. Rehearse restorations with workers at zero replicas and an egress policy before enabling live integrations.

Portable frontend builds obtain Firebase configuration from the selected API and use their current origin. Configure the backend’s FIREBASE_WEB_APP_ID; build-time Firebase variables are unnecessary. For iOS push notifications, download the installation’s GoogleService-Info.plist from Firebase and place it at app/ios/Runner/GoogleService-Info.plist, which is ignored by Git. Configure VOXOS_IOS_BUNDLE_ID and VOXOS_APPLE_TEAM_ID through the ignored app/ios/Flutter/Installation.xcconfig. Android uses VOXOS_ANDROID_APPLICATION_ID in ignored app/android/installation.properties. See the mobile guide in app/README.md for native registration and signing setup.

The API only persists durable agent runs and voice demand. It does not start or manage deployment infrastructure. Keep the agent-run worker, voice supervisor, and scheduler running alongside the API using Docker Compose, systemd, or the deployment platform’s ordinary process supervision. Add the WhatsApp worker when linked-device sessions are enabled and the Codex worker when containerized Codex tasks are enabled. All processes must share the same database, vault-encryption, and signing configuration.

Production application services should receive a pooled database URL for a least-privileged runtime login. The migration process alone should receive the direct migration/owner URL. Do not use the database project owner, a table owner, or a BYPASSRLS role as the runtime login. Local and Compose development leave DATABASE_TLS_MODE=driver-default so local Postgres remains usable; remote production databases should use verify-full with an appropriate CA bundle. Run python -m voxos.db.runtime_readiness with the proposed runtime URL before deployment; it exits nonzero when the role crosses the supported ownership, DDL, Alembic, or RLS privilege boundary.

Configure separate Sentry projects for the Python API/workers, React frontend, and Flutter app. SDK reporting starts when that component has a DSN. Errors use a 100% sample rate; performance traces default to 1%. Backend health checks are excluded from tracing. The Python API, agent-run worker, scheduler, WhatsApp worker, Codex worker, voice supervisor, registration children, and voice job processes share the backend project and carry a service tag.

For the API and workers, set these installation-owned environment variables:

SENTRY_DSN=<backend-project-dsn>
SENTRY_ENVIRONMENT=production
SENTRY_TRACES_SAMPLE_RATE=0.01
SENTRY_RELEASE=<release-or-commit>

SENTRY_RELEASE defaults to DEPLOYMENT_REVISION when configured. Host-mode development reads .env; Compose passes that file to application processes. Kubernetes operators can supply the same settings through application.env or the existing application Secret.

For the production frontend, include the public web-project DSN in the installation’s runtime-config.js referenced by frontend.publicConfigSecret:

window.__VOXOS_CONFIG__ = Object.freeze({
sentryDsn: "<web-project-dsn>",
sentryEnvironment: "production",
sentryRelease: "<release-or-commit>",
sentryTracesSampleRate: 0.01,
});

The same object can contain logoDevPublishableKey. Development can use VITE_SENTRY_DSN in frontend/.env.local. Production assets use runtime configuration, so one image can serve installations with different projects.

For Flutter, copy app/sentry.config.example.json to the ignored app/sentry.config.json and set SENTRY_DSN to the mobile-project DSN. Mobile Make targets and the TestFlight build script pass this file as Dart defines. Direct Flutter commands use --dart-define-from-file=sentry.config.json from app/. The environment defaults to production for release builds and development otherwise; add SENTRY_ENVIRONMENT to override it. Flutter uses the package version and build number as its release identifier.

Reporting retains exception types, stack locations and sampled operation timings. Privacy filters remove exception messages, request data, user data, custom extras, breadcrumbs and span payloads before transmission. Bulk logs, session recordings, screenshots and profiling are disabled. Python SDK integrations are explicit so installed AI-provider SDKs do not enable prompt or response collection automatically.

Set error rate limits under each project’s Client Keys (DSN) settings and keep Spike Protection enabled. A budget of 100 backend errors and 25 errors for each client per day totals at most 4,650 accepted errors in a 31-day month. Events above each daily cap are dropped; trace sampling is controlled separately by the SDK. Choose limits that fit the organization’s current allowance. Verify delivery with one synthetic exception in a development environment and check the matching project’s issue feed.

For readable release stack traces, upload web source maps and mobile debug symbols using Sentry’s source-map tools and Flutter debug-symbol guidance. Keep upload authentication tokens in private build configuration.

Run focused tests while changing behavior:

Terminal window
uv run pytest tests/product_docs
uv run pytest tests/agent_core/test_internal_tools.py
uv run pytest tests/agent_runtime/test_tool_authorization.py

Required final checks after code changes:

Terminal window
make format
make lint
make mypy
make pre-commit

Frontend checks:

Terminal window
cd frontend && npm run test
make frontend-lint

WhatsApp bridge checks:

Terminal window
make whatsapp-bridge-check

That target installs from package-lock.json, then runs ESLint, TypeScript typechecking, the bridge build, and Node tests.

Docs checks:

Terminal window
make docs-manifest
make docs-validate
make docs-refresh-check
make docs-index

make docs-index writes the product documentation index to the configured local database. It requires migrations, database connectivity, and an embedding API key unless run through a dry-run path. Use PLATFORM_OPENAI_API_KEY for product-doc embeddings. make docs-refresh-check performs the corresponding dry run without writing the live index.

Use Alembic autogenerate after model changes:

Terminal window
uv run alembic revision --autogenerate -m "describe change"

Review generated migrations before committing them. User-owned tables need RLS policies and wrong-user tests. Global product-doc tables do not store tenant data and do not need tenant RLS.

Review migrations, configuration, runtime database privileges, and worker health before routing production traffic to a new application revision.

make publication-check rejects installation files even when force-added to Git, source symlinks, and Python lockfiles using a private package index. make pre-commit runs this check together with formatting, typing, chart, frontend and secret checks. Keep environment files, Terraform state, keys, local attachments, database dumps and recovery archives in ignored storage. Sanitized .example files remain publishable. Application dependencies resolve from public PyPI with uv sync --frozen; image builds need no private index.

CI audits the locked Python and JavaScript runtime and development dependencies using pip-audit and npm audit. The dependency audit requires no maintainer credentials and gates API image publication. Published API, frontend, browser service and Steel images also receive Trivy runtime vulnerability checks, including packaged Java libraries and operating-system packages. Review security updates with the SDK integration tests and rebuild the affected images before release.

The Secret scan CI check fetches complete branch, tag and historical pull-request heads, then runs pinned Gitleaks with archive decoding and redacted output. Its exemptions identify exact synthetic values or hash formats and include regression tests for detecting new credentials in the same files. Report vulnerabilities through the private reporting channel in the repository’s SECURITY.md.

Published API and Steel images include source archives and license notices under /usr/share/voxos/source. Steel also exposes its source directory at /source/ on the service HTTP port. The API’s WhatsApp program is distributed under GPLv3 terms and the Steel/MuPDF service under AGPLv3 terms; applicable MIT, Apache and LGPL notices remain with their components. See Third-party software and source for access and redistribution instructions. Docker builds verify upstream source checksums; dependency-source versions are checked against the lockfiles and installed native bundle metadata. The API source directory includes libvips component archives, upstream build patches and locked librsvg Rust dependency sources.

The Python integrations use LiveKit Agents 1.8, MCP SDK 2 and HTTPX 2 for SDK transports. Exposed MCP servers are built with mcp.server.MCPServer and the Firebase token verifier; configure Streamable HTTP options on streamable_http_app(). The application’s other HTTP integrations use HTTPX.