@stixxert/pi-docker-sandbox
v0.1.0
Published
pi extension: a private docker sandbox (sbx microVM with its own daemon) as the agent's deploy target — the host's docker is never exposed.
Readme
docker-sandbox-extension
A pi extension that gives an AI coding agent a private
docker sandbox to deploy into — powered by
Docker Sandboxes (sbx).
Each pi session gets its own sandbox microVM with its own docker daemon,
running in parallel to the agent. The agent can pull images, build, run
containers, and docker compose up — while the host's docker is never
exposed to it.
Why
Agents that can deploy are powerful; agents that can reach your real docker daemon are a liability. With this extension:
- Every docker operation happens inside a disposable microVM with its own
daemon — nothing appears in the host's
docker ps. - The extension never touches a host docker socket or CLI; it only shells
out to
sbx exec. There is no fallback path to the host daemon. - Docker env vars (
DOCKER_HOST,DOCKER_CONTEXT, …) are scrubbed and the inner CLI is pinned to the sandbox daemon, so leaked host config cannot redirect it. docker_verifyruns a live isolation audit (PASS/FAIL per check).
Requirements
- pi (the extension runs in the host pi process)
- The Docker Sandboxes CLI (
sbx) — a standalone binary; no Docker Desktop required - A host hypervisor for
sbxto boot microVMs on. The sandbox is a real VM with its own kernel, so the virtualization backend is platform-specific:- macOS 14 (Sonoma)+ on Apple silicon — uses Apple's Virtualization
framework (Intel Macs are not supported by
sbx) - Linux x86_64 with KVM — needs hardware virtualization and your user
in the
kvmgroup (lsmod | grep kvmshould listkvm_intel/kvm_amd/kvm_arm64)
- macOS 14 (Sonoma)+ on Apple silicon — uses Apple's Virtualization
framework (Intel Macs are not supported by
See Install for one-line installs per platform.
Install
1. Install the sbx CLI (one line per platform):
macOS — Apple silicon, Sonoma 14+:
brew install docker/tap/sbx && sbx loginLinux — Debian/Ubuntu x86_64 with KVM:
curl -fsSL https://get.docker.com | sudo REPO_ONLY=1 sh && sudo apt-get install -y docker-sbx && sudo usermod -aG kvm "$USER" && newgrp kvm && sbx loginFirst sbx login opens a browser for Docker OAuth and asks you to pick a
default network policy ("Balanced" is a good default). newgrp kvm drops you
into a shell with the group active; log out and back in for it to stick.
2. Install the extension — as a pi package (recommended). The repo
declares itself via the pi manifest in package.json (loads index.ts),
so it installs like any pi package — from git, npm, or a local checkout:
pi install git:[email protected]:stixxert/pi-docker-sandbox
# or from npm:
pi install npm:@stixxert/pi-docker-sandbox
# or try it for a single run without installing:
pi -e git:[email protected]:stixxert/pi-docker-sandbox
# or install the checkout you're developing in:
pi install ./path/to/this/repoManually — the extension is a single file with no build step, so you can
copy it to one of pi's auto-discovery locations, then /reload in pi:
cp index.ts ~/.pi/agent/extensions/docker-sandbox.ts # global, or
cp index.ts .pi/extensions/docker-sandbox.ts # project-localor load it directly for a quick test:
pi -e ./index.tsThe sandbox is auto-provisioned on first use (2 CPU / 2 GB, session workspace mounted). No manual steps required.
Tools
| Tool | Purpose |
|------|---------|
| docker_status | Sandbox presence, engine version, resources |
| docker_verify | Live isolation audit (PASS/FAIL per check) |
| docker_resources | VM memory/cpu/disk + docker disk + per-container usage, with warnings |
| docker_prune | Reclaim sandbox space (docker system prune -af) |
| docker_ps / docker_images | List containers / images in the sandbox |
| docker_pull | Pull an image into the sandbox daemon |
| docker_run | Run a container (ports, env, memory cap, volumes, network, restart; detach or foreground) |
| docker_logs / docker_exec | Inspect a running container |
| docker_build | Build an image from a workspace directory |
| docker_init | Scaffold a Dockerfile (+ compose) with language detection |
| docker_compose | Deploy/manage compose projects (up -d --build) |
| docker_stop / docker_start / docker_rm | Container lifecycle |
| docker_curl | Probe a published port from the host process (GET/POST/PUT) |
| docker_sandbox_rm | Remove this session's sandbox (microVM + everything inside) |
| docker_gc | Sweep stale pi-sbx-* sandboxes left by crashed sessions |
All containers get the label com.pi.sandbox=true.
Multi-session sandbox naming
Each pi session gets its own sandbox named pi-sbx-<pid>-<random> — unique per
process, never repeats, and independent of any host environment (herdr, tmux,
terminal). Concurrent sessions never share docker state. For a stable,
persistent name (e.g. a shared sandbox reused across restarts), pin
DOCKER_SANDBOX explicitly.
DOCKER_SANDBOX— pin an explicit (e.g. shared, persistent) sandbox name.DOCKER_SANDBOX_AUTOCREATE=0— disable auto-provisioning (create manually withsbx create --name <name> shell <workspace-dir>from a host pane).DOCKER_SANDBOX_CPUS/DOCKER_SANDBOX_MEMORY— resources for auto-created sandboxes (default2/2g; sbx minimum memory is 1 GiB, values below are clamped).DOCKER_SANDBOX_WORKSPACE_RO=1— mount the project read-only into the sandbox (sbx requires the primary workspace to be rw, so a small scratch primary is created and the project is mounted as a read-only additional workspace). The agent writes project files through its normal tools (/workspacein its own VM); the sandbox and any container it runs can only read the project — even bind mounts from it are read-only. Builds and compose still work; volume binds sourced from the project get:roadded automatically.DOCKER_SANDBOX_KEEPALIVE=1— keep the sandbox VM running while the pi session is alive (watchdog pokes it every ~60s), defeating sandboxd's idle-stop for leave-it-running deploy flows.DOCKER_SANDBOX_TEMPLATE=<name>— use a pre-bakedsbx templatefor auto-created sandboxes (avoids re-pulling common images every session; create withsbx template save <name>from a prepared sandbox).
Ports (verified rules)
- sbx does not auto-forward docker
-pmappings — the extension explicitly publishes them (sbx ports <sandbox> --publish H:C) afterdocker_run/docker_compose upand reports the host URL (http://127.0.0.1:<hostport>/).docker_rmunpublishes again. - Host ports < 1024 are privileged in macOS/Unix for any non-root process (standard OS rule, not an sbx limitation) — irrelevant for dev (dev servers run on 3000/8080/…) and only matters when reproducing a production URL without a port number.
- Avoid container port 80 — the sandbox's port proxy resets it (verified:
node, busybox, nginx). Use 3000/8080/8000 (
docker_inittemplates do); nginx-style images just need their listen port changed — the host URL is unaffected. - UDP:
sbx portsacceptsH:C/udpand a round-trip was verified (node dgram echo); useports=["5000:5000/udp"]. - Idle-stop: sandboxd stops sandbox VMs ~2–4 min after the last
sbxcall;docker_rundefaults detached services torestart=unless-stoppedandsbx portsmappings survive VM restarts. For leave-it-running flows setDOCKER_SANDBOX_KEEPALIVE=1. - Reboot cleanup:
docker_rm/docker_compose downunpublish port mappings; after a host reboot let the session-start GC handle it, or runsbx rm --force $(sbx ls -q | grep pi-sbx)from a host pane. - Agent-side verification:
docker_curl(host-side fetch, GET/POST/PUT with optional body); human-side:http://localhost:<hostport>/.
See boundary.md for the full agent ↔ sbx boundary
model and usage guide (agent + human). The extension is not dependent on
gondolin — it runs in the host pi process and relies only on the
/workspace ↔ host-cwd path convention.
Sandbox lifecycle
| Event | What happens |
|-------|--------------|
| First docker_* call | Sandbox is auto-provisioned (unique name, session workspace mounted) and a detached watchdog is armed on the pi process |
| Session idle | Nothing — the sandbox stays ready; sbx exec auto-starts the VM if it was stopped |
| Session ends (exit, Ctrl+C/D, SIGHUP/SIGTERM, /new, /resume, /fork) | session_shutdown hook runs teardown immediately (fast path) |
| Session dies hard (kill -9, process-manager kill) | The watchdog (which polls the pi process and survives it) performs the same teardown within seconds — no event required |
| Watchdog also missed (host reboot) | Sandbox left stopped; the GC sweep reclaims it at the next session start |
| Manual | docker_sandbox_rm (this session's sandbox) / docker_gc (stale sandboxes) |
Teardown policy (DOCKER_SANDBOX_TEARDOWN, default remove for session
sandboxes, none for pinned DOCKER_SANDBOX names):
remove— delete the microVM and everything inside it (images, containers, volumes). State is rebuildable; nothing accumulates.stop— keep state, free memory; the VM restarts on next use.none— leave running (use for shared/persistent sandboxes).
Crash safety net: at every session start (and via docker_gc [hours]) the
extension sweeps sandboxes named pi-sbx-* that are stopped, not the
current session's, and older than DOCKER_SANDBOX_GC_HOURS (default 24).
Note: DOCKER_SANDBOX_GC_HOURS=0 disables the automatic session-start
sweep; the docker_gc tool still accepts hours=0 for a manual any-age
sweep. Running sandboxes and non-pi sandboxes are never touched.
Path mapping
The agent's VM mounts the workspace at /workspace; the extension maps
/workspace/<rel> → <host pi cwd>/<rel>, which is also the path inside the
sandbox (the workspace is direct-mounted there). So docker_build contexts
and docker_run volume binds "just work".
Example deploy flow
docker_status
docker_init(context=/workspace/myapp) # scaffold Dockerfile
docker_build(context=/workspace/myapp, tag=myapp:latest)
docker_run(image=myapp:latest, name=web, ports=["8080:80"], detach=true)
docker_logs(id=web, tail=50)
docker_verify # confirm isolation
docker_compose(file=/workspace/myapp/compose.yaml, action=up)Dockerfiles
Dockerfiles are ordinary files in the workspace — the agent authors/edits
them with its normal tools and builds them with docker_build. docker_init
bootstraps a sensible one for node (npm or pnpm), go, python, rust, or a
generic alpine base, plus a .dockerignore and optional compose.yaml.
Security
See security.md for the threat model, design
guarantees, and the docker_verify check list. For maximum project
protection, set DOCKER_SANDBOX_WORKSPACE_RO=1: the sandbox then has the
project mounted read-only and all project writes flow through the agent's
own sandboxed tools instead.
Host env forwarding (secure by default)
The extension passes only a minimal safe set of the pi host's environment
into the sandbox — HOME, PATH, USER, LOGNAME, TMPDIR, SHELL,
LANG, TERM (non-secret vars the sbx CLI and shells need). DOCKER_* /
COMPOSE_* are always stripped — a leaked DOCKER_HOST could redirect
the inner docker client. Everything else is confined by default:
DOCKER_SANDBOX_ENV_ALLOWLIST="A,B"— opt in precisely: forward the minimal set plus exactlyAandB(e.g. the vars your compose file interpolates with${VAR}). Docker-affecting vars cannot be allowlisted.DOCKER_SANDBOX_ENV_PASSTHROUGH=1— explicit opt-out: forward the host env minus the docker vars (rawsbx execsemantics, for legacy workflows that rely on broad compose interpolation).
Anything that runs inside the sandbox (containers, docker_exec'd commands)
can read whatever reaches it — so host API keys and tokens stay confined
unless you allowlist them deliberately. Prefer project-local .env files
(gitignored) over host-env interpolation for compose secrets.
docker_verify audits the active mode (docker_status reports it).
Development
npm install
npm run typecheck # strict tsc against the pi SDK types
npm test # loads the extension with a mock pi API; no sbx neededThe extension is a single file with no build step — index.ts is loaded
directly by pi (copy it to ~/.pi/agent/extensions/docker-sandbox.ts, or
pi install the repo, see Install). package.json doubles as the
pi package manifest (pi.extensions) and declares typecheck-only dev deps;
tsconfig.json keeps tsc --strict honest against the pi SDK types.
License
Apache-2.0 — see LICENSE.
