不预制也能跑
预制只是为了更快。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 维护,直接用常量:
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:
sudo npm install -g,模板里已有的 root 全局包可能产生文件冲突。NiceEval 会在之后发布的模板里统一这个默认 prefix;在发布说明明确新模板已经处理之前,继续保留 --prefix /usr/local。
从官方 Codex template 派生自己的模板:
scripts/build-e2b-template.ts
e2bCodingAgentTemplate("claude-code" | "codex" | "bub") 返回 E2B 原生的 TemplateBuilder,可以继续用 .aptInstall()、.runCmd()、.copy() 等 E2B 能力。构建出的 alias 把官方起点和项目依赖固定在同一个可复现的结果里。依赖变了就重新构建,换一个带版本号的 alias。
Bub Adapter 配了 pythonPlugins 时,构建模板时要把同一组包传给 factory。这样运行时才会识别出插件已经预装:
Docker:使用 NiceEval 维护的镜像,或从官方 node 基础镜像派生
要直接运行 NiceEval 内置的claude-code、codex 或 bub Adapter,可以用对应的公开镜像:niceeval/claude-code、niceeval/codex 或 niceeval/bub。
每个镜像只装自己的 Agent CLI,同时提供 linux/amd64 和 linux/arm64 版本。tag 和对应的 E2B 公共模板同号,版本号是镜像里 Agent 的版本。稳定的 CI 用具名常量或 digest,不要用会变的 latest:
library/* 官方镜像。Agent 版本或构建方式变化时才发布新 tag,和 NiceEval 库的发版节奏无关。
只需要一个 Agent,或者还要加项目自己的依赖时,写 Dockerfile 从 Docker 官方的 node:24-slim 派生:
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 }):
scripts/build-vercel-snapshot.ts
snap_7sIjfs71xfmVly0WEUTGhTBoMGeL,但它不能跨账号使用。
运行时 checkpoint
createCheckpoint() / restoreCheckpoint() 解决的是另一件事:把 Sandbox 里指定的 Linux 路径打包成 Buffer,再恢复到另一台已经创建好的 Sandbox:
.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、出错回滚或跨机器协调。