Skip to content

Isolation

An isolation class answers one question: how strong should the walls around this agent be? Your own signed code gets normal walls, code that pulls internet packages or runs LLM-written code gets a syscall-intercepting sandbox, and hostile input gets a full VM with its own kernel. You set the class in the agent spec. If the node cannot provide the wall you asked for, the run is refused, not quietly run somewhere weaker. On Kubernetes the class maps to a runtimeClassName on the pod (ADR 0009).

Isolation classesThe spec declares a class; the manager only runs it on a node that can provide it, and refuses rather than weakening it.
agent.yamlisolation: classSchedulerplaces the spawnRuntime managerchecks the classtrustedruncstandardgVisor (default)untrustedgVisor + egress-denyhostileKata microVMWorkload runsinside its classSpawn refusednever downgraded
Swipe sideways to pan.

1. No class declared

The spec leaves isolation unset, which resolves to standard.

  • Config
  • Scheduler
  • Trusted host code
  • Untrusted code

Pick the least-privileged class the workload can tolerate. Take the first row that matches:

  1. First-party, signed code: trusted.
  2. Loads packages from the internet, drives a browser, or runs user or LLM-generated code: untrusted.
  3. The input itself is adversarial: hostile.
  4. None of the above: standard, which is also what an unset class resolves to.
Class RuntimeClass Use when
trusted runc Signed first-party code only, on shared nodes. No untrusted package loading and no LLM-generated code path.
standard gvisor The default. Your own code on shared, gVisor-isolated nodes.
untrusted gvisor plus egress-deny Internet packages (PyPI, npm), a browser, or LLM-generated code. Adds an egress allowlist fence (a NetworkPolicy).
hostile Kata (kata-qemu or kata-fc) Adversarial workloads. A full-kernel microVM on a dedicated node pool with no co-tenancy.
wasm The configured Wasm RuntimeClass WebAssembly workloads.
devcontainer gvisor A long-lived workspace: a persistent pod and volume that survive across calls.

The RuntimeClass names are the defaults. The manager reads the actual names from its configuration.

Untrusted and hostile code never runs in a bare pod.

  • A node can satisfy untrusted only if it advertises the gVisor RuntimeClass.
  • A node can satisfy hostile only if it advertises a Kata RuntimeClass.
  • A node without the required RuntimeClass fails closed: the run is refused. untrusted and hostile are never downgraded to runc.
  • standard, devcontainer and wasm also refuse by default, but the manager can be told to run them on bare runc with PAL_ALLOW_RUNC_STANDARD=1. That flag never applies to untrusted or hostile.

The requirement is enforced twice. The manager refuses to build the job when the RuntimeClass is not configured, and the pod carries a node selector on the pal.dev/runtimeclass node label, which only nodes labelled as having the handler match.

If your cluster has not provisioned the gVisor and Kata RuntimeClasses, untrusted and hostile workloads are refused. That is correct, but you will see it. The data-plane installer provisions the classes, and a preflight check surfaces a gap before traffic.

Demo 02 (web-scraper) declares untrusted because it pulls third-party packages, and pairs it with an egress allowlist:

spec:
isolation: untrusted
network: allowlist_domain
egress_rules:
- pattern: "*.wikipedia.org"
http_methods: ["GET"]
rate_bps: 1048576 # 1 MiB/s

Where the runtime manager uses the Firecracker backend, the workload runs in its own microVM. By default the VM has a network device and the harness enforces the egress allowlist inside the guest. With PAL_EGRESS_MODE=broker on the manager (opt-in, Firecracker only) the VM has no network device, and egress leaves over vsock to a host broker. Kata and Kubernetes backends do not support broker mode. See Egress.

The Firecracker root filesystem is writable. Only the cert and spawn drive is mounted read-only. Inside the VM, the harness runs the workload, exec sessions and shell children as the workload uid, not its own, and withholds its own configuration variables from them.

Each workload Job is created with automountServiceAccountToken: false and a default-deny-egress NetworkPolicy. DNS on port 53 is the only exception. The policy is applied before the Job starts and removed with it. Pods run with the RuntimeDefault seccomp profile. The in-VM harness then enforces the declared allowlist on top, so the host policy and the guest allowlist are two independent fences.

Terminal window
# Throwaway kind + Calico cluster: egress deny (incl. 169.254.169.254), seccomp, cap-drop
make k8s-validate

The gVisor and Kata execution legs need a cluster with those RuntimeClasses installed and are skipped without one; GitHub-hosted runners cannot nest-virtualize. Optional cluster policies (Kyverno tenant baseline, cosign image verification, Cilium host egress) live in infra/helm/pal-data-plane/ and are off by default.

Related: Egress, Identity and secrets, Security model.