> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox

> 官方 Sandbox Docker、Vercel 或 E2B Provider。通过预制快照加速评估

Sandbox Provider 是创建和管理隔离运行环境的基础设施。[NiceEval](https://niceeval.com/) 把它们包装成同一个 `Sandbox` 接口，所以 Adapter 不需要知道当前用的是本地 Docker、Vercel micro-VM、E2B 还是第三方云服务。

## `Sandbox` 接口

Adapter 常用操作包括：

| 方法                         | 用途                                             |
| -------------------------- | ---------------------------------------------- |
| `runCommand(cmd, args)`    | 运行命令                                           |
| `runShell(script)`         | 运行 shell 脚本                                    |
| `readFile(path)`           | 读取文件                                           |
| `writeFiles(files)`        | 写入一组文件                                         |
| `uploadFiles(files)`       | 上传 workspace 或测试文件                             |
| `workdir`                  | provider 真实工作目录；省略的 `cwd` / `targetDir` 都解析到这里 |
| `runCommand(..., { cwd })` | 单条命令临时切换工作目录；相对路径按 `workdir` 解析                |
| `stop()`                   | 销毁环境                                           |

## 选择 Sandbox

在实验里设置 `sandbox` 字段

```ts theme={null}
// experiments/local.ts
import { defineExperiment } from "niceeval";
import { dockerSandbox } from "niceeval/sandbox";

export default defineExperiment({
  agent: myCodingAgent,
  model: "claude-sonnet-4-6",
  sandbox: dockerSandbox(), // 或 vercelSandbox() / e2bSandbox()
});
```

如果 experiment 和 `niceeval.config.ts` 都没有设置 `sandbox`，[NiceEval](https://niceeval.com/) 在创建 sandbox 时会直接报错，而不会猜一个 provider。

三个内置 provider 的 SDK 不随 NiceEval 一起安装——用哪个就装哪个，避免把用不到的依赖（以及它们的原生构建脚本）带进你的项目：

| provider          | 安装命令                                  |
| ----------------- | ------------------------------------- |
| `dockerSandbox()` | `pnpm add dockerode @types/dockerode` |
| `vercelSandbox()` | `pnpm add @vercel/sandbox`            |
| `e2bSandbox()`    | `pnpm add e2b`                        |

漏装时不会静默失败：NiceEval 在创建 sandbox 的那一刻报错并直接给出上面的安装命令，例如 `Docker sandbox requires 'dockerode'. Install it with: pnpm add dockerode @types/dockerode`。

## 生命周期

`dockerSandbox()` / `vercelSandbox()` / `e2bSandbox()` 返回的 spec 上有两个链式方法：`.setup(fn)` 和 `.teardown(fn)`。它们处理只有运行时才知道的环境内容，例如按实验写小配置、检查预制工具是否可用、安装 hook，或在多次 Attempt 之间载入和回存状态。

共享 helper 需要显式标注回调类型时，从公开入口导入，不要从某个 provider 的 spec 反推：

```ts theme={null}
import type { SandboxHook, SandboxHookContext } from "niceeval/sandbox";
```

稳定且体积大的依赖不要在这里重复安装。系统包、Agent CLI、编译好的二进制和大模型缓存应该先做进 Docker image、Vercel 沙箱快照或 E2B template。每个 Attempt 从预制环境启动，`.setup()` 只做薄薄的一层动态配置和 fail-fast 检查。

```ts theme={null}
export default defineExperiment({
  agent: codexAgent({ mcpServers: [mempalMcp] }),
  sandbox: e2bSandbox({ template: "fasteval-agents-mempal" }) // 已包含二进制和模型缓存
    .setup(mempalSetup("codex"))        // 预检、写 hook、载入状态
    .teardown(mempalTeardown("codex")), // 回存状态
  maxConcurrency: 1,                    // 载入和回存之间不能并发，声明串行
});
```

规则一览：

* **签名**：Hook 函数是 `(sandbox, ctx)`，不返回值。这里的 `ctx` 是窄的 Sandbox Hook Context，只有 experiment 身份、取消信号和反馈方法，不带 Agent 会话或 telemetry。
* **不可变**：每次 `.setup()` / `.teardown()` 都返回一个新 spec，原对象不变，可以继续链。
* **多个 Hook**：多个 `.setup()` 按追加顺序执行；多个 `.teardown()` 按追加的逆序执行（LIFO）；`.setup()` 链中途抛错时后续 `.setup()` 不再执行，`.teardown()` 链仍完整走完——半初始化的 Sandbox 同样要扫尾。
* **执行时机**：`setup` Hook 在 sandbox 创建后、git 基线之前最先跑——它写下的文件会进基线，不会被算进 agent 产出的 diff；`teardown` Hook 在 Adapter 的 `teardown` 之后、sandbox 销毁之前最后跑——把状态回存到外部正好用这个时机。
* **触发规则**：`teardown` 当且仅当同一 Sandbox 的 `setup` 时点已经走到才执行——`setup` 抛错不豁免，收尾代码要对可能未赋值的句柄做防御；`setup` 没跑到（Sandbox 没创建成功）则 `teardown` 同样跳过。
* **失败语义**：`setup` Hook 抛错，这次 Attempt 记为 `errored`（环境问题，不是 Agent 做错题）；`teardown` Hook 可以报告 diagnostic，默认不改变已经得到的判定。某个收尾动作是结果成立的必要条件时应抛错，由 runner 明确记录为致命错误。
* **不带 sandbox 的 Agent**：`defineAgent` 构造的 Agent 没有 sandbox，`sandbox` 字段对它不生效，Hook 自然不会跑。

### 把 setup 的产物传给 teardown

`setup` 不返回值，`teardown` 要用 `setup` 建立的句柄（一个已打开的连接、一个临时文件路径）时不能存进普通模块变量——同一个 spec 被多个并发 Attempt 复用同一个模块，模块变量会被后来的 Attempt 互相覆写。以 `sandbox` 实例作键存取：每个 Attempt 有自己独立的 `sandbox`，是天然的 per-attempt 键：

```ts theme={null}
import type { Sandbox } from "niceeval/sandbox";

// 并发 Attempt 各有自己的 sandbox：句柄按 sandbox 键控，不用普通模块变量
const forwarders = new WeakMap<Sandbox, { stop(): Promise<void> }>();

const spec = e2bSandbox({ template: "niceeval-agents" })
  .setup(async (sandbox, ctx) => {
    forwarders.set(sandbox, await startLogForwarder(sandbox, { signal: ctx.signal }));
  })
  .teardown(async (sandbox) => {
    await forwarders.get(sandbox)?.stop(); // setup 抛错也会进来：get 不到就跳过
  });
```

「载入 / 回存」这类收尾不需要 setup → teardown 句柄：状态键是 `ctx.experimentId` 的外部 KV，普通代码即可，见下段。

Hook 里可以用 `ctx.experimentId`（路径推导的实验 id）当状态隔离的键，比如不同实验各自维护一份跨 attempt 的缓存。跨 attempt 状态的载入和回存是你自己在 Hook 里写的普通代码——[NiceEval](https://niceeval.com/) 不提供状态存储；要保证同一实验的 attempt 不并发读写同一份状态，在 experiment 上声明 `maxConcurrency: 1`。

### 从 Hook 中输出进度和问题

长时间安装、预热和状态恢复可以调用 `ctx.progress(...)`。它只更新当前 Attempt 的短期状态，不会把每一步都写进结果。需要运行结束后仍能看到的问题使用 `ctx.diagnostic(...)`：

```ts theme={null}
const sandbox = e2bSandbox({ template: "niceeval-agents" })
  .setup(async (sandbox, ctx) => {
    ctx.progress({ message: "安装 memory helper", current: 1, total: 2 });
    await sandbox.runCommand("npm", ["install", "-g", "memory-helper"]);

    ctx.progress({ message: "预热 memory index", current: 2, total: 2 });
    try {
      await warmIndex(sandbox);
    } catch (error) {
      ctx.diagnostic({
        code: "memory-warmup-degraded",
        level: "warning",
        message: "预热失败，本次使用冷索引继续运行",
        data: { reason: String(error) },
        dedupeKey: "memory-warmup-degraded",
      });
    }
  });
```

`progress` 和 `diagnostic` 都不能指定全局阶段、颜色或输出流。Runner 知道当前回调属于 `sandbox.setup`，会把 Human 终端中的阶段显示为 sandbox setup。`diagnostic` 会随 Attempt 写入 `result.json`，之后可用 `niceeval show @<locator>` 回顾；它本身不会改变判定。环境无法继续时直接抛出异常。

三种 setup 各有职责。Adapter 的 `setup` 负责连接被测 Agent，例如安装 CLI 和写入鉴权配置；评估用例中 `test(t)` 开头的代码负责准备任务起始文件；Sandbox 的 `.setup()` 负责当前实验的额外环境准备。MCP server、Skill 和 model 等被测 Agent 配置仍然只从 Adapter 工厂参数传入。

## 预制环境与运行时 checkpoint

NiceEval 用 typed spec 统一引用预制环境，但不提供一个假的通用构建命令：

```ts theme={null}
dockerSandbox({ image: "my-evals:node24" })
vercelSandbox({ snapshotId: "snap_abc123" })
e2bSandbox({ template: "my-evals" })
```

Docker image、Vercel 沙箱快照和 E2B template 的凭据、构建上下文、发布和过期方式不同。项目应使用 provider 的官方工具维护构建脚本，把最终 ID 或名字放进 experiment。适合预制的判断很简单：如果所有 Attempt 都要下载或安装同一份内容，而且它稳定、昂贵或体积大，就把它移到预制环境。

### 从官方基线继续构建以提速

稳定、体积大、每个 Attempt 都相同的内容——系统包、Agent CLI、编译好的二进制、大模型缓存——应在跑评估用例之前烘焙进 provider 的可发布制品，让每个 Attempt 从预制环境启动、跳过运行时安装。三个内置 provider 都能从官方基线继续派生，不必从空白环境装 Agent；但它们的构建工具、凭据和发布语义不同，NiceEval 只统一**消费**产物 ID（`image` / `snapshotId` / `template`），不伪造跨 provider 的构建 DSL。

Adapter 与 Sandbox 不互相猜配置：Adapter 负责检查所需 CLI，Sandbox spec 负责选择 provider 和制品。Claude Code 与 Codex 缺少 CLI 时会回退到运行时安装，所以烘焙纯粹是提速；Bub 还会核对版本、OTel 插件和 Python 插件集合的安装指纹，不能仅凭 `command -v bub` 跳过安装，必须用预制环境。三个 provider 的构建都只在环境依赖变化时跑一次，产物换一个版本化名字，不要塞进每个 Attempt 的 `.setup()`。

#### E2B：从官方与公共模板派生

E2B 已提供 Claude Code 的 `claude` template 和 Codex 的 `codex` template。NiceEval 提供一个 E2B 专属的薄封装，让你从这两个官方起点继续链原生 E2B API。E2B 暂无 Bub 官方 template，因此 Bub 分支使用 NiceEval 固定到不可变 commit 的安装配方：

NiceEval 同时发布了三份任何 E2B Team 都能引用的公共模板。完整 namespace 与经过验证的 release
tag 由 NiceEval 自己维护，下游直接取完整引用：

```ts theme={null}
import {
  NICEEVAL_CLAUDE_CODE_E2B_TEMPLATE,
  NICEEVAL_CODEX_E2B_TEMPLATE,
  NICEEVAL_BUB_E2B_TEMPLATE,
} from "niceeval/sandbox/e2b-template";

e2bSandbox({ template: NICEEVAL_CLAUDE_CODE_E2B_TEMPLATE })
e2bSandbox({ template: NICEEVAL_CODEX_E2B_TEMPLATE })
e2bSandbox({ template: NICEEVAL_BUB_E2B_TEMPLATE })
```

这些基线已实际启动验证：Claude Code `2.1.207`、Codex `0.144.1`，Bub 安装指纹 `83770925b77a`。每个值都是带 tag 的完整跨 Team 引用；业务仓库不应复制这些字符串，也不应维护或读取另一份 NiceEval release 常量。派生模板需要记录 base 身份时，直接使用所选的完整 template ref。

```ts title="scripts/build-e2b-template.ts" theme={null}
import { Template } from "e2b";
import { e2bCodingAgentTemplate } from "niceeval/sandbox/e2b-template";

const template = e2bCodingAgentTemplate("codex")
  .aptInstall(["ripgrep", "jq"])
  .runCmd("corepack enable && pnpm --version")
  .copy("fixtures/toolchain.lock", "/opt/evals/toolchain.lock");

await Template.build(template, "acme-codex-evals:2026-07-13", {
  cpuCount: 2,
  memoryMB: 4096,
});
```

```bash theme={null}
e2b auth login
pnpm tsx scripts/build-e2b-template.ts
```

然后在 Experiment 里只引用构建结果：

```ts theme={null}
import { e2bSandbox } from "niceeval/sandbox";

sandbox: e2bSandbox({ template: "acme-codex-evals:2026-07-13" })
```

也可以直接从 NiceEval 公共模板继续派生，只支付项目依赖的构建成本：

```ts theme={null}
import { NICEEVAL_CODEX_E2B_TEMPLATE } from "niceeval/sandbox/e2b-template";

const template = Template()
  .fromTemplate(NICEEVAL_CODEX_E2B_TEMPLATE)
  .aptInstall(["ripgrep", "jq"])
  .runCmd("corepack enable");
```

`e2bCodingAgentTemplate("claude-code" | "codex" | "bub")` 返回原生 `TemplateBuilder`，不是 NiceEval 私有构建 DSL。你可以继续使用 `.aptInstall()`、`.runCmd()`、`.copy()` 等 E2B 能力。构建自己的 alias 会把官方起点和项目依赖冻结在同一个可复现制品里；依赖变更时重建并换一个版本化 alias。

若 Bub Adapter 配了 `pythonPlugins`，构建模板时把同一组 package 传给 factory，插件集合才会进入兼容性指纹并真正命中预装环境：

```ts theme={null}
e2bCodingAgentTemplate("bub", {
  bubPythonPackages: ["bub-plugin-memory==1.3.0"],
})
```

#### Docker：使用 NiceEval 维护的镜像，或从官方 node 基础镜像派生

要直接运行 NiceEval 内置的 `claude-code`、`codex` 或 `bub` Adapter，可以使用对应的公开镜像：
[`niceeval/claude-code`](https://hub.docker.com/r/niceeval/claude-code)、
[`niceeval/codex`](https://hub.docker.com/r/niceeval/codex) 或
[`niceeval/bub`](https://hub.docker.com/r/niceeval/bub)。每个镜像只包含自己的 Agent CLI，并为
`linux/amd64` 和 `linux/arm64` 发布 manifest；每个 NiceEval release 都有同名 tag。稳定 CI 要固定
release tag 或 digest，不要依赖会移动的 `latest`：

```ts theme={null}
import { dockerSandbox } from "niceeval/sandbox";

sandbox: dockerSandbox({ image: "niceeval/codex:v0.6.1" })
```

这个镜像是 NiceEval 维护的公开镜像，不是 Docker 的 `library/*` Official Image。它随 NiceEval
release 更新，里面的 Agent CLI 版本由该 release 的构建配方固定。

如果只需要一个 Agent，或还要加入项目专有依赖，写 Dockerfile 从 Docker 的官方基线
`node:24-slim` 派生。省略 `image` 时，NiceEval 也会按 runtime 使用该默认镜像：

```dockerfile title="Dockerfile" theme={null}
FROM node:24-slim
# slim 镜像不带 ca-certificates / git，Agent 和 npm 都要用
RUN apt-get update \
  && apt-get install -y --no-install-recommends ca-certificates git \
  && rm -rf /var/lib/apt/lists/*
# npm 全局装进 /usr/local/bin，正好落在沙箱注入的 PATH 上
RUN npm install -g @openai/codex@0.144.1
```

```bash theme={null}
docker build -t acme-codex-evals:2026-07-13 .
```

然后在 Experiment 里只引用构建结果：

```ts theme={null}
import { dockerSandbox } from "niceeval/sandbox";

sandbox: dockerSandbox({ image: "acme-codex-evals:2026-07-13" })
```

Docker Sandbox 默认以非 root 的 `node` 用户（UID 1000）跑命令，并把 `/usr/local/bin` 放进 PATH，所以 `npm install -g` 装的全局二进制天然可见；要装到别处的 Agent（如落在 `~/.local/bin`）记得让它进 PATH。本地快速迭代可以直接省略 `image` 用默认 `node:*-slim`，稳定 CI 引用不可变 tag。

#### Vercel：从官方 runtime 拍快照

Vercel 没有 E2B 式的 template registry，也没有 Dockerfile；沙箱快照是从一台跑起来的 microVM 拍出来的。用 Vercel SDK 从官方 runtime（`node24`）起一台 Sandbox，装好 Agent CLI，调 `.snapshot()` 拿到 `snap_...`，再把它交给 `vercelSandbox({ snapshotId })`：

```ts title="scripts/build-vercel-snapshot.ts" theme={null}
import { Sandbox } from "@vercel/sandbox";

const sandbox = await Sandbox.create({ runtime: "node24" }); // 官方 runtime 起 microVM
await sandbox.runCommand({
  cmd: "npm",
  args: ["install", "-g", "@openai/codex@0.144.1"],
  sudo: true, // 全局装进 /usr/local/bin，落在沙箱 PATH 上
});
const { snapshotId } = await sandbox.snapshot();
console.log(snapshotId); // snap_...
await sandbox.stop();
```

```bash theme={null}
pnpm tsx scripts/build-vercel-snapshot.ts
```

然后在 Experiment 里引用打印出的 ID：

```ts theme={null}
import { vercelSandbox } from "niceeval/sandbox";

sandbox: vercelSandbox({ snapshotId: "snap_xxx" })
```

Vercel snapshot 不支持 E2B 式公共发布：snapshot ID 受创建它的 Team/Project 权限控制，同项目成员可以复用，外部用户必须在自己的 Vercel Project 重拍一份。NiceEval 维护项目当前验证过的永不过期 snapshot 是 `snap_7sIjfs71xfmVly0WEUTGhTBoMGeL`，但它不是跨账号公共 ID。

### 运行时 checkpoint

`createCheckpoint()` / `restoreCheckpoint()` 是另一件事。它们把指定的 Linux 路径打包成 `Buffer`，可以在已创建的 Sandbox 之间恢复文件系统片段：

```ts theme={null}
import { createCheckpoint, restoreCheckpoint } from "niceeval/sandbox";

const data = await createCheckpoint(sandbox, ["/home/user/.cache/tool"]);
await restoreCheckpoint(nextSandbox, data);
```

这适合运行时缓存，不会创建可发布的 image/template/snapshot，也不管理共享、版本和过期。归档或恢复失败会直接抛错。

## 瞬时错误重试

内置 Provider 创建 Sandbox 时，限流、`fetch failed`、连接重置、5xx、临时网络不可达这类瞬时失败会自动做指数退避重试；模板不存在、凭据缺失这类配置错误第一次就报错。重试用尽后该 Attempt 记为 errored。`defineSandbox` 自定义 Provider 的 `create` 是你自己的函数，NiceEval 不替它重试。

`readFile`、`downloadFile`、`uploadFile`、批量写入和目录上传会对 429、5xx、`fetch failed`、连接重置等瞬时传输错误自动做有限重试。文件不存在、权限错误、取消和 Sandbox terminated 不重试。

`runCommand` 与 `runShell` 不自动重试。命令可能已经产生副作用，只有你能确认它可安全重复时，才在 hook 或评估用例中显式重试。

## Docker

Docker 适合本地开发和标准 CI。优点是简单、可控、无云端依赖；缺点是机器资源有限，冷启动和安装依赖可能较慢。

## Vercel Sandbox

Vercel Sandbox 适合需要云端隔离、更多资源或更稳定环境的任务。需要相应 token 或 OIDC 配置。

## 自定义 Provider

用 `defineSandbox` 接入其它服务。`create` 的 `feedback` 已绑定到 `sandbox.create` 阶段，可以报告分配实例、拉镜像或恢复沙箱快照的状态：

```ts theme={null}
import { defineSandbox } from "niceeval/sandbox";

export default defineSandbox({
  name: "modal",
  recommendedConcurrency: 8,
  async create({ timeout, runtime, feedback }) {
    feedback.progress({ message: "分配 Modal Sandbox" });
    const instance = await allocateModal({ timeout, runtime });

    if (instance.usedFallbackRegion) {
      feedback.diagnostic({
        code: "modal-fallback-region",
        level: "warning",
        message: `主区域不可用，使用 ${instance.region}`,
        data: { region: instance.region },
      });
    }

    return new ModalSandbox(instance);
  },
});
```

返回值实现 `Sandbox` 接口即可。Provider SDK 的原始日志不要直接写宿主进程的 `stdout` / `stderr`；短期状态走 `feedback.progress`，需要保留的问题走 `feedback.diagnostic`，无法创建环境时抛错。这样 Human dashboard 不会被日志打散，CI 也能保持单一有序输出。

## 权限和 root

不同 provider 对 root 权限、网络、文件系统和进程生命周期的约束不同。编写 fixture 时尽量避免依赖宿主机环境，把依赖写进 `package.json` 或 fixture setup。

## 性能建议

* 把稳定重依赖做进 image/template/snapshot，不在每个 Attempt 的 `.setup()` 重装。
* 减少 fixture 依赖体积。
* 对动态内容使用小而明确的缓存或预检。
* 控制 `maxConcurrency`（experiment 字段或 `--max-concurrency`），避免本地 Docker 资源耗尽。
* 把慢测试拆成必要的 gate 和可选的 soft 检查。

Warm pools 和复用属于 runner / scheduler 层面的能力，详见 [Runner](/docs/zh/explanation/runner)。
