Skip to main content

kubernetes-sigs/agent-sandbox runs isolated, stateful agent workloads as Sandbox objects, with warm pools (SandboxWarmPool) and claims (SandboxClaim) on top. It picks its isolation through the pod’s runtimeClassName, and k7d plugs in there with no upstream change: every Sandbox pod on k7 or k7-fc is its own k7d microVM — the native in-process VMM, or stock Firecracker under the jailer (Backends).

Tested with agent-sandbox v1.0.5 (every image and manifest digest-pinned in utils/agent-sandbox/pins.env) on k7d main after 0.9.0 — the integration, and the kubectl port-forward support it needs, landed after the 0.9.0 tarball.

Node prerequisites#

  1. k3s, pinned: sudo utils/install-k3s.sh (k3s v1.36.5+k3s1), or your own cluster.
  2. k7d, the shim and RuntimeClasses k7 and k7-fc: sudo ./packaging/install.sh --with-k3s from the release tarball (Installation). On a K7 cluster, k7 install --backend k7d,k7d-fc does the same.
  3. agent-sandbox, pinned and sha256-checked: utils/agent-sandbox/install.sh installs the controller, CRDs, extensions and the Go sandbox-router; utils/agent-sandbox/uninstall.sh removes all of it.

Then run the demo:

It creates a k7-fc warm pool and a claim, drives the upstream Python SDK through the router, and runs docker run hello-world inside a k7 sandbox.

Examples#

All in examples/agent-sandbox/. Every image is digest-pinned, and set CPU and memory limits — k7d sizes the VM from the container limits.

FileWhat
sandbox-k7.yamlBare Sandbox on k7: python-runtime, service: true, httpGet readiness probe
template-warmpool-claim-k7-fc.yamlSandboxTemplate (managed NetworkPolicy) + SandboxWarmPool (3) + SandboxClaim on k7-fc
template-docker-k7.yamlDocker-in-VM: in-guest dockerd via annotations, no privileged container
sdk_smoke.pyUpstream SDK (k8s-agent-sandbox==1.0.5.post1, hash-pinned): claim, run, file write / read

A bare Sandbox is an ordinary pod template with the RuntimeClass set:

A warm pool is a SandboxTemplate plus a SandboxWarmPool with replicas; a SandboxClaim adopts a booted VM from the pool without a restart (restartCount 0). networkPolicyManagement: Managed on the template gives the upstream default policy — ingress only from the sandbox-router, egress to public IPs only — and it is enforced on the pod IP of a k7d VM exactly as it would be for runc.

Docker-in-VM is two pod annotations on the template’s podTemplate; the workload keeps capabilities: {drop: [ALL]} and dockerd runs in the guest outside the container (Security — Docker as a guest service):

Support matrix#

From crates/k7d/tests/test_agent_sandbox.rs, single node, k3s v1.36.5+k3s1 (flannel, kube-router NetworkPolicy), 2026-10-07.

#agent-sandbox featurek7k7-fc
V1Cold Sandbox → Ready, podIPs, serviceFQDN✅✅
V2Python SDK through sandbox-router (run, files)✅✅
V3kubectl port-forward (SDK tunnel mode)✅✅
V5Template + WarmPool(3) + Claim; adoption keeps the VM, restartCount 0✅✅
V6Managed NetworkPolicy: public egress, private / link-local blocked, ingress only from the router✅ same as runc✅ same as runc
V7operatingMode: Suspended → Running: pod and VM gone, cold boot on resume✅✅
V7volumeClaimTemplates PVC data kept across suspend✅ (local-path via virtiofs)❌ refused loudly: “backend firecracker has no virtiofs”
V8shutdownTime + shutdownPolicy: Delete✅✅
V9httpGet readiness, exec liveness, startup probe under a burst of 4✅✅
V10Projected SA token rotation reaches the guest❌ gap (below)❌ gap
V11Docker-in-VM: overlay2 on ext4, hello-world, not privileged, CapEff 0✅✅
V13Out-of-band pause_vm: Ready flips False via the probe, back after resume✅✅
V14Upstream e2e TestRuntimeClassStartupComparison, TestRuntimeClassBurstRecovery✅✅
V14Upstream e2e TestRuntimeClassLifecycle⚠️ 1 of 3 runs: upstream test race✅

V4 (sandboxd + Go SDK) is not run: v1.0.5 publishes no sandboxd image. Warm-forking a Sandbox (V12) is the next increment; today a fork of a pod VM goes through the daemon API (fork_vm with pod_netns) or k7 fork on a K7 cluster.

Timings#

Intel Core Ultra 7 265 (20 threads), 62 GB, NVMe, Hetzner, single node, 2026-10-07, images already pulled:

Pathk7k7-fcrunc
Cold Sandbox → Ready, upstream TestRuntimeClassStartupComparison (no probe)0.84 s0.93 s—
Cold Sandbox → Ready, sandbox-k7.yaml (httpGet readiness, 5 s period)0.96–1.90 s5.91–7.17 s—
Warm SandboxClaim → Ready (pool of 3)0.05 s0.05 s0.05–0.06 s
Warm claim, upstream StartupComparison0.011 s0.011 s—

A warm claim adopts a running pool VM, so it costs the same as runc. With a readiness probe, cold start moves in steps of the probe period: k7-fc boots in about 1 s but misses the first probe.

Upstream TestRuntimeClassBurstRecovery with SANDBOX_POOL_SIZES=10,20,40 (two claims per pool slot, in batches):

PoolBackendPool fillWarm / grey / cold claimsWorst startAll readyClaims/s
10k71.52 s19 / 1 / 0 of 201.08 s1.67 s12.0
20k72.97 s30 / 1 / 9 of 403.54 s4.07 s9.8
40k75.02 s70 / 1 / 9 of 806.21 s7.33 s10.9
10k7-fc2.37 s20 / 0 / 0 of 200.94 s1.53 s13.0
20k7-fc2.62 s30 / 9 / 1 of 402.85 s3.36 s11.9
40k7-fc5.12 s70 / 2 / 8 of 805.75 s6.89 s11.6

Known gaps#

  • Kubelet volume updates do not reach the guest. ConfigMap, Secret, downwardAPI and projected volumes are packed into one read-only erofs image when the pod is created. kubelet rotates projected tokens and updates ConfigMaps on the host; the guest keeps the first copy. Use automountServiceAccountToken: false (the examples do) or tokens whose lifetime covers the sandbox. The default kube-api-access token stays valid for a year on clusters with --service-account-extend-token-expiration (the default), but it is never refreshed.
  • k7-fc has no virtiofs. hostPath and local-path PVCs fail Create naming the backend. Use k7 for volumeClaimTemplates on local-path storage.