Inference hooks 集成是一个 AI 安全服务器:一个由 Anthropic 调用的 HTTPS 服务。对于每个受管控的请求,您的服务器会收到一个经过签名的 POST 请求,其中包含对话记录,并需要返回允许或拒绝的裁决。本页面记录了构建该服务器所需的协议:请求和裁决的 schema、签名验证以及操作契约。
如需了解如何启用 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 必须是最终目标地址。配置 Inference hooks 介绍了管理员如何设置和测试该 URL。
每个请求都携带以下固定请求头,以及您的管理员配置的任何自定义请求头;一旦您的组织拥有签名密钥,还会携带验证签名中描述的 webhook-* 签名请求头:
| 请求头 | 值 |
|---|---|
Content-Type | application/json |
User-Agent | anthropic-dlp/1 |
Accept-Encoding | identity |
目前只有一种 hook 事件:prompt frame(提示帧),在每个受管控的推理请求开始推理之前发送一次。Anthropic 会保持该请求,直到您的 AI 安全服务器响应或裁决超时。
请求体是一个包含以下字段的 JSON 对象:
| 字段 | 类型 | 描述 |
|---|---|---|
type | string | Hook 事件类型。目前始终为 "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": "[email protected]"
},
"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 中的每个条目都有一个 role,值为 user 或 assistant(工具结果出现在 user 角色下,与公开的 Messages API 内容模型一致),以及一个通过 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 的隐藏推理过程或原始文件字节。
如果某个轮次的所有块都被排除,则该轮次会被完全省略,因此请勿假设用户和助手严格交替出现。
对话记录以未截断的形式发送,因此包含大型附件的长对话会产生较大的请求体,上限为 10 MB。请提高您服务器的请求体大小限制以接受该上限。一些常见的默认值远小于此,包括 nginx 的 client_max_body_size(1 MB)和 Express 的 express.json()(100 kB);被拒绝的请求体会被视为 webhook 失败,因此在 Allow the request(允许请求)失败处理模式下,超大的提示将在未经检查的情况下到达模型。
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;最多 50 个字符,字符集为 [A-Za-z0-9._:/-] | 您自己为此次评估分配的标识符。它会被记录在拒绝事件的 inference_hooks_request_denied 合规活动中,永远不会显示给终端用户。请保持其不透明:不包含请求内容和个人数据。 |
拒绝裁决永远不会因格式问题而被丢弃:超长的 deny_reason 会被截断,格式错误的 reference_id 会被静默丢弃,但 action 仍会被执行。
反之则不然。除了带有可解析裁决的 HTTP 200 之外的任何响应都是 webhook 失败,此时将应用您组织的失败处理设置,而非裁决。特别注意:
allow 或 deny 之外的任何 action 值都会被视为 webhook 失败。Anthropic 最多读取响应体的 64 KiB,且响应体必须未经压缩。不会跟随重定向,Cookie 会被忽略。裁决体中的未知字段会被忽略,因此您可以在此处记录的字段之外返回更丰富的对象。
请求按照 Standard Webhooks 规范进行签名,使用三个请求头。Anthropic 以小写形式发送请求头名称,而代理可以自由更改大小写,因此请以不区分大小写的方式查找它们。
| 请求头 | 内容 |
|---|---|
webhook-id | 此次投递的唯一标识符。与请求体的 request_id 相同。将其用作幂等键以及签名载荷的第一个组成部分。 |
webhook-timestamp | 请求签名时的 Unix 时间(秒),以十进制字符串表示。如果时间戳与您服务器的时钟相差超过五分钟(无论早晚),请拒绝该请求。 |
webhook-signature | 一个或多个以空格分隔的 v1,<base64> 值,每个值都是对 {webhook-id}.{webhook-timestamp}.{原始请求体字节} 计算的 HMAC-SHA256。如果任何一个值与您计算的值匹配(使用常量时间比较),则接受该请求。 |
有两个细节导致了大多数验证错误:
whsec_ 前缀之后的值,使用标准 base64 字母表(+ 和 /)编码,请求头中的签名也是如此。当密钥包含 + 或 / 时(大多数情况下都会包含),URL 安全解码器会得出错误的密钥字节。一旦您的组织拥有签名密钥,Anthropic 发送的每个请求都会被签名,并且启用 Inference hooks 需要签名密钥,因此请拒绝任何未签名的请求。有一个例外:在您的组织首次保存之前发送的连接测试是未签名的,因为签名密钥尚不存在。在您的管理员确认密钥存在之前接受未签名的请求,之后则拒绝它们。
轮换密钥是立即切换的,但使用旧密钥签名的请求在之后约一分钟内仍可能到达,再加上任何已在传输中的请求。请让您的 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,000 毫秒之间(默认为 5,000 毫秒)。该预算涵盖整个交换过程:连接、TLS 握手、请求和响应。
Anthropic 仅在连接尝试失败时重试一次,延迟 100 毫秒。重试共享相同的超时预算,并携带相同的 webhook-id 和相同的签名。一旦您的 AI 安全服务器已响应,该交换就不会再被重试。
超时、非 200 状态码(包括重定向)、无法解析或超大的响应体以及无法访问的端点都属于 webhook 失败。Webhook 失败永远不会变成拒绝;相反,您组织的失败处理设置决定受影响的请求是被阻止还是在未经检查的情况下继续。
可归因于您的 AI 安全服务器的持续 webhook 失败会触发熔断器,停止强制执行:Anthropic 停止联系您的服务器,失败处理将应用于每个请求。恢复需要在管理端进行:修复服务器,然后让您的管理员重新开启 Enforce verdicts(强制执行裁决)。参见熔断器。
强制执行会将您的 AI 安全服务器的往返时间添加到您组织中每个受管控请求的 "latency"(延迟)中。请保持裁决快速,并在向大型组织推出之前对您的服务器进行负载测试。
发送到您的 AI 安全服务器的请求来自 160.79.106.0/24,这是 Anthropic 已发布的出站 IP 范围的一部分。请将该地址块加入允许列表,而非同一页面上的入站范围(后者不涵盖该地址块)。加入允许列表可以缩小您服务器的暴露面,但它不能替代签名验证:该地址块承载的 Anthropic 出站流量不仅限于 Inference hooks。
协议的扩展不会破坏正确编写的服务器。您的服务器必须忽略:
metadata 中的未知键。source.application 值。actor.type 值。actor 是一个通过 type 区分的联合类型,目前仅发送 "user" 这一种;未来的类型仅保证 type 字段存在。type 无法识别的内容块。永远不要因为无法识别的块类型或字段而拒绝请求;读取您知道的字段并跳过其余部分。
未来将引入其他 hook 事件类型。新的事件类型是一种您的服务器无法通过跳过字段来处理的新增内容:请求仍然需要一个裁决。当顶层 type 是您无法识别的值时,请返回允许裁决而非错误状态码;错误响应是 webhook 失败,持续的失败会触发熔断器。
生产环境的 AI 安全服务器需要在线路协议之外做出一些设计选择。
基于 webhook-id 去重。 webhook-id 请求头在每次投递中都是唯一的,且与请求体的 request_id 相同,连接失败重试会复用它,因此它可以用作幂等键。如果您记录裁决,请以它作为记录的键。
记录裁决并关联拒绝事件。 存储您返回的每个裁决及其 reference_id。每次拒绝都会被记录为一个 inference_hooks_request_denied 合规活动,其中携带您的服务器返回的 reference_id,因此您可以将 Activity Feed(活动信息流)中的拒绝事件与您自己系统中的匹配记录进行关联。
使用始终允许的服务器进行归档。 如需实时捕获对话记录而不对其进行管控,请无条件返回 {"action": "allow"} 并在响应后持久化该帧。这是轮询 Compliance API 的基于推送的替代方案,在持久化之前先响应可以使您的往返时间不影响用户的关键路径。
为终端用户编写 deny_reason。 您返回的文本是用户在请求被阻止时看到的内容,会在 500 个字符处截断。告诉他们需要更改什么,例如需要删除哪类内容,而不是输出只有您的团队才能理解的扫描器代码。
启用 Inference hooks,连接并测试您的端点,控制强制执行、失败处理和推出范围。
了解 Inference hooks 是什么、裁决往返如何工作以及何时使用它们。
Was this page helpful?