> ## 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 环境：让每次评估更快开始

> 把系统包、Agent CLI 和大缓存做进 Docker 镜像、Vercel 快照或 E2B template，从官方基线派生；运行时缓存用 checkpoint 在 Attempt 之间延续。

每个 Attempt 都从同一个起点开始。系统包、Agent CLI、编译好的二进制、大模型缓存这类内容稳定、体积大，每次都一样，与其每个 Attempt 重装一遍，不如在跑评估之前做进预制环境，让 Attempt 直接从装好的环境启动。

三种 Provider 引用预制环境的写法一致：

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

NiceEval 只读取构建结果的 ID，不提供跨 Provider 的构建命令。Docker 镜像、Vercel 快照和 E2B template 的凭据、构建方式、发布和过期规则各不相同，构建脚本用 Provider 自己的官方工具维护，最后把 ID 或名字写进 Experiment。

## 不预制也能跑

预制只是为了更快。Adapter 和 Sandbox 各管一边：Sandbox 配置决定用哪个 Provider、从哪个预制环境启动；Adapter 检查需要的 CLI 在不在。Claude Code 和 Codex 缺少 CLI 时会在运行时安装：安装包在评估网络之外准备好，再通过文件接口送进 Sandbox。

* Codex 送进 Sandbox 的是自带运行时的原生安装包，镜像里没有 Node 也能装。
* Claude Code 要求镜像里有 npm。
* 默认的 Bub 0.4.0 除了 OTel 插件，还固定了 `any-llm-sdk==1.17.0` 和 `openai==2.31.0`。Bub 版本、这两个依赖、OTel 插件或 Python 插件列表任何一项变了，都会重新安装，不能只凭 `command -v bub` 判断已经装好。用旧安装标记做的预制环境会重装一次。你显式选了其它 Bub 版本时，它依赖的其它包由你自己负责。

## 从官方基线继续构建

三个内置 Provider 都可以从官方基线继续构建，不用从空白环境装 Agent。预制环境只在依赖变化时构建一次，每次构建换一个带版本号的名字。不要把构建放进每个 Attempt 都会执行的动态回调。

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

E2B 官方提供 Claude Code 的 `claude` template 和 Codex 的 `codex` template。NiceEval 提供一个 E2B 专用的薄封装，让你从这两个官方起点继续用 E2B 原生 API 构建。E2B 没有官方的 Bub template，所以 Bub 分支和 `bubAgent()` 使用同一套固定依赖和 OTel 插件。

NiceEval 还发布了三份任何 E2B Team 都能引用的公共模板。完整的名字和版本由 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 `0.4.0`。模板版本号跟着里面的 Agent 走，形如 `0.144.1-r1`，即 Agent 版本加 NiceEval 的构建修订号。三个 Agent 各自独立发版，和 NiceEval 库的版本无关。所以项目里直接用常量，不要复制这些字符串，也不用自己跟踪版本。派生模板需要记下基础模板时，同样用常量的值。

这批公共模板里，Claude Code 模板沿用 E2B 官方的 `/usr` npm prefix，普通用户不能直接写全局模块。运行时要再装 pnpm、yarn 等全局 Node 工具时，显式装到 `/usr/local`，两个模板都已经把它加进 PATH：

```ts theme={null}
await t.sandbox.runCommand("npm", [
  "install", "-g", "--prefix", "/usr/local", "pnpm@10.34.5",
]);
```

不要改用 `sudo npm install -g`，模板里已有的 root 全局包可能产生文件冲突。NiceEval 会在之后发布的模板里统一这个默认 prefix；在发布说明明确新模板已经处理之前，继续保留 `--prefix /usr/local`。

从官方 Codex template 派生自己的模板：

```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:0.144.1-r1", {
  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:0.144.1-r1" })
```

也可以直接从 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")` 返回 E2B 原生的 `TemplateBuilder`，可以继续用 `.aptInstall()`、`.runCmd()`、`.copy()` 等 E2B 能力。构建出的 alias 把官方起点和项目依赖固定在同一个可复现的结果里。依赖变了就重新构建，换一个带版本号的 alias。

Bub Adapter 配了 `pythonPlugins` 时，构建模板时要把同一组包传给 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` 版本。tag 和对应的 E2B 公共模板同号，版本号是镜像里 Agent 的版本。稳定的 CI 用具名常量或 digest，不要用会变的 `latest`：

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

sandbox: dockerSandbox({ source: { type: "image", image: NICEEVAL_CODEX_DOCKER_IMAGE } })
```

这些是 NiceEval 维护的公开镜像，不是 Docker 的 `library/*` 官方镜像。Agent 版本或构建方式变化时才发布新 tag，和 NiceEval 库的发版节奏无关。

只需要一个 Agent，或者还要加项目自己的依赖时，写 Dockerfile 从 Docker 官方的 `node:24-slim` 派生：

```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，正好在 Sandbox 的 PATH 上
RUN npm install -g @openai/codex@0.144.1
```

```bash theme={null}
docker build -t acme-codex-evals:0.144.1-r1 .
```

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

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

sandbox: dockerSandbox({ source: { type: "image", image: "acme-codex-evals:0.144.1-r1" } })
```

Docker Sandbox 用镜像自己声明的用户运行命令。上面的 Dockerfile 没写 `USER`，所以默认以 root 运行，`/usr/local/bin` 已经在 root 的 PATH 上，`npm install -g` 装的命令可以直接用。

需要非 root 用户时（比如 Claude Code 在 root 下会拒绝 `--dangerously-skip-permissions`），在 Dockerfile 里加一行 `USER node`，`node:24-slim` 自带这个用户。也可以用 `dockerSandbox({ source: { type: "image", image }, user: "node" })` 覆盖。

Agent 装到别处时（比如 `~/.local/bin`），记得把那个目录加进 PATH。`dockerSandbox` 必须写明 image，稳定的 CI 引用不会变的 tag。

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

Vercel 没有 E2B 那样的 template 仓库，也不用 Dockerfile。Vercel 的快照是从一台运行中的 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，正好在 Sandbox 的 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 快照不能像 E2B 那样公开发布。快照 ID 受创建它的 Team/Project 权限控制，同一个项目的成员可以复用，其他人要在自己的 Vercel Project 里重新拍一份。NiceEval 项目自己验证过、永不过期的快照是 `snap_7sIjfs71xfmVly0WEUTGhTBoMGeL`，但它不能跨账号使用。

## 运行时 checkpoint

`createCheckpoint()` / `restoreCheckpoint()` 解决的是另一件事：把 Sandbox 里指定的 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 或快照，也不管共享、版本和过期。打包或恢复失败会直接抛错。

要让这些目录在 Attempt 之间延续，在动态 `.before()` 里调用 `restoreCheckpoint()`。恢复成功后，用 `context.onCleanup()` 登记 `createCheckpoint()`，这样回存会在 Sandbox 销毁之前完成。

`sandboxReuse: true` 只在一次 `niceeval exp` 命令内部保留同一台 Sandbox。几个终端同时运行、都要读写同一份 checkpoint 时，在 Experiment 顶层声明 `sharedState: { key }`。NiceEval 在 Experiment `setup` 和创建 Sandbox 之前取得这个 key 的租约，等 checkpoint 回存、Sandbox 销毁和 Experiment `teardown` 都完成后才释放。它只保证同一时刻只有一方在用，不负责保存 checkpoint、出错回滚或跨机器协调。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.