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

Sandbox 接口

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

选择 Sandbox

在实验里设置 sandbox 字段
评估用例与 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

按顺序准备 Sandbox

每个 Provider factory 都返回不可变的 SandboxLayer。用 .before() 声明 Agent 开始前的动作,用 .after() 声明无条件收尾:
changeFrequency 是普通非负数。数值小的稳定动作排在前面,数值大的易变动作排在后面。内置常量只是便于阅读的数字:
Experiment、评估组、评估用例和 Sandbox Agent 都贡献到同一个顺序。显式 dependsOn 先决定依赖,再按数值排序。因此,评估组里频率较低的动作可以排到 Experiment 的易变动作之前。 所有声明式 Sandbox step 在运行时都有统一的安全 activity。shell() 显示你填写的 command,不显示 /bin/sh -lc 包装;command()exec 显示 executable 与 argv。writeText() / writeBytes() 只显示 目标路径、bytes 与 digest,不显示正文。upload / transfer 只显示 digest、目标路径与 bytes,不显示宿主 source path。 checkout locator 会去掉 userinfo、query 和 fragment;env 只列 keys。 很短的 progress 可能合并。需要稳定观察长脚本的细粒度进度时,把脚本拆成较小 action,并让每一段本身运行得 足够久或显式报告 progress;NiceEval 不追踪 shell 内部步骤。动态 callback 仍由 context.progress() 自己说明进度。

声明准备缓存覆盖范围

每个 .before(action) 都要如实声明它改变的完整状态范围。省略 cache.state 等于 sandboxState.all:该 action 的全部副作用都必须能恢复。它不是让 Provider 任选一部分目录缓存的开关。 只有 action 的全部副作用都局限于已经静止的 inner /var/lib/docker 时,才声明 sandboxState.dockerData。完整的 Docker Profile Action 示例见 让 Sandbox 使用 Docker cache.state 写在 action family 的 definition 或单次 instance 的任一处即可,不能两处重复, 也不能由 instance 覆盖 definition。family ID、输入、展开后的 steps、声明的 state 和内容摘要会自动进入 fingerprint。cache.fingerprint 只能补充 NiceEval 看不到的版本值,不能替代自动 fingerprint。 Provider 只会发布自己能完整保存的覆盖范围: 第一个默认 allopaque callback 或 Provider 不支持的 state 就是准备前缀的 barrier。该 action 以及它后面的所有 action 都会真实执行;后缀即使声明 dockerData,也不能跳过缺少祖先状态的步骤。 普通本地单容器 Docker 可以从最长匹配前缀启动。只有最后一个 .env action 变了时,固定的 Git checkout、 目录上传和工具安装不会重跑。每个 Attempt 都从缓存结果的私有副本启动,不会继承上一个 Agent 留下的改动。

定义可复用 Action

内置的 shell()writeText()uploadDirectory()gitCheckout() 都是 SandboxAction。项目也可以用相同机制定义自己的 family:
family 只组合 NiceEval 提供的固定 step,不能取得运行中的 Sandbox。上面的覆盖范围和 fingerprint 规则同样适用于自定义 family。

写入、上传与 Git checkout

这些内置函数都是同一种 SandboxAction,可以直接传给 .before() uploadFile()uploadDirectory() 接收 new URL("fixture/", import.meta.url),不接收随当前目录漂移的相对路径。实际 bytes、权限与目录 manifest 自动进入指纹。gitCheckout() 只接受不含凭据的 HTTPS URL 和完整 commit object ID。 需要在 Agent 返回后才公开的隐藏内容不要放进 .before()。先用 sandboxContent.file()sandboxContent.directory() 取得不可变 handle,再在 test(t) 中调用 t.sandbox.upload(handle, targetPath)。handle 只暴露内容摘要,不暴露宿主路径。

动态回调与收尾

