@agentunion/fastaun-browser 0.5.19 → 0.5.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 +129 -78
- package/_packed_docs/CHANGELOG.md +129 -78
- package/_packed_docs/INDEX.md +201 -195
- package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
- package/_packed_docs/sdk/INDEX.md +92 -80
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +15 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +64 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +10 -2
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +278 -121
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +46 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +149 -10
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +4 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +23 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +85 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +1581 -781
- package/dist/cert-utils.d.ts +49 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +102 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +7 -0
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +243 -28
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +89 -77
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +99 -80
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -17
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +5 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +33 -14
- package/dist/events.js.map +1 -1
- package/dist/facades.js +5 -5
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +72 -12
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +25 -8
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +142 -73
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +6 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +221 -58
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +2 -1
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +91 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +2 -0
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# 凭证与组织服务 SDK 开发与移植指南
|
|
2
|
+
|
|
3
|
+
> 适用版本:四套 AUN SDK `0.5.19`,组织服务协议 `0.8`。
|
|
4
|
+
>
|
|
5
|
+
> 本文面向实现其它语言 SDK、维护现有四端 SDK 或实现兼容 RPC 客户端的开发者。协议契约优先于任一参考实现;不能把 TypeScript、Python、JavaScript 或 Go 单端行为当作其它语言的隐含规范。
|
|
6
|
+
|
|
7
|
+
## 1. 实现层次和边界
|
|
8
|
+
|
|
9
|
+
SDK 应保持以下分层:
|
|
10
|
+
|
|
11
|
+
1. AUN 连接层负责认证、Gateway、设备/slot、JSON-RPC、重连和 access token。
|
|
12
|
+
2. 协议层负责 canonical JSON、凭证/binding/receipt/representation 的结构校验、签名和完整 Verify。
|
|
13
|
+
3. facade 层只做语言化命名、参数合并和 RPC method 映射;不要在 facade 中复制组织状态机。
|
|
14
|
+
4. 高层身份编排层负责组织实体 AID、名片上传、转让 rekey、checkpoint 和恢复。
|
|
15
|
+
|
|
16
|
+
Credential Service 与 Org Service 是独立服务,SDK 与服务之间只通过 AUN JSON-RPC、文件格式和环境约定协作。Credential 是业务服务:`credential.register/revoke/status/revocationList` 等业务 RPC 必须经 Gateway service-plane attach/dispatch,不得经 Kernel 调用;只有健康、状态诊断等控制面方法走 Kernel 控制面。`credential.status` 带凭证 CID 时始终是业务状态查询,走 service-plane。Credential facade 存在不代表 Credential 服务已部署;当前服务端明确保留但尚未实现的入口只有 `org.resource.create`、`org.task.create`、`org.host.add`、`org.workflow.create`,调用时返回业务码 `NOT_IMPLEMENTED`。其它同名资源、任务、主机和工作流方法不在当前 dispatcher 中,调用时按 JSON-RPC `Method not found` 处理。
|
|
17
|
+
|
|
18
|
+
## 2. 共同密码学和 JSON 规则
|
|
19
|
+
|
|
20
|
+
### 2.1 两类 canonical JSON
|
|
21
|
+
|
|
22
|
+
凭证协议对象(CAP、binding、receipt、representation、服务事件)使用协议 canonical JSON:对象键按 Unicode code point 排序,字符串 NFC,数字为最短十进制形式,拒绝孤立 surrogate;整数必须是有限且安全范围内的数学整数(绝对值不超过 `9007199254740991`)。签名输入是去除 `signature` 后的 UTF-8 bytes。
|
|
23
|
+
|
|
24
|
+
`org.*` 外层 `client_signature` 的请求摘要沿用现有 AUN 请求规则:对象键排序和数值规则相同,但字符串保持原值,不做 NFC;请求解析和签名前仍拒绝孤立 surrogate。两者不能共用一个“先 NFC 再签名”的简化实现。
|
|
25
|
+
|
|
26
|
+
签名算法固定为 ECDSA P-256/SHA-256,签名是 ASN.1 DER,外层是严格标准 Base64(必要时保留 `=`)。签名封套字段为:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{"cert_fingerprint":"sha256:<64位hex>","timestamp":1791321600,"signature":"<base64-der>"}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`public_key_fingerprint` 可选;`algorithm`、`kid`、`value` 等旧字段必须拒绝。证书 resolver 应支持 PEM、DER bytes 和标准 Base64 DER,并验证证书指纹、Common Name/AID、P-256 曲线、公钥指纹和签名时间处于证书有效期。
|
|
33
|
+
|
|
34
|
+
### 2.2 摘要和随机数
|
|
35
|
+
|
|
36
|
+
- 摘要格式统一为 `sha256:<64 位小写十六进制>`。
|
|
37
|
+
- nonce 使用安全随机字节的 base64url;默认请求 nonce 为 32 字节。
|
|
38
|
+
- `request_id` 是业务幂等键;Credential RPC 在同一 method、同一 request ID 下摘要不同时返回 `CREDENTIAL_IDEMPOTENCY_KEY_REUSE`。Org 的幂等冲突按对应 Org 契约返回 `IDEMPOTENCY_KEY_REUSE`。
|
|
39
|
+
- `credential.register` 与补偿 `credential.revoke` 可以复用业务 request ID,但按 method 独立去重。
|
|
40
|
+
- 不要把时间、nonce、CID 或摘要转成浮点;JavaScript/TypeScript 用 safe integer 检查,Go/Python 解析 JSON 时保留数学整数。
|
|
41
|
+
|
|
42
|
+
## 3. AUN-CAP/1 实现契约
|
|
43
|
+
|
|
44
|
+
### 3.1 结构验证
|
|
45
|
+
|
|
46
|
+
根/叶 CAP 至少含 `ver/cid/iat/iss/sub/aud/nbf/exp/cap/key_version/signature`;`delegation_depth`、`delegate`、`parent/proof_chain`、`security_level` 和 `ext` 按条件验证。根凭证的 `delegation_depth` 可省略并按 0 验证,若显式提供则必须为 0;子凭证必须显式提供直接父级有效深度加 1 的值,最大深度为 8。`security_level` 只允许 `low`、`medium`、`high`,缺省按 `medium` 处理;它是验证策略声明,不得被解释为可降低业务端最低安全策略或状态新鲜度要求。未知字段、空 AID、空受众、无约束能力、错误时间顺序、过深父链或错误签名结构必须 fail-closed。
|
|
47
|
+
|
|
48
|
+
成员 CAP 的固定关系是:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
iss = binding.org_id
|
|
52
|
+
sub = binding.member_aid
|
|
53
|
+
aud 包含 binding.org_id
|
|
54
|
+
cap = org / role.<binding.role> / binding.org_id
|
|
55
|
+
ext.credential_type = member
|
|
56
|
+
ext.membership_id/binding_version/membership_terms_hash 与 binding 相等
|
|
57
|
+
ext.bound_to_membership = true
|
|
58
|
+
delegate = false
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`createMemberCredential`/`create_member_credential`/`CreateMemberCredential` 应拒绝对这些字段的冲突覆盖,且不允许成员 CAP 带 parent delegation。
|
|
62
|
+
|
|
63
|
+
### 3.2 验证级别
|
|
64
|
+
|
|
65
|
+
实现至少提供两条明确 API:
|
|
66
|
+
|
|
67
|
+
- `verifySignature`:结构、证书和 CAP 签名,可选时间/受众;不查询状态、不做授权。
|
|
68
|
+
- `verify`:完整验证。
|
|
69
|
+
|
|
70
|
+
完整 Verify 的最小顺序:
|
|
71
|
+
|
|
72
|
+
1. 验证内嵌或外置父链一致性、每一层签名、时间收窄、audience 收窄、能力收窄和 root issuer。
|
|
73
|
+
2. 对每个父文档调用强 `resolveStatusProof`。受信 resolver 必须先验证 Credential 服务的签名、证书信任链和当前撤销状态,并确认 proof 中的查询 `request_id/nonce/target` 与实际请求及服务身份绑定;不能仅凭 proof 自报 `verified:true` 建立信任。SDK 再核对 `aun-credential-status/1`、CID、完整 CAP hash、active、asOf/proofExpiresAt、responder、request_id、nonce、target 和签名时间等字段。父 proof 缺失或畸形返回 `CREDENTIAL_STATUS_UNKNOWN`,CID/hash 明确不等返回 `CREDENTIAL_CHAIN_INVALID`。SDK 检查 proof 字段不等于自行建立 Credential 服务证书信任。
|
|
74
|
+
3. 检查当前 CAP 的 `ns/action/resource/constraints`,包括 path、IP、regex、时间窗口、数值、allowed/denied 和 rate limit。
|
|
75
|
+
4. 检查 `request_binding`:method、服务 target、params_hash、CID、request ID、nonce 和时间窗必须对应真实请求;通用及 admission binding 统一满足 `iat ≤ now < exp ≤ iat+300`。
|
|
76
|
+
5. 成员 CAP 必须有完整当前 binding、双签和 `resolveMembershipStatus=true`;仅凭历史 binding 或 CAP active 不得放行。
|
|
77
|
+
6. 代表调用必须验 representation proof,并通过受信 Ownership/actor 状态确认 actor 有权代表 target;actor 状态未知、封禁或 resolver 缺失拒绝。
|
|
78
|
+
|
|
79
|
+
未显式指定 `now` 时,完成 resolver、成员与代表授权检查(包括异步等待)后重新读取当前时间,复核 CAP、能力时间约束、request binding、状态 proof、已用证书及 representation proof 的有效时间窗,不能沿用检查开始时的时间放行已过期材料。显式传入有效 `now`(Go 为非零 `Now`)时按该固定时间完成验证。
|
|
80
|
+
|
|
81
|
+
根凭证的旧字符串 status callback 可以保留兼容;有 parent 时不能降级为字符串 active。验证函数不得修改输入凭证或补写缺省签名字段。
|
|
82
|
+
|
|
83
|
+
## 4. Membership binding、回执与请求绑定
|
|
84
|
+
|
|
85
|
+
### 4.1 Binding
|
|
86
|
+
|
|
87
|
+
`aun-membership-binding/1` 的不可变 payload 去掉 `member_signature` 和 `org_signature` 后签名。成员和组织必须签同一份条款,且绑定 `membership_id`、`binding_version`、`terms_hash`、`request_id`、nonce 和时间窗。binding 不是能力凭证,状态由 Org Service 保存。
|
|
88
|
+
|
|
89
|
+
实现 `createMembershipBinding` 时允许传入 member signer、org signer 或两者;`signMembershipBinding` 只能让 member AID 签 member、组织 AID 签 org。申请和邀请的签名顺序不同,但 wire 字段相同。
|
|
90
|
+
|
|
91
|
+
### 4.2 登记授权和回执
|
|
92
|
+
|
|
93
|
+
颁发者登记时 `registrar_aid` 必须等于 `credential.iss`。转交服务使用 `registration_authorization`,签名覆盖完整 CAP hash、CID、原 request ID/nonce、Credential 服务 target、relay AID 和有效期;转交服务不能变成颁发者。
|
|
94
|
+
|
|
95
|
+
登记回执是 Credential Service 签名的见证,不是授权凭证。验收时核对 `credential_hash`(包含原 signature)、CID、registrar、service、status、registered_at/exp、request ID、nonce 和当前时间。回执缺失、重复使用、摘要不一致或服务状态未知均拒绝最终化。
|
|
96
|
+
|
|
97
|
+
### 4.3 Org request binding
|
|
98
|
+
|
|
99
|
+
`prepareOrganizationRequest` 等价实现必须:
|
|
100
|
+
|
|
101
|
+
- 对所有 `org.*` 请求加入 `aad.org_id`,拒绝已有不一致 AAD;仅 `org.lifecycle.list`、`org.health`、`org.status` 允许 `org_id:null`。公开的 `org.lifecycle.get/list` 仍按服务端可见性规则处理,不因自动添加 AAD 而要求 CAP;`org.member.list` 等有组织目标的业务方法必须提供非空 `org_id`;
|
|
102
|
+
- 计算 `request_binding.params_hash` 和 representation proof 所需的业务参数摘要时去除 `request_binding`、`client_signature`、所有 `_` 前缀字段和 `credentials.representation_proof`;保留其它 credentials 与 AAD;
|
|
103
|
+
- `request_binding.params_hash` 与 representation proof 使用上述同一业务参数摘要;外层 `client_signature.params_hash` 在发送前对完整准备后的组织参数重新计算,保留 AAD、request binding 和授权包,只去除 `client_signature` 与 `_` 前缀字段,不得复用 `request_binding.params_hash`;
|
|
104
|
+
- 首次 `org.member.join` 和 `org.member.inviteAccept` 一律使用 `membership_id` 专用 admission request binding,不伪造 `credential_cid`;本人和代表调用都必须提交目标成员的 `member_signature`,本人不要求已有 active 成员 CAP;仅代表另一个目标(`for != signer.aid`)时需要 admission representation proof 和当前 ownership/status 校验。本人直接 `inviteAccept` 复用邀请保存的组织签名、CAP 模板和登记授权,SDK 不重签或重造这些材料;
|
|
105
|
+
- 对已有签名、binding、nonce 或外层重复签名做一致性比较,冲突直接报错。
|
|
106
|
+
|
|
107
|
+
外层 client signature 由 RPC pipeline 在发送 `org.*` 时生成,覆盖完整方法名、AID、时间戳和请求摘要。服务端仍须独立重算并验证,不能依赖 Gateway 的 verified 标志。
|
|
108
|
+
|
|
109
|
+
### 4.4 Credential request binding
|
|
110
|
+
|
|
111
|
+
`credential.register`、`credential.revoke`、`credential.status` 和 `credential.revocationList` 都必须携带 `request_binding`,至少绑定 `request_id`、nonce、target、完整 method、业务参数摘要、`iat/exp` 和 `credential_cid`。单凭证方法必须绑定对应非空 CID;`credential.revocationList` 是唯一例外,其 `request_binding.credential_cid` 固定为空字符串,摘要仍覆盖 issuer、cursor、limit、since_event_seq 等全部业务参数。不得把空 CID 例外扩展到其它方法。
|
|
112
|
+
|
|
113
|
+
## 5. Facade 和 RPC 映射
|
|
114
|
+
|
|
115
|
+
### 5.1 四端公共能力
|
|
116
|
+
|
|
117
|
+
| 能力 | Python | TypeScript | JavaScript | Go |
|
|
118
|
+
|---|---|---|---|---|
|
|
119
|
+
| Credential facade | `CredentialFacade` | `CredentialFacade` | `CredentialFacade` | `CredentialFacade` |
|
|
120
|
+
| Org facade | `OrgFacade` | `OrgFacade` | `OrgFacade` | `OrgFacade` |
|
|
121
|
+
| RPC 调用 | `await client.call` | `client.call` Promise | `client.call` Promise | `Call(ctx, method, params)` |
|
|
122
|
+
| 协议层请求准备 helper | `prepare_org_request` | `prepareOrganizationRequest` | `prepareOrganizationRequest` | `PrepareOrganizationRequest` |
|
|
123
|
+
| 完整验证 | `verify_credential` | `verifyCredential` | `verifyCredential` | `VerifyCredential` |
|
|
124
|
+
| 状态 proof | `resolve_status_proof` | `resolveStatusProof` | 可返回 Promise | `ResolveStatusProof` |
|
|
125
|
+
|
|
126
|
+
上表的“协议层请求准备 helper”不是 facade 的公开入口。公开 facade 方法分别为 Python `client.org.prepare_request`、TypeScript/JavaScript `client.org.prepareRequest`、Go `client.Org().PrepareRequest`;它们再调用表中的语言化 protocol helper。组织生命周期 facade 的创建方法也按各语言 API 命名,不应把统一概念名 `createOrg` 当作四端完全相同的公开方法名。
|
|
127
|
+
|
|
128
|
+
### 5.2 当前方法集合
|
|
129
|
+
|
|
130
|
+
Credential facade 映射:`credential.register`、`credential.revoke`、`credential.status`(`getStatus` 为别名)和 `credential.revocationList`,另有本地 `create/verifySignature/verify/verifyRegistrationReceipt`。
|
|
131
|
+
|
|
132
|
+
Org facade 映射:
|
|
133
|
+
|
|
134
|
+
- lifecycle:`create/get/list/update/suspend/resume/dissolve/transfer/transferAccept`;
|
|
135
|
+
- member:`list/get/join/approveJoin/renewCredential/rejectJoin/add/invite/inviteAccept/inviteReject/leave/kick/offboard/freeze/unfreeze/ban/unban`;
|
|
136
|
+
- credential:`getStatus/revocationList`;
|
|
137
|
+
- audit:`query`;authz:`check`;health/status。
|
|
138
|
+
|
|
139
|
+
Python/Go 另提供 `resource/task/host/workflow` 的保留 facade,分别对应 `org.resource.create`、`org.task.create`、`org.host.add`、`org.workflow.create`;当前服务 dispatcher 对这四个方法返回业务码 `NOT_IMPLEMENTED`。其它同名方法不在当前 dispatcher 中,调用时返回 JSON-RPC `Method not found`。`membership.reserve/get/commit/release` 是 Org 服务内部/受限接口,不能作为普通应用公共 API。
|
|
140
|
+
|
|
141
|
+
### 5.3 异步约定
|
|
142
|
+
|
|
143
|
+
Python RPC facade(如 `await client.call(...)`、`await client.org...`)是 async;Python 协议层本地 helper(如 `prepare_org_request`、`verify_credential`、`verify_credential_signature`)是同步函数。Node.js TypeScript 的协议 helper 同步;浏览器 JavaScript 中使用 WebCrypto 的 helper 异步返回 Promise。Go API 返回 `(value, error)`。异步差异只存在语言 API 层,不能改变 canonical bytes、字段缺省、错误码或 RPC envelope。
|
|
144
|
+
|
|
145
|
+
## 6. 高层实体身份和转让编排
|
|
146
|
+
|
|
147
|
+
本节中的 `createOrg`、`completeOrgTransfer` 是高层能力的概念名称,不是四端统一的公开方法名:Python 使用 `client.org.lifecycle.create`、`client.org.lifecycle.transfer_accept`;TypeScript/JavaScript 使用 `client.createOrg`、`client.completeOrgTransfer`;Go 使用 `client.CreateOrg`、`client.CompleteOrgTransfer`。实现这些入口时必须持久化(包括自动生成组织 ID 的创建路径):
|
|
148
|
+
|
|
149
|
+
- 组织实体私钥/证书和当前 owner;
|
|
150
|
+
- 初始创建或转让的输入摘要、proposal、request ID/nonce、阶段 params;
|
|
151
|
+
- transfer rekey 生成的新私钥 checkpoint;
|
|
152
|
+
- 成功后的完整多签名片原文、原 CAS、固定 owner 与实体证书指纹。
|
|
153
|
+
|
|
154
|
+
转让必须先 rekey 再 finalize:新 owner 生成新 P-256 key,rekey 返回新证书后导入,再以新证书重签双方角色材料并登记 CAP。finalize 使用独立持久 request ID;同 proposal、key、owner 和输入摘要恢复,材料改变返回 conflict。不要在 transfer 中复制旧组织私钥或跳过新证书指纹变化检查。
|
|
155
|
+
|
|
156
|
+
解散高层入口以当前 signer 和调用方提供的当前成员 binding/CAP 调用既有 prepare helper,补齐 request ID、nonce、AAD 和 `request_binding`,核验已有请求绑定并保留原成员材料。组织解散成功后记录当前实体证书指纹为失效,并清理创建/转让 checkpoint、待上传名片和当前设备/slot 恢复状态;不删除磁盘私钥,也不追溯使已加载的 AID/signer 对象失效,后续 `AIDStore.load` 应拒绝该证书。实体 agent.md 与通用 Ownership 隔离:实体私钥主签并显式 signer_aid=org_id,当前 owner token 上传,可选当前 owner 附签且逐块全验;现代实体指纹严格匹配,Org 不开放旧实体上传,通用 Ownership facade 对 Org/Group 返回 not_applicable。取消实体额外 V2 授权及其过期窗口;pending 仅由应用显式重试,保留完整原文与原 CAS,并新鲜核对固定 owner/实体代际,转移、rekey 或解散后停止旧待办,不刷新条件或重签。专属 create/transfer 自身 checkpoint 恢复机制不变。复用唯一通用签验 core、显式文件输入、resolver 与 multi-v1 表示协商见 [Ownership 移植指南](Ownership2.1-SDK开发与移植指南.md#4-原始文件严格解析与签名) 和 [API 手册](06-API手册.md#通用-sign--verify);本段不改变 CAP/成员 binding 签名机制。
|
|
157
|
+
|
|
158
|
+
## 7. 服务端兼容实现要点
|
|
159
|
+
|
|
160
|
+
Credential Service:
|
|
161
|
+
|
|
162
|
+
- `credential.register` 只接受完整 CAP,验证父链、CID guard、nonce、登记授权并签回执;
|
|
163
|
+
- `credential.revoke` 按 `(request_id, method)` 去重,支持父链级联和失败补偿封闭 CID;
|
|
164
|
+
- `credential.status` 返回签名 status proof;`revocationList` 返回签名增量;
|
|
165
|
+
- 服务身份或数据库不可用时返回状态未知,不签发临时服务签名。
|
|
166
|
+
|
|
167
|
+
Org Service:
|
|
168
|
+
|
|
169
|
+
- 对所有 `org.*` 独立验 client signature 和 AAD;再按方法及组织可见性执行授权。公开 `org.lifecycle.list/get` 的公开字段读取不要求 CAP/request binding,显式读取受保护字段、私有组织读取及 `org.member.list/get`、`org.credential.getStatus` 等受保护业务读取必须验证相应 CAP、membership binding、request binding、当前 actor/member 状态和权限;health/status 与 Name Service 受信读取按各自诊断/内部投影契约处理;
|
|
170
|
+
- `member.add` 只允许 Org 内部最终化调用;应用不能绕过 join/approve/invite 流程;
|
|
171
|
+
- 外部 Credential 操作不在 Org SQL 事务内,登记/吊销失败须持久化后台收尾;
|
|
172
|
+
- 事件先独立验签,再以 `(responder,event_id,current_aid)` 去重;去重容量为最近 4096 项,不能承诺跨重启 exactly-once。
|
|
173
|
+
|
|
174
|
+
两个服务通过现有 service-plane 注册和 Gateway 路由;服务 AID、数据库和日志目录隔离,不能通过代码 import 或共享表耦合。
|
|
175
|
+
|
|
176
|
+
## 8. 错误模型与兼容性
|
|
177
|
+
|
|
178
|
+
稳定业务错误码至少包括:`CREDENTIAL_INVALID`、`CREDENTIAL_SIGNATURE_INVALID`、`CREDENTIAL_STATUS_UNKNOWN`、`CREDENTIAL_REVOKED`、`CREDENTIAL_EXPIRED`、`CREDENTIAL_CHAIN_INVALID`、`CREDENTIAL_REQUEST_BINDING_INVALID`、`CREDENTIAL_CAPABILITY_DENIED`、`CREDENTIAL_IDEMPOTENCY_KEY_REUSE`、`MEMBERSHIP_STATE_INVALID`、`ROLE_ACCESS_DENIED`、`NOT_IMPLEMENTED`。
|
|
179
|
+
|
|
180
|
+
错误映射必须保留结构化业务码和 retryable 语义。网络/数据库/服务证书不可用可以重试;签名、摘要、字段、权限、状态和幂等 key 冲突不能盲重试。应用层不得根据错误文本推断授权结果。
|
|
181
|
+
|
|
182
|
+
跨平台实现应避免:
|
|
183
|
+
|
|
184
|
+
- 依赖语言默认 JSON 序列化顺序或浮点解析;
|
|
185
|
+
- 把 PEM 文本、DER bytes、Base64 DER 混成同一字符串比较;
|
|
186
|
+
- 使用系统时区计算 Unix 时间或把 `exp` 当成包含端点;
|
|
187
|
+
- 把 Python `None`、JS `undefined`、Go `nil` 无差别写入 wire;
|
|
188
|
+
- 把异步 WebCrypto 的 Promise、Go error 或 Python exception 泄漏进 RPC 字段。
|
|
189
|
+
|
|
190
|
+
## 9. 测试与验收
|
|
191
|
+
|
|
192
|
+
移植顺序建议固定为:
|
|
193
|
+
|
|
194
|
+
1. 先通过共享 protocol vectors:canonical JSON、签名封套、CID/hash、binding、receipt、representation 和错误码。
|
|
195
|
+
2. 再覆盖四类本地 helper:create、signature verify、full Verify、prepare request。
|
|
196
|
+
3. 再覆盖 Credential register/status/revoke/revocationList 的 RPC transcript 和幂等/补偿。
|
|
197
|
+
4. 再覆盖 Org create/join/approve/invite/inviteAccept/renew/offboard/transfer 两阶段和服务事件验签。
|
|
198
|
+
5. 最后做单域、跨语言、跨域和故障恢复;真实 MySQL、多实例部署、Linux/macOS 仍需按服务 TDD 计划单独验收。
|
|
199
|
+
|
|
200
|
+
必须包含的负向向量:错误 issuer/sub/audience、父链 hash 不符、状态 proof 缺字段或过期、成员 binding/CID/version 不符、representation target 改写、AAD/params_hash 冲突、重复 nonce、幂等 key 换摘要、旧证书转让、rekey checkpoint 丢失和 `NOT_IMPLEMENTED` 方法。
|
|
201
|
+
|
|
202
|
+
代码参考:
|
|
203
|
+
|
|
204
|
+
- TypeScript:`ts/src/org-protocol.ts`、`ts/src/facades.ts`;
|
|
205
|
+
- 浏览器 JavaScript:`js/src/org-protocol.ts`、`js/src/facades.ts`;
|
|
206
|
+
- Python:`python/src/aun_core/org_protocol.py`、`python/src/aun_core/org_facades.py`;
|
|
207
|
+
- Go:`go/org_protocol.go`、`go/org_facades.go`;
|
|
208
|
+
- 共同向量:`tests/vectors/org-0.8/protocol.json`;
|
|
209
|
+
- 服务实现:`extensions/services/credential/go`、`extensions/services/org/go`。
|
|
210
|
+
|
|
211
|
+
完整字段和阶段门禁见 [组织服务 0.8 详细设计](../../../extensions/services/docs/org/0.8/17-凭证与组织服务详细设计.md)、[TDD 实施计划](../../../extensions/services/docs/org/0.8/18-凭证与组织服务TDD实施计划.md) 和 [四 SDK 对齐变更清单](../../python/CHANGES_FOR_PARITY.md)。
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# AUN 跨设备已读未读 SDK 实现/移植指南
|
|
2
|
+
|
|
3
|
+
> 面向:Python、TypeScript、JavaScript、Go SDK 维护者,以及实现其它语言 SDK 的开发者
|
|
4
|
+
>
|
|
5
|
+
> 目标:在不改变 AUN wire 契约的前提下,补齐跨设备 P2P/Group 业务已读状态
|
|
6
|
+
>
|
|
7
|
+
> 依据:`docs/跨设备已读未读详细设计.md`、`docs/跨设备已读未读实施计划.md`
|
|
8
|
+
|
|
9
|
+
## 1. 移植原则
|
|
10
|
+
|
|
11
|
+
已读状态是独立的业务状态面。移植实现时应先复现协议和状态机,再接入本语言的 facade、事件循环和缓存。不要把它实现成消息 ACK、Pull 游标、Group 事件游标或 E2EE payload 的别名。
|
|
12
|
+
|
|
13
|
+
必须保持以下不变量:
|
|
14
|
+
|
|
15
|
+
1. 同一 AID、同一 scope、同一 Group/话题的状态只向公共顺序前进;
|
|
16
|
+
2. 不同设备的本地 inbox seq 不参与公共位置比较;
|
|
17
|
+
3. 查询返回 AID 汇总,不返回其它设备明细;
|
|
18
|
+
4. 写入成功后才返回成功并发送变更事件;
|
|
19
|
+
5. 事件丢失不影响最终状态,重连查询可以补偿;
|
|
20
|
+
6. P2P 和 Group 服务使用各自数据库,不能增加跨服务代码或数据库依赖。
|
|
21
|
+
|
|
22
|
+
## 2. 冻结的 wire 契约
|
|
23
|
+
|
|
24
|
+
### 2.1 RPC 方法
|
|
25
|
+
|
|
26
|
+
| Gateway 方法 | 服务内部方法 | 方向 | 说明 |
|
|
27
|
+
| --- | --- | --- | --- |
|
|
28
|
+
| `message.read` | `read` | 写 | 上报 P2P 话题已读位置 |
|
|
29
|
+
| `message.read_state` | `read_state` | 读 | 查询 P2P 汇总 |
|
|
30
|
+
| `group.read` | `read` | 写 | 上报 Group 话题已读位置 |
|
|
31
|
+
| `group.read_state` | `read_state` | 读 | 查询 Group 汇总 |
|
|
32
|
+
|
|
33
|
+
Gateway 对外使用完整 namespace,服务模块内部可以使用短名。`read` 是幂等写操作,客户端可以在超时后重试。
|
|
34
|
+
|
|
35
|
+
### 2.2 上报请求
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"states": [
|
|
40
|
+
{
|
|
41
|
+
"scope": "p2p",
|
|
42
|
+
"topic_key": "inbox:primary",
|
|
43
|
+
"last_read_message_id": "m-100"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"scope": "group",
|
|
47
|
+
"group_id": "team.agentid.pub",
|
|
48
|
+
"topic_key": "chat:general",
|
|
49
|
+
"last_read_message_id": "gm-200"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
上述数组只对照两种状态结构,实际需分别调用 Message/Group 方法。两种方法都允许省略或使用空 scope,分别按 `p2p`、`group` 处理,非空值必须匹配方法;Group 项必须有 `group_id`。只有省略 `states` 时才兼容顶层单条;显式空数组、null 或非数组值拒绝。四个已读门面保留显式 `None/null/nil` 字段供服务端校验,不改变其他门面的参数处理。字符串字段显式传入非字符串或 `null` 必须拒绝。同域批量先校验该域全部项;混合 Group 域逐域校验和提交,任何域失败则 RPC 返回错误,已提交部分通过幂等重试收敛,不承诺跨域回滚。AID、设备 ID 和 slot ID 从认证上下文取得,不从业务参数信任。P2P 已读状态仅由当前身份的 home 域管理;Gateway 绑定认证主体并拒绝 `message.read/read_state` 的 federation 入站和出站,防止伪造 `_auth.aid`。
|
|
56
|
+
|
|
57
|
+
### 2.3 查询请求和响应
|
|
58
|
+
|
|
59
|
+
查询过滤字段为 `scope`、`topic_key`、Group 的 `group_id`,以及只能等于当前认证主体的 `aid`。`message.read_state` 的 `scope` 缺省或为空按 `p2p` 处理,非空值只能为 `p2p`;`group.read_state` 缺省或为空按 `group` 处理,非空值只能为 `group`。`aid` 省略或为空不增加过滤,非空值只能等于当前认证 AID。无过滤字段表示当前 AID 的全部状态。
|
|
60
|
+
|
|
61
|
+
Group 全量或仅按 topic 查询利用完整成员目录发现远域群,逐群查询并合并当前 AID 有权限的状态,不使用默认分页截断范围。
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"scope": "p2p",
|
|
66
|
+
"states": [
|
|
67
|
+
{
|
|
68
|
+
"scope": "p2p",
|
|
69
|
+
"topic_key": "inbox:primary",
|
|
70
|
+
"last_read_message_id": "m-100",
|
|
71
|
+
"last_read_order": 100,
|
|
72
|
+
"current_device_seq": 317,
|
|
73
|
+
"mapping_status": "resolved",
|
|
74
|
+
"updated_at": 1780000000000
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
响应始终包含数组 `states`;单项时可镜像顶层字段,但 SDK 必须以数组为准。`current_device_seq` 只能由当前认证设备映射,无法映射时必须保留 `null`。`mapping_status` 只有 `resolved`、`not_received`、`unresolved` 三种值。
|
|
81
|
+
|
|
82
|
+
网络 JSON 中公共顺序和 seq 仍是整数;Gateway 本域服务和 federation 回程也必须保留整数精度,不能先转换成浮点。Python 使用整数、Go 使用 `int64`;TS/JS 对 `last_read_order` 和非空 `current_device_seq` 在安全范围内使用 number,超出时使用精确十进制字符串,并在 JSON 解码时保留整数原文,内部用精确整数比较。
|
|
83
|
+
|
|
84
|
+
### 2.4 上报响应
|
|
85
|
+
|
|
86
|
+
上报响应统一返回更新和忽略数量,以及与输入项对应的 `states`:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"updated": 1,
|
|
91
|
+
"ignored": 0,
|
|
92
|
+
"states": [
|
|
93
|
+
{
|
|
94
|
+
"scope": "p2p",
|
|
95
|
+
"topic_key": "inbox:primary",
|
|
96
|
+
"last_read_message_id": "m-100",
|
|
97
|
+
"changed": true
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
这些 `states` 是逐项设备写入回执,不是共享汇总,SDK 不得写入共享缓存。`changed=false` 表示重复上报或公共顺序更旧;`changed=true` 也不保证共享汇总前进。重复提交仍是成功的幂等操作,不应再次广播事件。
|
|
104
|
+
|
|
105
|
+
### 2.5 事件
|
|
106
|
+
|
|
107
|
+
事件名固定为 `message.read_changed` 和 `group.read_changed`。payload 包含 `owner_aid`、`scope`、`topic_key`、`last_read_message_id`、`last_read_order`、`updated_at`,Group 另含 `group_id`。事件不包含统一设备 seq,不进入 Pull 队列。
|
|
108
|
+
|
|
109
|
+
事件只在数据库提交成功且 AID 共享汇总前进后发送。Message 事件按 `owner_aid` 绑定来源域,Group 事件按 `group_id` 所属域路由;Gateway/federation 只把状态送到 `owner_aid` 的设备;该 AID 可以是外域 Group 成员,但不能广播给其他成员,显式 route 也不能扩大目标。Gateway 校验 Kernel 转发的可信服务来源;SDK 拒绝缺失 owner、非当前 AID 以及 scope 不匹配原事件的 payload。事件 ID 对 Message 使用 `[owner_aid, topic_key, last_read_message_id]`、对 Group 使用 `[owner_aid, group_id, topic_key, last_read_message_id]` 的 JSON 数组计算 SHA-256;`last_read_order` 不参与摘要,失败日志不包含 topic。
|
|
110
|
+
|
|
111
|
+
## 3. 字段、长度和规范化
|
|
112
|
+
|
|
113
|
+
### 3.1 长度约束
|
|
114
|
+
|
|
115
|
+
`owner_aid`、`device_id`、`group_id`、`topic_key`、`last_read_message_id` 均限制为 128 个字符。长度校验在 RPC handler 和 repository 边界都应有明确入口,不能通过截断来“修复”超长值。
|
|
116
|
+
|
|
117
|
+
`topic_key` 是不透明键:服务端和所有 SDK 必须原样保存、查询、返回和写入缓存。不能 trim、大小写折叠、Unicode 规范化、拆分或从消息内容推导。数据库列使用 `utf8mb4_0900_bin`,确保字节级精确比较。
|
|
118
|
+
|
|
119
|
+
### 3.2 scope 和 Group ID
|
|
120
|
+
|
|
121
|
+
`scope` 是 wire、响应和事件字段;Message 数据库只保存 P2P,Group 数据库只保存 Group,因此状态表不重复落库 `scope`。`group_id` 是 Group 状态的业务键,历史别名先解析为 canonical ID,新写状态键与响应/事件使用 canonical ID,历史别名同时参与事实、公共位置及设备映射的事务查询;不要在 P2P 表加入无意义的 Group 列。
|
|
122
|
+
|
|
123
|
+
## 4. 服务端存储和迁移
|
|
124
|
+
|
|
125
|
+
### 4.1 状态表
|
|
126
|
+
|
|
127
|
+
Message 与 Group 各自创建状态表:
|
|
128
|
+
|
|
129
|
+
| 表 | 字段 | 唯一索引 |
|
|
130
|
+
| --- | --- | --- |
|
|
131
|
+
| `message_read_states` | `id BIGINT UNSIGNED AUTO_INCREMENT`;`owner_aid`、`device_id`、`topic_key`、`last_read_message_id VARCHAR(128)`;`last_read_order`、`updated_at_ms BIGINT`;`is_targeted TINYINT` | `(owner_aid, topic_key, device_id, is_targeted)` |
|
|
132
|
+
| `group_read_states` | `id BIGINT UNSIGNED AUTO_INCREMENT`;`owner_aid`、`device_id`、`group_id`、`topic_key`、`last_read_message_id VARCHAR(128)`;`last_read_order`、`updated_at_ms BIGINT` | `(owner_aid, group_id, topic_key, device_id)` |
|
|
133
|
+
|
|
134
|
+
主键只使用 8 字节自增 ID。按 `utf8mb4` 最坏情况估算,Message 唯一索引约 `128*3*4+1=1537` 字节,Group 唯一索引约 `128*4*4=2048` 字节,均低于 InnoDB 3072 字节上限;不要把多个长字符串拼接成主键。
|
|
135
|
+
|
|
136
|
+
Message 的 `is_targeted=1` 设备事实与共享事实分开保存。定向状态不能覆盖共享状态,也不能触发共享汇总事件。目标元数据晚到时,读取和提交均须重新核实分类;已提交的共享槽候选在事务内按单调规则合并到定向槽,再按完整键及旧消息 ID/order 精确清除共享槽,保留更前的既有定向事实。Group 状态必须复用已有成员权限检查,跨实例权限复用既有 `RequireSendGroup` 与群版本同步,不能以陈旧成员缓存授权。
|
|
137
|
+
|
|
138
|
+
### 4.2 迁移步骤
|
|
139
|
+
|
|
140
|
+
迁移必须可重复执行,并且失败时保留原数据:
|
|
141
|
+
|
|
142
|
+
1. 读取现有表结构,检查所有字符串列的 `CHAR_LENGTH` 是否不超过 128;
|
|
143
|
+
2. 检查旧 scope 是否只有该服务固定值,检查新的唯一键是否冲突;
|
|
144
|
+
3. 检查 `topic_key` 列排序规则并改为同长度的 `utf8mb4_0900_bin`;
|
|
145
|
+
4. 为旧数据回填可解析的 `last_read_order`,未知位置保留 0;
|
|
146
|
+
5. 所有长度、scope 和冲突检查在结构变更前完成,没有 scope 列的旧表也要预检查;不截断、不丢弃数据;
|
|
147
|
+
6. MySQL 多步 DDL 不承诺整体回滚;启动时识别已完成结构,并幂等重试顺序为 0 的回填,避免新增列后回填中断被跳过。
|
|
148
|
+
|
|
149
|
+
如果同时迁移 Message 的 `message_delivery_targets`,也要先检查三列长度和 `(message_id,target_aid)` 冲突,再将复合主键替换为 8 字节自增 `id` 和唯一索引 `(message_id,target_aid)`。该投递目标表仍属于投递面,不能拿来代替已读状态表。
|
|
150
|
+
|
|
151
|
+
## 5. 顺序、未知位置和汇总算法
|
|
152
|
+
|
|
153
|
+
### 5.1 公共顺序
|
|
154
|
+
|
|
155
|
+
P2P 使用消息服务已有的公共创建位置,Group 使用 `group_messages.seq` 或 `group_inbox.seq`。同一 AID、同一 scope、同一话题比较多个设备事实时:
|
|
156
|
+
|
|
157
|
+
1. 先比较公共 `last_read_order`;
|
|
158
|
+
2. 同一公共位置使用 `last_read_message_id` 作为稳定并列键;
|
|
159
|
+
3. 未知顺序不以更新时间或 ID 猜测前进,未知位置不能压低已知位置;
|
|
160
|
+
4. 不使用设备 inbox seq 比较设备之间的新旧。
|
|
161
|
+
|
|
162
|
+
未知消息可以保存 `last_read_order=0` 的待解析事实,查询返回 `unresolved`,不能推进共享汇总或发送 changed 事件。两个未知位置不能用消息 ID 字典序猜测顺序;后续解析出公共位置后,条件回填消息 ID 精确匹配且 order=0 的事实并持久化;CAS 未命中时重读当前事实,再按事务中的公共顺序参与汇总,不能只更新缓存或覆盖并发前进的事实。批次先按既有顺序锁定全部键(Group 包括 aliases)并关闭 Rows,再执行首个普通公共位置查询,避免 REPEATABLE READ 旧快照。
|
|
163
|
+
|
|
164
|
+
### 5.2 当前设备映射
|
|
165
|
+
|
|
166
|
+
查询先通过全部设备事实选出最新公共消息,再只在当前认证设备上查本地映射:
|
|
167
|
+
|
|
168
|
+
- 找到该消息的当前设备 seq:`current_device_seq=<seq>, mapping_status=resolved`;
|
|
169
|
+
- 公共位置已知但当前设备没有投递:`current_device_seq=null, mapping_status=not_received`;
|
|
170
|
+
- 公共位置无法解析或消息已清理:`current_device_seq=null, mapping_status=unresolved`。
|
|
171
|
+
|
|
172
|
+
响应不要泄露其它设备的明细、设备 ID 或设备 seq。状态查询可以读取 pending/inflight 批次,让已经进入队列但尚未落库的事实提前可见;这不改变写入成功和事件发送的提交边界。
|
|
173
|
+
|
|
174
|
+
合并时保留同设备数据库事实与 pending 候选并按公共顺序取最大值。当前设备投递索引均缺失时绕过公共位置缓存回查数据库,避免 GC 后热进程误报 `not_received`。
|
|
175
|
+
|
|
176
|
+
## 6. CACHE 和异步批写
|
|
177
|
+
|
|
178
|
+
Message、Group 服务各自维护有界进程内缓存,默认容量 4096、TTL 30 秒:
|
|
179
|
+
|
|
180
|
+
- Message 设备缓存与 pending 键:`(owner_aid, device_id, scope, topic_key, is_targeted)`;
|
|
181
|
+
- Group 键:`(owner_aid, device_id, group_id, scope, topic_key)`;
|
|
182
|
+
- 公共顺序位置另按消息 ID 缓存,Group 需要包含 `group_id`。
|
|
183
|
+
|
|
184
|
+
写入流程应按以下顺序实现:
|
|
185
|
+
|
|
186
|
+
1. 校验和解析公共位置;
|
|
187
|
+
2. 更新当前键的缓存,并将新值放入按键合并队列;
|
|
188
|
+
3. 同一键只保留公共顺序较新的 pending 值;
|
|
189
|
+
4. 后台每 100ms 或队列达到 100 条时执行一条多值 `INSERT ... ON DUPLICATE KEY UPDATE`;
|
|
190
|
+
5. 每个批次在事务内锁定已提交设备事实,核实同设备更新和共享汇总前进,条件更新按公共顺序和 BINARY message ID 保持单调;
|
|
191
|
+
6. RPC 只等待本次依赖的提交句柄,不等待后续同主体/设备请求;提交后校正回执,每次真实汇总前进由一个句柄领取事件;
|
|
192
|
+
7. 批次失败时重新入队,仅对应句柄收到错误且不发事件;一个 RPC 跨多个批次时,失败不回滚先前提交的批次;
|
|
193
|
+
8. 关闭服务时先刷完队列再关闭数据库,刷写失败必须回传关闭流程。
|
|
194
|
+
|
|
195
|
+
查询需要合并内存中的 pending/inflight 行与已持久化行。成功 RPC 的事实已经持久化;成功返回前进程崩溃可能丢失尚未提交的队列,客户端可以安全重试。不能把 CACHE 当成事实存储。
|
|
196
|
+
|
|
197
|
+
## 7. Gateway、事件和 federation
|
|
198
|
+
|
|
199
|
+
服务端提交后只广播共享汇总前进事件。事件分发失败不回滚状态,客户端通过重连查询补偿。Message AID 客户端登录其所属域,Group 所属域可以经 federation 向外域成员转发。
|
|
200
|
+
|
|
201
|
+
跨域验收必须在本地 Docker 完成,使用 `agentid.pub` 与 `agentid.cn` 两个本地域名;不得把线上域名、线上 Gateway 或线上 MySQL 作为测试依赖。跨域用例需要覆盖:
|
|
202
|
+
|
|
203
|
+
- P2P/Group 消息在两个域间可见后的状态上报和查询;
|
|
204
|
+
- `current_device_seq` 只映射查询所在设备;
|
|
205
|
+
- `message.read_changed` 和 `group.read_changed` 的在线路由;
|
|
206
|
+
- 接收方离线时无事件也能通过查询恢复。
|
|
207
|
+
|
|
208
|
+
## 8. SDK facade 和本地缓存
|
|
209
|
+
|
|
210
|
+
四端都提供相同的逻辑方法:
|
|
211
|
+
|
|
212
|
+
| 语言 | P2P | Group | 事件订阅 |
|
|
213
|
+
| --- | --- | --- | --- |
|
|
214
|
+
| Python | `message.read()` / `message.read_state()` | `group.read()` / `group.read_state()` | `client.on("message.read_changed", handler)` |
|
|
215
|
+
| TypeScript | `message.read()` / `message.readState()` | `group.read()` / `group.readState()` | `client.on(...)`,返回 `Subscription` |
|
|
216
|
+
| JavaScript | `message.read()` / `message.readState()` | `group.read()` / `group.readState()` | `client.on(...)` |
|
|
217
|
+
| Go | `Message().Read(ctx, params)` / `ReadState` | `Group().Read(ctx, params)` / `ReadState` | `client.On(...)` |
|
|
218
|
+
|
|
219
|
+
语言方法名可以采用本语言惯用风格,但 JSON wire 字段必须保持 snake_case:`topic_key`、`last_read_message_id`、`group_id`、`current_device_seq`、`mapping_status`。不要因为 TypeScript/JavaScript 方法使用 `readState` 就把 wire 方法改成 `readState`。
|
|
220
|
+
|
|
221
|
+
### 8.1 观察响应和事件
|
|
222
|
+
|
|
223
|
+
SDK 从查询响应和两个 raw read event 更新共享汇总;写入成功只从输入登记补偿条件,不能将回执当成共享汇总。写入本地汇总缓存时:
|
|
224
|
+
|
|
225
|
+
1. 以 `(scope, group_id, topic_key)` 为缓存键,P2P 的 `group_id` 使用空值;
|
|
226
|
+
2. 只保存共享汇总,不保存设备明细;
|
|
227
|
+
3. 比较已知 `last_read_order`,同序以 Unicode 码点顺序比较 message ID,与 UTF-8 BINARY 一致;未知顺序不凭时间或 ID 猜测前进;
|
|
228
|
+
4. 旧事件和旧查询不能回退缓存,旧事件不交付应用;
|
|
229
|
+
5. 同位置查询可以更新设备映射,同位置事件不重复交付且保留映射,新位置事件不继承旧映射;
|
|
230
|
+
6. `owner_aid` 必须非空且等于当前身份,`scope` 必须匹配原始事件名;不满足条件的事件直接丢弃;
|
|
231
|
+
7. 保留 `current_device_seq=null` 和原始 `mapping_status`。
|
|
232
|
+
|
|
233
|
+
AID 或 device 改变后清理缓存与查询登记项,在途请求和补偿结果必须校验原身份代际,A→B→A 切换也必须隔离,不能写入当前缓存或发布旧代际完成事件;原 RPC 调用仍可向调用者返回结果。两个 raw/public `read_changed` 及 `read_state_sync` 队列项应保留入队时的身份代际,在出队及每个处理器调用前复核;处理器等待期间发生身份切换后,停止余下处理器及协议队列向应用队列的旧事件转发。
|
|
234
|
+
|
|
235
|
+
### 8.2 重连补偿
|
|
236
|
+
|
|
237
|
+
独立于状态缓存登记成功查询的原过滤条件,空结果也登记,全量及按群查询保持原范围;写入输入和有效事件登记对应话题。认证连接成功后调用对应查询,消息 `background_sync=false` 不关闭读状态补偿。失败不清空旧缓存。
|
|
238
|
+
|
|
239
|
+
逐项补偿结束后在 SDK 本地发布 `read_state_sync`,包含 `owner_aid`、去重的 `states` 和 `errors`。`states` 仅包含本轮成功查询涉及的状态键在完成时已接受的最新状态,不夹带空查询的旧缓存;较新事件可替代旧响应,设备映射字段可能缺失。错误项为 `{method, params, error}`,`error` 是错误文本。没有登记项也发空完成事件;身份切换时停止并丢弃旧补偿,不发布旧身份完成事件。应用在连接前订阅,不能把 connect 返回当作补偿完成。
|
|
240
|
+
|
|
241
|
+
### 8.3 代码落点参考
|
|
242
|
+
|
|
243
|
+
当前实现可作为移植时的行为参考。下表路径以 `aun-sdk-core` 项目根目录为基准;服务端 Go 模块位于相邻的 `extensions` 目录:
|
|
244
|
+
|
|
245
|
+
| 层 | 参考路径 |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| Message 状态 repository、迁移与单测 | `../extensions/services/message/go/internal/store/read_state.go`、`../extensions/services/message/go/internal/store/message_store.go`、`../extensions/services/message/go/cmd/message-go/startup_migration_test.go` |
|
|
248
|
+
| Group 状态 repository、服务入口、schema 与单测 | `../extensions/services/group/go/internal/repository/read_state.go`、`../extensions/services/group/go/internal/service/read_state.go`、`../extensions/services/group/go/internal/store/schema.go` |
|
|
249
|
+
| Python facade、事件缓存和重连补偿 | `python/src/aun_core/facades.py`、`python/src/aun_core/client.py` |
|
|
250
|
+
| TypeScript facade、事件接入和重连补偿 | `ts/src/facades.ts`、`ts/src/client.ts` |
|
|
251
|
+
| JavaScript facade、事件接入和重连补偿 | `js/src/facades.ts`、`js/src/client.ts` |
|
|
252
|
+
| Go facade、事件缓存和客户端生命周期 | `go/facades.go`、`go/client.go` |
|
|
253
|
+
|
|
254
|
+
移植时先用这些文件对照 wire 和状态转移,再把相同契约接到目标语言的 transport、事件循环和本地缓存;不要直接复制语言特定的内部类或数据库访问代码。
|
|
255
|
+
|
|
256
|
+
## 9. 各语言移植关注点
|
|
257
|
+
|
|
258
|
+
### Python
|
|
259
|
+
|
|
260
|
+
- facade 使用异步 `read()`、`read_state()`,参数和 wire 字段均为 snake_case;
|
|
261
|
+
- 将 raw event 接到现有 protocol dispatcher,再向应用 dispatcher 发布;
|
|
262
|
+
- 用 Python 整数保存 `last_read_order` 和 seq,保留 `None` 映射;
|
|
263
|
+
- 在连接生命周期的认证成功点调用 `_reconcile_read_states()`。
|
|
264
|
+
|
|
265
|
+
### TypeScript / JavaScript
|
|
266
|
+
|
|
267
|
+
- facade 方法名为 `read()`、`readState()`,调用 `message.read_state`/`group.read_state` wire 方法;
|
|
268
|
+
- 通过 `client.on()` 暴露 `message.read_changed`、`group.read_changed`,订阅句柄支持 `unsubscribe()`;
|
|
269
|
+
- 保留原始 `topic_key`,不要在 facade 统一 trim;
|
|
270
|
+
- 使用 `Map` 保存本地汇总,事件处理必须兼容 `null` 和未知字段;
|
|
271
|
+
- 读状态整数在传输解码时保持精度,比较使用 BigInt,API 输出 number/精确字符串;ID 按 Unicode 码点比较,不能直接使用 UTF-16 字符串大小关系;
|
|
272
|
+
- 重连由现有 lifecycle 驱动,补偿逻辑应挂在认证恢复之后。
|
|
273
|
+
|
|
274
|
+
### Go
|
|
275
|
+
|
|
276
|
+
- facade 使用 `Read(ctx, map[string]any)` 和 `ReadState(ctx, map[string]any)`,底层短名分别为 `read`、`read_state`;
|
|
277
|
+
- 使用 `int64` 保存公共顺序、更新时间和设备 seq,缺失映射用 `nil`;
|
|
278
|
+
- 事件 dispatcher 与 RPC 错误必须区分,事件处理器错误不能阻断其他事件;
|
|
279
|
+
- 共享缓存需要避免并发读写竞态,关闭时等待批写 worker 退出。
|
|
280
|
+
- 补偿使用连接后台任务的 context,不能沿用 Connect 返回后可能取消的调用 context。
|
|
281
|
+
|
|
282
|
+
### 其它语言
|
|
283
|
+
|
|
284
|
+
按以下层次实现即可:
|
|
285
|
+
|
|
286
|
+
1. JSON-RPC/WebSocket transport:原样发送四个完整方法名和 snake_case 参数;
|
|
287
|
+
2. 认证上下文:从当前连接取得 AID、设备 ID、slot ID,不让业务调用者覆盖;
|
|
288
|
+
3. Message/Group facade:提供单条/批量 `read` 和过滤 `read_state`;
|
|
289
|
+
4. 事件 dispatcher:接收两个事件名,隔离处理器异常;
|
|
290
|
+
5. 状态缓存:使用三元业务键和单调比较器,不缓存设备明细;
|
|
291
|
+
6. lifecycle:连接恢复后按独立登记的原过滤项补偿,并通过本地完成事件交付状态及错误;
|
|
292
|
+
7. 类型映射:明确区分空 seq、`not_received` 和 `unresolved`,避免把 JSON `null` 映射成默认整数。
|
|
293
|
+
|
|
294
|
+
## 10. 测试矩阵
|
|
295
|
+
|
|
296
|
+
移植实现至少应通过以下分层测试,按断点续跑原则执行:
|
|
297
|
+
|
|
298
|
+
### 10.1 契约和单元测试
|
|
299
|
+
|
|
300
|
+
- 四个 RPC 方法名、短名/全名路由和 snake_case 字段精确匹配;
|
|
301
|
+
- 顶层单条兼容、显式空/null 批次拒绝、四端显式空值保留、非法 scope、缺 Group ID、空 topic/消息 ID 和 128 字符边界;
|
|
302
|
+
- 原始 topic 的大小写、空格和 Unicode 组合形式保持区分;
|
|
303
|
+
- 单调、重复幂等、旧值忽略、同域参数预校验、跨批次/跨域部分提交后的幂等重试和同名 topic 的 P2P/Group 隔离;
|
|
304
|
+
- `resolved`、`not_received`、`unresolved` 和 `null` 映射;
|
|
305
|
+
- 事件旧值不交付、同位置映射保留、scope 与原事件绑定、必需 owner 校验、空/全量过滤补偿、完成事件及身份切换;
|
|
306
|
+
- TS/JS 超安全整数、Unicode BMP/补充平面 ID 比较及 Go Connect context 取消后补偿;
|
|
307
|
+
- CACHE 命中、TTL 淘汰、定向缓存隔离、晚到位置条件持久回填、Group 别名 canonical 化和跨实例权限同步、同键合并、多值批写、失败重试和关闭刷盘。
|
|
308
|
+
|
|
309
|
+
### 10.2 单域和 E2E
|
|
310
|
+
|
|
311
|
+
在本地 Docker 单域内使用两个设备验证 P2P、Group、多话题、批量、定向消息、成员权限、在线事件和断线重连。E2E 必须同时验证事件丢失后的查询恢复,并确认已读调用没有改变 ACK、Pull 或消息内容。
|
|
312
|
+
|
|
313
|
+
### 10.3 cross-SDK
|
|
314
|
+
|
|
315
|
+
先运行标准参数矩阵验证每个 SDK 的 wire,再使用共享 AID 让 Python、TypeScript、JavaScript、Go 轮流写入同一 AID,其余 SDK 查询同一 Message/Group 状态。每种写入语言都应被其它三种语言读取,且 `mapping_status`、`null` 和事件字段一致。
|
|
316
|
+
|
|
317
|
+
### 10.4 双域
|
|
318
|
+
|
|
319
|
+
只在本地 Docker 的 `agentid.pub` ↔ `agentid.cn` 拓扑运行跨域用例,覆盖 P2P/Group 状态写入、查询、在线事件、设备 seq 映射和离线查询补偿。禁止把公网测试结果当作本地移植实现的通过依据。
|
|
320
|
+
|
|
321
|
+
当前参考入口包括 `python/tests/integration_test_read_state.py`、`python/tests/e2e_test_read_state_multi_device.py`、`tests/cross-sdk`,以及 `../docker-deploy/tests/agentid_cross_domain_e2e.py --phase read-state`。命令、容器启动和断点续跑规则以 `docs/aun测试运行指南.md` 与 `docs/aun分布式测试运行指南.md` 为准;`federation-test/tests/cross_domain_read_state.py` 仅作隔离 federation 参考,不作为本轮双域验收证据。
|
|
322
|
+
|
|
323
|
+
## 11. 发布前检查
|
|
324
|
+
|
|
325
|
+
- 服务端两张状态表的主键为 8 字节自增 ID,唯一索引长度低于 3072 字节;
|
|
326
|
+
- Message 和 Group 状态表分别迁移,`scope` 只保留在 wire,不跨库共享;
|
|
327
|
+
- 写入等待批次提交后才返回成功和发事件;失败批次可重试;
|
|
328
|
+
- 四端方法名、参数、响应、事件和空值语义逐项对照;
|
|
329
|
+
- 服务端与 SDK 均不会 trim 或规范化 `topic_key`;
|
|
330
|
+
- 本地 Docker 单域、E2E、cross-SDK 和 `agentid.pub`/`agentid.cn` 双域测试均有日志;
|
|
331
|
+
- 文档、测试指南和索引中的命令都指向本地容器,不包含线上连接要求。
|