エージェントポリシーのパリティ
エージェントのポリシー — 呼び出し可能なツール、承認を求めなければならないタイミング、ランタイム予算、メモリのスコープ、スキル — は、ターンを組み立てて実行するどのコードパスであっても同一に適用されなければなりません。ZeroClaw は複数の異なる構築パスを通じてターンを構築し、歴史的に各パスがポリシー自体を適用していました。同じポリシーが複数の場所で再導出される場合、あるパスで尊重された設定が、別のパスでは静かにスキップされることがあります。
#8120(あるエージェントの MCP ツールが別のエージェントのセッションに現れる問題)は、そのような相違の一例でした。チャネルパスが適用していたエージェントごとのツールスコープが、別の構築パスでは欠落していたのです。エージェントポリシーの同等性検証ハーネスは、この種のバグをリリース前に可視化するために存在し、それが構築される基盤(#8156)は、構造上そうしたバグを発生し得ないものにするために存在します。
構築パス
ターンのエンジン入力(ツールレジストリ、承認マネージャー、解決済みランタイムノブ)は、複数の異なる箇所で組み立てられます:
| パス | エンジン入力をビルドする場所 |
|---|---|
| チャンネル | チャンネルオーケストレーター |
| RPC | Agent構造体(from_config / turn) |
| ゲートウェイ | ゲートウェイサーバー |
loop_::run | 非対話的な実行: cron ジョブ、デーモンのハートビート、サブエージェントの生成 |
| Delegate | サブエージェントへの委任 |
| SOP のライブなネストされたステップ | drive_live_sop_actions: 別のエージェントに委譲するステップが、そのエージェントのエンジン入力をインフライトで再組み立てする |
各パスは、同じエージェント設定に対して同じポリシーをエンジンに渡さなければなりません。パリティハーネスはまさにそれをアサートします:あるパスで適用された設定は、すべてのパスで適用されます。SOP ライブネストステップパスは、すでに実行中のターン内のサブターンです:ステップが異なるエージェントを指定した場合、その完全な実行コントラクトは、親ターンから継承されるのではなく、同じシームを通じて再アセンブルされます――ゲート済みツール、セキュリティポリシー、MCP スコープ、プロバイダーバインディングと温度パラメーター、解決済みランタイムコントロール、および親サーフェスのインタラクティビティモード下でステップエージェントのリスクプロファイルを持つ承認マネージャー。ステップは明示的な子トランスクリプト上で実行され(独自のシステムプロンプトとステップコンテキストを持ち、親の会話はステップエージェントのプロバイダーに届きません)、そのレコードはステップエージェントを実行中のアイデンティティとして、委譲エージェントを親相関としてスタンプします。再アセンブルできないパスは、クロスエージェントステップクローズドで失敗します。
パリティ行列
各ポリシー設定と各構築パスについて、その設定は強制されるか、部分的に強制されるか、または適用されないかのいずれかです。(設定 x パス) の行列は監査済みの差異レコードであり、各設定がどこで適用され、どこで適用されないかを示し、ソースに対して検証されています。「ギャップ」セルとは、パスが省略する設定です。
プロジェクトの基本原則の下では、ギャップは欠陥であり、デフォルトではありません。省略は許可ではありません。 制限を適用しない構築パスは、エージェントの権限を偶発的に拡大してしまい、それはまさに #8120 が該当した失敗そのものです。
収束目標: 1つの解決シーム
構造的な修正は、パスごとにポリシーを再導出するのをやめることです。#8156 では ResolvedAgentExecution キャリアが導入されました。これはエンジンのエージェントごとの入力を1つのバンドルにまとめる、動作に影響を与えない再グループ化です(agent/turn/execution.rs)。この変更ではその ResolvedAgentExecution::resolve コンストラクタを追加し、すべての本番ターンパスをそれ経由でルーティングします(入力を ResolvedIo + ResolvedRuntimeKnobs レイヤーにグループ化)。これにより、バンドルは各サイトでインラインに組み立てられるのではなく、1つのシームで生成されます。現在 resolve() は解決済みの入力を展開します(動作に影響を与えない)。後続の対応 PR では、フィールドごとの解決(スコープ付きレジストリ経由のツール、承認、ランタイムノブ)をそこに移動し、入力を封印します。その解決と封印が整うことで:
- 設定が適用される場所は正確に1か所だけであるため、相違が生じることはありません。
- プライベートフィールドを持つnewtype(例えばリゾルバーのみが生成できるスコープ付きツールレジストリなど)を使用すると、未解決のポリシーをエンジンに渡すことがコンパイルエラーになります。
最終的な状態は、この乖離が単にテストで検証されるだけでなく、コンパイル不能になるというものです。現在と将来の境界: ResolvedAgentExecution、その resolve() コンストラクタ、および ResolvedIo / ResolvedRuntimeKnobs 入力レイヤーはすべて master 上に存在し、すべての本番パスはそれらを通じて構築されます。TOOL サーフェスにも、ゲート付きコンストラクタが用意されました(以下の ScopedToolRegistry::assemble で、ゲートウェイがその最初のコンシューマです)。残りのサーフェスのフィールドごとの解決を resolve() に吸収し、バンドルのフィールドをその背後に封印する作業は、後続のサーフェス PR で行われます。
ツールアセンブリのシーム(エピックA、最初のサーフェス)
エージェントごとのツールレジストリは、単一のゲート付きコンストラクタ ScopedToolRegistry::assemble(crates/zeroclaw-runtime/src/tools/scoped.rs)を備えた最初のサーフェスです。このレジストリは、これまで6箇所の構築サイトで手作業により組み立てられてきました。これが、組み込みフィルタとMCPスコープをサイトごとにパッチしなければならなかった理由です(#7064、#6960、#8120)。assemble は次の順序で適用します。エージェントの config.peripherals(接続時 - 以下のノブを参照)、組み込みの allowed_tools/excluded_tools フィルタ、ACPメモリストリップ、mcp_bundles ごとのMCPサーバースコープに加え、ツールごとのゲーティング(eager または deferred。省略は付与を意味しません)とMCP機能ツールおよびピン留めリソースのプロンプトセクション、そして同一の SecurityPolicy の下でのスキル登録(スキルのないサイトは空のスライスを渡します - Epic Fのローダー統合まではゲートウェイがそうします)。
サイトごとの差異はデータとして表現され、セキュリティ手順のスキップとしては決して表現されません。ScopedAssembly の調整項目は絞り込みまたは保留のみを行い、ポリシーが付与する権限を拡大できるものはひとつもありません:
caller_allowed- 実行ごとの許可リスト(run()パス)。ポリシーフィルターおよび MCP ツールアクセスポリシーと交差し、これらを上書きすることはありません。connect_mcp- ACPの高速起動パスではfalse:MCPサーバーは解決も接続もされないため、何も付与されません。connect_peripherals- リストのみを扱う面ではfalse:周辺機器を読み込むと物理的にハードウェアが接続され(シリアルポートの排他的な占有が発生する)、ターンを実行しないレジストリでは決して行ってはならない処理です。exclude_memory- ACPメモリツールストリップ。
カットオーバー状況(ストラングル、1サイトあたり1 PR): gateway (#8640)、loop_::run (#8700)、および process_message (#8701) は現在すべて assemble 経由で構築されます。gateway のカットオーバー — レジストリビルダー両方、dashboard-agent シード、およびエージェントごとの /api/tools 一覧 — は、構築方法そのものによって gateway のフィルタギャップを解消しました: 以前の一覧には、エージェントのポリシーが拒否する未フィルタのビルトインが表示され(ライブ gateway チャットは既にフィルタ済みの process_message 経由で解決)、さらにポリシーがすべての遅延 MCP ツールを拒否していても tool_search スタブが表示されていました。一覧の主張を正確に保つためのスコープ上の注記: ペリフェラルは設計により一覧から除外されています(connect_peripherals: false — ハードウェアに接続せずに列挙することは将来の改善事項です)。process_message のカットオーバーは、2つ目の独立した乖離を解消しました: 以前はビルトインを filter_channel_builtin_tools 経由でフィルタしていました。これは non-Full 自律度において、正規の読み取り専用デフォルトを allowed_tools を通過させてしまうバリアントであり、他のすべてのパスは通常の apply_policy_tool_filter を適用していました。#8701 がそのバリアントを廃止したため、現在すべてのパスが同じ通常のフィルタを適用します(台帳 A4、乖離の特徴付けではなく、ファイル内のポジティブなパリティテストで裏付け)。
残りの手作業による実装箇所 — チャネルオーケストレーター(start_channels)、Agent::from_config、そしてデリゲートの独立ターゲットビルダー(independent_agentic_tools_for_target、このプログラムの進行中に #8239 で追加されたもの — シールが終わらせようとしている再発そのもの)— は後続の PR で移行されます。すべての箇所が assemble を通じて生成されるようになると、エンジンの tools フィールドは ScopedToolRegistry(assemble のみが構築するプライベートフィールドの newtype)にシールされ、スコープなしのレジストリをエンジンに渡すこと — あるいは、クロスマージがチャネルパスに対して一度実際に行ったように、構築箇所を密かに再インライン化すること — は、レビューでの発見ではなくコンパイルエラーになります。そのシールがなされるまで、未移行の箇所についてのクロスサイトの一致は慣例として維持されます。この継ぎ目が今日保証しているのは、それを通じてルーティングされるすべてのパスが単一の実装を共有するということです。
ハーネス
パリティハーネスは crates/zeroclaw-runtime/src/agent/parity.rs に置かれており、#7415 の safety_net.rs ターンエンジンオラクルの #[cfg(test)] 兄弟として、そのフィクスチャを再利用しています。これはパリティ行の INDEX を保持しており、各行にはその所有者となるエピック、公開の追跡リファレンス、そしてそれを裏付けるテスト(あるいは追跡済み乖離レコード)が記されているほか、2 層のテストを備えています。このインデックスは、意図的にパスごとの判定グリッドを一切エンコードしていません。手書きセルからなる静的なグリッドは、それ自体がデータであり、別の PR がパスを変更した際に古くなってしまい、それに気づくテストも存在しないためです。これこそ、このプログラムが終わらせようとしている、まさにその失敗です。したがって、強制可能な主張はテストの中にのみ存在します。メタテストはインデックスの記録管理(所有者、追跡、証拠の存在)を強制するだけで、それ以上のことは行いません。人間が読める(設定 × パス)グリッドはこのページに置かれています。2 つのテスト層は次のとおりです。
- L1 エンジンロック: 設定が
run_tool_call_loopに到達すると、エンジンはそれを尊重します(例:excluded_toolsのエントリは、モデルが呼び出しても決して実行されません)。 - L2 path-parity は、設定があらゆる構築パスで同一に解決されることをアサートするものです。サーフェスがすでに1つのシーム経由で解決している場合、その L2 テストは肯定的なパリティアサーションです。確認済みの乖離にまだ単一のシームがない場合は、現状どおりの乖離を固定する常時実行の特性テストとして出荷されます(2つのパスが現在異なることをアサートする)——所有するエピックがセマンティクスを統一したとき、そのアサーションは同じ PR で失敗し、肯定的なパリティアサーションに書き換える必要があります。乖離は静かにではなく、必ず目立つ形でのみ変更できます。
#[ignore]されたスペックはありません。既知の失敗を無視したテストは CI で実行されず何も保護しないため、目標は現在の状態に対するライブアサーションとして担われます。
一度に1つのサーフェスを成長させ、他のどのテストもカバーしていないものだけをアサートします:
- サーフェス(ツール、承認、ランタイム予算、コンテキストと履歴、メモリ、スキル)は、1つのPRずつ
resolveにストラングルされます。 - そのPRは、サーフェスのパリティテストを追加します。1つのエージェント設定が与えられたとき、あらゆる構築パスがエンジンに対して同じ解決済みの設定値を渡します。
- プリミティブ自身のユニットテスト、または
safety_netエンジンオラクルで既にカバーされている振る舞いは再記述されません。ハーネスはクロスパスのパリティアサーションのみを追加します。これはプリミティブごとのテストが検証しない性質です。
サーフェスに単一の解決シームができるまでは、パリティを検証する対象が存在しないため、その行は早計にグリーンとなるテストではなく、常時実行される乖離の特性評価として乖離記録に残ります。上記の「無視されるスペックを設けない」ルールに従い、#[ignore] 付きのスペックとすることは決してありません。
サーフェスの追加(今後の各サーフェスPRが従うワークフロー)
resolve() を配置した状態で(上記参照)、各サーフェス PR は次の手順に従います。
- サーフェスの解決と配線を構築サイトから
ResolvedAgentExecution::resolveに移動し、サイトごとのコピーを削除する。 - パリティテストを追加する: 特徴的なエージェント設定を構築し、各構築パスを実行して、エンジンが同一の解決済みの値を受け取ることをアサートする。
- サーフェスの行をenforced-on-every-pathに切り替える。
- 他の場所では strangle を振る舞い中立に保つ:
safety_netオラクルとプリミティブ自身のユニットテストはグリーンのままである。