Website · Documentation · Benchmark report
Run an agent's shell and file operations inside a gVisor Pod on your own
Kubernetes cluster. A Control Plane owns Workspaces, quotas and credentials.
Agents reach it through a Python SDK, the sandbox CLI, or a stdio MCP bridge —
and through nothing else. If the Control Plane or the Runtime is unreachable the
operation fails; it never falls back to running on the host.
┌─ your process ──────────────────────────────────────────────────────────┐
│ Python SDK sandbox CLI stdio MCP bridge │
│ sandbox_platform/ `sandbox …` for an agent runtime │
└──────────────────────────────┬──────────────────────────────────────────┘
│ HTTPS + tenant API key
│ the tenant is decided by the credential,
│ never by anything in the request body
════════════════════════════════▼═════════════════════════════════════════════
node: sandbox-local · namespace: sandbox-system trusted control plane
┌─────────────────────────────────────────────────────────────────────────┐
│ Control Plane (control_plane/) │
│ admission · quotas · tokens · workspace and runtime lifecycle │
│ │ │
│ ├── state ──────────► PostgreSQL · MySQL · SQLite │
│ └── checkpoints ────► S3-compatible object store │
│ workspace archives only: no process state, │
│ no memory, no writable container layer │
│ │
│ Operator Console (console/) static; holds no credential of its own │
└──────────────────────────────┬──────────────────────────────────────────┘
│ in-cluster, NetworkPolicy-scoped
════════════════════════════════▼═════════════════════════════════════════════
nodes: sandbox-local-w1…wN · namespace: sandbox-workloads Runtime worker pool
┌─────────────────────────────────────────────────────────────────────────┐
│ Runtime Pod RuntimeClass: gvisor │
│ runtime/ shell, PTY sessions, SSE streaming │
│ file-service/ the one path that writes workspace files │
│ │ │
│ └── /workspace ──► Workspace PVC ◄── Volume Agent (RWX placement) │
└─────────────────────────────────────────────────────────────────────────┘
If the Control Plane or the Runtime is unreachable the operation fails.
It never falls back to running on the host.
make bootstrap # create .venv and install SDK + test dependencies
make test # 880 unit and contract tests, no network, no cluster
make verify # complete Python, Console, manifest, Helm, wheel gate
make help # every Make target with its one-line descriptionThat is the contract suite: no cluster, no credentials, nothing to clean up.
The virtual environment is not optional on Debian, Ubuntu or Fedora — a
system-wide pip install is refused there (PEP 668). make test uses
.venv/bin/python when it exists and python3 otherwise, so make bootstrap
and make test need no activation step. CLI examples below use the explicit
.venv/bin/ path for the same reason.
Run the full local cluster when you want a real
gVisor Runtime.
Status: alpha (0.1.0). main is the only channel and not a stability
promise — Known limitations is deliberately specific.
Self-contained. This repository is the whole product: it depends on no other
repository, and other products integrate with it as an external tenant through the
API keys and contracts documented here.
| Compared with | What Sandbox Platform does differently |
|---|---|
| Docker-in-Docker or a shared container | Every Runtime is its own Kubernetes Pod under the gvisor RuntimeClass: non-root, read-only root filesystem, no service-account token, default-deny NetworkPolicy. No Docker socket is ever exposed to agent code. |
| Hosted sandbox services | Self-hosted on your cluster. Workspace files, checkpoints, credentials, and control-plane state stay with the operator. There is no vendor API in the request path. |
| Firecracker-style microVM stacks | Isolation comes from gVisor through the standard Kubernetes RuntimeClass, so any node with containerd and runsc qualifies — cluster nodes do not need KVM. |
Four things worth checking in the source before you spend more time:
- Workspace lifetime is separate from Runtime lifetime. Stopping a Runtime keeps its Workspace PVC and files. Checkpoints are an explicit recovery path, not the normal persistence path.
- The runtime-driver abstraction refuses to pretend.
SANDBOX_RUNTIME_DRIVERaccepts onlygvisor; any other value makes the Control Plane exit at startup rather than silently emit a gVisor Pod under another name (control_plane/core.py). There is no provider plug-in surface advertised that does not exist. - Tenant ownership is tested against a real database, not mocked away.
require_workspace_tenantreturnsTrueunconditionally when no store is configured, which would make an in-memory contract test useless.tests/test_api_authorization.pytherefore boots the Control Plane in a subprocess against SQLite, creates two tenants, and replays every by-id route with the wrong tenant's key — asserting a404arrives before any Kubernetes call, so ownership is proven to be checked ahead of the dependency. - Route authorization is a checked-in manifest, cross-verified three ways.
ROUTE_AUTHdeclares 60 routes; 53 require credentials and 7 do not (/livez,/readyz,/healthz,/metrics, and the three OIDC sign-in discovery endpoints, which a browser reaches precisely because it has no credential yet).tests/test_route_completeness.pyfails the build if theapi.pydispatch table, the OpenAPI document, andROUTE_AUTHever disagree.
Measured performance on the local reference profile (Apple Silicon, dedicated Lima VMs, 5 runs × 100 measured iterations, no warm Runtime pool):
| Operation | p50 | p95 |
|---|---|---|
| gVisor Runtime cold start (new Pod, scheduling to ready) | 2.497 s | 2.789 s |
| Warm execution | 35.72 ms | 48.08 ms |
| Workspace create | 29.21 ms | 51.42 ms |
Full method, raw evidence layout, and the explicit statement of what this number is not (it is not a cloud, multi-tenant, or node-cold measurement) are in the benchmark report.
No Control Plane needed — each of these prints what it accepts:
.venv/bin/sandbox --help # create / run / exec / stop / list
.venv/bin/sandboxctl --help # workspaces, templates, admin keys, audit
.venv/bin/sandbox-mcp --help # nine agent-scoped MCP tools and required env vars
make help # every Make target with its one-line descriptionFrom a fresh clone, this is the shortest path from prerequisites to a real gVisor command and durable Workspace proof:
make quickstartIt checks the host, creates .venv, creates or reuses the repository's isolated
Lima cluster, and proves all of the following against the live deployment:
- the command reports a gVisor kernel and runs non-root on a read-only root;
- the Runtime has no Kubernetes service-account token;
- a Workspace file survives stopping and replacing its Runtime;
- an unavailable Control Plane fails closed without executing on the host;
/healthz,X-Request-Id, and Prometheus metrics are visible.
After make quickstart succeeds, keep the Console forward running in one terminal:
make console-forwardOpen http://127.0.0.1:18081. In another terminal, print the local login key:
make --no-print-directory dev-tokenPaste that value into API key and choose Enter console. The local key is
administrator-equivalent: do not share it or put it in a URL. The Console retains it
only in that browser tab's sessionStorage; signing out or closing the tab removes it.
Production deployments should use OIDC or a revocable API key instead.
The command ends with the first Runtime-call latency and total elapsed time. It
also writes .sandbox/quickstart-summary.json and
.sandbox/showcase-result.json, so install success, phase duration, manual
interventions, and value-proof results can be compared between machines or CI
runs. make smoke-local reruns only the live proof against an existing cluster.
The local profile exposes raw Prometheus metrics but intentionally does not install
Prometheus or Grafana; the Console's Grafana-backed Observability tab appears only
when an operator configures that external dependency.
For contributors, make verify is the one-command pre-PR gate corresponding to
the executable parts of CI. It keeps successful output compact, stores one log per
phase under .sandbox/logs/verify/, and writes durations and outcome to
.sandbox/verify-summary.json; a failing phase prints its last 100 log lines.
make e2e-local runs all five live cluster scenarios. Release candidates can run
make acceptance to execute quickstart, the source gate, and the full live E2E in
that order with one command.
make up-local builds one multi-node kubeadm Kubernetes cluster in Lima. The
sandbox-local node hosts trusted system services; the tainted
sandbox-local-w1…wN pool hosts gVisor Runtime workloads. Cilium connects the
nodes and enforces the workload policies; CephFS keeps Workspaces mountable from
any active worker. The default is one worker, not a fixed maximum. This is the only local
cluster profile the project ships.
make doctor checks every item below and exits non-zero if one is missing, so run
it first rather than discovering a gap halfway through the VM build.
| Requirement | Detail |
|---|---|
Commands on PATH |
docker (daemon reachable), limactl, kubectl, helm, python3, openssl; on Linux also qemu-system-<arch> and shasum. Lima has one vmType on Linux, qemu, and boots the VM with the host architecture's system emulator, which limactl does not ship; shasum is what the Cilium and Rook chart installers verify with. macOS uses its own hypervisor framework and needs neither. |
| Python | 3.11 or newer |
| Host OS | macOS or Linux |
| Host architecture | amd64 or arm64 (scripts/local-cluster.yaml pins Ubuntu images for both; gVisor is installed for x86_64 and aarch64) |
| Available memory | 10.5 GiB free for the default new profile — 6 GiB for the control plane plus 4 GiB per worker; this is a hard check, not a warning |
| Free disk | 35 GiB free under $LIMA_HOME for the default new profile — the 60 GiB control-plane and 30 GiB-per-worker disks are sparse |
| Virtualization | On Linux, a readable and writable /dev/kvm. Without it Lima falls back to QEMU TCG software emulation, which boots kubeadm many times slower and is not usable in practice. make doctor warns rather than fails on this one. |
| Network | Egress to pull the Ubuntu cloud image, Kubernetes apt packages, Cilium, gVisor, Metrics Server, and Rook/Ceph images |
Set SANDBOX_DOCTOR_SKIP_RESOURCES=1 to bypass only the memory and disk checks. The
control-plane VM uses 4 CPUs, 6 GiB memory, and a 60 GiB disk by default,
adjustable through SANDBOX_LOCAL_CPUS, SANDBOX_LOCAL_MEMORY_GIB, and
SANDBOX_LOCAL_DISK_GIB. The Runtime worker uses 4 CPUs, 4 GiB memory, and a
30 GiB disk. SANDBOX_LOCAL_WORKER_COUNT selects the initial pool size;
SANDBOX_LOCAL_WORKER_CPUS, SANDBOX_LOCAL_WORKER_MEMORY_GIB, and
SANDBOX_LOCAL_WORKER_DISK_GIB size each worker. When all requested VMs already
exist, doctor switches to a 2 GiB memory / 5 GiB disk reuse gate; expanding the
pool adds capacity checks per missing worker. Explicit SANDBOX_DOCTOR_MIN_*
overrides still win. When VM memory or disk sizes are customized, the resource
gate derives its reservation from those configured sizes rather than retaining
the default-profile threshold.
Every profile owns a Lima user-v2 network named <control-plane>-net; this is
what gives each VM a distinct, mutually reachable address. Override it with
SANDBOX_LOCAL_NETWORK; override its automatically selected unused /24 with
SANDBOX_LOCAL_NETWORK_GATEWAY. An older VM created on Lima's isolated default usernet
is rejected rather than silently reused as a broken multi-node cluster.
make doctor
make bootstrap
make up-localThe first make up-local downloads and builds everything listed above, so its
duration is dominated by your network throughput; subsequent runs reuse the existing VM disks.
It ends by printing:
Control Plane: http://127.0.0.1:18080
Kubeconfig: /path/to/checkout/.sandbox/kubeconfig
The kubeconfig is written to .sandbox/kubeconfig inside the checkout, not to
~/.kube/config, so the local cluster cannot collide with a context you already use.
The Makefile exports KUBECONFIG for its own targets, so make dev-token,
make status-local, and the port-forward targets need no manual export.
Resize the Runtime pool without rebuilding the control plane:
make scale-workers WORKERS=3 # add/start w2 and w3, install gVisor, load Runtime image
make scale-workers WORKERS=1 # drain and stop w2 and w3; disks are retained
make scale-workers WORKERS=0 # allowed only when no Runtime Pods remainScale-down fails closed if a target node still has an active Runtime Pod. Release or checkpoint that Runtime first, then retry. Scaling back up reuses stopped worker disks; the stale Kubernetes Node registration is removed while stopped and kubeadm registers it again on scale-up. At zero workers, Runtime admission is paused and fails fast; the trusted storage and Console services remain available.
export SANDBOX_CONTROL_PLANE_URL=http://127.0.0.1:18080
export SANDBOX_TOKEN="$(make --no-print-directory dev-token)"
.venv/bin/sandbox run --name demo --stop -- python -c 'print("sandbox-ready")'sandbox-ready
The token is read through command substitution so its value never enters shell
history. The CLI passes the command's stdout and stderr through unchanged and exits
with the command's exit code. --stop releases the Runtime while preserving the named
Workspace demo.
make down-local # stop the control plane and every worker; VM disks and .sandbox/ remain
make destroy-local # delete the control plane, every worker, and generated statedown-local is the right choice between sessions — up-local reuses both stopped VMs.
destroy-local is irreversible: Workspace files, checkpoints, and the SQLite state
inside the VM are gone. It also removes the four fixed-tag project images from the
local Docker daemon; shared base layers remain available to Docker's cache.
Four surfaces reach the same Control Plane — a Python SDK, the sandbox CLI, a
stdio MCP bridge for agent runtimes, and the operator console. Each one is
documented with a worked example in docs/USAGE.md, along with
the tasks that come up once something is running: issuing tokens, taking a
checkpoint, importing files, and reading the audit log.
The diagram at the top of this file shows the layout. This section is the detail behind it.
| Component | Responsibility |
|---|---|
control_plane/ |
HTTP API, policy, authentication, lifecycle orchestration, the provider-neutral Runtime Driver contract, and the built-in gVisor driver |
runtime/ |
Minimal shell MCP server: synchronous execution, SSE streaming, PTY sessions |
file-service/ |
Canonical Workspace file operations, embedded in the Runtime; mutating file requests require a running Runtime |
k8s/ and overlays/ |
Declarative Kubernetes resources for the reference deployment |
charts/sandbox/ |
Independently deployable Helm package |
console/ |
Static operator console; embeds no credential and keeps a user-provided key only in tab-scoped sessionStorage |
sandbox_platform/ |
The published Python package: SDK and Sandbox facade, sandbox user CLI, sandboxctl operator CLI, and the sandbox-mcp stdio bridge |
Two Kubernetes namespaces separate the trust levels: sandbox-system holds the
Control Plane and Console, sandbox-workloads holds Runtime Pods, the volume agent,
and the default-deny NetworkPolicies.
What this project is not responsible for. It does not provide accounts, regions, VM snapshots, dynamic per-request firewall rules, brokered third-party credentials, public-domain routing, or billing. Checkpoints are workspace archives in S3-compatible storage: they do not capture processes, memory, or the writable container layer. Node autoscaling, registry mirroring, and CSI behavior belong to the cluster operator. See System specifications for the full list.
Every response carries X-Request-Id, and the same value appears in the Control Plane
access log as trace_id. The Control Plane adopts an inbound W3C traceparent when
there is one, so it joins a trace an upstream gateway already started rather than
beginning a parallel one; see the HTTP and SDK contract.
Known gap, stated here rather than left to be discovered: object-storage calls go out
through boto3, which does not yet propagate traceparent, so that hop is
not traced yet. Someone who finds a hole in a trace should be able to confirm it is
expected instead of first suspecting their own query.
Kustomize overlays under overlays/ layer on the provider-neutral base in
k8s/. Every overlay renders with kubectl kustomize <path> except
overlays/local-dev, which is a Kustomize Component consumed by overlays/local
rather than a deployable overlay; CI checks the others on every push.
| Profile | Intended for | Maturity |
|---|---|---|
overlays/local |
The 1+N make up-local cluster. SQLite state, CephFS RWX Workspaces, and a scalable Runtime pool. |
Reference. Exercises placement, isolation, scaling, and recovery behavior; its single-OSD Ceph and SQLite database are not production durability claims. |
overlays/rwo-single-node |
Clusters where all Workspace consumers share one RWO-capable Runtime node. Trades volume-agent availability for portability: one RWO claim, one pinned replica. | Reference. |
overlays/eks |
Amazon EKS. Swaps in an EFS-backed StorageClass, which the operator must install first. | Adapter example — see Known limitations before using it. |
overlays/external-deps |
Control-plane state and object storage outside the cluster (managed database, S3-compatible store). | Example with placeholders. Nothing references it by default; copy it and fill in your own Secrets. |
charts/sandbox |
Helm-based installs. make chart-lint and make chart-render validate it without a cluster. |
Independently deployable package. |
Production requires a conforming Kubernetes cluster with a NetworkPolicy-enforcing CNI and a working gVisor RuntimeClass. Read Production guide and Platform capability contract before deploying anything beyond the local profile.
- Agent shell and file operations do not fall back to the host when the Control Plane or Runtime is unavailable.
- Runtime Pods are non-root, use a read-only root filesystem, receive no Kubernetes service-account token, and run under gVisor in the reference configuration.
- Runtime egress is limited to cluster DNS and public TCP 80/443; private, link-local, and loopback ranges are denied.
- Workspace and runtime tokens are scoped: one Workspace cannot read or mutate another.
- Admin credentials are separate from tenant and runtime credentials.
- Object keys are owner-partitioned and object-store credentials are least privilege.
- Checkpoint restore rejects path traversal, links, devices, oversized archives, and unexpected archive structure.
- The Control Plane OpenAPI document
(
contracts/control-plane-openapi.yaml) is the authority for HTTP routes and authentication groups.
Report vulnerabilities through the process in SECURITY.md. The trust model, assets, and threat list are in docs/SECURITY_MODEL.md.
These are real and currently unfixed. A README that hides them wastes your time later.
overlays/eks cannot pull images as shipped. It patches EFS storage onto the base
and nothing else, so the five imagePullPolicy: Never settings in k8s/ survive into
the rendered EKS output. Those exist because the local profile side-loads images into
the VM. On EKS the Pods will never pull from a registry. Patch imagePullPolicy and
the image references before treating this overlay as deployable.
The sandbox-system namespace has no NetworkPolicy. All four shipped
NetworkPolicies target sandbox-workloads. The Control Plane and Console are also
exposed as NodePort services (30080 and 30081) in the base manifests. Both are
acceptable for the dedicated local cluster and are not acceptable on a shared cluster
without an ingress and namespace policies in front of them.
The workspace admission gate is a process-local lock.
_WORKSPACE_ADMISSION_LOCK in control_plane/volume.py serializes workspace
admission within one process. The base k8s/ manifests and overlays/eks run the
volume agent with replicas: 2 and a RollingUpdate strategy, so two processes hold
two independent locks and the workspace count can be exceeded under concurrency.
overlays/local and overlays/rwo-single-node patch this to replicas: 1 with a
Recreate strategy and are not affected.
The Control Plane is a single replica by design. k8s/control-plane.yaml sets
replicas: 1 with a Recreate strategy — deliberate, because several gates are
in-process, but it does mean a control-plane restart is a brief outage.
No signed release exists yet. Installation is source-based until the first signed
tag; main is not a stable release channel.
Architecture states the boundaries that were chosen here rather than merely left undone, and ROADMAP.md lists what is still outstanding before a first public release.
| Start here | For |
|---|---|
| Documentation index | Everything below, with audience labels |
| Architecture · ADRs | Process boundaries, module map, invariants, durable design decisions |
| Deployment and validation · Troubleshooting | Getting it running and fixing it when it is not |
| HTTP and SDK contract · Authentication contract | Writing a client |
| System specifications · Configuration | Defaults, limits, environment variables, secrets |
| Security model · Lifecycle and data | Trust boundaries, persistence, backup, deletion, recovery |
| Production guide · Platform capability contract · Compatibility | Deploying beyond the local profile |
| Benchmarks · Benchmark report | Reproducible measurement method and results |
| Release policy · Supply chain · Changelog | Versioning, SBOM, signing, provenance, history |
Read CONTRIBUTING.md for the development setup and the checks a pull request must pass. Changes must preserve workspace ownership, gVisor isolation, scoped credentials, object-storage boundaries, and fail-closed behavior when the Control Plane is unavailable.
CI runs on every pull request and every push to main: unit and contract tests on
Python 3.11 and 3.14, a wheel build and entry-point smoke test, Console lint,
typecheck, i18n check and build, manifest and Helm rendering, all four container image
builds, a full-history Gitleaks scan, and a Trivy filesystem scan gated at
HIGH/CRITICAL.
- Questions, bugs, and feature requests: SUPPORT.md
- Security vulnerabilities: SECURITY.md — private reporting, not a public issue
- Decision ownership: GOVERNANCE.md and MAINTAINERS.md
- Community expectations: CODE_OF_CONDUCT.md
The repository source is released under the MIT License, and as of
2026-09-02 no image built from it carries a strong-copyleft component: the Control
Plane used to bundle a patched MinIO Client (mc, AGPL-3.0) and now talks S3 through
boto3 (Apache-2.0). Third-party components and the obligations that still attach to
images built from earlier revisions are in THIRD_PARTY_NOTICES.md.