No. 21 · JUN 2026 · 8 Min Read
A Sandbox for User Code
Abstract
The execution sandbox and the policy that gates it are the heart of a user-code platform. You compose storage and scheduling from cloud primitives.
Your Users Already Run AI-Generated Code made the case for handing users a box of primitives, code that is first-party in purpose and third-party in trust. Where User Code Runs settled the boundary: for arbitrary code against real data, a microVM, each run inside its own kernel behind a hypervisor.
A sandbox by itself runs nothing useful. Boot one and it has no idea who it is acting for, what it may read, or where its work goes when the process exits. Two things turn a sandbox into a system. A policy that decides what any given run is allowed to touch. And the surfaces each run operates through: storage, scheduling, secrets. The sandbox and the policy are the heart. Everything else you compose from a primitive instead of shipping as a feature.
The Sandbox Is Off the Shelf Now
The wall from the last piece used to be a build. You took Firecracker, or gVisor, wrote a jailer, kept a pool of warm machines, handled snapshotting, and owned the whole thing forever. That is the version that felt like pulling teeth, and it is why teams reached for eval and hoped.
As of June 22, 2026 it is a managed primitive. AWS launched Lambda MicroVMs that day, and the shape is the one this architecture needs. You give it a Dockerfile and a code artifact; it builds an image and takes a Firecracker snapshot of the initialized memory and disk. run-microvm hands back an isolated instance with its own kernel and a dedicated HTTPS endpoint. One image launches many instances, each configured at start. Ingress is authenticated, no open ports. It suspends when idle and resumes from the snapshot on the next request, near-instant, up to eight hours of total runtime. AWS is plain about the intent: regular Lambda stays the right tool for request-response work, and MicroVMs are purpose-built for multi-tenant applications. Their own list of use cases reads like this series: AI coding assistants, interactive code environments, data analytics platforms, code that runs user-supplied scripts.
Cloudflare ships the same shape. The Sandbox SDK went GA in April 2026: a container with a full Linux environment, started on demand by name, asleep when idle, awake on the next request, stateful across calls. Same primitive, different cloud. For JavaScript or Wasm with no need for a shell, Workers for Platforms routes each tenant’s code into its own isolate and treats it as untrusted by default.
Both are an API call. You get an isolated machine with a kernel, a filesystem, and a clock, and it bills near zero while it sleeps. The strongest isolation boundary the industry sells short of separate hardware is now a primitive you rent by the second. Your users will lean on that boundary without ever knowing it is there. That is the whole job of a guardrail.
The Policy Is the Other Half
The sandbox keeps a run from escaping its machine. It says nothing about what the run may reach inside your system. A tenant’s code, perfectly contained in its own VM, can still ask for another tenant’s records if you let the request through. The policy answers that, and it is load-bearing enough to earn equal billing with the sandbox.
Two jobs, kept apart. Authentication says who is calling: Cognito on AWS, Access on Cloudflare, issuing a token with claims for which tenant, which role, which primitives this customer paid for. Authorization says what they may do, a different question that wants a different mechanism. Conflate the two and the authorization logic ends up smeared across the codebase as scattered if checks nobody can audit.
For what a caller may do, attributes beat roles. Role-based access makes you enumerate roles up front, then discover you need admin_but_only_for_region_west, and the list rots. Attribute-based access compares attributes on the request to attributes on the resource. The rule stops counting tenants. It states a relationship and holds for all of them.
permit(
principal,
action == Action::"read",
resource
)
when { resource.tenant == principal.tenant };
One rule. It names no tenant. A principal may read a resource when they share a tenant, true for the first customer and the ten-thousandth. On AWS you run it through Amazon Verified Permissions, which speaks Cedar as a managed service, or you push the same logic down into an IAM condition on aws:PrincipalTag so storage itself refuses the cross-tenant read. Either way the policy assumes the caller was already authenticated and answers only the authorization question.
This is correct by construction. The cross-tenant read is not caught after the fact. It is unrepresentable: the only path the policy ever lets a run name is its own tenant’s. There is nothing to check for, because the error cannot be expressed. That property is the reassurance as much as the defense. You are not auditing a thousand code paths. You are stating one relationship and trusting the engine to hold it.
How those claims get minted, how the running code acts as the user instead of a faceless service account, how secrets stay sealed per tenant, that is When User Code Acts as the User.
Everything Else Is Composed
With the heart in place, the rest of the system is composed, not built. Each thing the sandbox runs against, storage, scheduling, secrets, outbound networking, comes from a primitive the cloud already sells rather than infrastructure you stand up. The discipline is the same each time: find the primitive, scope it with the policy, mount it into the run.
The pieces are interchangeable, which is why none of this locks to one vendor. Files do not have to live in S3. Cron does not have to live in EventBridge. Pick the primitive that fits and scope it.
Storage
The part the user authors against is a filesystem.
When an agent helps someone build something, it reads context, writes code, and runs it. You could invent a bespoke API for each. You would be reinventing a worse filesystem. Models are exceptionally good at filesystems; they have seen more read this file, edit that file, run this command loops than almost any other interface. A filesystem is the most model-legible surface you can hand an agent, which matters because the developer on the other end of your MCP server is a model. The argument from Writing a Good MCP Server holds: collapse the surface into verbs the model already knows. List, read, write, execute.
Three of those operate on durable storage. The fourth, execute, is the seam where storage meets the sandbox. The server reads the relevant slice of the tree, mounts it into a fresh sandbox, runs the code, streams the output back, and tears the machine down. The tree persists. The machine does not. Storage is durable and the agent authors against it; the sandbox borrows a slice for the length of one run and vanishes.
The path is the resource the policy reasons about. A tenant’s tree lives under a prefix tied to their identity, /tenants/{tenant_id}/..., and the same rule governs read, write, and execute uniformly, because all three name a path. A run can only ever name its own subtree, and the sandbox was only ever mounted with that slice.
The primitive under the tree is a choice. On AWS, an S3 prefix per tenant, gated by the principal-tag condition. On Cloudflare, R2 for the plain object store, or Cloudflare Artifacts, which is closer to purpose-built for this: versioned, git-compatible storage meant for agents. A repository per user or per branch at the millions scale, fork from a baseline, let the agent commit, diff against the original, with scoped tokens that carry their own expiry. The authoring surface arrives with version history and per-tenant isolation already in the box. One job, three primitives, one policy deciding who may name what.
The Rest
Storage earns the detail because it is what the user authors against. The rest follow the same recipe: find the primitive, scope it with the policy, mount it into the run. Scheduling needs no central scheduler; a tenant’s app can carry its own cron in userspace, on Durable Object Alarms or inside the sandbox’s own lifecycle, instead of routing every job through EventBridge. Secrets and outbound access work the same way, which is most of what When User Code Acts as the User and A Credential Broker for User Code work out. Why composing these from primitives keeps the whole platform thin, and cheap enough to give every tenant its own, is Files Without S3, Cron Without EventBridge.
One Run, End to End
The heart and storage, traced through one run.
Steps 03 and 04 are storage meeting the sandbox: the durable tree hands a slice to a disposable machine. Steps 02 and 05 are the policy: the same attribute on the token that identified the caller is the attribute that makes step 05 impossible. The data outlives the compute. The boundary is physical, not promised.
The Shape
A sandbox, a policy, and a set of primitives. The sandbox is a machine you rent by the second and tear down when idle. The policy is one ABAC rule that decides what each run may name. Storage, scheduling, and the rest are each a primitive both clouds already sell, each scoped by that one rule. Cognito or Access says who. Cedar or a principal tag says what. The path says where. The sandbox makes the where physical.
What you did not build is a custom sandbox, a permission engine, a scheduler, or a feature surface that grows with every customer request. You rented the hard parts and wired them together. The industry built this pattern for untrusted third-party code; pointed inward, at code your own customers write and never publish, it holds without a fight.