Code sandboxes
sandboxRun untrusted or AI-written code in a sandbox with time, memory and cost limits, no secrets, and only the hosts you allow.
sandboxFree in your app, Apache-2.0Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
sandbox runs code you didn't write, from a user or a model, outside your app's process, with no secrets, files or network unless you allow them, and stops it at its limits.
Vercel Sandbox and E2B run it in microVMs in production; local runs a limited subprocess for development.
At a glance
sandbox:
driver: vercel-sandbox
limits: { timeout: 30s, memory: 512MB, output: 1MB, max-cost: $0.05 }
network: [pypi.org]import { sandbox } from "mainsdk";
const out = await sandbox.run({
code: "print(sum(range(10)))",
language: "python",
feature: "analysis", // no network, no secrets, killed at its limits
});Calls
sandbox.run({ code, language, files, network, feature })sandbox.create(opts)sandbox.get(id)sandbox.action({ name, intent, languages })new Sandbox(deps)createSandbox(deps)parseSandboxConfig(raw)secret(name)localSandboxDriver(opts)e2bNetworkPolicy(hosts)vercelNetworkPolicy(hosts)
Guarantees
Whatever the driver.
- Code never runs in the app's process
- No network unless allowed, and never wider than network.allow
- No secrets or app values unless named
- Killed at its time, memory, output and cost limits
- Every run audited with the code's hash, never the code
- The local driver is refused in production
Drivers
3 drivers. Select one to see its line in main.yaml.
sandbox:
driver: vercel-sandboxChange this one line to swap providers; your calls stay the same.Vercel integration
Codes
| Code | Where it appears | Meaning |
|---|---|---|
MAIN-SANDBOX-0010 | Checkmainsdk check, before deploy | eval, new Function or node:vm runs a value that isn't a string literal |
MAIN-SANDBOX-0011 | Checkmainsdk check, before deploy | The local sandbox driver in production |
MAIN-SANDBOX-0012 | Checkmainsdk check, before deploy | sandbox.network lists a host that network.allow doesn't |
MAIN-SANDBOX-0001 | RuntimeError responses and logs | A sandbox option is invalid (400); nothing ran |
MAIN-SANDBOX-0002 | RuntimeError responses and logs | The code reached its time limit and was killed |
MAIN-SANDBOX-0003 | RuntimeError responses and logs | The code passed its memory limit and was killed |
MAIN-SANDBOX-0004 | RuntimeError responses and logs | Output or files passed their limit; output is cut |
MAIN-SANDBOX-0005 | RuntimeError responses and logs | A host isn't in sandbox.network and network.allow, or the code tried a host it wasn't given (403) |
MAIN-SANDBOX-0006 | RuntimeError responses and logs | The sandbox driver is unavailable (503, retryable) |
MAIN-SANDBOX-0007 | RuntimeError responses and logs | The run would pass, or reached, its cost ceiling (402) |
MAIN-SANDBOX-0008 | RuntimeError responses and logs | No sandbox session with that id in the current tenant, or it was closed (404) |
MAIN-SANDBOX-0009 | RuntimeError responses and logs | A secret reference isn't under sandbox.secrets (403) |
Features
Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.
| Feature | Phase | Milestone | Spec ticket |
|---|---|---|---|
| Local driver with time and memory limits, an empty environment and no network | Phase 1 | 0.4 | sandbox#local-driver |
| Network allowlist parity with network.allow | Phase 1 | 0.4 | sandbox#network-parity |
| No secrets unless named; files through files | Phase 1 | 0.4 | sandbox#secrets-files |
| Sessions with run | Phase 1 | 0.4 | sandbox#sessions |
| Vercel Sandbox and E2B drivers | Phase 1 | 0.4 | sandbox#provider-drivers |
| A code-runner action for agents | Phase 1 | 0.4 | sandbox#action |
Quick start
sandbox:
driver: local # vercel-sandbox | e2b | local (development only)
local: { python: python3 }
permissions:
processes: [python3] # the local driver starts code through the system interface
disk: { temp: { max: 1GB } } # and works in a temp folder it removes afterwardsimport { sandbox } from "mainsdk";
export async function runSnippet(code: string) {
const out = await sandbox.run({ code, language: "python", feature: "notebook" });
return out.stop ? `stopped: ${out.stop}` : out.stdout;
}language is python, node or bash. feature names the run in the audit event. A run that is stopped still returns a result with stop set; it does not throw.
Configuration
sandbox:
driver: vercel-sandbox
limits: { timeout: 30s, memory: 512MB, output: 1MB, files: 50MB, max-cost: $0.05 }
network: [pypi.org] # the most any run may get; must also be in network.allow
secrets: [OPENWEATHER_KEY] # the only secrets a run may be given
vercel: { token: "secret:VERCEL_TOKEN", project: prj_123 }
network:
allow: [pypi.org]| Key | Meaning |
|---|---|
driver | vercel-sandbox, e2b or local. Default local. |
limits | timeout (30s), memory (512MB), output (1MB), files (50MB) and max-cost (none). A call can lower a limit, never raise it. |
network | Hosts any run may be given. Each must also be in network.allow. A call's network defaults to none. |
secrets | Names of secrets a run may receive through secret(name). |
local | Programs for the local driver: { python: python3, node: node }. List them under permissions.processes too. |
vercel | token (default secret:VERCEL_TOKEN), project, optional team and runtime. |
e2b | key (default secret:E2B_API_KEY) and optional template. |
price | Price per second of a box, such as $0.0001, to override a driver's estimate. |
An unknown key under sandbox is refused at load with the key named. The provider's API host is added to the network list for you.
What you get
- Code never runs in your app's process, and starts with an empty environment, an empty
/workholding only the files you pass, and an empty/out. - No network by default. Each host you give must be in both
sandbox.networkandnetwork.allow, or the call fails before anything runs. - Output is cut at
limits.outputand redacted for secret values and known key formats (it comes back as[redacted:secret]). - The worst-case cost (price per second times the timeout) is checked against
maxCostandlimits.max-costbefore the run starts. - One audit event per run with the SHA-256 of the code, driver, isolation, duration, exit code, stop reason, hosts and env names. Never the code, output or secret values.
- A provider outage fails only sandbox calls (
MAIN-SANDBOX-0006), not the rest of your app.
Examples
Pass files in and read results out
Files you pass appear in the code's working folder (/work); files it writes to the out folder next to it (../out/ on the local driver) come back as FileRefs owned by the caller.
import { sandbox } from "mainsdk";
export async function chart(csv: string) {
const out = await sandbox.run({
language: "python",
feature: "chart",
files: { "data.csv": csv },
code: "rows = open('data.csv').read().splitlines()\nopen('../out/count.txt', 'w').write(str(len(rows)))",
});
return out.files; // [{ id, ... }] for count.txt
}Give a run one secret and one host
import { sandbox, secret } from "mainsdk";
export function weather(code: string) {
return sandbox.run({
code,
language: "python",
feature: "weather",
network: ["pypi.org"],
env: { WEATHER_KEY: secret("OPENWEATHER_KEY") },
});
}A name that isn't under sandbox.secrets fails with MAIN-SANDBOX-0009 and nothing runs.
Keep a session open across commands
A session is scoped to the tenant that created it, closes after idle (5 minutes by default), and runs commands as a program and an argument array, never through a shell.
import { sandbox } from "mainsdk";
export async function twoSteps() {
const session = await sandbox.create({ feature: "notebook", idle: "1m" });
try {
await session.write("a.txt", "hello");
const out = await session.run("python3", ["-c", "print(open('a.txt').read())"]);
return out.stdout;
} finally {
await session.close();
}
}sandbox.get(id) returns an open session of the current tenant.
Let an agent run code
sandbox.action returns a code-runner action. Its input is { code, language, files? }, its risk is write unless you set it, and an agent calling it passes the agent's budget and approval rules. Its cost counts against the agent's budget.
import { ai, sandbox } from "mainsdk";
const runPython = sandbox.action({
name: "code.run",
intent: "Run Python to analyse the user's data",
languages: ["python"],
});
export function analyse(user: { id: string; roles: string[] }, goal: string) {
return ai.run({ model: "anthropic/claude-sonnet-4-5", feature: "analysis", agent: "analyst", user, goal, tools: [ai.tool(runPython)] });
}Reference
Most apps use the sandbox export, which main.yaml builds for you. These are the pieces underneath, for tests and custom wiring.
| Call | What it does |
|---|---|
new Sandbox(deps) | The interface class. deps takes the driver, the parsed config, the network matcher and a secret resolver, plus optional files, log, tenancy, agents and access. |
createSandbox(deps) | Builds a Sandbox from the same deps. |
parseSandboxConfig(raw) | Reads the sandbox section of main.yaml, fills the defaults and refuses an unknown key (MAIN-CONFIG-0003) or an invalid option (MAIN-SANDBOX-0001). |
localSandboxDriver({ permissions, programs? }) | The local driver: a limited subprocess for development and tests. programs sets the python and node paths. |
e2bNetworkPolicy(hosts) | The E2B egress policy: { allowOut: hosts, denyOut: ["0.0.0.0/0"] }, so only the given hosts are reachable. |
vercelNetworkPolicy(hosts) | The Vercel Sandbox egress policy: { mode: "deny-all" } for no hosts, otherwise { mode: "custom", allowedDomains: hosts }. |
Isolation you can read
Every result has isolation: microvm for the providers, namespace for the local driver on Linux with an empty network, and weak for the local driver otherwise (a proxy refuses other hosts; the code is not in a separate network). Treat weak as a development convenience.
Errors and checks
| Code | When | Fix |
|---|---|---|
MAIN-SANDBOX-0001 | An option is invalid: unknown language, empty code, a limit above sandbox.limits, a path outside /work or /out. | Fix the option; nothing ran. |
MAIN-SANDBOX-0002 to 0004, 0007 | The run hit its time, memory, output or cost limit. Returned as stop (timeout, memory, output, cost), not thrown; 0007 throws before the run when the worst case is over maxCost. | Raise the limit in main.yaml or lower the work. |
MAIN-SANDBOX-0005 | A host is not in sandbox.network and network.allow, or the code tried a host it wasn't given (logged). | Add it to both lists, or drop it from the call. |
MAIN-SANDBOX-0006 | The driver is unavailable (503, retryable). | Retry; check the provider and token. |
MAIN-SANDBOX-0008 | No open session with that id in the current tenant. | Create a new session. |
MAIN-SANDBOX-0009 | A secret() names a secret that isn't under sandbox.secrets. | List it there. |
mainsdk check reports MAIN-SANDBOX-0010 for eval, new Function or vm.* on anything but a string literal, MAIN-SANDBOX-0011 for the local driver in production, and MAIN-SANDBOX-0012 for a sandbox.network host missing from network.allow. See check codes.
Swapping drivers
Change sandbox.driver and nothing in your code changes: vercel-sandbox and e2b run code in microVMs through the provider's API (token or key as a secret reference), and local runs a subprocess for development. Every driver gets the same limits, the same network allowlist (enforced through the provider's egress policy) and the same audit event. local in production fails mainsdk check --env prod with MAIN-SANDBOX-0011.
Testing
The local driver needs no account; run the real code in a test and assert on stdout, stop and files. Put the interpreters under permissions.processes in the test's main.yaml. The provider drivers are covered by the conformance suite on recorded sessions.
Security and privacy
- Code never runs in your app's process.
- Secrets reach a run only when listed under
sandbox.secretsand passed by name; the audit event records names, never values. - Every run is audited with the SHA-256 of the code, never the code or its output.
- Sessions of one tenant can't be reached from another.
Next
Was this page helpful?