# Sandbox

> defineSandbox backends (local vs vercel), workspace seeding from agent/sandbox/workspace, bootstrap/onSession lifecycle, build-time prewarm, and proxy execution for shell/file tools.

- Repository: vercel/eve
- GitHub: https://github.com/vercel/eve
- Human docs: https://grok-wiki.com/public/docs/vercel-eve-759e1d74a10f
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-eve-759e1d74a10f/llms-full.txt

## Source Files

- `docs/sandbox.mdx`
- `packages/eve/src/public/definitions/sandbox.ts`
- `packages/eve/src/compiler/normalize-sandbox.ts`
- `packages/eve/src/execution/sandbox/prewarm.ts`
- `packages/eve/src/compiler/workspace-resources.ts`
- `docs/guides/deployment.md`

---

---
title: "Sandbox"
description: "defineSandbox backends (local vs vercel), workspace seeding from agent/sandbox/workspace, bootstrap/onSession lifecycle, build-time prewarm, and proxy execution for shell/file tools."
---

Every Eve agent owns exactly one isolated bash environment rooted at `/workspace`. The harness runs in the app runtime; shell and file operations execute inside the sandbox backend. Built-in `bash`, `read_file`, `write_file`, `glob`, and `grep` tools proxy through shared executors (`executeBashOnSandbox`, `executeReadFileOnSandbox`, and siblings) that resolve a live `SandboxSession` from the runtime context. Authored tools call `ctx.getSandbox()` for the same handle. Override behavior with `defineSandbox` only when you need setup hooks, seeded files, a pinned backend, or network policy.

## Architecture

```mermaid
flowchart TB
  subgraph harness["App runtime (harness)"]
    tools["Framework tools: bash, read_file, write_file, glob, grep"]
    authored["Authored tools via ctx.getSandbox()"]
    proxy["requireSandboxSession() → SandboxAccess.get()"]
    tools --> proxy
    authored --> proxy
  end

  subgraph lifecycle["Sandbox lifecycle"]
    prewarm["backend.prewarm() at build/dev"]
    create["backend.create() per durable session"]
    bootstrap["bootstrap({ use }) — template-scoped"]
    onSession["onSession({ use, ctx }) — session-scoped"]
    prewarm --> bootstrap
    create --> onSession
  end

  subgraph backends["SandboxBackend"]
    vercel["vercel() — Vercel Sandbox"]
    docker["docker() — local Docker"]
    micro["microsandbox() — local VM"]
    justbash["justbash() — pure-JS fallback"]
    default["defaultBackend() — availability chain"]
  end

  proxy --> create
  create --> backends
  prewarm --> backends
```

<Note>
Authored tool `execute` functions run in the app runtime with full `process.env`. Only sandbox-backed operations cross the trust boundary into `/workspace`.
</Note>

## Default tools and proxy execution

The model receives shell and file access through built-in tools. Each tool handler stays in the harness; execution delegates to sandbox primitives:

| Tool | Sandbox executor | Working directory |
| --- | --- | --- |
| `bash` | `executeBashOnSandbox` | `/workspace` |
| `read_file` | `executeReadFileOnSandbox` | `/workspace` |
| `write_file` | `executeWriteFileOnSandbox` | `/workspace` |
| `glob` | `executeGlobOnSandbox` | `/workspace` |
| `grep` | `executeGrepOnSandbox` | `/workspace` |

`requireSandboxSession()` centralizes context lookup: it reads `SandboxKey` from the active async-local scope, calls `SandboxAccess.get()`, and throws when sandbox access is missing or unavailable. Framework tools import this preamble statically so Nitro bundles can trace them; backend bindings load lazily inside the execution layer.

`write_file` enforces a read-before-write guarantee: the model must call `read_file` on a path before overwriting it. `glob` and `grep` probe for `rg` once per session and fall back to POSIX `find`/`grep` when ripgrep is absent. `bash` tail-truncates stdout and stderr to shared output limits.

Author a shell tool with `defineBashTool` to reuse the same executor core:

```ts title="agent/tools/run_check.ts"
import { defineBashTool } from "eve/tools";

export default defineBashTool({
  description: "Run a one-off shell check in the workspace.",
});
```

## SandboxSession API

