# Sandboxing an AI Agent: Containers, microVMs, and WASM

*Published 2025-09-30 · Dmitry Shirokov · shirokoff.ca/blog/agent-sandboxing-tool-isolation · as of late 2025*

The demo that changed my design wasn't an attack. A colleague's coding agent, asked to clean up a test directory, decided the tidiest reading of that request included a parent directory. It ran as the developer's user, with the developer's credentials in the environment, and took four seconds. **You don't sandbox an agent because a model is malicious — you sandbox it because a probabilistic system with real credentials will eventually take an action inside its permissions that you wouldn't have authorized, and prompt injection lets an attacker aim that behaviour.**

## Three capabilities to contain

| Capability | Worst case | Contained by |
|---|---|---|
| **Code execution** | Arbitrary code with your process's privileges | Runtime isolation: container, gVisor, microVM, WASM |
| **Filesystem & secrets** | Reads a credential, writes out of scope, deletes a parent dir | Per-run writable scope, read-only rest, no ambient creds |
| **Network egress** | Exfiltrates what it just read; finds an internal service | **Deny-by-default egress + allowlist** — the missing control |

Most teams invest in the reverse order. **Egress is where damage becomes irreversible**: a leaky runtime with strict egress is embarrassing; a tight runtime with open egress is a breach waiting for a prompt.

## Four isolation levels

**Same process** — stops nothing; every credential and internal host is in scope. Fine for typed read-only tools against your own API (that's an authorization question, not an execution risk). The mistake is running an *interpreter* here.

**Containers** — namespaces + cgroups + seccomp. Stops ambient filesystem access, unbounded resources, casual mistakes. Doesn't stop a kernel bug (shared host kernel) or a misconfiguration (privileged, host network, docker socket). Right default for *your* code with model-supplied arguments; insufficient for code the model wrote.

**gVisor / Firecracker microVMs** — two answers to "don't share the host kernel": a user-space kernel in front of syscalls, or a minimal VM per run booting in tens of milliseconds (the model behind several public code-execution services — a strong signal about where the bar sits). Cost: syscall performance, startup latency, per-run memory, real ops work. Reach for it **the moment your agent executes generated or third-party code** on infrastructure you care about.

**WebAssembly** — capability-based: no filesystem, sockets or clock unless granted. Best startup in this list (per-call sandboxing becomes practical), tiny footprint, deny-by-default that's easier to reason about than seccomp. Cost: the ecosystem — native extensions, system libraries and subprocesses are constrained. Choose it for computational tools, not integrative ones.

## The controls that matter more than the runtime

**Egress: deny by default, allowlist by name.** A sandbox with unrestricted outbound access isn't one. Route tool traffic through a proxy, log and alert on blocks. This turns "the model read a file it shouldn't" into a log line, because the read only matters if the data can leave.

**Credentials: scoped, short-lived, never ambient.** Tools get a token minted for that tool, tenant and operation, expiring in minutes — not whatever the process holds. Ordinary least privilege, made urgent because the thing choosing tools is a probability distribution.

**Filesystem: one writable scope per run**, read-only otherwise, destroyed after. **Resource caps**: CPU, memory, disk, wall-clock, plus a step cap — agents fail by looping, and an unbounded loop is a self-inflicted DoS, usually on the bill.

Write the policy as **data, one entry per tool** (isolation level, egress allowlist, scoped secrets, writable path, limits) so "what can this tool reach?" is a file a reviewer can read.

## Three ways a "sandbox" turns out not to be one

**The docker socket** mounted so the agent can manage containers — it can now start a privileged one. **The shared workspace** mounted across runs or tenants for caching — one run reads another tenant's files, defeating runtime isolation with a volume mount. **Ambient cloud credentials** — a reachable instance metadata service hands the sandbox the VM's role, putting a hole the shape of your cloud account in your filesystem boundary. Block metadata from tool sandboxes explicitly, and test it. In all three the runtime was fine; the configuration was the vulnerability.

## How much is enough?

It turns on one question: *who wrote the code that runs?* Your code, typed reads, no interpreter → in-process (spend effort on authorization and argument validation). Your code with side effects → container + writable scope + caps + egress allowlist (most enterprise tools). **Model-written or third-party code → microVM or gVisor**, and don't negotiate that down because a container was easier. Many small pure computations → WASM.

## Carry-away

Choose isolation per tool, not per application. Invest first in the two controls that aren't runtimes — deny-by-default egress and scoped short-lived credentials — because they're cheaper than any runtime decision and contain more damage. Add a fresh writable directory per run, read-only everything else, hard resource and step caps. And keep the policy as reviewable data: the isolation you can explain in a review is the isolation you actually have.

*Related: [What MCP standardizes](https://shirokoff.ca/blog/model-context-protocol) · [Data contracts in practice](https://shirokoff.ca/blog/data-contracts) · [Knowledge graphs and the semantic web](https://shirokoff.ca/blog/knowledge-graphs-semantic-web)*
