Inference hooks インテグレーションを開発する
署名付きの Inference hooks リクエストを受信し、検証し、allow または deny の判定を返す AI セキュリティサーバーを構築します。
Inference hooks インテグレーションとは、AI セキュリティサーバー、つまり Anthropic が呼び出す HTTPS サービスです。ガバナンス対象の各リクエストについて、サーバーは会話のトランスクリプトを含む署名付きの POST を受信し、allow または deny の「verdict」(判定)で応答します。このページでは、そのサーバーを構築するためのプロトコル、すなわちリクエストと判定のスキーマ、署名検証、および運用上の契約について説明します。
Inference hooks を有効にしてエンドポイントを指定するには、Inference hooks を設定するを参照してください。Inference hooks とは何か、いつ使用すべきかについては、Inference hooks の概要を参照してください。
最初の判定ラウンドトリップを実現する
最小限の動作するインテグレーションは、各リクエストを読み取って許可するサーバーです。以下のサーバーのいずれかを実行し、公開された https:// URL で公開します(たとえば、リバーストンネルサービスではなく、自分が管理するホスト上の TLS 終端リバースプロキシの背後に配置します。リクエストを受信するを参照してください)。その後、管理者にエンドポイントとして設定し、接続をテストしてもらいます。Test connection の結果には、サーバーが返した allow 判定が報告されます。
# 実行方法: python server.py
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
class VerdictHandler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1" # keep the connection open between verdicts
def do_POST(self):
# ボディを読み切ります。トランスクリプトは数メガバイトになることがあります。
self.rfile.read(int(self.headers.get("Content-Length", 0)))
verdict = b'{"action": "allow"}'
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(verdict)))
self.end_headers()
self.wfile.write(verdict)
ThreadingHTTPServer(("", 8000), VerdictHandler).serve_forever()リクエストを受信する
Anthropic は、管理者が設定した URL に HTTPS POST を送信します。設定された URL 全体がエンドポイントです。固定のパスサフィックスはないため、サーバーに適した任意のパスを選択してください。
AI セキュリティサーバーは Anthropic が到達できる場所にホストしてください。ポート 443 の https:// URL で、パブリックにルーティング可能なホスト上にあり(プライベート、ループバック、キャリアグレード NAT の範囲は接続時に拒否されます)、パブリック CA トラストストアに対して検証可能な証明書を持ち、リダイレクトなしで応答する必要があります。設定された URL は最終的な宛先でなければなりません。リバーストンネルホスト(ngrok や類似のトンネルサービス)はサポートされていません。Anthropic のネットワークポリシーによってブロックされます。自分が管理するドメインでサーバーをホストしてください。管理者が URL を設定してテストする方法については、Inference hooks を設定するで説明しています。
すべてのリクエストには、以下の固定ヘッダーに加えて、管理者が設定したカスタムリクエストヘッダー、および組織に署名シークレットが存在する場合は署名を検証するで説明する webhook-* 署名ヘッダーが含まれます。
| ヘッダー | 値 |
|---|---|
Content-Type | application/json |
User-Agent | anthropic-dlp/1 |
Accept-Encoding | identity |
現在、フックイベントは 1 つだけです。プロンプトフレームは、ガバナンス対象の推論リクエストごとに 1 回、推論が開始される前に送信されます。Anthropic は、AI セキュリティサーバーが応答するか、判定タイムアウトが経過するまでリクエストを保留します。
プロンプトフレーム
リクエストボディは、以下のフィールドを持つ JSON オブジェクトです。
| フィールド | 型 | 説明 |
|---|---|---|
type | string | フックイベント。現在は常に "prompt" です。将来的に他のイベントタイプが導入されるため、認識できない値は適切に処理してください(前方互換性を参照)。 |
request_id | string | 相関付けのための、推論呼び出しごとの不透明な識別子。webhook-id ヘッダーと等しくなります。 |
tenant_id | string または null | リクエストが属する組織の不透明な識別子。 |
actor | object | リクエストが帰属するプリンシパル。type で判別されます(現在送信される値は "user" のみ)。id(タグ付き識別子で、同じアカウントのリクエスト間で安定)と email_address(利用可能な場合)を持ちます。id と email_address はどちらも null になり得ます。 |
source | object | 発信元アプリケーション。application(source の値を参照)。 |
messages | array | 推論時点までの会話トランスクリプト。コンテンツブロックを参照してください。 |
session_id | string または null | 存在する場合の不透明な会話識別子。パースしないでください。Claude Code の場合、これはベストエフォートでクライアントが主張するセッション識別子です。 |
model | string または null | 利用可能な場合の、このリクエストの公開モデル識別子。 |
metadata | object | 文字列キーから文字列値への予約済み拡張マップで、現在は空で送信されます。これに何も要求せず、存在しない場合、存在する場合、および出現する任意のキーを許容してください。 |
リクエストボディの例:
{
"type": "prompt",
"request_id": "req_abc123",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"actor": {
"type": "user",
"id": "user_01AbCdEfGhIjKlMnOpQrStUv",
"email_address": "alice@example.com"
},
"source": {
"application": "claude-ai"
},
"session_id": "22222222-2222-2222-2222-222222222222",
"model": "claude-sonnet-4-5",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Summarize the attached report."
},
{
"type": "attachment",
"file_name": "q2-report.pdf",
"media_type": "application/pdf",
"size_bytes": 48213,
"text": "Q2 revenue grew 14% quarter over quarter..."
}
]
}
],
"metadata": {}
}コンテンツブロック
messages の各エントリには、user または assistant の role(ツール結果は公開 Messages API のコンテンツモデルに合わせて user ロールの下に表示されます)と、type で判別されるブロックの content 配列があります。
ブロックの type | フィールド |
|---|---|
text | text:テキストコンテンツ。 |
tool_use | id:対応するツール結果が参照する識別子。tool_name:ツールの名前。input:モデルがツールに渡した引数。 |
tool_result | content:ツールの出力をテキストとして表したもので、各パートは改行で結合されます。画像などのバイナリパートはプレースホルダーマーカーに置き換えられ、生のバイトは決して送信されません。is_error:ツール呼び出しが失敗したかどうか。tool_name:ツールの名前。これにより、ポリシーは以前のブロックを相互参照することなくツールの識別情報を条件にできます。tool_use_id:対応する tool_use ブロックの id。 |
attachment | file_name:元のファイル名またはパス。media_type:添付ファイルのメディアタイプ。size_bytes:元のファイルのサイズ。text:利用可能な場合の添付ファイルのテキストコンテンツ(抽出されたドキュメントテキスト、音声トランスクリプト、リンクメタデータなど)。生の添付ファイルバイトは決して送信されません。 |
認識できない type を持つブロックは、前方互換性のある追加です。保証されるフィールドは type のみです。ポリシーは存在する他のフィールドを検査してもかまいませんが、認識できないタイプを理由にリクエストを拒否してはなりません。
トランスクリプトに含まれるもの
トランスクリプトは、推論時点までのエンドユーザーが見ている会話です。トランスクリプトテキスト、ツール呼び出しとその結果、抽出された添付ファイルテキスト、および以前のターンが含まれます。システムプロンプト、ツール定義、Anthropic 内部のコンテキスト、Claude の隠れた推論、生のファイルバイトは決して含まれません。
すべてのブロックが除外されたターンは完全に省略されるため、user と assistant が厳密に交互に現れると想定しないでください。
トランスクリプトは切り詰められずに送信されるため、大きな添付ファイルを含む長い会話では、上限 10 MB までの大きなリクエストボディが生成されます。この上限を受け入れられるようにサーバーのボディ制限を引き上げてください。一般的なデフォルト値の多くはこれよりはるかに小さく、nginx の client_max_body_size は 1 MB、Express の express.json() は 100 kB です。拒否されたボディは webhook の失敗としてカウントされるため、Allow the request の失敗処理の下では、サイズ超過のプロンプトが検査されずにモデルに到達することになります。
source の値
source.application は閉じた列挙型ではなく、オープンな文字列です。既知の値は claude-ai と claude-code で、接続テストでは config-test が使用されます。新しい値が出現する可能性があり、サーバーは認識できない値を理由にリクエストを拒否してはなりません。
source.application は信頼境界ではなく、参考用のルーティングメタデータとして扱ってください。セキュリティ上重要なポリシー判断をこれだけに依存させないでください。
判定を返す
どちらの結果でも、HTTP 200 と JSON の判定ボディで応答します。action フィールドで判別します。リクエストを許可するには:
{
"action": "allow"
}拒否するには:
{
"action": "deny",
"deny_reason": "This prompt appears to contain customer payment card data, which your organization's policy does not allow.",
"reference_id": "scan_01HXPT4R9V"
}| フィールド | 制約 | 意味 |
|---|---|---|
action | "allow" または "deny"。必須 | allow は推論を続行させ、deny は拒否します。 |
deny_reason | string または null。最大 500 文字で、それより長い値は切り詰められます | action が deny の場合にエンドユーザーに表示されます。allow の場合は無視されます。 |
reference_id | string または null。[A-Za-z0-9._:/-] から最大 50 文字 | この評価に対する独自の識別子。拒否の inference_hooks_request_denied コンプライアンスアクティビティに記録され、エンドユーザーには決して表示されません。不透明に保ってください。リクエストの内容や個人データを含めないでください。 |
deny はフォーマットの問題で破棄されることは決してありません。サイズ超過の deny_reason は切り詰められ、不正な形式の reference_id は黙って削除され、action は引き続き尊重されます。
逆は成り立ちません。パース可能な判定を伴う HTTP 200 以外のものはすべて webhook の失敗であり、判定の代わりに組織の失敗処理が適用されます。特に:
- エラーステータスで deny を通知しないでください。200 以外のレスポンスは失敗であり、deny ではありません。
allowまたはdeny以外のaction値は webhook の失敗として扱われます。
Anthropic はレスポンスボディを最大 64 KiB まで読み取り、ボディは非圧縮でなければなりません。リダイレクトは追従されず、Cookie は無視されます。判定ボディ内の未知のフィールドは無視されるため、ここに記載されているフィールドと併せてより豊富なオブジェクトを返すことができます。
署名を検証する
リクエストは Standard Webhooks 仕様に従って、3 つのヘッダーを使用して署名されます。Anthropic はヘッダー名を小文字で送信しますが、プロキシが大文字小文字を変更する可能性があるため、大文字小文字を区別せずに検索してください。
| ヘッダー | 内容 |
|---|---|
webhook-id | この配信の一意の識別子。ボディの request_id と等しくなります。冪等性キーとして、また署名対象ペイロードの最初の構成要素として使用してください。 |
webhook-timestamp | リクエストが署名された時刻の Unix 時間(秒)を 10 進文字列で表したもの。サーバーの時計からどちらの方向にも 5 分以上離れたタイムスタンプは拒否してください。 |
webhook-signature | スペース区切りの 1 つ以上の v1,<base64> 値。それぞれが {webhook-id}.{webhook-timestamp}.{raw body bytes} に対する HMAC-SHA256 です。いずれかの値が自分の計算値と一致すればリクエストを受け入れます。比較には定数時間比較を使用してください。 |
検証バグの大半は次の 2 つの細部が原因です。
- 生のバイトを検証する。 JSON のパースや再エンコードの前に、受信したとおりのボディに対して HMAC を計算してください。
- 標準の base64 デコーダーでシークレットをデコードする。 署名シークレットは
whsec_プレフィックスの後の値で、標準の base64 アルファベット(+と/)でエンコードされています。ヘッダー内の署名も同様です。URL セーフなデコーダーは、シークレットに+または/が含まれている場合(ほとんどの場合そうです)、誤ったキーバイトを導出します。
組織に署名シークレットが存在すると、Anthropic が送信するすべてのリクエストは署名され、Inference hooks を有効にするにはシークレットが必要なため、署名なしで届いたリクエストはすべて拒否してください。例外が 1 つあります。組織の最初の保存の前に送信された接続テストは、署名シークレットがまだ存在しないため、署名なしで届きます。管理者がシークレットの存在を確認するまでは署名なしのリクエストを受け入れ、その後は拒否してください。
シークレットのローテーションは即時の切り替えですが、以前のシークレットで署名されたリクエストは、その後約 1 分間、およびすでに送信中のものについては引き続き届く可能性があります。切り替え中は AI セキュリティサーバーが両方のシークレットによる署名を受け入れるようにして、これらの遅延リクエストが拒否されないようにしてください。
以下のサンプルはサーバー実装であるため、shell タブはありません。AI セキュリティサーバーは単発のリクエストではなく、長時間稼働する HTTPS サービスです。各サンプルは言語の標準ライブラリのみを使用しています。Standard Webhooks プロジェクトも、ほとんどの言語向けの検証ライブラリを公開しています。
import base64
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300
def verify(secret: str, headers: dict[str, str], body: bytes) -> bool:
"""Return True if the body was signed by Anthropic for this organization.
Anthropic sends header names in lowercase, but proxies are free to
re-case them, so normalize the lookup to lowercase.
"""
lowercased = {name.lower(): value for name, value in headers.items()}
try:
message_id = lowercased["webhook-id"]
timestamp = lowercased["webhook-timestamp"]
signatures = lowercased["webhook-signature"]
except KeyError:
return False # unsigned request: not from Anthropic
try:
signed_at = int(timestamp)
except ValueError:
return False
if abs(time.time() - signed_at) > TOLERANCE_SECONDS:
return False # replayed, or the clocks disagree
try:
key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
except ValueError:
return False # misconfigured secret: reject rather than crash
payload = f"{message_id}.{timestamp}.".encode() + body
expected = b"v1," + base64.b64encode(
hmac.new(key, payload, hashlib.sha256).digest()
)
# バイト列で比較: strに対するcompare_digestは非ASCII入力で例外を送出するため。
return any(
hmac.compare_digest(expected, candidate.encode())
for candidate in signatures.split()
)運用上のセマンティクス
タイムアウトとリトライ
管理者は判定タイムアウトを 1〜10,000ms の間で設定します(デフォルトは 5,000ms)。この予算は、接続、TLS ハンドシェイク、リクエスト、レスポンスという交換全体をカバーします。
Anthropic は、接続試行が失敗した場合にのみ、100ms の遅延後に正確に 1 回リトライします。リトライは同じタイムアウト予算を共有し、同じ webhook-id と同じ署名を持ちます。AI セキュリティサーバーが一度応答すると、その交換は決してリトライされません。
Webhook の失敗
タイムアウト、200 以外のステータス(リダイレクトを含む)、パース不能またはサイズ超過のレスポンスボディ、到達不能なエンドポイントは、すべて webhook の失敗です。webhook の失敗が deny になることは決してありません。代わりに、組織の失敗処理設定によって、影響を受けたリクエストがブロックされるか、検査なしで続行されるかが決まります。
サーキットブレーカー
AI セキュリティサーバーに起因する webhook の失敗が継続すると、強制適用を停止する「circuit breaker」(サーキットブレーカー)が作動します。Anthropic はサーバーへの接続を停止し、すべてのリクエストに失敗処理が適用されます。
作動から 10 分後以降、Anthropic はサーバーが回復したかどうかをテストします。最大で約 1 分に 1 回、組織自身のトラフィックによって運ばれる 1 つのリクエストが、他のリクエストと同様に署名され同じ形式で、検査のためにサーバーに配信されます。通常どおり応答してください。有効な判定(allow または deny)はブレーカーをリセットし、強制適用が再開されます。webhook の失敗の場合、ブレーカーは作動したままで、テストが継続されます。いずれの場合も、テストリクエスト自体はそのユーザーのために続行されます。その判定は強制適用されず、テストが失敗しても Block the request の下であってもブロックされません。管理者はいつでもブレーカーをリセットすることもでき、管理者による設定変更は自動テストを停止します。サーキットブレーカーを参照してください。
各作動は、アクティビティフィードに inference_hooks_circuit_breaker_tripped アクティビティとして、作動ごとに 1 つ記録されます。ブレーカーが作動している間、リクエストごとの Inference hooks アクティビティは記録されないため、作動アクティビティが作動期間についてのフィード上の唯一の記録となります。
レイテンシ
強制適用により、組織内のすべてのガバナンス対象リクエストの「latency」(レイテンシ)に AI セキュリティサーバーのラウンドトリップが加算されます。判定を高速に保ち、大規模な組織に展開する前にサーバーの負荷テストを行ってください。
送信元 IP アドレス
AI セキュリティサーバーへのリクエストは、Anthropic が公開しているアウトバウンド IP 範囲の一部である 160.79.106.0/24 から発信されます。同じページにあるインバウンド範囲(このブロックをカバーしていません)ではなく、このブロックを許可リストに登録してください。許可リストはサーバーの露出を狭めますが、署名検証の代わりにはなりません。このブロックは Inference hooks 以外の Anthropic の送信トラフィックも運んでいるためです。
前方互換性
このプロトコルは、正しく書かれたサーバーを壊すことなく拡張されます。サーバーは以下を無視しなければなりません。
- プロンプトフレームの未知のトップレベルフィールド。
metadata内の未知のキー。- 新しい
source.applicationの値。 - 新しい
actor.typeの値。actorはtypeで判別されるユニオンであり、現在送信される種類は"user"のみです。将来の種類が保証するのはtypeが存在することだけです。 - 認識できない
typeを持つコンテンツブロック。
認識できないブロックタイプやフィールドを理由にリクエストを拒否しないでください。知っているフィールドを読み取り、残りはスキップしてください。
将来的に他のフックイベントタイプが導入されます。新しいイベントタイプは、フィールドをスキップするだけではサーバーが処理できない追加です。リクエストには依然として判定が必要です。トップレベルの type が認識できない値である場合は、エラーステータスではなく allow 判定を返してください。エラーレスポンスは webhook の失敗であり、失敗が継続するとサーキットブレーカーが作動します。
インテグレーションを設計する
本番環境の AI セキュリティサーバーでは、ワイヤープロトコル以外にもいくつかの設計上の選択を行います。
webhook-id で重複排除する。 webhook-id ヘッダーは配信ごとに一意で、ボディの request_id と等しく、接続失敗時のリトライでも再利用されるため、冪等性キーとして機能します。判定を記録する場合は、これをキーにしてください。
判定を記録し、拒否と結合する。 返した各判定をその reference_id とともに保存してください。すべての拒否は、サーバーが返した reference_id を持つ inference_hooks_request_denied コンプライアンスアクティビティとして記録されるため、アクティビティフィード内の拒否を自分のシステム内の対応するレコードと結合できます。
常に許可するサーバーでアーカイブする。 トランスクリプトを監視せずにリアルタイムでキャプチャするには、無条件に {"action": "allow"} を返し、応答後にフレームを永続化します。これは Compliance API をポーリングする代わりとなるプッシュベースの方法であり、永続化の前に応答することで、ラウンドトリップをユーザーのクリティカルパスから外すことができます。
deny_reason はエンドユーザー向けに書く。 返すテキストは、リクエストがブロックされたときにユーザーが目にするもので、500 文字で切り詰められます。自分のチームだけが解釈できるスキャナーコードを出力するのではなく、どの種類のコンテンツを削除すべきかなど、何を変更すればよいかをユーザーに伝えてください。
次のステップ
Inference hooks を有効にし、エンドポイントを接続してテストし、強制適用、失敗処理、ロールアウトを制御します。
Inference hooks とは何か、判定のラウンドトリップがどのように機能するか、いつ使用すべきか。
Was this page helpful?