跳转到主要内容
Sandbox Provider 是创建和管理隔离运行环境的基础设施。NiceEval 把它们包装成同一个 Sandbox 接口,所以 Adapter 不需要知道当前用的是本地 Docker、Vercel micro-VM、E2B 还是第三方云服务。

Sandbox 接口

Adapter 常用操作包括:

选择 Sandbox

在实验里设置 sandbox 字段
如果 experiment 和 niceeval.config.ts 都没有设置 sandboxNiceEval 在创建 sandbox 时会直接报错,而不会猜一个 provider。 三个内置 provider 的 SDK 不随 NiceEval 一起安装——用哪个就装哪个,避免把用不到的依赖(以及它们的原生构建脚本)带进你的项目: 漏装时不会静默失败: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 反推:
稳定且体积大的依赖不要在这里重复安装。系统包、Agent CLI、编译好的二进制和大模型缓存应该先做进 Docker image、Vercel 沙箱快照或 E2B template。每个 Attempt 从预制环境启动,.setup() 只做薄薄的一层动态配置和 fail-fast 检查。
规则一览:
  • 签名: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 的 AgentdefineAgent 构造的 Agent 没有 sandbox,sandbox 字段对它不生效,Hook 自然不会跑。

把 setup 的产物传给 teardown

setup 不返回值,teardown 要用 setup 建立的句柄(一个已打开的连接、一个临时文件路径)时不能存进普通模块变量——同一个 spec 被多个并发 Attempt 复用同一个模块,模块变量会被后来的 Attempt 互相覆写。以 sandbox 实例作键存取:每个 Attempt 有自己独立的 sandbox,是天然的 per-attempt 键:
「载入 / 回存」这类收尾不需要 setup → teardown 句柄:状态键是 ctx.experimentId 的外部 KV,普通代码即可,见下段。 Hook 里可以用 ctx.experimentId(路径推导的实验 id)当状态隔离的键,比如不同实验各自维护一份跨 attempt 的缓存。跨 attempt 状态的载入和回存是你自己在 Hook 里写的普通代码——NiceEval 不提供状态存储;要保证同一实验的 attempt 不并发读写同一份状态,在 experiment 上声明 maxConcurrency: 1

从 Hook 中输出进度和问题

长时间安装、预热和状态恢复可以调用 ctx.progress(...)。它只更新当前 Attempt 的短期状态,不会把每一步都写进结果。需要运行结束后仍能看到的问题使用 ctx.diagnostic(...)
progressdiagnostic 都不能指定全局阶段、颜色或输出流。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 统一引用预制环境,但不提供一个假的通用构建命令:
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 自己维护,下游直接取完整引用:
这些基线已实际启动验证:Claude Code 2.1.207、Codex 0.144.1,Bub 安装指纹 83770925b77a。每个值都是带 tag 的完整跨 Team 引用;业务仓库不应复制这些字符串,也不应维护或读取另一份 NiceEval release 常量。派生模板需要记录 base 身份时,直接使用所选的完整 template ref。
scripts/build-e2b-template.ts
然后在 Experiment 里只引用构建结果:
也可以直接从 NiceEval 公共模板继续派生,只支付项目依赖的构建成本:
e2bCodingAgentTemplate("claude-code" | "codex" | "bub") 返回原生 TemplateBuilder,不是 NiceEval 私有构建 DSL。你可以继续使用 .aptInstall().runCmd().copy() 等 E2B 能力。构建自己的 alias 会把官方起点和项目依赖冻结在同一个可复现制品里;依赖变更时重建并换一个版本化 alias。 若 Bub Adapter 配了 pythonPlugins,构建模板时把同一组 package 传给 factory,插件集合才会进入兼容性指纹并真正命中预装环境:

Docker:使用 NiceEval 维护的镜像,或从官方 node 基础镜像派生

要直接运行 NiceEval 内置的 claude-codecodexbub Adapter,可以使用对应的公开镜像: niceeval/claude-codeniceeval/codexniceeval/bub。每个镜像只包含自己的 Agent CLI,并为 linux/amd64linux/arm64 发布 manifest;每个 NiceEval release 都有同名 tag。稳定 CI 要固定 release tag 或 digest,不要依赖会移动的 latest
这个镜像是 NiceEval 维护的公开镜像,不是 Docker 的 library/* Official Image。它随 NiceEval release 更新,里面的 Agent CLI 版本由该 release 的构建配方固定。 如果只需要一个 Agent,或还要加入项目专有依赖,写 Dockerfile 从 Docker 的官方基线 node:24-slim 派生。省略 image 时,NiceEval 也会按 runtime 使用该默认镜像:
Dockerfile
然后在 Experiment 里只引用构建结果:
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 })
scripts/build-vercel-snapshot.ts
然后在 Experiment 里引用打印出的 ID:
Vercel snapshot 不支持 E2B 式公共发布:snapshot ID 受创建它的 Team/Project 权限控制,同项目成员可以复用,外部用户必须在自己的 Vercel Project 重拍一份。NiceEval 维护项目当前验证过的永不过期 snapshot 是 snap_7sIjfs71xfmVly0WEUTGhTBoMGeL,但它不是跨账号公共 ID。

运行时 checkpoint

createCheckpoint() / restoreCheckpoint() 是另一件事。它们把指定的 Linux 路径打包成 Buffer,可以在已创建的 Sandbox 之间恢复文件系统片段:
这适合运行时缓存,不会创建可发布的 image/template/snapshot,也不管理共享、版本和过期。归档或恢复失败会直接抛错。

瞬时错误重试

内置 Provider 创建 Sandbox 时,限流、fetch failed、连接重置、5xx、临时网络不可达这类瞬时失败会自动做指数退避重试;模板不存在、凭据缺失这类配置错误第一次就报错。重试用尽后该 Attempt 记为 errored。defineSandbox 自定义 Provider 的 create 是你自己的函数,NiceEval 不替它重试。 readFiledownloadFileuploadFile、批量写入和目录上传会对 429、5xx、fetch failed、连接重置等瞬时传输错误自动做有限重试。文件不存在、权限错误、取消和 Sandbox terminated 不重试。 runCommandrunShell 不自动重试。命令可能已经产生副作用,只有你能确认它可安全重复时,才在 hook 或评估用例中显式重试。

Docker

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

Vercel Sandbox

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

自定义 Provider

defineSandbox 接入其它服务。createfeedback 已绑定到 sandbox.create 阶段,可以报告分配实例、拉镜像或恢复沙箱快照的状态:
返回值实现 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