Self-hosting
Enabling sandboxes
A Sandbox gives one NPC a machine of its own: a filesystem that persists, a terminal, a browser, and the ability to install what it needs. Every NPC that has one gets its own, so NPCs work in parallel without colliding.
This is off by default and turning it on is a deliberate act. Read ADR 0006 before you do.
What you are agreeing to
Something has to create the machines, and that something needs the host's Docker socket. Access to the Docker socket is equivalent to root on the host: anything that can reach it can start a privileged Docker container with the host filesystem mounted.
The API server must not be that something. It holds the Provider keys, the database connection and the session secret, and it executes Tool calls the model wrote. Giving it the socket would make one prompt injection, one hostile MCP Server or one confused model a full host compromise rather than a lost machine.
So the socket goes to a separate service, sandbox-broker, and nothing else:
- The broker is the only service with
/var/run/docker.sockmounted. - It publishes no port. It is reachable only over the Compose network, and the reverse proxy has no route to it.
- It still requires a bearer token on every request, because "nothing else can reach it" is a property of your deployment, not of the code.
- The API server calls it over HTTP and never touches Docker itself.
The honest limit: Sandboxes share the host kernel. This contains a careless or manipulated model. It does not contain a determined attacker with a kernel exploit, and it is not a safe place to run deliberately hostile code.
Turn it on
Four steps, in this order.
1. Generate a broker token. The broker refuses to start unless
SANDBOX_BROKER_TOKEN is at least 16 characters, and that check is in the
broker itself rather than in Compose, so a short value fails loudly at start
rather than silently at runtime.
Put it in the root .env, alongside the broker's address:
Both the URL and the token must be non-empty for the API server to consider Sandboxes available. With either missing, Sandbox Tools report that the feature is not enabled rather than failing with a connection error a Player cannot act on.
2. Build the image NPCs work in.
That builds deploy/sandbox/Dockerfile and tags it agentsville-sandbox:latest. It
carries bash, git, jq, ripgrep, Python and Node, plus Chromium installed at
build time — a Sandbox with no route to the internet cannot fetch a browser the
first time it needs one, so it ships with one.
3. Create the internal network, if it does not already exist.
--internal is the whole point: machines on it can reach each other and nothing
else.
4. Start the stack with the profile.
Profiles combine, so keep whichever edge profile you already use. Without
--profile sandbox the broker is simply not started.
What a Sandbox is allowed to do
Every machine the broker creates runs with the same hardening, and every flag is load-bearing:
- A non-root user, uid 10001.
- All Linux capabilities dropped, and
no-new-privileges. - A read-only root filesystem, with two writable places:
/work, which is a named volume that persists, and/tmp, which does not. - Capped at 2 GB of memory, 1.5 CPUs and 512 processes.
- Attached to
agentsville_sandbox_net, which has no route out.
Internet access
Egress is off by default, and only the Operator can turn it on, per NPC. A Player who tries gets a 403 saying so.
The reason is plain: a Sandbox with a browser and a route out can send anything it can read anywhere. That is the whole trade, and it belongs to a person rather than a default. An NPC granted egress is placed on Docker's default bridge network instead of the internal one.
The grant is a row in the database, and the broker reads it every time it brings a machine up. That is what stops a restarted broker quietly returning a Sandbox to the wrong network.
Idle machines
A Sandbox that has been quiet for longer than SANDBOX_IDLE_MINUTES — 30 by
default — is stopped. Stopped, not destroyed: the volume survives, so the next
Message resumes where the NPC left off.
Destroying a Sandbox from the interface removes its volume. That is not reversible, and it is what revoking the grant does, because a revoked Sandbox should not leave its filesystem lying around.
Prove the isolation holds
Every property above is something a change to deploy/sandbox/ or to the driver
could quietly undo, and none of them fail loudly when they do — a Sandbox that
can reach the internet still runs commands perfectly well.
That runs scripts/sandbox-check.mjs, which starts two throwaway machines and
asserts from outside them: the process runs unprivileged, every capability is
dropped, the root filesystem is read-only, /work is writable, one NPC cannot
read another's files, there is no internet access, there is no Docker socket,
there is no route to the database, a browser is present, and files survive a
stop and start. It cleans up its own containers and volumes either way and exits
non-zero on any failure.
It needs Docker, the agentsville-sandbox image and the internal network, and it does
not need the rest of the stack running. Run it after any change to the Sandbox
image or the driver.
Next
- Using the sandbox — what a Player does with one.
- The sandbox boundary — the same boundary, drawn.
- Sandbox — the concept itself.