review.yaml 参考
review.yaml 告诉测试机怎么启动你们的 Agent、怎么让它的工具调用经过工具代理、怎么重置和读取测试后端、怎么评审结果。本页列出平台认的每一段、每个字段。
放在哪里
| 接法 | 放在哪里 | Agent 命令在哪个目录执行 |
|---|---|---|
| 连 GitHub 仓库 | 仓库里的 .reviewctl/review.yaml,或者平台覆盖层里;覆盖层盖在仓库的 .reviewctl/ 上面 | 仓库根目录 |
| 自带镜像 | 只放在平台覆盖层里(.reviewctl/review.yaml);覆盖层只读挂在容器的 /reviewctl | 镜像自己的 WORKDIR |
| 本地测试机 | 测试机上任意位置;命令用 --config 指定(默认是当前目录的 review.yaml) | 文件所在的目录(那个目录叫 .reviewctl 时,是它的上一级) |
| 网页直连对话 AI | 不用 |
agent、attach、backend、scenarios 里的路径相对 review.yaml 所在的目录。environment 和 services 里的路径相对仓库根目录,不能跑出仓库。
路径从 .reviewctl/ 算起
连 GitHub 仓库和自带镜像的项目,review.yaml 在 .reviewctl/ 下,所以这些都从 .reviewctl/ 算起,而不是从仓库根目录:
backend.reset的命令在.reviewctl/下执行,命令里写的脚本也从这里找;backend.reset的body_file和script,backend.state.reader的path和catalog;scenarios.dir;agent.version的prompt_files和tool_config_files。
比如脚本是仓库里的 .reviewctl/seed.mjs,就写 [node, seed.mjs]。写成 [node, .reviewctl/seed.mjs],找的是 .reviewctl/.reviewctl/seed.mjs。仓库根目录下的文件前面加 ../,比如 ../prompts/system.md。Agent 自己的命令(agent.command)不一样:它在仓库根目录执行(见上表)。
这类写错不会让文件读不进来,平台会提醒,不拦:保存覆盖层时、目标下向导里的「测试接入文件」下面、「测试新版本:全部选项」的「开始前检查」里,都会写出是第几行、找的是哪个路径、该怎么改。同时还提醒这几种不报错的写错:
agent.version.tool_config_files或prompt_files里的文件哪里都没有:平台跳过它,版本对比里的「工具配置」或「提示词」不会随它变;runner_env里的变量不会传给 Agent,平台自己也不用它(见 runner_env)。
整个文件的规则
- 用 YAML 写,
schema_version: 1。 - 每一层都拒绝不认识的字段。出错时检查报错里那个字段的拼写。
- 文件里不写密钥的值。字段名以
api_key、apikey、secret、token、password或passwd结尾的,不能直接写值。看起来像密钥的值(sk-…、sk-ant-…、ghp_…、Slack 令牌、Bearer …、JWT)写在哪里都会被拒。以_env结尾的字段写的是变量名,可以写;写成env:NAME或${NAME}的引用也可以。把变量名写进agent.env_passthrough,值放进项目密钥或测试机上。不是秘密的值可以写进runner_env,那里按值检查,不按名字。 - 环境变量名只用大写字母、数字和下划线,以字母开头。
- 模型写成
<提供方>:<模型编号>,比如openai:gpt-4.1-mini。测试机能调用的提供方有openai、anthropic、azure和openai-compatible。 - 检查文件:在测试机上运行
reviewctl doctor。在网页上,覆盖层编辑器不保存读不通的文件,并写明哪一行、哪个字段不对。
最小示例
必须有 8 段:schema_version、project、agent、attach、backend、judge、scenarios 和 budget。
schema_version: 1
project: { id: prj_01J8ZK3Q9MXYZ0000000000000, name: support-agent, domain: generic }
agent:
driver: command
command: [node, dist/agent.js, --message, "{{task}}"]
env_passthrough: [OPENAI_API_KEY]
attach: { mode: endpoint_only }
backend: { mode: own }
judge: { model: "openai:gpt-4.1-mini" }
scenarios: { dir: scenarios }
budget: { per_attempt: { minutes: 5, model_calls: 30 } }这样能启动 Agent、评审它的回复,但平台看不到工具调用,也读不到数据。要测这两样,加上 attach 的路由和 backend 的重置与读取。
完整示例
一个连仓库的项目,Agent 通过 REST 调用测试后端上的工具,测试后端用 Docker Compose 起:
schema_version: 1
project: { id: prj_01J8ZK3Q9MXYZ0000000000000, name: expense-agent, domain: expense }
environment: # 只用于连仓库
setup: [pnpm install --frozen-lockfile]
services: # 测试后端,起在 Agent 旁边
compose: docker-compose.test.yml
agent:
driver: command
command: [node, dist/agent.js, --message, "{{task}}", --user, "{{identity.user_id}}"]
env: { AGENT_VARIANT: baseline } # 普通设置
env_passthrough: [OPENAI_API_KEY] # 值来自项目密钥
model: "openai:gpt-4.1"
version: { git: true, prompt_files: [../prompts/system.md] }
attach: # 工具代理
mode: http_proxy
base_url_env: EXPENSE_TOOLS_BASE
upstream_base_url: http://127.0.0.1:47591/tools
routes:
claim_list: { method: GET, path: /claims }
claim_submit: { method: POST, path: /claims/{claim_id}/submit }
backend:
mode: own
reset: { type: http, url: http://127.0.0.1:47591/reset, state_ref_path: $.state_ref }
state: { reader: { kind: http, base_url_env: EXPENSE_STATE_BASE, catalog: scenarios/state_queries.yaml } }
judge: { model: "openai:gpt-4.1-mini" }
scenarios: { dir: scenarios }
gate: # 只用于本地运行;平台发起的运行按项目的通过标准
key_tasks:
- { scenario: submit-claim, condition: normal, pass_k: { k: 3, min: 1 } }
security: { block_on: [verified_impact, verified_violation] }
budget:
per_attempt: { minutes: 3, model_calls: 30 }
total_model_calls: 600各段一览
| 段 | 必填 | 连 GitHub 仓库 | 自带镜像 | 本地测试机 |
|---|---|---|---|---|
schema_version | 是 | 用 | 用 | 用 |
project | 是 | id 换成项目自己的 | id 换成项目自己的 | id 必须是项目的编号 |
environment | 否 | 用 | 不用:镜像就是环境 | 不用 |
services | 否 | 用 | 用 | 不用 |
agent | 是 | 用 | 用(在镜像里) | 用 |
attach | 是 | 用 | 用 | 用 |
backend | 是 | 用 | 用 | 用 |
judge | 是 | 用 | 用 | 用 |
scenarios | 是 | 只用 dir | 只用 dir | 用 dir;include 和 required 只给 reviewctl review 用 |
gate | 否 | 不用:按项目的通过标准 | 不用 | 只给 reviewctl review 用 |
budget | 是 | 用 | 用 | 用 |
cloud | 否 | 不用 | 不用 | 给 reviewctl review 和 reviewctl push 用 |
runner_env | 否 | 用 | 用 | 用 |
schema_version
固定写 1。
project
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
id | prj_ 加 26 个字符 | 必填 | 项目编号,和网页上显示的一样。reviewctl runner register 要求它就是要配对的项目。在沙盒里,平台会把它换成发起这次运行的项目的编号,所以一个仓库可以给几个项目用。 |
name | 文字 | 必填 | 本地报告里的名字。 |
domain | 文字 | 必填 | 业务领域,比如 expense 或 generic。orders-refunds 是自带模拟后端的领域。 |
environment
沙盒怎么准备克隆下来的仓库。只用于连 GitHub 仓库的项目;不写就按仓库根目录的锁文件探测运行时。
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
runtime | node@20、node@22、node@24、python@3.11、python@3.12、go@1.23 或 java@21 | 按 pnpm-lock.yaml、package-lock.json、yarn.lock、uv.lock、poetry.lock、requirements.txt、pyproject.toml、go.mod、pom.xml、build.gradle 探测(第一个匹配的算);仓库要的 Node 版本(.nvmrc、.node-version、.tool-versions、mise.toml,或者 package.json 的 engines.node 要 23 以上)选 node@20 或 node@24,.python-version 可以选旧一点的 Python | 决定默认的 setup 命令和 cache 目录;node@24 时安装和 Agent 都用 Node 24 跑(其余用 Node 22)。 |
setup | shell 命令列表,最多 20 条,每条最多 500 个字符 | 按锁文件:pnpm install --frozen-lockfile、npm ci、yarn install --frozen-lockfile、bun install --frozen-lockfile(bun.lock 或 bun.lockb,用沙盒镜像里的 bun;测试机上没有 bun 的旧镜像会改成 npx --yes bun install --frozen-lockfile,setup 里写的这两行也按机器上有的那种跑)、uv sync --frozen、go mod download 等 | 检出后在仓库根目录按顺序执行,带 CI=1 和项目密钥作为环境变量。一条命令 15 分钟没有输出就被停下(见准备环境卡住),此外受单次运行的时间上限约束。所有命令退出后,安装就结束了,日志写「依赖装好了」。 |
cache | 目录列表,最多 20 个 | Node.js 是 node_modules,Python 是 .venv,Go 和 Java 没有 | 跨运行保留 30 天,按运行时、架构、锁文件和安装命令区分。安装之后存、下次准备环境时先恢复,各是平台自己的一步,最多 8 分钟;存不上或恢复不了就跳过缓存,不影响这次测试。 |
arch | amd64 或 arm64 | 无 | 认这个字段,但现在不用。沙盒的架构以项目在「设置 → 沙盒」里的设置为准。 |
container | 见下 | 无:Agent 直接在虚拟机上跑 | 用仓库自己的容器构建镜像,让 Agent 在里面跑。 |
机器镜像里的运行时
在虚拟机上(不写 container 时),沙盒按这里写的 runtime、setup 和 cache 安装;没写的字段按锁文件探测。runtime 决定默认命令和缓存的键,不切换程序版本:机器镜像里总是有 Node.js 20 和 22(node 是 22;node20、node22 也在 PATH 上)、带 uv 的 Python 3.11 和 3.12、Go 1.23 和 Java 21。
setup 和 Agent 不是同一个用户
setup 和命令型的数据重置(backend.reset 的 type: command,每次执行前跑)都以测试机自己的用户执行。检出目录和 setup 装出来的文件(.venv、node_modules)都属于这个用户。
Agent 以另一个没有特权的用户运行。每次执行都在检出目录的一份新副本里跑,Agent 能写这份副本。按当前目录打开的文件(比如 sqlite3.connect("app.db"))直接能用,下一次执行换一份新的副本。这次执行的重置也在这份副本里跑,状态读取读的也是副本里的文件,所以 review.yaml 里的相对路径对重置、Agent 和核对来说是同一个文件。
.git、node_modules、.venv 和 cache 列的目录不复制,副本里链接到检出目录自己的那份,Agent 能读、能执行,但改不了。环境变量 REVIEW_ATTEMPT_DIR 指的目录 Agent 也能写,每次执行都是一个新的空目录。
不算这些目录,检出目录还超过 500 MB 的话就不复制。这时每次执行都在检出目录本身里跑,Agent 只能读它(运行日志里会说),要写的文件得放在 REVIEW_ATTEMPT_DIR 下。写不进去时,Agent 那边会报 attempt to write a readonly database 一类的错,预跑会在「入口程序有没有正常结束」或「测试环境有没有正常工作」里把这行报错写出来。
重置建的文件,Agent 不一定读得到。 沙盒里测试机的用户建的文件和目录,默认只有它自己能读写(umask 077)。平台在 Agent 启动前只把这次执行的仓库副本放开给 Agent,重置建在副本外面的(比如仓库旁边的一个目录)Agent 读不到。这时 Agent 那边报的是系统的「没有权限」(EACCES),看起来像 Agent 自己拒绝了,其实它根本没读到。要给 Agent 读的,建在仓库副本里,或者在重置里自己放开权限(目录 chmod 755、文件 chmod 644)。用命令型重置的运行,日志里会写这一句提醒。
environment.container
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
dockerfile | 路径 | dockerfile 和 devcontainer 必须写一个 | 要构建的 Dockerfile。 |
devcontainer | 路径 | 一个 devcontainer.json。认它的 image,或 build.dockerfile、build.context、build.args,以及 containerEnv 和 postCreateCommand;不支持 devcontainer features。 | |
context | 路径 | Dockerfile 所在的目录 | 构建上下文。只和 dockerfile 一起用。 |
workdir | 绝对路径 | /workspace | 检出目录(每次执行自己的那份副本)挂进容器的位置,每次执行都在这里跑。不能是 /reviewctl-scratch。 |
postCreateCommand 和 setup 在构建好的容器里各跑一次。构建出的镜像按提交保存 30 天,同一个提交不会构建第二次。Agent 在容器里以同一个无特权用户运行,网络规则和虚拟机上一样。
services
沙盒用 Docker Compose 在 Agent 旁边起的测试后端。要么指向仓库里的一个 Compose 文件:
services:
compose: docker-compose.test.yml要么按名字逐个列出(名字是小写短名,最长 31 个字符):
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
image | 镜像地址 | 必填 | 要起的容器镜像。 |
env | NAME: 值 的映射,最多 30 个 | 无 | 容器的环境变量。 |
ports | 端口号列表,最多 10 个 | 无 | 发布在 127.0.0.1 上给 Agent 用的端口。 |
ready | shell 命令 | 无 | 每 5 秒在服务的容器里执行一次,返回 0 之后才开始运行,比如 pg_isready -U postgres。 |
services 要在 3 分钟内起好。Agent 只能经 127.0.0.1、通过发布出来的端口访问它们。用自己的 Compose 文件时,把 Agent 要用的端口发布在 127.0.0.1 上(比如 "127.0.0.1:47591:47591"),容器里的服务监听 0.0.0.0。
backend 没有 reset 时,从第 2 次执行起,沙盒每次执行前都把 services 恢复成任务开始时的样子:删掉容器和数据卷,用同样的镜像重新起,同样等它们起好。前一次执行在服务里删掉的文件、改掉的数据,下一次执行时又是原样。运行日志会写每次恢复用了几秒。服务挂载的宿主机目录不恢复。配了 backend.reset 时用它,除非用 restore 要求同时重建服务:
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
restore | all,或 Compose 文件里的服务名列表 | 无 | 配了 backend.reset 时:每次执行的重置之前,先把这些服务(或全部)用镜像连同数据卷重新创建,别的服务不动。只能和 compose 一起写。没配 backend.reset 时全部 services 照样每次恢复。 |
services 里的服务,以及重置、安装命令起的容器,都不能拿到 Docker 或沙盒这台机器本身的控制权。下面这些都不允许:
- 挂
/var/run/docker.sock、/run/docker.sock,或它们所在的目录、指向它们的符号链接 privileged,或pid、network_mode、userns_mode写hostcap_add加SYS_ADMIN这类权限,或seccomp/apparmor写unconfined- 拿 GPU 以外的宿主机设备
- 挂这台机器的
/proc、/sys、/dev或用户目录,或可写地挂它的系统目录 - 设
DOCKER_HOST
这样的 Compose 文件,沙盒在启动前就拒绝,你们自己写的和平台写的都一样,并说清是哪个服务、挂了什么。执行之间这样起的容器,会在 Agent 运行前删掉,这次执行以同样的原因失败。要每次执行前换一套新的服务,不要起一个管容器的服务,而是不写 backend.reset,或者用 restore。
执行可能在 services 之外另起容器,比如重置时用它们的镜像、在它们的网络里为这一次新建一套服务。这些容器在这次执行结束后、下一次重置之前,就连同数据卷删掉。每次执行照旧用自己新起的,不会一次次积下来把沙盒的内存占满。
沙盒里的测试每执行完一次,也会交一份部分报告。测试没跑完沙盒就失联的话,最后这份就是这次测试的报告:已执行的照常判,没来得及执行的标成没有结果。
agent
每次执行怎么启动 Agent。
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
driver | command、http 或 harness | 必填 | 测试机和 Agent 怎么对接。 |
command | 参数列表 | command 时必填 | 命令行,每次执行起一个进程。占位符见下。 |
endpoint | 见下 | http 时必填 | 一个在跑的 HTTP 接口。 |
harness | JavaScript 模块路径 | harness 时必填 | 导出 runTask(input, wiring, signal) 的模块,在测试机进程里运行。 |
env | NAME: 值 的映射 | 无 | 给 Agent 的普通设置,比如 AGENT_VARIANT。不放密钥。 |
env_passthrough | 变量名列表 | 无 | Agent 可以从测试机的环境里拿到的变量:沙盒里是项目密钥,本地测试机上是机器的环境或 .env。沙盒里没有 OPENAI_API_KEY 这个项目密钥时,不管这里写没写,Agent 都会拿到调用平台模型用的 OPENAI_BASE_URL 和 OPENAI_API_KEY。 |
model | 模型引用 | 无 | Agent 自己用的模型,写进报告。评审模型必须和它不同。 |
version.git | true 或 false | true | 用 git 提交作代码版本。 |
version.prompt_files | 路径列表 | 无 | 这些文件的内容组成提示词版本。路径从 review.yaml 所在的目录算起;找不到的文件平台跳过,并提醒你。 |
version.model_env | 变量名 | 无 | 没写 model 时,存放 Agent 模型的变量。 |
version.tool_config_files | 路径列表 | 无 | 这些文件的内容组成工具配置版本。路径和找不到时同上。 |
reply | 文字 | stdout_last_json | 认这个字段,但测试机现在不用。 |
framework | 文字 | 无 | 只作记录。 |
Agent 进程只拿到 agent.env、env_passthrough 里的变量、PATH、HOME、LANG 这类基本变量,以及测试机加的:工具接线(见下)、问用户的地址 REVIEW_ASK_USER_URL(见Agent 要问用户时)、测试写明预先批准时的 REVIEW_PREAPPROVED_TOOLS(见预先批准)、标出接入准备用的 REVIEW_PREPARE_URL(见接入准备)、REVIEW_MAX_MODEL_CALLS、REVIEW_MAX_WALL_MS,测试用户有令牌时的 ACTOR_TOKEN,测试带文件时的 REVIEW_FILES 和 REVIEW_FILES_DIR,还有沙盒里没有 OPENAI_API_KEY 这个项目密钥时,调用平台模型用的 OPENAI_BASE_URL 和 OPENAI_API_KEY(这两个盖过 agent.env 里的同名变量)。机器上的其他东西都进不去。
command 方式
command 里的占位符:
| 占位符 | 换成 |
|---|---|
{{task}} | 这个测试的任务原文 |
{{identity.user_id}}、{{identity.role}} | 测试里的用户和角色 |
{{actor_token}} | 这个用户的令牌,来自测试指定的变量 |
{{max_model_calls}}、{{max_wall_ms}} | 这次执行的上限 |
{{files_json}} | 测试文件的 JSON:[{name, path, mime, size, sha256}] |
Agent 把结果写在标准输出的最后一行,一个 JSON 对象:text(回复),可选的 usage(model_calls、input_tokens、output_tokens)、stop_reason(error 或 max_steps)和 error。标准错误里的 JSON 行作为过程记录保留。
{"text": "报销单 C-1042 已提交。", "usage": {"model_calls": 4}}http 方式
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
endpoint.url | URL | 必填 | 每个任务以 POST 发到这里,请求体是 {"prompt", "actor_token", "max_steps"}。 |
endpoint.reply_path | 以 $ 开头的路径 | 必填 | 回复在返回 JSON 里的位置,比如 $.reply。以 server-sent events 返回的,取最后一个 final 事件。 |
endpoint.send_files | none 或 base64 | none | base64 把测试文件放进请求体:files: [{name, mime, sha256, base64}]。 |
endpoint.method、endpoint.body_template、endpoint.reset_url | 认这些字段,但测试机现在不用。 |
返回里可以带 usage.model_calls,和命令最后一行一样。
多轮对话
一个测试的用户可以说不止一句:写好的几句(场景的 task.turns),或者模拟用户(simulated_user:用户是谁、想办成什么、 最多几轮,可选什么时候停)。模拟用户由项目的评审模型扮演:它读每一条回复,写下一句话,或者说可以了;它看不到测试的期望结果和判断标准。测试的任务原文 (或题目的输入)是第一句;用模拟用户又不写输入时,第一句也由模拟用户来写(只限对话 AI:在测试机上,题目要写好第一句)。
入口每一轮被调用一次,带着到这一轮为止的对话:
| 方式 | 每一轮拿到什么 |
|---|---|
command | 每轮起一个进程。{{task}} 是这一轮的话,{{turn}} 是第几轮(1 是任务原文),{{history_json}} 是之前的对话 [{role, content}](用户的话和 Agent 自己的回复)。环境变量里也有:REVIEW_TURN、REVIEW_HISTORY。 |
http | 每轮 POST 一次。请求体在 prompt(这一轮的话)旁边多了 turn、history(之前的对话)和 messages(history 加上这一轮用户的话,聊天格式)。 |
harness | runTask 每轮调用一次,带 input.turn 和 input.history。 |
只有一句话的任务和以前发的一模一样,不带 turn、history、messages。几轮共用这次执行的时间上限和模型调用次数(模拟用户在两轮之间想的时间不算), 每一轮的工具调用都经过测试机的代理。故障和攻击可以只打某一轮:故障或篡改工具结果的攻击写 match.turn(这时 nth 数的是这一轮里的调用),篡改任务 输入的攻击写 turn(加在用户的第几句话上)。整段对话存在这次执行的 conversation.json 里。
多轮的字段
review.yaml 自己没有多轮的字段:它只说入口怎么收到每一轮(见上面)。对话写在测试里:
| 写在哪里 | 字段 | 类型和上限 |
|---|---|---|
场景(在 task 下) | turns | 任务原文之后用户说的话:1 到 11 句,每句最多 2 万个字符。 |
simulated_user | 见下面。任务原文算第 1 轮。不能和 turns 一起用。 | |
| 测试集的一道题(case) | turns | 用户说的话,包括第一句:2 到 10 句,每句最多 2 万个字符(代替 input)。 |
messages | 贴进来的一段对话 [{role, content}],1 到 50 条,最后一条要是用户的(代替 input)。 | |
simulated_user | 见下面;写了 input 时它就是第一句(在测试机上必须写)。不能和 turns 或 messages 一起用。 | |
| 数据集的一行(v2) | turns | [{role: "user" | "assistant" | "system", content}],最多 50 条(跟着这一行记录或导入的对话) |
simulated_user | 见下面。 | |
| 故障或攻击 | match.turn(故障、篡改工具结果的攻击)、turn(篡改任务输入的攻击) | 1 到 12;1 是任务原文。不能超过对话的最后一轮。 |
simulated_user:
| 字段 | 类型 | 默认 | 意思 |
|---|---|---|---|
persona | 文字,1 到 2000 个字符 | 必填 | 用户是谁,几句话。 |
goal | 文字,1 到 2000 个字符 | 必填 | 用户想在这段对话里办成什么。 |
max_turns | 整数,1 到 12 | 6 | 用户最多说几句,包括第一句。 |
stop_when | 文字,1 到 300 个字符 | — | 除了目标达成,什么时候停;不写就是目标达成或明显办不成时停。 |
task:
prompt: "我买错尺码的鞋了。"
# goal、goal_text … 和别的场景一样
simulated_user:
persona: "第一次网购的老人,说话简短,不懂术语。"
goal: "把买错尺码的鞋退掉,问清退款什么时候到账。"
max_turns: 6
stop_when: "客服说了退款什么时候到账。"测试机要有 multiturn 能力才能跑这些(新版的 reviewctl 都有);在测试机上,测试集写好的 turns 和贴进来的 messages 暂时不跑,模拟用户的题写了 input 就能跑。评分器 conversation_goal 判断模拟用户的目标有没有达成(评分器)。每段对话以写好的话说完、模拟用户停下、到了 max_turns 或出错结束,报告里写明是哪一种。
Agent 要问用户时
有的 Agent 做事前要先问用户:执行命令、改文件前请求权限,或者请用户确认一件事。在测试机上没有人回答,没有接好的话,Agent 那边就会报错(比如无界面运行时报 HOST_CAPABILITY_UNAVAILABLE),测试也就没测到要测的东西。
每次执行,测试机都给 Agent 设一个地址 REVIEW_ASK_USER_URL(harness 方式在 wiring 里)。接线把 Agent 自己的问题转给平台:
POST $REVIEW_ASK_USER_URL
{"question": "允许运行 bash 命令 ls -la 吗?", "kind": "permission"}question:问题原文。kind:请求权限写permission;业务上的确认(「要不要提交这张报销单?」)不写。
平台回 {"answer": "yes"} 或 {"answer": "no"},接线把它交回给 Agent。平台怎么答,按每个测试自己的设置:测试允许或要求问用户时,按测试设的回答(默认 yes;设成两种都测的,同一个情况以 yes 和 no 各跑一次)。测试不允许问用户时,平台回答 no,并带上 "forbidden": true。
问了什么、怎么答的,都记进这次执行的记录(一条 confirmation,带 kind,测试不允许问时标 forbidden),单次执行页上写「请用户允许「…」,用户回答 yes」,或者「问用户「…」,这个测试不允许问用户,平台回答 no」。判定把记录里的问题也算作「Agent 问了用户」,和回复里的反问一样。
接线要守的规矩:
- 只转,不替用户批准:不自己回答 yes,不关掉产品自己的权限检查,不打开「跳过确认」一类的开关。唯一的例外是测试写明了预先批准的工具。
- 没有
REVIEW_ASK_USER_URL(测试机的 reviewctl 太旧)时不要猜一个回答:结束这次执行,最后一行输出{"text": "", "stop_reason": "error", "error": "Agent 要问用户,测试机没有提供 REVIEW_ASK_USER_URL"}。 - 接线在测试机这边执行不了工具时(宿主缺这个能力,比如无界面运行时报
HOST_CAPABILITY_UNAVAILABLE;或者接线自己的转发出错),这次工具调用返回 HTTP 503,body 写{"error": {"code": "host_error", "message": "原来的报错原文"}}。报告把工具没执行成的攻击记为「攻击没打到目标」(平台先自动重跑一次),不算 Agent 的错;报错原文带HOST_CAPABILITY_UNAVAILABLE的不管 code 叫什么也一样,4xx 除外。工具自己执行了、出了错,或者产品自己的权限检查拒绝了,照原样返回它自己的错误,不要写成host_error。报错是 Agent 自己的权限层在无界面运行时要先问、没人能答(带HOST_CAPABILITY_UNAVAILABLE或cannot prompt for permission)的,平台不重跑:再跑也一样,见下面的预先批准。
「让 AI 补全并检查」写接线时会照这些规矩接,见连 GitHub 仓库。
测试里写明的预先批准
有的 Agent 无界面运行时,用某个工具前要先问权限,又没人能答,这个工具就一直没执行,测试也没测到。要测到它,可以在测试里写明预先批准这个工具:
task:
human_confirmation:
preapprove: [bash]task.human_confirmation.preapprove:工具名列表,1 到 10 个,只管这一个测试。这是人定的前提,由人定、由人确认:人在实验的未判定行上点「在测试里写明预先批准 <工具>」,平台按这一下存一个写明它的新版本草稿(也可以自己写进测试);AI 自动审批不确认它,平台也从不自己预先批准(见没测成以后)。- 这个测试的每次执行,测试机给 Agent 设
REVIEW_PREAPPROVED_TOOLS:工具名,逗号分隔(harness方式在wiring里)。没写的测试没有这个变量。 - 接线只在这次执行里、只对这些工具,用 Agent 自己的权限配置预先批准,比如启动参数里的允许列表、这次执行用的权限规则文件;别的工具照常经
REVIEW_ASK_USER_URL问。不因此关掉产品自己的权限检查、打开对所有工具都跳过确认的开关,也不改review.yaml里 Agent 自己的配置。 - 测试机上的 reviewctl 太旧、不认这个字段时,页面写明要更新 reviewctl。
接入准备
有的测试要先有前提才测得了:之前的一段会话、攻击前的对话、要找的数据。这些是测试接入准备的,不是 Agent 在这次测试里做的事:
- 数据用
backend.reset准备。它在每次执行之前跑,平台不把它当成 Agent 的调用。 - 之前的对话写成固定的消息交给 Agent,比如适配脚本读
.reviewctl/下写好的历史,作为对话记录传进去。不要为了造历史让 Agent 真去调用工具。 - 确实要先让 Agent 跑一遍才造得出来时(比如它的记忆只能由它自己写进去),接线在跑之前 POST
{"phase": "prepare"}到REVIEW_PREPARE_URL,跑完、把测试的话交给 Agent 之前 POST{"phase": "task"}:
POST $REVIEW_PREPARE_URL
{"phase": "prepare"}
… 让 Agent 跑一遍准备用的对话 …
POST $REVIEW_PREPARE_URL
{"phase": "task"}
… 把测试的话交给 Agent …这中间的工具调用,平台照样经工具代理交给工具,但不注入故障和攻击、不按禁止规则核对、不占调用上限,记成「接入准备的」。攻击有没有得逞、有没有做不允许的操作、调用计次都只算 Agent 在这次执行里自己做的调用;单次执行页上准备时的调用标着「接入准备的:不算 Agent 这次做的」,评审模型看到的也和 Agent 自己的分开。准备时写进数据库的改动,做完后的数据检查照样读得到:要核对、要计次的数据用 backend.reset 准备。每次执行都有 REVIEW_PREPARE_URL(harness 方式在 wiring 里);没有它(测试机的 reviewctl 太旧)时,不要在执行里造这种历史。
attach
工具代理:Agent 的工具调用经过测试机,每次调用都会记下来,也能往里注入故障和攻击。
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
mode | mcp_wrap、http_proxy、fixture 或 endpoint_only | 必填 | MCP 服务用 mcp_wrap,REST 工具用 http_proxy,配自带模拟后端用 fixture,看不到工具时用 endpoint_only。 |
mcp_config | 路径 | mcp_wrap 时必填 | Agent 的 MCP 配置文件(mcpServers)。只读不改。 |
servers | 服务名列表 | mcp_config 里的全部 | 哪些 MCP 服务走工具代理;其余的照原样直连。 |
apply | env 或 in_place | env | env 把包装后配置的路径放在 REVIEW_MCP_CONFIG 里交给 Agent;in_place 在这次执行期间把它写到 mcp_config 上,结束后放回原文件。 |
base_url_env | 变量名 | http_proxy 时必填 | Agent 从这个变量读工具的基础地址;测试机把它设成工具代理。 |
upstream_base_url | URL | http_proxy 时必填 | 真正的工具在哪里。 |
routes | 工具名 → {method, path} 的映射 | http_proxy 时必填 | 每个工具的 REST 路由。method 是 GET、POST、PUT 或 DELETE;path 以 / 开头,{name} 段变成参数。工具名用字母、数字、_ 和 -,最长 64 个字符。 |
enforce_forbidden_writes | true 或 false | false | 测试禁止的写操作直接拦下,而不只是记下来。 |
process_egress | true 或 false | false | Agent 进程的其他出站 HTTP(S) 也走代理(HTTP_PROXY、HTTPS_PROXY),它连过的每个主机都记下来;只拦带着埋点值的请求。 |
attach.mode: fixture 只和 backend.mode: fixture 一起用。
接成工具的,应该是 Agent 真正对外做事的那一步:执行一条命令、运行一段代码、读或写一个文件、发一个网络请求、调一个业务接口,每次经代理的调用都对应一件实际要做的事。被测程序每轮回复都会走一遍的内部步骤(比如把回复解析成改动再应用、把回复里列出的命令交给执行的分发函数),没有要做的事时也会被调用,不要把它本身接成工具:要看它做了什么,就接真正写文件、执行命令的那一步,或者只在它确实有要做的事时才经工具代理。Agent 没有声明工具、但它的代码会替用户执行命令或代码、读写文件、访问外部地址的,也要把这一步接上,不能当成「只看回复就够了」。
routes 里每条路由单独写一行。两条用逗号写在同一行(read: {method: POST, path: /read}, bash: {…})YAML 读不通,保存时会被拒,报错写明是第几行,要把逗号后面那一条移到下一行、缩进和前一条一样。
backend
你们的测试后端:每次执行前怎么重置,执行后怎么读出数据做数据检查。
mode: own
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
reset | 下面的重置方式之一 | 无 | 每次执行前运行。不写就不重置,数据检查就靠不住。 |
state.reader | 下面的读取方式之一 | 无 | 每次执行后读出数据。 |
recheck_after_ms | 0 到 30000 | 3000 | 检查后隔这么久(毫秒)再读一次数据,每个场景一次执行,用来接住来得晚的结果。0 关掉。 |
重置方式:
type | 字段 | 作用 |
|---|---|---|
command | command(参数列表) | 在 review.yaml 所在目录执行命令(连仓库和自带镜像是 .reviewctl/,见路径从 .reviewctl/ 算起),以测试机自己的用户,环境是测试机的环境(包括 runner_env)加 REVIEW_WORK_DIR 和 REVIEW_ATTEMPT_SHORT。它建的文件 Agent 不一定读得到,见上面 environment 里的提醒。 |
http | url;method(POST 或 PUT,默认 POST);body;body_file(一个 JSON 文件,合并在 body 下面);state_ref_path(返回里数据引用的位置,比如 $.state_ref) | 用 JSON 请求体调用你们的重置接口。 |
sql | url_env(放数据库地址的变量);script(SQL 文件路径) | 重建数据库再执行脚本。现在只支持 SQLite:sqlite:///path、file:///path 或普通路径。 |
重置的参数和请求体里的字符串中,{{attempt_short}}、{{attempt_id}} 和 {{work_dir}} 会被换掉。
读取方式:
kind | 字段 | 读什么 |
|---|---|---|
sqlite | path、catalog | 一个 SQLite 文件。 |
sql | url_env、catalog | 变量里地址指向的数据库;现在只支持 SQLite。 |
http | base_url_env、catalog | 你们的只读数据服务:GET {base}/tables?ref=<state_ref>&names=a,b 返回 {"<表名>": [行]},POST {base}/query 带 {ref, queries: [{id, query, params}]} 返回 {"<id>": 值}。要配 reset.state_ref_path。 |
catalog 是测试用到的具名数据查询(state_queries.yaml)的路径。
数据查询:state_queries.yaml
测试的数据检查(「做完后数据应该是什么样」)通过具名的数据查询读系统。每个名字读什么,写在 state_queries.yaml 里,每个名字一项;这个文件放在 scenarios.dir 目录里(平台写的是 dir: scenarios,也就是 .reviewctl/scenarios/state_queries.yaml),catalog 也指向它。测试机(或沙盒)每次领任务时把名字,以及每个 SQL 查询里 :名字 形式的参数,报给平台;查询本身不上传。测试里写了没报上来的名字,导入时会失败。
# 名字: 读取器执行的内容。名字用小写字母、数字和 _,以字母开头
claim_count: SELECT COUNT(*) FROM claims WHERE employee_id = :employee_id
claim_amount: >-
SELECT amount_cents FROM claims
WHERE invoice_no = :invoice_nokind: sqlite或sql:一条只读 SQL。参数写成:名字,由测试里的where填;where里少了哪个参数,读取会报错停下。核对的值是第一行的第一列,没有行时是null。条数、金额返回数字,状态返回文字。kind: http:这段文字原样作为POST {base}/query里的query发出,测试的where作为params;你们的服务按每个id回一个值。可以是服务认识的名字(claim_count: claim_count),也可以是它自己执行的查询。- 还没有查询时写
{}。reviewctl init就写这个,上面的例子作为注释。
测试在数据检查里写查询的名字。场景文件里:
expected_state:
- id: claim-saved
query: claim_count
where: { employee_id: e_alice }
op: eq # eq、neq、lt、lte、gt、gte、in、exists
value: 1
applies: [completed] # completed、safe_stop 或 any(默认)开发机 AI 交的 JSON(submit_answer,格式见 get_prompt)里,同一条检查写成 "data_checks": [{ "name": "…", "query": "claim_count", "where": { "employee_id": "e_alice" }, "op": "eq", "value": 1, "when": "completed" }]。攻击条件的 goal.impact_state 和 verdict.side_effect_queries 也这样写查询名。reviewctl scenarios validate 会逐个核对场景用到的名字在不在这个文件里。
这个文件是测试接入文件:接本地测试机的,在仓库里改;在沙盒里跑的,用 put_wiring_file 或在网页上存。改完先预跑一次,让测试机把新名字报上来,测试才能用。
mode: fixture
自带的模拟后端,不用你们的任何系统。
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
fixture.pack | 名字@版本 | 必填 | 模拟后端,现在是 orders-refunds@1。 |
fixture.expose | mcp、rest 的列表 | [mcp] | Agent 怎么访问工具。 |
fixture.tool_aliases | 你们的工具名 → 模拟后端的工具名 | 无 | Agent 给工具起的名字不一样时用。 |
recheck_after_ms | 0 到 30000 | 关 | 同 own。 |
judge
评审模型为需要它的判断标准评审回复。它必须和 agent.model 不同。
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
model | 模型引用 | 必填 | 评审模型。在沙盒里,写的是平台提供的模型时,平台用项目的模型评审(见模型),按评审模型用量计费;其他模型要把它的密钥加成项目密钥。本地测试机上,这台机器有它的密钥时用机器上的密钥调用;没有、写的又是平台提供的模型时,和沙盒里一样由平台用项目的模型评审,按用量计费。见评审模型的密钥。 |
second_model | 模型引用 | 无 | 第二个评审模型:第一个评审模型对一次回复判断不了时,请它用同样的提示词和引用规则再判一次(见第二个评审模型)。沙盒里平台评审的运行不用写,平台用它提供的、和第一个不是同一家的模型里最强的那个,这里写了的照用这里的。本地测试机上要有它的密钥;不写、没有密钥、或者和 model、agent.model 相同时都不复核,报告里会写明没有配置。两个评审模型判得不一样时按更强的算:强弱按平台的模型目录排(vertex:gemini-4-argon、azure:gpt-6.1-sol、vertex:gemini-3.8-flash、azure:gpt-6-luna,前面的更强),目录外的模型排在它们后面。model 和 second_model 都在目录外时分不出强弱,判得不一样就不下结论。 |
embedding_model | 模型引用(openai: 或 openai-compatible:) | 无 | 本地测试机上给语义相似、切题这类判断标准用;不写的话它们在那里一直是判断不了。平台评审的运行不用写:用平台的嵌入模型(见语义相似类判断标准)。 |
effort | low、medium 或 high | low | 认这个字段,但现在不用。 |
scenarios
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
dir | 路径 | 必填 | 放数据查询(state_queries.yaml)、seeds/ 和 attacks/ 的目录。对 reviewctl review 来说,场景文件也放在这里。 |
include | 场景编号列表 | ['*'](全部) | reviewctl review 只跑这些。 |
required | 场景编号列表 | 无 | 少了其中一个,reviewctl review 报错停下。 |
平台发起的运行用网页上确认过的测试,不用 dir 里的场景文件。
gate
在测试机上用 reviewctl review 发起的运行的通过标准。平台发起的运行按项目的通过标准。key_tasks 为空时,本地运行只出数字、不下结论。
| 字段 | 类型 | 作用 |
|---|---|---|
key_tasks | {scenario, condition, pass_k: {k, min}} 的列表 | 每个关键任务在 k 次里的通过率至少是 min(0 到 1)。 |
reliability | {recover_min_rate, max_duplicate_side_effects} | Agent 从故障里恢复的比例至少多少,最多允许几次重复办事(比如重复退款)。 |
security | {block_on, max_attack_success, low_risk} | block_on 列出 verified_impact 和 verified_violation:出现这两类证实的问题就不通过。max_attack_success 是测试集里允许得逞的攻击比例(默认 0)。low_risk: warn:测试集里只是低危的得逞(只说出内部细节,或靠平台假设的场景)不算不通过,只提示;平台给安全测试集的默认通过标准带着它,不写就每次得逞都算。 |
regression | {max_functional_drop} | 测试集:和对比的那次运行相比,正常提问的通过率最多能降多少(0 到 1)。 |
evaluators | {scenario, id, min_rate} 的列表 | 测试集:某条判断标准的通过率至少多少。 |
tags | {scenario, tag, pass_k: {k, min}} 的列表 | 测试集:带某个标签的测试题要通过。 |
metrics | {scenario, metric, min_score} 的列表 | 打分的测试集:一组判断标准的平均分至少 min_score。 |
budget
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
per_attempt.minutes | 数字 | 必填 | 一次执行的时间上限;超过就停下。 |
per_attempt.model_calls | 整数 | 必填 | 一次执行最多调用几次模型;Agent 从 REVIEW_MAX_MODEL_CALLS 拿到它。 |
total_model_calls | 整数 | 无 | 整次运行最多调用几次模型;用完后,剩下的执行记为没执行。Agent 不上报调用次数时,每次按单次上限算。平台发起的运行会把它调大到够跑完全部计划的执行。 |
usd_estimate_cap | 数字 | 无 | 认这个字段,但现在不用。 |
cloud
| 字段 | 类型 | 默认 | 作用 |
|---|---|---|---|
auto_push | true 或 false | false | 本地 reviewctl review 跑完就上传到平台(要先 reviewctl login)。 |
api_url | URL | 无 | reviewctl push 用的平台地址。 |
runner_env
测试机变量:review.yaml 给测试机自己设的环境变量,只放不是秘密的值,比如仓库自己起的服务的地址、它自带测试后端的测试账号。测试后端自带测试账号的仓库,这些就不用再加项目密钥。
runner_env:
EXPENSE_TOKEN_ALICE: tok-e_alice # 仓库自带测试后端里的测试员工
EXPENSE_STATE_BASE: http://127.0.0.1:47591 # 它的 Compose 文件起的状态服务| 谁读它们 | 怎么读 |
|---|---|
| 测试里的用户 | 测试用户的 env:变量名 读这个变量。测试机把这里列出的变量名当作允许的身份。 |
backend.state.reader、backend.reset | base_url_env 和 url_env 写变量名。 |
| 重置命令 | 用测试机的环境变量运行,包括这些。 |
| Agent | 只拿到 agent.env_passthrough 里列出的,和别的变量一样。agent.env 不一样:它只给 Agent 设变量。 |
environment.setup 和 services 拿不到它们。
runner_env 里的变量,Agent 只有列进 agent.env_passthrough 才拿得到。一个变量既没列进去,平台自己也不用(不是数据读取或重置的地址变量、不是 Agent 的模型变量、不是测试用户的身份,重置命令和它跑的脚本里也没提到),保存覆盖层、「测试接入文件」和「开始前检查」都会提醒:「runner_env 里的 X 不会传给 Agent……要传给 Agent,把它加到 agent.env_passthrough」。只提醒,不拦。
- 最多 30 个变量。变量名只用大写字母、数字和下划线,以字母开头。值只能一行,最长 2 KB。
- 同名的项目密钥,或测试机上设的同名变量,会盖过这里的值。这样不改仓库也能换掉仓库里的值。
- 值看起来像真的凭证时,文件读不进来,报错里会写明是哪个变量。算凭证的有:
sk-…、sk_live_…、ghp_…、github_pat_…、xoxb-…、AKIA…、AIza…、GOCSPX-…、re_…、Bearer …这类密钥格式,JWT,私钥(-----BEGIN),以及 40 个字符以上、看起来是随机的一串。这样的值放进项目密钥;用自己的测试机的,就设成那台机器的环境变量。 - 这些值不是秘密,不会隐藏。能看仓库的人都看得到,日志和报告里也不打码。
- 沙盒的运行日志会列出这里设了哪些变量,只写名字。
reviewctl doctor也会列出来,并写明哪些以环境里的为准。
自己的测试机先更新 reviewctl
2026 年 10 月 1 日以前的 reviewctl 不认识 runner_env,会因为有不认识的字段拒绝整个文件。先更新,再重启测试机。在沙盒里,review.yaml 设了 runner_env 的运行遇到更早的机器镜像时,会立刻结束,并说明沙盒镜像里的 reviewctl 较旧、不支持这个任务。等平台的镜像更新后重新开始即可。