E2B-compatible sandbox control plane. Use the standard E2B SDK with local Docker containers, Shuru microVMs, or Tart macOS VMs — swap to real E2B in production by changing one URL.
| Docker | Shuru | Tart | |
|---|---|---|---|
| Technology | Linux containers | macOS microVMs (Apple Virt) | macOS VMs (Apple Virt) |
| Platform | All | macOS only | macOS only |
| Guest OS | Linux | Alpine Linux | macOS |
| Isolation | Namespace/cgroup | Hardware VM | Hardware VM |
| Pause/resume | Yes | No (ephemeral) | Yes (suspend/resume) |
| Status | Default | Supported | Supported |
See docs/container-runtimes.md for detailed backend comparison.
# Build the base image
docker build -t sandbox-base:latest docker/sandbox
# Install
brew install circlesac/tap/sandbox
# or: npx @circlesac/sandbox
# Start (auto-generates API key on first run)
sandbox serveTo run the control plane itself in Docker on macOS, enable Host networking under Docker Desktop's Settings → Resources → Network and enable Docker at login. Then start the sandbox service:
docker build -t sandbox-base:latest docker/sandbox
docker compose build --no-cache sandbox
docker compose up -d sandboxThe Compose service mounts the Docker socket and ~/.sandbox, listens on the
host's port 49982, and restores itself after Docker Desktop or the Mac
restarts. Use docker compose ps sandbox and sandbox status to verify both
the container and the E2B control plane.
The service intentionally uses restart: always, not unless-stopped.
Docker Desktop marks restored containers as stopped during a clean restart,
and its host-network forwarder becomes ready after the Docker engine. The
Compose startup command records the Docker VM boot ID, waits 12 seconds, and
restarts the control plane once per VM boot so port 49982 is attached after
host networking is ready. Keep that startup guard when changing the service.
brew install shuru
shuru checkpoint create sandbox-base --allow-net -- sh -c '...'
# Set backend
# ~/.sandbox/config.json → { "backend": "shuru" }
sandbox serve# Install Tart and pull macOS base image (~24GB, one-time)
brew install cirruslabs/cli/tart
tart clone ghcr.io/cirruslabs/macos-sequoia-base:latest sandbox-base
# Build and install envd-lite into base image (one-time)
cd envd-lite
go generate ./...
go build -o envd-lite .
# See CONTRIBUTING.md for base image setup with envd-lite pre-installed
# Start with macOS support alongside Docker
SANDBOX_MACOS_BACKEND=tart sandbox serveenvd-lite is pre-installed in the base image with a LaunchAgent, so each sandbox only needs to clone the VM and boot it. VM creation takes ~30-60s depending on host CPU.
When a remote Padawan, Worker, or test runner needs to reach a local control
plane, run cloudflared in the same Docker Compose project. Derive a stable
per-device hostname from the Mac's hardware serial so multiple machines do not
claim the same DNS record:
SANDBOX_DEVICE_HASH="$(printf '%s' "$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformSerialNumber/{print $4}')" | shasum -a 256 | cut -c1-8)"
SANDBOX_TUNNEL_NAME="sandbox-${SANDBOX_DEVICE_HASH}"
SANDBOX_PUBLIC_HOSTNAME="sandbox-${SANDBOX_DEVICE_HASH}.crcl.es"
# One-time Cloudflare setup. `login` writes the local management certificate;
# the token file is the only credential mounted into the runtime container.
cloudflared tunnel login
cloudflared tunnel create "$SANDBOX_TUNNEL_NAME"
cloudflared tunnel route dns "$SANDBOX_TUNNEL_NAME" "$SANDBOX_PUBLIC_HOSTNAME"
cloudflared tunnel token "$SANDBOX_TUNNEL_NAME" > ~/.sandbox/cloudflared-tunnel-token
chmod 600 ~/.sandbox/cloudflared-tunnel-token
docker compose up -d cloudflared
docker compose ps sandbox cloudflared
curl -fsS "https://${SANDBOX_PUBLIC_HOSTNAME}/health"The cloudflared container routes the tunnel to http://localhost:49982, uses
host networking, waits for the sandbox health check, and has restart: always,
so Docker Desktop restores both services after login or reboot. Never commit
the token file or pass it on the command line.
Use https://sandbox-${SANDBOX_DEVICE_HASH}.crcl.es for both the E2B SDK's apiUrl
and sandboxUrl. This is a physical-device naming convention for the control
plane, not a Docker-only or per-container hostname.
The E2B SDK works the same across all backends. Use metadata.platform to select the platform:
import Sandbox from "e2b";
const opts = {
apiUrl: "http://localhost:49982",
apiKey: "sk-...",
validateApiKey: false,
};
// Linux sandbox (default)
const linux = await Sandbox.create("base", opts);
const result = await linux.commands.run("uname");
// → "Linux"
// macOS sandbox
const macos = await Sandbox.create("base", {
...opts,
metadata: { platform: "macos" },
});
const result2 = await macos.commands.run("sw_vers -productName");
// → "macOS"Recent E2B SDKs validate official Cloud keys as e2b_<hex> before making a
request. circlesac/sandbox uses its own authenticated sk-sandbox-* keys, so
custom-control-plane clients must set validateApiKey: false. This disables
only the SDK's local prefix check; the control plane still authenticates the
key. For Padawan's PTY smoke, use the equivalent environment setting:
E2B_VALIDATE_API_KEY=false \
SANDBOX_API_URL=http://localhost:49982 \
SANDBOX_API_KEY="$(jq -r .apiKey ~/.sandbox/config.json)" \
bun run smoke:ptygraph LR
SDK[E2B SDK] --> CP[Control Plane :49982]
CP --> |POST /sandboxes<br>DELETE /sandboxes/:id| Backend[Backend<br>Docker / Shuru / Tart]
Backend --> |container/VM lifecycle| Sandbox[Sandbox]
SDK --> |connectrpc| Proxy[Envd Proxy]
Proxy --> |Process.Start<br>Filesystem.ListDir<br>/files, /health| Envd[envd<br>in sandbox]
See CONTRIBUTING.md for development setup and project structure.