Run AI-Written Code Without Fear: Kubernetes' Own Agent Sandbox, in One YAML
This is not another container sandbox runtime — it is a missing layer of the Kubernetes API. Deployments own replicated stateless replicas; StatefulSets own a numbered, stable set of Pods. AI agent runtimes sit in the gap between the two: one container, long-lived, with its own hostname and its own disk, that can be put to sleep and woken up. agent-sandbox turns that gap into a CRD called Sandbox, and eight lines of YAML is the whole entry cost.
What it is, and what it deliberately isn’t
Is: a Sandbox CRD plus controller for isolated, stateful, singleton workloads — with three extension resources (SandboxTemplate, SandboxClaim, SandboxWarmPool) that turn bulk provisioning into something you can actually ship.
Isn’t: an isolation runtime. The README states the positioning flatly — “Agent Sandbox is a sandbox orchestrator. It delegates low-level container isolation to secure Sandbox Runtimes.” It never sandboxes anything itself; it fills in runtimeClassName on a Pod and lets gVisor, Kata or Firecracker do the actual containing.
That boundary is worth praising rather than dismissing. The usual failure mode in this category is welding orchestration to a runtime, ending up with something that can neither match a dedicated runtime’s isolation nor keep pace with Kubernetes API evolution. Here the choice is inverted: the API layer can swap implementations, and the implementation layer can swap APIs, with RuntimeClass — a slot Kubernetes already had — as the joint.
- Owner Kubernetes SIG Apps (kubernetes-sigs)
- Core API
agents.x-k8s.io/v1beta1·Sandbox - Latest release v1.0.7 (2026-10-15), one patch per week for five weeks
- Language / license Go · Apache-2.0
- Clients Python (
k8s-agent-sandbox) · Go · TypeScript in progress - Isolation source
runtimeClassName→ gVisor / Kata / Firecracker - Entry cost one
kubectl apply, 8 lines of YAML - Started repo created 2025-08, into v1.0.x during 2026
What the controller actually builds
The official minimal example really is that small:
1 | apiVersion: agents.x-k8s.io/v1beta1 |
spec has three parts: podTemplate (required), volumeClaimTemplates (optional, immutable after creation), and service (whether to also create a headless Service). The controller creates the Pod, the Service if asked, PVCs from the templates, then writes the outcome back to status: serviceFQDN, podIPs, nodeName, plus four condition types.

The left side is the declarative API and controller (lifecycle), the band below is the isolation layer it does not implement, the right side is where commands actually travel. All three layers are independently replaceable.
Two details betray that the authors write Kubernetes for a living.
First, there is no separate “Running” condition. A code comment spells it out: spec.operatingMode: Running is user intent, Ready=True is observed fact, and whether the Pod is up is absorbed into the Ready reason (DependenciesNotReady). That distinction kills a classic ambiguity — a resource that reports “running” while being unreachable.
Second, the InvalidConfiguration reason exists at all. A Sandbox name may be up to 253 characters, but the Service derived from it must be a DNS-1035 label capped at 63. Rather than hot-looping on a create that can never succeed, the controller reports a permanent failure and stops requeuing. Reserving a distinct reason for an error no retry can fix is the kind of thing you only do after being paged by it.
Hibernation is real, but it is not a checkpoint
operatingMode flips between Running and Suspended, and that is the whole “sleep and wake” story: the Pod is deleted while the Sandbox object and its PVCs survive, and resume re-attaches the same volume cleanly.

Running ⇄ Suspended is driven by operatingMode; shutdownTime expiry is a separate path. Note that podIPs / nodeName are cleared on suspension rather than left stale.
Two limits need stating, because they are where people get burned:
Disk survives; memory does not. Suspension preserves the filesystem only — processes, open sockets, and interpreter state are gone. Freezing memory too is the PodSnapshot line of work (there is a podsnapshot-golden-warmpool example and regression tests), not this field.
“Resume on traffic” is not shipped. The README’s automatic resume on network connection sits under Desired Sandbox Characteristics — a goals list. On the roadmap, Auto Suspend/Resume and Scale to Zero are both still marked Planned. What you can use today is patching a field yourself.
Warm-pool claims: an ownership change, not a Pod creation
For agent loops and RL evaluation, cold-start seconds are the real cost. The extensions address exactly that: SandboxTemplate holds the blueprint, SandboxWarmPool keeps replicas Sandboxes already Ready, and a SandboxClaim adopts one.

