Execution boundary patterns: namespaces vs MicroVMs vs WASI
When you decide to let an AI agent run code, the first design question is not “which library?”. It is “where does the boundary go, and what does it enforce?” There are four serious answers in production today, and they enforce different things at different costs. This post walks through them and ends at where agentvfs sits.
Pattern 1 — Linux namespaces (containers)
The default. Docker, Podman, containerd, runc — all implementations of the same primitive: kernel namespaces and cgroups isolating one process tree from another. Inside the namespace, the process sees its own filesystem (often an OCI image), its own network stack, its own PID space. The kernel is shared.
What it enforces. Strong process boundaries, decent resource caps via cgroups, network isolation, filesystem isolation by way of bind mounts and overlay layers.
What it leaves on the table. The kernel is shared, so kernel bugs are blast-radius bugs. Persistence is not a primitive; layers and volumes are the workaround. Per-task forks of a workspace require image manipulation or docker commit, which is too heavy for an agent loop that wants a fork per attempt. Policy at the command level is not built in; you bring seccomp or AppArmor.
When to pick it. When you are running code from many tenants and want process isolation that has been hardened for a decade. Especially when you already operate a container platform and want the agent to slot in.
When to skip it. When the boundary you actually need is at the top-level command rather than the kernel ABI, and when you want the workspace itself — not the process — to be the unit of forking and rollback.
Pattern 2 — MicroVMs
Firecracker, Cloud Hypervisor, Kata Containers. The idea: each task gets a real VM, but a tiny one — boot times in tens of milliseconds, memory in tens of megabytes — built on KVM rather than full QEMU.
What it enforces. A real hypervisor boundary. The guest kernel is not the host kernel. A kernel bug in the guest does not give you the host. Network, storage, and devices are virtualized through a tiny device model that is far smaller than QEMU’s.
What it leaves on the table. You are now operating VMs, even if they boot quickly. Disk image management. Networking inside the guest. The persistence and forking primitives of a workspace are still your problem to provide. Latency to spin a sandbox is small but nonzero, which matters for tight agent loops.
When to pick it. When the threat model is “this binary might be hostile and a kernel bug is in scope”. When you are building a multi-tenant code-execution service and need per-customer isolation that survives the next CVE.
When to skip it. When the threat is “the agent might propose a stupid command” rather than “the binary will attack the kernel”. MicroVMs are overkill for the failure mode you actually have, and the per-task primitives — fork, checkpoint, change summary — are still on you to build.
Pattern 3 — WASI / WebAssembly
Wasmtime, WasmEdge, the WASI ecosystem. The boundary is the WebAssembly runtime: a sandbox by design, capabilities granted explicitly through the WASI interface.
What it enforces. Memory safety by construction. Capability-based access — the module gets exactly the file handles, sockets, and clocks you grant. No syscalls, only WASI imports. Reproducibility is excellent; the same Wasm binary runs identically on any compliant host.
What it leaves on the table. The ecosystem. Most CLI tools that agents actually call — git, cargo, npm, python — are not Wasm binaries. The set of useful Wasm tools is growing but small relative to the native CLI surface. WASI’s filesystem story has progressed but is still less mature than POSIX. For an agent that wants to run cargo test, WASI is the wrong layer.
When to pick it. When you control the code surface. Plugin systems, sandboxed embedding, executing untrusted JavaScript or guest logic where you compile the world to Wasm anyway.
When to skip it. When the agent’s job is to drive existing CLI tooling. The mismatch between “what tools exist as Wasm” and “what tools agents call” is the dominant cost.
Pattern 4 — Workspace runtime (agentvfs)
The boundary is the top-level command and the workspace it operates on, not the kernel ABI and not the WebAssembly runtime.
What it enforces. A policy step before the command runs. An automatic checkpoint of the workspace if policy says so. A FUSE-mounted vault as the working directory, so the command runs against the agent’s workspace rather than the host filesystem. A timeout with SIGTERM-then-SIGKILL escalation. A structured ExecutionEnvelope with stdout, stderr, exit code, duration, the policy decision, and the list of changed files.
What it leaves on the table. Syscall-level isolation. agentvfs does not try to observe every subprocess; the architecture document calls this out as the explicit non-goal. If the command spawns a hostile child, the proxy does not catch it.
When to pick it. When the agent is yours, the threat is misbehavior rather than malice, the workspace is something the agent has to come back to, and you want the boundary at the line the agent is actually crossing — the top-level command.
When to skip it. When you need defense against hostile binaries; layer a container or microVM under agentvfs in that case.
A comparison the docs back
Below is how the four patterns line up on the dimensions that matter for an agent loop. The “agentvfs” column is grounded in the agentvfs README and docs/architecture.md; the others are public properties of those projects.
| Dimension | Namespaces (Docker) | MicroVM (Firecracker) | WASI (Wasmtime) | Workspace runtime (agentvfs) |
|---|---|---|---|---|
| Kernel shared with host | Yes | No | N/A (Wasm runtime) | Yes |
| Threat model | Untrusted process | Untrusted binary | Untrusted guest module | Honest-but-fallible agent |
| Workspace persistence | Volumes / layers | Disk image | Granted via capabilities | Single-file vault |
| Per-task fork primitive | Image rebuild | New VM | New module instance | vault fork in milliseconds |
| Rollback primitive | Discard container | Discard VM | Discard module | checkpoint save / restore |
| Policy on top-level command | External (OPA / scripts) | External | Capability set you grant | PolicyEngine built in |
| Changed-files report | docker diff | None native | None native | ChangeSummary in ExecutionEnvelope |
| Runs standard CLI tooling | Yes | Yes | Limited (Wasm-compiled only) | Yes (FUSE-mounted vault) |
| Boot cost | Hundreds of ms | Tens of ms | Sub-ms | None (in-process runtime) |
They compose
The patterns are not exclusive. The honest production setup for an agent that ships code in front of users is usually two layers: a workspace runtime for the agent-aware concerns (vault, fork, checkpoint, policy, change summary) and a process- or VM-level boundary for the worst-case threats.
agentvfs is designed to be the inner layer in that pair. The boundary it draws is cheap to evaluate, holds across a real agent loop, and gives you the structured output an agent harness needs. The boundary it does not draw — the syscall and the hypervisor — is the one you compose underneath when your threat model requires it.
Picking yours
If the agent runs your code, the threat is misbehavior, and the workspace is the unit of work: workspace runtime, layered under whatever process isolation your platform already gives you.
If the agent runs untrusted code from arbitrary users: namespaces or microVMs, with a workspace runtime sitting on top to give the agent loop fork, checkpoint, and change summary primitives.
If the code surface is one you control end-to-end and can compile to Wasm: WASI, accepting the smaller ecosystem in exchange for the better isolation story.
The wrong answer is to pretend the layers are interchangeable. They enforce different things at different costs. Pick the one whose threat model matches yours, and compose the rest.