Skip to content

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。

yaml
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 起:

yaml
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 ​

字段类型默认作用
idprj_ 加 26 个字符必填项目编号,和网页上显示的一样。reviewctl runner register 要求它就是要配对的项目。在沙盒里,平台会把它换成发起这次运行的项目的编号,所以一个仓库可以给几个项目用。
name文字必填本地报告里的名字。
domain文字必填业务领域,比如 expense 或 generic。orders-refunds 是自带模拟后端的领域。

environment ​

沙盒怎么准备克隆下来的仓库。只用于连 GitHub 仓库的项目;不写就按仓库根目录的锁文件探测运行时。

字段类型默认作用
runtimenode@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)。
setupshell 命令列表,最多 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 分钟;存不上或恢复不了就跳过缓存,不影响这次测试。
archamd64 或 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 文件:

yaml
services:
  compose: docker-compose.test.yml

要么按名字逐个列出(名字是小写短名,最长 31 个字符):

字段类型默认作用
image镜像地址必填要起的容器镜像。
envNAME: 值 的映射,最多 30 个无容器的环境变量。
ports端口号列表,最多 10 个无发布在 127.0.0.1 上给 Agent 用的端口。
readyshell 命令无每 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 要求同时重建服务:

字段类型默认作用
restoreall,或 Compose 文件里的服务名列表无配了 backend.reset 时:每次执行的重置之前,先把这些服务(或全部)用镜像连同数据卷重新创建,别的服务不动。只能和 compose 一起写。没配 backend.reset 时全部 services 照样每次恢复。

services 里的服务,以及重置、安装命令起的容器,都不能拿到 Docker 或沙盒这台机器本身的控制权。下面这些都不允许:

  • 挂 /var/run/docker.sock、/run/docker.sock,或它们所在的目录、指向它们的符号链接
  • privileged,或 pid、network_mode、userns_mode 写 host
  • cap_add 加 SYS_ADMIN 这类权限,或 seccomp/apparmor 写 unconfined
  • 拿 GPU 以外的宿主机设备
  • 挂这台机器的 /proc、/sys、/dev 或用户目录,或可写地挂它的系统目录
  • 设 DOCKER_HOST

这样的 Compose 文件,沙盒在启动前就拒绝,你们自己写的和平台写的都一样,并说清是哪个服务、挂了什么。执行之间这样起的容器,会在 Agent 运行前删掉,这次执行以同样的原因失败。要每次执行前换一套新的服务,不要起一个管容器的服务,而是不写 backend.reset,或者用 restore。

执行可能在 services 之外另起容器,比如重置时用它们的镜像、在它们的网络里为这一次新建一套服务。这些容器在这次执行结束后、下一次重置之前,就连同数据卷删掉。每次执行照旧用自己新起的,不会一次次积下来把沙盒的内存占满。

沙盒里的测试每执行完一次,也会交一份部分报告。测试没跑完沙盒就失联的话,最后这份就是这次测试的报告:已执行的照常判,没来得及执行的标成没有结果。

agent ​

每次执行怎么启动 Agent。

字段类型默认作用
drivercommand、http 或 harness必填测试机和 Agent 怎么对接。
command参数列表command 时必填命令行,每次执行起一个进程。占位符见下。
endpoint见下http 时必填一个在跑的 HTTP 接口。
harnessJavaScript 模块路径harness 时必填导出 runTask(input, wiring, signal) 的模块,在测试机进程里运行。
envNAME: 值 的映射无给 Agent 的普通设置,比如 AGENT_VARIANT。不放密钥。
env_passthrough变量名列表无Agent 可以从测试机的环境里拿到的变量:沙盒里是项目密钥,本地测试机上是机器的环境或 .env。沙盒里没有 OPENAI_API_KEY 这个项目密钥时,不管这里写没写,Agent 都会拿到调用平台模型用的 OPENAI_BASE_URL 和 OPENAI_API_KEY。
model模型引用无Agent 自己用的模型,写进报告。评审模型必须和它不同。
version.gittrue 或 falsetrue用 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 行作为过程记录保留。

json
{"text": "报销单 C-1042 已提交。", "usage": {"model_calls": 4}}

http 方式 ​

字段类型默认作用
endpoint.urlURL必填每个任务以 POST 发到这里,请求体是 {"prompt", "actor_token", "max_steps"}。
endpoint.reply_path以 $ 开头的路径必填回复在返回 JSON 里的位置,比如 $.reply。以 server-sent events 返回的,取最后一个 final 事件。
endpoint.send_filesnone 或 base64nonebase64 把测试文件放进请求体: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 加上这一轮用户的话,聊天格式)。
harnessrunTask 每轮调用一次,带 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 到 126用户最多说几句,包括第一句。
stop_when文字,1 到 300 个字符—除了目标达成,什么时候停;不写就是目标达成或明显办不成时停。
yaml
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 自己的问题转给平台:

http
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 无界面运行时,用某个工具前要先问权限,又没人能答,这个工具就一直没执行,测试也没测到。要测到它,可以在测试里写明预先批准这个工具:

yaml
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 的工具调用经过测试机,每次调用都会记下来,也能往里注入故障和攻击。

