solidity-auditor について
Solidity の監査担当者や開発チーム向けのスマートコントラクト監査スキルです。デプロイスクリプトも対象に含め、12 エージェントによる並列レビューを行い、複数回スキャンや検出事項の記録にも対応します。
主な機能
- 01除外ルールによる Solidity ファイルの選別
- 02デプロイスクリプトを監査対象に含める機能
- 0312 エージェントによる並列レビュー
- 04複数回スキャンと統合レポート
- 05任意の検出事項台帳
活用シーン
- 01Solidity コントラクトコードのレビュー
- 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ファイルをスキャンします。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/の外には何も書き込まれません。変わったのは、スキャンのたびにシェルによってレポートが組み立てられるようになった点です。そのため、このフラグでレポートを省略することはできません。再生成するのではなく、コピーします。後から編集する人はこれを間違いと判断して、元に戻してはいけません。 -
--memory(デフォルトではオフ):監査対象リポジトリの.solidity-auditor/memory.tsvに台帳を作成し、スキャン間で検出事項を記録します。パス数が1を超えると、このフラグの有無にかかわらずメモリが自動的に有効になります。 -
--loop [N](デフォルトではオフ):1回のスキャンでN回のパスを実行します。各パスには、それ以前のパスで見つかった内容が伝えられ、最後に1つの統合レポートが作成されます。数値を指定せずに--loopを指定した場合は、3回のパスを実行します。--loop 1は1パスのスキャンです。このフラグはフラグを使いたいランナー向けのものです。指定された場合、Turn 1b の選択処理では質問せず、その指定に従います。
用語(このファイル全体で使用):
- 実行(run) — 12エージェントによる1回のパス。
- スキャン(scan) — このスキルの1回の呼び出し。1回のスキャンには、1回以上の実行が含まれます。
台帳の scans 列はスキャン数を数えます。レポートの seen in k/N runs は、1回のスキャン内での実行数を数え、台帳には書き込まれません。数値も名称も別です。混同しないでください。
スキャンで実行するパス数は {passes} です。Turn 1b で確定し、1以上になります。ループ本体は Turn 2 のステップ2c、Turn 2 のステップ3、Turn 3a、Turn 3b、Turn 4 で構成され、パスごとに1回実行されます。その前のすべての処理はスキャンごとに1回実行され、Turn 5 では {passes} の値にかかわらず、スキャンを1回だけ終了します。
厳守事項 — 通常のパスは通常のままにしてください。 フラグなしの1パススキャンでは、メモリに関するステップには一切進みません。Turn 1c、Turn 2のステップ2、Turn 4のステップ4、Turn 4のステップ6のいずれにも進みません。台帳を読み込まず、どこにもファイルを書き込まず、メモリ機能が追加される前とまったく同じレポートを出力します。メモリが有効になるのは、
--memoryが指定された場合、またはパス数が1を超える場合のみです。後任の編集者がメモリ関連のステップを無条件に実行しようとしたら、それは整理ではなく、この仕様を意図的に破ることになります。
1パスの回答では、ループやメモリの仕組みに一切進みません。 台帳は読み書きされません。Scopeテーブルに
PassesやMemoryの行はありません。seen in k/N runsも表示されません。KNOWN/NEWタグも付きません。パスごとのサマリー行もありません。出力されるレポートは、ループモードが導入される前にこのスキルが出力していたレポートです。 ピッカーの痕跡は、質問そのものだけです。書き込まれるもの。 すべてのスキャンは
.solidity-auditor/runs/{stamp}/を作成し、run-1.md、scope.tsv、full-report.mdをそれぞれ1つずつ書き込みます。パス数にかかわらず、すべてのレポートはこれらのファイルから組み立てられ、name、mode、files、thresholdはすべてのレポートに必要だからです。この段落には以前、1パスの回答では.solidity-auditor/ディレクトリもruns/ファイルも作成されないと書かれていましたが、現在は誤りです。また、そもそもこのルールが守ろうとしていたのはその点ではありません。このルールが守るものを正確に言うと、次のとおりです。 通常のパスでは、スキャンは台帳を読み込まず、
mem_キーを書き込まず、ScopeテーブルにはMode、Files reviewed、Confidence threshold (1-100)のちょうど3行だけを出力します。Passesの行もMemoryの行もありません。ディスクへの書き込みは出力ではありません。後任の編集者がメモリ関連のステップを無条件に実行するようにしたら、それは意図的な仕様違反です。runsディレクトリへの書き込みは、そうしたステップには該当しません。1パスの実行で
--memoryを指定した場合のみ例外となり、メモリは有効になりますが、ループの仕組みは有効になりません。
オーケストレーション
Turn 1 — 検出。 バナーを表示してから、1つのメッセージ内で次のツール呼び出しを並列に実行します。
a. モード選択に応じて、対象範囲内の .sol ファイルを find で探すBashコマンド
b. **/references/hacking-agents/shared-rules.md を対象とするGlob — references/ ディレクトリ(2階層上)を {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} として保存します。スキャンごとにタイムスタンプは1つだけとし、ここで一度だけ生成します。 runsディレクトリ、runファイル、--file-output によるコピーにはすべてこのタイムスタンプが付くため、レポートと、その作成元となったrunsを目視で紐付けられます。
Turn 1a — スキャンディレクトリを開く。 find の結果が返された後、1つのBashコマンド内で次を実行します。
mkdir -p .solidity-auditor/runs/{stamp}
: > .solidity-auditor/runs/{stamp}/scope.tsv
次に、このターンで判明している3つのscopeキーを書き込みます。scopeキーは1つの 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。{file list}—findが返した対象範囲内のすべてのパスを、1行にスペース区切りで並べたもの。findの順序を維持します。アセンブラーはこれらを3件ずつの行に折り返すため、出力順はここに書き込んだ順序になります。
scope.tsvはkey<TAB>value形式で、追記のみです。同じキーが複数ある場合は最後の行が優先されます。 キーがなければテーブル行も表示されません。これにより、特別な条件分岐なしで、通常のスキャンのScopeテーブルを3行に保てます。値にタブや改行が含まれると行が壊れるため、エスケープではなく除去します。ここで使うキーに、どちらの文字も必要ありません。4つのターンにまたがる6つの書き込み処理がこの1つのファイルに追記しますが、どの処理も既存の行を書き換えたり削除したりすることはありません。モデルが値を設定するキーは
nameとmodeの2つだけです。それ以外はシェルが把握しているか、アセンブラーが自ら読み取ります。しきい値は定数から、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 — モデルとパス数。 このターンでは、12 のエージェントが使用するモデルと、スキャンを実行するパス数の2 つの質問を 1 回の AskUserQuestion 呼び出しで尋ねます。作業開始前にランナーは一度中断されます。
2 つの質問でスキップされる条件は異なります。
AskUserQuestionがなく、modelパラメーターを受け取るAgentツールを備えたランタイム(Codex、Gemini、Cursor のネイティブエージェント)では、モデルの質問は何も言わずにスキップされ、{agent_model}は未設定のままとなり、その代わりの文章も表示されません。パス数の質問はスキップされません。ターン 1b-ii の出力ブロックに進み、そこで停止して回答を待ちます。このターン全体がスキップされることはありません。後から編集する人は、「このターン全体をスキップ」という包括的なルールを復活させてはいけません。このターンが 1 つの質問だけを尋ねていたときは正しいルールでしたが、現在は当てはまりません。
ターン 1b-i — AskUserQuestion 呼び出し(Claude Code)。 2 つの質問を 1 回の呼び出しで尋ねます。Agent ツールが model パラメーターを受け取らない場合は、パス数の質問だけを尋ねます。
質問 1 — モデル:
-
システムプロンプトを確認し、自分自身のモデルファミリー(Opus、Sonnet、Haiku)を特定します。バージョンの数字は無視します。Agent ツールの
modelパラメーターが受け取るのはファミリー名("opus"/"sonnet"/"haiku")であり、ランタイムがそのファミリーの最新バージョンに解決します。 -
呼び出しに次の質問を含めます。
- 質問:
"Which Claude model should the 12 audit agents use?" - 単一選択の選択肢を 3 つ用意します。オーケストレーター自身のファミリーを
(Recommended)として最初に配置します。 - 各選択肢の
descriptionフィールドにlatestを設定します。 - 各選択肢の
previewフィールドには、次の内容をそのまま設定します(すべての空白を正確に保持してください。3 つの幅を同じにします)。
Opus のプレビュー:
┌──────────────────────────────────────────────────────────┐ │ opus · highest reasoning · most expensive │ └──────────────────────────────────────────────────────────┘Sonnet のプレビュー:
┌──────────────────────────────────────────────────────────┐ │ sonnet · balanced reasoning · mid cost │ └──────────────────────────────────────────────────────────┘Haiku のプレビュー:
┌──────────────────────────────────────────────────────────┐ │ haiku · lowest reasoning · cheapest │ └──────────────────────────────────────────────────────────┘ - 質問:
-
ランナーの選択を
{agent_model}として保存します。回答がない場合は、オーケストレーター自身のモデルをデフォルトとして使用します。
質問 2 — パス数。同じ呼び出しの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 つ用意し、
3 passesを最初に配置して(Recommended)を付けます。それぞれに、このターンの形式のpreviewボックスを付けます。ボックスの幅は60 文字で、モデル選択と同じ幅にします。1 つのプロンプト内の 2 つの質問が一体に見えるようにします。空白を保持して、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 │ └──────────────────────────────────────────────────────────┘これらの数値は推測ではなく、最低所要時間です。 2,228 行の Solidity を含む 10 個のファイルを対象に、Opus 上で 1 パスにつき 12 個のエージェントを使って実施した実際の 3 パススキャンでは、実時間で43 分かかりました。パス 1 に 11 分、パス 2 と 3 にそれぞれ約 16 分かかりました。
known-findings.mdが増えるにつれて、12 個すべてのバンドルに追加されるため、後続のパスほど時間がかかります。個々のエージェントの実行時間は 3.5~11 分でした。コードベースが大きければ時間がかかり、小さいモデルなら速くなります。倍率ではなく分数で示してください。「約 3 倍の時間」では、ランナーは待つべきか、昼食後に戻るべきか判断できません。これらの数値を再計測した場合は、別の場所に推定値を追加するのではなく、ここを修正してください。 -
それ以外の数値には、専用の選択肢は必要ありません。
AskUserQuestionは常に自由記述欄付きの その他 の選択肢を追加し、実行者はそこに数値を入力します。「任意の数値」と書かれた4つ目の選択肢は追加しないでください。選択肢は固定された選択肢なので、数値を入力できず、先に進めなくなります。その他の回答から最初の整数を解析します。1未満または10超の場合は、もう一度だけ尋ねます。2回目の回答も使用できない場合は、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 は提示された3つの選択肢です。それ以外の整数は、その数のパスを意味します。Turn 1b-i の手順5と同じ上限・下限を適用します。1未満または10超の場合は、もう一度だけ尋ね、その後は1パスにします。
Turn 1b-iii — 実行者がすでに回答している場合。 すでに回答済みのことは尋ねません。
| 受け取った内容 | ピッカーの動作 |
|---|---|
--loop 5 | パスに関する質問を黙って省略します。5パスです。 |
数値のない --loop | パスに関する質問を黙って省略します。3パスです。 |
| 「4パス実行」「これを4回監査して」 | パスに関する質問を黙って省略します。4パスです。 |
| 「ループモード」「何回か実行して」—数値のないリクエスト | 尋ねます。 実行者が求めたのは機能であり、回数ではありません。 |
| 何もない | 尋ねます。 |
最初の3行では、質問を省略する際に何も表示しません。--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 の手順ではありません。2つの結果によってスキャン全体が停止または縮小されるため、高コストの処理を始める前に判定する必要があります。
-
シェルを確認します。 メモリのマージには
awkを使用します。command -v awk >/dev/nullを一度実行します。失敗した場合は、memory needs a bash shell — on Windows install Git for Windowsと出力し、このスキャンではメモリをオフにして、通常の1回だけのスキャンとして実行し、ファイルには一切触れません。マージの別実装はありません。決して破ってはならないルールは、1つの言語だけに記述されています。 -
古い一時ファイルを確認します。
.solidity-auditor/memory.tsv.tmpが存在する場合は、前回のスキャンが完了していません。warning: .solidity-auditor/memory.tsv.tmp left by an unfinished scan — overwritingと出力して続行します。このスキャンのマージ処理で上書きされます。 -
台帳を読み込みます。
.solidity-auditor/memory.tsvが存在しない場合は、これが初回のスキャンです。メモリは空で、すべての検出結果はNEWとなり、ファイルは最後に書き込まれます。それ以外の場合は、読み込んでから使用前に検証します。- 1行目の最初のタブ区切りフィールドが、正確に
#solidity-auditor-memory v1であること - それ以降のすべての行に、タブ区切りフィールドが正確に 6 個あること 失敗が発生した場合は、そのパスと問題点を出力し、スキャンを停止します。エージェントもレポートも書き込みも行いません。ユーザーがファイルを修正または削除します。新たにやり直してはいけませんし、警告付きで続行してもいけません — 1つの不正な行が実際のメモリを破壊してはなりません。
- 1行目の最初のタブ区切りフィールドが、正確に
-
それをコピーします。
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でプルーニングが実行される場合はその後のもの)。
関数ごとのバグクラス語彙 — 各
contract|functionプレフィックスについて、台帳がすでに保持しているバグクラスラベル — は、オーケストレーターの頭の中には保持されません。ターン2のステップ2cがそれを{bundle_dir}/known-findings.mdに書き出し、エージェントは自分のバンドル内でそれを読み、ターン4がディスクからそれを読み戻します。これがメモリではなくファイルであるのは、2人の読み手が長いスキャンを隔てているためであり、また同じラベルが両方に届かなければならないためです。この語彙が、正確なキー照合を誠実に保つものです: バグクラスは言葉で書かれたラベルであるため、同じバグに別のラベルを付けると2番目のレコードになり、メモリは静かに失敗します。既存のラベルを返し渡すことが、同じバグが同じキーを保つ方法です。
ターン2 — 準備。 1つのメッセージで、並列ツール呼び出しを行います: (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も同じ理由で、1つ下のレベルで読み取られます。report-formatting.mdは所見ブロックの 形 を定め、report-language.mdはその 中の言葉 を定めます。ターン4のステップ5aがタイトルと Description を書き、アセンブラーがそれらをそのまま貼り付けるため、それらのバイトが開発者が行動できる文を書く最後の機会です。それは簡略化技術英語(ASD-STE100)であり、任意のスタイリングではありません: 開発者が読めない所見は、修正されない所見です。同じファイルがすべてのエージェントバンドルに入っているため、パスが書く文とエージェントが渡した文は1つの規則に従います。
次に source.md をビルドし、メモリステップを実行し、その後でのみバンドルを cat します — この順序で、なぜならバンドルはメモリステップが書き込むファイルを運ぶからです:
ターン2はループ全体に分割されています。 ステップ1は スキャンごとに1回 実行されます:
source.mdはパス間で変更されないことが証明可能です — ループ全体で1つの git SHA、パス間にプルーニングなし — そしてそれはビルドの高コストな半分です。ステップ2cとステップ3は パスごとに1回 実行されます、なぜならそれらだけが既知のものを運び、すでにディスク上にあるファイルから12個のバンドルを再 cat するのは1つの Bash コマンドだからです。ステップ2aと2bはステップ1とともにスキャンごとに1回実行されます、両方ともその凍結されたソースを読むためです。1つの
{bundle_dir}を再利用し、すべてを上書きします。source.mdは1回書き込まれ、二度と触れられません;known-findings.mdと12個のagent-N-bundle.mdファイルは各パスで上書きされます。ランナーが1パスを選んでも10パスを選んでもディスクはフラットなままです — パスごとのディレクトリはリポジトリ全体の12 × N コピーを保持することになるでしょう。これが安全なのは、ターン3bがハードバリアだからです: パス K+1 が上書きするときにパス K のエージェントがまだバンドルを読んでいることはありません。受け入れられるコスト: ループ後には、パス2がそのエージェントに何を伝えたかを見ることはできません。永続的な記録はランファイルと台帳です。
- スキャンごとに 1 回。
{bundle_dir}/source.md— スコープ内のすべての.solファイル。それぞれに### pathヘッダーとフェンス付きコードブロックが付きます。 - Turn 2 ステップ 2 — 名前マップ、プルーニング、既知の検出事項(以下)。メモリがオフの場合は全体がスキップされます。パート a と b はスキャンごとに 1 回実行され、パート c は毎回のパスで実行されます。読み取る台帳はループが学習するにつれて成長するためです。
- 毎回のパス。 エージェントバンドルは、
catを使用した単一の Bash コマンド(シェル変数やヒアドキュメントは使用しない)で生成されます =source.md+ エージェント固有のファイル:
| バンドル | 追加されるファイル({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 個すべて | + report-language.md、shared-rules.md の後に追加 — 無条件、すべてのモード、すべてのパスで。エージェントの description: はレポートの Description のシードであるため、言語ルールはエディタだけでなくライターにも届く必要があります。 |
| 12 個すべて | + {bundle_dir}/known-findings.md、最後に追加 — メモリがオンでかつステップ 2 がそのファイルを書き込んだ場合のみ。通常のスキャンでは決して追加されず、台帳に記録がない場合も決して追加されません。 |
各バンドル = source.md + SOP + 専門分野 + shared-rules + report-language(+ 既知の検出事項がある場合はそれも)。エージェントはバンドルを読み取ります。初期スキャンには Read/Grep は不要です。クロスファイル調査のために的を絞った Read/Grep が許可されています。
Turn 2 step 2 — 名前マップ、プルーニング、既知の所見。 メモリがオフの場合、このステップ全体をスキップしてください。 このステップが独立したターンではなく Turn 2 にあるのは、3 つのパートすべてが source.md を読み取るためです。source.md は Turn 2 で作成されたばかりなので、独立したターンにすると、そのファイルを境界越しに渡すだけになってしまいます。パート c が各バンドルに含まれるファイルを書き込むため、番号はバンドルの cat より前の 2 です。プルーニングされたレコードがエージェントに渡ることがあってはなりません。
a. 名前マップを必ず作成する — スキャンごとに 1 回、ステップ 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 列目はキーのセグメントと同じ方法で正規化した識別子(小文字に変換し、英数字以外の文字の連続部分をハイフン 1 つに置き換えたもの)です。2 列目はソース内での綴りです。両側を同じ方法で正規化することで、比較が厳密になります。キーのセグメントをソースの未加工テキストと照合することはできません。たとえば、withdrawAll は withdrawall として格納されるためです。
b. プルーニング — スキャンごとに 1 回のみ、リポジトリ全体のスキャンが終わった後に実行します。 デフォルトモードの場合にのみ実行してください。名前付きファイルのスキャンでは実行しないでください(スキャンされていないファイルからは、そのファイルに含まれるレコードについて何も証明できません)。また、1 回のスキャン中に 2 回目のプルーニングを実行しないでください。ループ内の後続のパスも同じ固定されたソースを読み取るため、2 回目のプルーニングで新たに判明することはありません。
プルーニングの対象はコピーであり、稼働中の台帳ではありません。最初のマージ時に、プルーニングの結果が稼働中のファイルに反映されます。稼働中のファイルを直接プルーニングすると、次回の再構築で元に戻ってしまいます。
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> の形式では記述されないため、それらのいずれかに関するレコードは、コントラクトが残っている限り保持されます。
プルーニングでは、所見だけでなく Leads も削除されます。 Lead も台帳の完全なレコードなので、同じルールが適用されます。プルーニングされた Lead は、出力内のほかの場所にも痕跡を残しません。そのため、削除した行はすべて実行担当者に出力します。kind、キー、タイトルを、それぞれ 1 行に出力してください。Pruned N records (contract or function no longer in scope): の下に表示します。レコードを黙って削除してはいけません。
プルーニング後、Turn 1c step 6 のキー一覧用にコピーを読み直してください。プルーニングされたレコードをエージェント、マージ、または「Known from earlier scans」セクションに渡してはいけません。
c. mem_before を書き込んでから、{bundle_dir}/known-findings.md を作成します。
まず mem_before — マージが読み取る時点のコピーの行数、つまりプルーニング後の件数です。ここでのみ書き込み、パート 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 件 · スキャン後に 14 件 は、マージで扱ったものと同じセットを表している必要があります。
このキーは、アセンブラーが読み取るメモリフラグです。 Memory 行が表示され、「Known from earlier scans」セクションが存在するかどうかを決めるのは、このキーの有無です。memory-before.tsv の有無ではありません。メモリがオフの場合、mem_ キーが書き込まれることはなく、このステップ全体がスキップされるため、フラグがスキャンの状態と食い違うことはありません。
このキーは、以下の早期終了より前に書き込むため、台帳が空でも Memory 行に スキャン前に 0 件 と表示されます。
次に {bundle_dir}/known-findings.md — エージェントと 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 を作成せず、バンドルにも何も追加しないでください。空の台帳(初回のスキャン、またはすべてのレコードがプルーニングで削除されたスキャン)で、12 個のエージェントに空の見出しを渡してはいけません。
それ以外の場合は、{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人のエージェントと、それをキーに書き込むTurn 4のステップ4に適用されます。
ビルダーは、任意の6列の台帳ファイルを受け取ります。 単一実行のスキャンでは、上記のとおり、そのファイルは枝刈りした複製です。マルチパスのスキャンでは、各パスの後、バンドルを再結合する前に、新しくマージされた.solidity-auditor/memory.tsvからknown-findings.mdを再構築します。これにより、パスK+1のエージェントは、パスKで見つかった、すでに調査済みの内容を読み取れます。メモリをループに引き継ぐ目的はそれだけです。コマンドは変わらず、指定するファイルだけが変わります。
すべてのバンドルとsource.mdの行数を出力します。Agent呼び出しのプロンプト自体にソースコードを埋め込まないでください。
Turn 3a — 12人のエージェントをすべて起動します。 すべてのパスで実行します。1つのメッセージで、12人全員を並列のバックグラウンドAgent呼び出し(run_in_background=true)として起動します。Turn 1bで{agent_model}が設定されている場合は、すべてのAgent呼び出しにmodel={agent_model}を渡します。{agent_model}が未設定の場合(Turn 1bをスキップした場合 — Codex、Geminiなど)は、modelパラメーターを完全に省略し、デフォルト値で代用しないでください。各エージェントの完了時にオーケストレーターへ通知が届きます。ポーリングやスリープはしないでください。フェーズは1つのみで、後から追加の起動はありません。12人全員から完了通知が届いてからTurn 3bに進みます。
エージェント1~9には単一専門分野プロンプト(Turn 3a-i)を使用します。エージェント10~12にはギャップハンタープロンプト(Turn 3a-ii)を使用します。
Turn 3a-i — 単一専門分野プロンプト(エージェント1~9)。 {resolved_path}/agent-prompts.mdの「Single-specialty prompt」にあるテンプレートを使用し、{bundle_dir}、エージェント番号、バンドルの行数を置き換えます。
Turn 3a-ii — ギャップハンタープロンプト(エージェント10~12)。 同じファイルにある「Gap-hunter prompt」のテンプレートを使用します。
このファイルに記載されているルールのうち、文章ではなく条件であるため、ここで繰り返すものが2つあります。
- **「Known findings」**の段落を含めるのは、メモリが有効で、
known-findings.mdが追加されている場合に限ります。通常のスキャンでは、プロンプトは従来どおりバイト単位で同一です。存在しないセクションについての段落を含めると、エージェントがそのセクションを探すことになるためです。 - READ-ONLYの段落は無条件です。すべてのエージェント、すべてのモード、すべてのパスで適用されます。実際のスキャンで、その必要性が証明されました。あるエージェントが監査対象のリポジトリ内にFoundryの概念実証ファイルを作成し、その後削除しました。作業ツリーはクリーンなままで、保存されたSHAも正確でしたが、それでも誤りでした。後から編集する際に条件付きにしてはならず、任意の設定として弱めてもいけません。
Turn 3b — 12人のエージェント全員の完了を待ちます。 すべてのパスで実行します。起動した12人全員から完了通知が届いたら、Turn 4に進みます。全エージェントが終了するまでは重複排除に進まないでください。自然に完了するまで実行させます。ポーリングやスリープはせず、完了通知が届いた場合にのみ対応してください。
待機中に、最初のパスのみ、{resolved_path}/dedup-and-assembly.mdを読みます。 ここにはTurn 4とTurn 5の全内容が記載されています。このターンは、スキャン中にオーケストレーターがほかに行う作業がない唯一のタイミングなので、読み込みに実時間はかかりません。また、Turn 4の開始前に内容を把握しておくことで、Turn 4がその場で手順を考えることを防げます。後続のパスでは、すでに内容を把握しています。
エージェントが停止した場合。 戻ってきた11人でパスを続行します。どのモードでも、再起動してはいけません。 再試行すると、カバレッジの12分の1のために待ち時間が無制限になる可能性がありますが、ループなら追加コストなしで対応できます。次のパスで同じ12の専門分野を再実行し、今回のエージェントが見つけた内容も把握できます。損失を3か所すべてに記録してください。記録しないと、カバレッジの損失が見えなくなります。パスの要約行(Turn 4のステップ5)、run-K.mdのヘッダー、レポートのPasses行です。
パス全体で何も生成されなかった場合 — バンドルの構築に失敗した場合、または12人全員が停止した場合:
どちらを行うよりも先に、失敗を記録します。 何も生成しなかったパスは実行ファイルを書き出さないため、それ以外に、そのパスが計画されていたことをディスク上で知る方法がありません。
printf '%s\t%s\n' pass_{K}_failed 1 >> .solidity-auditor/runs/{stamp}/scope.tsv
- パス 1 で何も生成されない場合: これはループの失敗ではなく、今回の実行での失敗です。台帳への書き込みはありません。Turn 5 に進み、何も出力しないのではなく、検出結果のないレポートを組み立てます —
_None — this scan raised no findings._— 。1 パスのスキャンではPasses行が省略されるため、アセンブラーは代わりに Run files 行でその旨を示します —⚠️ No pass produced a run file. This scan reviewed nothing.実行ディレクトリはそのまま残り、試みた内容を示すscope.tsvが格納されています。 - 後続のパスで何も生成されない場合: ループを停止し、Turn 5 に直接進みます。完了したパスをもとにレポートが組み立てられ、
Passes行に失敗したパスが示されます。何も失われません。各パスの後に台帳が再構築されているためです。仕組みが壊れた後も次のパスへ進むと、ランナーの費用が無駄になります。
Turn 4 — 重複排除、検証、記録。 すべてのパスで実行され、ループ本体の最後にあたります。このパスのエージェント結果を重複排除し、ゲート評価を行い、タグ付けし、パスを記録して、台帳を書き込みます。レポートは出力しません。スキャンごとにレポートは 1 つだけで、Turn 5 で出力されます。中間の重複排除リストは出力しないでください。
メモリがオフの場合、ステップ 4 と 6 は完全にスキップされます。その他のステップは、パス数にかかわらず実行されます。
{resolved_path}/dedup-and-assembly.md の 「Turn 4」 の手順に従ってください。
その後、ループ本体を {passes} 回実行した(または途中で停止した)ら、Turn 5 に一度だけ進みます。
Turn 5 — 組み立て、出力、クリーンアップ。 1 パスのスキャンや、ループが途中で停止した場合も含め、パス数にかかわらずスキャンごとに一度だけ実行されます。このターンを実行せずにスキャンが終了することはありません。
このターンではモデルによる判断は行いません。 2 つのファイルをコピーし、assemble.sh を実行し、awk で数を数えて、出力します。このスキルでレポートを生成するのは assemble.sh だけです。その出力の言い換え、順序の変更、追加、独自の言葉での要約はしないでください。このターンがレポート作成の手順として簡素すぎるように見えるなら、それが意図したとおりです。その理由は dedup-and-assembly.md に記録されています。
{resolved_path}/dedup-and-assembly.md の 「Turn 5」 の手順に従ってください。メモリがオフの場合はステップ 1 をスキップします。ステップ 4 は --file-output が渡された場合にのみ実行されます。
バナー
ほかのことを行う前に、これをそのまま出力してください:
██████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
██╔══██╗██╔══██╗██╔════╝██║ ██║██╔═══██╗██║ ██║ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
██████╔╝███████║███████╗███████║██║ ██║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║ ██║╚██╗ ██╔╝ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
██║ ██║ ██║███████║██║ ██║╚██████╔╝ ╚████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