Self-hosted execution infrastructure for AI agents.
Run agent code on any cloud, any machine — or none at all. Speak any SDK. Keep full control.
Your clouds. Your machines. Your rules.
One command brings up the control plane and a Docker worker from the published images — no Go toolchain, no build, no cloning the repo.
curl -O https://raw.githubusercontent.com/sandrpod/sandrpod/main/docker/docker-compose.yml
docker compose up -d
That pulls ghcr.io/sandrpod/{server,poder,toolbox}
and starts:
the control plane on localhost:8080
and one Docker worker (Poder) that dials back over a reverse tunnel — no inbound ports on the worker.
First run downloads ~170 MB here (server + worker). The sandbox runtime (
toolbox
, ~530 MB) is pulled by the worker the first time you create a sandbox, so step 2 below is the slow one. After that, both steps take seconds.
Check it:
curl localhost:8080/health # {"status":"ok",...}
curl localhost:8080/api/v1/poders # wait for one worker to show "state":"ONLINE"
/health
answers as soon as the control plane binds, which is before the worker has finished registering. The second command is the one that tells you the stack is actually ready.
⚠️ This dev stack runs withauthentication disabled(anonymous admin) — fine on localhost. Before exposing it, setSANDRPOD_TOKEN
(see[docs/AUTH_AND_KEYS.md]).
pipx install sandrpod-cli # recommended — isolated, nothing to activate
sandrpod-cli --api-url http://localhost:8080 create demo --provider local
sandrpod-cli --api-url http://localhost:8080 execute demo "echo hello from SandrPod; python3 -c 'print(6*7)'"
Real output from a cold run (no cached images) against v0.5.10:
Sandbox: demo
Job ID: job-1788150453342465216-7j5di2LP
Status: provisioning
State: PENDING
State: RUNNING
Sandbox 'demo' is ready
hello from SandrPod
42
SandrPod is the control plane for AI agent code execution — open source and self-hosted. It turns your own infrastructure into a fleet of on-demand sandboxes that agents create, run code in, and tear down, while you keep ownership of where that code runs, what it can touch, and where the data lives.
Hosted sandbox services make you rent their runtime in their region. SandrPod inverts that: you bring the substrate — any cloud, plain Docker, or a bare machine — and SandrPod is the thin, portable layer that schedules, tunnels, and governs execution across it. It rests on three pillars:
Run anywhere you own— 8 clouds (incl. Aliyun & Tencent), Docker, or a machine with no Docker at all.** Speak any SDK**— a native REST/Python/TS API, LangChain/deepagents, and the** unmodified E2B SDKas a drop-in. Stay in control**— reverse-tunnel workers with zero inbound ports, an opt-in permission gate + decision audit, and self-hosted data that never leaves your infra.
One binary schedules sandboxes across whatever infrastructure you have:
AWS · GCP · Azure · Aliyun · Tencent · DigitalOcean · Hetzner · Oracle —
plus plain Docker, or a bare machine with no Docker via sandrpod-agent
.
Aliyun and Tencent make China-region and data-residency deployments first-class — something hosted services don't offer. Each provider has a guide under docs/ (
AWS,
GCP,
Tencent, …). Remote exec uses each cloud's managed run-command API where it exists (AWS SSM, Aliyun CloudAssist, Azure Run Command, Tencent TAT, Oracle Instance Agent) and an ephemeral-key SSH path elsewhere (GCP, DigitalOcean, Hetzner).
SandrPod isn't tied to one client. It exposes a native REST API and Python/TS
SDKs, a first-class LangChain/deepagents backend, and the full E2B
wire protocol — so the unmodified e2b
/ e2b-code-interpreter
SDKs work against it with nothing but two env vars. Bring the ecosystem you already use; migrate nothing.
Zero inbound ports. Workers dialoutto the control plane over a WebSocket reverse tunnel — run them behind NAT, in a private subnet, or on a laptop.Governance, not just isolation. Opt-in employee-PC mode adds a per-machine permission gate (path consent, command denylist, PTY consent) and a decision audit pipeline (NDJSON + central HTTP upload) — turning "run agent code" into "governwhat agents may touch on real machines."Your data stays yours. Self-hosted, Apache 2.0, no phone-home.
| Hosted (E2B, Modal, …) | SandrPod |
|
|---|---|---|
License |
Closed | Apache 2.0, open source |
Where it runs |
Their infra / region | Your cloud, Docker, or bare metal |
Clouds |
Vendor-managed | AWS · GCP · Azure · Aliyun · Tencent · DO · Hetzner · Oracle |
China regions |
✗ | ✓ Aliyun + Tencent |
No-Docker mode |
— | sandrpod-agent : any machine becomes a sandbox |
SDKs |
Their SDK | Native + LangChain + drop-in E2B |
Inbound ports on workers |
n/a | None (reverse tunnel) |
Governance / audit |
— | Opt-in permission gate + decision audit |
Data residency |
Their region | Your account, your region |
Recorded live against a SandrPod deployment: the package is the one from PyPI, the only change is which host it points at. Source: assets/e2b-demo.py.
One example of "speak any SDK": already have code on the E2B SDK? Point it at your
SandrPod and it just works — Sandbox.create
, files.*
, commands.*
(foreground/background/PTY), run_code
, watch_dir
, get_metrics
, ``
/resume
:
import os
os.environ["E2B_API_KEY"] = "e2b_your_key" # issued by your SandrPod
os.environ["E2B_API_URL"] = "https://sandbox.you.com" # control plane
os.environ["E2B_SANDBOX_URL"] = "https://sandbox.you.com" # per-sandbox envd
from e2b import Sandbox # the real, unmodified e2b SDK
sbx = Sandbox.create()
sbx.files.write("/tmp/hi.txt", "hello from my own cloud")
print(sbx.commands.run("cat /tmp/hi.txt").stdout) # → hello from my own cloud
from e2b_code_interpreter import Sandbox as CI
ci = CI.create()
print(ci.run_code("import numpy as np; np.arange(6).sum()").text) # → 15
For a zero-config drop-in (no env vars, just a domain), run the gateway behind a
wildcard domain — SANDRPOD_E2B_DOMAIN=sandbox.you.com
*.sandbox.you.com
DNS
- TLS. Full surface, wire-protocol details, and the coverage matrix (verified against the real, unmodified E2B SDK over a real container) are in.docs/E2B_COMPAT.md
E2B SDK ──┐
Native SDK ├─→ API Server (Control Plane, :8080)
LangChain ─┤ ↕ WebSocket + yamux reverse tunnel
CLI ───────┘ Poder (Worker) ──→ Toolbox (Sandbox container)
sandrpod-agent ──→ (direct mode, any machine as a sandbox)
| Component | Description |
|---|---|
| API Server | |
| The control plane: native REST + E2B-compatible gateway, sandbox CRUD, scheduling, tunnel proxying | |
| Poder | |
| Worker node. Persistent WebSocket tunnel to the control plane; manages Docker container lifecycle | |
| sandrpod-agent | |
| Registers the local machine directly as a sandbox with an embedded Toolbox — no Docker required | |
| sandrpod-tray | |
| Optional user-session GUI for employee-PC mode: tray icon + consent prompts + local settings page | |
| Toolbox | |
| Execution service inside each sandbox. PTY, file ops, background processes, sessions |
Employee-PC mode (opt-in): whensandrpod-agent
runs on a real employee laptop rather than a server, enable a per-PC permission gate (path consent + command denylist + PTY consent) and a decision-audit pipeline that ships every allow/deny/warn event to a central HTTP endpoint. Both are off by default (--permission-mode=off
) — see.[docs/PERMISSION_AND_AUDIT.md]
…or with plain curl #
(no install)
curl -X POST localhost:8080/api/v1/sandboxes \
-d '{"name":"demo","provider_type":"local"}'
curl -X POST "localhost:8080/api/v1/sandboxes/execute?sandbox=demo" \
-d '{"code":"print(6*7)","language":"python"}'
python
from langchain_sandrpod import SandrPodClient
sb = SandrPodClient(api_url="http://localhost:8080").get_sandbox("demo")
print(sb.execute("echo hello from SandrPod").output)
from e2b import Sandbox
print(Sandbox.create().commands.run("echo hello from SandrPod").stdout)
Other ways to run it #
Production, with TLS and a real domain— the reference deployment: PostgreSQL, a wildcard certificate, and the E2B surface on.docker compose -f docker/docker-compose.prod.yml up -d --wait
, walked through end to end (DNS, certificate, renewal, acceptance sweep) in— or as a narrative, with the two failures it took to get there, indocs/PRODUCTION_DEPLOYMENT.mdthis writeup.PostgreSQL only(no TLS termination):docker compose -f docker/docker-compose.pg.yml up -d
— seedocs/MULTI_INSTANCE_DEPLOYMENT.mdto scale out.Build from source(contributors):docker compose -f docker/docker-compose.local.yml up -d --build
From Go directly:go run ./cmd/server -port 8080 -db sqlite:./data/sandrpod.db
, then add a worker with thedocker run …/poder
one-liner orgo run ./cmd/poder -api-url=http://localhost:8080
.No Docker at all— turn any machine into a sandbox:go run ./cmd/agent -api-url=http://localhost:8080 -name=my-machine
The default docker-compose.yml
persists to SQLite (a sandrpod-data
volume), so sandboxes and tokens survive a restart.
sandrpod-tray serve # user-session tray + consent dialogs (🛡)
go run ./cmd/agent -api-url=http://localhost:8080 -name=my-laptop \
-permission-mode=prompt \
-audit-upload-url=https://your-platform/api/audit/decisions/batch
Permission modes: off
(default) | prompt
(consent dialog outside work_dir
) | strict
(silent deny outside work_dir
).
Full architecture, permissions.json
schema, tray CLI, and audit protocol: ** docs/PERMISSION_AND_AUDIT.md**.
pip install langchain-sandrpod
python
from langchain_sandrpod import SandrPodClient
from deepagents import create_deep_agent
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o", temperature=0)
client = SandrPodClient(api_url="http://localhost:8080")
sb = client.get_sandbox("my-sandbox")
agent = create_deep_agent(model=model, backend=sb)
result = agent.invoke({"messages": [{"role": "user", "content": "Write a quicksort and run it"}]})
with client.sandbox("temp-sb") as sb:
agent = create_deep_agent(model=model, backend=sb)
result = agent.invoke({"messages": [...]})
The backend also exposes richer per-sandbox capabilities directly:
sb.run_code("x = 40", context="ctx1") # stateful Jupyter-style kernel
sb.run_code("x + 2", context="ctx1")["text"] # → "42" (x persisted)
sb.metrics() # {cpu_count, cpu_used_pct, mem_*, disk_*}
with sb.watch_dir("/workspace") as w: # filesystem watch
events = w.get_new_events()
See pkg/sdk/python/langchain_sandrpod/examples/ for full examples.
pipx install sandrpod-cli # or: uv tool install sandrpod-cli
sandrpod-cli config set-url http://localhost:8080
sandrpod-cli list
sandrpod-cli create my-sandbox --provider local --image ghcr.io/sandrpod/toolbox:latest
sandrpod-cli create gpu-box --provider gcp --region asia-east1-a --instance-type e2-medium
sandrpod-cli execute my-sandbox "ls /workspace" # one-shot (stateless)
sandrpod-cli stream my-sandbox "make build" # real-time streamed output
sandrpod-cli run my-sandbox "z = 10" --context c1 # stateful kernel — z persists in context c1
sandrpod-cli stats my-sandbox # live CPU / memory / disk
sandrpod-cli fs watch my-sandbox /workspace # print filesystem events
sandrpod-cli shell my-sandbox # interactive PTY
sandrpod-cli delete my-sandbox
sandrpod-cli poder list
sandrpod-cli poder delete <poder-id>
SandrPod speaks two HTTP surfaces: its own native REST API, and — when the
E2B gateway is enabled — the full E2B control-plane + envd
protocol.
Native API
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/sandboxes |
|
| List sandboxes | ||
| POST | /api/v1/sandboxes |
|
| Create sandbox | ||
| DELETE | /api/v1/sandboxes/{name} |
|
| Delete sandbox | ||
| POST | /api/v1/sandboxes/execute |
|
| Execute code | ||
| GET | /api/v1/sandboxes/{name}/toolbox/* |
|
| Proxy to Toolbox (file upload/download, etc.) | ||
| GET | /api/v1/poders |
|
| List Poder nodes | ||
| DELETE | /api/v1/poders/{id} |
|
| Delete a Poder record |
E2B-compatible API — POST /sandboxes
, GET /sandboxes/{id}
,
/sandboxes/{id}/{,resume,metrics,connect}
, and the envd
Filesystem/Process connect-RPC services. See docs/E2B_COMPAT.md.
make build-all
go build -o server ./cmd/server
go build -o poder ./cmd/poder
go build -o agent ./cmd/agent
go build -o sandrpod-tray ./cmd/sandrpod-tray # CGO required
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dist/server-linux-amd64 ./cmd/server
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o dist/sandrpod-agent-linux-amd64 ./cmd/agent
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o dist/sandrpod-agent-darwin-arm64 ./cmd/agent
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o dist/sandrpod-agent-windows-amd64.exe ./cmd/agent
docker buildx build --platform linux/amd64 -f docker/Dockerfile.poder -t ghcr.io/sandrpod/poder:latest --load .
docker buildx build --platform linux/amd64 -f docker/Dockerfile.toolbox -t ghcr.io/sandrpod/toolbox:latest --load .
Longer pieces, with the commands and the failures they were measured from:
Self-hosting an E2B-compatible sandbox stack, from an empty server— one VM, one domain, four containers, and the unmodified E2B SDK checked function by function. 48 of 50 calls pass; the two that do not are named.Two ways to give a deepagents agent a sandbox you own—langchain-e2b
andlangchain-sandrpod
down the same twelve assertions. Both pass. Testing both surfaced two real defects — both of them mine to fix — and a third finding that turned out to be a deliberate contract I had misread as a bug.
More at blog.sandrpod.com.
Deploying this inside a company, or hitting something the docs don't cover? Open an issue, email me at zhaochj@126.com, or find me on X as @itscjzhao.
Apache 2.0