agent-bridge-aun 1.0.46 → 1.0.49

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
@@ -69,3 +69,6 @@ ABWeb 的 AUN 设置只展示已连接 AID,并按来源分组。成功注册
69
69
  AUN 独立包支持核心后台每五分钟检测更新;可信提供者在自动安装策略下会下载新副本并重载通道。
70
70
 
71
71
  显式关联已有 EvolCore Agent 的 AID 时,先执行 `ec agent disable <aid>`,再配置 `ecagent` 路由并重载。`ec` 不存在或命令失败时,只将所选 AID 的 JSON 配置更新为 `enabled=false`、`active_baseagent=ecagent` 和对应路由;其它 AID 保持原值。仍有 EvolCore 运行实例时,必须确认重载成功才完成关联,避免原 Claude 与桌面 Agent 同时回复。纯密钥库身份不调用 `ec`,解绑不自动启用原 Agent。
72
+ ## Ability Owner 确认
73
+
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 保留用于兼容旧管理客户端。
@@ -127,6 +127,18 @@
127
127
  "type": "string",
128
128
  "pattern": "^[a-z0-9][a-z0-9_-]{2,63}\\.[a-z0-9-]+(?:\\.[a-z0-9-]+)+$"
129
129
  }
130
+ },
131
+ "ability_owner_verifications": {
132
+ "type": "object",
133
+ "default": {},
134
+ "description": "由 AUN 插件维护的 Owner 确认结果;仅当值与当前 ability_owners 相同时有效。",
135
+ "propertyNames": {
136
+ "pattern": "^[a-z0-9][a-z0-9_-]{2,63}\\.[a-z0-9-]+(?:\\.[a-z0-9-]+)+$"
137
+ },
138
+ "additionalProperties": {
139
+ "type": "string",
140
+ "pattern": "^[a-z0-9][a-z0-9_-]{2,63}\\.[a-z0-9-]+(?:\\.[a-z0-9-]+)+$"
141
+ }
130
142
  }
131
143
  },
