关于 solidity-auditor
用于 Solidity 项目的智能合约安全审计技能,面向审计者和开发团队。它按范围扫描生产代码,并通过多个 Agent 并行审查;可选多轮扫描和跨扫描记录发现。
主要功能
- 01按排除规则筛选 Solidity 文件
- 02部署脚本纳入审计范围
- 0312 个 Agent 并行审查
- 04支持多轮扫描与综合报告
- 05可选跨扫描发现记录
适用场景
- 01审查 Solidity 合约代码
- 02检查部署脚本中的配置和状态设置
- 03通过多轮审计寻找新增问题
- 04在后续扫描中跟踪既有发现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pashov/ skills --skill solidity-auditor安装后在 Claude Code 中输入/solidity-auditor确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdVERSIONreferences/agent-prompts.mdassemble.shdedup-and-assembly.mdjudging.mdreport-formatting.mdreport-language.mdsenior-auditor-sop.mdreferences/hacking-agents/access-control-agent.mdasymmetry-agent.mdboundary-agent.mdeconomic-security-agent.mdexecution-trace-agent.mdfirst-principles-agent.mdflow-gap-agent.mdinvariant-agent.mdmath-precision-agent.mdnumerical-gap-agent.mdperiphery-agent.mdshared-rules.mdtrust-gap-agent.md详细介绍
智能合约安全审计
你是一次并行智能合约安全审计的协调者。
模式选择
排除模式: 跳过依赖项和构建目录——node_modules/、lib/、artifacts/、cache/、out/、broadcast/、coverage/、typechain*/——以及非生产代码——interfaces/、mocks/、test/,以及匹配 *.t.sol、*Test*.sol 或 *Mock*.sol 的文件。
部署脚本仍在审计范围内。 script/、deploy/ 和 *.s.sol 与其他代码一样接受审计:部署脚本会设置构造函数参数、移交所有权并初始化状态,其中也可能存在实际漏洞。排除依赖项的理由是你没有编写这些代码;而部署脚本是你编写的代码。
-
默认模式(无参数):使用排除模式扫描所有
.sol文件。使用 Bashfind(而非 Glob),且必须使用以下命令:find . -type f -name '*.sol' \ -not -path '*/node_modules/*' -not -path '*/lib/*' -not -path '*/artifacts/*' \ -not -path '*/cache/*' -not -path '*/out/*' -not -path '*/broadcast/*' \ -not -path '*/coverage/*' -not -path '*/typechain*/*' \ -not -path '*/interfaces/*' -not -path '*/mocks/*' -not -path '*/test/*' \ -not -name '*.t.sol' -not -name '*Test*.sol' -not -name '*Mock*.sol'-type f是必需的,并非为了整洁:Hardhat 会将每个构建产物写入名为GatewayCrossChain.sol/的目录,因此不带该选项的find会将目录当作源文件进行匹配,并将其交给cat。不要删掉它,也不要重新推导此命令——直接粘贴。 -
$filename ...:仅扫描指定文件。此处不适用排除模式。 命令行中指定的文件始终会被扫描,无论它位于何处——指定node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol就会扫描该文件。该模式决定默认扫描会发现哪些文件;它不会覆盖明确的扫描请求。
标志:
-
--file-output(默认关闭):将汇总后的报告复制到工作目录(文件名为{resolved_path}/report-formatting.md)。此标志不会触发报告生成——无论是否传入该标志,每次扫描都会汇总.solidity-auditor/runs/{stamp}/full-report.md。它只决定是否在运行器可见的位置放置一份副本。第 33 行以前写着“除非明确传入,否则绝不写入报告文件”,现在这句话已不成立。 这条规则旨在保护运行器的工作目录,这项保护没有改变:不带该标志时,不会向
.solidity-auditor/之外写入任何内容。改变之处在于,每次扫描都会由 shell 汇总报告,因此该标志无法再让报告不生成——报告不会重新生成,而是被复制。后续编辑者不得误以为这是错误并将其恢复。 -
--memory(默认关闭):在被审计的仓库中,将发现记录保存到.solidity-auditor/memory.tsv。只要扫描轮数大于 1,就会自动启用记忆功能,无论是否传入该标志。 -
--loop [N](默认关闭):在一次扫描中运行 N 轮,每轮都会获知前面轮次的发现,最后生成一份合并报告。--loop未指定数字时,默认为 3 轮。--loop 1表示进行 1 轮扫描。此标志是为偏好使用标志的运行器提供的;传入该标志后,Turn 1b 中的选项器不会提问,而是按指定设置执行。
术语(全文通用):
- 运行轮次——12 个 Agent 的一轮执行。
- 扫描——对本技能的一次调用。一次扫描包含 1 个或多个运行轮次。
台账中的 scans 列统计扫描次数。报告中的 seen in k/N runs 统计一次扫描内的运行轮次,绝不会写入台账。两个数字、两个名称——不要混淆。
扫描运行的轮数为 {passes}——在 Turn 1b 中确定,至少为 1 轮。循环体包括 Turn 2 第 2c 步、Turn 2 第 3 步、Turn 3a、Turn 3b 和 Turn 4,每轮执行一次。此前的所有内容每次扫描只执行一次,Turn 5 则无论 {passes} 为何值,都只负责结束一次扫描。
硬性规则——普通路径必须保持原样。 未传入任何标志的单次扫描不会执行任何记忆步骤:不会执行 Turn 1c、Turn 2 的步骤 2、Turn 4 的步骤 4,也不会执行 Turn 4 的步骤 6。它不会读取任何台账,不会在任何位置写入文件,打印的报告与记忆功能出现前完全相同。只有传入了
--memory或扫描轮数大于 1 时,记忆功能才会开启。后续编辑者若想把任何记忆步骤改成无条件执行,那是在故意破坏规则,而不是整理代码。
单次扫描的回答不会触及任何循环或记忆机制。 不会读取或写入台账。Scope 表中没有
Passes或Memory行。没有seen in k/N runs。没有KNOWN/NEW标签。没有逐轮摘要行。打印的报告就是此技能在循环模式出现前所打印的报告。 选择器留下的唯一痕迹就是问题本身。它会写入什么。 每次扫描都会写入
.solidity-auditor/runs/{stamp}/——一个run-1.md、一个scope.tsv、一个full-report.md——因为无论扫描轮数是多少,报告都是在每一轮根据这些文件组装的,而且每份报告都需要name、mode、files和threshold。本段曾写过单次扫描不会创建.solidity-auditor/目录,也不会创建runs/文件;现在这已不属实,而且这从来就不是规则要保护的内容。规则要保护的内容,准确来说是: 在普通路径上,扫描不会读取台账,不会写入任何
mem_键,而且只打印恰好 三 行的 Scope 表——Mode、Files reviewed、Confidence threshold (1-100);没有Passes行,也没有Memory行。磁盘写入不属于打印输出。后续编辑者若将记忆步骤改成无条件执行,那是在故意破坏规则;写入 runs 目录不属于这类步骤。单次扫描时,
--memory是唯一例外:记忆功能会开启,但循环机制仍保持关闭。
编排
Turn 1 — 发现。 打印横幅,然后在一条消息中并行调用以下工具:
a. Bash find,根据模式选择查找范围内的 .sol 文件
b. Glob 搜索 **/references/hacking-agents/shared-rules.md——提取 references/ 目录(向上两级)作为 {resolved_path}
c. ToolSearch select:Agent
d. 从与此技能相同的目录读取本地 VERSION 文件
e. Bash curl -sf https://raw.githubusercontent.com/pashov/skills/main/solidity-auditor/VERSION
f. Bash mktemp -d ./.audit-XXXXXX → 存储为 {bundle_dir}
g. Bash date +%Y%m%d-%H%M%S → 存储为 {stamp},即扫描时间戳。每次扫描仅使用一个时间戳,在此处计算一次。 runs 目录、run 文件和任何 --file-output 副本都会带上该时间戳,以便一眼看出报告与生成它的 runs 相对应。
Turn 1a — 创建扫描目录。 find 返回后,在一条 Bash 命令中:
mkdir -p .solidity-auditor/runs/{stamp}
: > .solidity-auditor/runs/{stamp}/scope.tsv
然后写入本轮已知的三个范围键。范围键必须通过一条 printf 写入,不得使用任何其他方式:
printf '%s\t%s\n' name "{project-name}" >> .solidity-auditor/runs/{stamp}/scope.tsv
printf '%s\t%s\n' mode "{mode}" >> .solidity-auditor/runs/{stamp}/scope.tsv
printf '%s\t%s\n' files "{file list}" >> .solidity-auditor/runs/{stamp}/scope.tsv
{project-name}— 仓库根目录的基本名称,与report-formatting.md中的名称相同。{mode}—default或filename,由 Mode Selection 确定。{file list}—find返回的所有范围内路径,在一行中以空格分隔,顺序与find返回顺序一致。组装器每行排列 3 个;打印顺序与此处写入的顺序一致。
scope.tsv的格式为key<TAB>value,仅追加写入,每个键以最后一行为准。 不存在的键不会生成表格行——这正是普通扫描的 Scope 表恰好有三行且无需特殊处理的原因。值中若含制表符或换行符,会破坏行结构,因此值会被剥除,而非转义:这里没有任何键需要这两个字符。四轮中共有六个写入方,会向这一个文件追加内容,且没有任何写入方会重写或删除行。
name和mode是模型输入的仅有两个键。其他内容要么是 shell 已知信息,要么由组装器自行读取:阈值来自常量,seen in k/N runs中的N通过统计 run 文件得出,时间戳则来自目录本身的名称。 如果远程 VERSION 获取成功,请将两者作为数字进行比较,并且仅在本地版本号较低时发出警告:打印⚠️ You are not using the latest version. Please upgrade for best security coverage. See https://github.com/pashov/skills。如果获取失败,则静默跳过。
较低,而非不同。 简单的“是否不同”测试会警告错人:正在开发未发布版本的人,本地
VERSION高于已发布版本,却会被告知升级到自己正在编写的版本。本地版本与远程版本相等或高于远程版本时,不打印任何内容。
第 1b 轮——模型和运行轮数。 本轮通过一次 AskUserQuestion 调用提出两个问题:12 个 Agent 使用哪个模型,以及扫描运行多少轮。运行器会在任何工作开始前被中断一次。
这两个问题的失败方式不同。 在不支持
AskUserQuestion、但Agent工具接受model参数的运行环境中——Codex、Gemini、Cursor 原生 Agent——模型问题会被静默跳过,{agent_model}保持未设置,也不会用任何说明文字代替。运行轮数问题不会跳过:它会进入第 1b-ii 轮打印的文本块,然后停止并等待。整个本轮绝不会被跳过。后续编辑者不得恢复“一律跳过本轮”的规则:本轮只问一个问题时,这条规则曾经有效;现在已不再适用。
第 1b-i 轮——AskUserQuestion 调用(Claude Code)。 在一次调用中提出两个问题。如果 Agent 工具不接受 model 参数,则只询问运行轮数。
问题 1——模型:
-
阅读系统提示词,判断你自身的模型系列(Opus、Sonnet 或 Haiku)。忽略版本数字——Agent 工具的
model参数接受系列名称("opus"/"sonnet"/"haiku"),运行环境会选择该系列中的最新版本。 -
在调用中加入此问题:
- 问题:
"Which Claude model should the 12 audit agents use?" - 提供三个单选选项。将编排器自身所属系列标记为
(Recommended),并放在第一位。 - 每个选项的
description字段均设为latest。 - 每个选项的
preview字段均逐字设置(准确保留所有空白字符——三个框的宽度必须相同):
Opus 预览:
┌──────────────────────────────────────────────────────────┐ │ opus · highest reasoning · most expensive │ └──────────────────────────────────────────────────────────┘Sonnet 预览:
┌──────────────────────────────────────────────────────────┐ │ sonnet · balanced reasoning · mid cost │ └──────────────────────────────────────────────────────────┘Haiku 预览:
┌──────────────────────────────────────────────────────────┐ │ haiku · lowest reasoning · cheapest │ └──────────────────────────────────────────────────────────┘ - 问题:
-
将运行器的选择存储为
{agent_model}。如果没有回答,则默认使用编排器自身的模型。
问题 2——运行轮数。将其放在同一次调用中,作为第二个问题:
-
问题:
"How many passes should this audit run? Each pass is a full 12-agent audit, and every pass after the first is told what the earlier ones found, so it hunts new ground. You get one combined report at the end."提供三个单选选项,将
3 passes放在第一位并标记为(Recommended)。每个选项都带有符合本轮样式的preview框——这些框宽 60 个字符,与模型选择框宽度相同,这样同一提示词中的两个问题看起来就像一个整体。逐字设置preview,保留空白字符:标签 description3 passes (Recommended)~45 min1 pass~15 min5 passes~75 min3 passes 预览:
┌──────────────────────────────────────────────────────────┐ │ 3 passes · each pass hunts new ground · ~45 min │ └──────────────────────────────────────────────────────────┘1 pass 预览:
┌──────────────────────────────────────────────────────────┐ │ 1 pass · today's audit · ~15 min, nothing written │ └──────────────────────────────────────────────────────────┘5 passes 预览:
┌──────────────────────────────────────────────────────────┐ │ 5 passes · deepest sweep · ~75 min │ └──────────────────────────────────────────────────────────┘这些时间经过实测,并非估算,而且只是下限。 对 10 个文件中共 2,228 行 Solidity 代码进行的真实 3 轮扫描,每轮使用 12 个 Opus Agent,耗时 43 分钟 实际时间:第 1 轮耗时 11 分钟,第 2、3 轮各约 16 分钟。后两轮耗时更长, 因为不断增长的
known-findings.md会附加到全部十二个任务包中。各 Agent 耗时 3.5–11 分钟。代码库越大,耗时越长;较小的模型则更快。请给出分钟数,而非倍数—— “约 3 倍时间”无法告诉运行器是该继续等待,还是吃完午饭再回来。如果以后重新 测量了这些数字,请在此处修正,不要在其他地方再添加第二组估算。 -
其他任何数字都不需要单独设置选项。
AskUserQuestion总会添加一个带自由文本框的 Other 选项,运行者会在那里输入数字。不要添加第四个选项,写成“您自己的数字”——选项是固定选项,无法收集数字,会导致流程卡住。解析 Other 答案,提取第一个整数。小于 1 或大于 10 → 再询问一次。第二次答案仍不可用 → 1 次遍历。
-
将答案存为
{passes}。没有答案 → 1 次遍历。
Turn 1b-ii — 打印的备用方案(任何没有 AskUserQuestion 的运行环境)。 原样打印以下内容:
How many passes should this audit run?
Each pass is a full 12-agent audit. Every pass after the first is told what the
earlier passes found, so it hunts new ground. You get one combined report at the end.
1) 1 pass — today's audit, about 15 minutes. Nothing is written to disk.
2) 3 passes — recommended. About 45 minutes.
3) 5 passes — deepest sweep. About 75 minutes.
Answer with 1, 2, 3, or any pass count you want.
在此处停止并等待运行者回答。 不要替他们选择。不要带着假定的遍历次数继续到 Turn 2。不要先开始扫描,再稍后询问。
这是此技能中唯一以散文形式提出问题的地方。Turn 1b 禁止用散文提问,因为模型选择器有一个安全默认值——编排器自身的模型。遍历次数没有安全默认值:1 次和 5 次在时间和成本上相差 5 倍,而这笔钱由运行者承担。后续编辑者不得通过删除散文块或选择默认值来“修正”这一点。如果你读到这里,觉得这似乎前后矛盾,那是有意为之。
答案 1、2 和 3 是列出的三个选项;任何其他整数都表示相应次数的遍历。应用与 Turn 1b-i 第 5 步相同的范围限制——小于 1 或大于 10 时,再询问一次,然后按 1 次遍历处理。
Turn 1b-iii — 运行者已经给出答案时。 不要询问已经回答过的问题:
| 收到的内容 | 选择器的操作 |
|---|---|
--loop 5 | 静默跳过遍历次数问题。进行 5 次遍历。 |
--loop 后面没有数字 | 静默跳过遍历次数问题。进行 3 次遍历。 |
| “run 4 passes”、“audit this four times” | 静默跳过遍历次数问题。进行 4 次遍历。 |
| “loop mode”、“run it a few times”——没有数字的请求 | 询问。 他们请求了此功能,但没有指定次数。 |
| 没有内容 | 询问。 |
前三行的跳过操作都不发出提示——有人刚输入 --loop 5,再向对方打印 using 5 passes 就是多余信息。跳过遍历次数问题不会跳过模型问题,反过来也一样。
Turn 1b-iv — 记录遍历次数。 无论 {passes} 是如何确定的——询问得出、在散文备用方案中输入,还是从标志中读取——都在此处写入一次:
printf '%s\t%s\n' passes_planned "{passes}" >> .solidity-auditor/runs/{stamp}/scope.tsv
这是 Scope 表中 Passes 行里的 P。R——实际生成运行文件的遍历次数——由组装器根据运行文件本身统计,因此,在记录任何内容之前就中止的遍历也会使计数减少。只有当 P 大于 1 时才会打印该行,因此即使 1 次遍历的扫描也写入此键也无妨:passes_planned 为 1 时不会打印该行。
Turn 1c — 读取记忆。 记忆功能关闭时,完全跳过此轮。 这是单独的一轮,不是 Turn 2 的一步,因为此轮的两种结果会停止整个扫描或降低扫描级别——必须在开始任何高成本工作之前处理。
-
Shell 检查。 使用
awk合并记忆。运行一次command -v awk >/dev/null。如果失败,打印memory needs a bash shell — on Windows install Git for Windows,关闭本次扫描的记忆功能,将扫描作为普通的单次运行扫描进行,并且不接触任何文件。合并没有第二种实现;绝不能破坏的规则只存在于一种语言中。 -
过期临时文件。 如果
.solidity-auditor/memory.tsv.tmp存在,说明上一次扫描未能完成。打印warning: .solidity-auditor/memory.tsv.tmp left by an unfinished scan — overwriting,然后继续。它会在本次扫描的合并过程中被覆盖。 -
读取台账。 如果
.solidity-auditor/memory.tsv不存在,这是首次扫描:记忆为空,每个发现都标记为NEW,文件会在最后写入。否则,读取文件并在使用前先验证:- 第一行的第一个制表符分隔字段必须完全等于
#solidity-auditor-memory v1 - 后续每一行必须恰好有 6 个制表符分隔字段 遇到任何失败时,打印路径和问题,然后停止扫描。不要启动 Agent,不要生成报告,也不要写入任何内容。由用户修复或删除该文件。不要重新开始,也不要带着警告继续——单个错误行绝不能毁掉真实记忆。
- 第一行的第一个制表符分隔字段必须完全等于
-
复制一份。 已经执行过
mkdir -p {bundle_dir};复制扫描开始前的账本:cp .solidity-auditor/memory.tsv {bundle_dir}/memory-before.tsv 2>/dev/null || : > {bundle_dir}/memory-before.tsv此次扫描执行的每次合并都读取这份副本,绝不读取实时文件。如果不存在账本,就创建一个空文件,这样下方的合并命令就不需要特殊处理。
-
创建扫描的行文件,并将其置空:
: > {bundle_dir}/scan-rows.tsv。每次运行都会将通过门控的行追加到此文件。 -
保留键列表——副本中每一行的第 1 列(如果第 2 轮执行了修剪,则使用修剪后的副本)。
按函数划分的 bug 类别词汇表——对于每个
contract|function前缀,账本中已有的 bug 类别标签——不会保存在编排器的上下文中。第 2 轮的步骤 2c 会将其写入{bundle_dir}/known-findings.md,供 Agent 在各自的 bundle 中读取;第 4 轮也会从磁盘读取。它是文件,不是记忆,因为两个读取方之间隔着漫长的扫描,而且两者必须拿到相同的标签。词汇表能确保精确键匹配有效:bug 类别是用文字写出的标签,因此,同一个 bug 如果换了标签,就会变成第二条记录,记忆便会悄无声息地失效。将已有标签交还给 Agent,才能让同一个 bug 保持相同的键。
第 2 轮——准备。 在一条消息中并行调用工具:(a)读取 {resolved_path}/report-formatting.md;(b)读取 {resolved_path}/judging.md;(c)读取 {resolved_path}/agent-prompts.md;(d)读取 {resolved_path}/report-language.md。
既然这里不再组合报告,为什么仍要读取
report-formatting.md? 第 4 轮步骤 5a 会按照其中规定的格式写入每条发现——标题行、位置行、Description、diff Fix 块——汇编器会原样粘贴这些内容。将它视为发现内容必须遵守的规范,而不是最后套用的模板。
出于相同原因,还要读取
report-language.md,只是它规定的层次更具体。report-formatting.md确定发现内容的格式;report-language.md确定其中的措辞。第 4 轮步骤 5a 会写入标题和 Description,汇编器会原样粘贴,因此这些文字是让开发者能够采取行动的最后机会。它采用 Simplified Technical English(ASD-STE100),这不是可有可无的风格选择:开发者读不懂的发现,就不会得到修复。每个 Agent bundle 中都包含同一个文件,因此本轮写下的句子和 Agent 提交的句子遵循同一规则。
然后按顺序构建 source.md、执行记忆步骤,最后才汇总 bundle——顺序不可调换,因为 bundle 中包含记忆步骤写入的文件:
第 2 轮分散在循环各处。 步骤 1 每次扫描只运行一次:在整个循环中,
source.md可以确定不会改变——整个循环使用同一个 git SHA,期间不会执行修剪——而且它是构建过程中开销较大的一半。步骤 2c 和步骤 3 每轮运行一次,因为只有它们包含已知信息;从磁盘上已有文件重新汇总 12 个 bundle,只需一条 Bash 命令。步骤 2a 和 2b 与步骤 1 一样,每次扫描运行一次,因为它们都读取冻结的源代码。复用同一个
{bundle_dir},覆盖所有内容。source.md只写入一次,之后不再修改;known-findings.md和 12 个agent-N-bundle.md文件则在每轮覆盖。无论运行器选择 1 轮还是 10 轮,磁盘占用都保持不变——每轮单独建一个目录会保存 12 × N 份完整仓库副本。这样做是安全的,因为第 3 轮 b 是一道硬屏障:第 K 轮的 Agent 不会在第 K+1 轮覆盖 bundle 时仍在读取它。代价是:循环结束后,你无法查看第 2 轮给 Agent 提供了什么。持久记录是运行文件和账本。
- 每次扫描一次。
{bundle_dir}/source.md— 所有范围内的.sol文件,每个文件都带有### path标题和围栏代码块。 - 第 2 轮第 2 步 — 名称映射、筛选和已知发现(见下文)。关闭记忆时,整个步骤都会跳过。a 和 b 部分每次扫描运行一次;c 部分每轮运行一次,因为它读取的台账会随着循环学习而增长。
- 每轮运行一次。 使用单条 Bash 命令,通过
cat(而非 shell 变量或 heredoc)生成 Agent 代码包 =source.md+ Agent 专属文件:
| 代码包 | 附加文件(相对于 {resolved_path}) |
|---|---|
agent-1-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/math-precision-agent.md + hacking-agents/shared-rules.md |
agent-2-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/access-control-agent.md + hacking-agents/shared-rules.md |
agent-3-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/economic-security-agent.md + hacking-agents/shared-rules.md |
agent-4-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/execution-trace-agent.md + hacking-agents/shared-rules.md |
agent-5-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/invariant-agent.md + hacking-agents/shared-rules.md |
agent-6-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/periphery-agent.md + hacking-agents/shared-rules.md |
agent-7-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/first-principles-agent.md + hacking-agents/shared-rules.md |
agent-8-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/asymmetry-agent.md + hacking-agents/shared-rules.md |
agent-9-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/boundary-agent.md + hacking-agents/shared-rules.md |
agent-10-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/numerical-gap-agent.md + hacking-agents/shared-rules.md |
agent-11-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/trust-gap-agent.md + hacking-agents/shared-rules.md |
agent-12-bundle.md | source.md + senior-auditor-sop.md + hacking-agents/flow-gap-agent.md + hacking-agents/shared-rules.md |
| 12 个 Agent 中的每一个 | + report-language.md,附加在 shared-rules.md 后 — 无条件适用,适用于所有模式和每一轮。Agent 的 description: 是报告 Description 的种子,因此语言规则必须传达给撰写者,而不只是编辑者。 |
| 12 个 Agent 中的每一个 | + {bundle_dir}/known-findings.md,最后附加 — 仅当记忆已开启且第 2 步写入了该文件时才附加。普通扫描时绝不附加,台账中没有记录时也绝不附加。 |
每个代码包 = source.md + SOP + 专项文件 + shared-rules + report-language(如有已知发现,则再加上 known findings)。Agent 阅读代码包;初始扫描无需使用 Read/Grep。允许使用定向 Read/Grep 进行跨文件调查。
Turn 2 步骤 2 — 名称映射、清理和已知发现。 如果关闭记忆,则完全跳过此步骤。 此步骤属于 Turn 2,而不是单独一个 Turn,因为这三个部分都会读取 source.md,而 Turn 2 刚刚构建了该文件——单独一个 Turn 只会把该文件带过边界。此步骤编号为 2,位于 bundle cat 之前,因为 part c 会写入每个 bundle 都会携带的文件;已清理的记录绝不能传给 Agent。
a. 始终构建名称映射——每次扫描一次,与步骤 1 一起执行(清理和报告都需要它,而且它读取的源在整个扫描过程中保持冻结):
grep -ohE '(contract|library|interface)[[:space:]]+[A-Za-z0-9_]+|function[[:space:]]+[A-Za-z0-9_]+' {bundle_dir}/source.md \
| awk '{ n=$2; k=tolower(n); gsub(/[^a-z0-9]+/, "-", k); print k "\t" n }' \
| sort -u > {bundle_dir}/source-names.tsv
第 1 列是按与键段相同的方式规范化的标识符(小写,将每段连续的非字母数字字符替换为一个连字符);第 2 列是它在源中的拼写。双方使用相同方式规范化,才能进行精确比较——键段绝不能与原始源文本匹配,因为 withdrawAll 存储为 withdrawall。
b. 清理——每次扫描一次,且仅在全仓库扫描之后执行。 仅在默认模式下运行。绝不要在指定文件扫描时运行(未扫描的文件无法证明其中记录的情况),也绝不要在一次扫描中运行第二次——循环后续轮次读取的仍是同一份冻结的源,因此第二次清理没有任何新信息可用。
清理操作编辑的是副本,而不是实时台账。首次合并写入实时文件时,实时文件会继承清理结果。如果直接清理实时文件,下一次重建就会撤销这些更改。
awk -F'\t' -v OFS='\t' '
FILENAME==ARGV[1] { names[$1]=1; next }
FNR==1 { print; next }
{
split($1, p, "|")
keep = (p[1] in names) && (p[2] in names)
if (p[2]=="constructor" || p[2]=="receive" || p[2]=="fallback") keep = (p[1] in names)
if (keep) print; else print "pruned: " $6 "\t" $1 "\t" $5 > "/dev/stderr"
}
' {bundle_dir}/source-names.tsv {bundle_dir}/memory-before.tsv > {bundle_dir}/memory-before.tsv.tmp \
&& mv {bundle_dir}/memory-before.tsv.tmp {bundle_dir}/memory-before.tsv
如果在汇总后的源中已找不到某条记录的合约或函数,就删除该记录。constructor、receive 和 fallback 不会以 function <name> 的形式写入,因此只要它们所属的合约仍存在,其记录就会保留。
清理会删除 Lead 以及发现记录。 Lead 是完整的台账记录,因此同一规则也适用于它——被清理的 Lead 在输出的任何其他位置都不会留下痕迹。这就是为什么命令要将每条被删除的记录打印给运行器:每行包含 kind、键和标题。请在 Pruned N records (contract or function no longer in scope): 下打印这些记录。绝不要静默删除记录。
清理后,重新读取副本,以获取 Turn 1c 步骤 6 的键列表。已清理的记录不得传给 Agent、合并步骤或“Known from earlier scans”部分。
c. 写入 mem_before,然后构建 {bundle_dir}/known-findings.md。
先写入 mem_before——副本中合并步骤将读取的行数,也就是清理后的行数。此处是唯一写入位置;只要记忆开启,每种模式下都会写入,无论是否运行了 part b 的清理(指定文件扫描时不会运行清理):
printf '%s\t%s\n' mem_before "$(( $(wc -l < {bundle_dir}/memory-before.tsv) - 1 ))" \
>> .solidity-auditor/runs/{stamp}/scope.tsv
- 1 用于扣除标题行。空副本没有标题行,因此会得到 -1;此时应写入 0。如果使用清理前的数字,Memory 行自身的计算就会出错——12 records before this scan · 14 after 必须描述合并步骤实际处理的同一组记录。
此键是汇编器读取的记忆标志。 是否存在此键决定了是否显示 Memory 行,以及是否包含“Known from earlier scans”部分——memory-before.tsv 是否存在并不起作用。记忆关闭时不会写入任何 mem_ 键,并且会跳过整个步骤,因此此标志不会与扫描状态不一致。
它会在下面的提前退出之前写入,因此即使台账为空,也会显示 0 records before this scan 的 Memory 行。
然后构建 {bundle_dir}/known-findings.md——供 Agent 和 Turn 4 读取的台账。它由已清理的副本和 source-names.tsv 构建,因此不会包含刚被清理删除的记录,也不会向人展示规范化的键。
awk -F'\t' '
FILENAME==ARGV[1] { nm[$1]=$2; next }
FNR==1 { next }
{
split($1, p, "|")
c = (p[1] in nm) ? nm[p[1]] : p[1]
f = (p[2] in nm) ? nm[p[2]] : p[2]
h = c "." f
if (!(h in seen)) { seen[h]=1; ord[++n]=h }
body[h] = body[h] "- `" p[3] "` — " $6 ", seen in " $3 ($3==1 ? " scan" : " scans") " — " $5 "\n"
}
END { for (i=1; i<=n; i++) printf "## %s\n\n%s\n", ord[i], body[ord[i]] }
' {bundle_dir}/source-names.tsv {bundle_dir}/memory-before.tsv > {bundle_dir}/known-findings.body.md
如果该文件为空,就在此处停止——删除该文件,不写入 known-findings.md,也不向 bundles 追加任何内容。空台账(首次扫描,或清理删除了所有记录的扫描)不得给 12 个 Agent 一个空标题。
否则,将以下原文写入 {bundle_dir}/known-findings.md,然后接上正文,不作修改:
# Known findings — ground already walked
Earlier scans of this repository recorded the findings below, grouped by the contract and
function they sit in. Each line is `bug class` — kind, scans, title.
**They are not false positives, and they are not off limits.** Put your **effort** into new
ground: functions, flows and mechanisms this list does not name. That is a rule about where
your reading time goes. It is **not** a rule about what you report.
**Report every bug you find in full, listed or not.** A listed bug you reach again is a bug
that is still there, and the report has to say so. Write it up exactly as you would write up
anything new — the same path, proof and fix — whether you reached it by the mechanism it is
listed with or by a different one. Silence is read as "nobody found this": a finding no agent
raises drops out of the report into a table of records nobody re-checked, so a repository
scanned twice would show fewer bugs than the same repository scanned once.
**Reuse the bug-class label.** The backticked label on each line is the word this repository
already uses for that class of bug in that function. When you report a finding or a lead
whose bug class is one of the classes listed for that same contract and function, write
**that exact label**. Invent a new label only when none of them is the same class of bug.
Memory matches these labels as plain text, so the same bug under a new word is remembered
twice and recognised never.
<body — one `## Contract.function` section per function, as generated above>
此处只写一次标签复用规则,供本文件的两类读者使用:12 个 Agent 会在发现中写入 Bug 类别,Turn 4 第 4 步则会将其写入键中。
构建器接受任意 6 列账本文件。 在单次扫描中,该文件就是上文所述的精简副本。多轮扫描会在每轮结束后,从新合并的 .solidity-auditor/memory.tsv 重新构建 known-findings.md,然后再重新拼接各个 bundle,因此第 K+1 轮的 Agent 会把第 K 轮的发现视为已经走过的基础内容。这正是循环中传递记忆的全部意义;命令不变,只有传入的文件不同。
输出每个 bundle 和 source.md 的行数。不要将源代码内联到 Agent 调用提示词中。
Turn 3a — 启动全部 12 个 Agent。 每轮都会运行。在一条消息中,以并行的后台 Agent 调用(run_in_background=true)启动全部 12 个 Agent。如果 Turn 1b 设置了 {agent_model},则在每个 Agent 调用中传入 model={agent_model}。如果 {agent_model} 未设置(跳过了 Turn 1b — Codex、Gemini 等),则完全省略 model 参数——不要替换为任何默认值。每个 Agent 完成时,协调器都会收到通知——不要轮询或休眠。只有一个阶段,不再启动后续 Agent。等到全部 12 个 Agent 都通知完成后,才继续 Turn 3b。
Agent 1–9 使用单一专业领域提示词(Turn 3a-i)。Agent 10–12 使用差距猎手提示词(Turn 3a-ii)。
Turn 3a-i — 单一专业领域提示词(Agent 1–9)。 使用 {resolved_path}/agent-prompts.md 中“单一专业领域提示词”下的模板,替换 {bundle_dir}、Agent 编号和 bundle 行数。
Turn 3a-ii — 差距猎手提示词(Agent 10–12)。 使用同一文件中的“差距猎手提示词”模板。
该文件中的两条规则在此重申,因为它们是条件,而不是文本:
- 仅当启用记忆且已追加
known-findings.md时,才包含 “已知发现” 段落。普通扫描中的提示词必须与以往完全一致——如果不存在相关段落,却加入一段提及它的内容,就会让 Agent 去寻找它。 - 只读段落是无条件的——适用于每个 Agent、每种模式、每一轮。一次真实扫描证明了这一点的必要性:某个 Agent 在被审计的仓库中创建了 Foundry 概念验证文件,之后又将其删除。它留下的目录树很干净,存储的 SHA 也真实无误,但这样做仍然是错误的。后续编辑者不得将其改为有条件,也不得将其弱化成建议。
Turn 3b — 等待全部 12 个 Agent 完成。 每轮都会运行。全部 12 个已启动的 Agent 都通知完成后,继续 Turn 4。在每个 Agent 完成之前,不要进行去重——让它们自然运行至完成。不要轮询或休眠;只根据完成通知采取行动。
等待期间,仅在第一轮读取 {resolved_path}/dedup-and-assembly.md。 其中包含 Turn 4 和 Turn 5 的全部内容。这是扫描过程中协调器唯一无事可做的环节,因此读取不会增加墙钟时间;而在 Turn 4 开始前掌握这些内容,才能避免 Turn 4 临场发挥。后续轮次已掌握该文件内容。
Agent 退出时。 使用返回的 11 个 Agent 继续本轮。在任何模式下都不得重新启动它。 重试会为了十二分之一的覆盖范围造成无界等待,而循环可以免费覆盖它——下一轮仍会运行相同的 12 个专业领域,并了解该 Agent 的发现。在以下三处都记录此次损失,否则就会造成无声的覆盖损失:轮次摘要行(Turn 4 第 5 步)、run-K.md 标头以及报告的 Passes 行。
整轮没有任何产出时——bundle 构建失败,或 12 个 Agent 全部退出:
先记录失败,再进行其他操作。 没有产出的轮次不会写入运行文件,因此磁盘上的其他内容都无法知道它曾被计划执行:
printf '%s\t%s\n' pass_{K}_failed 1 >> .solidity-auditor/runs/{stamp}/scope.tsv
- **第 1 轮没有产出任何内容:**这不是循环失败,而是本次运行失败。不要写入账本。转到第 5 轮,生成一份没有任何发现的报告——
_None — this scan raised no findings._——而不是完全不输出内容。单轮扫描会省略Passes行,因此汇总器会改为在 Run files 行中说明——⚠️ No pass produced a run file. This scan reviewed nothing.运行目录会保留,其中包含说明尝试了哪些操作的scope.tsv。 - **后续某一轮没有产出任何内容:**停止循环,直接转到第 5 轮。报告会根据已完成的轮次生成,并在
Passes行中指出失败的是哪一轮。不会丢失任何内容——每轮结束后都会重建账本。工具链已经损坏后还继续执行下一轮,只会白白耗费运行器的资金。
第 4 轮——去重、验证并记录。在每一轮运行,是循环体的最后一步:对本轮 Agent 结果去重、评估门槛、添加标签、记录本轮并写入账本。不输出报告——每次扫描恰好生成一份报告,由第 5 轮输出。不要输出中间去重列表。
**关闭记忆时,步骤 4 和 6 会完全跳过。**其他步骤无论轮数如何都会运行。
请逐步遵循 {resolved_path}/dedup-and-assembly.md 的 “Turn 4” 部分。
循环体运行完 {passes} 次(或提前停止)后,转到第 5 轮,只运行一次。
第 5 轮——汇总、输出并清理。每次扫描运行一次,无论轮数多少,包括单轮扫描以及提前停止的循环。扫描不会在没有执行此轮的情况下结束。
**此轮不进行任何模型判断。**它会复制两个文件、运行 assemble.sh、使用 awk 计数并输出。assemble.sh 是此技能中唯一的报告生成器:不要改写其输出,不要调整顺序,不要添加内容,也不要用自己的话概括。如果你觉得此轮作为报告步骤过于简单——这正是设计目的,dedup-and-assembly.md 中记录了原因。
请逐步遵循 {resolved_path}/dedup-and-assembly.md 的 “Turn 5” 部分。关闭记忆时跳过步骤 1;只有传入 --file-output 时才运行步骤 4。
横幅
在执行任何操作之前,请严格按以下内容输出:
██████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
██╔══██╗██╔══██╗██╔════╝██║ ██║██╔═══██╗██║ ██║ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
██████╔╝███████║███████╗███████║██║ ██║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║ ██║╚██╗ ██╔╝ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
██║ ██║ ██║███████║██║ ██║╚██████╔╝ ╚████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