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…
f7a8cb56
View source ↗
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(inlocal.mkformake run-local-*, or indeploy/update.sh’s environment) layers prax-sandbox’sdocker-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 smallcap_drop. Writes to/workspacestay 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 coreutilstimeout, its process group is stopped at the deadline, and the tool sees exit code 124. A process thatsetsids 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:
desktop_list_windows— what is open, which window has focus, where.desktop_type(text, window="terminal" | id | title, press_enter)anddesktop_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.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 theSANDBOX_CODING_AGENT_ENABLEDflag. 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 (seedocs/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_pythonruns throwaway Python in the sandbox’s scratch venv;sandbox_shellruns a shell command in the container. The sandbox spoke (delegate_sandbox) is now a headless sub-agent that writes and runs code directly viasandbox_shell— no session lifecycle — registered wheneverSANDBOX_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. Needsduckdb+pandasin the sandbox image (installed into/opt/prax-venv; the tool addresses that venv’s python by absolute path, likelean_checkpins/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 throughprax/utils/shell.py:run_command, which reaches the container only whenRUNNING_IN_DOCKER=true; on a native install they run on the Prax host (see the Known gap indocs/security/sandbox-execution-boundary.md). browser_serviceconnects 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).