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

Sandbox 接口

Adapter 常用操作包括: 两个 OrThrow 方法抛出的错误消息会附带经过清理、脱敏和截断的 stderr 尾部。stderr 为空时回退 stdout。排障时可以先看这条摘要,需要完整输出时再读取 SandboxCommandExitError.result

选择 Sandbox

在实验里设置 sandbox 字段
Eval 与 Experiment 都没声明 template-bearing layer 时,link planning 会在创建任何 Sandbox 前失败,而不会猜一个 provider。 三个内置 provider 的 SDK 不随 NiceEval 一起安装——用哪个就装哪个,避免把用不到的依赖(以及它们的原生构建脚本)带进你的项目: 漏装时不会静默失败:NiceEval 在创建 sandbox 的那一刻报错并直接给出上面的安装命令,例如 Docker sandbox requires 'dockerode'. Install it with: pnpm add dockerode @types/dockerode

Docker Compose:workspaceService 指定主 Sandbox

一道题需要不止一个容器时(比如应用容器加一个数据库),用 dockerComposeSandbox 声明整份 Compose 环境:
workspaceService 指定 Compose 文件里哪个 service 是主 Sandbox。Agent、t.sandbox 的命令与文件操作、workdir、变更 diff,全部只落在这一个容器上。Compose 文件里的其它 service(数据库、mock server 之类)是伴随资源,不经过 Sandbox 接口——不能对它们 runCommand、上传或读写文件,也看不到它们的 diff。要用到这些伴随 service,只能靠题目自己的网络访问,例如从 client 容器里用 Compose 内置 DNS 访问 db:5432

让 Agent自己运行 Docker

如果 Agent需要执行 docker builddocker rundocker compose,可以显式挂已有 Unix socket, 也可以选择 raw privileged或 managed rootless Docker-in-Docker。三种模式的适用场景、Dockerfile和 完整配置见让 Sandbox使用 Docker

生命周期

