eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
233 lines (168 loc) • 16.2 kB
text/mdx
---
title: "Sandbox"
description: "The agent's isolated bash environment, including built-in file tools, a seeded /workspace, backends, lifecycle, and network policy."
---
The sandbox is the agent's isolated bash environment: a filesystem rooted at `/workspace` where it can run shell commands, execute scripts, and read or write files without ever touching your app runtime. Every eve agent has exactly one. The built-in `bash`, `read_file`, `write_file`, `glob`, and `grep` tools already target it, and your authored code can too.
A working sandbox exists by default, with nothing to author. Override it only to add setup, seed files, pick a backend, or lock down the network.
The default sandbox is not a substitute for configuring network policy, credentials, retention, deletion, or other controls your application requires.
## Using the sandbox
The model already has shell and file access through the default tools:
| Tool | Does |
| -------------------------- | ----------------------------------- |
| `bash` | run a shell command in the sandbox |
| `read_file` / `write_file` | read/write files under `/workspace` |
| `glob` | find files by pattern |
| `grep` | search file contents |
All of them run with `/workspace` as the working directory. Any authored runtime function (a tool, a step, a model callback) can get a live sandbox handle with `ctx.getSandbox()`.
```ts title="agent/tools/run_analysis.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";
export default defineTool({
description: "Run a Python analysis script and return its output.",
inputSchema: z.object({ script: z.string() }),
async execute({ script }, ctx) {
const sandbox = await ctx.getSandbox();
await sandbox.writeTextFile({ path: "analysis/run.py", content: script });
const result = await sandbox.run({ command: "python analysis/run.py" });
return { stdout: result.stdout };
},
});
```
`ctx.getSandbox()` takes no arguments, is async, and only works inside authored runtime execution.
`/workspace` is one namespace across every backend, so `/workspace/foo` points at the same file whether the backend is local or Vercel. When you need to interpolate a path into a generated command, `sandbox.resolvePath("repo/build.py")` anchors a relative path to its absolute `/workspace/repo/build.py` form.
The handle does more than `run` and `writeTextFile`. In every method, relative paths resolve from `/workspace` and absolute paths pass through untouched:
| Method | Does |
| ---------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `run({ command })` | run one command, block until it exits, return `{ stdout, stderr, ... }` |
| `spawn(options)` | launch a long-running process (server, watcher) and return a `SandboxProcess` handle |
| `readTextFile` / `writeTextFile` | read/write a UTF-8 (or specified encoding) file; `readTextFile` supports 1-based line ranges |
| `readBinaryFile` / `writeBinaryFile` | read/write raw bytes (images, archives, anything non-text) |
| `readFile` / `writeFile` | stream a file in/out as bytes |
| `removePath({ path, force, recursive })` | delete one file or directory; `force` ignores missing paths, `recursive` removes non-empty dirs |
| `resolvePath(path)` | anchor a relative path to its absolute `/workspace/...` form |
| `setNetworkPolicy(policy)` | change egress policy mid-turn (backend-dependent; see [Network policy](#network-policy)) |
Since `run` blocks until the command exits, use `spawn` when the process should keep running while the agent does other work:
```ts
const sandbox = await ctx.getSandbox();
const server = await sandbox.spawn({ command: "python -m http.server 8000" });
// ...do other work against the server...
await server.kill();
```
A `SandboxProcess` exposes `stdout`/`stderr` byte streams, `wait()` (resolves with the exit code), and `kill()` (idempotent).
`sandbox.id` is a stable per-session identifier that persists across reconnects to the same logical session. Use it as the cache key for per-session state that must outlive individual step executions.
The option types (`SandboxSpawnOptions`, `SandboxReadBinaryFileOptions`, `SandboxWriteBinaryFileOptions`, and so on) are named exports from `eve/sandbox`, alongside `SandboxProcess`.
## Seeding `/workspace`
Mount authored files into the sandbox at session start by placing them under `agent/sandbox/workspace/`. This requires the folder layout (`agent/sandbox/sandbox.ts`), not the top-level shorthand:
```text
agent/sandbox/
sandbox.ts ← optional override (see below)
workspace/
schema.sql ← lands at /workspace/schema.sql
scripts/run.sh ← lands at /workspace/scripts/run.sh
```
Every file under `workspace/` mirrors into the sandbox cwd with its structure intact, and eve lists the top-level entries to the model in the prompt automatically. One subtree is off limits. Skill discovery already seeds skill files under `/workspace/skills/`, so authoring `agent/sandbox/workspace/skills/...` is rejected; put those under `agent/skills/` instead.
## Overriding the sandbox
To add setup, seed files, or pick a backend, author `defineSandbox`. There are two layouts:
- `agent/sandbox.ts`: shorthand. Use it when you need only a definition, no seeded files.
- `agent/sandbox/sandbox.ts`: folder layout. Use it when you also seed `agent/sandbox/workspace/**`. If both exist, the folder layout wins.
```ts title="agent/sandbox/sandbox.ts"
import { defineSandbox } from "eve/sandbox";
import { vercel } from "eve/sandbox/vercel";
export default defineSandbox({
backend: vercel({ runtime: "node24", resources: { vcpus: 2 } }),
revalidationKey: () => "repo-bootstrap-v1",
async bootstrap({ use }) {
const sandbox = await use();
await sandbox.run({ command: "apt-get install -y jq" });
},
async onSession({ use }) {
await use({ networkPolicy: "deny-all" });
},
});
```
`defineSandbox` and `defaultBackend` live on `eve/sandbox`. Omit `backend` and the runtime falls back to `defaultBackend()` (see [Backends](#backends)).
## Backends
The backend decides where the sandbox runs. eve ships four pinned factories from nested `eve/sandbox/*` imports plus an availability-aware default from `eve/sandbox`:
| Backend | Runs the sandbox |
| ------------------ | ---------------------------------------------------------------------------------------------- |
| `vercel()` | on [Vercel Sandbox](https://vercel.com/docs/sandbox). |
| `docker()` | locally in a Docker container, driven through the `docker` CLI. |
| `microsandbox()` | locally in a lightweight [microsandbox](https://www.npmjs.com/package/microsandbox) VM. |
| `justbash()` | locally in the pure-JS `just-bash` interpreter (no daemon or VM, but no real binaries either). |
| `defaultBackend()` | picks the best available: Vercel Sandbox on hosted Vercel → Docker → microsandbox → just-bash. |
Configuring a pinned factory uses that backend unconditionally. `docker()` always requires a reachable Docker daemon, and `vercel()` always creates hosted sandboxes (including from local dev, with Vercel credentials).
With `backend` omitted, eve uses `defaultBackend()`, which resolves on first use in priority order:
1. **Vercel Sandbox** when deploying on Vercel (`process.env.VERCEL` is set), since local container/VM runtimes can't run there.
2. **Docker** when a daemon is reachable through a Docker-compatible `docker` CLI (Docker Desktop, OrbStack, Colima, Podman via its docker-compatible CLI; override the binary with `EVE_DOCKER_PATH`).
3. **microsandbox** when the host supports it: macOS on Apple Silicon, or glibc Linux with KVM enabled.
4. **just-bash** as the dependency-free fallback.
`defaultBackend()` also accepts a keyed bag so each inner backend gets its own typed create options:
```ts
import { defaultBackend, defineSandbox } from "eve/sandbox";
export default defineSandbox({
backend: defaultBackend({
vercel: { networkPolicy: "deny-all", resources: { vcpus: 4 } },
docker: { image: "ghcr.io/vercel/eve:latest" },
microsandbox: { memoryMiB: 2048 },
}),
});
```
### Docker
`docker()` drives the Docker CLI directly. The default base image is `ghcr.io/vercel/eve:latest`, eve's published sandbox runtime image. eve creates `/workspace` and verifies Bash during framework setup, before authored bootstrap code runs. Configure it through `docker({ image, env, pullPolicy, networkPolicy })`, and install authored runtime tools in sandbox bootstrap or provide them through a custom image. Templates are committed as local Docker images and reused across sessions when the sandbox source, seed files, `revalidationKey`, and Docker backend options still match. Sessions run as long-lived containers whose filesystems persist `/workspace` changes across turns for the same durable session. `eve dev` prunes stale template images in the background.
### microsandbox
`microsandbox()` runs each sandbox in a lightweight local VM with snapshot-backed templates, a `vercel-sandbox` user, and a firewall capable of domain-level network policies and credential brokering. It is the closest local match to hosted Vercel Sandbox. The default base image is `ghcr.io/vercel/eve:latest`, eve's published sandbox runtime image. During framework setup, before authored bootstrap code runs, eve verifies Bash and creates `/workspace` and the sandbox user. Install authored runtime tools in sandbox bootstrap or provide them through a custom image. Supported hosts are macOS on Apple Silicon, or Linux (glibc) with KVM. The `microsandbox` npm package and its VM runtime are not bundled with eve, so `eve dev` installs both automatically when missing (disable with `setup: { autoInstall: false }`); production processes fail with actionable install errors instead.
### just-bash
`justbash()` needs no daemon or VM, but commands run in a simulated bash with a virtual filesystem under `.eve/sandbox-cache/`, with no real binaries (`git`, `node`, package managers) and no network isolation. The `just-bash` package is an optional peer dependency, so `eve dev` installs it into your application automatically when missing (disable with `autoInstall: false`); production processes fail with an actionable install error instead.
You can also write your own backend. A `SandboxBackend` is an adapter object with a `name`, a `create`, and an optional `prewarm`. It can point at your own container runner, VM pool, internal sandbox service, or another isolation layer, as long as it returns the `SandboxSession` operations eve needs. See the `SandboxBackend*` types on `eve/sandbox`.
## Lifecycle
There are two hooks, scoped differently:
- **`bootstrap({ use })`** is template-scoped and runs once when the template is built. Put reusable setup here that every later session inherits, such as cloning a baseline repo, installing dependencies, or seeding files. Call `use()` to get a `SandboxSession`. Only template filesystem state and supported backend metadata carry into later sessions; config like network policy does not. If external inputs affect what bootstrap produces, set `revalidationKey: () => string` so eve knows when to rebuild the template (authored sandbox source and seed contents are already tracked for you).
- **`onSession({ use, ctx })`** is durable-session-scoped and runs once per session. Put per-session setup here, including network policy, resources, timeout, per-user credentials, and one-time markers. Because it runs inside the active runtime context, it can read `ctx.session` and derive the current principal without baking credentials into the template. Call `use(opts?)` to get a `SandboxSession`; `opts` flow to the backend's update path after create.
If you require a network policy or other configuration for every session, configure it on the backend factory or in `onSession`; do not rely on bootstrap-only configuration.
```ts
import { defineSandbox } from "eve/sandbox";
import { vercel } from "eve/sandbox/vercel";
export default defineSandbox({
backend: vercel(),
async onSession({ use, ctx }) {
const sandbox = await use({ networkPolicy: "deny-all" });
const user = ctx.session.auth.current;
if (user === null) return;
await sandbox.writeTextFile({ path: "SESSION_USER.txt", content: `${user.principalId}\n` });
},
});
```
Sessions are persistent, and how the underlying runtime idles out depends on the backend. On the Vercel backend, the VM times out after a period of inactivity (default 30 minutes); eve preserves the filesystem and resumes the sandbox on the next message as if nothing happened, even days later. The Docker backend keeps a long-lived container per durable session and persists `/workspace` across turns without that timeout, and the just-bash backend stores its virtual filesystem under `.eve/sandbox-cache/`. In every case, `/workspace` survives between turns for the same session.
## Network policy
Egress rules go on the backend factory or in `onSession`'s `use()`. There are three forms:
```ts
networkPolicy: "allow-all"; // default
networkPolicy: "deny-all"; // block all egress, including DNS
networkPolicy: {
allow: ["ai-gateway.vercel.sh", "*.github.com"],
subnets: { deny: ["10.0.0.0/8"] },
};
```
Default egress is `allow-all`. For non-public, sensitive, regulated, or production workloads, configure `deny-all` or an explicit allow-list before running untrusted tools or handling sensitive data.
Set it on the factory (`vercel({ networkPolicy: "deny-all" })`) and it applies before authored `bootstrap` code runs; framework-owned base setup may briefly keep egress open to install required packages. Set it in `onSession`'s `use()` to override per-session. The common pattern combines both: leave the factory open so `bootstrap` can `git clone`, then lock down in `onSession`. To change the policy mid-turn, call `sandbox.setNetworkPolicy(...)` on the live handle.
Domain-level allow-lists and credential brokering are supported by `vercel()` and `microsandbox()`. The Docker backend honors only `"allow-all"` and `"deny-all"` (at creation and via `setNetworkPolicy`); the just-bash backend rejects `setNetworkPolicy` entirely.
## Credential brokering
Secrets never enter the sandbox. Instead, the network policy's per-domain `transform` injects credentials at the firewall, so a header can authenticate egress to a host while the secret stays out of the sandbox process entirely:
```ts
async onSession({ use }) {
await use({
networkPolicy: {
allow: {
"github.com": [{ transform: [{ headers: { authorization: "Basic your_base64_credentials_here" } }] }],
"*": [],
},
},
});
}
```
The `"*": []` catch-all keeps general egress open while the `transform` applies only to `github.com`. For mid-turn brokering, call `setNetworkPolicy` with the same shape. The [Vercel Sandbox docs](https://vercel.com/docs/sandbox) cover the brokering mechanism itself.
## What to read next
- [Subagents](./subagents): each subagent gets its own sandbox, independent of its parent.
- [Tools](./tools): authored tools run in the app runtime (full `process.env`); only sandbox tools run in the sandbox.
- [Security model](./concepts/security-model): the app-runtime/sandbox trust boundary in full.
- [Vercel Sandbox](https://vercel.com/docs/sandbox): platform docs, including credential brokering and persistence limits.