agent-bridge-aun 1.0.41 → 1.0.45-sdk0531.3

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/README.md CHANGED
@@ -6,12 +6,35 @@
6
6
 
7
7
  安装后在 ABWeb 的插件管理中启动 AUN 通道。收到的 AID 消息仅投递给用户在 AUN 身份页中明确关联的 Agent;未关联的 AID 消息会被忽略。旧配置中的 `agent_plugin` 字段不再作为默认路由。配置字段 `owner_aid` 是可选字符串,用作默认 Owner。AID 创建接口为 `POST /api/plugins/aun/aid`,请求内容为 `{"aid":"新AID.agentid.cn","owner_aid":"所有者AID.agentid.cn"}`。
8
8
 
9
- 创建使用 SDK 的兼容注册流程;Owner AID 只保存为本地归属配置,不代表 AUN 协议 Owner 关系已签名或激活。Gateway 信任根使用随插件提供并校验指纹的 AUN 根证书,不从 Gateway 下载未经验证的 CA 链。
9
+ 创建调用 SDK 0.5.31 的 `register(aid, { type: "agent", owner })`。注册、连接成功与 Owner 双边共识是不同状态;Owner 声明仍可能为 `single_sided/pending`,不能把“已创建”解释为归属已激活。创建后尝试把新 AID 加入 Owner 名录,默认 `allow_pa_use: true`;只有本地拥有 Owner 私钥且 Storage 可用时才能同步,失败通过 `directorySync` 单独返回。Gateway 信任根使用随插件提供并校验指纹的 AUN 根证书,不从 Gateway 下载未经验证的 CA 链。
10
10
 
11
11
  核心支持插件命令后,可运行 `mab aun status`、`mab aun list`、`mab aun agents`、`mab aun default-owner`、`mab aun default-owner set <owner-aid>`、`mab aun create <aid> [--owner <owner-aid>] [--agent <plugin-id>]`、`mab aun bind <aid> <plugin-id>` 和 `mab aun unbind <aid>`。关联或解绑只修改指定 AID,不会影响同一 Agent 下的其它 AID,也不会写入 Agent 全局 AID 配置。`default-owner` 不带参数显示当前配置,`default-owner set <owner-aid>` 校验格式并持久化新的默认值。`create` 明确传入 `--owner` 时优先使用该值;省略时回退到同一配置中的 `owner_aid`,两者都没有时返回错误,不会创建 AID。创建新身份时,实际使用的 Owner 会保存为下次默认值。ABWeb 从同一插件配置读取 `owner_aid` 并预填创建表单,因此 CLI 和 ABWeb 共用默认 Owner。Agent 从当前已就绪的插件动态发现。创建后连接或关联失败时,响应会标明 AID 已创建;先用 `mab aun list` 检查,不要重试创建。运行日志不记录原始参数或 Owner AID;默认 Owner 查询会在终端显示当前值。
12
12
 
13
13
  该插件依赖 `@agentunion/fastaun`,要求 Node.js 22.16 或更高版本。
14
14
 
