Skip to content

Latest commit

 

History

127 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sandbox Platform

License: MIT

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.

Try it

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 description

That 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.


Why this instead of the alternatives

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_DRIVER accepts only gvisor; 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_tenant returns True unconditionally when no store is configured, which would make an in-memory contract test useless. tests/test_api_authorization.py therefore 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 a 404 arrives 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_AUTH declares 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.py fails the build if the api.py dispatch table, the OpenAPI document, and ROUTE_AUTH ever 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.


Look at the surfaces

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 description

One command to see the point

From a fresh clone, this is the shortest path from prerequisites to a real gVisor command and durable Workspace proof:

make quickstart

It 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.

Open the Console

After make quickstart succeeds, keep the Console forward running in one terminal:

make console-forward

Open http://127.0.0.1:18081. In another terminal, print the local login key:

make --no-print-directory dev-token

Paste 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.

Run the full local cluster

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.

Prerequisites

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.

Bring it up

make doctor
make bootstrap
make up-local

The 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 remain

Scale-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.

First command

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.

Tear it down

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 state

down-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.


Using it

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.

Architecture

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.

Request tracing

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.


Deployment profiles

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.


Security boundary

  • 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.


Known limitations

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.


Documentation

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

Contributing, support, and license

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.

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.

About

gVisor-isolated Kubernetes sandbox with an HTTP API, SDK and MCP bridge

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages