メインコンテンツへ移動
AIチュートリアル

ToolReplayでAIエージェントのツール呼び出しを監査

このチュートリアルでは、ToolReplayのインストール、厳格なJSONLトランスクリプトとスコープファイルの準備、replay・scope・seal・verify監査の実行、決定論的なレポートの解釈、CIへのチェック統合、プロジェクトのセキュリティモデルと制限事項について説明します。

ToolReplayでAIエージェントのツール呼び出しを監査

ToolReplayの機能

ToolReplayは、実際のツールを呼び出さずに、記録されたAIエージェントのツール呼び出しを監査します。トランスクリプトを調べ、次の3つの問題を特定します。非決定的な応答、冗長な呼び出し、エージェントが宣言した権限外の呼び出しです。また、トランスクリプトをSHA-256ハッシュチェーンに封印し、封印済みファイルが編集されていないことを検証できます。

ToolReplayはライブ呼び出しを行わず、記録された応答を再生するため、監査は安全で副作用がなく、再現可能です。元のツールが利用できない場合でも使用できます。

ToolReplayのインストールと実行

ToolReplayにはPython 3.11以降が必要で、実行時のサードパーティ依存関係はありません。ネットワークにはアクセスしません。

ソースチェックアウトから、モジュールを直接実行します。

python -m toolreplay version

コンソールスクリプトをインストールすることもできます。

pip install .
toolreplay version

どちらの方法でも、インストールされているバージョンが次のように表示されます。

toolreplay 0.6.0

トランスクリプト形式を理解する

トランスクリプトはJSON Linesファイルです。空白以外の各行は1回のツール呼び出しを表し、必ず4つのフィールドだけを含める必要があります。

  • index:0から始まり、1ずつ増加する整数。
  • tool:呼び出されたツールの名前。
  • args:ツールの引数を含むオブジェクト。
  • response:記録されたツールの応答を含むオブジェクト。

有効なレコードは次のようになります。

{"index": 4, "tool": "write_file", "args": {"path": "docs/intro.md", "text": "edited"}, "response": {"ok": true}}

解析は意図的に厳格です。不明なフィールド、欠落したフィールド、無効なオブジェクト型、整数でないインデックス、順序どおりでないインデックスがあると、致命的なエラーになります。Pythonではboolがintのサブクラスとして扱われますが、ブール値は整数として受け付けられません。

ToolReplayは、正規化JSONを使用して呼び出しを比較します。ツール名と引数は、キーをソートし、不要な空白を除いてエンコードされます。そのため、キーの順序が異なる引数オブジェクトも同じ呼び出しとして扱われます。ToolReplayが決定性を確認する際、応答にも同じ正規化エンコードが使用されます。

再生監査を実行する

replayコマンドは、トランスクリプトを記録された応答と照合します。リポジトリに含まれる問題のあるサンプルを試します。

python -m toolreplay replay samples/session-dirty.jsonl

このコマンドは、決定的な行単位のレポートを生成します。

calls: 6
divergence: index 5
findings: 2
index 2: redundant-call: tool 'read_file' repeats the identical call at index 1 with no state change between them
index 5: non-determinism: tool 'search' returned a different response than the identical call at index 3

ヘッダーは上から順に読みます。calls: 6は、6件の有効なレコードが解析されたことを意味します。divergence: index 5は、決定論的に再実行した場合に記録と最初に食い違う箇所を示します。findings: 2は、その後に続く検出結果の件数です。

検出結果はインデックス、次に種類の順でソートされるため、同じ入力に対する監査を繰り返してもバイト単位で同一の出力になります。

非決定性

ToolReplayは、正規化された各呼び出しに対する最初の応答を記憶します。同一の呼び出しで後から異なる記録済み応答が返された場合、非決定性を報告します。サンプルでは、installに対するsearch呼び出しがインデックス3と5に現れますが、応答に記録されたヒット件数が異なります。インデックス5が食い違いの起点になります。

これはエージェントにとって重要です。最初の回答に基づく判断は、2回目の回答が返されていた場合に変わる可能性があるためです。この検出結果は、セッションが記録内で安定していない情報に依存していることを示します。

冗長な呼び出し

2つの呼び出しの間に状態を変更し得る記録済みイベントがない場合に限り、繰り返された呼び出しは冗長とみなされます。デフォルトの変更ツールはwrite_file、delete_file、create_file、move_file、run_commandです。同一の呼び出しの間に異なる呼び出しが1つでもあれば、状態が変化した可能性があるとして保守的に扱われます。

サンプルでは、read_fileがインデックス1と2でdocs/intro.mdを読み取り、その間に状態の変化がないため、インデックス2が検出されます。write_fileの呼び出し後に行われる読み取りは、冗長とはみなされません。

エージェントの権限を確認する

エージェントの権限定義がある場合は、scopeを使用します。

python -m toolreplay scope samples/session-dirty.jsonl samples/scope.json

サンプルの出力は次のとおりです。

calls: 6
findings: 1
index 4: permission-overreach: tool 'write_file' is not in the declared scope for agent 'docs-reader'

設計上、再生とスコープ確認は別のコマンドです。再生はセッション自体を評価するため、必要なのはトランスクリプトだけです。スコープ確認は各呼び出しを別の権限ファイルと照合します。

スコープファイルは、エージェント名と許可されたツールの完全なリストを含む1つのJSONオブジェクトです。