字段类型默认作用
modemcp_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 服务走工具代理;其余的照原样直连。
applyenv 或 in_placeenvenv 把包装后配置的路径放在 REVIEW_MCP_CONFIG 里交给 Agent;in_place 在这次执行期间把它写到 mcp_config 上,结束后放回原文件。
base_url_env变量名http_proxy 时必填Agent 从这个变量读工具的基础地址;测试机把它设成工具代理。
upstream_base_urlURLhttp_proxy 时必填真正的工具在哪里。
routes工具名 → {method, path} 的映射http_proxy 时必填每个工具的 REST 路由。method 是 GET、POST、PUT 或 DELETE;path 以 / 开头,{name} 段变成参数。工具名用字母、数字、_ 和 -,最长 64 个字符。
enforce_forbidden_writestrue 或 falsefalse测试禁止的写操作直接拦下,而不只是记下来。
process_egresstrue 或 falsefalseAgent 进程的其他出站 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_ms0 到 300003000检查后隔这么久(毫秒)再读一次数据,每个场景一次执行,用来接住来得晚的结果。0 关掉。

重置方式:

type字段作用
commandcommand(参数列表)在 review.yaml 所在目录执行命令(连仓库和自带镜像是 .reviewctl/,见路径从 .reviewctl/ 算起),以测试机自己的用户,环境是测试机的环境(包括 runner_env)加 REVIEW_WORK_DIR 和 REVIEW_ATTEMPT_SHORT。它建的文件 Agent 不一定读得到,见上面 environment 里的提醒。
httpurl;method(POST 或 PUT,默认 POST);body;body_file(一个 JSON 文件,合并在 body 下面);state_ref_path(返回里数据引用的位置,比如 $.state_ref)用 JSON 请求体调用你们的重置接口。
sqlurl_env(放数据库地址的变量);script(SQL 文件路径)重建数据库再执行脚本。现在只支持 SQLite:sqlite:///path、file:///path 或普通路径。

重置的参数和请求体里的字符串中,{{attempt_short}}、{{attempt_id}} 和 {{work_dir}} 会被换掉。

读取方式:

kind字段读什么
sqlitepath、catalog一个 SQLite 文件。
sqlurl_env、catalog变量里地址指向的数据库;现在只支持 SQLite。
httpbase_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 查询里 :名字 形式的参数,报给平台;查询本身不上传。测试里写了没报上来的名字,导入时会失败。

yaml
# 名字: 读取器执行的内容。名字用小写字母、数字和 _,以字母开头
claim_count: SELECT COUNT(*) FROM claims WHERE employee_id = :employee_id
claim_amount: >-
  SELECT amount_cents FROM claims
  WHERE invoice_no = :invoice_no
  • kind: sqlite 或 sql:一条只读 SQL。参数写成 :名字,由测试里的 where 填;where 里少了哪个参数,读取会报错停下。核对的值是第一行的第一列,没有行时是 null。条数、金额返回数字,状态返回文字。
  • kind: http:这段文字原样作为 POST {base}/query 里的 query 发出,测试的 where 作为 params;你们的服务按每个 id 回一个值。可以是服务认识的名字(claim_count: claim_count),也可以是它自己执行的查询。
  • 还没有查询时写 {}。reviewctl init 就写这个,上面的例子作为注释。

测试在数据检查里写查询的名字。场景文件里:

yaml
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.exposemcp、rest 的列表[mcp]Agent 怎么访问工具。
fixture.tool_aliases你们的工具名 → 模拟后端的工具名无Agent 给工具起的名字不一样时用。
recheck_after_ms0 到 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:)无本地测试机上给语义相似、切题这类判断标准用;不写的话它们在那里一直是判断不了。平台评审的运行不用写:用平台的嵌入模型(见语义相似类判断标准)。
effortlow、medium 或 highlow认这个字段,但现在不用。

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_pushtrue 或 falsefalse本地 reviewctl review 跑完就上传到平台(要先 reviewctl login)。
api_urlURL无reviewctl push 用的平台地址。

runner_env ​

测试机变量:review.yaml 给测试机自己设的环境变量,只放不是秘密的值,比如仓库自己起的服务的地址、它自带测试后端的测试账号。测试后端自带测试账号的仓库,这些就不用再加项目密钥。

yaml
runner_env:
  EXPENSE_TOKEN_ALICE: tok-e_alice              # 仓库自带测试后端里的测试员工
  EXPENSE_STATE_BASE: http://127.0.0.1:47591    # 它的 Compose 文件起的状态服务
谁读它们怎么读
测试里的用户测试用户的 env:变量名 读这个变量。测试机把这里列出的变量名当作允许的身份。
backend.state.reader、backend.resetbase_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 较旧、不支持这个任务。等平台的镜像更新后重新开始即可。

下一步 ​

  • 给文件里写到的变量加项目密钥。
  • 沙盒与上限:规格档、架构,以及机器镜像里有什么。
  • 命令行:reviewctl init 生成第一份 review.yaml,reviewctl doctor 检查它。

本站的法律文件都是草稿,待律师审阅,尚未生效。