关于 rust-review
供 Claude Code 使用的 Rust 应用与库安全审查技能,通过多个阶段协调子 Agent 检查代码,并在去重和复核误报、严重度后生成 Markdown 与 SARIF 报告。
主要功能
- 01审查 Rust 内存安全与 unsafe 边界
- 02检查并发、FFI 和异步风险
- 03将代码簇分配给并行子 Agent
- 04对发现去重并评估误报
- 05生成 Markdown 与 SARIF 报告
适用场景
- 01审查库中的 unsafe Rust 代码
- 02评估 Rust 服务的 panic 拒绝服务风险
- 03审计代码库中的并发与 FFI 风险
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trailofbits/ skills --skill rust-review安装后在 Claude Code 中输入/rust-review确认已加载。
详细介绍
Rust 安全审查
在主对话中运行(通过 /rust-review:rust-review 调用)。编排器将 Task* 账本用作重试记录;工作者和评审者没有 Task 工具。工作者和评审者是命名的插件子 Agent(rust-review:rust-review-worker、rust-review:rust-review-dedup-judge、rust-review:rust-review-fp-judge);工具集在 plugins/rust-review/agents/*.md 中声明。审查结果通过共享输出目录中的 Markdown-with-YAML 文件交换。
适用场景
Rust 应用/库安全审查:安全/不安全边界审计、unsafe 代码块中的内存安全、并发隐患、服务器上由 panic 引发的拒绝服务、FFI 安全、异步运行时错误。
不适用场景
- 纯 C / 纯 C++ 代码库——请改用
c-review。 - 智能合约(Solana 程序 / NEAR 合约 / Ink!)——请改用
solana-vulnerability-scanner或针对特定合约的技能。 - 没有用户空间分配器的内核模式 Rust 驱动程序——覆盖不完整;只能作为建议标记。
- 密钥/密钥内存卫生(清零、
Zeroize/ZeroizeOnDrop/secrecy的用法、遗留的栈/堆副本)——请使用zeroize-audit技能;rust-review 不涵盖内存清零。
子 Agent
| 子 Agent 类型 | 用途 | 工具集 |
|---|---|---|
rust-review:rust-review-worker | 执行分配的集群,撰写审查结果 | Read、Write、Edit、Bash |
rust-review:rust-review-dedup-judge | 合并重复项(首先运行) | Read、Write、Edit、Glob |
rust-review:rust-review-fp-judge | 误报 + 严重程度 + 最终报告(其次运行) | Read、Write、Edit、Bash |
工具来自各 Agent 启动时的 frontmatter。编排器的 Task*/Agent/Bash/等工具来自此技能的 allowed-tools。**搜索工具 / Bash 交互:**在当前版本的 Claude Code 中,获准使用 Bash 的 Agent 不会同时获准使用专用的 Glob 或 Grep 工具(调用会返回 No such tool available;执行环境要求改用 Bash 调用 find/grep/rg)。因此,只有去重评审者——唯一不持有 Bash 的 Agent——会使用 Glob;工作者、误报评审者和编排器则通过 Read / Bash find / rg / grep / test -f 来解析和搜索路径。由于集群/查找器提示词种子使用 ripgrep 正则表达式语法(\s、\d、\b),持有 Bash 的 Agent 必须使用 rg 运行这些提示词种子。如果未安装 rg,调用会明确失败(command not found)——请改用带 POSIX 字符类的 grep -E(\s→[[:space:]],\d→[[:digit:]],删除 \b);切勿使用原样包含 \s 的 grep,因为它会悄无声息地返回空结果,导致错误地标记为 cleared。不要将 Glob/Grep 重新加入持有 Bash 的 Agent 协议。
架构
coordinator: write context.md → build_run_plan.py → TaskCreate × M
→ spawn primer (foreground) → spawn M workers (parallel)
→ classify Phase-7 outcomes + write findings-index.txt
→ dedup-judge → fp-judge → report safety net (SARIF + REPORT.md) → return REPORT.md
输出目录包含:context.md、plan.json、worker-prompts/、findings/、findings-index.d/(按工作器划分的分片)、findings-index.txt、coverage/(按工作器划分的覆盖率门控文件)、run-summary.md、dedup-summary.md、fp-summary.md、REPORT.md、REPORT.sarif。
路径约定: 后续每个阶段都会调用 ${RUST_REVIEW_PLUGIN_ROOT}/scripts/*.py,因此请先将该变量解析为包含 prompts/clusters/unsafe-boundary.md(以及 scripts/build_run_plan.py)的插件目录。按以下顺序尝试,以第一个命中的结果为准:
- 原生 Claude Code —
${CLAUDE_PLUGIN_ROOT},如果Bash: ls "${CLAUDE_PLUGIN_ROOT}/prompts/clusters/unsafe-boundary.md"能解析到该文件,则接受。 - Codex —
${CODEX_PLUGIN_ROOT}(如果该变量存在且能解析到标记文件,则以相同方式设置)。 - 备用搜索 — 搜索 Codex 在
~/.codex下的安装、Claude 在~/.claude下的安装,以及本地检出目录/仓库运行环境:Bash: find ~/.claude ~/.codex . -path '*/plugins/rust-review/prompts/clusters/unsafe-boundary.md' -print -quit 2>/dev/null。取匹配结果,并移除末尾的/prompts/clusters/unsafe-boundary.md,即可得到根目录(先搜索主目录,再搜索.,因此已安装的副本优先于被审计仓库中的随附副本)。
将解析出的根目录设为 RUST_REVIEW_PLUGIN_ROOT。如果三种方式都失败,则中止并列出搜索过的根目录——不要在变量为空的情况下进入第 4 阶段(每个 uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/..." 调用都会因路径错误而失败,且错误信息难以理解)。
范围约定: 在整个运行过程中,请将以下两个范围分开处理:
finding_scope_root— 用户要求审计的子树。工作器只能提交漏洞位置位于此子树内的发现。context_roots— 工作器和评审者可检查的只读仓库根目录/文件,用于验证可达性、调用方、包装器、构建标志、缓解措施和威胁模型细节。除非用户明确禁止查看更广泛的上下文,否则默认为.。允许读取finding_scope_root之外的上下文,但不得在该范围之外提交发现。
需要避免的错误理由
- “
unsafe很少见,所以手动跳过内存安全集群。” 不要编辑集群列表——在阶段 1 中准确设置has_unsafe,并让build_run_plan.py来决定。每种内存安全漏洞类别(UAF、double-free、未初始化读取、Vec::set_len、union UB)都需要unsafe,因此当has_unsafe=true时,规划器会运行整个memory-safety集群;当其为false时,则会正确省略该集群——不存在“无论如何都运行”的选项。unsafe-boundary 集群则不同:它没有requires,始终会运行(已整合;即使没有明显的unsafe { }代码块,其安全文档和repr(C)规范仍适用于 FFI 声明)。 - “编译器已经发现了。” 借用检查器能证明安全代码中不存在数据竞争;但它无法证明
unsafe代码块、panic 的可达性、ABBA 死锁、原子加载/存储顺序或 FFI ABI 不匹配方面不存在问题。 - “如果有
// SAFETY: documented infallible,那unwrap()就没问题。”// SAFETY:用于记录unsafe操作,而不是不可失败的断言。对文档称为不可失败的输入调用unwrap()仍有风险,因为文档可能有误——按低严重性提交,并让 FP 判定器决定。 - “
has_unsafe=false,所以跳过运行。” 纯安全 Rust crate 仍可能存在 panic-DoS、原子竞争、drop-panics 和 trait 实现风险。请运行始终启用的集群。 - “后台启动可以让工作器并行运行。” 并非如此——同一条助手消息中的
Agent调用本来就会并发运行。run_in_background=true会绕过阶段 6a 的提示词缓存,因此每个工作器都要完整创建缓存(cache_read_input_tokens=0),约 15 K 个 token 的提示词会被浪费 M 次。默认做法:启动工作器时省略run_in_background。 - “我会在行内重新推导集群列表/路径/轮次前缀,而不运行
build_run_plan.py。” 该脚本是选择和渲染的唯一依据。改述脚本内容会遗漏工作器自检所需的字段,导致worker-N abort: spawn prompt malformed。务必运行脚本并Read plan.json。 - “运行只部分成功——我就根据已完成的部分写
REPORT.md。” 用成功报告掩盖部分运行属于正确性缺陷。如果任何阶段 5 集群任务的状态不是completed,请在run-summary.md和最终回复中醒目说明。 - “没有发现问题——跳过阶段 8。” 始终运行两位判定器和阶段 8b:当索引为空时,dedup-judge 会写入最简无操作
dedup-summary.md;fp-judge 会写入空的REPORT.md/REPORT.sarif;对于空情况,阶段 8b 的 SARIF 生成器会输出results: []。SARIF 消费方依赖稳定的产物集合。 - “预检时用
Bash: ls README*没问题。” 在 zsh 下,未匹配的 glob 会在2>/dev/null执行之前中止整个复合命令。请使用find(即使没有匹配项也不会失败),不要使用Glob;同时持有Bash的 Agent 无法使用Glob。
编排工作流
请在主对话中运行以下阶段。
阶段 0:收集参数
入口: 已调用 skill。出口: 已确定 threat_model、worker_model、severity_filter;已确定 scope_subpath 或将其设为 ".";finding_scope_root=scope_subpath;已确定 context_roots。
直接调用 skill(不使用命令包装器)。解析用户在 /rust-review:rust-review 行传入的任何自由文本参数(例如 flamenco only、high severity only、use haiku),并预先填入这些参数所暗示的答案,然后只用一次 AskUserQuestion 调用询问所有缺失的必需参数。绝不要默默为必需参数设置默认值。
必需参数:
| 参数 | 取值 | 如何从参数中推断 |
|---|---|---|
threat_model | REMOTE / LOCAL_UNPRIVILEGED / BOTH | 类似“remote”“network”“attacker”的词 → REMOTE;“local”“unprivileged” → LOCAL_UNPRIVILEGED;否则询问。 |
worker_model | haiku / sonnet / opus | 参数中明确给出的模型名称。否则询问(不得默默设置默认值)。 |
severity_filter | all / medium / high | “all”“every”“noisy” → all;“medium and above” → medium;“high only”“criticals only” → high。否则询问——不得默默设置默认值。 |
scope_subpath | 相对于仓库的目录(可选) | 类似“X only”“just audit X/”“review subdirectory X”的短语 → src/X/ 或匹配的子目录。根据仓库的顶层子目录进行模糊匹配。若未指定,设为 ".";若有歧义,则询问。 |
仅对尚未确定的必需参数(threat_model、worker_model、severity_filter)调用 AskUserQuestion 一次;只有当用户明确要求缩小范围但范围有歧义时,才将 scope_subpath 一并纳入。如果所有必需参数都已预先填入,且范围未指定或已确定,则跳过提问。
确定 scope_subpath 后,设置 finding_scope_root="${scope_subpath:-.}"。默认将 context_roots=".",以便 worker 能在缩小范围的子树之外核实调用方和构建设置,而不提交范围外的发现结果。如果用户明确要求禁止查看更广泛的上下文,则设置 context_roots="${finding_scope_root}",并说明可达性置信度可能较低。
阶段 1:先决条件
入口: 阶段 0 已完成。出口: 已确定 has_unsafe、has_ffi、has_concurrency、has_async、has_packed_repr、has_fs_io 标志。如果 ${finding_scope_root} 下不存在 *.rs 文件,则中止并显示清晰的提示。
首先确认存在 uv(command -v uv)——从阶段 4 开始的所有辅助脚本都通过它运行。如果缺少,请中止并告知用户安装它(brew install uv 或使用官方安装程序);没有它,下游步骤都无法运行。
使用以下 Bash 命令在 ${finding_scope_root:-.} 内进行探测(输出非空 ⇒ 标志为 true)。由于此协调器持有 Bash,专用的 Grep/Glob 工具不可用——请通过 Bash 使用 grep/rg/find。(探测正则使用 \s/\b;如果你的 grep 不支持 GNU \s,请使用 rg -uu 运行它们——它支持 \s,且仍会搜索被忽略的文件;如果也未安装 rg,则将 \s 替换为 [[:space:]],并去掉 \b。扩大匹配范围在这里是安全的:误报能力标志只会多安排一个无害的工作器,而漏报则会跳过整个检查阶段。)
# Rust source presence (precondition)
find "${finding_scope_root:-.}" -name '*.rs' -print -quit
# has_unsafe
grep -rlE '\bunsafe\s+(extern|fn|impl|trait)\b|\bunsafe\s*\{' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_ffi
grep -rlE 'extern\s+"(C|system|stdcall|cdecl|win64|sysv64|aapcs|fastcall|thiscall|vectorcall|efiapi)(-unwind)?"|\bextern\s+fn\b|extern\s+\{|#\[repr\((C|transparent)\b|\b(CString|CStr)\b|use\s+(libc|core::ffi|std::ffi|std::os::raw|cty)|\blibc::|\b(bindgen|cbindgen)\b|\bc_void\b' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_concurrency
grep -rlE '\b(std::(thread|sync)|parking_lot::|crossbeam|rayon::|tokio::sync|core::sync::atomic|std::sync::atomic|Atomic[A-Za-z0-9_]*|UnsafeCell|static\s+mut|unsafe\s+impl\s+(Send|Sync)|memmap2::|Mmap(Options|Mut)?|MAP_SHARED|shm_open|mmap\s*\(|memfd_create|shared_memory|raw_sync|CreateFileMapping|MapViewOfFile|once_cell|sigaction|signal_hook|nix::sys::signal|libc::signal|libc::sigaction)' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_async
grep -rlE '\basync\s+(fn|move|\{)|\.await\b|tokio::|async_std::|futures::' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_packed_repr (outer #[repr(...packed...)] and inner #![repr(...packed...)])
grep -rlE '#!?\[repr\([^]]*packed' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_fs_io (path types / construction)
grep -rlE '\bPathBuf\b|\bPath\b' --include='*.rs' "${finding_scope_root:-.}" | head -1
# has_fs_io (fs module and file APIs)
grep -rlE '\bfs::|\bFile::(open|create)\b|OpenOptions|\.exists\(\)|\.metadata\(|symlink_metadata|read_dir|read_to_string' --include='*.rs' "${finding_scope_root:-.}" | head -1
与其他标志一样,任一探测的输出非空,就表示该标志为 true。这些检测器有意采取保守策略:拿不准时就将标志设为 true,因为误报标志只会多安排一个无害的工作器,而漏报则会跳过实际需要的检查阶段。has_fs_io 根据路径类型(PathBuf/Path,也涵盖 &Path 参数和裸 Path:: 调用)以及文件系统锚点(fs::/File::/OpenOptions/read_dir/…)进行判断,而不是根据裸 .join(/.push( 调用判断——通过路径类型锚点即可覆盖路径构造;将 join/push 排除在条件之外,可避免匹配到无关的迭代器/JoinHandle join 和 Vec::push,否则几乎每个 crate 都会触发该条件。
关于 Cargo.toml:也要探测 [dependencies] 中的 tokio、async-std 等,以便即使作用域子路径中尚无 .await,也将 has_async=true(库 crate 通常会重新导出)。
还要探测是否存在 Cargo.toml(仅供参考——在 run-summary.md 中注明审查针对的是 Cargo 工作区、单个 crate,还是零散的 .rs 文件):
# context_roots may be comma-separated (build_run_plan.py treats it as a list),
# so probe each root rather than passing "a,b" as one (nonexistent) path.
echo "${context_roots:-.}" | tr ',' '\n' | while IFS= read -r root; do
find "${root:-.}" -name 'Cargo.toml' -print -quit
done | head -1
阶段 2:输出目录
入口: 阶段 1 标志已设置。出口: 已解析出绝对路径 output_dir;${output_dir}/findings/ 和 ${output_dir}/coverage/ 已存在。
解析 output_dir 的绝对路径(默认值:$(pwd)/.rust-review-results/$(date -u +%Y%m%dT%H%M%SZ)/):
mkdir -p "${output_dir}/findings" "${output_dir}/coverage"
coverage/ 子目录用于存放每个工作器的覆盖门审计文件(coverage/worker-{N}.md)。工作器将结果写入该目录,而不是将表格嵌入回复中——参见 agents/rust-review-worker.md 第 5 步。
第 3 阶段:代码库上下文
入口: ${output_dir} 已存在。出口: 已写入 ${output_dir}/context.md。
浏览 README.{md,rst,txt} 和任何构建/清单文件(Cargo.toml、Cargo.lock、rust-toolchain.toml、build.rs)——在任何 Read 之前先用 find(通过 Bash)进行预检查(对缺失文件执行 Read 会中止本轮;由于编排器持有 Bash,因此无法使用 Glob)。预检查时不要使用 Bash: ls README*:在 zsh 下,如果通配符没有匹配项,整个复合命令会在执行 2>/dev/null 之前中止。请使用 find . -maxdepth 2 -name 'README*' -o -name 'Cargo.toml' -o -name 'rust-toolchain.toml' -o -name 'build.rs';即使没有匹配项,也不会失败。
将以下内容写入 ${output_dir}/context.md:YAML frontmatter(threat_model、severity_filter、scope_subpath、finding_scope_root、context_roots、has_unsafe、has_ffi、has_concurrency、has_async、has_packed_repr、has_fs_io、output_dir、cargo_manifest;cargo_manifest 的值为 workspace/single-crate/absent,若存在则附上路径),然后撰写简短的 Markdown 正文,包含五个部分——用途(1-3 句话)、范围(说明 finding_scope_root 中的内容,以及范围外的发现不在检查范围内)、入口点(不可信数据的进入位置:网络、文件、CLI、IPC、serde 反序列化、FFI 输入)、信任边界(沙盒化的对等方、受信任的对等方与任意远程对象之间的区别)、现有加固措施(模糊测试工具、MIRI 运行、clippy::pedantic、cargo-deny、cargo-audit)。
阶段 4:构建运行计划(确定性)
入口: 已知能力标志和 threat_model;${output_dir}/findings/ 已存在。出口: 已写入 ${output_dir}/plan.json 和 ${output_dir}/worker-prompts/*.txt;已知 M = worker_count。
选择、筛选、路径解析和生成派生提示词均委托给脚本,以确保派生提示词完整且一致:
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/build_run_plan.py" \
--plugin-root "${RUST_REVIEW_PLUGIN_ROOT}" --output-dir "${output_dir}" \
--threat-model "${threat_model}" --severity-filter "${severity_filter}" \
--scope-subpath "${finding_scope_root:-.}" --context-roots "${context_roots:-.}" \
--has-unsafe "${has_unsafe}" --has-ffi "${has_ffi}" \
--has-concurrency "${has_concurrency}" --has-async "${has_async}" \
--has-packed-repr "${has_packed_repr}" --has-fs-io "${has_fs_io}" \
--max-passes-per-worker 4
脚本会写入 plan.json、worker-prompts/worker-N.txt,以及(若 --cache-primer=true,这是默认值)worker-prompts/cache-primer.txt,并在 stdout 输出 JSON 摘要。任何提示词缺失时都会以非零状态码退出——显示消息并停止。使用默认的 --max-passes-per-worker 4 时,规划器会为纯安全 Rust(无 FFI/并发/异步;始终启用 info-disclosure)选择约 8 个集群 → M ≈ 13 个工作器;为并发安全 Rust 选择约 10 个集群 → M ≈ 15;为完整 Rust(unsafe + FFI + 并发 + 异步,并在 has_fs_io 时启用 input-os-safety,在 has_packed_repr 时启用 layout-safety)选择约 15 个集群 → M ≈ 23。M 是分块后的工作器数量(plan.workers.length),因此会高于集群数量——分块会拆分包含多个阶段的非整合集群(例如 panic-dos、memory-safety),而两个整合集群(unsafe-boundary、concurrency-locking)永远不会分块:每个集群由一个工作器构建一次共享清单并运行其所有阶段。recursion-dos 每个工作器只运行一个阶段。返回后,读取 plan.json 获取结构化选择结果——绝不要重新推导筛选结果或路径。
--max-passes-per-worker N 用于限制每个工作器的阶段数。规划器会确定性地将阶段数超过 N 的任意非整合集群拆分为 ceil(K/N) 个连续分块;每个分块都会成为独立的 rust-review-worker 派生调用,并使用带 -{i} 后缀的 cluster_id(例如 panic-dos-1、panic-dos-2)。整合集群(unsafe-boundary、concurrency-locking)不受此限制——无论阶段数或覆盖设置如何,都绝不分块,因此一个工作器会构建一次共享的 Phase-A 清单并运行所有阶段(分块整合集群会迫使每个分块都重新构建该清单,而工作器实际上会跳过此步骤)。共享提示词缓存前缀和 Cluster prompt: 路径在所有分块中逐字节完全相同,因此缓存预热器仍可为每个工作器预热缓存。默认值 4 是针对 manifest.json 中的重尾集群校准的。一些输出量大的非整合集群会在清单级别声明更小的 max_passes_per_worker 覆盖值,让每个开销较大的阶段都由独立工作器处理(例如 recursion-dos)。传入 --max-passes-per-worker 0 可禁用所有分块,包括清单覆盖值(每个集群使用一个工作器)。
阶段 5:创建记账任务(仅编排器内部)
入口: ${output_dir}/plan.json 已存在;M = plan.workers.length。出口: 已创建 cluster_task_ids[](与 plan.workers 一一对应),且全部为 pending。
任务台账仅用于编排器记账(TUI 可见性 + 第 7 阶段重试跟踪)——工作器永远不会读取或写入它。每个工作器创建一个 TaskCreate,并在 metadata 中填入 kind="cluster"、worker_n、cluster_id、spawn_prompt_path、pass_prefixes、attempt=1——所有值均从 plan.workers[i] 原样复制。按 plan.workers 的顺序跟踪 cluster_task_ids[]。
阶段 6:派生工作器(可选的缓存预热器优先,然后并行派生 M 个工作器)
入口: 已填充 cluster_task_ids[];${output_dir}/worker-prompts/worker-N.txt 中存在各工作器的派生提示词文件。出口: 所有 M 次 Agent 调用——包括每一轮——均已返回(并行派生块已完成)。
阶段 6a:缓存预热器(取决于 plan.run.cache_primer)
从冷启动开始并行批处理无法共享缓存(所有 M 个请求同时分发,没有一个完成写入)。要预热前缀,先启动一个小型预热器——前台(后台启动的任务不会与后续前台启动的任务共享缓存)。
如果 plan.run.cache_primer == true,build_run_plan.py 已写入 ${output_dir}/worker-prompts/cache-primer.txt。在单独的助手消息中启动它:Read 该文件,将内容原样作为 Agent 的 prompt 传入,并设置 subagent_type=rust-review:rust-review-worker、model=${worker_model}、description="Rust review cache primer",不要设置 run_in_background。脚本写入的前缀与 worker-1.txt 中 <context> 块之前的内容逐字节相同——正是这种逐字节一致性,才能让并行工作器命中缓存。预热器结尾包含 Cache primer: true,工作器系统提示词会将其视为一种一等模式,并在单条文本响应中原样返回 worker-PRIMER abort: cache primer (no analysis performed),且不调用任何工具。丢弃这条中止信息——阶段 7 会忽略它(没有 worker-N ID)。
前台启动本身已会串行化,因此阶段 6b 之前不需要 sleep。如果 plan.run.cache_primer == false,则完全跳过阶段 6a。
阶段 6b:并行启动 M 个实际工作器(每波一条消息,最多 16 个)
停止——撰写启动消息前请先阅读此处。
必须以前台方式启动工作器(不设置
run_in_background字段,或设置run_in_background=false)。 此处的“并行”是指一条助手消息包含该波的Agent调用——这样就会让它们并发运行。(对于较大的M,拆分成连续的多波,每波最多 16 个调用,每波各用一条消息——见下文“必需的启动形式”。)后台启动不是并行执行此技能的方式。后台启动会破坏阶段 6a 的预热器缓存:每个工作器在首次运行时都要完整创建缓存(
cache_read_input_tokens=0),预热器的约 15 K 个 token 会被浪费 M 次。两次实际运行都恰好出现了这种情况——每个工作器启动时first_cr=0。发送启动消息前,请检查草稿:每个
Agent调用都必须不包含run_in_background键。如果写了run_in_background=true,请删除。
必需的启动形式: 发出一条包含该波 Agent 工具调用的助手消息——正是这条消息让它们并发运行。按顺序启动(每条消息一个 Agent 调用)会串行执行审查,同样是错误的,但这种失误很明显(表现为耗时);后台启动造成的失误则是隐性的(表现为成本)。
当 M 超出每条消息的上限时分波。 执行框架会限制单条助手消息可分发的 Agent 调用数量(观察到:Claude Code 中约为 20 个——一次实际的 25 个工作器运行中,系统悄悄只保留了前 20 个,剩余 5 个不得不在第二条消息中启动)。因此,当 M > 16 时,请预先规划各波:将工作器拆分为连续的多波,每波最多 16 个 Agent 调用,每波各用一条助手消息。规则如下:
- 在一个波次内:同一条消息中的所有
Agent调用都必须在前台执行(不得使用run_in_background),且形态与单波次运行完全相同。 - 跨波次:第 k+1 波必须作为单独的消息发送,并且只能在第 k 波的所有
Agent调用都返回后发送(工具调用消息会结束本轮)。因此,各波次之间会按顺序执行——这是正确的,而且很明显;请接受这一点。不要尝试让它们重叠执行。 - 切勿使用
run_in_background=true,试图在一条消息中容纳更多工作器。增加的是波次,绝不是后台任务——后台执行会破坏预热缓存(见 STOP 框),也是本技能要防范的首要错误。 - **跨波次缓存:**预热前缀的缓存 TTL 约为 5 分钟,每次命中都会刷新,因此连续发送的波次会持续命中缓存(25 个工作器的运行确认,第二波的
cache_read≈14 K)。如果后续波次将在上一波结束 5 分钟以上才启动(M非常大或工作器运行较慢),请先在单独的消息中重新运行 Phase-6a 预热,然后再发送该波次。 - 平衡各波次(例如
M=25时分为 13+12,而不是 20+5),这样每波都不会逼近上限,最后一波也不会只剩少数工作器。 - 所有波次都返回后,使用全部 M 个工作器的结果进入 Phase 7。
对于每个工作器 N ∈ [1..M](在其分配的波次中):
Read: ${output_dir}/worker-prompts/worker-N.txt- 将文件内容原样作为
Agent工具的prompt参数传入:
| 参数 | 值 |
|---|---|
subagent_type | rust-review:rust-review-worker |
model | ${worker_model} (haiku / sonnet / opus) |
description | Rust review worker N |
prompt | worker-N.txt 的完整文本(不作修改) |
run_in_background | **必须省略此字段,或将其设为 false。**绝不能设为 true。请参阅上面的前台启动警告。 |
启动提示词是唯一依据。请原样传入——工作器的自检要求每个字段都必须存在;任何偏差都会触发 worker-N abort: spawn prompt malformed。
应拒绝的反模式:
- 传入
run_in_background=true(见上面的警告)。 - 当
M较大时,在一条消息中塞入超过约 16 个Agent调用——执行框架可能会静默保留前约 20 个,并丢弃其余调用。应使用平衡的波次,每波 ≤16 个;绝不在后台启动,以覆盖全部 M 个工作器。 - 不读取
worker-N.txt,而是手动输入启动提示词。 - 插入与 Task 相关的指令(“先调用 TaskList”、“Assigned task id: ”)。工作器没有 Task 工具。
- 编辑渲染后的提示词再传入(删掉“冗余”字段、合并 pass 列表等)。
阶段 7:等待 Worker 并对结果分类
**入口:**所有 Phase-6 Agent 调用均已返回。**出口:**每个集群都已成功,或已重试至上限;${output_dir}/findings-index.txt 已写入。
Phase-6 的 Agent 调用会阻塞,直到每个 Worker 返回。检查每个 Worker 的返回文本,并按顺序应用以下分类器——匹配到的第一项优先:
| # | 匹配(在返回文本中) | 结果 | 操作 |
|---|---|---|---|
| 1 | worker-N complete: | 暂定成功 | 解析 wrote N finding files 数量,然后在将 TaskUpdate 设为 completed 前运行下方的产物验证器。 |
| 2 | abort: spawn prompt malformed、abort: pre-work budget exceeded 或 abort: TaskList unavailable(旧版) | 不可重试的编排器错误 | 停止运行,并报告中止信息及生成提示词的路径。重新运行相同提示词会再次失败——预工作预算耗尽始终意味着 Worker 未通过自检,重试也无法解决。 |
| 3 | 其他 worker-N abort: | 可重试 | 标记为 pending,设置 metadata.abort_reason、needs_respawn=true,并递增 attempt。 |
| 4 | Agent 报错,或没有 complete:/abort: 标记 | 可重试 | 与 #3 相同(Worker 暂时性崩溃)。 |
如果存在任何不可重试的情况,则停止。否则,重新生成 Worker 前,先清除磁盘上每个可重试 Worker 的前缀空间——Phase-7 索引基于磁盘构建,因此崩溃尝试留下的高 ID 残留文件(替代 Worker 不会重新生成的文件)否则会再次进入报告。遍历 Worker 的实际 pass_prefixes(取自其任务 metadata),将 ${pfx} 替换为各个实际前缀——不要使用字面值 PREFIX 运行命令:
# zsh-safe: `find … -delete` never aborts on no-match (an `rm PREFIX-*.md` glob would).
# Replace `PREFIX1 PREFIX2` with the worker's actual space-separated pass_prefixes.
for pfx in PREFIX1 PREFIX2; do
find "${output_dir}/findings" -maxdepth 1 -type f -name "${pfx}-*.md" -delete
done
然后,在一个并行块中重新生成每个 pending 且 attempt <= 2 的可重试 Worker(每个集群最多尝试 2 次)。首次失败时,attempt 刚递增至 2,因此判断条件必须允许 2,才能进行唯一一次重试——attempt < 2 会阻止所有重试。第二次失败会将 attempt 递增至 3,不满足 <= 2,重试也就此结束。替代 Worker 会为每个前缀复用确定性的发现项 ID,因此清空前缀空间后重新写入,即可得到一致的分片、覆盖范围和磁盘文件集。
完成状态健全性检查 + 写入索引
对于每个暂定的 complete: 集群,在将任务标记为已完成之前,根据 plan.json 验证工作线程所属的分片、覆盖文件、覆盖行、已归档 ID 和声称的发现数量。每个已完成的工作线程运行一条命令,也可以在一条命令中验证多个工作线程。以下两种声称数量的形式均有效;不要传入未分组在 --claimed-count 标志后、也未重复该标志的裸 worker-N=N 值。
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
--worker worker-N --claimed-count worker-N=<claimed_count_from_complete_line>
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
--worker worker-1 --worker worker-2 \
--claimed-count worker-1=0 worker-2=3
uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/validate_artifacts.py" "${output_dir}/plan.json" \
--worker worker-1 --worker worker-2 \
--claimed-count worker-1=0 --claimed-count worker-2=3
如果验证以非零状态退出,则将该完成状态视为格式错误且可重试(分类器行 #4):将任务标记为 pending,将验证器输出存储在 metadata.abort_reason 中,设置 needs_respawn=true,并递增 attempt。缺少 findings-index.d/worker-N.txt、缺少 coverage/worker-N.md、缺少覆盖行、无效的 skipped: 行、分片或磁盘上不存在的已归档 ID,以及声称数量不匹配,均属于格式错误的完成状态。超过重试上限后,保持集群任务未完成,并在 run-summary.md 和最终响应中呈现验证器输出。只有通过验证的暂定完成状态才可通过 TaskUpdate 标记为 completed。
然后构建索引。规范索引是磁盘上实际存在的发现文件集合,而非分片并集——从磁盘构建可确保:即使某个发现文件没有匹配的分片条目(工作线程在 Write 和追加分片之间崩溃,或工作线程提示词警告的单前缀空分片陷阱),它仍会进入去重 → fp-judge → REPORT/SARIF 流程,而不会悄然消失;并且每个索引条目都能对应到一个实际文件:
# Canonical index = every finding file on disk. `find` never fails on no-match
# (an empty findings/ yields an empty index — the unambiguous "zero findings"
# signal). `sort -u` collapses Phase-7 retry duplicates: replacement workers reuse
# deterministic ids, so the same path appears once.
find "${output_dir}/findings" -maxdepth 1 -type f -name '*.md' 2>/dev/null \
| sort -u > "${output_dir}/findings-index.txt"
# Reconcile against the per-worker shards: any path on disk but in NO shard is an
# orphan whose worker failed to record it. It is already in the index above (so it
# is NOT dropped) — print it so the bookkeeping gap can be surfaced. Non-fatal.
if [ -d "${output_dir}/findings-index.d" ]; then
# Reconcile by basename (finding ids are unique), so a path-format difference
# between the worker `find` and this one (trailing slash, /var↔/private/var)
# cannot manufacture false orphans. Any basename on disk but in no shard is an
# orphan whose worker failed to record it.
comm -13 \
<(find "${output_dir}/findings-index.d" -maxdepth 1 -type f -name 'worker-*.txt' -exec awk 1 {} + 2>/dev/null | sed 's#.*/##; /^[[:space:]]*$/d' | sort -u) \
<(find "${output_dir}/findings" -maxdepth 1 -type f -name '*.md' 2>/dev/null | sed 's#.*/##' | sort -u)
fi
分片仍是每个工作线程的审计记录(validate_artifacts.py 会检查分片),也是 dedup-judge 的崩溃恢复回退机制,但它们不再决定哪些内容能进入流水线。对于 reconcile 打印的每个孤立基本名,根据 plan.json 将其 <PREFIX> 映射到所属工作线程,并在 run-summary.md 中注明该工作线程的分片不完整——该发现已在索引中(因此不会丢失),但应明确记录这一账目缺口。仍需将索引行数与工作线程声称写入的 wrote N 数量总和交叉核对;记录不匹配情况,但不要中止。
完成任务更新并创建索引后,运行 TaskList,并写入 ${output_dir}/run-summary.md,内容包括:
- 已解析的参数(
threat_model、severity_filter、finding_scope_root、context_roots、能力标志has_unsafe/has_ffi/has_concurrency/has_async、Cargo 清单状态) - 工作线程结果表(
worker_n、cluster_id、声称的发现数量、分片行数、覆盖文件路径(coverage/worker-{N}.md)、任务状态、重试/中止状态) findings-index.txt行数,以及与工作线程声称数量的任何不匹配- Phase 8 完成后的裁判状态,或跳过/失败的原因
如果任何 Phase-5 集群任务未处于 completed 状态——或者任何工作线程返回的 complete: 行中带有 truncated at hard cap 标记(表示它在搜索完所有轮次前就达到工具调用上限;其覆盖文件中会有一行或多行 cleared (NOT SEARCHED — truncated at hard cap))——请在 run-summary.md 和最终响应中醒目说明。达到硬上限而被截断的工作线程,在账本中仍标记为 completed,但结果只是部分结果:不要让这个 completed 状态掩盖未完成的覆盖范围,即使报告看起来成功也不行。
即使发现数量为零,也始终运行 Phase 8——两个裁判都会在索引为空时提前结束:dedup-judge 会写入最简的无操作 dedup-summary.md,fp-judge 会写入空的 REPORT.md/REPORT.sarif,让 SARIF 使用者获得稳定的一组产物。
阶段 8:判定流程(顺序执行,去重 → 误报 + 严重性)
入口:findings-index.txt 已存在。**出口:**dedup-judge 和 fp-judge 均已返回;已写入 dedup-summary.md、fp-summary.md、REPORT.md,并且最好也写入 REPORT.sarif。
每个判定器的完整协议都在其系统提示词中(agents/rust-review-{dedup,fp}-judge.md);生成时的提示词只传递每次运行所需的变量。不要引用 prompts/internal/judges/——这些文件不存在。
**停止——这两个判定器必须按顺序运行,不能并行。**与阶段 6b 的工作器不同(你要在同一条消息中生成 M 次
Agent调用,正是因为这样会并发运行),这两个判定器之间存在严格的数据依赖:fp-judge 必须看到 dedup-judge 写入的merged_into/also_known_as注释,而且它只会跳过已带有merged_into的文件。如果你在同一条消息中发出两个Agent调用,它们会并发运行——fp-judge 会在任何合并注释写入之前读取 findings,将每个重复项都作为单独的主项进行判定,而且由于dedup-summary.md尚不存在,还会触发“去重未运行”回退逻辑,导致生成的REPORT.md/ SARIF 条目膨胀且重复。在单独一条助手消息中生成 dedup-judge,等待其返回
dedup-judge complete:(或abort:),然后再在另一条消息中生成 fp-judge。生成 fp-judge 前,请确认 dedup 已完成——Bash: test -f ${output_dir}/dedup-summary.md必须成功(或者你已看到 dedup 的complete:标记)。绝不要在同一条消息中放入两个判定器的Agent调用。
- 第一条消息——
Agent(subagent_type="rust-review:rust-review-dedup-judge", description="Dedup judge", prompt=f"output_dir: {output_dir}")。等待其返回,并在继续前对其分类(见下文)。 - 然后,在另一条消息中——
Agent(subagent_type="rust-review:rust-review-fp-judge", description="FP + severity judge", prompt=f"output_dir: {output_dir}\nsarif_generator_path: {sarif_generator_path}")——将sarif_generator_path解析为${RUST_REVIEW_PLUGIN_ROOT}/scripts/generate_sarif.py。
**判定器失败处理。**采用与阶段 7 相同的分类方式,应用于判定器的返回文本:
… complete:→ 成功。… abort:→ 该判定器不可重试。显示 abort 行和ls -l ${output_dir}/findings-index.txt的结果,然后仍然运行阶段 8b(其 SARIF +REPORT.md安全网可确保生成完整的产物集,即使判定器中止也不例外——参见阶段 8b 的“fp-judge 已返回,或运行提前中止”入口),并停止,不再生成其他判定器。“停止”是指不再继续判定流程,并不表示跳过阶段 8b。- 没有
complete:(帮助消息 / 错误 / 问题)→ **可重试一次。**使用SendMessage(to=<agentId>, …),而不是重新生成(该 Agent 已经付出了解析协议的成本)。请包含findings-index.txt中明确列出的 finding 路径。如果第二次尝试仍然失败,显示对话记录,并继续运行阶段 8b。
阶段 8b:报告安全网(SARIF + REPORT.md)
入口: fp-judge 已返回,或运行提前中止。出口: ${output_dir}/REPORT.sarif 和 ${output_dir}/REPORT.md 均已存在。
test -d "${output_dir}/findings" && uv run --no-project "${RUST_REVIEW_PLUGIN_ROOT}/scripts/generate_sarif.py" "${output_dir}"
只要 findings/ 存在,就无条件运行 SARIF 生成器——它具有幂等性(完全覆盖),对于没有存活项的运行会输出 results: [],并能处理部分运行(没有 fp_verdict 的 findings 会以 LIKELY_TP 输出,不受 severity_filter 限制,因为其严重级别从未经过 judge 验证;同时标记为 unjudged: true / severity_validated: false,并带有 [UNVALIDATED SEVERITY — not judged] 消息前缀——因此,推断出的严重级别猜测绝不会在 medium/high 过滤条件下被静默丢弃)。始终覆盖文件,可防止 fp-judge 在写入中途崩溃后将损坏的 REPORT.sarif 留在磁盘上。
如果生成器在 stdout 上打印 WARNING: skipped N … 行(它还会在 SARIF 中记录 invocations[].properties.skipped_findings,并为每个被丢弃的文件生成一条 warning 通知),则一个或多个 finding 文件不可读,或没有可解析的 frontmatter,因此被排除在报告之外。这是一个被丢弃的结果——应在 run-summary.md 和最终回复中醒目地说明其数量和路径,处理方式与非 completed 的集群任务相同。不要让其他方面正常的 SARIF 掩盖这项损失。
接下来,确保 REPORT.md 存在。与 SARIF(机械生成)不同,REPORT.md 是 fp-judge 整理后的产物,因此不要覆盖 judge 写入的文件。(fp-judge 使用 Bash heredoc 写入 REPORT.md,而不是 Write 工具,因为 harness 会阻止子 Agent 使用 Write 工具写入报告文件——不要通过重新强制要求使用 Write 来“修复”judge。编排器是主 Agent,不受此限制,因此它下面的 Write 可以正常工作。)检查文件是否存在;如果不存在(judge 崩溃,连其 Bash heredoc 写入也失败,或它在聊天文本中返回了报告,却没有写入文件),编排器应自行写入 REPORT.md,而不是让运行失败:
- 如果 fp-judge 在其 transcript 中返回了报告正文,则将该文本原样
Write到${output_dir}/REPORT.md。 - 否则根据磁盘上的 findings 生成报告:从
findings-index.txt中选取存活的主要 findings(fp_verdict ∈ {TRUE_POSITIVE, LIKELY_TP},且没有merged_into;如果 judge 从未运行,则将没有fp_verdict的 finding 视为存活项);仅对经过 judge 判定的存活项应用context.md中的severity_filter——未判定的 findings(没有fp_verdict)不受过滤条件限制,并归入Unvalidated (severity not judged)章节,标记为[UNVALIDATED SEVERITY — not judged],与 SARIF 的行为保持一致,避免严格过滤条件静默丢弃它们——然后写入一份遵循 fp-judge 模板的REPORT.md:YAML frontmatter(stage: final-report、threat_model、severity_filter、total_primaries、reported_findings)、严重级别分布表格,然后按严重级别分组,为每个报告的 finding 设置一个章节(CRITICAL/HIGH 内嵌 Description / Code / Data flow / Impact / Recommendation 正文;MEDIUM/LOW 引用 finding 文件)。
无论哪种情况,都要在 ${output_dir}/run-summary.md 中注明 REPORT.md 是由编排器合成的(并非由 judge 编写)。仅当 ${output_dir}/findings/ 不存在时(Phase 2 失败),才跳过 SARIF 生成器和此项检查。此阶段结束后,更新 ${output_dir}/run-summary.md,注明 judge / SARIF / 报告状态。
阶段 9:返回报告
进入条件: 阶段 8b 已完成。退出条件: 成功标准中的每一项均已验证为真;REPORT.md 已返回给调用方。
撰写回复前,请逐项检查下方的成功标准,并根据磁盘上的文件确认每一项(集群任务使用 TaskList,文件使用 ls/Read)。如果任何标准未通过,请在回复中明确指出失败——不要用成功报告掩盖未完成的运行。
然后 Read ${output_dir}/REPORT.md,并将其内容返回给调用方。附加一个 Artifacts 列表,指向 findings/、findings-index.txt、run-summary.md、dedup-summary.md、fp-summary.md、REPORT.md、REPORT.sarif。
Finding 文件的 Frontmatter——三个阶段
权威规范:agents/rust-review-worker.md(“Finding File Format”)。分三个阶段写入:
- Worker——基础字段(
id、bug_class、title、location、function、confidence、worker)+ 七个正文部分。 - Dedup-judge——对于重复项,添加
merged_into;对于吸收了其他项的主项,添加also_known_as+locations。 - FP+Severity judge——为每个主项添加
fp_verdict+fp_rationale;对于保留项(TRUE_POSITIVE/LIKELY_TP),还添加severity、attack_vector、exploitability、severity_rationale。
Bug 类别 / 集群
权威来源:prompts/clusters/manifest.json。37 个 bug 类别位于由 always 门控的集群中(因此集群始终运行);其中 35 个始终触发,另外 2 个——logic-correctness 中的 adversarial-trait(TRAITADV)和 closure-panic(CLOSUREPANIC)——还带有 requires: has_unsafe,因此仅在 has_unsafe=true 时触发。启用所有条件门控后,所有集群共包含 69 个 bug 类别。memory-safety 集群由 has_unsafe 门控(其中所有 bug 类别都要求存在 unsafe);PATHJOIN 和 TOCTOU 通过 input-os-safety 由 has_fs_io 门控,PACKEDREF 位于条件集群 layout-safety 中(has_packed_repr);PTREXPOSE 则通过 info-disclosure 集群保持始终启用。unsafe-boundary 和 concurrency-locking 已完全合并(运行时不会重新读取它们的子提示词)。
成功标准
阶段退出条件已涵盖其中大部分;编排器可见的最终状态如下:
- 阶段 5 的每个集群任务均为
completed(通过TaskList验证)。 ${output_dir}/run-summary.md存在,且记录了解析后的范围/上下文、Cargo 清单探测结果、worker 声明与索引计数、任务状态以及 judge/SARIF 状态。- 每个主项(不含
merged_into)都有fp_verdict+fp_rationale;每个保留项(TRUE_POSITIVE/LIKELY_TP)也都有severity、attack_vector、exploitability、severity_rationale。 REPORT.md存在,并按severity_filter进行严重性筛选(即使 fp-judge 未能写入该文件,阶段 8b 的安全网也能确保这一点)。REPORT.sarif存在(阶段 8b 的安全网也能确保这一点)。