Managed Agentsのツール確認リクエストに応答する実装
Managed Agentsでalways_askのツールが呼ばれたときのイベントの流れと、user.tool_confirmationで許可・拒否を返す実装のポイントをまとめます。
権限ポリシーがalways_askのツールをエージェントが呼び出すと、セッションはrequires_actionで一時停止し、応答が届くまで無期限に待ち続けます。再開させるにはuser.tool_confirmationイベントを送り、resultにallowかdenyを指定します。この一連の流れをアプリケーション側でどう実装するかが、確認リクエスト対応の実務上の要点です。
前提
以降の実装例は、次の3点が揃っている状態を前提にしています。
- APIリクエストには
anthropic-beta: managed-agents-2026-04-01ヘッダーが必須です(SDKを使う場合は自動付与されます)。 - 対象のセッションは既に作成済みで、エージェントツールセット(
agent_toolset_20260401)またはMCPツールセットのいずれかが有効になっています。 - 対象ツールの権限ポリシーが
always_askに設定済みです。エージェントツールセットのデフォルトはalways_allow、MCPツールセットのデフォルトはalways_askで、ツールセット単位のデフォルトとは別に、個別ツール単位で上書きできます。エージェントツールセットではconfigs配列にname(利用可能なツール名の固定リストから選ぶ)とpermission_policyを指定し、たとえば「ツール全体はalways_allowのままbashだけalways_askにする」といった設定が可能です。MCPツールセットも同じconfigs構造で上書きでき、nameにはMCPサーバー側が報告するツール名をそのまま使います。名前が一致しないと上書きは反映されず、デフォルトのポリシーのまま動くので注意してください。
権限ポリシーの変更はエージェント側の設定更新であり、更新後に作成したセッションから反映されます。既に動いているセッションはツールセット構成を作成時点のまま保持し続け、ポリシーを更新しても遡って適用されません。稼働中セッションの挙動を変えたい場合は、そのセッションを終了して新しいセッションを作り直す必要があります。
権限ポリシーはあくまで「有効なツールをいつ実行するか」を制御するもので、ツールをエージェントから完全に外したい場合は権限ポリシーではなく無効化(disable)を使います。always_askにしても、そのツール自体は依然としてエージェントの選択肢として見えており、呼び出されるたびに確認が挟まるだけです。ツールの存在そのものを消したいのか、呼び出しに人の判断を挟みたいのかで、設定すべき項目が変わる点は区別しておく必要があります。
always_askのツールが呼ばれてから止まるまで
always_askポリシーのツールが呼び出されると、次の順序でイベントが発生します。
- セッションが
agent.tool_use(エージェントツールセットの場合)またはagent.mcp_tool_use(MCPツールセットの場合)イベントを発行する - セッションが
session.status_idleイベントで一時停止する。このstop_reason.typeはrequires_actionで、承認待ちのイベントIDがstop_reason.event_ids配列に入っている - アプリケーション側が、待たれているイベントIDごとに
user.tool_confirmationイベントを送る - 承認待ちのイベントがすべて解決されると、セッションは
runningに戻る。許可したツールは実行され、拒否したツールは実行されずに、エージェントには拒否理由を含むツール結果が渡される
セッションは応答が来るまで無期限に待機します。タイムアウトで自動的にどちらかへ倒れることはないため、通知の仕組みを用意しない限り、承認待ちのセッションは人が気づくまで止まったままになります。
承認待ちのイベントIDをどう受け取るか
承認が必要なツール呼び出しのイベントIDは、session.status_idleイベントのstop_reason.event_ids配列から取得します。取得経路は2つあります。イベントストリームを開きっぱなしにしてsession.status_idleイベントをその場で読む方法と、イベントストリームを常時購読しない構成向けにwebhookでセッションの一時停止だけを通知させる方法です。
webhookを使う場合、届くsession.status_idledイベント(ストリーム側のsession.status_idleとは名前が異なる点に注意)のペイロードにはセッションIDと組織・ワークスペースIDしか入っておらず、stop_reasonそのものは含まれません。つまりwebhookはあくまで「セッションが止まったので確認しに行け」という起点であり、実際にrequires_actionなのか、それとも新しいメッセージ待ちのend_turnなのかは、通知を受けてからイベントストリームを取得するかセッションを取得し直して判断する形になります。webhookの配信は順序を保証しないため、状態判定は届いたイベントの順番ではなく、取得し直した最新の状態を基準にする必要があります。
複数のツール呼び出しが同時に承認待ちになっているケースでは、stop_reason.event_idsに複数のIDが並びます。この場合、1回のeventsリクエストにまとめて複数のuser.tool_confirmationを含めて送信できます。1件ずつ個別のリクエストに分ける必要はありません。
許可・拒否を実装する
許可するときはresultに"allow"を指定するだけです。
curl -fsSL "https://api.anthropic.com/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{
"events": [
{
"type": "user.tool_confirmation",
"tool_use_id": "'$AGENT_TOOL_USE_EVENT_ID'",
"result": "allow"
}
]
}'拒否する場合はresultを"deny"にし、deny_messageで理由を添えます。このメッセージはそのままエージェントへのツール結果に載るため、次にどう振る舞ってほしいかを含めておくと、エージェントが同じツール呼び出しを繰り返さずに代替手段へ切り替えやすくなります。
curl -fsSL "https://api.anthropic.com/v1/sessions/$SESSION_ID/events" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d '{
"events": [
{
"type": "user.tool_confirmation",
"tool_use_id": "'$MCP_TOOL_USE_EVENT_ID'",
"result": "deny",
"deny_message": "本番プロジェクトへのIssue作成は禁止です。ステージング側を使ってください。"
}
]
}'deny_messageは必須ではありませんが、空のまま拒否だけ返すと、エージェントは「なぜ拒否されたか」を推測するしかなくなります。特に人が判断して拒否したケースでは、判断理由を1文で添えるだけで後続の振る舞いが安定します。
ストリーミングループで自動応答する実装
イベントストリームを開きっぱなしにして、その場でsession.status_idleを監視する実装パターンも一般的です。stop_reason.typeがrequires_actionならイベントIDごとに承認を送り、end_turnならループを抜けます。この2つのstop_reason.typeを分岐で扱うのが実装の骨格になります。
exec {stream_fd}< <(curl --fail-with-body -sS -N \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events/stream?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-H "accept: text/event-stream")
while IFS= read -r -u "$stream_fd" line; do
[[ $line == data:* ]] || continue
event_json="${line#data: }"
stop_reason=$(jq -r 'select(.type == "session.status_idle") | .stop_reason.type // empty' <<<"$event_json")
case "$stop_reason" in
requires_action)
while IFS= read -r event_id; do
jq -n --arg id "$event_id" \
'{events: [{type: "user.tool_confirmation", tool_use_id: $id, result: "allow"}]}' |
curl --fail-with-body -sS \
"https://api.anthropic.com/v1/sessions/$SESSION_ID/events?beta=true" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: managed-agents-2026-04-01" \
-H "content-type: application/json" \
-d @-
done < <(jq -r '.stop_reason.event_ids[]' <<<"$event_json")
;;
end_turn)
break
;;
esac
done
exec {stream_fd}<&-この例はすべて自動許可(result: "allow")にしていますが、実運用ではevent_jsonからツール名や引数を取り出し、ルールに合致したものだけ許可してそれ以外はdenyにする、という条件分岐をrequires_actionブロックの中に足す形になります。end_turnはエージェントが自発的にターンを終えたことを示す別のstop_reason.typeで、承認待ちとは無関係にループを終了させる合図として扱います。この2つを混同すると、本来承認を待つべき場面でループが先に終了してしまい、session.status_idleのまま応答が届かない状態を見逃すことになります。
実装で判断が割れやすいポイント
user.tool_confirmationをどこで発行するかは、人間の承認を挟むUIを作る場合と、ルールベースで自動判定する場合で実装の重心が変わります。人間の承認を挟む場合は、agent.tool_use / agent.mcp_tool_useイベントの内容(どのツールに何の引数を渡そうとしているか)をそのまま画面に出し、ボタン操作をallow / denyに変換するだけの薄い層になります。ルールベースで自動判定する場合は、ツール名や引数の内容を見て許可・拒否をコード側で決定し、denyのときだけ理由をテンプレート化してdeny_messageに詰めるという構成になります。
どちらの実装でも共通して注意すべきなのは、カスタムツールはこの仕組みの対象外だという点です。カスタムツールの呼び出しはagent.custom_tool_useイベントとして届き、実行するかどうかの判断と結果の返却(user.custom_tool_result)はアプリケーション側の別ロジックで行います。user.tool_confirmationとuser.custom_tool_resultを混同して同じハンドラで処理しようとすると、イベント型の不一致でエラーになります。
よくあるつまずき
ポリシーを更新したのに古いセッションが反応しない。前提で触れたとおり、稼働中のセッションは作成時点のツールセット構成をそのまま保持します。ポリシー変更のテストは新しいセッションを作り直して行う癖をつけてください。既存セッションで挙動が変わらないのは不具合ではなく仕様です。
MCPツールを追加したら急に止まるようになった。MCPツールセットのデフォルトポリシーはalways_askです。信頼しているMCPサーバーであっても、新しく追加されたツールは個別にalways_allowへ上書きしない限り確認待ちになります。ツールが増えるたびに承認フローが増えるのは、意図的な安全側デフォルトです。
configsで個別ツールを上書きしたのに反映されない。エージェントツールセットのnameは利用可能なツール名の固定リストから、MCPツールセットのnameはMCPサーバーが報告するツール名から、それぞれ正確な文字列を指定する必要があります。綴りが一致しないとそのツールはツールセットのデフォルトポリシーのまま動き、上書きは黙って無視されます。
予算上限と承認待ちが同時に起きたときの分岐を誤る。セッションにbudgetを設定している場合、コストが上限に達するとstop_reason: budget_reachedでも一時停止します。ただし、あるスレッドが承認待ちで別のスレッドが予算上限で止まっているときは、セッション全体のstop_reasonはbudget_reachedではなくrequires_actionになります。承認待ちの解消はモデルへの新しいリクエストを発生させないため、これはいつもどおりuser.tool_confirmationで応答すればよく、budget側の特別扱いは不要です。ここをbudget_reachedだけを見て分岐すると、承認待ちのセッションを予算超過と誤認して放置してしまいます。
webhookの通知だけで確認待ちだと判断してしまう。webhookのペイロードにはstop_reasonが入っていないため、通知が届いた時点では確認待ちなのか単なる新規メッセージ待ちなのか区別できません。通知はあくまで「見に行くきっかけ」で、実際の判定にはイベントストリームかセッションの再取得が必要です。
まとめ
always_askのツールが呼ばれるとセッションはrequires_actionで無期限に止まり、stop_reason.event_idsに並ぶイベントIDそれぞれへuser.tool_confirmationを送るまで再開しません。許可はresult: "allow"、拒否はresult: "deny"とdeny_messageの組み合わせで実装し、複数の承認待ちは1回のリクエストにまとめて送れます。カスタムツールはagent.custom_tool_use / user.custom_tool_resultという別のイベント対で扱われる点が、実装時に混同しやすい境界線です。Managed Agents全体の構成はAgent SDKのManaged Agentsの設計思想、カスタムツールの実装方法はAgent SDKカスタムツールの作り方で扱っています。