mcpField
S
solidity-auditor

solidity-auditor

github.com/pashov安全1.3k836 次安装1 周前更新
代码仓库

关于 solidity-auditor

用于 Solidity 项目的智能合约安全审计技能,面向审计者和开发团队。它按范围扫描生产代码,并通过多个 Agent 并行审查;可选多轮扫描和跨扫描记录发现。

主要功能

  1. 01按排除规则筛选 Solidity 文件
  2. 02部署脚本纳入审计范围
  3. 0312 个 Agent 并行审查
  4. 04支持多轮扫描与综合报告
  5. 05可选跨扫描发现记录

适用场景

  1. 01审查 Solidity 合约代码
  2. 02检查部署脚本中的配置和状态设置
  3. 03通过多轮审计寻找新增问题
  4. 04在后续扫描中跟踪既有发现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pashov/skills --skill solidity-auditor

安装后在 Claude Code 中输入/solidity-auditor确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
VERSION
references/
agent-prompts.md
assemble.sh
dedup-and-assembly.md
judging.md
report-formatting.md
report-language.md
senior-auditor-sop.md
references/hacking-agents/
access-control-agent.md
asymmetry-agent.md
boundary-agent.md
economic-security-agent.md
execution-trace-agent.md
first-principles-agent.md
flow-gap-agent.md
invariant-agent.md
math-precision-agent.md
numerical-gap-agent.md
periphery-agent.md
shared-rules.md
trust-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 文件。使用 Bash find(而非 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——模型:

  1. 阅读系统提示词,判断你自身的模型系列(Opus、Sonnet 或 Haiku)。忽略版本数字——Agent 工具的 model 参数接受系列名称("opus" / "sonnet" / "haiku"),运行环境会选择该系列中的最新版本。

  2. 在调用中加入此问题:

    • 问题:"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                 │
    └──────────────────────────────────────────────────────────┘
    
  3. 将运行器的选择存储为 {agent_model}。如果没有回答,则默认使用编排器自身的模型。

问题 2——运行轮数。将其放在同一次调用中,作为第二个问题:

  1. 问题:"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,保留空白字符:

    标签description
    3 passes (Recommended)~45 min
    1 pass~15 min
    5 passes~75 min

    3 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 倍时间”无法告诉运行器是该继续等待,还是吃完午饭再回来。如果以后重新 测量了这些数字,请在此处修正,不要在其他地方再添加第二组估算。

  2. 其他任何数字都不需要单独设置选项。 AskUserQuestion 总会添加一个带自由文本框的 Other 选项,运行者会在那里输入数字。不要添加第四个选项,写成“您自己的数字”——选项是固定选项,无法收集数字,会导致流程卡住。

    解析 Other 答案,提取第一个整数。小于 1 或大于 10 → 再询问一次。第二次答案仍不可用 → 1 次遍历。

  3. 将答案存为 {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 的一步,因为此轮的两种结果会停止整个扫描或降低扫描级别——必须在开始任何高成本工作之前处理。

  1. Shell 检查。 使用 awk 合并记忆。运行一次 command -v awk >/dev/null。如果失败,打印 memory needs a bash shell — on Windows install Git for Windows,关闭本次扫描的记忆功能,将扫描作为普通的单次运行扫描进行,并且不接触任何文件。合并没有第二种实现;绝不能破坏的规则只存在于一种语言中。

  2. 过期临时文件。 如果 .solidity-auditor/memory.tsv.tmp 存在,说明上一次扫描未能完成。打印 warning: .solidity-auditor/memory.tsv.tmp left by an unfinished scan — overwriting,然后继续。它会在本次扫描的合并过程中被覆盖。

  3. 读取台账。 如果 .solidity-auditor/memory.tsv 不存在,这是首次扫描:记忆为空,每个发现都标记为 NEW,文件会在最后写入。否则,读取文件并在使用前先验证:

    • 第一行的第一个制表符分隔字段必须完全等于 #solidity-auditor-memory v1
    • 后续每一行必须恰好有 6 个制表符分隔字段 遇到任何失败时,打印路径和问题,然后停止扫描。不要启动 Agent,不要生成报告,也不要写入任何内容。由用户修复或删除该文件。不要重新开始,也不要带着警告继续——单个错误行绝不能毁掉真实记忆。
  4. 复制一份。 已经执行过 mkdir -p {bundle_dir};复制扫描开始前的账本:

    cp .solidity-auditor/memory.tsv {bundle_dir}/memory-before.tsv 2>/dev/null || : > {bundle_dir}/memory-before.tsv
    

    此次扫描执行的每次合并都读取这份副本,绝不读取实时文件。如果不存在账本,就创建一个空文件,这样下方的合并命令就不需要特殊处理。

  5. 创建扫描的行文件,并将其置空:: > {bundle_dir}/scan-rows.tsv。每次运行都会将通过门控的行追加到此文件。

  6. 保留键列表——副本中每一行的第 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 提供了什么。持久记录是运行文件和账本。

  1. 每次扫描一次。 {bundle_dir}/source.md — 所有范围内的 .sol 文件,每个文件都带有 ### path 标题和围栏代码块。
  2. 第 2 轮第 2 步 — 名称映射、筛选和已知发现(见下文)。关闭记忆时,整个步骤都会跳过。a 和 b 部分每次扫描运行一次;c 部分每轮运行一次,因为它读取的台账会随着循环学习而增长。
  3. 每轮运行一次。 使用单条 Bash 命令,通过 cat(而非 shell 变量或 heredoc)生成 Agent 代码包 = source.md + Agent 专属文件:
代码包附加文件(相对于 {resolved_path})
agent-1-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/math-precision-agent.md + hacking-agents/shared-rules.md
agent-2-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/access-control-agent.md + hacking-agents/shared-rules.md
agent-3-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/economic-security-agent.md + hacking-agents/shared-rules.md
agent-4-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/execution-trace-agent.md + hacking-agents/shared-rules.md
agent-5-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/invariant-agent.md + hacking-agents/shared-rules.md
agent-6-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/periphery-agent.md + hacking-agents/shared-rules.md
agent-7-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/first-principles-agent.md + hacking-agents/shared-rules.md
agent-8-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/asymmetry-agent.md + hacking-agents/shared-rules.md
agent-9-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/boundary-agent.md + hacking-agents/shared-rules.md
agent-10-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/numerical-gap-agent.md + hacking-agents/shared-rules.md
agent-11-bundle.mdsource.md + senior-auditor-sop.md + hacking-agents/trust-gap-agent.md + hacking-agents/shared-rules.md
agent-12-bundle.mdsource.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。

横幅

在执行任何操作之前,请严格按以下内容输出:


██████╗  █████╗ ███████╗██╗  ██╗ ██████╗ ██╗   ██╗     ███████╗██╗  ██╗██╗██╗     ██╗     ███████╗
██╔══██╗██╔══██╗██╔════╝██║  ██║██╔═══██╗██║   ██║     ██╔════╝██║ ██╔╝██║██║     ██║     ██╔════╝
██████╔╝███████║███████╗███████║██║   ██║██║   ██║     ███████╗█████╔╝ ██║██║     ██║     ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║   ██║╚██╗ ██╔╝     ╚════██║██╔═██╗ ██║██║     ██║     ╚════██║
██║     ██║  ██║███████║██║  ██║╚██████╔╝ ╚████╔╝      ███████║██║  ██╗██║███████╗███████╗███████║
╚═╝     ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝ ╚═════╝   ╚═══╝       ╚══════╝╚═╝  ╚═╝╚═╝╚══════╝╚══════╝╚══════╝

评论

支持 Markdown