Providing Prax a sandbox

Prax delegates code execution, the browser, and the desktop to a sandbox — a Docker environment that now lives in its own project, prax-sandbox (a sibling repo, installed as the prax_sandbox_client dependency). Prax runs with or without…

Synced from Prax at f7a8cb56 View source ↗

← Infrastructure

Prax delegates code execution, the browser, and the desktop to a sandbox — a Docker environment that now lives in its own project, prax-sandbox (a sibling repo, installed as the prax_sandbox_client dependency). Prax runs with or without a sandbox, and the sandbox can be local or remote. This page is about how to give Prax a sandbox; the sandbox’s own internals (the image, the desktop, the browser/CDP) are documented in the prax-sandbox repo (docs/sandbox.md, docs/desktop.md, docs/browser.md).

On / off

SANDBOX_ENABLED (default true). Set SANDBOX_ENABLED=false to run Prax as a pure harness: no sandbox tools, no delegate_sandbox / delegate_desktop, no run_python, no container dependency. Chat, memory, notes, scheduling, and the channels all work without it.

Local sandbox (the default, and the functional example)

Run the sandbox container next to Prax. The prax-sandbox repo’s compose is the functional reference — it is what make run-local-all and deploy/update.sh start:

cd ../prax-sandbox && make build && docker compose up -d

prax’s own docker-compose.yml / docker-compose.lite.yml define the same sandbox service for the all-in-Docker path. Since 2026-09-07 they match the reference: no compose-level healthcheck (the image’s pgrep -x supervisord is what prax waits on), no model keys, no /source mount.

Resource limits and exec deadlines are opt-in. By default the container has no memory, process or /tmp bound, and docker exec has no deadline — the 2026-07-08 disk-full outage was an unbounded ffmpeg writing 21 GB into the container’s /tmp. Two switches, both recommended:

  • SANDBOX_LIMITS=1 (in local.mk for make run-local-*, or in deploy/update.sh’s environment) layers prax-sandbox’s docker-compose.limits.yml: memory without extra swap (SANDBOX_MEM_LIMIT, 3g), SANDBOX_PIDS_LIMIT (1024), a sized tmpfs /tmp (SANDBOX_TMP_SIZE, 1g) and a small cap_drop. Writes to /workspace stay unbounded — Docker cannot size a bind mount.
  • SANDBOX_EXEC_TIMEOUT_ENFORCED=true (a Prax setting) makes every timeout Prax passes real: the command runs under coreutils timeout, its process group is stopped at the deadline, and the tool sees exit code 124. A process that setsids itself escapes the group; the pids limit is the backstop.

Packages installed in the container last as long as the container. make restart-sandbox restarts it rather than recreating it (since 2026-10; it used to run down + up, which threw away everything installed), and make run-local-* and deploy/update.sh reuse it unless its image or compose settings changed. To keep a package across a recreation, list it in ../prax-sandbox/sandbox/local-packages.txt: both start paths run prax-sandbox’s scripts/ensure-image.sh, which rebuilds the image only when that list changed. A bad entry is skipped and reported (in the start output, make local-status, and the container’s log), not fatal. What was installed by hand is recorded in the workspace at .sandbox/installed-apt.txt. Details: prax-sandbox’s README, “Your own packages”.

What /workspace is depends on how you started it. make run-local-all passes WORKSPACE_DIR=<workspaces>/<PRAX_USER_ID> (the user’s own workspace); deploy/update.sh passes WORKSPACE_DIR through and defaults it to the whole workspaces/ tree, so set it to the per-user directory on a multi-user box or every user’s files are visible from the container. Prax does not assume either shape: it reads the directory bound at /workspace from Docker (prax/services/sandbox_mount.py) and derives every container path from it — the sandbox agent’s “your directory”, the shell translation, delivery of files the agent reports. A user whose workspace is not in the mount is told so rather than handed another user’s path.

Prax reaches it in-process — the control plane holds the docker socket, execs into the container for shell/file ops, and talks to sandbox:9223 (CDP) for the browser. Relevant settings: SANDBOX_HOST, SANDBOX_IMAGE (prax-sandbox:latest), SANDBOX_TIMEOUT, SANDBOX_MAX_CONCURRENT.

Remote sandbox

Run the sandbox on another box behind the control daemon and point Prax at it:

SANDBOX_DAEMON_URL=https://sandbox-host:8843
SANDBOX_DAEMON_TOKEN=…
SANDBOX_TLS_VERIFY=true            # true | false | path to a CA bundle
# SANDBOX_CLIENT_CERT=… SANDBOX_CLIENT_KEY=…   # opt-in mTLS

The same SandboxClient switches to an HTTPS + bearer transport — shell, the file API, and CDP all work over the wire as plain request/response calls (there is no SSE streaming: the client’s _iter_sse helper has no callers and the daemon has no event-stream route; the client’s pull_tar/push_tar exist but Prax does not call them). Empty SANDBOX_DAEMON_URL → in-process. See the prax-sandbox repo’s docs/remote.md for deploying the daemon (TLS, tokens, Tailscale-or-not).

Prax on the desktop

TeamWork’s Desktop tab shows the sandbox’s Linux desktop live, and Prax can use it with the user, the way ChatGPT’s and Claude’s computer use do, but structured first, with pixels last:

  1. desktop_list_windows — what is open, which window has focus, where.
  2. desktop_type(text, window="terminal" | id | title, press_enter) and desktop_key(keys, window) — the window is brought to the front and the keys go to it. “The terminal” means the terminal window on that desktop.
  3. desktop_screenshot(question) — the configured vision model reads the screen (a terminal’s output, a button’s position). The fallback, not the first move.

They run inside the sandbox container through the sandbox client in every deployment shape (they used to run on the Prax host on a native install). When the user is on the Desktop tab, Prax is told so and given these rules; sandbox_shell says in its result that its output was not visible there, and the claim auditor flags a reply that says something is on the user’s screen when no tool put it there. The orchestrator carries desktop_type and desktop_screenshot itself (DESKTOP_KERNEL_TOOLS, default on); multi-step GUI work goes to delegate_desktop.

How Prax uses it

2026-07 — no coding-agent crutch. The sandbox image no longer ships the OpenCode/Claude-Code/Codex CLIs or a coding-agent server, and the multi-round coding-session tools (sandbox_start/message/review/finish/abort/search/execute) were removed entirely (#142) along with the SANDBOX_CODING_AGENT_ENABLED flag. Prax codes directly — run_python, workspace_save/workspace_patch (syntax-linted), source_read/source_grep, sandbox_shell. This removes a black-box dependency and the need for any model key inside the sandbox (see docs/security/sandbox-execution-boundary.md — prax-sandbox’s compose passes none; prax’s own compose still does, a tracked leftover). The direct-execution sandbox tools below are unaffected.

  • run_python runs throwaway Python in the sandbox’s scratch venv; sandbox_shell runs a shell command in the container. The sandbox spoke (delegate_sandbox) is now a headless sub-agent that writes and runs code directly via sandbox_shell — no session lifecycle — registered whenever SANDBOX_ENABLED.
  • data_query (opt-in, DATA_TOOLS_ENABLED) runs a DuckDB SQL query in the sandbox for deterministic number/tabular crunching — DuckDB reads CSV/Parquet/JSON files directly (SELECT … FROM '/workspace/active/x.csv'), so most “process these numbers” tasks are one query rather than a coding session. Needs duckdb + pandas in the sandbox image (installed into /opt/prax-venv; the tool addresses that venv’s python by absolute path, like lean_check pins /opt/elan). Spoke-internal (prax/agent/data_tools.py); classified LOW — it’s read/compute in the already-isolated container. Degrades with a clear message when the flag, libs, or sandbox are absent.
  • The desktop spoke drives the Linux desktop via the desktop_* tools. These go through prax/utils/shell.py:run_command, which reaches the container only when RUNNING_IN_DOCKER=true; on a native install they run on the Prax host (see the Known gap in docs/security/sandbox-execution-boundary.md).
  • browser_service connects Playwright to the sandbox’s Chrome over CDP, and falls back to a local headless Chrome when there is no sandbox.
  • Self-improvement runs natively (self_improve_read/write/patch/test/verify/ deploy) against the mounted source — no coding-agent CLI in the loop.

All of these are thin wrappers over the prax_sandbox_client.SandboxClient facade, configured by prax/services/sandbox_bridge.py (which builds a SandboxConfig from Prax settings + Prax callbacks). Prax depends on prax-sandbox via a uv path source (../prax-sandbox).