mcpField
R
rust-review

rust-review

github.com/trailofbits安全7.9k2.1k 次安装今天更新
代码仓库

关于 rust-review

供 Claude Code 使用的 Rust 应用与库安全审查技能,通过多个阶段协调子 Agent 检查代码,并在去重和复核误报、严重度后生成 Markdown 与 SARIF 报告。

主要功能

  1. 01审查 Rust 内存安全与 unsafe 边界
  2. 02检查并发、FFI 和异步风险
  3. 03将代码簇分配给并行子 Agent
  4. 04对发现去重并评估误报
  5. 05生成 Markdown 与 SARIF 报告

适用场景

  1. 01审查库中的 unsafe Rust 代码
  2. 02评估 Rust 服务的 panic 拒绝服务风险
  3. 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)的插件目录。按以下顺序尝试,以第一个命中的结果为准:

  1. 原生 Claude Code — ${CLAUDE_PLUGIN_ROOT},如果 Bash: ls "${CLAUDE_PLUGIN_ROOT}/prompts/clusters/unsafe-boundary.md" 能解析到该文件,则接受。
  2. Codex — ${CODEX_PLUGIN_ROOT}(如果该变量存在且能解析到标记文件,则以相同方式设置)。
  3. 备用搜索 — 搜索 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_modelREMOTE / LOCAL_UNPRIVILEGED / BOTH类似“remote”“network”“attacker”的词 → REMOTE;“local”“unprivileged” → LOCAL_UNPRIVILEGED;否则询问。
worker_modelhaiku / sonnet / opus参数中明确给出的模型名称。否则询问(不得默默设置默认值)。
severity_filterall / 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](在其分配的波次中):

  1. Read: ${output_dir}/worker-prompts/worker-N.txt
  2. 将文件内容原样作为 Agent 工具的 prompt 参数传入:
参数值
subagent_typerust-review:rust-review-worker
model${worker_model} (haiku / sonnet / opus)
descriptionRust review worker N
promptworker-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 的返回文本,并按顺序应用以下分类器——匹配到的第一项优先:

#匹配(在返回文本中)结果操作
1worker-N complete:暂定成功解析 wrote N finding files 数量,然后在将 TaskUpdate 设为 completed 前运行下方的产物验证器。
2abort: spawn prompt malformed、abort: pre-work budget exceeded 或 abort: TaskList unavailable(旧版)不可重试的编排器错误停止运行,并报告中止信息及生成提示词的路径。重新运行相同提示词会再次失败——预工作预算耗尽始终意味着 Worker 未通过自检,重试也无法解决。
3其他 worker-N abort:可重试标记为 pending,设置 metadata.abort_reason、needs_respawn=true,并递增 attempt。
4Agent 报错,或没有 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 调用。

  1. 第一条消息——Agent(subagent_type="rust-review:rust-review-dedup-judge", description="Dedup judge", prompt=f"output_dir: {output_dir}")。等待其返回,并在继续前对其分类(见下文)。
  2. 然后,在另一条消息中——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”)。分三个阶段写入:

  1. Worker——基础字段(id、bug_class、title、location、function、confidence、worker)+ 七个正文部分。
  2. Dedup-judge——对于重复项,添加 merged_into;对于吸收了其他项的主项,添加 also_known_as + locations。
  3. 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 的安全网也能确保这一点)。

评论

支持 Markdown