User-defined Playground

OpenClaw on K3s with agent-sandbox + gVisor Playground

A 3-node K3s cluster with gVisor (runsc), the kubernetes-sigs/agent-sandbox controllers, and the OpenClaw + gVisor example pre-deployed. OpenClaw's Control UI is available as a dedicated tab.

Startup configuration
dev-machine
cplane-01
node-01
node-02
OpenClaw on K3s with agent-sandbox + gVisor playground: A 3-node K3s cluster with gVisor (runsc), the kubernetes-sigs/agent-sandbox controllers, and the OpenClaw + gVisor example pre-deployed. OpenClaw's Control UI is available as a dedicated tab.

Overview

This playground boots a 3-node K3s cluster with gVisor, the agent-sandbox controllers, and the upstream OpenClaw + gVisor example already deployed. Everything is baked into the VM snapshots — no installation steps on start.

Open the OpenClaw tab to reach the Control UI. Give the cluster ~60s after boot for K3s to settle and the sandbox pod to come back; if the tab shows an error page, just reload it.

What's running

Cluster — K3s v1.36 on cplane-01, node-01, node-02. dev-machine has kubectl and the cloned repo at ~/agent-sandbox.

gVisorrunsc and containerd-shim-runsc-v1 are installed on all three nodes and wired into K3s's containerd via a drop-in:

/var/lib/rancher/k3s/agent/etc/containerd/config-v3.toml.d/runsc.toml

A RuntimeClass/gvisor (handler runsc) is registered. Verify the sandbox really runs on the gVisor kernel:

kubectl exec openclaw-sandbox-claim -- head -1 /proc/version
# Linux version 4.19.0-gvisor ...

agent-sandbox v0.5.6 (core + extensions) in the agent-sandbox-system namespace.

The example from examples/openclaw-gvisor-sandbox: a SandboxTemplateSandboxWarmPool (1 pre-warmed spare) → SandboxClaim that adopts a sandbox from the pool. The resulting pod, openclaw-sandbox-claim, runs OpenClaw 2026.3.23 as uid 1000 with all capabilities dropped and a 2Gi PVC mounted at /workspace/.openclaw.

kubectl get sandboxtemplate,sandboxwarmpool,sandboxclaim,sandbox,pods

Exposure — a NodePort Service (openclaw-gateway, 18789 → 30789) that the OpenClaw tab points at. Note that kubectl port-forward does not work under gVisor: the app binds inside gVisor's user-space netstack, so the host kernel sees nothing listening in the pod's netns. Always go through a Service.

Using OpenClaw

1. Paste the gateway token. It's stored on dev-machine:

cat ~/openclaw-token.txt
# or read it straight from the running pod:
kubectl exec openclaw-sandbox-claim -- printenv OPENCLAW_GATEWAY_TOKEN

2. Approve the browser pairing. OpenClaw uses a zero-trust device authorization policy, so the first connection from a new browser shows "pairing required". From dev-machine:

~/openclaw-pair.sh

It finds the pending request and approves it — then refresh the tab. The pairing lives on the PVC, so it survives pod restarts.

3. Add a provider API key. The dashboard loads without one, but chat will fail. This part is destructive, because the SandboxClaim snapshots the template's pod spec at adoption time and never re-syncs — you have to tear down claim + warm pool + template together:

cd ~/agent-sandbox/examples/openclaw-gvisor-sandbox

kubectl create secret generic openclaw-provider-keys \
  --from-literal=ANTHROPIC_API_KEY="sk-ant-..."

# uncomment the ANTHROPIC_API_KEY env block in openclaw-template.yaml, then:
kubectl delete -f openclaw-claim.yaml -f openclaw-warmpool.yaml -f openclaw-template.yaml

TOKEN="$(cat ~/openclaw-token.txt)"
sed "s/dummy-token-for-sandbox/${TOKEN}/g" openclaw-template.yaml | kubectl apply -f -
kubectl apply -f openclaw-warmpool.yaml
kubectl apply -f openclaw-claim.yaml

Deleting the template drops the PVC, so you'll re-pair the browser afterwards. Swap the env var name for other providers (OPENAI_API_KEY, GEMINI_API_KEY, …), or use envFrom: [secretRef: {name: openclaw-provider-keys}] for several at once.

Two deviations from the upstream example

1. Origin allowlist is wide open. Every play gets a different public hostname, and under --bind=lan OpenClaw refuses requests from unknown origins, so openclaw-config.yaml ships with:

{"gateway": {"controlUi": {"allowedOrigins": ["*"]}}}

That disables origin allowlisting — the gateway token and device pairing are the only gates left. To tighten it for your play, run:

~/openclaw-allow-origin.sh https://<your-play-host>

which pins the allowlist and restarts the pod (the PVC, and therefore the pairing, survives).

2. The template's NetworkPolicy is relaxed. The SandboxTemplate generates a default policy that only allows ingress from the sandbox-router pod and blocks all private-range egress — which dropped NodePort traffic arriving from other nodes and broke cluster DNS. spec.networkPolicy on the template now additionally allows:

  • ingress on TCP 18789 from 0.0.0.0/0
  • egress to kube-dns on port 53 (so the agent can resolve provider APIs)
kubectl get netpol openclaw-template-network-policy -o yaml

Egress to private ranges (10/8, 172.16/12, 192.168/16, 169.254/16) is still blocked, so the sandbox cannot reach the cluster's own API server or the node network — only the public internet.

Persistence model

PVCs are named <vctName>-<sandboxName> and owned by the Sandbox CR:

  • delete the pod → the controller respawns it and reattaches the same PVC, workspace data survives
  • delete the Sandbox (or the claim, with shutdownPolicy: Delete) → the PVC is garbage-collected and the data is gone
Start
Settings