Inference hooks インテグレーションを開発する
署名付きのInference hooksリクエストを受信して検証し、許可または拒否の判定を返すAIセキュリティサーバーを構築します。
Inference hooks統合とは「AI security server」(AIセキュリティサーバー)のことで、Anthropicが呼び出すHTTPSサービスです。管理対象のリクエストごとに、サーバーは会話のトランスクリプトを含む署名付きのPOSTを受信し、許可または拒否の「verdict」(判定)を返します。このページでは、そのサーバーを構築するためのプロトコルとして、リクエストと判定のスキーマ、署名検証、運用上の取り決めを説明します。
Inference hooksを有効にしてエンドポイントを指定する方法については、Inference hooksを設定するを参照してください。Inference hooksの概要と使用すべき場面については、Inference hooksの概要を参照してください。
最初の判定のやり取りを確認する
最小限の動作する統合は、各リクエストを読み取って許可するだけのサーバーです。以下のいずれかのサーバーを実行し、公開されたhttps:// URLで公開します。たとえば、自分で管理するホスト上のTLS終端リバースプロキシの背後に配置します。リバーストンネルサービスは使用しないでください(リクエストを受信するを参照)。次に、管理者にエンドポイントとして設定して接続をテストしてもらいます。Test connectionの結果に、サーバーが返した許可の判定が表示されます。
# 実行方法: 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を含みます(ソースの値を参照)。 |
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と、typeで区別されるブロックのcontent配列があります。ツールの結果は、公開Messages APIのコンテンツモデルと同様にuserロールの下に含まれます。
ブロックの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、textブロックのtext、tool_resultブロックのcontentとis_errorを除き、これらのフィールドは値が不明な場合にnullになることがあります。たとえば、画像はfile_nameとtextがnullのattachmentブロックとして届きます。
認識できないtypeのブロックは、前方互換性のために追加されたものです。このようなブロックで保証されるフィールドはtypeだけです。ポリシーでほかのフィールドを調べてもかまいませんが、認識できないタイプを理由にリクエストを拒否してはいけません。
トランスクリプトの内容
トランスクリプトは、推論時点までの会話をエンドユーザーが見ているとおりに表したものです。トランスクリプトのテキスト、ツール呼び出しとその結果、添付ファイルから抽出されたテキスト、以前のターンが含まれます。システムプロンプト、ツール定義、Anthropic内部のコンテキスト、Claudeの非表示の推論、生のファイルバイトは含まれません。
すべてのブロックが除外されたターンは丸ごと省略されます。そのため、userとassistantが厳密に交互に並ぶことを前提にしないでください。
トランスクリプトは切り詰められずに送信されます。そのため、大きな添付ファイルを含む長い会話では、リクエストボディも大きくなります。実際には、モデルのコンテキストウィンドウによってボディは約10 MB未満に収まりますが、プロトコル上は最大64 MiBまで許容されます。一般的なデフォルト値の中には、これよりはるかに小さいものがあります。たとえば、nginxのclient_max_body_sizeは1 MB、Expressのexpress.json()は100 kBです。ボディが拒否されるとWebhook障害として扱われます。そのため、障害時の処理がAllow the requestの場合、サイズの大きすぎるプロンプトは検査されないままモデルに届きます。
ソースの値
source.applicationは、固定の列挙型ではなく自由な文字列です。一般的な値はclaude-ai、claude-code、coworkです。接続テストと、サーキットブレーカーの自動復旧チェックでは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_reasonは切り詰められ、不正な形式のreference_idは通知なしに破棄されますが、actionはそのまま適用されます。
逆の場合は異なります。解析可能な判定を含むHTTP 200以外の応答はすべてWebhook障害となり、判定の代わりに組織の障害時の処理が適用されます。特に、次の点に注意してください。
- エラーステータスで拒否を伝えないでください。200以外の応答は拒否ではなく障害として扱われます。
allowまたはdeny以外のaction値は、Webhook障害として扱われます。
Anthropicが読み取るレスポンスボディは最大64 KiBで、ボディは非圧縮である必要があります。リダイレクトには従わず、Cookieは無視されます。判定ボディ内の不明なフィールドは無視されるため、ここで説明するフィールドに加えて、より多くの情報を含むオブジェクトを返すこともできます。
署名を検証する
リクエストは、Standard Webhooks仕様に従い、3つのヘッダーを使って署名されます。Anthropicはヘッダー名を小文字で送信しますが、プロキシによって大文字・小文字が変更される場合があります。そのため、ヘッダーは大文字と小文字を区別せずに参照してください。
| ヘッダー | 内容 |
|---|---|
webhook-id | この配信の一意の識別子。ボディのrequest_idと同じ値です。冪等性キーとして、また署名対象ペイロードの最初の要素として使用します。 |
webhook-timestamp | リクエストが署名された時刻。10進数文字列で表したUnix時間(秒)です。サーバーの時計と前後どちらかに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つあります。シークレットが必須になる前にInference hooksを有効にした組織では、管理者がシークレットを生成するまで署名のないリクエストが送信され続けます。署名のないリクエストは、シークレットが存在することを管理者が確認するまでの間だけ受け入れ、その後は拒否してください。
シークレットのローテーションは即座に切り替わります。ただし、以前のシークレットで署名されたリクエストが、その後約1分間届く可能性があります。すでに送信中のリクエストも同様です。これらの遅れて届くリクエストが拒否されないように、切り替え期間中はAIセキュリティサーバーで両方のシークレットによる署名を受け入れてください。
以下のサンプルはサーバー実装です。AIセキュリティサーバーは単発のリクエストではなく長時間稼働するHTTPSサービスであるため、shellタブはありません。各サンプルは、その言語の標準ライブラリのみを使用しています。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障害
次のものはすべてWebhook障害として扱われます。
- タイムアウト
- 200以外のステータス(リダイレクトを含む)
- 解析できない、またはサイズが大きすぎるレスポンスボディ
- 到達できないエンドポイント
Webhook障害が拒否として扱われることはありません。代わりに、組織の障害時の処理の設定によって、影響を受けたリクエストをブロックするか、検査なしで続行するかが決まります。
サーキットブレーカー
AIセキュリティサーバーに起因するWebhook障害が続くと、「circuit breaker」(サーキットブレーカー)が作動して適用が停止します。Anthropicはサーバーへの接続を停止し、すべてのリクエストに障害時の処理が適用されます。
作動から10分後、Anthropicはサーバーが復旧したかどうかのチェックを開始します。チェックは最大で約1分に1回行われ、Test connectionと同じ合成テストリクエスト(source.applicationはconfig-test)がサーバーに送信されます。このリクエストはほかのリクエストと同様に署名されており、ユーザーのコンテンツは含まれません。通常どおり応答してください。
- 有効な判定(許可または拒否)が返されると、ブレーカーがリセットされ、適用が再開されます。
- Webhook障害が発生した場合は、ブレーカーは作動したままとなり、チェックが続行されます。
管理者はいつでもブレーカーをリセットできます。また、管理者が設定を変更すると自動チェックは停止します。詳しくはサーキットブレーカーを参照してください。
作動するたびに、Activity Feedにinference_hooks_circuit_breaker_trippedアクティビティが1件記録されます。ブレーカーの作動中は、リクエストごとのInference hooksアクティビティは記録されません。そのため、作動期間についてフィードに残る記録は、この作動アクティビティだけです。
レイテンシ
適用を有効にすると、組織内の管理対象リクエストすべてのレイテンシに、AIセキュリティサーバーとのやり取りにかかる時間が加わります。判定は高速に返すようにし、大規模な組織に展開する前にサーバーの負荷テストを行ってください。
送信元IPアドレス
AIセキュリティサーバーへのリクエストは160.79.106.0/24から送信されます。これは、Anthropicが公開している送信IP範囲の一部です。許可リストにはこのブロックを追加してください。同じページに記載されている受信IP範囲にはこのブロックが含まれないため、そちらは使用しないでください。許可リストを使うとサーバーの露出を減らせますが、署名検証の代わりにはなりません。このブロックには、Inference hooks以外のAnthropicの送信トラフィックも含まれるためです。
前方互換性
プロトコルは、正しく実装されたサーバーを壊さない形で拡張されます。サーバーは次のものを無視する必要があります。
- プロンプトフレームの不明なトップレベルフィールド。
metadata内の不明なキー。- 新しい
source.application値。 - 新しい
actor.type値。actorはtypeで区別されるユニオン型で、現在送信される種類は"user"のみです。将来追加される種類で保証されるのは、typeが存在することだけです。 - 認識できない
typeを持つコンテンツブロック。
認識できないブロックタイプやフィールドを理由にリクエストを拒否しないでください。既知のフィールドを読み取り、それ以外はスキップしてください。
今後、ほかのフックイベントタイプが追加される予定です。新しいイベントタイプは、フィールドをスキップするだけでは対応できません。そのリクエストにも判定が必要だからです。トップレベルのtypeが認識できない値の場合は、エラーステータスではなく許可の判定を返してください。エラー応答はWebhook障害となり、障害が続くとサーキットブレーカーが作動します。
統合を設計する
本番環境のAIセキュリティサーバーでは、通信プロトコル以外にもいくつかの設計上の判断が必要です。
webhook-idで重複を排除する。 webhook-idヘッダーは配信ごとに一意で、ボディのrequest_idと同じ値です。接続失敗時のリトライでも同じ値が使われるため、冪等性キーとして使用できます。判定を記録する場合は、この値をレコードのキーにしてください。
判定を記録し、拒否と突き合わせる。 返した各判定を、そのreference_idとともに保存してください。すべての拒否は、サーバーが返したreference_idを含むinference_hooks_request_deniedコンプライアンスアクティビティとして記録されます。これにより、Activity Feedの拒否を、自社システム内の対応するレコードと突き合わせることができます。
常に許可するサーバーでアーカイブする。 トランスクリプトを制限せずにリアルタイムで取得するには、無条件に{"action": "allow"}を返し、応答した後にフレームを保存します。これは、Compliance APIをポーリングする方法に代わるプッシュ型の手段です。保存する前に応答することで、サーバーとのやり取りがユーザーのクリティカルパスに影響しなくなります。
deny_reasonはエンドユーザー向けに書く。 返したテキストは、リクエストがブロックされたときにユーザーに表示されます(500文字で切り詰められます)。自社チームにしか理解できないスキャナーのコードを出力するのではなく、どの種類のコンテンツを削除すべきかなど、ユーザーが何を変更すればよいかを伝えてください。
次のステップ
Inference hooksを有効にし、エンドポイントを接続してテストし、適用、障害時の処理、展開を制御します。
Inference hooksとは何か、判定のやり取りの仕組み、使用すべき場面について説明します。
Was this page helpful?