CMDCGateway.Webhook (cmdc_gateway v0.4.1)

Copy Markdown View Source

A2A webhook 派发与签名 — Phase 12 NEW.5 (12L)。

对标 ADP Ch.15 A2A 4 种交互机制中的 webhook 模式 — 给 >5 分钟长任务用 (SSE 在家庭路由器 / NAT 下会被超时杀掉,webhook 是唯一可靠路径)。

设计

  • 异步派发tasks/sendWithWebhook 立即返回 taskId + accepted; Agent 任务在后台 Task 内执行;状态转变时 POST 给 callbackUrl
  • 4 类回调(payload event 字段):
    • task.accepted — Task 接受,session 已创建
    • task.statusUpdate — Agent 运行中状态变化(working / tool_calling)
    • task.artifactUpdate — 流式 message_delta(可选)
    • task.completed — Agent 正常结束 + 完整回复
    • task.failed — Agent abort / timeout / error
  • HMAC-SHA256 签名:用户传 webhookSecret,CMDC 在 X-CMDC-Signature header 带 sha256=<hex>,回调方可验签防伪造
  • 重试:3 次指数退避(1s / 2s / 4s),失败写入 dead letter log

v0.4.0 实现说明

  • HTTP 客户端用 Erlang 内置 :httpc(零额外依赖)
  • WebhookDispatcher 用 Task.start_link 异步运行 + EventBus subscribe
  • 不持久化(内存);进程崩溃后失败的 callback 不补发(留 v0.4.1)

使用

# 1. 客户端发起
POST /v1/a2a/tasks/sendWithWebhook
{
  "id": "task-abc",
  "callbackUrl": "https://my-app.com/webhook",
  "webhookSecret": "my-shared-secret",
  "message": {"role": "user", "parts": [{"type": "text", "text": "..."}]}
}

# 2. CMDC 立即返回
 202 {"jsonrpc": "2.0", "id": ..., "result": {"taskId": "task-abc", "status": "accepted"}}

# 3. 后台异步派发回调
POST https://my-app.com/webhook
X-CMDC-Signature: sha256=<hex>
Body: {"event": "task.completed", "taskId": "...", "result": {...}}

# 4. 客户端验签
assert verify_signature(body, header, "my-shared-secret")

Summary

Functions

构造 Webhook 标准 payload。

同步 POST 一个 webhook payload,失败按指数退避重试 3 次。

对 JSON-encoded body 用 secret 签名,返回 sha256=<hex>

校验签名。expected_header 是收到的 X-CMDC-Signature 值。

Types

callback_url()

@type callback_url() :: String.t()

event()

@type event() :: :accepted | :status_update | :artifact_update | :completed | :failed

payload()

@type payload() :: map()

secret()

@type secret() :: String.t()

Functions

build_payload(event, task_id, extra)

@spec build_payload(event(), String.t(), map()) :: payload()

构造 Webhook 标准 payload。

dispatch(callback_url, payload, opts \\ [])

@spec dispatch(callback_url(), payload(), keyword()) ::
  :ok | {:error, term(), pos_integer()}

同步 POST 一个 webhook payload,失败按指数退避重试 3 次。

选项

  • :secret — 启用 HMAC 签名(推荐)
  • :timeout_ms — 每次请求超时(默认 5_000)
  • :dispatch_fn — 注入替代 HTTP 函数(用于测试)

返回 :ok{:error, reason, attempts}

sign_payload(body, secret)

@spec sign_payload(iodata(), secret()) :: String.t()

对 JSON-encoded body 用 secret 签名,返回 sha256=<hex>

适合放进 X-CMDC-Signature header。

verify_signature(body, expected_header, secret)

@spec verify_signature(iodata(), String.t(), secret()) :: boolean()

校验签名。expected_header 是收到的 X-CMDC-Signature 值。

使用 :crypto.hash_equals/2(OTP 25+)做常数时间比较防 timing attack; 老 OTP 自动降级到 byte_size + 逐字符比较。