agent-bridge-aun 1.0.49 → 1.0.58
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 +8 -1
- package/dist/src-ts/plugins/aun/ability-control.js +26 -4
- package/dist/src-ts/plugins/aun/ability-operation-store.js +90 -10
- package/dist/src-ts/plugins/aun/agentmd-command.js +67 -0
- package/dist/src-ts/plugins/aun/agentmd-menu.js +222 -0
- package/dist/src-ts/plugins/aun/agentmd-natural.js +17 -0
- package/dist/src-ts/plugins/aun/fs.js +2 -1
- package/dist/src-ts/plugins/aun/index.js +427 -60
- package/dist/src-ts/plugins/aun/menu.js +333 -0
- package/dist/src-ts/plugins/aun/public/app.js +14 -7
- package/dist/src-ts/plugins/aun/runtime-menu.js +52 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ mab aun repair-old-aids --json
|
|
|
23
23
|
|
|
24
24
|
安装后在 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"}`。
|
|
25
25
|
|
|
26
|
-
创建调用 SDK 0.5.31 的 `register(aid, { type: "agent", owner })`。注册、连接成功与 Owner 双边共识是不同状态;Owner 声明仍可能为 `single_sided/pending
|
|
26
|
+
创建调用 SDK 0.5.31 的 `register(aid, { type: "agent", owner })`。注册、连接成功与 Owner 双边共识是不同状态;Owner 声明仍可能为 `single_sided/pending`,不能把“已创建”解释为归属已激活。创建后先读取新鲜签名归属;关联请求已送达但 Owner 尚未确认时,接口返回 `202`、`status: "等待 Owner 确认"`,页面和 CLI 仅给普通提醒,日志为 `info`,不重复注册或发送邀请,也不尝试用 Owner 公钥登录。仅共识成立后尝试把新 AID 加入 Owner 名录,默认 `allow_pa_use: true`;只有本地拥有 Owner 私钥且 Storage 可用时才能同步,失败通过 `directorySync` 单独返回。Gateway 信任根使用随插件提供并校验指纹的 AUN 根证书,不从 Gateway 下载未经验证的 CA 链。
|
|
27
27
|
|
|
28
28
|
核心支持插件命令后,可运行 `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 查询会在终端显示当前值。
|
|
29
29
|
|
|
@@ -72,3 +72,10 @@ AUN 独立包支持核心后台每五分钟检测更新;可信提供者在自
|
|
|
72
72
|
## Ability Owner 确认
|
|
73
73
|
|
|
74
74
|
能力发布和远程调用审批由配置的 `ability_owners[providerAid]` 对应 Owner 在 AUN 加密私聊中管理。发送 `/ability help` 查看命令;`/ability list` 查看能力,`/ability publish <路径...>` 发布,`/ability revoke` 撤销,`/ability pending` 查看待审批清单。逐项处理使用通知中的 `/ability approve <操作键> <确认指纹>` 或 `/ability reject <操作键> <确认指纹>`。只有一项已送达申请时可直接回复“确认”或“拒绝”;多项需先送达固定清单,再用这两个词批量处理。新申请不会加入已展示清单,插件重载后需重新发送 `/ability pending`。Owner 首次发送有效管理命令时,AUN 验证其已配置 AID 并持久保存验证记录;远端消息不能设置或认领 Owner。能力的网页发布、Owner 验证和待审批区域已从 ABWeb 移除;已认证的本机管理 API 保留用于兼容旧管理客户端。
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
### 按 Peer 持续授权
|
|
78
|
+
|
|
79
|
+
Owner 在完整调用申请通知下回复“允许12小时”,或使用 `/ability approve <操作键> <确认指纹> 12h`,可允许所示 Peer 对此 Provider 的此项能力连续调用12小时,覆盖该契约的所有合法参数。另可回复“永久允许”或 `/ability approve <操作键> <确认指纹> permanent`,授权同一范围直到撤销或配置改变。普通“确认”仍只批准本次。多项时先 `/ability pending` 核对固定清单。通知里的完整授权标签也可直接回复;缺少完整清单时会重新展示,核对后再回复,不会将审批标签交给桌面执行。
|
|
80
|
+
|
|
81
|
+
`/ability grants` 查看到期及撤销命令,`/ability revoke-grant <授权ID>` 随时撤销。重载不会续期,其他 Peer 或能力不能复用;取消发布、解绑或更换 Owner 会撤销。已过 TTL 的旧调用不会因授权恢复执行。详见本工程 `docs/aun-ability-call-flow.md`。
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isAbilitySelectionText = isAbilitySelectionText;
|
|
3
4
|
exports.parseAbilityControl = parseAbilityControl;
|
|
4
5
|
exports.parseAbilityControlMessage = parseAbilityControlMessage;
|
|
5
6
|
const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
|
|
6
7
|
const pathPattern = /^(?!protocol\/|definitions\/)[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?(?:\/[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?)+$/;
|
|
7
8
|
const fingerprintPattern = /^[0-9a-f]{64}$/;
|
|
8
9
|
const maxControlTextLength = 4096;
|
|
10
|
+
const selectionPattern = /^\d+(?:[,\s]+\d+)*$/;
|
|
11
|
+
function isAbilitySelectionText(text) {
|
|
12
|
+
return selectionPattern.test(text.trim());
|
|
13
|
+
}
|
|
9
14
|
function parseAbilityControl(text) {
|
|
10
15
|
if (text === "确认")
|
|
11
16
|
return { action: "confirm-view" };
|
|
12
17
|
if (text === "拒绝")
|
|
13
18
|
return { action: "reject-view" };
|
|
19
|
+
if (text === "允许12小时" || text === "允许该 Peer 使用此能力12小时")
|
|
20
|
+
return { action: "grant-view", grantDurationMs: 43_200_000 };
|
|
21
|
+
if (text === "永久允许" || text === "永久允许该 Peer 使用此能力")
|
|
22
|
+
return { action: "grant-view", grantDurationMs: null };
|
|
14
23
|
const trimmed = text.replace(/^ +| +$/g, "");
|
|
24
|
+
if (isAbilitySelectionText(trimmed)) {
|
|
25
|
+
const indexes = trimmed.replace(/,/g, " ").split(/\s+/).map(value => Number(value));
|
|
26
|
+
if (indexes.some(value => !Number.isSafeInteger(value) || value < 1))
|
|
27
|
+
throw new Error("Ability 清单编号格式无效:请使用从 1 开始的编号");
|
|
28
|
+
if (new Set(indexes).size !== indexes.length)
|
|
29
|
+
throw new Error("Ability 清单编号不能重复");
|
|
30
|
+
return { action: "select-view", indexes };
|
|
31
|
+
}
|
|
15
32
|
if (!/^\/ability(?:\s|$)/.test(trimmed))
|
|
16
33
|
return undefined;
|
|
17
34
|
if (text.length > maxControlTextLength)
|
|
@@ -19,7 +36,7 @@ function parseAbilityControl(text) {
|
|
|
19
36
|
if (!/^[a-z0-9./ _-]+$/.test(trimmed))
|
|
20
37
|
throw new Error("Ability 管理命令格式无效:请发送单行完整命令,仅使用空格分隔参数");
|
|
21
38
|
const [, action, ...args] = trimmed.split(/ +/);
|
|
22
|
-
if (["help", "list", "revoke", "pending"].includes(action) && args.length === 0)
|
|
39
|
+
if (["help", "list", "revoke", "pending", "grants"].includes(action) && args.length === 0)
|
|
23
40
|
return { action: action };
|
|
24
41
|
if (action === "publish") {
|
|
25
42
|
if (!args.length)
|
|
@@ -30,10 +47,15 @@ function parseAbilityControl(text) {
|
|
|
30
47
|
throw new Error("Ability 发布路径不能重复");
|
|
31
48
|
return { action, paths: args };
|
|
32
49
|
}
|
|
33
|
-
if ((action === "approve" || action === "reject") && args.length === 2) {
|
|
34
|
-
if (!args.every(value => fingerprintPattern.test(value)))
|
|
50
|
+
if ((action === "approve" || action === "reject") && (args.length === 2 || args.length === 3)) {
|
|
51
|
+
if (!args.slice(0, 2).every(value => fingerprintPattern.test(value)))
|
|
35
52
|
throw new Error("Ability 操作键与确认指纹格式无效:必须是完整的 64 位小写十六进制摘要");
|
|
36
|
-
|
|
53
|
+
if (args.length === 3 && (action !== "approve" || !["12h", "permanent"].includes(args[2])))
|
|
54
|
+
throw new Error("持续授权只支持批准命令的 12h 或 permanent 选项");
|
|
55
|
+
return { action, key: args[0], confirmationFingerprint: args[1], ...(args.length === 3 ? { grantDurationMs: args[2] === "permanent" ? null : 43_200_000 } : {}) };
|
|
56
|
+
}
|
|
57
|
+
if (action === "revoke-grant" && args.length === 1 && /^[0-9a-f-]{16,128}$/.test(args[0])) {
|
|
58
|
+
return { action, grantId: args[0] };
|
|
37
59
|
}
|
|
38
60
|
throw new Error("Ability 管理命令或参数数量不正确,请发送 /ability help 查看用法");
|
|
39
61
|
}
|
|
@@ -90,10 +90,22 @@ function validEntry(value) {
|
|
|
90
90
|
return false;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
function validGrant(value) {
|
|
94
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
95
|
+
return false;
|
|
96
|
+
const grant = value;
|
|
97
|
+
return [grant.id, grant.providerAid, grant.authenticatedCaller, grant.path, grant.definitionId, grant.issuedByAid].every(value => typeof value === "string" && value.length > 0 && value.length <= 512)
|
|
98
|
+
&& Array.isArray(grant.effects) && grant.effects.length > 0 && grant.effects.every(value => typeof value === "string")
|
|
99
|
+
&& !!grant.executionTarget && typeof grant.executionTarget === "object" && !Array.isArray(grant.executionTarget) && grant.executionTarget.ownerAid === grant.issuedByAid
|
|
100
|
+
&& Number.isSafeInteger(grant.createdAt) && grant.createdAt >= 0
|
|
101
|
+
&& (grant.expiresAt === null || Number.isSafeInteger(grant.expiresAt) && grant.expiresAt - grant.createdAt === 43_200_000)
|
|
102
|
+
&& (grant.revokedAt === undefined || Number.isSafeInteger(grant.revokedAt) && grant.revokedAt >= grant.createdAt);
|
|
103
|
+
}
|
|
93
104
|
class AbilityOperationStore {
|
|
94
105
|
file;
|
|
95
106
|
now;
|
|
96
107
|
entries = new Map();
|
|
108
|
+
grants = new Map();
|
|
97
109
|
writes = Promise.resolve();
|
|
98
110
|
constructor(file, now) {
|
|
99
111
|
this.file = file;
|
|
@@ -117,7 +129,9 @@ class AbilityOperationStore {
|
|
|
117
129
|
catch {
|
|
118
130
|
throw failure("operation_store_corrupt", "操作账本损坏,已停止新的执行");
|
|
119
131
|
}
|
|
120
|
-
if (!snapshot || snapshot.version
|
|
132
|
+
if (!snapshot || ![1, 2].includes(snapshot.version) || !Array.isArray(snapshot.entries) || !snapshot.entries.every(validEntry)
|
|
133
|
+
|| (snapshot.version === 2 && !Array.isArray(snapshot.grants))
|
|
134
|
+
|| (snapshot.grants !== undefined && (!Array.isArray(snapshot.grants) || !snapshot.grants.every(validGrant))))
|
|
121
135
|
throw failure("operation_store_corrupt", "操作账本格式无效,已停止新的执行");
|
|
122
136
|
let migrated = false;
|
|
123
137
|
for (const saved of snapshot.entries) {
|
|
@@ -137,6 +151,12 @@ class AbilityOperationStore {
|
|
|
137
151
|
store.entries.set(entry.key, entry);
|
|
138
152
|
migrated ||= legacy;
|
|
139
153
|
}
|
|
154
|
+
for (const saved of (snapshot.grants ?? [])) {
|
|
155
|
+
if (store.grants.has(saved.id))
|
|
156
|
+
throw failure("operation_store_corrupt", "授权账本含重复授权 ID");
|
|
157
|
+
store.grants.set(saved.id, saved);
|
|
158
|
+
}
|
|
159
|
+
migrated ||= snapshot.version === 1;
|
|
140
160
|
if (migrated)
|
|
141
161
|
await store.persist(new Map(store.entries));
|
|
142
162
|
await store.interruptExecuting();
|
|
@@ -147,11 +167,11 @@ class AbilityOperationStore {
|
|
|
147
167
|
this.writes = next;
|
|
148
168
|
return next;
|
|
149
169
|
}
|
|
150
|
-
async persist(next) {
|
|
170
|
+
async persist(next, grants = this.grants) {
|
|
151
171
|
const directory = (0, node_path_1.dirname)(this.file);
|
|
152
172
|
await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
|
|
153
173
|
const temp = `${this.file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
154
|
-
const data = `${JSON.stringify({ version:
|
|
174
|
+
const data = `${JSON.stringify({ version: 2, entries: [...next.values()], grants: [...grants.values()] })}\n`;
|
|
155
175
|
try {
|
|
156
176
|
const handle = await (0, promises_1.open)(temp, "wx", 0o600);
|
|
157
177
|
try {
|
|
@@ -163,6 +183,7 @@ class AbilityOperationStore {
|
|
|
163
183
|
}
|
|
164
184
|
await (0, promises_1.rename)(temp, this.file);
|
|
165
185
|
this.entries = next;
|
|
186
|
+
this.grants = grants;
|
|
166
187
|
}
|
|
167
188
|
finally {
|
|
168
189
|
await (0, promises_1.rm)(temp, { force: true });
|
|
@@ -209,14 +230,16 @@ class AbilityOperationStore {
|
|
|
209
230
|
return { created: true, entry: structuredClone(entry) };
|
|
210
231
|
});
|
|
211
232
|
}
|
|
212
|
-
approve(key, fingerprint) {
|
|
213
|
-
return this.confirm(key, fingerprint, true);
|
|
233
|
+
approve(key, fingerprint, options = {}) {
|
|
234
|
+
return this.confirm(key, fingerprint, true, options.grantDurationMs, options.grantId);
|
|
214
235
|
}
|
|
215
236
|
reject(key, fingerprint) {
|
|
216
237
|
return this.confirm(key, fingerprint, false);
|
|
217
238
|
}
|
|
218
|
-
confirm(key, fingerprint, approve) {
|
|
239
|
+
confirm(key, fingerprint, approve, grantDurationMs, grantId) {
|
|
219
240
|
return this.serial(async () => {
|
|
241
|
+
if (grantDurationMs !== undefined && (!approve || grantDurationMs !== null && grantDurationMs !== 43_200_000))
|
|
242
|
+
throw failure("grant_invalid", "持续授权只支持批准 12 小时或永久");
|
|
220
243
|
const expired = this.expirePending(this.entries, this.now());
|
|
221
244
|
if (expired.changed)
|
|
222
245
|
await this.persist(expired.entries);
|
|
@@ -227,6 +250,8 @@ class AbilityOperationStore {
|
|
|
227
250
|
throw failure("confirmation_mismatch", "确认内容与原请求不一致");
|
|
228
251
|
if (existing.state !== "pending_confirmation")
|
|
229
252
|
return { started: false, entry: structuredClone(existing) };
|
|
253
|
+
if (grantId && !this.grantActive(grantId, existing))
|
|
254
|
+
throw failure("grant_unavailable", "持续授权已到期、撤销或范围不匹配");
|
|
230
255
|
const now = this.now();
|
|
231
256
|
const confirmationExpired = now >= existing.confirmationExpiresAt;
|
|
232
257
|
const messageExpired = now >= existing.expiresAt;
|
|
@@ -236,8 +261,56 @@ class AbilityOperationStore {
|
|
|
236
261
|
...(!started ? { result: confirmationExpired ? response(existing, "confirmation_expired", "Owner 确认申请已过期,不会执行") : approve && messageExpired ? response(existing, "message_expired", "调用已超过最晚启动时间") : response(existing, "user_denied", "Owner 已拒绝本次调用") } : {}) };
|
|
237
262
|
const next = new Map(this.entries);
|
|
238
263
|
next.set(key, entry);
|
|
239
|
-
|
|
240
|
-
|
|
264
|
+
let grant = grantId ? this.grants.get(grantId) : undefined;
|
|
265
|
+
const ownerAid = typeof existing.executionTarget.ownerAid === "string" ? existing.executionTarget.ownerAid : "";
|
|
266
|
+
if (grantDurationMs !== undefined && !ownerAid)
|
|
267
|
+
throw failure("grant_invalid", "持续授权缺少明确 Owner");
|
|
268
|
+
if (grantDurationMs !== undefined && ownerAid && !existing.result) {
|
|
269
|
+
grant = { id: (0, node_crypto_1.randomUUID)(), providerAid: existing.providerAid, authenticatedCaller: existing.authenticatedCaller,
|
|
270
|
+
path: existing.path, definitionId: existing.definitionId, effects: [...existing.effects], executionTarget: structuredClone(existing.executionTarget),
|
|
271
|
+
issuedByAid: ownerAid, createdAt: now, expiresAt: grantDurationMs === null ? null : now + grantDurationMs };
|
|
272
|
+
const grants = new Map(this.grants);
|
|
273
|
+
grants.set(grant.id, grant);
|
|
274
|
+
await this.persist(next, grants);
|
|
275
|
+
}
|
|
276
|
+
else
|
|
277
|
+
await this.persist(next);
|
|
278
|
+
return { started, entry: structuredClone(entry), ...(grant ? { grant: structuredClone(grant) } : {}) };
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
grantActive(id, input) {
|
|
282
|
+
const grant = this.grants.get(id);
|
|
283
|
+
return Boolean(grant && grant.revokedAt === undefined && (grant.expiresAt === null || grant.expiresAt > this.now())
|
|
284
|
+
&& grant.providerAid === input.providerAid && grant.authenticatedCaller === input.authenticatedCaller
|
|
285
|
+
&& grant.path === input.path && grant.definitionId === input.definitionId
|
|
286
|
+
&& grant.executionTarget.pluginId === input.executionTarget.pluginId
|
|
287
|
+
&& grant.executionTarget.pluginVersion === input.executionTarget.pluginVersion
|
|
288
|
+
&& grant.executionTarget.ownerAid === input.executionTarget.ownerAid);
|
|
289
|
+
}
|
|
290
|
+
activeGrant(input) {
|
|
291
|
+
return this.serial(async () => {
|
|
292
|
+
const grant = [...this.grants.values()].find(item => this.grantActive(item.id, input));
|
|
293
|
+
return grant ? structuredClone(grant) : undefined;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
listGrants(providerAid) {
|
|
297
|
+
return this.serial(async () => [...this.grants.values()]
|
|
298
|
+
.filter(grant => providerAid === undefined || grant.providerAid === providerAid)
|
|
299
|
+
.map(grant => structuredClone(grant)));
|
|
300
|
+
}
|
|
301
|
+
revokeGrant(providerAid, grantId) {
|
|
302
|
+
return this.serial(async () => {
|
|
303
|
+
const grant = this.grants.get(grantId);
|
|
304
|
+
if (!grant || grant.providerAid !== providerAid)
|
|
305
|
+
throw failure("grant_not_found", "授权不存在");
|
|
306
|
+
if (grant.revokedAt === undefined) {
|
|
307
|
+
const next = new Map(this.grants);
|
|
308
|
+
const revoked = { ...grant, revokedAt: this.now() };
|
|
309
|
+
next.set(grantId, revoked);
|
|
310
|
+
await this.persist(this.entries, next);
|
|
311
|
+
return structuredClone(revoked);
|
|
312
|
+
}
|
|
313
|
+
return structuredClone(grant);
|
|
241
314
|
});
|
|
242
315
|
}
|
|
243
316
|
finish(key, state, result) {
|
|
@@ -313,6 +386,13 @@ class AbilityOperationStore {
|
|
|
313
386
|
const now = this.now();
|
|
314
387
|
const expired = this.expirePending(this.entries, now);
|
|
315
388
|
const next = expired.entries;
|
|
389
|
+
const grants = new Map(this.grants);
|
|
390
|
+
let grantsChanged = false;
|
|
391
|
+
for (const [id, grant] of grants)
|
|
392
|
+
if (grant.providerAid === providerAid && grant.revokedAt === undefined) {
|
|
393
|
+
grants.set(id, { ...grant, revokedAt: now });
|
|
394
|
+
grantsChanged = true;
|
|
395
|
+
}
|
|
316
396
|
let invalidated = 0;
|
|
317
397
|
for (const entry of next.values()) {
|
|
318
398
|
if (entry.providerAid !== providerAid || entry.state !== "pending_confirmation")
|
|
@@ -322,8 +402,8 @@ class AbilityOperationStore {
|
|
|
322
402
|
result: response(entry, code, message) });
|
|
323
403
|
invalidated++;
|
|
324
404
|
}
|
|
325
|
-
if (invalidated || expired.changed)
|
|
326
|
-
await this.persist(next);
|
|
405
|
+
if (invalidated || expired.changed || grantsChanged)
|
|
406
|
+
await this.persist(next, grants);
|
|
327
407
|
return invalidated;
|
|
328
408
|
});
|
|
329
409
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseAgentMdCommand = parseAgentMdCommand;
|
|
4
|
+
/** 只识别整条名片命令;引号用于分隔参数,文本从不交给 shell。 */
|
|
5
|
+
function parseAgentMdCommand(text) {
|
|
6
|
+
const prefix = text.match(/^(?:ec[ \t]+aid|mab[ \t]+aun)[ \t]+agentmd(?:[ \t]+|$)/);
|
|
7
|
+
if (!prefix)
|
|
8
|
+
return undefined;
|
|
9
|
+
const input = text.slice(prefix[0].length);
|
|
10
|
+
const args = [];
|
|
11
|
+
let position = 0;
|
|
12
|
+
while (position < input.length) {
|
|
13
|
+
while (/\s/.test(input[position] ?? "") && position < input.length)
|
|
14
|
+
position++;
|
|
15
|
+
if (position === input.length)
|
|
16
|
+
break;
|
|
17
|
+
const quote = input[position];
|
|
18
|
+
if (quote === "'" || quote === '"') {
|
|
19
|
+
const start = position++;
|
|
20
|
+
if (quote === '"') {
|
|
21
|
+
while (position < input.length) {
|
|
22
|
+
if (input[position] === "\\") {
|
|
23
|
+
position += 2;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (input[position++] === '"')
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
args.push(JSON.parse(input.slice(start, position)));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error("双引号参数必须是完整的 JSON 字符串;普通正文可使用单引号");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const end = input.indexOf("'", position);
|
|
38
|
+
if (end < 0)
|
|
39
|
+
throw new Error("名片命令的单引号未闭合");
|
|
40
|
+
args.push(input.slice(position, end));
|
|
41
|
+
position = end + 1;
|
|
42
|
+
}
|
|
43
|
+
if (position < input.length && !/\s/.test(input[position]))
|
|
44
|
+
throw new Error("引号参数后需要空格或换行");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
const end = input.slice(position).search(/\s/);
|
|
48
|
+
const length = end < 0 ? input.length - position : end;
|
|
49
|
+
args.push(input.slice(position, position + length));
|
|
50
|
+
position += length;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let json = false;
|
|
54
|
+
// 格式选项只允许出现在尾部,正文中的选项字样保持原样。
|
|
55
|
+
if (args.at(-1) === "--json") {
|
|
56
|
+
json = true;
|
|
57
|
+
args.pop();
|
|
58
|
+
}
|
|
59
|
+
else if (args.at(-2) === "--format" && args.at(-1) === "json") {
|
|
60
|
+
json = true;
|
|
61
|
+
args.splice(-2);
|
|
62
|
+
}
|
|
63
|
+
const [verb = "", aid = "", ...values] = args;
|
|
64
|
+
if (!["get", "set", "put"].includes(verb) || !aid)
|
|
65
|
+
throw new Error("用法:ec aid agentmd get <当前AID> | set <当前AID> <字段> <值> | put <当前AID> <完整Markdown>");
|
|
66
|
+
return { verb, aid, values, json };
|
|
67
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentMdMenu = exports.AGENTMD_PROTECTED_FIELDS = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_util_1 = require("node:util");
|
|
6
|
+
const yaml_1 = require("yaml");
|
|
7
|
+
exports.AGENTMD_PROTECTED_FIELDS = ["aid", "type", "owner", "ownership", "ownership_proof", "ownership_release", "entity_cert_fingerprint"];
|
|
8
|
+
const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const idleTimeout = 30 * 24 * 60 * 60;
|
|
10
|
+
/** 仅实现 App 名片编辑器所需的 Menu 子集,不把控制消息交给模型。 */
|
|
11
|
+
class AgentMdMenu {
|
|
12
|
+
tokens = new Map();
|
|
13
|
+
queues = new Map();
|
|
14
|
+
clear() { this.tokens.clear(); }
|
|
15
|
+
/** 本机入口由宿主管理认证保护,与远端 Menu 共用写锁和发布校验。 */
|
|
16
|
+
async local(aid, verb, values, port) {
|
|
17
|
+
return this.command(aid, verb, values, port);
|
|
18
|
+
}
|
|
19
|
+
/** 手机文本入口保留原始认证信封,不伪造本机调用者或 Menu token。 */
|
|
20
|
+
async text(event, aid, verb, values, port, transportAid = aid) {
|
|
21
|
+
return this.command(aid, verb, values, port, event, transportAid);
|
|
22
|
+
}
|
|
23
|
+
async command(aid, verb, values, port, event, transportAid = aid) {
|
|
24
|
+
const previous = this.queues.get(aid) ?? Promise.resolve();
|
|
25
|
+
const pending = previous.catch(() => undefined).then(async () => {
|
|
26
|
+
const execute = (payload) => this.execute({ ...event, payload }, aid, port, event ? "text" : "local", transportAid);
|
|
27
|
+
if (verb === "get" && !values.length)
|
|
28
|
+
return execute({ type: "menu.query", name: "aid", args: { view: "agentmd" } });
|
|
29
|
+
// 先核验调用者,再读取用于字段更新的基线。
|
|
30
|
+
await execute({ type: "menu.query", name: "aid", args: { view: "agentmd" } });
|
|
31
|
+
const base = await port.read();
|
|
32
|
+
let content;
|
|
33
|
+
if (verb === "put" && values.length === 1)
|
|
34
|
+
content = values[0];
|
|
35
|
+
else if (verb === "set" && values.length === 2) {
|
|
36
|
+
const [field, value] = values;
|
|
37
|
+
if (!["name", "description", "version", "tags", "exttype", "body"].includes(field))
|
|
38
|
+
reject("INVALID_ARGUMENT", "仅支持 name/description/version/tags/exttype/body");
|
|
39
|
+
const parsed = port.parse(base.content);
|
|
40
|
+
const match = parsed.payload.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
41
|
+
if (!match)
|
|
42
|
+
reject("INVALID_ARGUMENT", "名片头部格式无效");
|
|
43
|
+
if (field === "body")
|
|
44
|
+
content = `${match[0].trimEnd()}\n${value}\n`;
|
|
45
|
+
else {
|
|
46
|
+
const document = (0, yaml_1.parseDocument)(match[1]);
|
|
47
|
+
let next = value;
|
|
48
|
+
if (field === "tags") {
|
|
49
|
+
try {
|
|
50
|
+
next = JSON.parse(value);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
reject("INVALID_ARGUMENT", "tags 必须是 JSON 字符串数组");
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(next) || next.some(item => typeof item !== "string"))
|
|
56
|
+
reject("INVALID_ARGUMENT", "tags 必须是 JSON 字符串数组");
|
|
57
|
+
}
|
|
58
|
+
document.set(field, next);
|
|
59
|
+
content = `---\n${document.toString()}---\n${parsed.payload.slice(match[0].length)}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else
|
|
63
|
+
reject("INVALID_ARGUMENT", "用法:mab aun agentmd get <aid> | set <aid> <字段> <值> | put <aid> <完整Markdown>");
|
|
64
|
+
return execute({ type: "menu.action", name: "aid", action: "agentmd.publish", args: { content, expectedEtag: base.etag, expectedRelationVersion: base.ownership.relation_version } });
|
|
65
|
+
});
|
|
66
|
+
this.queues.set(aid, pending);
|
|
67
|
+
try {
|
|
68
|
+
return await pending;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (this.queues.get(aid) === pending)
|
|
72
|
+
this.queues.delete(aid);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async handle(event, aid, port, authorized = false) {
|
|
76
|
+
if (!object(event) || !object(event.payload) || typeof event.payload.type !== "string" || !event.payload.type.startsWith("menu."))
|
|
77
|
+
return undefined;
|
|
78
|
+
const request = event.payload;
|
|
79
|
+
// 响应及事件也在此消费,避免其中的 text 被误当作桌面任务。
|
|
80
|
+
if (["menu.response", "menu.event"].includes(String(request.type)))
|
|
81
|
+
return undefined;
|
|
82
|
+
if (typeof request.id !== "string" || !request.id.trim() || request.id.length > 256)
|
|
83
|
+
return undefined;
|
|
84
|
+
const header = { type: "menu.response", id: request.id, ...(typeof request.name === "string" ? { name: request.name } : {}) };
|
|
85
|
+
const run = async () => {
|
|
86
|
+
try {
|
|
87
|
+
return { ...header, data: await this.execute(event, aid, port, authorized ? "text" : "menu") };
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
const code = typeof error.code === "string" ? String(error.code) : "REQUEST_FAILED";
|
|
91
|
+
// SDK 异常可能带服务端或存储细节,不原样返回。
|
|
92
|
+
return { ...header, error: { code, message: error instanceof MenuError ? error.message : "名片操作未完成,请刷新后核对远端状态" } };
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const previous = this.queues.get(aid) ?? Promise.resolve();
|
|
96
|
+
const pending = previous.catch(() => undefined).then(run);
|
|
97
|
+
this.queues.set(aid, pending);
|
|
98
|
+
try {
|
|
99
|
+
return await pending;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (this.queues.get(aid) === pending)
|
|
103
|
+
this.queues.delete(aid);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async execute(event, aid, port, mode = "menu", transportAid = aid) {
|
|
107
|
+
const local = mode === "local";
|
|
108
|
+
const request = event.payload;
|
|
109
|
+
const envelope = object(event.envelope) ? event.envelope : {};
|
|
110
|
+
const peer = local ? (await port.read()).ownership.owner_aid : typeof event.from === "string" ? event.from : envelope.from;
|
|
111
|
+
if (!local && (typeof peer !== "string" || !peer || peer === aid || event.encrypted !== true
|
|
112
|
+
|| (envelope.from !== undefined && envelope.from !== peer) || (envelope.to !== undefined && envelope.to !== transportAid)
|
|
113
|
+
|| (event.to !== undefined && event.to !== transportAid) || event.group_id || envelope.group_id
|
|
114
|
+
|| event.chat_type === "group" || envelope.chat_type === "group"))
|
|
115
|
+
reject("FORBIDDEN", "名片管理仅支持经过认证的加密私聊");
|
|
116
|
+
const device = typeof event.sender_device_id === "string" ? event.sender_device_id : "";
|
|
117
|
+
const slot = typeof event.sender_slot_id === "string" ? event.sender_slot_id : "";
|
|
118
|
+
const key = JSON.stringify([aid, peer, device, slot]);
|
|
119
|
+
const read = async () => {
|
|
120
|
+
if (!port.current())
|
|
121
|
+
reject("FORBIDDEN", "通道或关联已改变,请重新打开编辑器");
|
|
122
|
+
const card = await port.read();
|
|
123
|
+
if (!port.current())
|
|
124
|
+
reject("FORBIDDEN", "通道或关联已改变,请重新打开编辑器");
|
|
125
|
+
const owner = card.ownership;
|
|
126
|
+
if (owner.status !== "consensus" || owner.sub_aid !== aid || owner.owner_aid !== peer || owner.cached !== false || owner.lock !== "locked")
|
|
127
|
+
reject("FORBIDDEN", "仅当前协议 Owner 可编辑名片");
|
|
128
|
+
if (!Number.isSafeInteger(owner.relation_version) || Number(owner.relation_version) < 1 || !card.etag || owner.agent_md_etag !== card.etag)
|
|
129
|
+
reject("OWNERSHIP_CONFLICT", "名片与归属版本不一致,请刷新");
|
|
130
|
+
const parsed = port.parse(card.content);
|
|
131
|
+
if (["human", "group", "org", "host"].includes(String(parsed.frontmatter.type).trim().toLowerCase()))
|
|
132
|
+
reject("NOT_SUPPORTED", "此入口仅编辑普通 Agent 名片");
|
|
133
|
+
if (parsed.frontmatter.aid !== aid || parsed.frontmatter.owner !== peer)
|
|
134
|
+
reject("OWNERSHIP_CONFLICT", "名片身份与归属不一致");
|
|
135
|
+
return { ...card, ...parsed };
|
|
136
|
+
};
|
|
137
|
+
const base = await read();
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
for (const [tokenKey, record] of this.tokens)
|
|
140
|
+
if (now - record.usedAt >= idleTimeout * 1000)
|
|
141
|
+
this.tokens.delete(tokenKey);
|
|
142
|
+
if (request.type === "menu.token.request") {
|
|
143
|
+
if (!this.tokens.has(key) && this.tokens.size >= 1000)
|
|
144
|
+
reject("MENU_TOKEN_REJECTED", "Menu 授权数量已达上限");
|
|
145
|
+
const token = `mt_${(0, node_crypto_1.randomBytes)(32).toString("base64url")}`;
|
|
146
|
+
this.tokens.set(key, { token, usedAt: now });
|
|
147
|
+
return { menu_token: token, idle_timeout: idleTimeout };
|
|
148
|
+
}
|
|
149
|
+
const token = this.tokens.get(key);
|
|
150
|
+
if (mode === "menu" && (!token || token.token !== request.menu_token))
|
|
151
|
+
reject("MENU_TOKEN_REQUIRED", "Menu token 已失效,请重新申请");
|
|
152
|
+
if (token)
|
|
153
|
+
token.usedAt = now;
|
|
154
|
+
const args = object(request.args) ? request.args : {};
|
|
155
|
+
if (request.type === "menu.list" && request.name === "menu" && args.view === "catalog") {
|
|
156
|
+
if (!object(args.context) || args.context.kind !== "agent" || Object.keys(args.context).some(key => key !== "kind"))
|
|
157
|
+
reject("INVALID_CONTEXT", "名片目录需要 Agent 上下文");
|
|
158
|
+
if (Object.keys(args).some(key => !["view", "context", "acceptSchemaVersions"].includes(key)))
|
|
159
|
+
reject("INVALID_ARGUMENT", "目录参数不受支持");
|
|
160
|
+
if (args.acceptSchemaVersions !== undefined && !(Array.isArray(args.acceptSchemaVersions) ? args.acceptSchemaVersions : [args.acceptSchemaVersions]).includes(1))
|
|
161
|
+
reject("SCHEMA_VERSION_UNSUPPORTED", "仅支持目录版本 1");
|
|
162
|
+
return { kind: "menu.catalog", $schema_version: 1, context: { kind: "agent", self: aid, actor: peer, requesterRole: "owner" }, entries: [{
|
|
163
|
+
id: "aid.agentmd", name: "aid", type: "setting", label: "能力介绍与名片", section: "agent", order: 71,
|
|
164
|
+
control: "markdown-editor", operations: {
|
|
165
|
+
query: { supported: true, allowed: true, cmd: "/aid", args: { view: "agentmd", protocolVersion: 1 } },
|
|
166
|
+
action: { supported: true, allowed: true, cmd: "/aid", action: "agentmd.publish", args: { protocolVersion: 1 } },
|
|
167
|
+
},
|
|
168
|
+
}] };
|
|
169
|
+
}
|
|
170
|
+
if (request.name !== "aid" && request.cmd !== "/aid")
|
|
171
|
+
reject("NOT_SUPPORTED", "当前仅支持名片编辑 Menu");
|
|
172
|
+
const publish = request.type === "menu.action" && request.action === "agentmd.publish";
|
|
173
|
+
const query = request.type === "menu.query" && args.view === "agentmd";
|
|
174
|
+
if (!publish && !query)
|
|
175
|
+
reject("NOT_SUPPORTED", "名片操作不受支持");
|
|
176
|
+
const allowed = query ? ["view", "protocolVersion", "aid", "self", "scope"] : ["content", "expectedEtag", "expectedRelationVersion", "protocolVersion", "aid", "self", "scope"];
|
|
177
|
+
if (Object.keys(args).some(key => !allowed.includes(key)) || (args.aid !== undefined && args.aid !== aid) || (args.self !== undefined && args.self !== aid) || (args.scope !== undefined && args.scope !== "agent"))
|
|
178
|
+
reject("INVALID_ARGUMENT", "名片参数或目标不受支持");
|
|
179
|
+
if (args.protocolVersion !== undefined && args.protocolVersion !== 1)
|
|
180
|
+
reject("SCHEMA_VERSION_UNSUPPORTED", "仅支持名片协议版本 1");
|
|
181
|
+
const data = (card) => ({
|
|
182
|
+
protocol: "evol.agentmd", protocolVersion: 1, aid, content: card.payload, etag: card.etag,
|
|
183
|
+
relationVersion: card.ownership.relation_version, verification: "verified", atomicVersionCheck: false,
|
|
184
|
+
});
|
|
185
|
+
if (query)
|
|
186
|
+
return { ...data(base), protectedFields: exports.AGENTMD_PROTECTED_FIELDS, limits: { maxPayloadBytes: 4096, maxDescriptionCharacters: 100 } };
|
|
187
|
+
if (typeof args.content !== "string" || Buffer.byteLength(args.content) > 6144 || typeof args.expectedEtag !== "string" || !args.expectedEtag.trim() || args.expectedEtag.length > 512 || /[\r\n]/.test(args.expectedEtag) || !Number.isSafeInteger(args.expectedRelationVersion) || Number(args.expectedRelationVersion) < 1)
|
|
188
|
+
reject("INVALID_ARGUMENT", "发布需要正文、ETag 和归属版本");
|
|
189
|
+
if (base.etag !== args.expectedEtag)
|
|
190
|
+
reject("REMOTE_VERSION_CONFLICT", "名片已改变,请重新读取后发布");
|
|
191
|
+
if (base.ownership.relation_version !== args.expectedRelationVersion)
|
|
192
|
+
reject("OWNERSHIP_CONFLICT", "归属已改变,请重新读取");
|
|
193
|
+
const candidate = port.parse(args.content);
|
|
194
|
+
for (const field of exports.AGENTMD_PROTECTED_FIELDS)
|
|
195
|
+
if (Object.hasOwn(base.frontmatter, field) !== Object.hasOwn(candidate.frontmatter, field) || !(0, node_util_1.isDeepStrictEqual)(base.frontmatter[field], candidate.frontmatter[field]))
|
|
196
|
+
reject("PROTECTED_FIELD", `不能修改身份字段:${field}`);
|
|
197
|
+
// SDK 不暴露条件上传,串行写入并在上传前重读,明确返回非原子版本检查。
|
|
198
|
+
const latest = await read();
|
|
199
|
+
if (latest.etag !== base.etag)
|
|
200
|
+
reject("REMOTE_VERSION_CONFLICT", "名片已改变,请重新读取");
|
|
201
|
+
if (latest.ownership.relation_version !== base.ownership.relation_version)
|
|
202
|
+
reject("OWNERSHIP_CONFLICT", "归属已改变,请重新读取");
|
|
203
|
+
const guard = () => { if (!port.current())
|
|
204
|
+
reject("FORBIDDEN", "通道或关联已失效"); };
|
|
205
|
+
guard();
|
|
206
|
+
await port.upload(candidate.payload, guard);
|
|
207
|
+
const accepted = await read();
|
|
208
|
+
const body = (payload) => payload.replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---(?:\n|$)/, "").trim();
|
|
209
|
+
if (accepted.ownership.relation_version !== base.ownership.relation_version || !(0, node_util_1.isDeepStrictEqual)(accepted.frontmatter, candidate.frontmatter) || body(accepted.payload) !== body(candidate.payload))
|
|
210
|
+
reject("PUBLISH_NOT_CONFIRMED", "远端尚未确认本次发布,请刷新核对");
|
|
211
|
+
return { ...data(accepted), published: true, versionChecked: true, nameChanged: base.frontmatter.name !== accepted.frontmatter.name };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
exports.AgentMdMenu = AgentMdMenu;
|
|
215
|
+
class MenuError extends Error {
|
|
216
|
+
code;
|
|
217
|
+
constructor(code, message) {
|
|
218
|
+
super(message);
|
|
219
|
+
this.code = code;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function reject(code, message) { throw new MenuError(code, message); }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseNaturalProfileCommand = parseNaturalProfileCommand;
|
|
4
|
+
/** 确定性的中文名片指令;不从模糊措辞推断新内容或授权。 */
|
|
5
|
+
function parseNaturalProfileCommand(text) {
|
|
6
|
+
const target = "([a-z0-9][a-z0-9_-]*(?:\\.[a-z0-9-]+)*)";
|
|
7
|
+
const change = new RegExp(`^(?:请)?(?:帮我)?(?:把|将)\\s*${target}\\s*的(?:名片)?(描述|简介|能力介绍|介绍正文|显示名称|名称)\\s*(?:修改为|设置为|改成|改为|设为)\\s*[::]?\\s*([\\s\\S]+)$`, "i").exec(text.trim());
|
|
8
|
+
if (change) {
|
|
9
|
+
let value = change[3].trim();
|
|
10
|
+
if ((value.startsWith("“") && value.endsWith("”")) || (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
|
|
11
|
+
value = value.slice(1, -1);
|
|
12
|
+
const field = ["描述", "简介"].includes(change[2]) ? "description" : ["显示名称", "名称"].includes(change[2]) ? "name" : "body";
|
|
13
|
+
return { target: change[1].toLowerCase(), verb: "set", values: [field, value] };
|
|
14
|
+
}
|
|
15
|
+
const read = new RegExp(`^(?:请)?(?:帮我)?(?:查看|看一下|读取)\\s*${target}\\s*的(?:名片|描述|简介|能力介绍)[。!!]?\\s*$`, "i").exec(text.trim());
|
|
16
|
+
return read ? { target: read[1].toLowerCase(), verb: "get", values: [] } : undefined;
|
|
17
|
+
}
|
|
@@ -134,7 +134,8 @@ async function readAunFsBytes(storage, path, options) {
|
|
|
134
134
|
throw new AunFsError("OUTPUT_LIMIT", "FS 文件超过读取预算");
|
|
135
135
|
if (expectedSize !== undefined && expectedSize !== size)
|
|
136
136
|
throw new AunFsError("INVALID_RESPONSE", "FS 文件大小已变化");
|
|
137
|
-
|
|
137
|
+
// 分块上限查询当前连接,不能要求读取者拥有目标 AID 的配额管理权限。
|
|
138
|
+
const limits = storage.lowlevel?.getLimits ? record(await storage.lowlevel.getLimits()) : {};
|
|
138
139
|
const inline = limits.max_inline_bytes ?? 64 * 1024;
|
|
139
140
|
if (!Number.isSafeInteger(inline) || Number(inline) < 1)
|
|
140
141
|
unsupported("Storage 内联读取上限无效");
|