# Agent sandboxes

Run an agent's own workload in a per-agent sandbox (a gVisor pod on Kubernetes, a Firecracker microVM, or a container) whose only way out is its own egress proxy with a default-deny allow-list.

Code mode already runs its child in a jail with no network at all (see [Limits and defaults](/docs/reference/limits-and-defaults)). An agent workload that must reach the network itself, such as a customer's agent container or a skill's script, runs in an **agent sandbox** instead: its own isolation boundary, and its own egress proxy next to it that it cannot get around.

Agent sandboxes are **off by default**. While they are off, and for every agent not listed, nothing changes: agents run as they always have, and no extra infrastructure runs. A sandbox exists only while its run does and is deleted when the run ends.

## Backends

The platform picks the strongest backend the host has, or the one you name. A backend you name that the host lacks refuses rather than running something weaker.

| Backend | Where | Isolation | How egress is fenced |
| --- | --- | --- | --- |
| `k8s` | A Kubernetes cluster, including k3s on Graviton | gVisor (`runsc`) through a RuntimeClass, where the cluster has one | Two NetworkPolicies: the agent pod reaches only its proxy's port, with no DNS and no metadata service; the proxy reaches DNS and public addresses only |
| `firecracker` | A host with `/dev/kvm` and firecracker-containerd | A microVM | nftables: the VM subnet reaches only the proxy's port |
| `docker` | A host with Docker | gVisor when the daemon has `runsc`, else runc | An `--internal` network whose only other member is the proxy container |

Graviton `t4g` and `c7g` instances are not metal and expose no `/dev/kvm`, so on the production k3s cluster the sandbox uses the `k8s` backend. gVisor runs on arm64.

The proxy is its own pod on Kubernetes, not a sidecar: a sidecar shares the agent's network namespace, and a NetworkPolicy cannot tell two containers of one pod apart.

## The egress proxy

Each sandbox gets its own proxy, and it refuses by default:

- A host that is not on the sandbox's allow-list is refused **before any DNS lookup**, so a sandbox cannot use name resolution to send data out.
- An allowed name that resolves to a private, loopback, link-local (the cloud metadata service) or other non-public address is refused, and the connection is pinned to the address that was checked.
- Only ports 80 and 443 unless an entry names its port (`db.example.com:5432`).
- The instance's [egress policy](/docs/govern/data-handling) still applies on top, under the agent's scope. A sandbox allow-list can narrow what your organization allows, never widen it.

Allow-list entries are host names or `*.suffix` wildcards. A bare `*` is refused.

## Turn it on

1. On Kubernetes, apply `deploy/k3s/sandbox/agent-sandbox.yaml` once (a namespace, a default-deny policy and the rights to create and delete sandbox pods). For gVisor, install `runsc` on the nodes and apply `runtimeclass-gvisor.yaml`.
2. Review the plan for an agent:

   ```bash
   curl -X POST "$BASE/api/admin/platform/agent-sandbox/plan" -H "Authorization: Bearer $TOKEN" \
     -d '{"agent": "helper", "command": ["python", "job.py"]}'
   ```

3. Turn it on and give the agent its allow-list. Adding a host loosens the sandbox, so it needs `governance.weaken` and a reason:

   ```bash
   curl -X PUT "$BASE/api/admin/platform/agent-sandbox" -H "Authorization: Bearer $TOKEN" \
     -d '{"enabled": true, "agents": {"helper": {"allow": ["api.vendor.com"]}}, "reason": "CHG-1042"}'
   ```

4. Run a workload: `POST /api/admin/platform/agent-sandbox/run` with `agent`, `command` and optional `env`. The response has the exit status, the output's tail and every egress decision.

Set `require_runtime_class: true` to refuse to run on a cluster without the gVisor RuntimeClass, instead of falling back to the node's runtime behind the same policies.

## Audit

Every launch, refusal, failure and teardown writes an `agent_sandbox.*` row. Every egress refusal writes `agent_sandbox.egress.deny`, and the first connection to each allowed host writes `agent_sandbox.egress.allow`. A configuration change writes `agent_sandbox.config.update` with before and after, marked as a weakening when it loosens anything.