{"agent": "docs-reader", "allowed_tools": ["read_file", "list_dir", "search"]}

ツール名は完全一致かつ大文字と小文字を区別して照合されます。フィールドの欠落、誤った型、文字列でない要素は拒否されます。スコープ確認は名前に基づいて行われ、引数の検査や、許可されたツールが許可されたパスを対象としているかどうかの判定は行いません。

トランスクリプトを封印する

sealコマンドは、トランスクリプトをハッシュチェーン化されたJSONLファイルに変換します。

python -m toolreplay seal samples/session-dirty.jsonl > sealed.jsonl

各封印済みリンクには、元のレコードフィールドに加えて prev と digest が含まれます。最初のリンクでは、前のダイジェストとして64個のゼロの16進数文字を使用します。それ以降のSHA-256ダイジェストには、前のダイジェストと、インデックス、ツール、引数、応答を含む現在のレコードの正規バイト列が含まれます。

各リンクは直前のリンクに依存するため、以前のレコードを変更すると、そのレコードのダイジェストと、それ以降のすべてのダイジェストが変わります。したがって、このチェーンによって編集を検出でき、最初に壊れたリンクの位置を特定できます。

封印済みファイルを検証する

完全なチェーンは次のコマンドで検証できます。

python -m toolreplay verify sealed.jsonl

変更されていないファイルでは、次のように表示されます。

chain: intact

記録された応答やその他のチェーン対象フィールドが変更されると、検証結果に最初に壊れたリンクと、期待されるダイジェストおよび検出されたダイジェストが表示されます。

chain: broken
first broken link: index 3
expected: 34d19a9dfb112baa07afa37994ddcbbebe2973674385f3942340653d08d205bd
found: 40aadf3b8156217f0b5b1d95010b6b79f82a26ccf5e780aed0f5f311f865028a

封印により、順序、インデックス、ツール、引数、応答の変更など、封印後の編集を検出できます。ただし、これはデジタル署名ではありません。ファイルを編集できる人は、再封印もできます。来歴を証明するには、ToolReplayの外部で管理される鍵を使って最終ダイジェストに署名する必要があります。

CIでToolReplayを使用する

ToolReplayの終了コードは、ビルドゲートに適しています。

  • 0:監査に問題がない、またはチェーンが完全である。
  • 1:検出事項がある、またはチェーンが壊れている。
  • 2:不正な入力やファイルの欠落など、使用方法のエラーが発生した。

CIジョブから直接、リプレイとスコープチェックを実行できます。

python -m toolreplay replay session.jsonl
python -m toolreplay scope session.jsonl scope.json

終了コード1は、ツールが監査を完了し、報告すべき事項を検出したことを意味します。終了コード2は監査を実行できなかったことを意味するため、自動化処理では不正な入力と実際の検出事項を区別できます。

再現可能な比較を行うには、各実行結果を封印して差分を確認します。

python -m toolreplay seal run-a.jsonl > run-a.sealed.jsonl
python -m toolreplay seal run-b.jsonl > run-b.sealed.jsonl
git --no-pager diff --no-index run-a.sealed.jsonl run-b.sealed.jsonl

レポートにはタイムスタンプやランダムな値が含まれないため、同一の入力からは同一の出力が生成されます。最初に異なる封印済みの行によって、2つの実行結果が一致しなくなった位置を特定できます。また、変更されたダイジェストによって、それ以降のリンクがその差異に依存していることが分かります。

高度な使い方と実用上の制限

  • トランスクリプトを追記しやすく保つ。 JSON Linesを使うと、プロデューサーは呼び出しごとに1件のレコードを追記でき、バージョン管理で変更を容易にレビューできます。
  • 記録された応答を保持する。 リプレイは実際のツールを呼び出さず、トランスクリプトに記録された問題のみを検出できます。
  • 冗長性は慎重に解釈する。 デフォルトの変更ツール一覧は固定されており、その一覧にないカスタムの変更ツールは、冗長性分析では読み取り専用として扱われます。
  • 比較の境界を理解する。 非決定性が検出されるのは、同じ正規化された呼び出しが繰り返された場合だけです。意味的に関連する応答を返す異なる呼び出しは、同値性の評価対象になりません。
  • 権限と対象指定を分けて考える。 スコープチェックの対象はツール名だけです。許可されたツールが禁止されたリソースに対して使用された場合、それを検出することはできません。
  • 封印は完全性のために使い、身元確認のためには使わない。 チェーンによって、封印後にファイルが変更されたかどうかは分かりますが、誰がチェーンを作成したかは証明できません。

テストスイートを実行する

このプロジェクトではPython標準ライブラリの unittest フレームワークを使用しており、サードパーティ製のテスト依存関係はありません。リポジトリのルートから次を実行します。

python -m unittest discover -s tests

テストでは、厳密なトランスクリプト解析、正規エンコーディング、ハッシュチェーンの作成と改ざん検出、リプレイによる検出事項、スコープ適用、CLIの動作、終了コードを検証します。

まとめ

ToolReplayは、AIエージェントのセッションを対象とした、目的を絞った監査ワークフローを提供します。まず replay で一貫性のない動作や無駄な動作を見つけ、scope で宣言されたツール権限を確認し、seal と verify を組み合わせて記録後の変更を検出します。厳密な入力検証と決定的なレポートにより、デバッグ、レビュー、バージョン管理、CIでの適用に実用的です。