@agentunion/fastaun-browser 0.2.19 → 0.2.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/_packed_docs/CHANGELOG.md +26 -0
- package/_packed_docs/agent.md/SCHEMA.md +173 -0
- package/_packed_docs/agent.md/examples/codeagent-claudecode.md +61 -0
- package/_packed_docs/agent.md/examples/human-developer.md +60 -0
- package/_packed_docs/agent.md/examples/openclaw-lobster.md +52 -0
- package/_packed_docs/agent.md/examples/signed-openclaw-lobster.md +43 -0
- package/_packed_docs/protocol/00-/346/200/273/350/247/210/344/270/216/345/210/206/345/261/202.md +205 -0
- package/_packed_docs/protocol/00A-/350/256/276/350/256/241/345/216/237/345/210/231-/344/270/272Agent/350/200/214/347/224/237.md +197 -0
- package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +549 -0
- package/_packed_docs/protocol/02-/350/257/201/344/271/246/344/270/216/344/277/241/344/273/273/344/275/223/347/263/273.md +810 -0
- package/_packed_docs/protocol/03-Gateway-/350/277/236/346/216/245/346/250/241/345/274/217.md +262 -0
- package/_packed_docs/protocol/04-Peer-/345/255/220/345/215/217/350/256/256.md +180 -0
- package/_packed_docs/protocol/05-Relay-/345/255/220/345/215/217/350/256/256.md +164 -0
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1135 -0
- package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +234 -0
- package/_packed_docs/protocol/08-AUN-E2EE-Group.md +900 -0
- package/_packed_docs/protocol/08-AUN-E2EE.md +413 -0
- package/_packed_docs/protocol/09-/345/256/211/345/205/250/350/200/203/350/231/221.md +316 -0
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +804 -0
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +271 -0
- package/_packed_docs/protocol/12-Stream-/345/255/220/345/215/217/350/256/256.md +329 -0
- package/_packed_docs/protocol/13-Agent/350/241/214/344/270/272/350/247/204/350/214/203.md +141 -0
- package/_packed_docs/protocol/14-/344/272/244/344/272/222/346/234/272/345/210/266-/345/223/215/345/272/224/346/250/241/345/274/217/344/270/216/350/207/252/344/270/273/346/250/241/345/274/217.md +170 -0
- package/_packed_docs/protocol/README.md +71 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +118 -0
- package/_packed_docs/protocol/agent.md/examples/codeagent-claudecode.md +61 -0
- package/_packed_docs/protocol/agent.md/examples/human-developer.md +60 -0
- package/_packed_docs/protocol/agent.md/examples/openclaw-lobster.md +52 -0
- package/_packed_docs/protocol/aun-docs-guide.md +49 -0
- package/_packed_docs/protocol/index.md +114 -0
- package/_packed_docs/protocol//350/215/211/346/241/210-agent.md/347/255/276/345/220/215/345/215/217/350/256/256.md +205 -0
- package/_packed_docs/protocol//350/215/211/346/241/210-/346/213/222/347/273/235/344/277/241/345/217/267/345/215/217/350/256/256.md +249 -0
- package/_packed_docs/protocol//351/231/204/345/275/225A-/346/234/257/350/257/255/350/241/250.md +337 -0
- package/_packed_docs/protocol//351/231/204/345/275/225B-/346/211/251/345/261/225/346/200/247/346/214/207/345/215/227.md +80 -0
- package/_packed_docs/protocol//351/231/204/345/275/225C-/347/247/201/351/222/245/347/256/241/347/220/206/344/270/216/350/272/253/344/273/275/346/201/242/345/244/215.md +704 -0
- package/_packed_docs/protocol//351/231/204/345/275/225D-Root_CA_/346/262/273/347/220/206/346/234/272/345/210/266.md +620 -0
- package/_packed_docs/protocol//351/231/204/345/275/225E-Root_CA_/345/207/206/345/205/245/346/265/201/347/250/213.md +605 -0
- package/_packed_docs/protocol//351/231/204/345/275/225F-Issuer_CA_/347/224/263/350/257/267/346/265/201/347/250/213.md +548 -0
- package/_packed_docs/protocol//351/231/204/345/275/225G-AID_/345/255/244/345/204/277/351/242/204/351/230/262/344/270/216/346/225/221/346/217/264/346/234/272/345/210/266.md +513 -0
- package/_packed_docs/protocol//351/231/204/345/275/225H-Identity/346/234/215/345/212/241/345/256/236/347/216/260/346/214/207/345/215/227.md +619 -0
- package/_packed_docs/protocol//351/231/204/345/275/225I-/350/267/250/345/237/237/346/266/210/346/201/257/350/267/257/347/224/261/345/256/236/347/216/260/346/214/207/345/215/227.md +492 -0
- package/_packed_docs/protocol//351/231/204/345/275/225J-/345/256/242/346/210/267/347/253/257/346/216/245/345/205/245/347/244/272/344/276/213.md +402 -0
- package/_packed_docs/protocol//351/231/204/345/275/225K-Agent_Web/345/217/221/347/216/260/345/215/217/350/256/256.md +130 -0
- package/_packed_docs/protocol//351/231/204/345/275/225L-E2EE/345/256/236/347/216/260/346/214/207/345/215/227.md +267 -0
- package/_packed_docs/protocol//351/231/204/345/275/225M-JWT/350/256/244/350/257/201/345/256/236/347/216/260/346/214/207/345/215/227.md +367 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +223 -0
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +354 -0
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +172 -0
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +373 -0
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +611 -0
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +1152 -0
- package/_packed_docs/sdk/07-/351/224/231/350/257/257/345/244/204/347/220/206.md +150 -0
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +89 -0
- package/_packed_docs/sdk/09-custody-api-manual.md +445 -0
- package/_packed_docs/sdk/09-group-rpc-manual.md +1895 -0
- package/_packed_docs/sdk/09-message-rpc-manual.md +597 -0
- package/_packed_docs/sdk/09-meta-rpc-manual.md +142 -0
- package/_packed_docs/sdk/09-payload-reference.md +702 -0
- package/_packed_docs/sdk/09-storage-rpc-manual.md +408 -0
- package/_packed_docs/sdk/09-stream-rpc-manual.md +275 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +72 -0
- package/_packed_docs/sdk/INDEX.md +131 -0
- package/_packed_docs/sdk/README.md +307 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +13 -11
- package/dist/auth.js.map +1 -1
- package/dist/client.d.ts +38 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +179 -97
- package/dist/client.js.map +1 -1
- package/dist/namespaces/auth.d.ts +1 -0
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +20 -6
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/transport.d.ts +9 -1
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +24 -0
- package/dist/transport.js.map +1 -1
- package/package.json +40 -37
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
# AUN SDK Python - E2EE 加密通信
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 默认行为
|
|
6
|
+
|
|
7
|
+
SDK 默认开启端到端加密:
|
|
8
|
+
|
|
9
|
+
- **P2P 消息**(`message.send`)、**P2P 思考内容**(`message.thought.put`)、**群组消息**(`group.send`)和**群思考内容**(`group.thought.put`)默认加密发送,无需显式传 `encrypt=True`
|
|
10
|
+
- **群组 E2EE** 是必选能力,当前 Python SDK 固定启用;群组密钥的创建、分发、轮换、恢复均自动完成
|
|
11
|
+
- 接收端(推送、pull、`message.thought.get` 和 `group.thought.get`)均自动解密,无需额外操作
|
|
12
|
+
|
|
13
|
+
如需发送明文消息,显式传入 `encrypt=False`:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# 发送明文 P2P 消息
|
|
17
|
+
await client.call("message.send", {
|
|
18
|
+
"to": "bob.agentid.pub",
|
|
19
|
+
"payload": {"type": "text", "text": "这是明文"},
|
|
20
|
+
"encrypt": False,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
# 发送明文群消息
|
|
24
|
+
await client.call("group.send", {
|
|
25
|
+
"group_id": "g-abc123.agentid.pub",
|
|
26
|
+
"payload": {"type": "text", "text": "这是明文"},
|
|
27
|
+
"encrypt": False,
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 发送加密消息
|
|
34
|
+
|
|
35
|
+
`message.send` 默认加密发送,SDK 自动完成加密:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
await client.call("message.send", {
|
|
39
|
+
"to": "bob.agentid.pub",
|
|
40
|
+
"payload": {"type": "text", "text": "秘密消息"},
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
SDK 优先使用 prekey_ecdh_v2,并默认要求前向保密:
|
|
45
|
+
|
|
46
|
+
1. **prekey_ecdh_v2** — 对方有预上传的 prekey,四路 ECDH(ephemeral×prekey + ephemeral×identity + sender×prekey + sender×identity),前向安全,附带发送方签名
|
|
47
|
+
2. **long_term_key** — 对方无 prekey,双路 ECDH(ephemeral×recipient_identity + sender×recipient_identity)+ HKDF 派生密钥(降级模式),附带发送方签名
|
|
48
|
+
|
|
49
|
+
> Python SDK 默认 `require_forward_secrecy=true`,当加密结果不满足前向保密(无论是因为无 prekey 还是 prekey 加密失败降级到 long_term_key)时拒绝发送并抛出错误。需显式配置 `require_forward_secrecy=false` 才允许降级。
|
|
50
|
+
|
|
51
|
+
每条消息独立生成临时 ECDH 密钥对,实现一消息一密钥。
|
|
52
|
+
|
|
53
|
+
## ProtectedHeaders 与可验证上下文
|
|
54
|
+
|
|
55
|
+
`protected_headers` 是 E2EE 信封里的可选元数据字典,语义接近 HTTP headers:适合放 `device_id`、`slot_id`、`device_name`、`os`、`sdk_version`、`app_name` 等需要被接收端识别、且需要防篡改的非业务内容。`payload` 仍然类似 HTTP body,业务内容应放在 `payload` 内。
|
|
56
|
+
|
|
57
|
+
`protected_headers` 会随 E2EE 信封发送,接收端可以读取,因此它提供完整性保护,不提供机密性保护。不要把访问令牌、私钥、隐私正文或其他只允许端到端可见的内容放入 `protected_headers`;这类内容应放进加密的 `payload`。
|
|
58
|
+
|
|
59
|
+
发送方可以在以下 SDK 调用中传入 `protected_headers`,各 SDK 也兼容别名 `headers`:
|
|
60
|
+
|
|
61
|
+
- `message.send`
|
|
62
|
+
- `message.thought.put`
|
|
63
|
+
- `group.send`
|
|
64
|
+
- `group.thought.put`
|
|
65
|
+
|
|
66
|
+
`payload_type` 不需要应用层传入。SDK 会读取加密前 `payload.type`,自动写入 `protected_headers.payload_type`,接收端解密后会校验它与明文 `payload.type` 一致。
|
|
67
|
+
|
|
68
|
+
示例:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from aun_core import ProtectedHeaders
|
|
72
|
+
|
|
73
|
+
headers = (
|
|
74
|
+
ProtectedHeaders()
|
|
75
|
+
.set("device_id", "dev-123")
|
|
76
|
+
.set("slot_id", "desktop")
|
|
77
|
+
.set("sdk_version", "0.2.8")
|
|
78
|
+
.set("app_name", "my-agent")
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
await client.call("message.send", {
|
|
82
|
+
"to": "bob.agentid.pub",
|
|
83
|
+
"payload": {"type": "text", "text": "秘密消息"},
|
|
84
|
+
"protected_headers": headers,
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
应用层也可以直接传普通字典:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
await client.call("group.send", {
|
|
92
|
+
"group_id": "10001.example.com",
|
|
93
|
+
"payload": {"type": "text", "text": "群组消息"},
|
|
94
|
+
"protected_headers": {"device_id": "dev-123", "slot_id": "desktop"},
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 防篡改机制
|
|
99
|
+
|
|
100
|
+
为兼容旧 E2EE 信封,`protected_headers` 和 `context` 不加入原有信封整体 AAD。它们各自带一个 `_auth` 字段,自包含完整性校验信息:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"type": "e2ee.encrypted",
|
|
105
|
+
"ciphertext": "...",
|
|
106
|
+
"protected_headers": {
|
|
107
|
+
"device_id": "dev-123",
|
|
108
|
+
"slot_id": "desktop",
|
|
109
|
+
"payload_type": "text",
|
|
110
|
+
"_auth": {
|
|
111
|
+
"alg": "HMAC-SHA256",
|
|
112
|
+
"tag": "base64..."
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"context": {
|
|
116
|
+
"type": "run",
|
|
117
|
+
"id": "run-xxx",
|
|
118
|
+
"_auth": {
|
|
119
|
+
"alg": "HMAC-SHA256",
|
|
120
|
+
"tag": "base64..."
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
计算规则:
|
|
127
|
+
|
|
128
|
+
1. 解密流程会派生出本条消息的 `message_key`。
|
|
129
|
+
2. `metadata_key = HMAC-SHA256(message_key, "aun-envelope-metadata-key-v1")`。
|
|
130
|
+
3. 对字典去掉 `_auth` 后做 canonical JSON:UTF-8、key 排序、紧凑分隔符。
|
|
131
|
+
4. `tag = HMAC-SHA256(metadata_key, domain + "\0" + canonical_json(body))`。
|
|
132
|
+
5. `domain` 对 `protected_headers` 为 `aun-protected-headers-v1`,对 `context` 为 `aun-protected-context-v1`。
|
|
133
|
+
|
|
134
|
+
只有持有本条消息密钥的发送端和接收端能生成或验证 `_auth.tag`。中间服务可以看到这些元数据,但不能在不破坏校验的情况下修改它们。
|
|
135
|
+
|
|
136
|
+
兼容策略:
|
|
137
|
+
|
|
138
|
+
- 老消息没有 `protected_headers` / `context` 时照常解密。
|
|
139
|
+
- 新消息一旦携带 `protected_headers` 或 E2EE 信封内的 `context`,就必须携带对应 `_auth`。
|
|
140
|
+
- `_auth` 校验失败、`payload_type` 与解密后 `payload.type` 不一致,或信封内 `context` 与外层 thought selector 不一致时,SDK 视为解密失败。
|
|
141
|
+
|
|
142
|
+
### 接收端读取
|
|
143
|
+
|
|
144
|
+
SDK 会在验签/验 `_auth` 后,把 `_auth` 去掉,只把业务可见字段回传给应用层:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
msg = result["messages"][0]
|
|
148
|
+
headers = msg.get("e2ee", {}).get("protected_headers", {})
|
|
149
|
+
device_id = headers.get("device_id")
|
|
150
|
+
payload_type = headers.get("payload_type")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
对于 thought,顶层 `context.type + context.id` 仍是服务端定位 thought head 的 selector;E2EE 信封内的 `context` 只是对这个 selector 的端到端完整性绑定。
|
|
154
|
+
|
|
155
|
+
## P2P 思考内容
|
|
156
|
+
|
|
157
|
+
P2P 思考内容不是普通消息,不广播、不进 `message.pull`、不分配 seq、无需 ack,也不持久化。它只通过 `message.thought.put/get` 读写,并强制使用 P2P E2EE。
|
|
158
|
+
|
|
159
|
+
thought selector 只使用顶层 `context.type + context.id`,推荐 `{"type": "run", "id": "run-xxx"}`。如果需要展示被引用消息摘要,应放在加密后的 `payload.quote` 或 `payload.client_context` 中,不作为服务端 selector。
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
await client.call("message.thought.put", {
|
|
163
|
+
"to": "bob.agentid.pub",
|
|
164
|
+
"context": {"type": "run", "id": "run-xxx"},
|
|
165
|
+
"payload": {"type": "thought", "text": "先核对 Bob 的约束,再输出答复"},
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
读取对方写给当前用户的 thought 时,指定 `sender_aid` 和同一个 `context`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
result = await client.call("message.thought.get", {
|
|
173
|
+
"sender_aid": "bob.agentid.pub",
|
|
174
|
+
"context": {"type": "run", "id": "run-xxx"},
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
读取自己写给对方的 thought 时,还需要指定 `peer_aid` 或 `to`:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
result = await client.call("message.thought.get", {
|
|
182
|
+
"sender_aid": "alice.agentid.pub",
|
|
183
|
+
"peer_aid": "bob.agentid.pub",
|
|
184
|
+
"context": {"type": "run", "id": "run-xxx"},
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
SDK 返回的 `result["thoughts"]` 是已解密数组。`message.thought.get` 是查询操作,重复读取同一条 thought 不按消息 replay 消费处理。
|
|
189
|
+
|
|
190
|
+
## 群组加密消息
|
|
191
|
+
|
|
192
|
+
SDK 的群组 E2EE 编排对使用者透明:
|
|
193
|
+
- **建群**:`group.create` 成功后,SDK 自动为 owner 初始化 epoch 1 并异步同步到服务端
|
|
194
|
+
- **加人**:成员加入、审批通过或邀请码入群后,SDK 自动 CAS 轮换 epoch,并通过 P2P E2EE 分发新 epoch 密钥
|
|
195
|
+
- **发送**:`group.send` 默认加密,建群后即可立即发送
|
|
196
|
+
- **思考内容**:`group.thought.put` 强制走群 E2EE;`group.thought.get` 返回前由 SDK 解密为 `thoughts[]`
|
|
197
|
+
|
|
198
|
+
### 前置配置
|
|
199
|
+
|
|
200
|
+
群组 E2EE 是必选能力,所有客户端必须支持。当前 Python SDK 固定启用,无需也不能通过配置关闭;成员加入时也固定轮换 epoch,无应用层开关:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
client = AUNClient({})
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**重要**:所有客户端必须声明群组 E2EE 能力。服务端当前仅对在线客户端做能力校验;离线客户端入群时不做强制检查。消息是否加密由发送者自主决定。
|
|
207
|
+
|
|
208
|
+
### 发送
|
|
209
|
+
|
|
210
|
+
`group.send` 默认加密发送:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
await client.call("group.send", {
|
|
214
|
+
"group_id": "g-abc123.agentid.pub",
|
|
215
|
+
"payload": {"type": "text", "text": "群组加密消息"},
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
SDK 自动处理:
|
|
220
|
+
- 建群后自动创建群组密钥(`group.create` 返回后)
|
|
221
|
+
- 加人后自动 CAS 轮换密钥并分发给当前成员(含新成员)(`group.add_member` 返回后)
|
|
222
|
+
- 踢人后自动 CAS 轮换密钥并分发给剩余成员(`group.kick` 返回后)
|
|
223
|
+
- 成员退群后由剩余在线 admin/owner 收到 `group.changed` 事件后自动 CAS 轮换密钥(离开者自身不执行轮换)
|
|
224
|
+
- 审批通过后自动 CAS 轮换密钥并分发给当前成员(含新成员)(`group.review_join_request` 返回后)
|
|
225
|
+
- 批量审批通过后自动 CAS 轮换密钥并分发给当前成员(含新成员)(`group.batch_review_join_request` 返回后)
|
|
226
|
+
- 通过邀请码入群(`group.use_invite_code`)后,SDK 自动向群内 admin/owner 发起密钥恢复请求;恢复是异步过程,后续 pull 或再次收到群消息时才能成功解密
|
|
227
|
+
|
|
228
|
+
### 接收
|
|
229
|
+
|
|
230
|
+
推送和 pull 均自动解密:
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
# 推送
|
|
234
|
+
client.on("group.message_created", lambda msg: print(msg["payload"]))
|
|
235
|
+
|
|
236
|
+
# Pull
|
|
237
|
+
result = await client.call("group.pull", {"group_id": "g-abc123.agentid.pub", "after_message_seq": 0})
|
|
238
|
+
for msg in result["messages"]:
|
|
239
|
+
if msg.get("e2ee"):
|
|
240
|
+
print(f"加密消息: {msg['payload']}")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### 群思考内容
|
|
244
|
+
|
|
245
|
+
群思考内容不是普通群消息,不广播、不进 `group.pull`、不分配 seq、无需 ack,也不持久化。它只通过 `group.thought.put/get` 读写,并强制使用群组 E2EE。
|
|
246
|
+
|
|
247
|
+
群 thought selector 同样只使用顶层 `context.type + context.id`。
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
await client.call("group.thought.put", {
|
|
251
|
+
"group_id": "g-abc123.agentid.pub",
|
|
252
|
+
"context": {"type": "run", "id": "run-xxx"},
|
|
253
|
+
"payload": {"type": "thought", "text": "正在比较两个候选方案"},
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
读取时必须指定 thought 作者和同一个 `context`:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
result = await client.call("group.thought.get", {
|
|
261
|
+
"group_id": "g-abc123.agentid.pub",
|
|
262
|
+
"sender_aid": "alice.agentid.pub",
|
|
263
|
+
"context": {"type": "run", "id": "run-xxx"},
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
SDK 返回的 `result["thoughts"]` 是已解密数组。`group.thought.get` 是查询操作,重复读取同一条 thought 不按消息 replay 消费处理。
|
|
268
|
+
|
|
269
|
+
### 手动操作(通常不需要)
|
|
270
|
+
|
|
271
|
+
`rotate_epoch()` 是纯本地密码学 API。多客户端并发环境下,应通过服务端 CAS(`group.e2ee.get_epoch` + `group.e2ee.rotate_epoch`)确保原子性,SDK 自动编排已使用 CAS 路径。
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
# 手动轮换 epoch(纯本地,不经过服务端 CAS 校验)
|
|
275
|
+
info = client.group_e2ee.rotate_epoch("g-abc123.agentid.pub", member_aids)
|
|
276
|
+
|
|
277
|
+
# 查询状态
|
|
278
|
+
client.group_e2ee.has_secret("g-abc123.agentid.pub") # 是否持有密钥
|
|
279
|
+
client.group_e2ee.current_epoch("g-abc123.agentid.pub") # 当前 epoch
|
|
280
|
+
client.group_e2ee.get_member_aids("g-abc123.agentid.pub") # 已知成员列表
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## 接收加密消息
|
|
284
|
+
|
|
285
|
+
### 推送接收
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
def handler(msg):
|
|
289
|
+
if msg.get("encrypted"):
|
|
290
|
+
print(f"加密消息: {msg['payload']}")
|
|
291
|
+
|
|
292
|
+
client.on("message.received", handler)
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Pull 接收
|
|
296
|
+
|
|
297
|
+
`message.pull` 返回的消息已自动解密:
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
result = await client.call("message.pull", {"after_seq": 0, "limit": 50})
|
|
301
|
+
for msg in result["messages"]:
|
|
302
|
+
if msg.get("encrypted"):
|
|
303
|
+
print(f"加密消息: {msg['payload']}")
|
|
304
|
+
print(f"加密模式: {msg['e2ee']['encryption_mode']}")
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Prekey 管理
|
|
310
|
+
|
|
311
|
+
连接时 SDK 自动上传 prekey,并定时轮换(默认每小时)。一般无需手动管理。
|
|
312
|
+
|
|
313
|
+
手动上传 prekey:
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
# 通过 AUNClient 上传(生成 + RPC)
|
|
317
|
+
await client._upload_prekey()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
底层 API(E2EEManager 只生成材料,不做 RPC):
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
prekey_material = client.e2ee.generate_prekey()
|
|
324
|
+
# 返回 {"prekey_id": "...", "public_key": "...", "signature": "...", "created_at": ...}
|
|
325
|
+
# 需要自行上传:await transport.call("message.e2ee.put_prekey", prekey_material)
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## 裸 WebSocket 开发者指南
|
|
330
|
+
|
|
331
|
+
> 本节面向不使用 AUNClient 连接管理、自行管理 WebSocket 连接的开发者。
|
|
332
|
+
|
|
333
|
+
### 架构说明
|
|
334
|
+
|
|
335
|
+
`E2EEManager` 是纯密码学工具类,无 I/O 依赖。构造函数只需:
|
|
336
|
+
|
|
337
|
+
- `identity_fn` — 返回当前身份信息的函数
|
|
338
|
+
- `keystore` — 密钥存储实现
|
|
339
|
+
|
|
340
|
+
所有 I/O(获取证书、获取 prekey、RPC 调用)由调用方自行处理。
|
|
341
|
+
|
|
342
|
+
### 实例化 E2EEManager
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
from aun_core.e2ee import E2EEManager
|
|
346
|
+
from aun_core.keystore.file import FileKeyStore
|
|
347
|
+
|
|
348
|
+
e2ee = E2EEManager(
|
|
349
|
+
identity_fn=lambda: my_identity,
|
|
350
|
+
keystore=FileKeyStore("~/.aun/myapp"),
|
|
351
|
+
)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 加密消息
|
|
355
|
+
|
|
356
|
+
```python
|
|
357
|
+
# 1. 获取对方证书(HTTP)和 prekey(RPC,可选)
|
|
358
|
+
peer_cert_pem = await fetch_cert(peer_aid) # 调用方实现
|
|
359
|
+
prekey = await fetch_prekey(peer_aid) # 调用方实现,可能为 None
|
|
360
|
+
|
|
361
|
+
# 2. 加密(传入 prekey 会自动缓存,后续可传 None 复用缓存)
|
|
362
|
+
envelope, ok = e2ee.encrypt_message(
|
|
363
|
+
to_aid="bob.agentid.pub",
|
|
364
|
+
payload={"type": "text", "text": "秘密消息"},
|
|
365
|
+
peer_cert_pem=peer_cert_pem,
|
|
366
|
+
prekey=prekey,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# 3. 通过自己的 WebSocket 发送
|
|
370
|
+
aad = envelope["aad"]
|
|
371
|
+
await my_rpc_call("message.send", {
|
|
372
|
+
"to": "bob.agentid.pub",
|
|
373
|
+
"payload": envelope,
|
|
374
|
+
"type": "e2ee.encrypted",
|
|
375
|
+
"encrypted": True,
|
|
376
|
+
"message_id": aad["message_id"],
|
|
377
|
+
"timestamp": aad["timestamp"],
|
|
378
|
+
})
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
P2P 消息的投递语义来自连接阶段声明的 `delivery_mode`:
|
|
382
|
+
|
|
383
|
+
- `fanout`:广播到在线实例,并保留离线历史
|
|
384
|
+
- `queue`:只做单实例实时消费,不进入历史
|
|
385
|
+
|
|
386
|
+
### 解密消息
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
# 解密单条消息(内置本地防重放)
|
|
390
|
+
decrypted = e2ee.decrypt_message(raw_message)
|
|
391
|
+
if decrypted is not None:
|
|
392
|
+
print(decrypted["payload"]) # 明文
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Prekey 缓存
|
|
396
|
+
|
|
397
|
+
E2EEManager 内置 prekey 缓存(TTL 默认 1 小时):
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
# 首次传入 prekey → 自动缓存
|
|
401
|
+
envelope, ok = e2ee.encrypt_message(..., prekey=fetched_prekey)
|
|
402
|
+
|
|
403
|
+
# 后续传 None → 自动复用缓存
|
|
404
|
+
envelope, ok = e2ee.encrypt_message(..., prekey=None)
|
|
405
|
+
|
|
406
|
+
# 手动管理
|
|
407
|
+
e2ee.cache_prekey("bob.agentid.pub", prekey_dict)
|
|
408
|
+
cached = e2ee.get_cached_prekey("bob.agentid.pub")
|
|
409
|
+
e2ee.invalidate_prekey_cache("bob.agentid.pub")
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### 生成 Prekey
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
prekey_material = e2ee.generate_prekey()
|
|
416
|
+
# 返回 {"prekey_id": "...", "public_key": "...", "signature": "...", "created_at": ...}
|
|
417
|
+
# 自行上传到服务端
|
|
418
|
+
await my_rpc_call("message.e2ee.put_prekey", prekey_material)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### 群组 E2EE(GroupE2EEManager)
|
|
422
|
+
|
|
423
|
+
`GroupE2EEManager` 是群组 E2EE 的纯工具类,与 `E2EEManager` 平行。密码学和状态管理全自动,网络发送由调用方负责。
|
|
424
|
+
|
|
425
|
+
#### 实例化
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
from aun_core.e2ee import GroupE2EEManager
|
|
429
|
+
from aun_core.keystore.file import FileKeyStore
|
|
430
|
+
|
|
431
|
+
group_e2ee = GroupE2EEManager(
|
|
432
|
+
identity_fn=lambda: my_identity,
|
|
433
|
+
keystore=FileKeyStore("~/.aun/myapp"),
|
|
434
|
+
)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
#### 建群后创建 epoch 并分发
|
|
438
|
+
|
|
439
|
+
```python
|
|
440
|
+
info = group_e2ee.create_epoch(group_id, member_aids)
|
|
441
|
+
# info = {epoch, commitment, distributions: [{to, payload}, ...]}
|
|
442
|
+
|
|
443
|
+
for dist in info["distributions"]:
|
|
444
|
+
# 通过 P2P E2EE 发送密钥分发消息
|
|
445
|
+
envelope, _ = e2ee.encrypt_message(
|
|
446
|
+
to_aid=dist["to"], payload=dist["payload"],
|
|
447
|
+
peer_cert_pem=fetch_cert(dist["to"]),
|
|
448
|
+
)
|
|
449
|
+
await my_rpc_call("message.send", {
|
|
450
|
+
"to": dist["to"], "payload": envelope,
|
|
451
|
+
"type": "e2ee.encrypted", "encrypted": True,
|
|
452
|
+
...
|
|
453
|
+
})
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
#### 加密群消息
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
envelope = group_e2ee.encrypt(group_id, {"type": "text", "text": "hello"})
|
|
460
|
+
await my_rpc_call("group.send", {
|
|
461
|
+
"group_id": group_id,
|
|
462
|
+
"payload": envelope,
|
|
463
|
+
"type": "e2ee.group_encrypted",
|
|
464
|
+
"encrypted": True,
|
|
465
|
+
})
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
#### 解密群消息
|
|
469
|
+
|
|
470
|
+
```python
|
|
471
|
+
decrypted = group_e2ee.decrypt(raw_group_message)
|
|
472
|
+
# 内置防重放 + 外层 group_id/from/sender_aid 校验
|
|
473
|
+
# 非加密消息原样返回,解密失败返回 None
|
|
474
|
+
|
|
475
|
+
# 批量解密(用于 group.pull)
|
|
476
|
+
results = group_e2ee.decrypt_batch(messages)
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### 处理 P2P 密钥消息
|
|
480
|
+
|
|
481
|
+
所有密钥协议消息(分发/请求/响应)通过 P2P E2EE 传输。收到后先 P2P 解密,再交给 `handle_incoming`:
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
inner = e2ee.decrypt_message(p2p_message) # P2P 层解密
|
|
485
|
+
result = group_e2ee.handle_incoming(inner["payload"])
|
|
486
|
+
|
|
487
|
+
if result == "distribution":
|
|
488
|
+
pass # 密钥已自动存储
|
|
489
|
+
elif result == "distribution_rejected":
|
|
490
|
+
pass # epoch 降级被拒
|
|
491
|
+
elif result == "response":
|
|
492
|
+
pass # 密钥恢复响应已存储
|
|
493
|
+
elif result == "request":
|
|
494
|
+
# 需要回复:查成员列表 → 构建响应 → P2P 发送
|
|
495
|
+
members = get_group_members(group_id)
|
|
496
|
+
response = group_e2ee.handle_key_request_msg(inner["payload"], members)
|
|
497
|
+
if response:
|
|
498
|
+
p2p_e2ee_send(inner["payload"]["requester_aid"], response)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
#### 踢人后轮换
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
info = group_e2ee.rotate_epoch(group_id, remaining_member_aids)
|
|
505
|
+
for dist in info["distributions"]:
|
|
506
|
+
p2p_e2ee_send(dist["to"], dist["payload"])
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
配合服务端 CAS 防脑裂(推荐):
|
|
510
|
+
|
|
511
|
+
```python
|
|
512
|
+
# 1. 读当前 epoch
|
|
513
|
+
status = await my_rpc_call("group.e2ee.get_epoch", {"group_id": group_id})
|
|
514
|
+
# 2. CAS 递增(服务端校验 admin/owner 角色 + 原子递增 + rotation_signature 验签)
|
|
515
|
+
cas = await my_rpc_call("group.e2ee.rotate_epoch", {
|
|
516
|
+
"group_id": group_id, "current_epoch": status["epoch"],
|
|
517
|
+
# Python SDK 自动附加 rotation_signature 和 rotation_timestamp
|
|
518
|
+
# 裸客户端必须自行签名:sign("{group_id}|{current_epoch}|{new_epoch}|{aid}|{timestamp}")
|
|
519
|
+
})
|
|
520
|
+
if cas["success"]:
|
|
521
|
+
info = group_e2ee.rotate_epoch_to(group_id, cas["epoch"], member_aids)
|
|
522
|
+
for dist in info["distributions"]:
|
|
523
|
+
p2p_e2ee_send(dist["to"], dist["payload"])
|
|
524
|
+
# CAS 失败说明别的 admin 先轮换了,放弃即可
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### 加人后轮换并分发密钥
|
|
528
|
+
|
|
529
|
+
```python
|
|
530
|
+
# 成员加入改变成员集,推荐先通过服务端 CAS 推进 epoch。
|
|
531
|
+
status = await my_rpc_call("group.e2ee.get_epoch", {"group_id": group_id})
|
|
532
|
+
cas = await my_rpc_call("group.e2ee.rotate_epoch", {"group_id": group_id, "current_epoch": status["epoch"], ...})
|
|
533
|
+
if cas["success"]:
|
|
534
|
+
info = group_e2ee.rotate_epoch_to(group_id, cas["epoch"], updated_member_aids)
|
|
535
|
+
for dist in info["distributions"]:
|
|
536
|
+
p2p_e2ee_send(dist["to"], dist["payload"])
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
#### 解密失败时请求恢复
|
|
540
|
+
|
|
541
|
+
当前 Python SDK 优先请求本地成员列表中的第一个候选者;零状态时退化为请求当前消息发送者。
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
recovery = group_e2ee.build_recovery_request(
|
|
545
|
+
group_id, epoch, sender_aid=msg.get("sender_aid"),
|
|
546
|
+
)
|
|
547
|
+
if recovery:
|
|
548
|
+
p2p_e2ee_send(recovery["to"], recovery["payload"])
|
|
549
|
+
# 频率限制:同群同 epoch 30 秒内不重复请求
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
#### 状态查询
|
|
553
|
+
|
|
554
|
+
```python
|
|
555
|
+
group_e2ee.has_secret(group_id) # 是否持有密钥
|
|
556
|
+
group_e2ee.current_epoch(group_id) # 当前 epoch,无密钥返回 None
|
|
557
|
+
group_e2ee.get_member_aids(group_id) # 已知成员列表
|
|
558
|
+
group_e2ee.load_all_secrets(group_id) # {epoch: secret_bytes} 映射
|
|
559
|
+
group_e2ee.cleanup(group_id) # 清理过期旧 epoch(默认保留 7 天)
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## 自定义密钥存储
|
|
565
|
+
|
|
566
|
+
实现 `KeyStore` Protocol,可替换默认的文件存储:
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
class MyKeyStore:
|
|
570
|
+
def load_key_pair(self, aid: str) -> dict | None: ...
|
|
571
|
+
def save_key_pair(self, aid: str, key_pair: dict) -> None: ...
|
|
572
|
+
def load_cert(self, aid: str) -> str | None: ...
|
|
573
|
+
def save_cert(self, aid: str, cert_pem: str) -> None: ...
|
|
574
|
+
def load_identity(self, aid: str) -> dict | None: ...
|
|
575
|
+
def save_identity(self, aid: str, identity: dict) -> None: ...
|
|
576
|
+
def load_metadata(self, aid: str) -> dict | None: ...
|
|
577
|
+
def save_metadata(self, aid: str, metadata: dict) -> None: ...
|
|
578
|
+
|
|
579
|
+
client = AUNClient({"aun_path": "..."})
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
`AUNClient` 不接收外部 `keystore` 参数。若需替换 `KeyStore`,应按 SDK 内部接口规范扩展实现,再由对应语言 SDK 的内部装配层接入。
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## 自定义敏感数据存储
|
|
587
|
+
|
|
588
|
+
实现 `SecretStore` Protocol。默认行为:Windows 使用 DPAPI,其他平台使用内存存储。
|
|
589
|
+
|
|
590
|
+
```python
|
|
591
|
+
class MySecretStore:
|
|
592
|
+
def protect(self, scope: str, name: str, plaintext: bytes) -> dict: ...
|
|
593
|
+
def reveal(self, scope: str, name: str, record: dict) -> bytes | None: ...
|
|
594
|
+
def clear(self, scope: str, name: str) -> None: ...
|
|
595
|
+
|
|
596
|
+
client = AUNClient({"aun_path": "..."})
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
`AUNClient` 构造函数只保留 `aun_path`、`root_ca_path`、`seed_password`。`SecretStore` / `KeyStore` / `SQLiteBackup` 属于 SDK 内部基础设施,不作为应用层构造参数暴露。
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## 当前安全默认值
|
|
604
|
+
|
|
605
|
+
| 默认行为 | 说明 |
|
|
606
|
+
|---------|------|
|
|
607
|
+
| P2P 消息默认要求发送方签名 | 无 `sender_signature` 的消息被拒绝 |
|
|
608
|
+
| 群组消息默认要求发送方签名 | `require_signature=True`,无签名或无发送方证书的消息被拒绝 |
|
|
609
|
+
| 群组 E2EE 为固定启用能力 | `group_e2ee=true`,不可关闭 |
|
|
610
|
+
| 默认要求前向保密 | `require_forward_secrecy=true`,无 prekey 时拒绝 long_term_key 降级 |
|
|
611
|
+
| 客户端操作签名 | `group.send`/`group.kick`/`group.add_member`/`group.leave` 等操作自动附加 `client_signature`,服务端强制验签 |
|