Claude Media
Managed Agentsのツール確認リクエストに応答する実装

Managed Agentsのツール確認リクエストに応答する実装

Managed Agentsでalways_askのツールが呼ばれたときのイベントの流れと、user.tool_confirmationで許可・拒否を返す実装のポイントをまとめます。

ツールの権限ポリシーがalways_askだと、エージェントがそのツールを呼んだ時点でセッションはrequires_actionで止まり、応答が届くまで無期限に待ちます。再開するにはuser.tool_confirmationイベントを送り、resultにallowかdenyを指定します。autoポリシーでも、サーバーが判断を下せなかった呼び出しは同じ待ち状態に入ります。この待ち状態をアプリケーション側でどう受け止めて返すかが、確認リクエスト対応の実装の中身です。

前提

以降の実装例は、次の3点が揃っている状態を前提にしています。

  • APIリクエストにはanthropic-beta: managed-agents-2026-04-01ヘッダーが必須です(SDKを使う場合は自動付与されます)。
  • 対象のセッションは作成済みで、エージェントツールセット(agent_toolset_20260401)かMCPツールセットが有効になっています。
  • 対象ツールの権限ポリシーがalways_ask(またはauto)です。

ポリシーの既定値はツールセットごとに違います。エージェントツールセットはalways_allow、MCPツールセットはalways_askで、autoはどのツールセットでも既定になりません。ツール単位で変えるにはconfigs配列にnameとpermission_policyを書きます。たとえばツールセット全体をalways_allowにしたままbashだけalways_askにできます。MCPツールセットでも構造は同じで、nameにはMCPサーバーが報告するツール名を入れます。

権限ポリシーを更新しても、稼働中のセッションには反映されません。セッションは作成時点のツールセット構成を保持し、更新は更新後に作られたセッションにだけ適用されます。ポリシー変更の動作確認は、新しいセッションを作って行います。

ポリシーが決めるのは、有効なツールをいつ実行するかです。ツールをエージェントから外したいときは、ポリシーではなくツールの無効化(disable)を使います。

3つのポリシーで、人が呼ばれる場面はどう違うか

人の確認が入るかどうかは、ポリシーによって次のように分かれます。

くらべる

always_askとautoの違い

毎回止まる

always_ask

対象ツールの呼び出しは、例外なくセッションを止めて承認を待ちます。人が見る前に実行されることはありません。

サーバーが振り分ける

auto

サーバーがツール・入力・それまでのセッション内容を見て、実行・拒否・承認待ちのどれかを選びます。拒否は人の確認を経ずに確定し、クライアントからは覆せません。

autoはあくまで自動判定で、人間のチェックポイントではありません。サーバーが安全と判断した呼び出しは、誰かが見る前に実行され、その影響は元に戻せないこともあります。user.messageで送った内容は利用者の意図として扱われるため、エンドユーザーの入力をそのまま中継する構成では、その入力が呼び出しを通す材料にもなります。人のレビューを必須にしたいツールにはalways_askを指定します。

承認待ちが発生してから止まるまで

always_askのツール、またはautoで判断がつかなかったツールが呼ばれると、次の順序で進みます。

手順

承認待ちのイベントの流れ

  1. 1

    ツール呼び出しのイベントが出る

    エージェントツールセットならagent.tool_use、MCPツールセットならagent.mcp_tool_useが発行されます。

  2. 2

    セッションが止まる

    session.status_idleイベントが出て、stop_reason.typeはrequires_actionになります。承認待ちのイベントIDはstop_reason.event_ids配列に入っています。

  3. 3

    IDごとに応答を送る

    待たれているイベントIDそれぞれについて、tool_use_idにそのIDを入れたuser.tool_confirmationを送ります。

  4. 4

    セッションが再開する

    承認待ちがすべて解決するとセッションはrunningに戻ります。許可したツールは実行され、拒否したツールは実行されず、エージェントには拒否理由を含むツール結果が渡されます。

セッションは応答が来るまで無期限に待機します。通知の仕組みがなければ、承認待ちのセッションは人が気づくまで止まったままです。

呼び出しの判定結果をイベントから読む

どのポリシーでも、agent.tool_useとagent.mcp_tool_useのイベントにはevaluated_permissionが付き、値は"allow"・"ask"・"deny"のどれかです。多くのイベントにはevaluationオブジェクトも付き、どのポリシーがその結果を出したかがtypeに入ります。

autoでbashが高リスクと判定されて拒否されたイベントは、たとえば次の形になります。

{
  "name": "bash",
  "input": { "command": "rm -rf /workspace/reports" },
  "evaluated_permission": "deny",
  "evaluation": {
    "type": "auto",
    "evaluated_permission": { "type": "deny", "reason_code": "high_risk" }
  }
}

evaluationの形は、always_allow・always_ask・autoの3種類で次のように変わります。

evaluation.typeトップレベルのevaluated_permission意味
always_allowトップレベルのevaluated_permission"allow"意味そのまま実行された
always_askトップレベルのevaluated_permission"ask"意味承認待ちで止まった
auto(内側がallow)トップレベルのevaluated_permission"allow"意味サーバーが安全と判断して実行した
auto(内側がask、reason_codeはindeterminate)トップレベルのevaluated_permission"ask"意味判断がつかず承認待ちで止まった
auto(内側がdeny、reason_codeはhigh_risk)トップレベルのevaluated_permission"deny"意味高リスクと判断して拒否した

承認待ちに応答するクライアントが見るべきなのは、evaluated_permissionが"ask"のイベントだけです。"ask"以外のイベントにuser.tool_confirmationを送ると、APIは400エラーで拒否します。autoが出した拒否もこの対象です。

reason_codeは、クライアントが分岐に使ったり監査記録に残したりするための値で、エンドユーザーに見せる文言ではありません。evaluationが付かないイベントが2種類あります。有効化されていないツールをエージェントが指名した場合はevaluated_permission: "deny"だけが付きます。evaluationの導入前に記録されたイベントでは、"allow"ならalways_allow、"ask"ならalways_askとして読みます。未知のevaluation.typeやreason_codeが来ても落ちないように書いておくと安全です。

承認待ちのイベントIDをどう受け取るか

イベントIDを受け取る経路は2つあります。イベントストリームを開きっぱなしにしてsession.status_idleをその場で読む方法と、ストリームを常時購読しない構成向けにwebhookで一時停止だけを通知させる方法です。

webhookで届くsession.status_idledは、ストリーム側のsession.status_idleとは名前が違います。ペイロードに入っているのはイベントの種類、セッションID、組織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は必須ではありませんが、空のまま拒否だけ返すと、エージェントは「なぜ拒否されたか」を推測するしかなくなります。deny_messageはツール結果に含まれるので、理由を添えるとエージェントが拒否の理由を受け取れます。

ターミナルから手で返す

実装を組む前に挙動を試すだけなら、antCLIから同じイベントを送れます。ant beta:sessions connect <セッションID>でセッションに接続すると、承認待ちのツール呼び出しで入力欄が「Allow tool call?」に変わります。「Yes」「No」「No, and tell the agent why」から選ぶと、CLIがuser.tool_confirmationとして送り、理由を入力すればdeny_messageに入ります。このコマンドはantCLI 1.32.0以降で使え、対話端末が必要です。スクリプトからはant beta:sessions:events sendでイベントを直接送ります。

ストリーミングループで自動応答する実装

イベントストリームを開きっぱなしにして、その場で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で、承認待ちとは無関係にループを終了させる合図として扱います。requires_actionをend_turnと同じ扱いでループから抜けると、承認待ちのまま応答を返さずにセッションを放置することになります。

実装で判断が割れやすいポイント

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で扱うのは、サーバーが実行するエージェントツールセットとMCPツールセットだけです。

よくあるつまずき

MCPツールを追加したら急に止まるようになった。MCPツールセットの既定はalways_askです。信頼しているMCPサーバーでも、default_config.permission_policyかconfigsでalways_allowにしない限り、新しいツールは確認待ちになります。MCPサーバー側にツールが増えても承認なしでは実行されない、という意図の既定です。

autoで拒否された呼び出しにuser.tool_confirmationを送ったら400になった。autoが出した拒否は確定していて、クライアントから許可に変えられません。応答を送る前にevaluated_permissionが"ask"であることを確かめます。

予算上限と承認待ちが同時に起きたときの分岐を誤る。予算付きのセッションは、上限に達するとbudget_reachedで一時停止します。ただし、あるスレッドが承認待ちで別のスレッドが予算上限で止まっているときは、セッション全体のstop_reasonはrequires_actionになります。承認待ちへの応答は予算上限中でも受け付けられ、結果は記録されます。ただし予算上限で止まったスレッドは、予算を変更するまで再開しません。budget_reachedだけを見て分岐すると、承認待ちのセッションを予算超過と取り違えて放置します。

まとめ

承認待ちに応答するコードは、stop_reason.typeがrequires_actionのときだけ動き、evaluated_permissionが"ask"のイベントIDにだけuser.tool_confirmationを返す形にしておくと、autoを混ぜても壊れません。人のレビューを必須にしたいツールはalways_askを選びます。Managed Agents全体の構成はAgent SDKのManaged Agentsの設計思想、カスタムツールの実装はAgent SDKカスタムツールの作り方で扱っています。

この記事を共有:XはてブLinkedIn