15
+ ## 原生文件命令
16
+
17
+ `mab fs` 与 `mab aun fs` 共用 AUN 插件当前连接,不要求安装或运行 EvolCore。`--as` 只能选择后台已连接的身份;只有一个连接时可省略,多连接时必须指定。远端后端根据经过验证的名片类型选择,不依赖 Owner 名录同步。
18
+
19
+ ```bash
20
+ mab fs stat 209530905.agentid.cn:/.well-known/ability/index.json --as devproj.agentid.cn --json
21
+ mab fs cat 209530905.agentid.cn:/.well-known/ability/index.json --as devproj.agentid.cn --authoritative --json
22
+ mab fs cp ./说明.txt devproj.agentid.cn:/docs/说明.txt --as devproj.agentid.cn
23
+ mab fs cp devproj.agentid.cn:/docs/说明.txt ./下载.txt --as devproj.agentid.cn
24
+ ```
25
+
26
+ 命令包括 `ls`、`stat`、`lstat`、`cat`、`cp`、`mv`、`rm`、`mkdir`、`ln -s`、`chmod`、`setfacl`、`getfacl`、`token`、`find`、`df`、`link`、`mount`、`approve`、`reject` 和 `umount`(也接受 `unmount`)。SDK 后端没有对应能力时返回 `UNSUPPORTED`。群空间当前不支持正文读取、软链、可见性修改、令牌、分享链接或挂载审批;个人空间不支持跨 AID 移动,跨 personal/group 的复制或移动也会拒绝。`stat` 默认不跟随末级软链,`-L` 才跟随。
27
+
28
+ 文本读取最多 256 KiB,二进制摘要输入最多 64 KiB,列表最多 512 项;超限明确失败。普通 Storage 读取按服务内联上限分块,并在读取前后核对大小、版本和已有哈希。本机文件上传和下载使用独立的管理身份校验及二进制传输接口,单文件最多 32 MiB、传输最多 60 秒,不把本机路径交给后台读写。下载先在目标目录暂存,校验大小和 SHA-256 后提交;覆盖已有文件必须显式加 `--overwrite`。`--authoritative` 使用个人 Storage 的权威对象读取,不以缓存参数替代,也不适用于群空间。本机认证管理入口可取得新签发的 token 和分享链接,插件不把这些值写入运行日志。
29
+
30
+ ## AUN 私聊中的兼容入口
31
+
32
+ 私聊可以发送一条完整、单行的 `ec fs cat <AID:/路径>` (可选 `--format json`)。插件直接解析参数,不执行 shell,不安装或替换本机 `ec`。普通聊天提及、引用、代码块和多条命令不会触发执行;消息里的 `--as` 不能选择宿主身份。
33
+
34
+ 兼容入口只读取能够匿名公开访问的文件,不借用宿主对私有对象的权限。公开正文走有大小限制的匿名 HTTPS 读取;`stat`、`lstat`、目录、配额和 ACL 没有可证明的匿名视图,当前返回 `UNSUPPORTED`。私聊兼容入口不接受 `--token` 或 `--authoritative`。文件写入、删除、权限、令牌及挂载变更会提示需要 Owner 在本机明确操作,不接受远端文本自报授权。回复保留原私聊话题。
35
+
36
+ 文件可读只证明 FS 发布可访问。还需核对 L1 的 `publisher`、`capabilities` 和每个精确 `detail_ref` 对应的 L3;在线发现及真实 Ability 调用分别验收,不能以索引可读代替执行成功。
37
+
15
38
  附件回复按图片、视频和普通文件分组发送,并保留原话题和请求引用。图片使用不带普通说明正文的 `type: "image"` 消息,说明和下载链接另发加密文本,兼容 AUN 网页的图片卡片显示规则;MIME 类型为 `video/*` 的附件使用 `type: "video"`,普通文件使用 `type: "file"`,二者正文均附下载链接。三类附件参考 EvolCore 的标准附件格式,由插件直接通过 AUN SDK 上传可公开读取的对象,发送不带受限分享 URL 的标准对象引用,便于接收端取得下载票据及播放器分段读取。持有对象地址者可读取附件;旧版本已发送的私有分享不会自动改为公开。发送失败或状态未确认时仍向原 Peer 提供下载链接,不自动重发附件。图片已发送而说明失败时明确报告说明发送阶段,不误报 FS 上传失败。附件链路不调用 `ec` 命令,也不依赖 EvolCore 运行。
16
39
 
17
40
  ABWeb 的 AUN 设置只展示已连接 AID,并按来源分组。成功注册并记录在插件 `created_aids` 中的 AID 属于本工程;共享密钥库中其余 AID 属于 EvolCore。旧版 `created-aids.json` 记录继续兼容。来源只用于展示,连接和关联由 AUN 插件直接管理。列表先显示已关联 Agent 的身份,各组按最近成功关联的时间倒序排列,时间相同或旧记录没有时间时按 AID 名称排序。关联时间保存在插件配置的 `binding_times` 中,刷新或重启后保留;旧记录不补造时间,再次关联成功后开始记录。
@@ -1,19 +1,126 @@
1
1
  {
2
2
  "type": "object",
3
3
  "properties": {
4
- "identities": { "type": "array", "items": { "type": "string" } },
5
- "created_aids": { "type": "array", "items": { "type": "string" } },
6
- "bindings": { "type": "object", "additionalProperties": { "type": "string" } },
7
- "binding_times": { "type": "object", "additionalProperties": { "type": "string" } },
8
- "aid": { "type": "string", "minLength": 1 },
9
- "owner_aid": { "type": "string", "minLength": 1 },
10
- "agent_plugin": { "type": "string", "description": "兼容旧配置;消息只按 AID 的显式关联路由。" },
11
- "aun_path": { "type": "string", "minLength": 1 },
12
- "encryption_seed": { "type": "string" },
13
- "verify_ssl": { "type": "boolean", "default": true },
14
- "root_ca_path": { "type": "string" },
15
- "allow_from": { "type": "string", "default": "", "description": "留空接受所有 AUN Peer;填写 AID 后限制来源,可用英文逗号分隔。" },
16
- "delivery_mode": { "enum": ["fanout", "queue"] }
4
+ "identities": {
5
+ "type": "array",
6
+ "items": {
7
+ "type": "string"
8
+ }
9
+ },
10
+ "created_aids": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "string"
14
+ }
15
+ },
16
+ "bindings": {
17
+ "type": "object",
18
+ "additionalProperties": {
19
+ "type": "string"
20
+ }
21
+ },
22
+ "binding_times": {
23
+ "type": "object",
24
+ "additionalProperties": {
25
+ "type": "string"
26
+ }
27
+ },
28
+ "aid": {
29
+ "type": "string",
30
+ "minLength": 1
31
+ },
32
+ "owner_aid": {
33
+ "type": "string",
34
+ "minLength": 1
35
+ },
36
+ "agent_plugin": {
37
+ "type": "string",
38
+ "description": "兼容旧配置;消息只按 AID 的显式关联路由。"
39
+ },
40
+ "aun_path": {
41
+ "type": "string",
42
+ "minLength": 1
43
+ },
44
+ "encryption_seed": {
45
+ "type": "string"
46
+ },
47
+ "verify_ssl": {
48
+ "type": "boolean",
49
+ "default": true
50
+ },
51
+ "root_ca_path": {
52
+ "type": "string"
53
+ },
54
+ "allow_from": {
55
+ "type": "string",
56
+ "default": "",
57
+ "description": "留空接受所有 AUN Peer;填写 AID 后限制来源,可用英文逗号分隔。"
58
+ },
59
+ "delivery_mode": {
60
+ "enum": [
61
+ "fanout",
62
+ "queue"
63
+ ]
64
+ },
65
+ "ability_selections": {
66
+ "type": "object",
67
+ "default": {},
68
+ "description": "按 Provider AID 保存明确批准的能力发布路径;默认为不发布。",
69
+ "additionalProperties": {
70
+ "type": "array",
71
+ "items": {
72
+ "type": "string"
73
+ },
74
+ "uniqueItems": true
75
+ }
76
+ },
77
+ "ability_ledgers": {
78
+ "type": "object",
79
+ "default": {},
80
+ "description": "AUN 能力发布归属与中断恢复账本,由通道维护。",
81
+ "additionalProperties": {
82
+ "type": "object",
83
+ "additionalProperties": {
84
+ "type": "object",
85
+ "required": [
86
+ "definition_id",
87
+ "release_id",
88
+ "plugin_id",
89
+ "plugin_version"
90
+ ],
91
+ "properties": {
92
+ "definition_id": {
93
+ "type": "string"
94
+ },
95
+ "release_id": {
96
+ "type": "string"
97
+ },
98
+ "plugin_id": {
99
+ "type": "string"
100
+ },
101
+ "plugin_version": {
102
+ "type": "string"
103
+ },
104
+ "previous_definition_ids": {
105
+ "type": "array",
106
+ "items": {
107
+ "type": "string"
108
+ }
109
+ }
110
+ },
111
+ "additionalProperties": false
112
+ }
113
+ }
114
+ },
115
+ "ability_owners": {
116
+ "type": "object",
117
+ "default": {},
118
+ "description": "宿主管理员为每个 Provider AID 明确委托的能力确认 Owner;不等于 AUN 协议 Ownership,需验证该 AID 的控制权。",
119
+ "additionalProperties": {
120
+ "type": "string",
121
+ "pattern": "^[a-z0-9][a-z0-9_-]{2,63}\\.[a-z0-9-]+(?:\\.[a-z0-9-]+)+$"
122
+ }
123
+ }
17
124
  },
18
125
  "additionalProperties": false
19
126
  }
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AbilityOperationStore = void 0;
4
+ exports.abilityOperationKey = abilityOperationKey;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_path_1 = require("node:path");
8
+ function failure(code, message) {
9
+ return Object.assign(new Error(`${code}: ${message}`), { code });
10
+ }
11
+ function canonical(value, parents = new Set()) {
12
+ if (value === null || typeof value === "string" || typeof value === "boolean")
13
+ return JSON.stringify(value);
14
+ if (typeof value === "number" && Number.isFinite(value))
15
+ return JSON.stringify(value);
16
+ if (!value || typeof value !== "object" || parents.has(value))
17
+ throw failure("params_invalid", "操作数据必须是有效 JSON");
18
+ parents.add(value);
19
+ try {
20
+ if (Array.isArray(value))
21
+ return `[${Array.from(value, item => canonical(item, parents)).join(",")}]`;
22
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
23
+ throw failure("params_invalid", "操作数据必须是普通 JSON 对象");
24
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonical(value[key], parents)}`).join(",")}}`;
25
+ }
26
+ finally {
27
+ parents.delete(value);
28
+ }
29
+ }
30
+ function digest(value) { return (0, node_crypto_1.createHash)("sha256").update(canonical(value)).digest("hex"); }
31
+ function abilityOperationKey(input) {
32
+ return digest([input.providerAid, input.authenticatedCaller, input.operationId]);
33
+ }
34
+ function operationFingerprint(input) {
35
+ return digest({ providerAid: input.providerAid, authenticatedCaller: input.authenticatedCaller, operationId: input.operationId,
36
+ path: input.path, definitionId: input.definitionId, params: input.params, effects: input.effects, executionTarget: input.executionTarget, async: input.async ?? false });
37
+ }
38
+ function confirmationFingerprint(input, fingerprint) {
39
+ return digest({ key: abilityOperationKey(input), fingerprint, expiresAt: input.expiresAt });
40
+ }
41
+ function validateInput(input) {
42
+ for (const field of ["providerAid", "authenticatedCaller", "operationId", "callId", "path", "definitionId"]) {
43
+ if (typeof input[field] !== "string" || !input[field] || input[field].length > 512 || /[\u0000-\u001f\u007f]/.test(input[field]))
44
+ throw failure("params_invalid", `操作字段 ${field} 无效`);
45
+ }
46
+ if (!input.params || typeof input.params !== "object" || Array.isArray(input.params)
47
+ || !input.executionTarget || typeof input.executionTarget !== "object" || Array.isArray(input.executionTarget)
48
+ || !Array.isArray(input.effects) || !input.effects.length || !input.effects.every(item => typeof item === "string" && item.length > 0)
49
+ || !Number.isSafeInteger(input.expiresAt) || input.expiresAt <= 0
50
+ || !Number.isSafeInteger(input.dedupeRetentionSeconds) || input.dedupeRetentionSeconds <= 0
51
+ || !Number.isSafeInteger(input.dedupeRetentionSeconds * 1000)
52
+ || (input.async !== undefined && typeof input.async !== "boolean"))
53
+ throw failure("params_invalid", "操作契约、参数或过期时间无效");
54
+ if (Buffer.byteLength(canonical(input.params)) > 256 * 1024)
55
+ throw failure("params_invalid", "操作参数超过 256 KiB");
56
+ canonical(input.executionTarget);
57
+ }
58
+ function response(entry, code, message, executionState = "not_executed") {
59
+ return { type: "ability", role: "result", call_id: entry.callId, operation_id: entry.operationId, ability_path: entry.path, definition_id: entry.definitionId,
60
+ ok: false, execution_state: executionState, error: { code, message } };
61
+ }
62
+ function validEntry(value) {
63
+ if (!value || typeof value !== "object" || Array.isArray(value))
64
+ return false;
65
+ const entry = value;
66
+ try {
67
+ validateInput(entry);
68
+ if (entry.key !== abilityOperationKey(entry) || entry.fingerprint !== operationFingerprint(entry)
69
+ || entry.confirmationFingerprint !== confirmationFingerprint(entry, entry.fingerprint)
70
+ || !["pending_confirmation", "executing", "succeeded", "failed", "unknown"].includes(entry.state)
71
+ || ![entry.createdAt, entry.updatedAt, entry.retainUntil].every(Number.isSafeInteger)
72
+ || entry.createdAt < 0 || entry.updatedAt < entry.createdAt || entry.retainUntil < entry.createdAt + entry.dedupeRetentionSeconds * 1000)
73
+ return false;
74
+ if (["succeeded", "failed", "unknown"].includes(entry.state) && (!entry.result || typeof entry.result !== "object" || Array.isArray(entry.result)))
75
+ return false;
76
+ if (["pending_confirmation", "executing"].includes(entry.state) && entry.result !== undefined)
77
+ return false;
78
+ if (entry.result !== undefined)
79
+ canonical(entry.result);
80
+ return true;
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ class AbilityOperationStore {
87
+ file;
88
+ now;
89
+ entries = new Map();
90
+ writes = Promise.resolve();
91
+ constructor(file, now) {
92
+ this.file = file;
93
+ this.now = now;
94
+ }
95
+ static async open(file, options = {}) {
96
+ const store = new AbilityOperationStore(file, options.now ?? Date.now);
97
+ let raw;
98
+ try {
99
+ raw = await (0, promises_1.readFile)(file, "utf8");
100
+ }
101
+ catch (error) {
102
+ if (error.code === "ENOENT")
103
+ return store;
104
+ throw error;
105
+ }
106
+ let snapshot;
107
+ try {
108
+ snapshot = JSON.parse(raw);
109
+ }
110
+ catch {
111
+ throw failure("operation_store_corrupt", "操作账本损坏,已停止新的执行");
112
+ }
113
+ if (!snapshot || snapshot.version !== 1 || !Array.isArray(snapshot.entries) || !snapshot.entries.every(validEntry))
114
+ throw failure("operation_store_corrupt", "操作账本格式无效,已停止新的执行");
115
+ let migrated = false;
116
+ for (const entry of snapshot.entries) {
117
+ if (store.entries.has(entry.key))
118
+ throw failure("operation_store_corrupt", "操作账本含重复幂等键");
119
+ const legacyExpired = entry.result?.error?.code === "expired";
120
+ if (legacyExpired) {
121
+ const result = structuredClone(entry.result);
122
+ result.error = { ...result.error, code: "message_expired" };
123
+ store.entries.set(entry.key, { ...entry, result });
124
+ migrated = true;
125
+ }
126
+ else
127
+ store.entries.set(entry.key, entry);
128
+ }
129
+ if (migrated)
130
+ await store.persist(new Map(store.entries));
131
+ await store.interruptExecuting();
132
+ return store;
133
+ }
134
+ serial(run) {
135
+ const next = this.writes.catch(() => undefined).then(run);
136
+ this.writes = next;
137
+ return next;
138
+ }
139
+ async persist(next) {
140
+ const directory = (0, node_path_1.dirname)(this.file);
141
+ await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
142
+ const temp = `${this.file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
143
+ const data = `${JSON.stringify({ version: 1, entries: [...next.values()] })}\n`;
144
+ try {
145
+ const handle = await (0, promises_1.open)(temp, "wx", 0o600);
146
+ try {
147
+ await handle.writeFile(data, "utf8");
148
+ await handle.sync();
149
+ }
150
+ finally {
151
+ await handle.close();
152
+ }
153
+ await (0, promises_1.rename)(temp, this.file);
154
+ this.entries = next;
155
+ }
156
+ finally {
157
+ await (0, promises_1.rm)(temp, { force: true });
158
+ }
159
+ }
160
+ expirePending(entries, now) {
161
+ const next = new Map(entries);
162
+ let changed = false;
163
+ for (const entry of entries.values()) {
164
+ if (entry.state !== "pending_confirmation" || now < entry.expiresAt)
165
+ continue;
166
+ next.set(entry.key, { ...entry, state: "failed", updatedAt: Math.max(entry.updatedAt, now),
167
+ retainUntil: Math.max(entry.retainUntil, now + entry.dedupeRetentionSeconds * 1000),
168
+ result: response(entry, "message_expired", "调用消息已过期,不会执行") });
169
+ changed = true;
170
+ }
171
+ return { entries: next, changed };
172
+ }
173
+ draft(input) {
174
+ return this.serial(async () => {
175
+ validateInput(input);
176
+ const copied = JSON.parse(canonical(input));
177
+ const key = abilityOperationKey(copied);
178
+ const fingerprint = operationFingerprint(copied);
179
+ const now = this.now();
180
+ const expired = this.expirePending(this.entries, now);
181
+ const next = new Map([...expired.entries].filter(([, item]) => !["succeeded", "failed"].includes(item.state) || item.retainUntil > now));
182
+ const existing = next.get(key);
183
+ if (existing) {
184
+ if (expired.changed)
185
+ await this.persist(next);
186
+ if (existing.fingerprint !== fingerprint)
187
+ throw failure("idempotency_conflict", "相同 operation_id 的请求内容已改变");
188
+ return { created: false, entry: structuredClone(existing) };
189
+ }
190
+ const retainUntil = now + copied.dedupeRetentionSeconds * 1000;
191
+ if (!Number.isSafeInteger(retainUntil))
192
+ throw failure("params_invalid", "操作保留期无效");
193
+ const entry = { ...copied, key, fingerprint, confirmationFingerprint: confirmationFingerprint(copied, fingerprint),
194
+ state: "pending_confirmation", createdAt: now, updatedAt: now, retainUntil };
195
+ next.set(key, entry);
196
+ await this.persist(next);
197
+ return { created: true, entry: structuredClone(entry) };
198
+ });
199
+ }
200
+ approve(key, fingerprint) {
201
+ return this.confirm(key, fingerprint, true);
202
+ }
203
+ reject(key, fingerprint) {
204
+ return this.confirm(key, fingerprint, false);
205
+ }
206
+ confirm(key, fingerprint, approve) {
207
+ return this.serial(async () => {
208
+ const expired = this.expirePending(this.entries, this.now());
209
+ if (expired.changed)
210
+ await this.persist(expired.entries);
211
+ const existing = this.entries.get(key);
212
+ if (!existing)
213
+ throw failure("operation_not_found", "操作记录不存在");
214
+ if (existing.confirmationFingerprint !== fingerprint)
215
+ throw failure("confirmation_mismatch", "确认内容与原请求不一致");
216
+ if (existing.state !== "pending_confirmation")
217
+ return { started: false, entry: structuredClone(existing) };
218
+ const now = this.now();
219
+ const messageExpired = now >= existing.expiresAt;
220
+ const started = approve && !messageExpired;
221
+ const entry = { ...existing, state: started ? "executing" : "failed", updatedAt: Math.max(existing.updatedAt, now),
222
+ retainUntil: Math.max(existing.retainUntil, now + existing.dedupeRetentionSeconds * 1000),
223
+ ...(!started ? { result: messageExpired ? response(existing, "message_expired", "调用消息已过期,不会执行") : response(existing, "user_denied", "Owner 已拒绝本次调用") } : {}) };
224
+ const next = new Map(this.entries);
225
+ next.set(key, entry);
226
+ await this.persist(next);
227
+ return { started, entry: structuredClone(entry) };
228
+ });
229
+ }
230
+ finish(key, state, result) {
231
+ return this.serial(async () => {
232
+ const existing = this.entries.get(key);
233
+ if (!existing)
234
+ throw failure("operation_not_found", "操作记录不存在");
235
+ if (existing.state !== "executing")
236
+ throw failure("operation_not_executing", "只有本次已批准执行的操作可以保存结果");
237
+ if (!result || typeof result !== "object" || Array.isArray(result))
238
+ throw failure("result_invalid", "操作结果必须是 JSON 对象");
239
+ const saved = JSON.parse(canonical(result));
240
+ if (Buffer.byteLength(canonical(saved)) > 512 * 1024)
241
+ throw failure("result_invalid", "操作结果超过 512 KiB");
242
+ const now = this.now();
243
+ const entry = { ...existing, state, updatedAt: Math.max(existing.updatedAt, now), retainUntil: Math.max(existing.retainUntil, now + existing.dedupeRetentionSeconds * 1000),
244
+ result: { ...saved, type: "ability", role: "result", call_id: existing.callId, operation_id: existing.operationId, ability_path: existing.path, definition_id: existing.definitionId } };
245
+ const next = new Map(this.entries);
246
+ next.set(key, entry);
247
+ await this.persist(next);
248
+ return structuredClone(entry);
249
+ });
250
+ }
251
+ replay(key, callId) {
252
+ return this.serial(async () => {
253
+ if (typeof callId !== "string" || !callId || callId.length > 512 || /[\u0000-\u001f\u007f]/.test(callId))
254
+ throw failure("params_invalid", "调用关联标识无效");
255
+ const expired = this.expirePending(this.entries, this.now());
256
+ if (expired.changed)
257
+ await this.persist(expired.entries);
258
+ const existing = this.entries.get(key);
259
+ if (!existing)
260
+ return undefined;
261
+ const entry = structuredClone(existing);
262
+ entry.callId = callId;
263
+ if (entry.result)
264
+ entry.result.call_id = callId;
265
+ return entry;
266
+ });
267
+ }
268
+ get(key) {
269
+ return this.serial(async () => {
270
+ const expired = this.expirePending(this.entries, this.now());
271
+ if (expired.changed)
272
+ await this.persist(expired.entries);
273
+ const entry = this.entries.get(key);
274
+ return entry ? structuredClone(entry) : undefined;
275
+ });
276
+ }
277
+ interruptExecuting() {
278
+ return this.serial(async () => {
279
+ const executing = [...this.entries.values()].filter(entry => entry.state === "executing");
280
+ if (!executing.length)
281
+ return;
282
+ const next = new Map(this.entries);
283
+ for (const entry of executing)
284
+ next.set(entry.key, { ...entry, state: "unknown", updatedAt: Math.max(entry.updatedAt, this.now()), result: response(entry, "execution_unknown", "执行曾被中断,必须先核对结果,不会自动重试", "unknown") });
285
+ await this.persist(next);
286
+ });
287
+ }
288
+ pending() {
289
+ return this.serial(async () => {
290
+ const expired = this.expirePending(this.entries, this.now());
291
+ if (expired.changed)
292
+ await this.persist(expired.entries);
293
+ return [...this.entries.values()].filter(entry => entry.state === "pending_confirmation").map(entry => structuredClone(entry));
294
+ });
295
+ }
296
+ invalidateProvider(providerAid, code = "provider_invalidated", message = "Provider 发布或 Owner policy 已改变,本次调用不会执行") {
297
+ return this.serial(async () => {
298
+ if (typeof providerAid !== "string" || !providerAid || typeof code !== "string" || !code || typeof message !== "string" || !message)
299
+ throw failure("params_invalid", "Provider 失效参数无效");
300
+ const now = this.now();
301
+ const expired = this.expirePending(this.entries, now);
302
+ const next = expired.entries;
303
+ let invalidated = 0;
304
+ for (const entry of next.values()) {
305
+ if (entry.providerAid !== providerAid || entry.state !== "pending_confirmation")
306
+ continue;
307
+ next.set(entry.key, { ...entry, state: "failed", updatedAt: Math.max(entry.updatedAt, now),
308
+ retainUntil: Math.max(entry.retainUntil, now + entry.dedupeRetentionSeconds * 1000),
309
+ result: response(entry, code, message) });
310
+ invalidated++;
311
+ }
312
+ if (invalidated || expired.changed)
313
+ await this.persist(next);
314
+ return invalidated;
315
+ });
316
+ }
317
+ }
318
+ exports.AbilityOperationStore = AbilityOperationStore;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAbilityMessage = isAbilityMessage;
4
+ exports.parseAbilityEnvelope = parseAbilityEnvelope;
5
+ exports.abilityFailureResult = abilityFailureResult;
6
+ const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
7
+ function isAbilityMessage(value) {
8
+ if (!value || typeof value !== "object")
9
+ return false;
10
+ const body = value.payload;
11
+ return !!body && typeof body === "object" && !Array.isArray(body) && body.type === "ability";
12
+ }
13
+ function parseAbilityEnvelope(value) {
14
+ if (!isAbilityMessage(value))
15
+ return undefined;
16
+ const event = value;
17
+ const envelope = event.envelope && typeof event.envelope === "object" ? event.envelope : undefined;
18
+ // fastaun 解密事件的顶层 from 来自认证信封,不能读取业务 payload 自报身份。
19
+ const callerAid = typeof event.from === "string" ? event.from : typeof envelope?.from === "string" ? envelope.from : "";
20
+ if (envelope?.from !== undefined && envelope.from !== callerAid)
21
+ return undefined;
22
+ const payload = event.payload;
23
+ if (!aidPattern.test(callerAid) || event.encrypted !== true || payload.role !== "call")
24
+ return undefined;
25
+ if (typeof payload.call_id !== "string" || !payload.call_id || typeof payload.operation_id !== "string" || !payload.operation_id || typeof payload.ability_path !== "string")
26
+ return undefined;
27
+ if (payload.ability_path !== "protocol/discover" && typeof payload.definition_id !== "string")
28
+ return undefined;
29
+ if (payload.params !== undefined && (!payload.params || typeof payload.params !== "object" || Array.isArray(payload.params)))
30
+ return undefined;
31
+ return { callerAid, encrypted: true, payload };
32
+ }
33
+ function abilityFailureResult(payload, code, message) {
34
+ return {
35
+ type: "ability", role: "result",
36
+ call_id: payload.call_id,
37
+ operation_id: payload.operation_id,
38
+ ...(typeof payload.ability_path === "string" ? { ability_path: payload.ability_path } : {}),
39
+ ...(typeof payload.definition_id === "string" ? { definition_id: payload.definition_id } : {}),
40
+ ok: false, execution_state: "not_executed", error: { code, message },
41
+ };
42
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OwnerProof = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const lifetime = 10 * 60_000;
6
+ const limit = 1024;
7
+ const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
8
+ function key(session, providerAid, ownerAid) {
9
+ return JSON.stringify([session.id, providerAid, ownerAid]);
10
+ }
11
+ function validSession(session, now) {
12
+ return typeof session.id === "string" && session.id.length > 0 && session.id.length <= 512
13
+ && !/[\u0000-\u001f\u007f]/.test(session.id) && Number.isSafeInteger(session.expiresAt) && session.expiresAt > now;
14
+ }
15
+ class OwnerProof {
16
+ entries = new Map();
17
+ now;
18
+ constructor(options = {}) { this.now = options.now ?? Date.now; }
19
+ prune(now) {
20
+ for (const [recordKey, entry] of this.entries)
21
+ if (entry.expiresAt <= now)
22
+ this.entries.delete(recordKey);
23
+ }
24
+ start(session, providerAid, ownerAid) {
25
+ const now = this.now();
26
+ if (!validSession(session, now))
27
+ throw new Error("管理会话无效或已过期");
28
+ if (typeof providerAid !== "string" || !aidPattern.test(providerAid) || typeof ownerAid !== "string" || !aidPattern.test(ownerAid))
29
+ throw new Error("Provider 或 Owner AID 无效");
30
+ this.prune(now);
31
+ const recordKey = key(session, providerAid, ownerAid);
32
+ if (!this.entries.has(recordKey) && this.entries.size >= limit)
33
+ throw new Error("Owner 证明记录已达上限,请稍后重试");
34
+ const nonce = (0, node_crypto_1.randomBytes)(32).toString("hex");
35
+ const expiresAt = Math.min(session.expiresAt, now + lifetime);
36
+ this.entries.set(recordKey, { providerAid, ownerAid, sessionExpiresAt: session.expiresAt, expiresAt, nonceHash: (0, node_crypto_1.createHash)("sha256").update(nonce).digest("hex") });
37
+ return { nonce, expiresAt };
38
+ }
39
+ accept(providerAid, authenticatedSenderAid, nonce, encrypted) {
40
+ const now = this.now();
41
+ this.prune(now);
42
+ if (encrypted !== true || !authenticatedSenderAid || typeof nonce !== "string" || !/^[0-9a-f]{64}$/.test(nonce))
43
+ return false;
44
+ const nonceHash = (0, node_crypto_1.createHash)("sha256").update(nonce).digest("hex");
45
+ for (const entry of this.entries.values()) {
46
+ if (entry.providerAid !== providerAid || entry.ownerAid !== authenticatedSenderAid || entry.nonceHash !== nonceHash)
47
+ continue;
48
+ delete entry.nonceHash;
49
+ entry.expiresAt = Math.min(entry.sessionExpiresAt, now + lifetime);
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ verified(session, providerAid, ownerAid) {
55
+ const now = this.now();
56
+ this.prune(now);
57
+ if (!validSession(session, now))
58
+ return false;
59
+ const entry = this.entries.get(key(session, providerAid, ownerAid));
60
+ return entry !== undefined && entry.nonceHash === undefined;
61
+ }
62
+ clear(providerAid) {
63
+ if (providerAid === undefined)
64
+ this.entries.clear();
65
+ else
66
+ for (const [recordKey, entry] of this.entries)
67
+ if (entry.providerAid === providerAid)
68
+ this.entries.delete(recordKey);
69
+ }
70
+ }
71
+ exports.OwnerProof = OwnerProof;