132
144
  "additionalProperties": false
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseAbilityControl = parseAbilityControl;
4
+ exports.parseAbilityControlMessage = parseAbilityControlMessage;
5
+ const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
6
+ const pathPattern = /^(?!protocol\/|definitions\/)[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?(?:\/[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?)+$/;
7
+ const fingerprintPattern = /^[0-9a-f]{64}$/;
8
+ const maxControlTextLength = 4096;
9
+ function parseAbilityControl(text) {
10
+ if (text === "确认")
11
+ return { action: "confirm-view" };
12
+ if (text === "拒绝")
13
+ return { action: "reject-view" };
14
+ const trimmed = text.replace(/^ +| +$/g, "");
15
+ if (!/^\/ability(?:\s|$)/.test(trimmed))
16
+ return undefined;
17
+ if (text.length > maxControlTextLength)
18
+ throw new Error("Ability 管理命令过长,最多允许 4096 个字符");
19
+ if (!/^[a-z0-9./ _-]+$/.test(trimmed))
20
+ throw new Error("Ability 管理命令格式无效:请发送单行完整命令,仅使用空格分隔参数");
21
+ const [, action, ...args] = trimmed.split(/ +/);
22
+ if (["help", "list", "revoke", "pending"].includes(action) && args.length === 0)
23
+ return { action: action };
24
+ if (action === "publish") {
25
+ if (!args.length)
26
+ throw new Error("Ability 发布命令缺少路径参数");
27
+ if (args.some(path => path.length > 512 || !pathPattern.test(path)))
28
+ throw new Error("Ability 路径格式无效:必须符合插件 Ability path 规则且不能使用协议保留命名空间");
29
+ if (new Set(args).size !== args.length)
30
+ throw new Error("Ability 发布路径不能重复");
31
+ return { action, paths: args };
32
+ }
33
+ if ((action === "approve" || action === "reject") && args.length === 2) {
34
+ if (!args.every(value => fingerprintPattern.test(value)))
35
+ throw new Error("Ability 操作键与确认指纹格式无效:必须是完整的 64 位小写十六进制摘要");
36
+ return { action, key: args[0], confirmationFingerprint: args[1] };
37
+ }
38
+ throw new Error("Ability 管理命令或参数数量不正确,请发送 /ability help 查看用法");
39
+ }
40
+ function record(value) {
41
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
42
+ }
43
+ function parseAbilityControlMessage(value, providerAid, ownerAid) {
44
+ const raw = record(value);
45
+ if (!raw)
46
+ return undefined;
47
+ const body = record(raw.payload);
48
+ const text = typeof raw.payload === "string" ? raw.payload : typeof body?.text === "string" ? body.text : "";
49
+ const command = parseAbilityControl(text);
50
+ if (!command)
51
+ return undefined;
52
+ const envelope = record(raw.envelope);
53
+ if (raw.envelope !== undefined && !envelope)
54
+ throw new Error("Ability 管理消息的认证信封格式无效");
55
+ const peerId = typeof raw.from === "string" ? raw.from : typeof envelope?.from === "string" ? envelope.from : "";
56
+ if (!aidPattern.test(peerId) || peerId === providerAid || !aidPattern.test(providerAid))
57
+ throw new Error("Ability 管理消息来源必须是该 Provider 配置的 Owner 身份");
58
+ for (const fields of [raw, body, envelope]) {
59
+ if (!fields)
60
+ continue;
61
+ if ([fields.from, fields.sender_aid].some(sender => sender !== undefined && sender !== peerId))
62
+ throw new Error("Ability 管理消息来源与认证信封身份不一致");
63
+ if (fields.to !== undefined && fields.to !== providerAid)
64
+ throw new Error("Ability 管理消息收件者与当前 Provider 不一致");
65
+ }
66
+ const privateMessage = [raw, body, envelope].every(fields => !fields || (fields.group_id === undefined && fields.group_aid === undefined
67
+ && (fields.chat_type === undefined || ["private", "direct", "p2p", "dm"].includes(String(fields.chat_type)))));
68
+ if (raw.encrypted !== true || !privateMessage)
69
+ throw new Error("Ability 管理命令仅接受 Owner 的加密私聊消息");
70
+ if ((raw.type !== undefined && !["text", "message"].includes(String(raw.type))) || (body?.type !== undefined && body.type !== "text")
71
+ || raw.attachments !== undefined || body?.attachments !== undefined)
72
+ throw new Error("Ability 管理命令必须是无附件的纯文本消息,通知和错误不能作为命令");
73
+ if (!ownerAid || !aidPattern.test(ownerAid))
74
+ throw Object.assign(new Error("尚未配置有效的 Ability Owner,请先在本机配置该 Provider 的 Owner AID"), { code: "ability_owner_unconfigured" });
75
+ if (peerId !== ownerAid)
76
+ throw new Error("Ability 管理消息来源必须是该 Provider 配置的 Owner 身份");
77
+ return { command, peerId,
78
+ ...(typeof raw.message_id === "string" ? { messageId: raw.message_id } : {}),
79
+ ...(typeof body?.thread_id === "string" ? { threadId: body.thread_id } : {}),
80
+ };
81
+ }
@@ -5,6 +5,7 @@ exports.abilityOperationKey = abilityOperationKey;
5
5
  const node_crypto_1 = require("node:crypto");
6
6
  const promises_1 = require("node:fs/promises");
7
7
  const node_path_1 = require("node:path");
8
+ const confirmationLifetimeMs = 12 * 60 * 60 * 1000;
8
9
  function failure(code, message) {
9
10
  return Object.assign(new Error(`${code}: ${message}`), { code });
10
11
  }
@@ -35,7 +36,10 @@ function operationFingerprint(input) {
35
36
  return digest({ providerAid: input.providerAid, authenticatedCaller: input.authenticatedCaller, operationId: input.operationId,
36
37
  path: input.path, definitionId: input.definitionId, params: input.params, effects: input.effects, executionTarget: input.executionTarget, async: input.async ?? false });
37
38
  }
38
- function confirmationFingerprint(input, fingerprint) {
39
+ function confirmationFingerprint(input, fingerprint, confirmationExpiresAt) {
40
+ return digest({ key: abilityOperationKey(input), fingerprint, expiresAt: input.expiresAt, confirmationExpiresAt });
41
+ }
42
+ function legacyConfirmationFingerprint(input, fingerprint) {
39
43
  return digest({ key: abilityOperationKey(input), fingerprint, expiresAt: input.expiresAt });
40
44
  }
41
45
  function validateInput(input) {
@@ -62,11 +66,14 @@ function response(entry, code, message, executionState = "not_executed") {
62
66
  function validEntry(value) {
63
67
  if (!value || typeof value !== "object" || Array.isArray(value))
64
68
  return false;
65
- const entry = value;
69
+ const raw = value;
70
+ const legacy = raw.confirmationExpiresAt === undefined;
71
+ const entry = legacy ? { ...raw, confirmationExpiresAt: raw.expiresAt } : raw;
66
72
  try {
67
73
  validateInput(entry);
68
74
  if (entry.key !== abilityOperationKey(entry) || entry.fingerprint !== operationFingerprint(entry)
69
- || entry.confirmationFingerprint !== confirmationFingerprint(entry, entry.fingerprint)
75
+ || !Number.isSafeInteger(entry.confirmationExpiresAt) || entry.confirmationExpiresAt <= 0
76
+ || entry.confirmationFingerprint !== (legacy ? legacyConfirmationFingerprint(entry, entry.fingerprint) : confirmationFingerprint(entry, entry.fingerprint, entry.confirmationExpiresAt))
70
77
  || !["pending_confirmation", "executing", "succeeded", "failed", "unknown"].includes(entry.state)
71
78
  || ![entry.createdAt, entry.updatedAt, entry.retainUntil].every(Number.isSafeInteger)
72
79
  || entry.createdAt < 0 || entry.updatedAt < entry.createdAt || entry.retainUntil < entry.createdAt + entry.dedupeRetentionSeconds * 1000)
@@ -113,7 +120,10 @@ class AbilityOperationStore {
113
120
  if (!snapshot || snapshot.version !== 1 || !Array.isArray(snapshot.entries) || !snapshot.entries.every(validEntry))
114
121
  throw failure("operation_store_corrupt", "操作账本格式无效,已停止新的执行");
115
122
  let migrated = false;
116
- for (const entry of snapshot.entries) {
123
+ for (const saved of snapshot.entries) {
124
+ const legacy = saved.confirmationExpiresAt === undefined;
125
+ const entry = legacy ? { ...saved, confirmationExpiresAt: saved.expiresAt,
126
+ confirmationFingerprint: confirmationFingerprint(saved, saved.fingerprint, saved.expiresAt) } : saved;
117
127
  if (store.entries.has(entry.key))
118
128
  throw failure("operation_store_corrupt", "操作账本含重复幂等键");
119
129
  const legacyExpired = entry.result?.error?.code === "expired";
@@ -125,6 +135,7 @@ class AbilityOperationStore {
125
135
  }
126
136
  else
127
137
  store.entries.set(entry.key, entry);
138
+ migrated ||= legacy;
128
139
  }
129
140
  if (migrated)
130
141
  await store.persist(new Map(store.entries));
@@ -161,11 +172,11 @@ class AbilityOperationStore {
161
172
  const next = new Map(entries);
162
173
  let changed = false;
163
174
  for (const entry of entries.values()) {
164
- if (entry.state !== "pending_confirmation" || now < entry.expiresAt)
175
+ if (entry.state !== "pending_confirmation" || now < entry.confirmationExpiresAt)
165
176
  continue;
166
177
  next.set(entry.key, { ...entry, state: "failed", updatedAt: Math.max(entry.updatedAt, now),
167
178
  retainUntil: Math.max(entry.retainUntil, now + entry.dedupeRetentionSeconds * 1000),
168
- result: response(entry, "message_expired", "调用消息已过期,不会执行") });
179
+ result: response(entry, "confirmation_expired", "Owner 确认申请已过期,不会执行") });
169
180
  changed = true;
170
181
  }
171
182
  return { entries: next, changed };
@@ -188,9 +199,10 @@ class AbilityOperationStore {
188
199
  return { created: false, entry: structuredClone(existing) };
189
200
  }
190
201
  const retainUntil = now + copied.dedupeRetentionSeconds * 1000;
191
- if (!Number.isSafeInteger(retainUntil))
202
+ const confirmationExpiresAt = now + confirmationLifetimeMs;
203
+ if (!Number.isSafeInteger(retainUntil) || !Number.isSafeInteger(confirmationExpiresAt))
192
204
  throw failure("params_invalid", "操作保留期无效");
193
- const entry = { ...copied, key, fingerprint, confirmationFingerprint: confirmationFingerprint(copied, fingerprint),
205
+ const entry = { ...copied, key, fingerprint, confirmationExpiresAt, confirmationFingerprint: confirmationFingerprint(copied, fingerprint, confirmationExpiresAt),
194
206
  state: "pending_confirmation", createdAt: now, updatedAt: now, retainUntil };
195
207
  next.set(key, entry);
196
208
  await this.persist(next);
@@ -216,11 +228,12 @@ class AbilityOperationStore {
216
228
  if (existing.state !== "pending_confirmation")
217
229
  return { started: false, entry: structuredClone(existing) };
218
230
  const now = this.now();
231
+ const confirmationExpired = now >= existing.confirmationExpiresAt;
219
232
  const messageExpired = now >= existing.expiresAt;
220
- const started = approve && !messageExpired;
233
+ const started = approve && !confirmationExpired && !messageExpired;
221
234
  const entry = { ...existing, state: started ? "executing" : "failed", updatedAt: Math.max(existing.updatedAt, now),
222
235
  retainUntil: Math.max(existing.retainUntil, now + existing.dedupeRetentionSeconds * 1000),
223
- ...(!started ? { result: messageExpired ? response(existing, "message_expired", "调用消息已过期,不会执行") : response(existing, "user_denied", "Owner 已拒绝本次调用") } : {}) };
236
+ ...(!started ? { result: confirmationExpired ? response(existing, "confirmation_expired", "Owner 确认申请已过期,不会执行") : approve && messageExpired ? response(existing, "message_expired", "调用已超过最晚启动时间") : response(existing, "user_denied", "Owner 已拒绝本次调用") } : {}) };
224
237
  const next = new Map(this.entries);
225
238
  next.set(key, entry);
226
239
  await this.persist(next);
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OwnerProof = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const lifetime = 10 * 60_000;
6
+ const confirmationLifetime = 12 * 60 * 60_000;
6
7
  const limit = 1024;
7
8
  const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
8
9
  function key(session, providerAid, ownerAid) {
@@ -32,7 +33,7 @@ class OwnerProof {
32
33
  if (!this.entries.has(recordKey) && this.entries.size >= limit)
33
34
  throw new Error("Owner 证明记录已达上限,请稍后重试");
34
35
  const nonce = (0, node_crypto_1.randomBytes)(32).toString("hex");
35
- const expiresAt = Math.min(session.expiresAt, now + lifetime);
36
+ const expiresAt = Math.min(session.expiresAt, now + confirmationLifetime);
36
37
  this.entries.set(recordKey, { providerAid, ownerAid, sessionExpiresAt: session.expiresAt, expiresAt, nonceHash: (0, node_crypto_1.createHash)("sha256").update(nonce).digest("hex") });
37
38
  return { nonce, expiresAt };
38
39
  }
@@ -51,6 +52,27 @@ class OwnerProof {
51
52
  }
52
53
  return false;
53
54
  }
55
+ acceptConfirmation(providerAid, authenticatedSenderAid, text, encrypted) {
56
+ const now = this.now();
57
+ this.prune(now);
58
+ if (encrypted !== true || !authenticatedSenderAid || text !== "确认")
59
+ return false;
60
+ for (const entry of this.entries.values()) {
61
+ if (entry.providerAid !== providerAid || entry.ownerAid !== authenticatedSenderAid || entry.nonceHash === undefined)
62
+ continue;
63
+ this.clear(providerAid);
64
+ return true;
65
+ }
66
+ return false;
67
+ }
68
+ hasPendingConfirmation(providerAid) {
69
+ const now = this.now();
70
+ this.prune(now);
71
+ for (const entry of this.entries.values())
72
+ if (entry.providerAid === providerAid && entry.nonceHash !== undefined)
73
+ return true;
74
+ return false;
75
+ }
54
76
  verified(session, providerAid, ownerAid) {
55
77
  const now = this.now();
56
78
  this.prune(now);
@@ -68,15 +68,14 @@ async function configureAidForPlugin(pluginId, aid, port, run = runEc, home = pr
68
68
  throw new Error("插件 ID 格式无效");
69
69
  if (!Number.isInteger(port) || port < 1 || port > 65535)
70
70
  throw new Error("后台端口无效");
71
- const path = pluginId === "dsh" ? "/dsh/v1" : `/api/agents/${pluginId}/v1`;
72
- const ecagent = { baseUrl: `http://127.0.0.1:${port}${path}`, apiKey: "mab-local", model: pluginId };
71
+ const baseagent = { provider: "provider-1", model: pluginId };
73
72
  try {
74
73
  // MAB 直接接收 AUN 消息,先停用该 AID 的 EvolCore Agent,避免双重回复。
75
74
  const disabled = await run(["agent", "disable", aid, "--format", "json"], home);
76
75
  if (!disabled?.ok)
77
76
  throw new Error(`停用原 Agent 失败:${disabled?.code ?? ""} ${disabled?.error ?? "未知错误"}`);
78
77
  for (const [key, value] of [
79
- ...Object.entries(ecagent).map(([key, value]) => [`baseagents.ecagent.${key}`, value]),
78
+ ...Object.entries(baseagent).map(([key, value]) => [`baseagents.ecagent.${key}`, value]),
80
79
  ["active_baseagent", "ecagent"],
81
80
  ]) {
82
81
  const result = await run(["agent", "set", aid, key, value, "--format", "json"], home);
@@ -95,7 +94,10 @@ async function configureAidForPlugin(pluginId, aid, port, run = runEc, home = pr
95
94
  throw new Error(`AID ${aid} 原配置格式无效,未覆盖文件`);
96
95
  config.enabled = false;
97
96
  config.active_baseagent = "ecagent";
98
- config.baseagents = { ...config.baseagents, ecagent: { ...config.baseagents?.ecagent, ...ecagent } };
97
+ const ecagent = config.baseagents?.ecagent ?? {};
98
+ delete ecagent.baseUrl;
99
+ delete ecagent.apiKey;
100
+ config.baseagents = { ...config.baseagents, ecagent: { ...ecagent, ...baseagent } };
99
101
  const temporary = `${file}.mab-${(0, node_crypto_1.randomUUID)()}.tmp`;
100
102
  try {
101
103
  await (0, promises_1.writeFile)(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });