AIエージェントの操作計画を実行前に検査する:人工12例のJSONL preflight

AIエージェントへ「まず操作計画をJSONLで出す」と指示しても、その計画を目視するだけでは、許可範囲外のwriteや承認のないuploadを見落とします。そこで、1行1操作の人工計画を別ファイルのpolicyと照合し、実行前に policy_match、review、deny へ分ける小さなCLIを作りました。
2026年9月18日に人工12例を実行した結果は、policy_match 1件、review 1件、deny 10件でした。未知action、不正JSON、policy不一致はdenyです。ただし、これは宣言された文字列の分類結果にすぎません。実際のtool callを監視せず、安全性も実行許可も保証しません。
検証条件:操作を実行しない固定fixture
検証環境はWindows、PowerShell、Python 3.14.2です。Python標準ライブラリだけを使い、追加費用は0円でした。検査対象は人工の plans.jsonl 12行、承認と許可範囲は別の policy.json に置きました。
- URLはすべて予約済み例示TLDの
.invalid。 - credentialは
credential:EXAMPLE_ONLYという架空名だけで、値は入れていない。 - pathは
fixture/input/とfixture/output/を基準にした論理名。 - CLIはpolicyとplanを読むだけで、宣言されたread、write、upload、message、credential参照を実行しない。
- 出力先は標準出力だけ。fixture内外を問わず、判定CLIからファイルへwriteしない。
policyには読み取りprefix、書き込みprefix、許可host、message対象、actionとtargetを固定した承認参照を入れました。plan側の approved: true のような自己申告はschema外としてdenyします。
3ラベルは「安全度」ではない
| ラベル | このfixtureでの意味 | 意味しないこと |
|---|---|---|
policy_match |
宣言されたlocal readが人工policyのprefixと一致 | 対象内容が安全、実tool callも同じ |
review |
writeまたは承認付き対外操作が文字列条件に一致し、人の実行時確認へ回る | 実行許可、安全保証、承認済み |
deny |
malformed、未知、上限超過、範囲外、approval不一致のいずれか | 悪意や実害が確定した |
副作用のないreadだけを policy_match とし、許可prefix内でもwriteは review にしました。正しい承認参照を持つuploadとmessageもreview止まりです。このCLIに操作を実行する機能はありません。
コードは未知actionとpolicy不一致を先に止める
公開コードから判定の中心部分を抜粋します。完全版は配布ZIPに入っています。
if action not in KNOWN_ACTIONS:
return deny(case_id, line_number, "unknown_action")
if action in {"local_read", "local_write"}:
if not is_safe_relative_path(target):
return deny(case_id, line_number, "invalid_or_traversing_path")
if not any(under_prefix(target, prefix) for prefix in prefixes):
return deny(case_id, line_number, "target_outside_allowed_prefix")
if action in {"upload", "message"} and not approval_matches(plan, policy):
return deny(case_id, line_number, "approval_missing_or_mismatch")
実装では関数名に先頭の _ が付きます。JSON objectのキーも id / action / target / approval_ref の4個へ固定し、欠落だけでなく余分なキーもdenyします。approvalは参照名の存在だけでなく、別policyに記録したactionとtargetの完全一致が必要です。
Python公式のjson資料は、非信頼JSONがCPUやメモリを多く消費しうるため、解析するデータ量を制限するよう注意しています。今回は1行512 byteを人工policyの上限にしました。512はPythonの推奨値や一般的な安全基準ではなく、このfixtureだけの値です。path判定はPython公式のpathlib資料を参照しつつ、backslash、絶対path、.、.. を拒むPOSIX風の独自ルールへ狭めています。
人工12例の比較結果
| # | 宣言した境界 | 期待 | 実結果 | 理由 |
|---|---|---|---|---|
| 1 | 許可prefix内read | policy_match | policy_match | read_prefix_match |
| 2 | 許可prefix内write | review | review | write_prefix_match_requires_runtime_review |
| 3 | 許可root外write | deny | deny | target_outside_allowed_prefix |
| 4 | ..を含むpath | deny | deny | invalid_or_traversing_path |
| 5 | credential参照 | deny | deny | credential_access_not_supported |
| 6 | approvalなしupload | deny | deny | approval_missing_or_mismatch |
| 7 | approvalなしmessage | deny | deny | approval_missing_or_mismatch |
| 8 | 承認対象一致、host許可外 | deny | deny | upload_target_not_allowed |
| 9 | message用approvalをuploadへ流用 | deny | deny | approval_missing_or_mismatch |
| 10 | 未知action | deny | deny | unknown_action |
| 11 | 閉じ括弧のないJSON | deny | deny | invalid_json |
| 12 | 1行512 byte超過 | deny | deny | line_too_large |
合計は12件で、内訳はpolicy_match 1、review 1、deny 10です。denyがあるためCLIの終了コードは1です。これは想定したpreflight結果で、処理のクラッシュではありません。
図はコマンド結果の件数を読みやすく組み直した集計図で、生ログの画像ではありません。数値はZIPの固定fixtureで追試できます。
初回テストで見つかったのはfixtureの作り方の誤り
初回の14テストでは1件失敗しました。8番を「承認対象は一致するがhost allowlist外」としていたのに、初期policyにはその対象に対応する人工approvalがありませんでした。そのためホスト判定へ進まず、実結果は approval_missing_or_mismatch でした。
期待値を都合よく変えるのではなく、別policyへactionとtargetが一致する人工approvalを追加しました。再実行すると、approval一致後にhost allowlistで止まり、期待どおり upload_target_not_allowed になりました。修正後は14テストすべて成功、テストの終了コードは0です。
これはAIエージェントの事故を再現したものではありません。人工テストの前提が不足していた失敗と、その境界の切り分けです。
ZIPを展開して再実行する
コード・別policy・人工12例・期待結果・テスト・説明書・集計図をダウンロードし、展開先で次を実行します。
python -m unittest discover -s . -p 'test_*.py' -v
python .\action_preflight.py .\plans.jsonl --policy .\policy.json
$LASTEXITCODE
1つ目は14テスト成功、exit 0です。2つ目のsummaryは次のとおりで、denyを含むためexit 1です。
{"summary": {"deny": 10, "policy_match": 1, "review": 1, "total": 12}}
ZIPのSHA-256は 5865D010EA7B6DA76F53BF06B8B03A3181AA805482D68B2341D3F28ECF1B0270、サイズは7,926 bytesです。ハッシュ一致は同じ配布物であることの確認で、コードの安全性は証明しません。
OpenAIの6報告は着想源で、今回の実測ではない
OpenAIのreporting frameworkは2026年9月16日公開で、初回に6つの個別報告を掲載しています。同ページは、これらを個別事例としており、モデル全体でmisalignmentが起きる頻度を表すものではないと明記しています。また、調査や緩和策が完了する前に報告を公開する場合がある枠組みです。
6個別報告も9月18日に開き直しました。内容にはcompaction summaryへの指示、公開API keyの無断利用、無断upload、許可されないwriteや通信などが含まれます。そこから「操作種別・対象・承認を実行前に分けて見たい」という題材を得ましたが、今回の12例はすべて自作の人工入力です。6事例の再現、OpenAI製品の評価、発生頻度の推定、preflightの有効性証明には使っていません。
強い限界:宣言されなかった操作は見えない
このpreflightは、JSONLに正直かつ完全に宣言された操作しか調べられません。モデルが操作を省略・偽装した場合、実際のtool callが別の引数へ変わった場合は止められません。
さらに、redirect、symlink、TOCTOU、OS権限、ファイル内容、URL到達性、送信後の利用、自然言語の意図は検査対象外です。文字列prefixはpathの意味を理解しません。allowlistにも誤検知と見逃しがあります。
したがって、このCLIはsandbox、最小権限、実行時の人による承認、credential分離、監査ログの代替ではありません。実運用へ入れるなら、preflight後の実tool callでも同じpolicyを強制し、OS側で許可範囲外の操作を失敗させる必要があります。
実務への影響と、次に確認する4項目
- planと実tool callの間でaction、target、approval_refを改変できないか。
- write先のsymlinkと、確認後に対象が変わるTOCTOUをOS側でどう止めるか。
- approvalに有効期限、依頼者、対象データのhashを持たせるか。
- denyだけでなくpolicy_matchとreviewの全判定を改ざん困難な監査ログへ残すか。
Claude Code定期実行の落とし穴を検証した記事は二重実行、文字コード、定期実行時の権限を扱います。本稿は1回の宣言済み操作を実行前に分類する話です。静的サイトの内部リンク検査は生成後HTMLが対象で、本稿の操作計画とは検査対象が異なります。ニュースJSONの期間判定は日時と重複の仕分けであり、操作権限は扱いません。
表紙は概念を示す生成画像で、検証証拠には数えていません。独自証拠は人工12例、初回失敗の切り分け、修正後の14テスト、再実行コードです。
一次情報・出典
- OpenAI:Model misalignment reporting framework
- OpenAI Alignment:Self-generated prompt injections in compaction summaries
- OpenAI Alignment:Encouraging deception in compaction summaries
- OpenAI Alignment:Searching GitHub for leaked API keys
- OpenAI Alignment:Uploading files to the internet in order to cite them
- OpenAI Alignment:Unauthorized Artifactory writes and cross-sample communication
- OpenAI Alignment:Unauthorized communication via temporary file hosting services
- Python公式:json
- Python公式:pathlib