Webhook 投递规范 (v1)
本页是 Hashee Agent Webhook Delivery v1 协议的权威规范。覆盖无状态 Agent 的 注册、投递格式、签名算法、重试策略、unreachable 状态机、delivery log 保留。
1. 背景
1.1 为什么 webhook 模式非必须不可
- Cloudflare Workers 跑 Hashee 自己的系统 Agent(
apps/agent-hashee/) - 第三方开发者常用 Vercel / Cloudflare Workers / Lambda(免费层 + 快速部署)
- 只支持 WebSocket 会把 Agent 开发门槛抬到”必须有 VPS”——对生态不友好
1.2 E2EE 层级权衡
无状态 webhook Agent 不参与 Layer 4 (Double Ratchet) 和 Layer 5 (X3DH)。 仅 Layers 1-3 + Layer 6 适用:
| Layer | 内容 | Webhook |
|---|---|---|
| L1 内容加密 (AES-256-GCM) | 每消息 fresh CEK | ✓ |
| L2 包装 (X25519 + HKDF + AES-256-GCM) | per-recipient wrap | ✓ |
| L3 签名 (Ed25519) | 不可否认 | ✓ |
| L4 Ratchet | — | ✗(无状态无 session store) |
| L5 X3DH | — | ✗(同上) |
| L6 Grant Ledger | DB-resident | ✓ |
详见 E2EE 规范。
1.3 用户端语义
用户的 mobile 客户端按 peer 持 ratchet state:
- 对 WebSocket (stateful) Agent → 客户端跑完整 Layers 1-5(完整 forward secrecy + post-compromise security)
- 对 Webhook (stateless) Agent → 客户端对此 peer 降级到 Layers 1-3 (每消息 fresh CEK + wrap + sign 都在,但无 Ratchet 派生的 PCS)
客户端分支:GET /agents/:id 返回 connection_mode。Mobile 决定:
websocket→ ratchet 路径webhook→ envelope-only 路径polling→ V2+
客户端 UI 披露:建立 H2A 关系时,对 webhook Agent 显示 “此 Agent 跑在无状态环境。安全等级:per-message CEK + 端到端加密 + 签名, 但无 perfect forward secrecy。“
2. Webhook 注册
Agent 在 POST /agents 或 PATCH /agents/:id 时传:
{ "connection_mode": "webhook", "webhook_url": "https://my-agent.example.com/hashee-webhook", "health_check_url": "https://my-agent.example.com/health"}2.1 后端强制
webhook_url必须 HTTPS(HTTP 在验证阶段拒)- 例外:
http://localhost/http://127.0.0.1/http://*.lvh.me在 dev 环境接受(SDK dev-mode flag 门控) - 后端生成 32+ 字节随机
webhook_secret - 后端存
bcrypt(webhook_secret, cost=10)— 详见 §3.1 hash 算法选型理由 - Secret TOFU 一次性返回给 Agent owner(plaintext);之后不可取回
2.2 Secret 旋转
POST /agents/:id/webhook/rotate-secret:
- 返回新 secret(一次性)
- 双 secret 宽限期 1 小时:旧 + 新 secret 都验签通过
- 1 小时后旧 secret hard-invalidate
- SDK 必须支持 dual-secret fallback 验证
2.3 Hash 算法选型理由(security 例外)
webhook_secret_hash 用 bcrypt(cost=10) 而不是 Argon2id。是项目级
Argon2id 要求的文档化例外。统一通过 apps/api/src/lib/password.ts::hashPassword
落地(bcryptjs)。
为什么不用 Argon2id:
- Hashee API 跑在 Cloudflare Workers
- Argon2id 是 memory-hard;Workers 上 Argon2id (m=64MiB, t=3, p=1) 单次 hash 约 200-500 ms CPU,超过 per-request 预算(50 ms free / 30 s paid plan)
为什么不用 cost=12(历史曾经的选择):
- cost-12 单次约 150-300 ms。在 viral register storm(1000 用户 × 100/秒)下
/auth/registerp95 达 82 秒,远超 V1 5 秒目标 - Wave 2B-C-A.ii T1(2026-05-11 user-locked Fork d)下调到 cost-10:单次 约 40-80 ms,吞吐恢复到 viral 目标范围
- 安全权衡:OWASP 2023 cost-10 对应约 3 年 GPU-time 单密码破解(随机 ≥8-char 密码),仍满足合理强度
- 旧 cost-12 hash 通过
bcrypt.compare()自动检测 hash 内嵌 cost 仍正确验证 —— 无需 re-hash 迁移 - 客户端 Argon2id KEK(保护密码 → keystore KEK)是 E2EE 规范 §6 密钥恢复的 独立保护层;server-side bcrypt 只是多层防御的其中一层
为什么 bcrypt(cost=10) 仍然安全:
- webhook secret 是服务端生成的 32+ 字节随机 — 256+ bit 熵
- bcrypt(cost=10) 在这个熵等级下安全裕度充足
- 威胁模型是 stolen hash 的 offline brute force,不是弱密码字典攻击; memory-hard hardening 对这个熵等级是过度工程
例外范围:
- ✓
agents.webhook_secret_hash— Agent webhook shared secret(bcrypt cost-10) - ✓
users.password_hash— 用户登录密码(同样 bcrypt cost-10) - ✗ 客户端密钥派生(保护密码 → keystore KEK /
encrypted_key_backupsArgon2id KEK)仍 Argon2id-only(见 E2EE 规范 §6)
3. 投递格式
POST /hashee-webhook HTTP/1.1Host: my-agent.example.comContent-Type: application/jsonUser-Agent: Hashee-Webhook/1.0X-Hashee-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000X-Hashee-Timestamp: 1713712345X-Hashee-Signature: 3a4b5c... (raw hex, no v1= prefix)X-Hashee-Delivery-Attempt: 1
{ "event": "message.new", "agent_id": "agent_xyz", "timestamp": 1713712345, "data": { "message_id": "msg_abc", "conversation_id": "conv_def", "envelope": { /* hashee-envelope wire format */ } }}3.1 签名算法
signed_string = timestamp + "." + delivery_id + "." + bodysignature = hex(HMAC_SHA256(webhook_secret, signed_string))header_value = signature // RAW HEX, 无前缀不变量:
delivery_id是投递 ID(每次 HTTP attempt 唯一,含重试),不是event_id- Raw 小写 hex;无
v1=前缀(V1 故意不版本化) timingSafeEqual对 hex 字符串- 最小 secret 长度:32 字符
3.2 时间窗 + 去重
X-Hashee-Timestamp必须在 Agent 接收时间 ±5 分钟内(TIMESTAMP_TOLERANCE_S = 300)X-Hashee-Delivery-Id在 10 分钟 sliding window 内去重(DEDUP_WINDOW_MS = 600_000)
3.3 平台不变量
- 每次重试用新
delivery_id(同一 event 在不同 delivery id 下多次投递) - Agent 看到同一 message body 多个 delivery id 是正常的
- 业务层去重需要检查 envelope-level
message_id - 如 Agent 处理成功但 HTTP 响应超时,Hashee 重试用新 delivery id;
Agent 应基于
envelope.message_id去重 + 回 200 跳过
设计将 SDK 层投递去重(防重放)与业务层消息去重(防 handler 重复执行)分开。
4. 事件类型
Webhook 模式支持的事件严格少于 WebSocket:
| event | data | 触发 |
|---|---|---|
message.new | { message_id, conversation_id, envelope } | 新消息(含 artifact_tool_call) |
artifact_response | { message_id, conversation_id, envelope } | 用户回 artifact(含 tool_response) |
relation.established | { user_id, relation_id, granted_scopes } | H2A 关系建立 |
relation.terminated | { user_id, relation_id, reason } | 关系终止 |
relation.suspended | { user_id, relation_id } | 用户暂停 |
relation.restored | { user_id, relation_id } | 用户恢复 |
不通过 webhook 投递的事件(仅 WS + cron):
- 治理事件 (
governance.*) - Reaction 事件
- 群组成员变更
- TTL 过期
5. Agent 响应合约
- HTTP 响应 deadline:10 秒。非 2xx 或超时 → 视为失败 → 触发重试
- 响应 body 可为空或
{} - 业务处理(LLM 调用 / 外部 API)SHOULD NOT 内联 block 响应。推荐:
- Cloudflare Workers:
ctx.waitUntil(processAsync(event)),立即返 200 - Lambda:enqueue 到 SQS / 内部 queue,返 200
- Node.js:返 200,在 EventEmitter callback 处理
- Cloudflare Workers:
- 回复消息禁止 piggy-back 在 webhook 响应 body;回复走 REST
POST /agents/:id/conversations/:cid/messages
6. 重试策略
6.1 指数退避
| Attempt | Delay |
|---|---|
| 1 | 立即 |
| 2 | 失败后 1 秒 |
| 3 | 4 秒 |
| 4 | 16 秒 |
| 5 | 1 分钟 |
| 6 | 5 分钟 |
| 7 | 30 分钟 |
6.2 失败分类
重试:
- HTTP
5xx - 连接超时 / DNS 失败 / TLS 失败
- HTTP
408/429(特殊 case)
不重试(永久失败):
- HTTP
400-407,409-428,430-499→ Agent 显式拒绝 - 连续
rejected超阈值(如 10 个事件)→ 标 Agentunreachable
6.3 Unreachable 状态机
7 次全失败(或 rejected 阈值)→ agents.unreachable_since = now()。
- Owner 收
security_alert:“你的 Agent X 不可达;投递暂停” - 新消息 queue 但不投递
- 恢复触发:
- 一次成功 health check (
GET agents.health_check_url) → 清unreachable_since - 或 owner 跑
POST /agents/:id/webhook/test
- 一次成功 health check (
- 恢复后,queue 消息按
created_at顺序重发。queue 保留:最多 7 天
7. Payload 大小限制
- Webhook 请求 body hard cap:1 MB(平台侧在 dispatch 前强制;超限走 R2 对象引用)
- 语义边界:webhook 投递的是事件 + envelope,不是大文件传输
- 文件 / 多媒体 artifact ciphertext 算”大 payload”,必须走 R2 对象引用:
- Envelope 携带:R2 key + per-object CEK wrap envelope + SHA-256 digest
- Agent 通过签名 R2 URL 或 Agent-authenticated REST 端点单独拉对象
- 这把 webhook body 大小保持 bounded
- 1 MB 范围:文本消息(含长 Markdown / 代码块)/ artifact metadata / tool_call arguments
- 超过 1 MB 进 R2 路径
Owner 明确拒绝原 10 MB cap:webhook 不是大数据通道。
8. SDK Surface
8.1 当前 primitives
import { verifyWebhookSignature, parseWebhookPayload } from "@hasheeai/agent-sdk-ts";// 低阶 primitives;宿主应用负责 wire 起来8.2 高阶 createWebhookDispatcher
import { createWebhookDispatcher } from "@hasheeai/agent-sdk-ts";
const dispatcher = createWebhookDispatcher({ secret: () => process.env.HASHEE_WEBHOOK_SECRET!, agentId: process.env.HASHEE_AGENT_ID!, agentToken: process.env.HASHEE_AGENT_TOKEN!, privateKey: loadPrivateKey(), signingPrivateKey: loadSigningKey(), onMessage: async (msg, agent) => { await agent.send(msg.conversation_id, { type: "text", text: `echo: ${msg.content}` }); }, onToolResponse: async (resp) => { /* ... */ }, onRelationEstablished: async (relation) => { /* ... */ },});
// Cloudflare Workerexport default { async fetch(request, env, ctx) { return dispatcher(request, { env, ctx }); },};Dispatcher 职责:
- 读
X-Hashee-*headers → 调verifyWebhookSignature。失败 → 401 - 验证 timestamp → skew → 400
- 去重
delivery_id→ duplicate → 200(幂等) JSON.parse→ 失败 → 400- 按
event字段路由到对应 handler - E2EE 解密
message.new/artifact_response。失败 → 200 + log(不让后端重试 decrypt 错) - 在
try/catch内调 handler。handler 错 → log + 返 200(防重试放大) - 返 200 + 空 body
9. Delivery Log 与保留
agent_webhook_delivery_log 表:
| 列 | 类型 | 说明 |
|---|---|---|
id | uuid PK | |
agent_id | text FK | |
delivery_id | uuid | X-Hashee-Delivery-Id |
event_type | text | message.new 等 |
attempt | int | 1..7 |
status | enum pending / success / failed / rejected | |
response_code | int, nullable | Agent 的 HTTP status |
response_ms | int, nullable | latency |
error_message | text, nullable | 截到 512 字节 |
created_at / completed_at | timestamptz |
保留:30 天。Cron 每天扫一次。
PII:行不存 body;error_message 截断;失败响应 body 不存。
跨规范关系
| 文档 | 连接 |
|---|---|
| E2EE 规范 | §2.4 stateless agent exception — Layers 1-3 only |
| Agent 开发者平台规范 | §4 注册时含 connection_mode = webhook + webhook_url |
| Capability Manifest 规范 | tool call / response 走 webhook 的 message.new / artifact_response 事件 |
| Backend Architecture v6.0 (内部 spec) | cron “Webhook retry queue” + 新表 agent_webhook_delivery_log |
| SDK v6.0 (内部 spec) | createWebhookDispatcher Stage 3 新增 |
验收清单 (Stage 3 closeout)
-
POST /agents/:id/webhook/rotate-secret部署 + 1 小时双 secret 宽限期 - 后端强制 webhook body cap = 1 MB;超限走 R2 对象引用
- 重试策略在 queue-consumer 实现(§6.1 schedule + §6.2 分类)
-
agents.unreachable_since状态机功能 + owner 收security_alert+ 恢复(health check 或/webhook/test) -
agent_webhook_delivery_log表 + 30 天 cron sweep -
createWebhookDispatcher()在packages/public/agent-sdk-ts/落地
相关页面
- Webhook 协议参考 (SDK) — SDK 高阶封装与 dispatcher 模板
- Hello World — Webhook 模式 — 端到端实现
- Webhook API — API 字段速查
- 部署到 Cloudflare Workers
- E2EE 规范
- Agent 开发者平台规范