`ctx.getSandbox()` is async, takes no arguments, and works only inside authored runtime functions (tools, hooks, channel handlers). Relative paths resolve from `/workspace`; absolute paths pass through unchanged.

| Method | Behavior |
| --- | --- |
| `run({ command })` | Run one command, block until exit, return `{ stdout, stderr, exitCode }` |
| `spawn(options)` | Launch a long-running process; returns `SandboxProcess` with `stdout`/`stderr` streams, `wait()`, `kill()` |
| `readTextFile` / `writeTextFile` | UTF-8 (or specified encoding) text I/O; `readTextFile` supports 1-based line ranges |
| `readBinaryFile` / `writeBinaryFile` | Raw byte I/O |
| `readFile` / `writeFile` | Stream byte I/O |
| `removePath({ path, force, recursive })` | Delete files or directories |
| `resolvePath(path)` | Anchor a relative path to `/workspace/...` |
| `setNetworkPolicy(policy)` | Change egress mid-turn (backend-dependent) |

`sandbox.id` is a stable per-session identifier across reconnects. Use it as a cache key for per-session state that must outlive individual step executions.

Option types (`SandboxSpawnOptions`, `SandboxReadTextFileOptions`, and others) export from `eve/sandbox`.

## Workspace seeding

Mount authored files into `/workspace` at session start by placing them under `agent/sandbox/workspace/`. This requires the folder layout (`agent/sandbox/sandbox.ts`), not the top-level shorthand:

:::files
agent/sandbox/
  sandbox.ts                ← optional override
  workspace/
    schema.sql              ← lands at /workspace/schema.sql
    scripts/run.sh          ← lands at /workspace/scripts/run.sh
:::

At compile time, Eve copies `sandboxWorkspaces` and skill packages into `.eve/compile/workspace-resources/<nodeId>/`, hashes the tree, and records a `workspaceResourceRoot` on the compiled manifest. Prewarm and session create pass those files as `seedFiles` to the backend.

Every file under `workspace/` mirrors into the sandbox cwd with structure intact. Eve lists top-level entries in the model prompt automatically.

<Warning>
`agent/sandbox/workspace/skills/` is reserved. Skill discovery seeds `/workspace/skills/` from `agent/skills/`; authoring `workspace/skills/...` is rejected at compile time.
</Warning>

## Authoring with defineSandbox

`defineSandbox` from `eve/sandbox` is an identity helper that attaches types. `backend` is optional; when omitted, the runtime substitutes `defaultBackend()`.

Two discovery layouts exist:

| Layout | Path | Use when |
| --- | --- | --- |
| Shorthand | `agent/sandbox.ts` | Definition only, no seeded files |
| Folder | `agent/sandbox/sandbox.ts` + `workspace/` | Seeded files or folder organization |

If both exist, the folder layout wins. Subagents override independently at `subagents/<name>/sandbox.ts` (or the folder form) and do not inherit the parent's sandbox.

```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" });
  },
});
```

<ParamField body="backend" type="SandboxBackend | () => SandboxBackend">
Backend factory or value. Factory form is invoked lazily on first access and memoized for the process lifetime.
</ParamField>

<ParamField body="bootstrap" type="(input: SandboxBootstrapContext) => void | Promise<void>">
Template-scoped hook. Call `use(options?)` to open the template session before snapshot capture. Only template filesystem state carries into later sessions.
</ParamField>

<ParamField body="onSession" type="(input: SandboxSessionContext) => void | Promise<void>">
Durable-session-scoped hook. Runs once per session before steps use the sandbox. `input.ctx` exposes `session.id`, `auth`, and `turn`. Call `use(opts?)` to apply per-session backend options.
</ParamField>

<ParamField body="revalidationKey" type="() => string | Promise<string>">
Required when `bootstrap` is present and external inputs affect template output. Evaluated at compile/build time and frozen into compiled artifacts. Authored source and seed contents are tracked automatically.
</ParamField>

When no `agent/sandbox.ts` (or folder equivalent) exists, the framework auto-provides a default sandbox via `defaultBackend()`.

## Backends

A `SandboxBackend` implements `prewarm` (build-time template capture) and `create` (runtime session open/reattach). Eve ships four pinned factories plus an availability-aware default:

| Import | Backend name | Runs where |
| --- | --- | --- |
| `vercel()` from `eve/sandbox/vercel` | `vercel` | [Vercel Sandbox](https://vercel.com/docs/sandbox) |
| `docker()` from `eve/sandbox/docker` | `local` (Docker) | Local Docker daemon via CLI |
| `microsandbox()` from `eve/sandbox/microsandbox` | `local` (microsandbox) | Lightweight local VM |
| `justbash()` from `eve/sandbox/just-bash` | `local` (just-bash) | Pure-JS interpreter, no real binaries |
| `defaultBackend()` from `eve/sandbox` | Resolved at runtime | Availability chain |

### Local vs Vercel

<Tabs>
  <Tab title="Hosted Vercel">
    Pin with `vercel()` or omit `backend` and let `defaultBackend()` select Vercel when `process.env.VERCEL` is set. Local container/VM backends cannot run on Vercel infrastructure. Sessions idle-timeout (default 30 minutes on Vercel); Eve preserves the filesystem and resumes on the next message. Domain-level network policies and credential brokering are fully supported.
  </Tab>
  <Tab title="Local development">
    With `backend` omitted, `defaultBackend()` resolves in priority order: Docker (reachable daemon) → microsandbox (Apple Silicon macOS or glibc Linux with KVM) → just-bash (dependency-free fallback). Pin a backend unconditionally with `docker()`, `microsandbox()`, or `justbash()`. `vercel()` also works locally when Vercel credentials are configured.
  </Tab>
</Tabs>

`defaultBackend()` accepts a keyed options bag so each inner backend gets typed create options:

```ts title="agent/sandbox/sandbox.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 },
  }),
});
```

### Backend notes

**Docker** — Default image `ghcr.io/vercel/eve:latest`. Eve creates `/workspace` and verifies Bash before authored `bootstrap`. Templates reuse across sessions when source, seeds, `revalidationKey`, and Docker options match. `eve dev` prunes stale template images in the background.

**microsandbox** — Closest local match to hosted Vercel Sandbox: snapshot templates, `vercel-sandbox` user, domain-level firewall. `eve dev` auto-installs the `microsandbox` package when missing; production fails with actionable install errors.

**just-bash** — No daemon or VM. Virtual filesystem under `.eve/sandbox-cache/`. No real binaries (`git`, `node`, package managers) and no network isolation. `eve dev` auto-installs `just-bash` when missing.

Custom backends implement the public `SandboxBackend` interface (`name`, `prewarm`, `create`). Types export from `eve/sandbox`.

## Lifecycle

```mermaid
stateDiagram-v2
  [*] --> TemplatePlan: compile discovers sandbox
  TemplatePlan --> Skipped: no bootstrap, no seeds
  TemplatePlan --> Prewarm: seeds and/or bootstrap
  Prewarm --> TemplateReady: backend.prewarm captures snapshot
  TemplateReady --> SessionCreate: first message in durable session
  SessionCreate --> OnSession: onSession({ use, ctx }) once
  OnSession --> Live: steps proxy tools to SandboxSession
  Live --> Live: turns reuse persisted session state
  Live --> Resume: backend idle timeout / crash
  Resume --> Live: backend.create with existingMetadata
```

### bootstrap vs onSession

| Hook | Scope | When it runs | What persists |
| --- | --- | --- | --- |
| `bootstrap({ use })` | Template | Once when the template is built (prewarm) | Template filesystem and supported backend metadata |
| `onSession({ use, ctx })` | Durable session | Once per session, inside active runtime context | Per-session config (network policy, resources, credentials) |

Put reusable setup in `bootstrap` (clone baseline repo, install dependencies). Put per-session setup in `onSession` (network lockdown, per-user markers). Because `onSession` runs inside the harness, it can read `ctx.session.auth` without baking credentials into the template.

Network policy set on the backend factory applies before authored `bootstrap`. Policy set in `onSession`'s `use()` overrides per session. Call `sandbox.setNetworkPolicy(...)` on the live handle to change policy mid-turn.

### Template key strategies

`createRuntimeSandboxTemplatePlan` chooses how templates are keyed:

| Plan kind | Condition | Prewarm |
| --- | --- | --- |
| `none` | No `bootstrap`, no workspace seeds | Skipped (`templateKey` is `null`) |
| `workspace-content` | Seeds only | Keyed by workspace content hash |
| `bootstrap` | `bootstrap()` present | Keyed by `revalidationKey`, source hash, and content hash |

Template keys include `nodeId` so root and subagent sandboxes do not collide. Vercel stable templates key on `VERCEL_PROJECT_ID` so build-time prewarm and deployed runtime derive the same key.

## Build-time prewarm

Prewarm captures reusable sandbox templates before traffic arrives. `prewarmSandboxes` iterates every registered sandbox in the compiled agent graph and calls `backend.prewarm(...)` with `bootstrap`, `seedFiles`, and a stable `templateKey`.

<Steps>
  <Step title="Compile workspace resources">
    `materializeWorkspaceResources` writes `.eve/compile/workspace-resources/` and records content hashes on the manifest.
  </Step>
  <Step title="Collect prewarm targets">
    For each graph node with a sandbox, derive `templatePlan` and `templateKey`. Skip nodes where the plan is `none`.
  </Step>
  <Step title="Invoke backend.prewarm">
    Backends capture template state idempotently. Build logs report `reused` vs freshly built templates.
  </Step>
  <Step title="Runtime session create">
    `ensureSandboxAccess` calls `backend.create({ templateKey, sessionKey, existingMetadata })`. Missing templates throw `SandboxTemplateNotProvisionedError`; dev mode retries after on-demand prewarm.
  </Step>
</Steps>

### When prewarm runs

| Environment | Trigger | Failure behavior |
| --- | --- | --- |
| Vercel build | `VERCEL` and `VERCEL_DEPLOYMENT_ID` both set | Build fails (`runVercelBuildPrewarm`) |
| `eve dev` | Background prewarm on startup and after authored-source changes | Logged error; retried on first sandbox access |
| `eve start` (production) | `prewarmBuiltAppSandboxes` before serving | Startup failure |

Prewarm covers template construction only. `onSession()` still runs at runtime, once per durable session.

<Check>
Build logs show lines like `Eve: initialized N sandbox templates (X reused, Y built)`. A cache hit reports `reused: true` from the backend.
</Check>

## Network policy and credential brokering

Egress rules accept 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"] },
}
```

Domain-level allow-lists and credential brokering work on `vercel()` and `microsandbox()`. Docker honors only `"allow-all"` and `"deny-all"`. just-bash rejects `setNetworkPolicy` entirely.

Secrets never enter the sandbox process. Per-domain `transform` injects headers at the firewall:

```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 `transform` applies only to `github.com`.