只有运行时才能决定的分支可以直接写 .before(async (sandbox, context) => ...)。回调总会真实执行,并截断后续可共享的准备前缀。 资源成功取得后,立即用 context.onCleanup() 登记释放。闭包可以安全保存这次调用的句柄,不需要写两份彼此猜测状态的回调:
callback 的 sandbox.sandboxId 是当前主物理 Sandbox 的 Provider-native ID。通常把它当作不透明关联值;只有这条 layer 已明确绑定具体 Provider 时,可信宿主代码才应把它交给对应 SDK 取得 Attempt 级辅助资源。辅助资源不属于 NiceEval 管理的 Case 拓扑,也不进入缓存;创建成功后必须立即登记幂等 cleanup。准备前缀可能在首个 callback 前替换主实例,但 callback 开始后,本 Attempt 的 cleanup 与 after 会看到同一 ID。 无条件、幂等的收尾可以写成 .after()。所有已登记的收尾按实际登记顺序的反向执行,前面的动作失败时也会继续完成已经登记的项目:
回调中的 context.progress() 只更新当前 Attempt 的短期状态。context.diagnostic() 保存需要回顾的问题。环境无法继续时直接抛错;NiceEval 会把它记录为环境错误,而不是 Agent 做错题。 不带 Sandbox 的 defineAgent 没有这条准备流程。Sandbox Agent 可以在自己的 sandbox 字段贡献 command-only layer,但不能选择或替换 Provider template。

预制环境与运行时 checkpoint

NiceEval 用 typed spec 统一引用预制环境,但不提供一个假的通用构建命令:
Docker image、Vercel Sandbox 快照和 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 0.4.0 配方除 OTel 插件外,还固定 any-llm-sdk==1.17.0openai==2.31.0。安装指纹同时覆盖 Bub requirement、这两个 client override、OTel 插件和规范化后的 Python 插件集,不能仅凭 command -v bub 跳过安装。旧 marker 的预制环境会重装一次;显式选择其它 Bub 版本时,传递依赖闭包仍由调用方负责。三个 Provider 的构建都只在环境依赖变化时跑一次,产物换一个版本化名字,不要放进每个 Attempt 都会执行的动态回调。

E2B:从官方与公共模板派生

E2B 已提供 Claude Code 的 claude template 和 Codex 的 codex template。NiceEval 提供一个 E2B 专属的薄封装,让你从这两个官方起点继续链原生 E2B API。E2B 暂无 Bub 官方 template,因此默认 Bub 分支与 bubAgent() 使用同一份完整 client 闭包和 OTel 插件: NiceEval 同时发布了三份任何 E2B Team 都能引用的公共模板。完整 namespace 与版本由 NiceEval 自己维护, 下游直接取完整引用:
这些基线都实际启动验证过:Claude Code 2.1.207、Codex 0.144.1、Bub 0.4.0(安装指纹随 Bub requirement、client override、OTel 插件和 Python 插件 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。Sandbox 快照是从一台跑起来的 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 延续,在动态 .before() 中调用 restoreCheckpoint()。恢复成功后用 context.onCleanup() 登记 createCheckpoint(),让回存在实例退出、Provider finalizer 之前执行。 sandboxReuse: true 只让同一个物理实例在本 Invocation 内继续存在。多个 Invocation 会读写同一 checkpoint 时,在 Experiment 顶层声明 sharedState: { key }。NiceEval 在 Experiment setup 或创建 Sandbox 前取得租约,等 checkpoint 回存、Provider finalizer 与 Experiment teardown 完成后释放。它只提供互斥,不负责 checkpoint 存储、事务恢复或跨机器协调。

瞬时错误重试

内置 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 配置。

自定义 Provider

defineSandbox 接入其它服务。自定义 Provider 的作者必须负责文件系统、进程、网络与凭据隔离;NiceEval 不会为一个任意 Sandbox 实现自动补上这些安全边界。createfeedback 已绑定到 sandbox.create 阶段,可以报告分配实例、拉镜像或恢复 Sandbox 快照的状态:
返回值实现 Sandbox 接口即可。Provider SDK 的原始日志不要直接写宿主进程的 stdout / stderr。短期状态走 feedback.progress,需要保留的问题走 feedback.diagnostic,无法创建环境时抛错。这样 Human dashboard 不会被日志打散,CI 也能保持单一有序输出。

权限和执行身份

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

性能建议

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

在 Attempt 之间复用 Provider 启动

Provider 启动主导反馈时间时,在实验里声明 sandboxReuse: true。多条 Attempt 可以依次共用一个 Sandbox:每个复用实例只创建一次,每条真实 Attempt 仍有 attempt .before() / .after() 计划,题间重置工作目录。声明式 Docker action 可另外复用匹配的准备前缀。
这是可签入的实验语义,不是命令行上的运行模式:同一个实验不能临时打开或关闭复用,需要全新 Sandbox 的对照就另写一个不声明它的实验。代价、四层生命周期的次数和准备代码要满足的幂等要求见复用 Sandbox