My agent platform has a skill where you describe something you want built and the agent writes the code and runs it. Clean this spreadsheet. Scrape this list. It works well.
It also means a stranger can type a sentence and my servers will execute whatever program comes out of it.
People overcomplicate the security question here. Nobody is worried the model has gone rogue. The point is simpler: a user’s prompt is input I do not control, the model turns that input into a program, and I run it. It does not matter what the model intended.
So the code needs a real boundary around it. What I did not expect is that the boundary turned out to be the easy part.
Pick the boundary, then stop thinking about it
The options are well documented and the decision is not close for most teams.
| Boundary | Isolation | Start time | When it fits |
|---|---|---|---|
Same process, vm or exec | None worth the name | Instant | Never, for untrusted code |
| Container, shared kernel | Namespaces and cgroups | Sub-second | Trusted-ish workloads |
| gVisor | Syscalls intercepted in user space | Sub-second | Untrusted code, most cases |
| Firecracker microVM | Hardware-enforced | Around a second | Untrusted code, hostile tenants |
Do not try to do this inside your language. Node’s vm module tells you in its own documentation
that it is not a security boundary, and the history of JavaScript sandbox escapes is basically one
long list of people finding a way to reach back out to the host.
I use Cloudflare’s Sandbox, which runs on Containers, one per session. That choice was mostly about not operating a second orchestration system, and the isolation properties come from the platform rather than from anything I wrote. Which is the point. Whichever row you pick, pick it once.
The design decision that mattered more
I originally ran the coding loop in the Worker: the model proposed an edit, the Worker applied it, ran a command, fed the output back. It kept hitting the model’s output limit and truncating tool-call JSON mid-string, which fails in a way that is tedious to diagnose because the error surfaces as malformed arguments rather than as a length problem.
The version that works inverts it. The Worker does not run a coding loop. It boots a container, runs a coding agent inside it, and streams that process’s output back. Two models, two jobs:
- A cheap fast model in the orchestrator, talking to the user and deciding to delegate.
- A strong model driving the coding agent inside the container, iterating until the task is done.
The orchestrator never sees the file edits. It sees a task id, progress lines, and a result. Its context stays small, and the coding loop is bounded by the container rather than by a single response.
What breaks in production
Isolation is table stakes. These are the problems that generate tickets.
Cancellation is the one to design for on day one. A user says stop while the container is mid
npm install, and you need a control path that does not depend on the running process cooperating.
That means killing the container and treating the partial workspace as garbage. Retrofitting this
into a loop that assumes it runs to completion is a rewrite, not a patch.
Quotas come next. One tenant should not consume the pool. Mine are enforced before the container boots, against the same token wallet the rest of the platform uses, because container time and model tokens both cost money and a tenant does not distinguish between them.
Then there is getting results out. The container has a filesystem the user cannot see, so I mirror workspace changes to object storage as they happen. A run cancelled at 80% still leaves the files it wrote. Streaming progress as newline-delimited JSON into a key the frontend polls was the least clever option available and has never caused a problem.
Last, put the timeout on the task rather than the request. A coding task can legitimately take minutes; the HTTP request that started it cannot. Anything long-running goes through a queued object with its own alarm, and the request returns a task id immediately.
Should the container reach the network?
Mostly no, and the exception is annoying.
Code that installs a dependency needs a package registry. Code that fetches a URL because the user asked it to needs the open internet, which is the same SSRF exposure a webhook sender has, except the attacker gets to write arbitrary code rather than supply a URL.
I allow package registries and block private address ranges. It is a compromise rather than a solution, and I would tighten it to a proxy with an allow-list if the workload justified the work. Anyone claiming a network-enabled sandbox is fully contained has not thought about DNS.
Sources
- Cloudflare Sandbox, Cloudflare
- Cloudflare Containers, Cloudflare
- gVisor documentation, Google
- Firecracker microVM, Amazon Web Services
- Server Side Request Forgery Prevention Cheat Sheet, OWASP
Elson Tan