Agents in the Sandbox: Containers, gVisor and MicroVMs for Code You Do Not Trust
Notes from the LangChain Community London x Focused.io evening on agent sandboxes: why your agent is the attacker in your threat model, how containers, gVisor and Firecracker microVMs actually differ at the kernel boundary, and why the network and credential layer matters as much as the box.
14 Nov 2026
On Thursday 12 November I went to "Agents in the Sandboxes", an evening run by LangChain Community London with Focused.io at Fora on Chancery Lane. The question on the invite was blunt: what happens when an AI agent goes rogue? Three talks tried to answer it from three angles. Agustin Rodriguez, a Senior Agent Engineer at Focused.io, gave a practical introduction to why agents need a sandbox at all. Daniel Kneipp, a software engineer at LangChain, walked through how LangSmith Sandboxes are built, with a microVM per agent, a host that treats every guest as hostile, and an outbound proxy that lets the agent use credentials it never sees. Abd Bastola from BCG closed with threat modelling for agents, covering what is still exposed once you have a sandbox.
The timing was hard to miss. The next afternoon LangChain announced a $300 million Series C at a $3 billion valuation, led by CapitalG, and said more than 7,000 organisations now rely on LangSmith. A year ago, in October 2025, the company raised $125 million at $1.25 billion. Investors are not paying 2.4 times more for a prompt library. They are paying for the layer that runs, observes and governs agents in production, and the hard part of that layer is exactly what the evening was about: letting a model execute code and call tools without handing it your infrastructure.
"Use a sandbox" is where most advice stops, and it is not enough. The three common isolation options make very different promises, and the gap between them is where real incidents live.
Your agent is the attacker
Classic application security assumes the code is yours and the input is hostile. Agents flip that. The code is written at runtime by a model, and the model's instructions are partly written by whatever it last read: a web page, a PDF, a GitHub issue, a support ticket. Prompt injection means any of those sources can steer the agent. OWASP puts prompt injection first in its Top 10 for LLM applications, and lists excessive agency (giving the model more permissions and autonomy than the task needs) alongside it.
So the honest threat model for an agent that can run code is this: assume the code is malicious, because sometimes it will be. Not because the model is evil, but because it is very good at following instructions, including ones planted by somebody else. Simon Willison's "lethal trifecta" is the cleanest summary I know. An agent that has access to private data, is exposed to untrusted content, and can communicate externally can be tricked into leaking that data. Remove any one of the three and the attack falls apart. A sandbox is mainly a tool for controlling the second and third legs: what the generated code can touch, and where it can send things.
That splits the problem into three questions that are worth keeping apart. The first is compute isolation: if the code tries to break out of its environment, what stops it reaching the host or other tenants? The second is network and credential isolation: if the code stays inside the box, what can it reach, and which secrets can it use or steal? The third is lifecycle: how long the environment lives, what persists, and what the next run inherits. Most debates about sandboxes only cover the first question. The talks were right to spend as much time on the second.
Option one: containers share the kernel
A container is not a security boundary in the way a VM is. It is a normal Linux process with a restricted view of the system. Namespaces (pid, mount, net, user, uts, ipc, cgroup) give it its own process tree, filesystem view and network stack. Cgroups cap its CPU, memory and pids. Seccomp filters which system calls it may make, capabilities strip root's powers, and AppArmor or SELinux add mandatory access control on top.
All of that is enforced by one thing: the host kernel. Every syscall the agent's code makes, open, mmap, ioctl, io_uring_enter, goes straight into the same kernel that runs everything else on the machine. The Linux syscall surface is several hundred calls wide and the kernel ships privilege escalation bugs every year. If the generated code finds one, the namespace walls do not matter, because the code is now running as the kernel.
You can shrink that surface. A tight seccomp profile is the single most useful hardening step for a container that runs untrusted code:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "openat", "close", "fstat", "mmap", "munmap",
"brk", "rt_sigaction", "rt_sigreturn", "exit_group", "futex",
"clock_gettime", "getrandom", "execve", "wait4", "clone3"],
"action": "SCMP_ACT_ALLOW"
}
]
}
Pair it with a non-root user, user namespaces, a read-only root filesystem, --cap-drop=ALL, no host mounts and --network=none by default, and you have a reasonable box for code you mostly trust: your own tools, a linter, a test runner on your own repository. The trouble is that an agent writing arbitrary Python needs a wide set of syscalls to be useful, so the allowlist grows, and with it the attack surface. For genuinely untrusted, prompt-injectable code from many tenants on shared hardware, a shared kernel is the wrong foundation. Containers are packaging, not isolation.
Option two: gVisor puts a kernel in user space
gVisor, from Google, keeps the container interface and replaces what sits underneath it. Its core is the Sentry, an application kernel written in Go that implements the Linux syscall interface itself. When the sandboxed process makes a syscall, the Sentry intercepts it and handles it in user space. Only a small, filtered set of host syscalls is ever made by the Sentry itself, so the host kernel sees a much narrower surface. File access goes through a separate component, the Gofer (or a directly mounted filesystem in newer configurations), so the Sentry does not need broad host filesystem access either.
How the interception happens depends on the platform. The old ptrace platform was portable but slow. The KVM platform uses hardware virtualisation features to trap syscalls. The default today, systrap, uses seccomp to trap syscalls and hands them to the Sentry with much lower overhead than ptrace. Either way the shape is the same: the agent's code talks to a Linux written in a memory-safe language, and that Linux talks to the real one through a keyhole.
The big win is that gVisor ships as runsc, an OCI runtime, so Docker, containerd and Kubernetes (as GKE Sandbox) can use it by swapping the runtime while your images, registries and tooling stay the same. It starts fast and stays light because there is no guest kernel to boot, and the host attack surface is far smaller than a plain container's.
The price shows up in two places. Syscall-heavy work gets slower: CPU-bound code runs at near-native speed, but every syscall is now a trip through the Sentry, and agents that compile code, walk large trees of small files or do a lot of networking feel it. Then there are compatibility gaps. gVisor implements most of Linux, not all of it, and some ioctls, parts of /proc and /sys, and some newer kernel interfaces are partial or missing. If an agent's toolchain hits one of them, you find out in production.
gVisor is a strong choice when you want container ergonomics and a real step up in isolation, and your agents run a known toolchain you can test against it. Google Cloud Run and GKE Sandbox run on it, which tells you it holds up at scale.
Option three: Firecracker gives every agent its own kernel
A microVM goes all the way: every sandbox gets its own guest kernel, running on hardware virtualisation (KVM on Linux). The agent's code can make any syscall it likes, including exploiting a bug in the guest kernel, and it is still inside a virtual machine. To reach the host it has to break the hypervisor, which is a much smaller and much more scrutinised target than the Linux syscall table.
Firecracker, which AWS built for Lambda and Fargate, is the reference design here, and it is what the LangChain talk described for LangSmith Sandboxes. It is a virtual machine monitor written in Rust that throws away almost everything a general purpose VMM like QEMU carries. There is no BIOS and no PCI bus. The emulated devices are a handful: virtio-net, virtio-block, virtio-vsock, a serial console and a minimal keyboard controller used only to stop the VM. Less device emulation means less code an attacker can poke at from inside the guest. The project documents boot to user space in around 125 milliseconds and a memory overhead of under 5 MiB per microVM.
Firecracker is also defence in depth, not one wall. Each microVM process is started by the jailer, which puts the VMM itself in a chroot, a fresh set of namespaces, cgroups and a seccomp filter, and drops privileges before the guest boots. So even a hypervisor escape lands an attacker in a jailed, unprivileged process, not root on the host. This is what "the host treats every guest as hostile" looks like in practice.
You drive it through a small REST API on a Unix socket. A minimal microVM is four calls:
SOCK=/tmp/fc.sock
curl --unix-socket $SOCK -X PUT http://localhost/boot-source \
-d '{"kernel_image_path": "vmlinux", "boot_args": "console=ttyS0 reboot=k panic=1"}'
curl --unix-socket $SOCK -X PUT http://localhost/drives/rootfs \
-d '{"drive_id": "rootfs", "path_on_host": "agent-rootfs.ext4",
"is_root_device": true, "is_read_only": false}'
curl --unix-socket $SOCK -X PUT http://localhost/machine-config \
-d '{"vcpu_count": 2, "mem_size_mib": 1024}'
curl --unix-socket $SOCK -X PUT http://localhost/actions \
-d '{"action_type": "InstanceStart"}'
The other feature that matters for agents is snapshotting. Firecracker can snapshot a running microVM's memory and device state to disk and restore it later. Boot a VM once, load Python and your common libraries, snapshot it, and every new agent session restores from that warm image instead of cold booting. That is how hosted sandbox providers get start times that feel instant, and it also gives you a clean way to pause a long agent task and resume it on another host.
The costs are real. You need KVM, which means bare metal or a cloud instance type with nested virtualisation, and many managed container platforms will not give you that. You also take on more moving parts: guest kernels, root filesystem images, a tap device or vsock per VM, and an orchestration layer on top. Kata Containers is the usual way to hide that behind the container interface in Kubernetes, running each pod inside a lightweight VM backed by Firecracker, Cloud Hypervisor or QEMU. And once you count the guest kernel and its page cache, memory per sandbox ends up higher than gVisor, even though the VMM's own overhead is tiny.
For multi-tenant, untrusted, prompt-injectable code, this is the boundary I would pick, and it is what most of the hosted agent sandbox products have converged on. Docker itself moved this way earlier in 2026, with Docker Sandboxes running agents in microVMs rather than plain containers.
Side by side
| Container (runc) | gVisor (runsc) | Firecracker microVM | |
|---|---|---|---|
| What enforces isolation | Host kernel (namespaces, cgroups, seccomp) | User-space kernel (Sentry) in front of the host kernel | Hardware virtualisation and a separate guest kernel |
| Kernel the agent's code talks to | Shared host kernel | gVisor's Sentry | Its own guest kernel |
| To escape, an attacker must break | The host kernel | The Sentry, then a narrow host syscall set | The VMM, then the jailer's sandbox |
| Start time | Milliseconds | Milliseconds | About 125 ms cold, faster from a snapshot |
| Compatibility | Full Linux | Most of Linux, some gaps | Full Linux |
| Main cost | Weak boundary for hostile code | Syscall overhead, edge-case gaps | Needs KVM, more infrastructure |
| Good fit | Your own trusted tools | Known toolchains, container-native platforms | Untrusted, multi-tenant agent code |
There are lighter options too. WebAssembly runtimes and V8 isolates give you a capability-based sandbox with no syscalls at all unless you grant them, and start in microseconds. They are excellent when the agent only needs to run a small function in a supported language, and a poor fit when it needs pip install and a shell.
The box is not enough: network and credentials
This was the part of the evening I found most useful, because it is where teams that have "solved" sandboxing still get burned. A perfectly isolated microVM does nothing for you if the code inside it can read a database password from an environment variable and curl it to an attacker's server. Kernel isolation protects the host. It does not protect your data from the agent.
The pattern the LangChain talk described for LangSmith is an outbound proxy that holds the credentials. The sandbox has no secrets in it. When code inside makes a request to an allowed API, the request leaves through a proxy on the host, which checks the destination against policy and attaches the real credential on the way out. Tools that insist on an environment variable get a placeholder value. The agent can use the API but can never read the key, so it cannot leak it, and you can revoke or rotate it without touching the sandbox.
Stripped down, the proxy's core logic is small:
ALLOWED = {
"api.github.com": {"header": "Authorization", "secret": "github_token"},
"api.stripe.com": {"header": "Authorization", "secret": "stripe_restricted_key"},
}
def handle_outbound(request, sandbox_id):
host = request.url.host
rule = ALLOWED.get(host)
if rule is None:
audit.log(sandbox_id, "blocked", host)
return Response(status=403)
# Never trust a credential the sandbox supplied; strip and replace.
request.headers.pop(rule["header"], None)
request.headers[rule["header"]] = "Bearer " + vault.get(rule["secret"], scope=sandbox_id)
audit.log(sandbox_id, "allowed", host, request.method, request.url.path)
return forward(request)
A real implementation needs more care. To inject headers into HTTPS traffic the proxy has to terminate TLS, so the sandbox must trust a per-sandbox certificate authority. DNS must go through the proxy too, or code can exfiltrate data in DNS queries. The allowlist should be per task, not per platform: an agent fixing a bug in one repository needs GitHub, not Stripe. And the credentials themselves should be scoped down, a fine-grained token for one repository rather than an organisation admin key, so that even a fully compromised session can only do what that one task needed.
Default deny is the rule. A sandbox should start with no network at all and gain destinations one by one. The most common way this goes wrong is a sandbox that was given open internet access because pip install needed it. If the agent needs packages, give it a pull-through mirror of your package index, not the open internet.
Lifecycle: ephemeral by default
The last layer is time. A sandbox that lives forever accumulates state, and state is where injected instructions hide. A poisoned file written by one session should never be read by the next, so the safe default is one sandbox per task, created from a known image or snapshot and destroyed when the task ends. Anything worth keeping, a diff, an artifact or a report, leaves through a channel you inspect rather than by surviving on disk. Hard limits on CPU, memory, disk and wall-clock time mean a runaway loop or a fork bomb costs you a timeout, not a node. And everything that happens inside should be traced, the commands run, the files written, the network calls attempted and blocked, because when something goes wrong you need the story and not just the exit code.
The talks also covered where sandboxes sit in an agent architecture, and it is broader than "the code execution tool". The same isolated environment is what you want for an eval harness that runs agent-written code against tests, and for a CI runner that executes changes an agent proposed. Anywhere the code was not written by a human you trust, the same box applies.
Beyond the sandbox
The closing talk from BCG made the point that the sandbox only answers the compute question. It does nothing about an agent that has been given legitimate access and uses it badly. If your agent can send email, issue refunds or merge pull requests, it can be talked into doing those things. The controls there are not technical isolation but design: least-privilege tool permissions, separating the agent that reads untrusted content from the agent that holds powerful tools, human approval for high-impact or irreversible actions, and rate limits on anything that touches money or customers.
Where I came out after the evening is fairly opinionated. Treat agent-generated code as hostile, always, not just when it looks suspicious. Choose the boundary by how much you trust the code: containers for your own tools, gVisor when you want container ergonomics and a stronger wall, microVMs for anything untrusted and multi-tenant. Keep secrets out of the box entirely and default-deny the network. Make every sandbox short-lived and fully traced. Then accept that none of this covers an agent misusing access it was legitimately given, which is a permissions and product design problem that no hypervisor solves. I do not think anyone at the event had a clean answer to that last part yet, and I am suspicious of anyone who claims to.
The $300 million is a bet that companies will run a lot of agents on a lot of private data in the next few years. Whether that goes well depends far less on which model they pick than on whether the box around it was built by someone who assumed the agent was the attacker.
Production-Ready Systems with LLMs and Agents
A live Maven cohort for engineers: eight 90-minute sessions where you build LLM and agent systems that survive real traffic, real cost and real failure.
Cohort 2 is running now. Join the waitlist for the next cohort.
View the live cohortKeep reading
- LLM Tracing vs Evals: Which One Tells You Why Your Agent Broke
- Containers vs Cloudflare Computer: There Is Not Enough Compute for One Box Per Agent
- Graph Engineering: Every Edge You Draw Takes a Decision Away From the Model
- Semantic Caching for AI Requests, Explained Simply
- The Most Honest Piece of Documentation Hugging Face Ships Is Its Logo
- Why I Route Every AI Call Through OpenRouter Instead of Direct Endpoints