AIエージェントは、言語モデルが選択した行動を実際のツールで実行し、作業を継続するプログラムです。この際、安全装置は「してはいけない」という文言を表示するだけでなく、許可されていないリクエストを実行境界で停止させる必要があります。停止したというログと、相手サーバーにリクエストが到達しなかったという証拠は異なります。
ARTEXの動作構造とハーネス・ガードレールの役割を検討したことがあるなら、次の質問は小規模で実験できます。禁止されたリクエストは実際に出ないのか、失敗しても呼び出し制限を使うのか、中断後に次のリクエストが続かないのか、という点です。
今回は、Python標準ライブラリのみでローカルHTTPサーバーとツールを一つ作成し、直接確認しました。これは外部APIや実際のLLMを使用しない、ツール実行境界の決定的な試験です。ARTEX全体やLLMの自動試験が成功したという意味ではありません。観測日は2026年10月10日で、実行環境はmacOSのPython 3.9.6です。

1. まず「合格」の意味を定義する
サーバーは127.0.0.1の一時ポートからのみリクエストを受け付けます。業務データの代わりに、固定された応答とメモリ内の受信カウンターを使用します。サーバーが実際のリクエストを読み取ったときにカウンターを増やすため、ツールが返した状態とは別に到達の有無を確認できます。
最初は、正常なリクエストが一度到着し、HTTP 200を受け取るかを確認します。正常なリクエストもブロックするプログラムは、禁止リクエスト0回を示す可能性があるためです。この正常な対照を通過した後、以下の失敗を注入します。
| 試験 | 注入する条件 | 合格判定 |
| 禁止リクエスト | リストにないIDとURL形式の入力 | すべて拒否、サーバー受信増加0 |
| リダイレクト | 許可されたパスが/forbiddenへ302応答 | 最初のリクエストのみ受信、宛先は0 |
| 失敗・再試行 | 503応答を繰り返し、試行予算2 | 2回受信、3回目の送信をブロック |
| 通信失敗 | リクエスト受信後、応答なしで接続終了 | 予算1を消費、次の送信をブロック |
| リクエストタイムアウト | 応答を保留、リクエスト制限0.25秒 | 接続クリーンアップ、次の呼び出しをブロック |
| 全体タイムアウト | リクエスト制限2秒、全体監督制限0.35秒 | 実行タスク終了、次の呼び出しをブロック |
| 明示的キャンセル | サーバー受信確認後、実行キャンセル | キャンセル完了、追加受信0 |
ここでの予算は、送信を開始しようとする許可された呼び出し試行回数です。ポリシーで拒否された入力は送信試行を開始しないため、この予算を使いません。リクエストを開始した後に失敗した試行は返還されません。コスト・トークン・同時実行数は今回測定した予算ではありません。
2. ツールにURL選択権を与えない
ツールの入力はURLではなく、OK、FAIL、SLOW_CANCELのような事例IDです。コード内のリストがIDを固定されたパスに変換し、接続先は常に実習サーバーの127.0.0.1と一時ポートです。入力値でホストやポートを変更する経路はありません。
CASES = {
"OK": "/ok", "FAIL": "/fail", "DROP": "/drop",
"REDIRECT": "/redirect", "SLOW_REQUEST": "/slow-request",
"SLOW_GLOBAL": "/slow-global", "SLOW_CANCEL": "/slow-cancel",
}
この選択は、汎用URLツールを安全に完成させたという意味ではありません。実習で必要とされない入力の自由度を排除したものです。OWASPも過度な機能・権限・自律性を危険原因として説明し、任意のコマンドやURLを受け取るツールよりも、必要な機能を狭く提供する方式を推奨しています。 OWASP LLM06:2025 Excessive Agency
リダイレクトも同じ境界です。最初にURLだけを確認した後、応答のLocationを自動的に追跡すると、最初に許可されていなかった宛先に移動する可能性があります。実習の小さなHTTPクライアントは、3xxステータスをredirect_blockedとして返し、2回目のリクエストを作成しません。実際に/redirectは1回、/forbiddenは0回受信されました。
このリストはプログラム内部の統制です。他のコードが直接ソケットを開いたり、他のツールを呼び出したりする状況まで防ぐオペレーティングシステムの隔離装置ではありません。実際のエージェントに接続するには、すべての実行パスが統制を経由するかどうかを別途確認する必要があります。
3. 失敗する前に予算を消費する
予算を成功回数で計算すると、常に失敗するツールが上限に達しない可能性があります。今回の実装では、送信前に試行を予約し、エラーやタイムアウトが発生しても差し引かれた値を返還しません。以下は、添付の実習ファイルの実際の呼び出し境界です。
async def call(self, case_id):
if self.stopped is not None:
return "stopped"
if case_id not in CASES:
return "forbidden"
if self.spent >= self.budget:
return "budget_exhausted"
self.spent += 1 # I/O前に減算、失敗しても返金なし
try:
return await asyncio.wait_for(
self.exchange(CASES[case_id]), self.request_timeout)
except asyncio.TimeoutError:
self.stop("request_timeout")
return "request_timeout"
except asyncio.CancelledError:
self.stop("cancelled")
raise
except (OSError, asyncio.IncompleteReadError,
asyncio.LimitOverrunError):
return "transport_error"
503を返す事例を3回呼び出そうとしたところ、結果はhttp_503 → http_503 → budget_exhaustedでした。サーバーは2回受信し、3回目のリクエストは作成されませんでした。応答なしで接続を切断する事例も、1回の失敗で予算1を使い果たしました。これは失敗記録だけでなく、受信数と使用した試行数を合わせて確認した結果です。
実習にはHTTPライブラリの隠れた再試行はありません。呼び出しの繰り返しを担当するplan()が失敗後に同じツールを再度呼び出し、毎回同じ予算チェックを経ます。著者の推奨は、実際のシステムでも再試行が発生する層を見つけ、同じ試行予算に接続することです。SDK内部の再試行と上位実行ループの再試行が別々に動くと、上位呼び出し数だけでは実際の送信数を知ることはできません。
4. リクエスト制限と全体監督制限を分ける
1つのリクエストの応答を待つ時間と、計画全体が実行される時間は異なる制限です。リクエストごとに制限を新たに開始すると、複数のリクエストを続ける総時間は長くなる可能性があります。今回の実装では、接続・送信・応答読み取りを1つのコルーチンにまとめ、リクエスト制限を適用し、別途の監督関数が計画全体を待ちます。
リクエスト制限試験では、サーバーが応答を保留しました。0.25秒の制限で呼び出しは約0.253秒でrequest_timeoutとして終了し、同じツールの次の呼び出しはstoppedでした。これは、この実習でリクエストタイムアウト後に作業を中断するように選択されたポリシーです。すべてのサービスがタイムアウトごとに全体作業を中断しなければならないという仕様ではありません。
全体監督試験では、リクエスト制限を2秒に増やし、監督制限を0.35秒に設定しました。リクエスト制限よりも早く、約0.352秒でsupervisor_timeoutとして終了しました。監督関数は、まず中断状態を記録し、実行タスクをキャンセルした後、終了するまで待ちます。
tool.stop("supervisor_timeout")
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
cancel()を呼び出したという事実だけで実行が終了したとは判断しません。ツール内ではCancelledErrorを再度発生させ、接続はfinallyで閉じます。監督者はその整理が完了した後に戻ります。Pythonドキュメントも、タスクがキャンセル例外を飲み込む可能性があり、wait_for()がキャンセル完了を待つために指定された時間よりも長くかかる場合があると説明しています。上記の測定値は観測結果であり、絶対的な終了時刻を保証するものではありません。 Python タスクのキャンセルとタイムアウト
このコードは単一のイベントループでツールを順次呼び出します。並列ワーカー、実行を妨げる同期コード、キャンセルを無視するライブラリは試験していません。その環境では、共有予算の同時減算と、すでに開始されたリクエストの境界をさらに検証する必要があります。
5. キャンセルはすでに受信したリクエストを元に戻さない
明示的キャンセル試験は、リクエストがサーバーに到着したというイベントをまず待ちます。次に、新規呼び出しをブロックする中断状態を記録し、実行タスクをキャンセルします。タスクのcancelled()が真であるか、次の正常な呼び出しが拒否されるか、受信数が増加しないかを確認します。
この3つの条件は通過しました。しかし、サーバーで保留中だった処理を解放すると、クライアントキャンセル後もサーバーの合成効果カウンターが1増加しました。リクエストタイムアウトと全体タイムアウト試験でも同じ効果が残りました。効果はメモリ内の数値増加のみであり、ファイルや外部リソースは変更されていません。
したがって、ここで確認された「停止」は、クライアントの実行を終了し、それ以降のツール呼び出しを送信しないことです。相手サーバーがすでに受け取った作業をキャンセルしたり、元に戻したりしたという保証ではありません。キャンセル時点より前に開始されたリクエストが後で到着する競合状況全体を立証した試験でもありません。
実際の変更作業を扱う場合、著者の推奨は、サーバー側の作業キャンセルサポート、重複効果を防ぐべき等性キー、処理結果の照会と必要に応じた補償作業を別途設計することです。これらの機能は今回の実習では実装または検証されていません。
シェルコマンドでツールを実装する場合も境界が広がります。subprocess.run(timeout=…)は、タイムアウト時に自身が開始した子プロセスを終了し、待機する動作を説明します。これだけで、すべての子孫プロセスやすでに送信したリクエストの効果まで消滅すると解釈することはできません。また、Popen.communicate(timeout=…)はタイムアウトだけでは子プロセスを終了しないため、別途整理が必要です。今回の実習ではサブプロセスを作成しておらず、プロセスツリーの終了も試験していません。 Python subprocess 公式ドキュメント
6. 小さなファイル一つで再現し、結果を読み取る
添付の agent_safety_lab.py を空の実習ディレクトリに保存して実行します。Python 3.9以上とローカルソケットを開ける実行環境が必要で、パッケージのインストール、アカウント、APIキーは不要です。URLや対象サーバーを入力するオプションもありません。
python3 --version
python3 agent_safety_lab.py --output results.json
今回保存した実際の出力は以下の通りです。
PASS allowed_positive_control
PASS forbidden_no_reception
PASS redirect_not_followed
PASS http_failures_retries_spend_budget
PASS transport_failure_spends_budget
PASS request_timeout_stops_new_calls
PASS supervisor_timeout_drains_execution
PASS cancel_blocks_new_calls_without_rollback
PASS cleanup
checks=9 passed=9 total_received=8
正常な対照と整理を含め、9つの検査すべてが合格し、全体のサーバー受信は8回でした。終了後、待機タスク0個、サーバー処理タスク0個、開いているクライアント接続0個、およびリスニングソケットの閉鎖を確認しました。実習が生成したサブプロセスは0個であり、実行に使用した一時作業ディレクトリも削除しました。原稿とコード、測定JSONはレビューのために保存しました。
実行がブロックされた場合、まずエラーの位置を確認します。ローカルポートのバインディングが拒否された場合、サーバーが起動していないため、ブロック試験の成功とは数えません。今回の最初の実行も実行サンドボックスがバインディングをブロックし、承認されたローカル実習実行範囲で再実行した後に上記の結果を得ました。セキュリティ設定を変更したり、外部サーバーに置き換えたりはしていません。他の環境では測定時間が異なる場合があり、FAILや例外が発生した場合はその結果を保存し、原因を確認します。
次に実際のエージェントを検証する際には、このファイルの合格結果をそのまま採用するのではなく、そのエージェントのすべてのツール・再試行・中断経路に同じ判定基準を適用する必要があります。拒否結果、実際の受信数、予算の枯渇、実行終了を合わせて確認することで、安全装置がどの境界で機能したかを説明できます。
提案カテゴリ: 一般IT/AI
提案タグ: AIエージェント, ガードレール, Python, ツール実行, 呼び出し予算, タイムアウト, キャンセル
コメントを残す