The fast path only moves ownerReference from pool to claim — no Pod is created. Only the slow path cold-starts from the template.
The adoption never touches the scheduler: pick an unclaimed warm Sandbox, swap its owner from the pool to the claim, label it agents.x-k8s.io/launch-type: warm, done. The project describes burst throughput around 300 sandboxes per second, with a roadmap target of pulling end-to-end claim latency from 200ms toward 100ms and then 50ms. Numbers like that do not come from a faster scheduler — they come from replacing “create” with “relabel.”
Three catches on that fast path, all documented in field comments:
envandvolumeClaimTemplateson a claim force a cold start. Pool Pods lack your variables and volumes, so they must be rebuilt. The comment says so directly: “adding this field means the Sandbox will always be cold-started”. Keep configuration in the template if you want the warm path.- Template changes propagate conservatively.
updateStrategydefaults toOnReplenish: stale Sandboxes are not deleted immediately — they are replaced when you delete them manually or when a claim adopts one. OnlyRecreatetears down unclaimed stale Sandboxes at once. Neither strategy ever touches an already-claimed Sandbox. - Refill traffic can exhaust API-server fairness. The project ships
docs/apf-insulation.mdteaching you to separate claim adoption from bulk refill withFlowSchemaandPriorityLevelConfiguration. That document existing is itself evidence someone hit the wall at volume.
Data plane: Router for HTTP, sandboxd for execution
Creation goes through the API; running commands goes another way. sandbox-router is a stateless reverse proxy that fans traffic out to thousands of sandbox Pods using X-Sandbox-ID / X-Sandbox-Namespace / X-Sandbox-Port — without creating a Service per sandbox, which is what makes it scale. Resolution order: explicit Pod IP > informer cache > DNS fallback.
Commands land in sandboxd inside the Pod, defined by KEP-539.2 (runtime interface standardization), which deliberately splits protocols:
1 | sandboxd |
Processes use gRPC because Start is a long-lived server-streaming RPC that must keep emitting stdout/stderr; files use REST so bytes move as raw application/octet-stream with no generated stubs required. SDKs depend on these two wire contracts rather than on the sandboxd binary — which means a third-party runtime is compatible as soon as it implements the same proto and OpenAPI surface.
One trap that bites immediately: the Router accepts HTTP/2 from clients but disables HTTP/2 on upstream connections, so it cannot carry gRPC. The user guide is blunt: “it is not a complete sandboxd transport.” To use ProcessService you dial the Pod IP directly or kubectl port-forward — the Router will not relay it for you.
Browsers add another wrinkle: navigations, <iframe src> and new WebSocket(url) cannot set custom headers — a platform limit, not an implementation gap. The Router’s answer is --path-routing-prefix, moving identity into the URL (/router/<ns>/<id>/<port>/...). Two related decisions are quietly correct: upgraded connections are exempt from --proxy-timeout (otherwise code-server’s WebSocket gets cut at exactly three minutes with close code 1006), and Origin is stripped on upgrades (since Host has already been rewritten, keeping it trips the backend’s CSRF check).
Security: three defaults say more than “supports gVisor”
The real risk with LLM-generated code is not the absence of a sandbox — it is a sandbox with a bypass left open. Three concrete things are done here:
automountServiceAccountTokendefaults to false when provisioned through aSandboxTemplate. A compromised Pod gets no ServiceAccount token and therefore no path to the API server. BareSandboxresources do not inherit this default; you needValidatingAdmissionPolicy.- Managed NetworkPolicy. Templates generate one shared policy by default: ingress only from the Router, egress only to the public internet, internal RFC1918 space and cloud metadata endpoints blocked. The trade-off is stated too: default-deny ingress also blocks sidecar ports, so health checks fail unless you explicitly allow them.
- System-reserved label filtering. The easiest one to miss and among the most serious. A Service selects Pods by the
agents.x-k8s.io/sandbox-name-hashlabel — so if tenants could set that key inpodTemplate.metadata.labels, tenant B could label its own Pod with tenant A’s hash and have A’s traffic delivered to itself. The controller treats every key underagents.x-k8s.io/andextensions.agents.x-k8s.io/as reserved, filters user metadata before merging, and writes the hash label itself afterwards. The threat model also concedes the hash is still FNV-1a, with a truncated SHA-256 tracked separately.
The Router keeps its own ledger: it strips Authorization before forwarding (otherwise any sandbox could use the caller’s credential against the API server) and validates X-Sandbox-Pod-IP against restricted address classes, blocking 169.254.169.254. But the default authorizer is AllowAll for Python-client compatibility, which makes that IP-class check the only SSRF defense standing — and the project recommends a custom authorizer for production.
Who should use it, who should wait
Reach for it
- Untrusted generated code inside your cluster: agent code interpreters, browser sandboxes, coding agents —
aiderandopenhandsexamples exist. - RL and evaluation loops: SWE-bench, R2E-Gym and similar high-churn workloads, where warm-pool adoption is the decisive advantage; Gymnasium and Nemo-Gym integrations are in-tree.
- Persistent cloud dev environments: one disk-backed, stably-named, suspendable Jupyter or code-server per person.
- Kubernetes-native governance: RBAC, NetworkPolicy, HPA and GitOps all apply directly, with no second scheduling system to learn.
Wait, or look elsewhere
- Two sandboxes on a laptop: the cluster is the cost. E2B, Modal or Daytona get you moving faster.
- Memory snapshots or resume-on-traffic: one isn’t in the API yet, the other is still roadmap.
- Any compatibility requirement: releases are v1.0.7 but the API is v1beta1, and
v1alpha1plus its conversion webhooks were deleted outright — no back-compat promise. - Many sandboxes per Pod: Planned; today it is one-to-one.
- Expecting isolation to appear by itself: omit
runtimeClassNameand you get an ordinary runc container.
Verdict
Rating: ★★★★☆ (4 / 5)
The most important thing this project got right is refusing to become a runtime. Orchestration lives in the API, isolation in RuntimeClass, execution in two sandboxd wire contracts, ingress in the Router — four independently replaceable layers, which is precisely the shape that lets a component in this ecosystem survive. Replacing “create” with “adopt” in the warm pool is the other judgment worth naming: latency you buy by removing work, not by tuning.
The point lost is time: auto-suspend/resume, scale-to-zero, template rolling updates, the MCP server and the TypeScript SDK all still sit on the roadmap, and the API remains v1beta1 with no compatibility commitment. If your agent platform ships this year, you will write that layer yourself.
If you already run a Kubernetes cluster and are genuinely looking for somewhere to put a pip install that an LLM just wrote and nobody read twice — this is the lowest-friction recommendation I have written for that scenario: eight lines of YAML, and go.