跳转到内容

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 LedgerDB-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 /agentsPATCH /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_hashbcrypt(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/register p95 达 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_backups Argon2id KEK)仍 Argon2id-only(见 E2EE 规范 §6)

3. 投递格式

POST /hashee-webhook HTTP/1.1
Host: my-agent.example.com
Content-Type: application/json
User-Agent: Hashee-Webhook/1.0
X-Hashee-Delivery-Id: 550e8400-e29b-41d4-a716-446655440000
X-Hashee-Timestamp: 1713712345
X-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 + "." + body
signature = 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:

eventdata触发
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 处理
  • 回复消息禁止 piggy-back 在 webhook 响应 body;回复走 REST POST /agents/:id/conversations/:cid/messages

6. 重试策略

6.1 指数退避

AttemptDelay
1立即
2失败后 1 秒
34 秒
416 秒
51 分钟
65 分钟
730 分钟

6.2 失败分类

重试

  • HTTP 5xx
  • 连接超时 / DNS 失败 / TLS 失败
  • HTTP 408 / 429(特殊 case)

不重试(永久失败)

  • HTTP 400-407, 409-428, 430-499 → Agent 显式拒绝
  • 连续 rejected 超阈值(如 10 个事件)→ 标 Agent unreachable

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
  • 恢复后,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 Worker
export default {
async fetch(request, env, ctx) {
return dispatcher(request, { env, ctx });
},
};

Dispatcher 职责

  1. X-Hashee-* headers → 调 verifyWebhookSignature。失败 → 401
  2. 验证 timestamp → skew → 400
  3. 去重 delivery_id → duplicate → 200(幂等)
  4. JSON.parse → 失败 → 400
  5. event 字段路由到对应 handler
  6. E2EE 解密 message.new / artifact_response。失败 → 200 + log(让后端重试 decrypt 错)
  7. try/catch 内调 handler。handler 错 → log + 返 200(防重试放大)
  8. 返 200 + 空 body

9. Delivery Log 与保留

agent_webhook_delivery_log

类型说明
iduuid PK
agent_idtext FK
delivery_iduuidX-Hashee-Delivery-Id
event_typetextmessage.new
attemptint1..7
statusenum pending / success / failed / rejected
response_codeint, nullableAgent 的 HTTP status
response_msint, nullablelatency
error_messagetext, nullable截到 512 字节
created_at / completed_attimestamptz

保留: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/ 落地

相关页面