接收第一条消息
这一页把”用户发了句 hi → Agent 收到 hi”中间发生的每一步全部展开。 读完后你应该能回答:哪一步在客户端、哪一步在后端、哪些字节是密文、 哪些是元数据、SDK 替你做了什么、什么时候轮到你的代码出场。
链路全图
Hashee Mobile / Desktop Hashee 后端 你的 Agent 进程 │ │ │ [1] 用户键入 "hi" │ │ │ │ │ [2] 客户端 Core 加密管线: │ │ ├─ generateCek() (32B 随机) │ │ ├─ encryptContent(cek, "hi", aad) │ │ │ → ContentEnvelope (Layer 1) │ │ ├─ wrapCek(cek, agent_x25519_pk) │ │ │ → WrapEnvelope per recipient (Layer 2) │ │ ├─ signEnvelope(unsigned, user_ed25519_sk) │ │ │ → SignedEnvelope (Layer 3) │ │ ├─ Ratchet step → 包外层 (Layer 4 if WS) │ │ └─ encodeWireEnvelope() → base64 │ │ │ │ │ ├─POST /messages or WS frame──────────► │ │ │ │ │ [3] 后端校验: │ │ ├─ session token / WS auth │ │ ├─ rate limit │ │ ├─ recipient (Agent) 存在 │ │ └─ payload size │ │ │ │ │ [4] 写 messages 表 │ │ ├─ 仅元数据 │ │ │ (sender_id, conv_id, │ │ │ encrypted_payload BLOB, │ │ │ timestamps) │ │ └─ 大对象 (附件) 走 R2 │ │ │ │ │ [5] 路由: │ │ ├─ ConversationDO 收到事件 │ │ ├─ 找到接收方 Agent 的连接 │ │ │ (WS / Webhook / Polling) │ │ └─ DeliveryShardDO 扇出 │ │ ├──WS frame──────────────────►│ │ │ │ │ │ [6] SDK Transport 收到 frame │ │ │ │ │ [7] SDK 解密管线: │ │ ├─ decodeWireEnvelope() → SignedEnvelope │ │ ├─ verifyEnvelope(env, user_ed25519_pk) │ │ │ ✗ 签名失败 → 丢弃 + decrypt_failure_report │ │ ├─ findWrapForRecipient(env, agent_id) │ │ │ → 该 Agent 的 WrapEnvelope │ │ ├─ unwrapCek(wrap, agent_x25519_sk) → CEK │ │ ├─ decryptContent(cek, content, aad) → "hi" │ │ └─ if Ratchet → 推进接收链 │ │ │ │ │ [8] InboundMessage 派发: │ │ ├─ parseInboundMessage() │ │ │ → { conversation_id, sender_id, │ │ │ payload: { type: "text", text: "hi" }, │ │ │ created_at, headers, attachments } │ │ ├─ 调用所有 messageHandlers │ │ │ → addMessageHandler 注册的 callback │ │ │ │ │ [9] 你的代码出场: │ │ ├─ msg.payload.text === "hi" │ │ ├─ 调 LLM / 业务逻辑 │ │ └─ agent.send(conv_id, {type:"text", text:"hello"}) │ │ │ │ │ [10] SDK 加密发送 (镜像 [2]): │ │ ├─ generateCek │ │ ├─ encryptContent │ │ ├─ wrapCek (now to user_x25519_pk) │ │ ├─ signEnvelope (with agent_ed25519_sk) │ │ └─ WS frame send │ │ ◄─────────WS frame──────── │ │ │ │ │ │ 路由 / 写库 / DO 扇出 │ │ ◄──WS frame────── │ │ │ │ [11] 客户端 Core 解密管线 (镜像 [7]): │ │ │ │ │ [12] 用户看到 "hello" 气泡 │ │后端可见 vs 不可见
| 字段 | 后端可见? |
|---|---|
conversation_id, sender_id, recipient_id | ✓ 元数据 |
created_at, client_message_id (idempotency) | ✓ |
encrypted_payload (整段 base64 wire envelope) | ✓ 但是密文 |
attachment_id 引用 | ✓ R2 对象引用 |
| 消息明文(“hi” / “hello”) | ✗ |
| CEK | ✗ |
| 用户 / Agent 私钥 | ✗ |
| Ed25519 签名内容 | ✓ 但加密后只是字节流 |
SDK 替你做了什么
发消息时(你只调 agent.send(...)):
| 步骤 | SDK 内部函数(@hasheeai/agent-sdk-ts) |
|---|---|
| 1. 生成一次性 CEK | generateCek() |
| 2. 加密内容(AES-256-GCM + AAD) | encryptContent(cek, plaintext, aad) |
| 3. 为每个收件人 wrap CEK | wrapCek(cek, recipient_x25519_pk) |
| 4. 计算 canonical envelope JSON | canonicalizeEnvelope(unsigned) |
| 5. Ed25519 签名 | signEnvelope(unsigned, agent_ed25519_sk) |
| 6. 编码成 wire envelope | encodeWireEnvelope({signed, wraps}) |
| 7. WS 发送 / HTTP POST | transport.send(frame) |
收消息时(你只在 addMessageHandler 拿到明文):
| 步骤 | SDK 内部函数 |
|---|---|
| 1. 解码 wire envelope | decodeWireEnvelope(base64) |
| 2. 验证 Ed25519 签名 | verifyEnvelope(signed, sender_ed25519_pk) |
| 3. 找到给本 Agent 的 wrap | findWrapForRecipient(env, agent_id) |
| 4. unwrap CEK | unwrapCek(wrap, agent_x25519_sk) |
| 5. 解密内容 | decryptContent(cek, content, aad) |
| 6. 解析为 InboundMessage | parseInboundMessage() |
| 7. 调用你的 handler | dispatchInboundFrame(msg, handlers) |
AAD 绑定
Layer 1 加密的 AAD(Additional Authenticated Data)= 44 字节:
[ conversation_id (16) | epoch_id (8 BE) | "hashee-envelope-v1.1" (20) ]意义:
- 同一 CEK + 同一密文,换 conversation_id 就解不出来 → 防”密文搬运”。
- 换 epoch_id 也解不出来 → 支持密钥轮换的 forward secrecy。
- wire 字面量绑死 → 防协议混淆攻击(v1.1 密文不能被解析为 v1)。
详见 E2EE 规范 §3.1。
出错时会发生什么
| 错误 | SDK 行为 |
|---|---|
| 签名不通过 | 丢弃 + 调 reportDecryptFailure(reason: "signature") |
| 找不到给本 Agent 的 wrap | 丢弃 + reportDecryptFailure(reason: "no_wrap") |
| unwrap CEK 失败(私钥不对) | 丢弃 + reportDecryptFailure(reason: "unwrap") |
| AES-GCM 解密 tag 不匹配 | 丢弃 + reportDecryptFailure(reason: "aead") |
| Payload 不是合法 JSON | 丢弃 + reportDecryptFailure(reason: "parse") |
reportDecryptFailure 默认走 console.warn + 上报 telemetry endpoint,
不抛异常给你的 handler——单条消息坏掉不影响后续消息。
你可以注册 addStatusHandler 来获得连接级 / decrypt 失败级事件。
演示视频
下面这段约 90 秒的视频把 Hashee app 与 Agent 终端并排播放,逐帧高亮加密链路上每一步发生的位置:
视频文字版逐节描述(无视频也能阅读)
- 00:00 – 00:10 — 屏幕分两半。左:Hashee Mobile 模拟器主页,已添加 DemoBot;
右:终端,DemoBot 进程显示
connection: connected。 - 00:10 – 00:20 — 左侧打开 DemoBot 会话,输入框输入 “hi” 准备发送。屏幕底部 叠加文字 “[1] 用户键入”。
- 00:20 – 00:35 — 用户点发送;左侧画面短暂高亮 “[2] 客户端加密管线” 标签, 屏幕边缘流动显示步骤:generateCek / encryptContent / wrapCek / signEnvelope。 消息气泡变为”已发送”。
- 00:35 – 00:50 — 中间叠加云端示意(盲管道),文字 “[3-5] 后端只看密文 + 路由”,背景流动显示 ConversationDO + DeliveryShardDO 节点动画。
- 00:50 – 01:05 — 右侧终端弹出新行
[hashee] inbound message conv=01906... payload.text="hi"。叠加文字 “[6-9] SDK 解密 → 你的代码”。 - 01:05 – 01:20 — 终端显示业务代码触发 OpenAI 调用 +
agent.send(...)调用。 屏幕边缘再次流动 “[10] 加密发送” 步骤标签。 - 01:20 – 01:30 — 镜像回到左侧,DemoBot 头像下方”正在输入…”消失, Agent 回复气泡 “Hello! 你刚说了 ‘hi’” 浮现。
常见误区
Q: 后端能看到我的 Agent 内部 prompt 吗? 不能。LLM API 调用发生在你的 Agent 进程内,Hashee 后端不参与。
Q: 我能在后端管理 Agent 的对话历史吗? 可以,但你看到的是密文。你要保存明文,必须在你的 Agent 侧自己存。 (推荐方案:Agent 进程把”对话摘要”加密后写到你自己的数据库。)
Q: 用户撤销 Agent 后,我手里的明文怎么办?
平台无法替你删除——这是 Agent 侧的责任。
你应该响应 relation.revoked 事件 + 删除自己存的相关数据。
详见 更新与撤销。