dockerSandbox({ source: { type: "image", image } })vercelSandbox({ snapshotId })e2bSandbox({ template }) 都返回 SandboxLayer。返回值有 .prepare(command).setup(fn).teardown(fn) 方法。 这些方法处理只有运行时才知道的环境内容。例如按实验写配置、检查预制工具、安装 hook,或在多次 Attempt 之间载入和回存状态。 共享辅助函数需要显式标注回调类型时,从公开入口导入,不要从某个 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(...)
progressdiagnosticfact 是互斥的反馈通道。短期状态用 progress,真实异常、退化或需要处理的问题才用 diagnostic,中性运行事实用 ctx.fact(...)。fact 使用反向域 name,完整 JSON document 上限为 65,536 UTF-8 bytes。正常容量、缓存大小、版本和命中状态不能无条件显示成 warning。环境无法继续时直接抛出异常。
progressdiagnostic 都不能指定全局阶段、颜色或输出流。Runner 知道当前回调属于 sandbox.setup,会把 Human 终端中的阶段显示为 Sandbox setup。diagnostic 写入 Attempt-owned 诊断通道,之后可用 niceeval show --run <runId> --page attempt-<attemptId> 回顾。它本身不会改变判定。Sandbox 无法继续时直接抛出异常。 三种 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 时会回退到运行时安装(构建输出在题面网络之外准备,经文件 API 送入),所以烘焙纯粹是提速。Codex 送进去的是该平台自带运行时的原生包,任务镜像不带 Node 也能装,Claude Code 仍要求镜像里有 npm。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 固定版本的安装配方(Bub 版本 + 同代 OTel 插件): NiceEval 同时发布了三份任何 E2B Team 都能引用的公共模板。完整 namespace 与版本由 NiceEval 自己维护, 下游直接取完整引用:
这些基线都实际启动验证过:Claude Code 2.1.207、Codex 0.144.1、Bub 0.4.0(安装指纹随版本与 OTel 插件 pin 一起变)。模板的版本号跟着它装的那个 Agent 走(形如 0.144.1-r1:Agent 版本 + NiceEval 配方修订号),三个 Agent 各自独立发版,NiceEval 库自己的版本不参与命名。所以业务仓库直接用常量,不复制这些字符串、也不跟踪版本。派生模板需要记录 base 身份时,同样用常量的值。 当前这批公共模板中,Claude Code 模板沿用 E2B 官方 /usr npm prefix,普通用户不能直接写全局模块。需要在运行时追加 pnpm、yarn 等全局 Node 工具时,显式把它们装到两个模板都已加入 PATH 的 /usr/local
不要改用 sudo npm install -g。模板里已有的 root 全局包可能产生文件冲突。NiceEval 后续会在 新模板发布时统一这条默认 prefix。在发布说明明确新 release 已满足前,继续保留显式 --prefix /usr/local
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。tag 与对应的 E2B 公共模板同号——版本位是镜像里那个 Agent 的版本。稳定 CI 用具名常量或 digest,不要依赖会移动的 latest
这个镜像是 NiceEval 维护的公开镜像,不是 Docker 的 library/* Official Image。它在 Agent 版本或 构建配方变化时发布新 tag,跟 NiceEval 库的发版节奏无关。 如果只需要一个 Agent,或还要加入项目专有依赖,写 Dockerfile 从 Docker 的官方基线 node:24-slim 派生:
Dockerfile
然后在 Experiment 里只引用构建结果:
Docker Sandbox 沿用镜像自己声明的执行身份:上面这份 Dockerfile 没写 USER,容器就默认以 root 跑命令,/usr/local/bin 已经在 root 的 PATH 上,npm install -g 装的全局二进制天然可见。需要非 root 身份时(比如 Claude Code 在 root 下会拒绝 --dangerously-skip-permissions),在 Dockerfile 里加一行 USER nodenode:24-slim 镜像自带这个用户)。也可以用 dockerSandbox({ source: { type: "image", image }, user: "node" }) 显式覆盖。 要装到别处的 Agent(如落在 ~/.local/bin)记得让它进 PATH。dockerSandbox 要求显式 image。稳定 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,也不管理共享、版本和过期。归档或恢复失败会直接抛错。 如果这些目录或快照要跨 Attempt 延续,把 restoreCheckpoint() 放进 SandboxLayer.setup(),把 createCheckpoint() 放进配对的 teardown()setup() 在实际 Sandbox 创建后恢复,因此新 run 也走同一边界。teardown() 在实例退休、Provider finalizer 前回存。 sandboxReuse: true 只让同一个物理实例在本 Invocation 内继续存在。多个 Invocation 读写同一 checkpoint 时,在 Experiment 顶层声明 sharedState: { key },让另一边在创建 Sandbox 前等待。

瞬时错误重试

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

Docker

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

Vercel Sandbox

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

本地目录

localSandbox() 让 Agent 直接在你机器上的一个 Git 仓库里跑,不需要 Docker,也不需要云凭据:
要评别的目录,传 localSandbox({ dir: "/path/to/repo" }) NiceEval 只观察、不还原:Agent 改了什么会真实落在你的工作树上。NiceEval 用一份独立的私有 Git 记录采集 diff 和评分,不碰你自己的 .git、暂存区和未提交改动,跑完也不会执行任何 git reset。要保留还是丢弃 Agent 的改动,由你决定。 使用前记住三件事:
  • Agent 以你的身份、在你的机器上执行命令。只在你信任任务和 Prompt 的时候用它。要隔离就用 Docker 或云 Provider。
  • 本地目录同一时刻只跑一个 Attempt(强制串行),--max-concurrency 抬不高它。
  • 连续跑多个评估用例时,前一个的改动会留在工作树上,成为后一个的起点。要每题干净起点,用容器 Provider。
{ user: "..." }--keep-sandbox 对本地目录不可用,会直接报错:NiceEval 不在你的机器上提权或换身份,也不需要「留存」一个本来就在你工作树里的现场。

自定义 Provider

defineSandbox 接入其它服务。createfeedback 已绑定到 sandbox.create 阶段,可以报告分配实例、拉镜像或恢复沙箱快照的状态:
返回值实现 Sandbox 接口即可。Provider SDK 的原始日志不要直接写宿主进程的 stdout / stderr。短期状态走 feedback.progress,需要保留的问题走 feedback.diagnostic,无法创建环境时抛错。这样 Human dashboard 不会被日志打散,CI 也能保持单一有序输出。

权限和执行身份

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

性能建议

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

让公共准备只付一次

冷启动(起容器、装依赖、装 Agent CLI)主导反馈时间时,在实验里声明 sandboxReuse: true:多条 Attempt 依次共用一个 Sandbox,创建与 Sandbox 级 setup 每个 Sandbox 只付一次,题与题之间只把工作目录重置回公共准备完成时的状态。
这是可签入的实验语义,不是命令行上的运行模式:同一个实验不能临时打开或关闭复用,需要全新 Sandbox 的对照就另写一个不声明它的实验。代价、四层生命周期的次数和准备代码要满足的幂等要求见复用 Sandbox