## Troubleshooting

| Symptom | Likely cause |
| --- | --- |
| `SandboxTemplateNotProvisionedError` | Template not prewarmed; dev retries automatically, production needs successful build prewarm |
| `agent/sandbox/workspace/skills/` discovery error | Reserved path; move skills to `agent/skills/` |
| `This tool requires sandbox access on the runtime context` | Tool called outside a managed harness step |
| `filePath must be an absolute path` | Model passed a relative path to `read_file`/`write_file`; use `/workspace/...` |
| Build fails during sandbox prewarm | `bootstrap()` threw or backend credentials missing; fix bootstrap or Vercel Sandbox access |
| just-bash / microsandbox missing in production | Optional peers not installed; `eve dev` auto-installs, production requires explicit install |

## Related pages

<CardGroup>
  <Card title="Security model" href="/security-model">
    App runtime vs sandbox trust boundaries, secret brokering, and fail-closed defaults.
  </Card>
  <Card title="Default harness" href="/default-harness">
    Built-in bash and file tools, compaction, and override patterns.
  </Card>
  <Card title="Project layout" href="/project-layout">
    `agent/sandbox/` discovery, path-derived naming, and `.eve/` compile artifacts.
  </Card>
  <Card title="Auth and deployment" href="/auth-and-deployment">
    Vercel build output, sandbox backend selection, prewarm constraints, and production verification.
  </Card>
  <Card title="State, hooks, and session context" href="/state-hooks-and-context">
    `ctx.getSandbox()`, `ctx.getSkill()`, and where managed-context APIs are valid.
  </Card>
  <Card title="Subagents and schedules" href="/subagents-and-schedules">
    Each subagent gets its own sandbox, independent of its parent.
  </Card>
</CardGroup>
