agent-bridge-aun 1.0.46 → 1.0.48
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 +3 -0
- package/config.schema.json +12 -0
- package/dist/src-ts/plugins/aun/ability-operation-store.js +23 -10
- package/dist/src-ts/plugins/aun/ability-owner.js +23 -1
- package/dist/src-ts/plugins/aun/evolcore.js +6 -4
- package/dist/src-ts/plugins/aun/index.js +176 -30
- package/dist/src-ts/plugins/aun/peer-messaging.js +5 -3
- package/dist/src-ts/plugins/aun/public/app.js +10 -9
- package/dist/src-ts/plugins/aun/public/index.html +2 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
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
|
+
ABWeb 为 Provider AID 发起能力管理授权时,AUN 会向配置的 Owner AID 发送加密通知。Owner 在同一 AUN 会话中直接回复“确认”即可,不需要复制命令或令牌。确认按 Provider AID 保存,AUN 插件重载或升级后继续有效;只有 Provider 的 Owner 配置改变时才会撤销并要求重新确认。旧版 `/ability-owner <nonce>` 回复仍兼容。
|
package/config.schema.json
CHANGED
|
@@ -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
|
|
@@ -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
|
|
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.
|
|
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
|
|
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.
|
|
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, "
|
|
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
|
-
|
|
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", "
|
|
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 +
|
|
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
|
|
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(
|
|
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
|
-
|
|
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 });
|
|
@@ -30,7 +30,7 @@ const ownership_1 = require("./ownership");
|
|
|
30
30
|
const sdk_loader_1 = require("./sdk-loader");
|
|
31
31
|
const owned_profiles_1 = require("./owned-profiles");
|
|
32
32
|
const manifest = {
|
|
33
|
-
id: "aun", version: "1.0.
|
|
33
|
+
id: "aun", version: "1.0.48", type: "channel", displayName: "AUN 通道",
|
|
34
34
|
entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
|
|
35
35
|
capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
|
|
36
36
|
configSchema: "config.schema.json",
|
|
@@ -107,6 +107,7 @@ class AunPlugin {
|
|
|
107
107
|
abilitySelections = {};
|
|
108
108
|
abilityLedgers = {};
|
|
109
109
|
abilityOwners = {};
|
|
110
|
+
abilityOwnerVerifications = {};
|
|
110
111
|
abilityOperations;
|
|
111
112
|
peerMessages;
|
|
112
113
|
abilityAuditUnavailable = false;
|
|
@@ -511,8 +512,23 @@ class AunPlugin {
|
|
|
511
512
|
this.config.ability_selections = this.abilitySelections;
|
|
512
513
|
this.config.ability_ledgers = this.abilityLedgers;
|
|
513
514
|
this.config.ability_owners = this.abilityOwners;
|
|
515
|
+
this.config.ability_owner_verifications = this.abilityOwnerVerifications;
|
|
514
516
|
await (0, promises_1.writeFile)((0, node_path_1.join)(this.context.dataDir, "config.json"), `${JSON.stringify(this.config, null, 2)}\n`, { mode: 0o600 });
|
|
515
517
|
}
|
|
518
|
+
async saveAbilityOwnerVerification(aid, ownerAid) {
|
|
519
|
+
const previous = this.abilityOwnerVerifications;
|
|
520
|
+
this.abilityOwnerVerifications = { ...previous, [aid]: ownerAid };
|
|
521
|
+
try {
|
|
522
|
+
await this.persist();
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
this.abilityOwnerVerifications = previous;
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
isAbilityOwnerVerified(aid, ownerAid) {
|
|
530
|
+
return this.abilityOwners[aid] === ownerAid && this.abilityOwnerVerifications[aid] === ownerAid;
|
|
531
|
+
}
|
|
516
532
|
async activate(context) {
|
|
517
533
|
this.context = context;
|
|
518
534
|
this.config = { ...context.config };
|
|
@@ -610,12 +626,21 @@ class AunPlugin {
|
|
|
610
626
|
|| Object.entries(owners).some(([provider, owner]) => !aidPattern.test(provider) || typeof owner !== "string" || !aidPattern.test(owner))))
|
|
611
627
|
throw new Error("ability_owners 必须按 Provider AID 配置有效的 Owner AID");
|
|
612
628
|
this.abilityOwners = { ...owners };
|
|
629
|
+
const verifications = this.config.ability_owner_verifications;
|
|
630
|
+
if (verifications !== undefined && (!verifications || typeof verifications !== "object" || Array.isArray(verifications)
|
|
631
|
+
|| Object.entries(verifications).some(([provider, owner]) => !aidPattern.test(provider) || typeof owner !== "string" || !aidPattern.test(owner))))
|
|
632
|
+
throw new Error("ability_owner_verifications 必须按 Provider AID 配置有效的 Owner AID");
|
|
633
|
+
const persistedVerifications = { ...verifications };
|
|
634
|
+
this.abilityOwnerVerifications = Object.fromEntries(Object.entries(persistedVerifications).filter(([provider, owner]) => this.abilityOwners[provider] === owner));
|
|
635
|
+
const staleOwnerVerification = Object.keys(this.abilityOwnerVerifications).length !== Object.keys(persistedVerifications).length;
|
|
613
636
|
for (const aid of Array.isArray(this.config.identities) ? this.config.identities : []) {
|
|
614
637
|
if (typeof aid === "string" && aidPattern.test(aid))
|
|
615
638
|
this.identities.add(aid);
|
|
616
639
|
}
|
|
617
640
|
if (this.aid)
|
|
618
641
|
this.identities.add(this.aid);
|
|
642
|
+
if (staleOwnerVerification)
|
|
643
|
+
await this.persist();
|
|
619
644
|
this.acceptingMessages = true;
|
|
620
645
|
// 只订阅明确关联 Agent 的身份,避免接管 EvolCore 或其它 AID 的消息。
|
|
621
646
|
for (const aid of Object.keys(this.bindings)) {
|
|
@@ -735,7 +760,11 @@ class AunPlugin {
|
|
|
735
760
|
const connection = this.clients.get(aid);
|
|
736
761
|
if (!connection || connection.subscription || !this.bindings[aid])
|
|
737
762
|
return;
|
|
738
|
-
connection.subscription = connection.client.on("message.received", (payload) => this.
|
|
763
|
+
connection.subscription = connection.client.on("message.received", (payload) => this.enqueueMessage(payload, aid));
|
|
764
|
+
}
|
|
765
|
+
enqueueMessage(payload, aid) {
|
|
766
|
+
// SDK 串行等待事件回调;任务由现有入站账本排空,不能占住 SDK 的消息分发队列。
|
|
767
|
+
void this.onMessage(payload, aid).catch(error => this.context?.logger.warn("AUN 入站消息处理失败", { aid, stage: "处理已接收消息", error: safeError((0, agent_1.errorDetails)(error)) }));
|
|
739
768
|
}
|
|
740
769
|
async connectAid(aid) {
|
|
741
770
|
try {
|
|
@@ -747,7 +776,7 @@ class AunPlugin {
|
|
|
747
776
|
const ownedSubscription = this.ownedProfiles && !this.config.owner_watch_aids?.includes(aid)
|
|
748
777
|
? client.on("owned_agentmd.changed", payload => this.onOwnedProfile(aid, payload)) : undefined;
|
|
749
778
|
const subscription = this.bindings[aid]
|
|
750
|
-
? client.on("message.received", (payload) => this.
|
|
779
|
+
? client.on("message.received", (payload) => this.enqueueMessage(payload, aid))
|
|
751
780
|
: undefined;
|
|
752
781
|
this.clients.set(aid, { client, subscription, ownedSubscription });
|
|
753
782
|
try {
|
|
@@ -932,7 +961,7 @@ class AunPlugin {
|
|
|
932
961
|
for (const aid of Object.keys(this.bindings)) {
|
|
933
962
|
const definitions = await this.abilityDefinitions(aid);
|
|
934
963
|
const ownerAid = this.abilityOwners[aid];
|
|
935
|
-
const ownerVerified = Boolean(session && ownerAid && this.
|
|
964
|
+
const ownerVerified = Boolean(session && ownerAid && this.isAbilityOwnerVerified(aid, ownerAid));
|
|
936
965
|
const publishAvailable = Boolean(this.context?.managementActor?.(request) && ownerVerified && this.context?.callPluginAbility && this.abilityOperations && !this.abilityAuditUnavailable);
|
|
937
966
|
result[aid] = { agent: this.bindings[aid], selected: this.abilitySelections[aid] ?? [], definitions, published: Object.keys(this.abilityLedgers[aid] ?? {}).length > 0, pluginVersion: this.context?.pluginVersion?.(this.bindings[aid]), ownerAid, ownerVerified, publishAvailable, reason: publishAvailable ? "需本机已认证管理会话,每次执行仍须明确批准。" : "逐次 Owner 确认需本机已认证管理会话,并验证明确配置的 Owner AID;已有索引仍可撤销。" };
|
|
938
967
|
}
|
|
@@ -957,10 +986,10 @@ class AunPlugin {
|
|
|
957
986
|
if (paths.length) {
|
|
958
987
|
const session = this.context?.managementSession?.(request);
|
|
959
988
|
const owner = this.abilityOwners[aid];
|
|
960
|
-
if (!session || !owner || !this.
|
|
989
|
+
if (!session || !owner || !this.isAbilityOwnerVerified(aid, owner) || !this.context?.callPluginAbility || !this.abilityOperations || this.abilityAuditUnavailable)
|
|
961
990
|
return this.json(response, 409, { error: "逐次 Owner 确认需可用操作账本、本机已认证管理会话,并先验证该 Provider 的 Owner AID" });
|
|
962
991
|
const operation = (this.abilityPublications.get(aid) ?? Promise.resolve()).catch(() => undefined).then(async () => {
|
|
963
|
-
if (!this.
|
|
992
|
+
if (!this.isAbilityOwnerVerified(aid, owner) || this.abilityOwners[aid] !== owner || this.revokingAbilityAids.has(aid))
|
|
964
993
|
throw new Error("Owner 验证已失效或身份正在撤销,请重新确认");
|
|
965
994
|
const storage = this.clients.get(aid)?.client.storage;
|
|
966
995
|
if (!storage)
|
|
@@ -1000,6 +1029,8 @@ class AunPlugin {
|
|
|
1000
1029
|
const ownerAid = typeof body.ownerAid === "string" ? body.ownerAid.trim() : "";
|
|
1001
1030
|
if (!aidPattern.test(aid) || !this.bindings[aid] || !aidPattern.test(ownerAid))
|
|
1002
1031
|
return this.json(response, 400, { error: "需要已关联的 Provider AID 和明确指定的 Owner AID" });
|
|
1032
|
+
if (this.isAbilityOwnerVerified(aid, ownerAid))
|
|
1033
|
+
return this.json(response, 200, { ok: true, aid, ownerAid, alreadyVerified: true });
|
|
1003
1034
|
if (this.abilityOwners[aid] !== ownerAid) {
|
|
1004
1035
|
this.revokingAbilityAids.add(aid);
|
|
1005
1036
|
this.abilityRevisions.set(aid, (this.abilityRevisions.get(aid) ?? 0) + 1);
|
|
@@ -1007,12 +1038,16 @@ class AunPlugin {
|
|
|
1007
1038
|
try {
|
|
1008
1039
|
await this.invalidateAbilityOperations(aid);
|
|
1009
1040
|
const previous = this.abilityOwners;
|
|
1041
|
+
const previousVerifications = this.abilityOwnerVerifications;
|
|
1010
1042
|
this.abilityOwners = { ...previous, [aid]: ownerAid };
|
|
1043
|
+
this.abilityOwnerVerifications = { ...previousVerifications };
|
|
1044
|
+
delete this.abilityOwnerVerifications[aid];
|
|
1011
1045
|
try {
|
|
1012
1046
|
await this.persist();
|
|
1013
1047
|
}
|
|
1014
1048
|
catch (error) {
|
|
1015
1049
|
this.abilityOwners = previous;
|
|
1050
|
+
this.abilityOwnerVerifications = previousVerifications;
|
|
1016
1051
|
throw error;
|
|
1017
1052
|
}
|
|
1018
1053
|
}
|
|
@@ -1021,7 +1056,7 @@ class AunPlugin {
|
|
|
1021
1056
|
}
|
|
1022
1057
|
}
|
|
1023
1058
|
const challenge = this.ownerProof.start(session, aid, ownerAid);
|
|
1024
|
-
await this.callAunWithRecovery(aid, { to: ownerAid, encrypt: true, payload: { type: "text", text:
|
|
1059
|
+
await this.callAunWithRecovery(aid, { to: ownerAid, encrypt: true, payload: { type: "text", text: `AUN Ability 管理授权确认\nProvider AID:${aid}\nOwner AID:${ownerAid}\n用途:管理本 Provider 已公开的 Ability,并逐项确认能力调用。\n请直接回复:确认\n确认截止:${new Date(challenge.expiresAt).toLocaleString("zh-CN")}(最长 12 小时,受发起管理会话有效期限制)。\n此操作只验证 Owner 身份,不批准任何能力调用。` } });
|
|
1025
1060
|
this.context?.logger.info("AUN Owner 会话验证已发起,需本机已认证管理会话", { aid, ownerAid });
|
|
1026
1061
|
this.json(response, 200, { ok: true, aid, ownerAid, expiresAt: challenge.expiresAt });
|
|
1027
1062
|
}
|
|
@@ -1054,7 +1089,7 @@ class AunPlugin {
|
|
|
1054
1089
|
if (!original)
|
|
1055
1090
|
return this.json(response, 404, { error: "能力操作不存在" });
|
|
1056
1091
|
const ownerAid = this.abilityOwners[original.providerAid];
|
|
1057
|
-
if (!ownerAid || !this.
|
|
1092
|
+
if (!ownerAid || !this.isAbilityOwnerVerified(original.providerAid, ownerAid))
|
|
1058
1093
|
return this.json(response, 403, { error: "需本机已认证管理会话,并完成该 Provider 的 Owner AID 验证" });
|
|
1059
1094
|
if (original.confirmationFingerprint !== body.confirmationFingerprint)
|
|
1060
1095
|
return this.json(response, 409, { error: "确认指纹与待确认请求不一致" });
|
|
@@ -1065,7 +1100,7 @@ class AunPlugin {
|
|
|
1065
1100
|
if (failure || (this.abilityRevisions.get(original.providerAid) ?? 0) !== revision || this.revokingAbilityAids.has(original.providerAid) || original.executionTarget.pluginId !== pluginId || original.executionTarget.ownerAid !== ownerAid || original.executionTarget.pluginVersion !== (this.context?.pluginVersion?.(pluginId) ?? ""))
|
|
1066
1101
|
return this.json(response, 409, { error: failure?.message ?? "能力执行目标或 Owner 配置已改变,请重新发起调用" });
|
|
1067
1102
|
}
|
|
1068
|
-
if (!this.acceptingMessages || !this.
|
|
1103
|
+
if (!this.acceptingMessages || !this.isAbilityOwnerVerified(original.providerAid, ownerAid))
|
|
1069
1104
|
return this.json(response, 403, { error: "通道或 Owner 管理会话已失效" });
|
|
1070
1105
|
const confirmation = body.approved ? await store.approve(original.key, body.confirmationFingerprint) : await store.reject(original.key, body.confirmationFingerprint);
|
|
1071
1106
|
let entry = confirmation.entry;
|
|
@@ -1075,10 +1110,12 @@ class AunPlugin {
|
|
|
1075
1110
|
let handlerAuthorized = false;
|
|
1076
1111
|
try {
|
|
1077
1112
|
const failure = await this.abilityInvocationFailure(entry.providerAid, entry.authenticatedCaller, entry.path, entry.definitionId, entry.params);
|
|
1078
|
-
const
|
|
1113
|
+
const messageExpired = Date.now() >= entry.expiresAt;
|
|
1114
|
+
const confirmationExpired = Date.now() >= entry.confirmationExpiresAt;
|
|
1115
|
+
const expired = messageExpired || confirmationExpired;
|
|
1079
1116
|
const invalidated = (this.abilityRevisions.get(entry.providerAid) ?? 0) !== revision || this.revokingAbilityAids.has(entry.providerAid);
|
|
1080
|
-
if (failure || expired || invalidated || !this.
|
|
1081
|
-
result = { ...(0, ability_operations_1.abilityFailureResult)({ call_id: entry.callId, operation_id: entry.operationId, ability_path: entry.path, definition_id: entry.definitionId }, expired ? "message_expired" : failure?.code ?? (invalidated ? "provider_invalidated" : "permission_denied"),
|
|
1117
|
+
if (failure || expired || invalidated || !this.isAbilityOwnerVerified(entry.providerAid, ownerAid) || this.abilityOwners[entry.providerAid] !== entry.executionTarget.ownerAid || this.bindings[entry.providerAid] !== entry.executionTarget.pluginId || (this.context?.pluginVersion?.(String(entry.executionTarget.pluginId)) ?? "") !== entry.executionTarget.pluginVersion) {
|
|
1118
|
+
result = { ...(0, ability_operations_1.abilityFailureResult)({ call_id: entry.callId, operation_id: entry.operationId, ability_path: entry.path, definition_id: entry.definitionId }, expired ? (messageExpired ? "message_expired" : "confirmation_expired") : failure?.code ?? (invalidated ? "provider_invalidated" : "permission_denied"), messageExpired ? "调用已超过最晚启动时间" : confirmationExpired ? "Owner 确认申请已过期,不会执行" : failure?.message ?? "Owner 管理会话或执行目标已改变,本次能力没有执行"), execution_state: "not_executed" };
|
|
1082
1119
|
state = "failed";
|
|
1083
1120
|
}
|
|
1084
1121
|
else {
|
|
@@ -1086,7 +1123,7 @@ class AunPlugin {
|
|
|
1086
1123
|
const confirmed = entry;
|
|
1087
1124
|
const beforeStart = () => {
|
|
1088
1125
|
const currentSession = context.managementSession?.(request);
|
|
1089
|
-
if (this.context !== context || this.abilityOperations !== store || this.abilityAuditUnavailable || !this.acceptingMessages || !context.managementActor?.(request) || !currentSession || currentSession.id !== session.id || !this.
|
|
1126
|
+
if (this.context !== context || this.abilityOperations !== store || this.abilityAuditUnavailable || !this.acceptingMessages || !context.managementActor?.(request) || !currentSession || currentSession.id !== session.id || !this.isAbilityOwnerVerified(confirmed.providerAid, ownerAid) || this.abilityOwners[confirmed.providerAid] !== ownerAid)
|
|
1090
1127
|
throw Object.assign(new Error("管理会话、Owner 证明或操作账本已失效,本次能力没有执行"), { code: "confirmation_unavailable" });
|
|
1091
1128
|
const pluginId = String(confirmed.executionTarget.pluginId);
|
|
1092
1129
|
const rules = String(this.config.allow_from ?? "").split(",").map(value => value.trim()).filter(Boolean);
|
|
@@ -1102,8 +1139,8 @@ class AunPlugin {
|
|
|
1102
1139
|
catch (error) {
|
|
1103
1140
|
this.context?.logger.warn("AUN Ability 执行失败,结果将持久保存", { aid: entry.providerAid, path: entry.path, error: safeError(error) });
|
|
1104
1141
|
const code = error.code;
|
|
1105
|
-
const notExecuted = !handlerAuthorized && ["message_expired", "provider_invalidated", "confirmation_unavailable"].includes(code ?? "");
|
|
1106
|
-
result = { ...(0, ability_operations_1.abilityFailureResult)({ call_id: entry.callId, operation_id: entry.operationId, ability_path: entry.path, definition_id: entry.definitionId }, notExecuted ? code : "execution_failed", notExecuted ? (code === "message_expired" ? "调用已超过最晚启动时间" : "管理会话或 Provider 状态已改变,本次能力没有执行") : "能力执行失败,请检查 Provider 运行日志;不会自动重试"), execution_state: notExecuted ? "not_executed" : "failed" };
|
|
1142
|
+
const notExecuted = !handlerAuthorized && ["message_expired", "confirmation_expired", "provider_invalidated", "confirmation_unavailable"].includes(code ?? "");
|
|
1143
|
+
result = { ...(0, ability_operations_1.abilityFailureResult)({ call_id: entry.callId, operation_id: entry.operationId, ability_path: entry.path, definition_id: entry.definitionId }, notExecuted ? code : "execution_failed", notExecuted ? (code === "message_expired" ? "调用已超过最晚启动时间" : code === "confirmation_expired" ? "Owner 确认申请已过期,不会执行" : "管理会话或 Provider 状态已改变,本次能力没有执行") : "能力执行失败,请检查 Provider 运行日志;不会自动重试"), execution_state: notExecuted ? "not_executed" : "failed" };
|
|
1107
1144
|
state = "failed";
|
|
1108
1145
|
}
|
|
1109
1146
|
entry = await store.finish(entry.key, state, result);
|
|
@@ -1713,17 +1750,61 @@ class AunPlugin {
|
|
|
1713
1750
|
return;
|
|
1714
1751
|
}
|
|
1715
1752
|
const body = raw?.payload && typeof raw.payload === "object" && !Array.isArray(raw.payload) ? raw.payload : undefined;
|
|
1753
|
+
const envelope = raw?.envelope && typeof raw.envelope === "object" ? raw.envelope : undefined;
|
|
1754
|
+
const sender = typeof raw?.from === "string" ? raw.from : typeof envelope?.from === "string" ? envelope.from : undefined;
|
|
1755
|
+
if (body?.text === "确认" && this.ownerProof.hasPendingConfirmation(aid)) {
|
|
1756
|
+
const ownerAid = this.abilityOwners[aid];
|
|
1757
|
+
const authenticated = Boolean(ownerAid && sender === ownerAid && raw?.encrypted === true
|
|
1758
|
+
&& (raw.to === undefined || raw.to === aid) && (raw.sender_aid === undefined || raw.sender_aid === sender)
|
|
1759
|
+
&& (envelope?.from === undefined || envelope.from === sender) && (envelope?.to === undefined || envelope.to === aid));
|
|
1760
|
+
if (!authenticated || !this.ownerProof.acceptConfirmation(aid, sender, body.text, true)) {
|
|
1761
|
+
this.context?.logger.warn("AUN Owner 确认未通过验证", { traceId: inboundId, aid, sender: sender ?? "", stage: "Owner 确认校验", reason: authenticated ? "没有有效的待确认通知" : "需要配置 Owner AID 通过加密消息直接回复确认" });
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
try {
|
|
1765
|
+
await this.saveAbilityOwnerVerification(aid, ownerAid);
|
|
1766
|
+
}
|
|
1767
|
+
catch (error) {
|
|
1768
|
+
this.context?.logger.error("AUN Owner 验证状态保存失败", { traceId: inboundId, aid, ownerAid, stage: "保存 Owner 验证", error: safeError((0, agent_1.errorDetails)(error)) });
|
|
1769
|
+
try {
|
|
1770
|
+
await this.sendText(aid, sender, "Owner 控制权确认失败:验证结果无法持久保存,请检查 AUN 插件配置写入权限。", typeof body.thread_id === "string" ? body.thread_id : undefined, true);
|
|
1771
|
+
}
|
|
1772
|
+
catch (replyError) {
|
|
1773
|
+
this.context?.logger.warn("AUN Owner 验证结果回复失败", { aid, peer: sender, verified: false, stage: "发送 Owner 验证回执", error: safeError((0, agent_1.errorDetails)(replyError)) });
|
|
1774
|
+
}
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
await this.sendText(aid, sender, `Owner 控制权验证成功:${aid} 的 Owner 已保存。此操作不会批准任何能力调用,请回到 ABWeb 继续操作。`, typeof body.thread_id === "string" ? body.thread_id : undefined, true);
|
|
1779
|
+
}
|
|
1780
|
+
catch (error) {
|
|
1781
|
+
this.context?.logger.warn("AUN Owner 验证结果回复失败", { aid, peer: sender, verified: true, stage: "发送 Owner 验证回执", error: safeError((0, agent_1.errorDetails)(error)) });
|
|
1782
|
+
}
|
|
1783
|
+
this.context?.logger.info("AUN Owner 确认验证成功并已持久保存", { traceId: inboundId, aid, ownerAid });
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1716
1786
|
const ownerCommand = typeof body?.text === "string" && /^\/ability-owner(?:\s|$)/.test(body.text) ? body.text.match(/^\/ability-owner ([0-9a-f]{64})$/) : undefined;
|
|
1717
1787
|
if (typeof body?.text === "string" && /^\/ability-owner(?:\s|$)/.test(body.text)) {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1788
|
+
let verified = Boolean(ownerCommand && (envelope?.from === undefined || envelope.from === sender) && this.ownerProof.accept(aid, sender, ownerCommand[1], raw?.encrypted === true));
|
|
1789
|
+
if (verified) {
|
|
1790
|
+
const ownerAid = this.abilityOwners[aid];
|
|
1791
|
+
try {
|
|
1792
|
+
if (ownerAid === sender)
|
|
1793
|
+
await this.saveAbilityOwnerVerification(aid, ownerAid);
|
|
1794
|
+
else
|
|
1795
|
+
verified = false;
|
|
1796
|
+
}
|
|
1797
|
+
catch (error) {
|
|
1798
|
+
verified = false;
|
|
1799
|
+
this.context?.logger.error("AUN Owner 验证状态保存失败", { traceId: inboundId, aid, stage: "保存 Owner 验证", error: safeError((0, agent_1.errorDetails)(error)) });
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1721
1802
|
this.context?.logger.info("AUN 宿主委托的 Owner 控制权验证已处理", { aid, verified });
|
|
1722
1803
|
if (sender && raw?.encrypted === true && (envelope?.from === undefined || envelope.from === sender)) {
|
|
1723
1804
|
const text = verified
|
|
1724
|
-
? `Owner 控制权验证成功:${aid}
|
|
1805
|
+
? `Owner 控制权验证成功:${aid} 的 Owner 已保存。此操作不会批准任何能力调用,请回到 ABWeb 继续操作。`
|
|
1725
1806
|
: !ownerCommand
|
|
1726
|
-
? "Owner
|
|
1807
|
+
? "Owner 控制权验证失败:指令格式不正确。请在 ABWeb 重新发送确认通知,并直接回复“确认”。"
|
|
1727
1808
|
: "Owner 控制权验证失败:验证指令已过期、已使用,或与本次 Owner 身份不匹配。请在 ABWeb 重新发起验证,并使用指定 Owner AID 回复。";
|
|
1728
1809
|
try {
|
|
1729
1810
|
await this.sendText(aid, sender, text, typeof body.thread_id === "string" ? body.thread_id : undefined, true);
|
|
@@ -1764,14 +1845,51 @@ class AunPlugin {
|
|
|
1764
1845
|
}
|
|
1765
1846
|
return;
|
|
1766
1847
|
}
|
|
1767
|
-
const
|
|
1848
|
+
const queuedAt = Date.now();
|
|
1849
|
+
const notices = new Set();
|
|
1850
|
+
let queueTimer;
|
|
1851
|
+
const reportQueue = (started = false) => {
|
|
1852
|
+
if (!started && notices.size)
|
|
1853
|
+
return;
|
|
1854
|
+
const elapsedSeconds = Math.floor((Date.now() - queuedAt) / 1000);
|
|
1855
|
+
const stage = started ? "结束会话排队" : "等待同会话前序任务";
|
|
1856
|
+
const text = started ? `本条消息已结束会话排队,开始处理(排队 ${elapsedSeconds} 秒)。`
|
|
1857
|
+
: elapsedSeconds === 0 ? "消息已排队:当前会话仍有任务未完成,本条将按接收顺序处理。"
|
|
1858
|
+
: `本条消息仍在排队,已等待 ${elapsedSeconds} 秒;尚未转交 Agent,前序任务完成后自动处理。`;
|
|
1859
|
+
const payload = {
|
|
1860
|
+
type: "notice", subtype: started ? "task.dequeued" : "task.queued", severity: "info", text,
|
|
1861
|
+
task_id: message.messageId || inboundId, sent_at: Date.now(),
|
|
1862
|
+
...(message.threadId ? { thread_id: message.threadId } : {}),
|
|
1863
|
+
...(message.messageId ? { ref_message_id: message.messageId } : {}),
|
|
1864
|
+
};
|
|
1865
|
+
this.context?.logger.info("AUN 会话排队状态", { traceId: message.messageId || inboundId, aid, peer: message.peerId, threadId: message.threadId ?? "", agentPlugin, stage, elapsedSeconds });
|
|
1866
|
+
// 通知不占执行队列;notice 不会被对端作为普通任务自动互答。
|
|
1867
|
+
const notice = this.callAunWithRecovery(aid, { to: message.peerId, payload, encrypt: true })
|
|
1868
|
+
.then(() => undefined)
|
|
1869
|
+
.catch(error => this.context?.logger.warn("AUN 排队通知发送失败", { aid, peer: message.peerId, traceId: message.messageId || inboundId, stage, error: safeError((0, agent_1.errorDetails)(error)) }))
|
|
1870
|
+
.finally(() => { notices.delete(notice); });
|
|
1871
|
+
notices.add(notice);
|
|
1872
|
+
};
|
|
1873
|
+
const current = (pending ?? Promise.resolve()).catch(() => undefined).then(() => {
|
|
1874
|
+
clearInterval(queueTimer);
|
|
1875
|
+
if (pending)
|
|
1876
|
+
reportQueue(true);
|
|
1877
|
+
return this.processMessage(message, aid, agentPlugin, inboundId);
|
|
1878
|
+
});
|
|
1768
1879
|
this.pendingMessages.set(sourceKey, current);
|
|
1880
|
+
if (pending) {
|
|
1881
|
+
reportQueue();
|
|
1882
|
+
queueTimer = setInterval(reportQueue, 60_000);
|
|
1883
|
+
queueTimer.unref();
|
|
1884
|
+
}
|
|
1769
1885
|
try {
|
|
1770
1886
|
await current;
|
|
1771
1887
|
}
|
|
1772
1888
|
finally {
|
|
1889
|
+
clearInterval(queueTimer);
|
|
1773
1890
|
if (this.pendingMessages.get(sourceKey) === current)
|
|
1774
1891
|
this.pendingMessages.delete(sourceKey);
|
|
1892
|
+
await Promise.allSettled(notices);
|
|
1775
1893
|
}
|
|
1776
1894
|
}
|
|
1777
1895
|
async onFsMessage(raw, message, aid) {
|
|
@@ -1840,6 +1958,7 @@ class AunPlugin {
|
|
|
1840
1958
|
const { payload, callerAid } = call;
|
|
1841
1959
|
const rules = String(this.config.allow_from ?? "").split(",").map(value => value.trim()).filter(Boolean);
|
|
1842
1960
|
let result;
|
|
1961
|
+
let ownerNotice;
|
|
1843
1962
|
if (rules.length && !rules.includes("*") && !rules.includes(callerAid))
|
|
1844
1963
|
result = (0, ability_operations_1.abilityFailureResult)(payload, "permission_denied", "调用来源不在 AUN 允许名单中");
|
|
1845
1964
|
else if (payload.ability_path === "protocol/discover") {
|
|
@@ -1895,6 +2014,8 @@ class AunPlugin {
|
|
|
1895
2014
|
else {
|
|
1896
2015
|
const draft = await store.draft({ providerAid: aid, authenticatedCaller: callerAid, operationId: String(payload.operation_id), callId: String(payload.call_id), path, definitionId: String(payload.definition_id), params, effects: definition.effects, dedupeRetentionSeconds: definition.dedupe_retention_seconds, executionTarget: { pluginId, pluginVersion, ownerAid }, expiresAt, ...(payload.async === undefined ? {} : { async: payload.async }) });
|
|
1897
2016
|
result = this.abilityOperationResponse(draft.entry);
|
|
2017
|
+
if (draft.created)
|
|
2018
|
+
ownerNotice = draft.entry;
|
|
1898
2019
|
}
|
|
1899
2020
|
}
|
|
1900
2021
|
}
|
|
@@ -1907,6 +2028,30 @@ class AunPlugin {
|
|
|
1907
2028
|
}
|
|
1908
2029
|
}
|
|
1909
2030
|
await this.sendAbilityResponse(aid, callerAid, result);
|
|
2031
|
+
if (ownerNotice)
|
|
2032
|
+
await this.notifyAbilityOwner(ownerNotice);
|
|
2033
|
+
}
|
|
2034
|
+
async notifyAbilityOwner(entry) {
|
|
2035
|
+
const ownerAid = String(entry.executionTarget.ownerAid ?? "");
|
|
2036
|
+
const text = [
|
|
2037
|
+
"有新的远程 Ability 调用申请",
|
|
2038
|
+
`调用方:${entry.authenticatedCaller}`,
|
|
2039
|
+
`Provider AID:${entry.providerAid}`,
|
|
2040
|
+
`能力路径:${entry.path}`,
|
|
2041
|
+
`operation_id:${entry.operationId}`,
|
|
2042
|
+
`Owner 审批截止:${new Date(entry.confirmationExpiresAt).toLocaleString("zh-CN")}`,
|
|
2043
|
+
`调用最晚启动时间:${new Date(entry.expiresAt).toLocaleString("zh-CN")}`,
|
|
2044
|
+
"请在 ABWeb → AUN 通道 → 待确认能力调用中核对并批准一次或拒绝。只有在调用最晚启动时间前批准才会执行;12 小时审批期限不会延长协议调用 TTL。",
|
|
2045
|
+
].join("\n");
|
|
2046
|
+
try {
|
|
2047
|
+
const sent = await this.callAunWithRecovery(entry.providerAid, { to: ownerAid, encrypt: true, payload: { type: "text", text } });
|
|
2048
|
+
if (typeof sent?.status !== "string" || !successfulSendStatuses.has(sent.status))
|
|
2049
|
+
throw new Error(`AUN 通知投递状态未确认:${sent?.status === undefined ? "未返回状态" : String(sent.status)}`);
|
|
2050
|
+
this.context?.logger.info("AUN Ability Owner 申请通知已投递", { aid: entry.providerAid, ownerAid, callerAid: entry.authenticatedCaller, path: entry.path, operationId: entry.operationId, status: sent.status, stage: "Owner 申请通知" });
|
|
2051
|
+
}
|
|
2052
|
+
catch (error) {
|
|
2053
|
+
this.context?.logger.warn("AUN Ability Owner 申请通知发送失败,待确认请求仍保留在 ABWeb", { aid: entry.providerAid, ownerAid, callerAid: entry.authenticatedCaller, path: entry.path, operationId: entry.operationId, stage: "Owner 申请通知", error: safeError((0, agent_1.errorDetails)(error)) });
|
|
2054
|
+
}
|
|
1910
2055
|
}
|
|
1911
2056
|
async abilityInvocationFailure(aid, callerAid, path, definitionId, params) {
|
|
1912
2057
|
if (this.abilityAuditUnavailable)
|
|
@@ -2038,17 +2183,18 @@ class AunPlugin {
|
|
|
2038
2183
|
groups.set(kind, group);
|
|
2039
2184
|
}
|
|
2040
2185
|
groupsCount = groups.size;
|
|
2186
|
+
const captionKind = (groupsCount === 1 ? groups.keys().next().value : undefined) ?? "attachment";
|
|
2187
|
+
const payloadText = withAttachmentDownloadLinks(text || (captionKind === "image" ? "图片已生成。" : captionKind === "video" ? "视频已生成。" : captionKind === "file" ? "文件已生成。" : "附件已生成。"), captionKind, refs);
|
|
2188
|
+
fallbackText = payloadText;
|
|
2189
|
+
fallbackLinks = attachmentDownloadLinks(refs);
|
|
2041
2190
|
for (const [kind, group] of groups) {
|
|
2042
2191
|
signal?.throwIfAborted();
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
const payload = { type: kind, ...(kind === "image" ? {} : { text: payloadText, format: "markdown" }), attachments: group };
|
|
2192
|
+
// AUN 网页可能隐藏附件卡片的正文;所有附件发完后只另发一次完整正文。
|
|
2193
|
+
const payload = { type: kind, attachments: group };
|
|
2046
2194
|
if (message.threadId)
|
|
2047
2195
|
payload.thread_id = message.threadId;
|
|
2048
2196
|
if (message.messageId)
|
|
2049
2197
|
payload.ref_message_id = message.messageId;
|
|
2050
|
-
fallbackText = payloadText;
|
|
2051
|
-
fallbackLinks = attachmentDownloadLinks(group);
|
|
2052
2198
|
sending = true;
|
|
2053
2199
|
// message.send 失败时可能已送达,不自动重试或降级明文,避免重复发送和泄露引用。
|
|
2054
2200
|
const sendResult = await this.callAunWithRecovery(aid, { to: message.peerId, payload, encrypt: true });
|
|
@@ -2057,17 +2203,17 @@ class AunPlugin {
|
|
|
2057
2203
|
this.context?.logger.info("AUN 附件消息已发送", { ...trace, stage: "发送附件消息", kind, count: group.length });
|
|
2058
2204
|
sending = false;
|
|
2059
2205
|
sent++;
|
|
2060
|
-
if (
|
|
2206
|
+
if (sent === groupsCount && payloadText) {
|
|
2061
2207
|
try {
|
|
2062
2208
|
signal?.throwIfAborted();
|
|
2063
2209
|
const { attachments: _attachments, ...caption } = payload;
|
|
2064
2210
|
const result = await this.callAunWithRecovery(aid, { to: message.peerId, payload: { ...caption, type: "text", text: payloadText, format: "markdown" }, encrypt: true });
|
|
2065
2211
|
if (typeof result?.status !== "string" || !successfulSendStatuses.has(result.status))
|
|
2066
|
-
throw new Error(`AUN
|
|
2067
|
-
this.context?.logger.info("AUN
|
|
2212
|
+
throw new Error(`AUN 附件正文发送状态未确认:${result?.status === undefined ? "未返回状态" : String(result.status)}`);
|
|
2213
|
+
this.context?.logger.info("AUN 附件正文已发送", { ...trace, stage: "发送附件正文" });
|
|
2068
2214
|
}
|
|
2069
2215
|
catch (error) {
|
|
2070
|
-
throw new agent_1.AgentOperationError("AUN_ATTACHMENT_CAPTION_SEND_FAILED", "
|
|
2216
|
+
throw new agent_1.AgentOperationError("AUN_ATTACHMENT_CAPTION_SEND_FAILED", "附件已发送,发送 AUN 附件正文(结果未确认,不自动重发)", error);
|
|
2071
2217
|
}
|
|
2072
2218
|
}
|
|
2073
2219
|
}
|
|
@@ -189,7 +189,7 @@ class AunPeerMessagingStore {
|
|
|
189
189
|
return false;
|
|
190
190
|
const payload = value.payload;
|
|
191
191
|
const senderDeviceId = typeof value.sender_device_id === "string" ? value.sender_device_id : undefined;
|
|
192
|
-
const requestId = payload.type === "ability" && payload.role
|
|
192
|
+
const requestId = payload.type === "ability" && ["result", "ack"].includes(String(payload.role)) ? payload.call_id : payload.thread_id;
|
|
193
193
|
if (typeof requestId !== "string")
|
|
194
194
|
return false;
|
|
195
195
|
const current = this.entries.get(key(selfAid, requestId));
|
|
@@ -197,9 +197,11 @@ class AunPeerMessagingStore {
|
|
|
197
197
|
return false;
|
|
198
198
|
const expected = current.payload;
|
|
199
199
|
if (expected.type === "ability") {
|
|
200
|
-
if (payload.type !== "ability" || payload.role
|
|
200
|
+
if (payload.type !== "ability" || !["result", "ack"].includes(String(payload.role)) || ["call_id", "operation_id", "ability_path", "definition_id"].some(field => expected[field] !== payload[field]))
|
|
201
201
|
return false;
|
|
202
|
-
if (typeof payload.
|
|
202
|
+
if (typeof payload.execution_state !== "string" || discovery(expected) && (!senderDeviceId || payload.role !== "result"))
|
|
203
|
+
return false;
|
|
204
|
+
if (payload.role === "ack" ? !["pending", "pending_confirmation", "executing", "accepted"].includes(payload.execution_state) || payload.result !== undefined || payload.ok === false : typeof payload.ok !== "boolean")
|
|
203
205
|
return false;
|
|
204
206
|
}
|
|
205
207
|
else if (expected.type !== "text" || payload.type !== "text" || typeof payload.text !== "string")
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
for (const value of [aid, `${item.agent} / ${item.pluginVersion || "版本未知"}`]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); }
|
|
83
83
|
const ownerCell = document.createElement("td"); ownerCell.className = "ability-owner";
|
|
84
84
|
const owner = document.createElement("input"); owner.type = "text"; owner.value = item.ownerAid || ""; owner.placeholder = "明确指定 Owner AID"; owner.setAttribute("aria-label", `${aid} 宿主委托 Owner AID`);
|
|
85
|
-
const challenge = document.createElement("button"); challenge.type = "button"; challenge.textContent = "发送 Owner
|
|
85
|
+
const challenge = document.createElement("button"); challenge.type = "button"; challenge.textContent = "发送 Owner 确认通知";
|
|
86
86
|
const proof = document.createElement("p"); proof.setAttribute("role", "status");
|
|
87
87
|
ownerCell.append(owner, challenge, proof); row.append(ownerCell);
|
|
88
88
|
const choices = document.createElement("td");
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
save.title = verified ? state.item.reason || "保存当前选择的能力" : "请先验证当前管理会话的宿主委托 Owner";
|
|
103
103
|
revoke.disabled = state.busy || !state.item.published;
|
|
104
104
|
challenge.disabled = state.challengeBusy || !owner.value.trim();
|
|
105
|
-
proof.textContent = verified ? "
|
|
105
|
+
proof.textContent = verified ? "当前 Owner 已验证;每次调用仍须单独批准。" : state.challengeExpiresAt > Date.now() && owner.value.trim() === state.item.ownerAid ? `验证通知已通过 AUN 发送给指定 Owner,请直接回复“确认”;截止:${new Date(state.challengeExpiresAt).toLocaleString()}` : "当前 Owner 未验证,请发送确认通知。";
|
|
106
106
|
} };
|
|
107
107
|
abilityRows.set(aid, state);
|
|
108
108
|
owner.oninput = () => { state.sync(); for (const pending of pendingRows) pending.sync(); };
|
|
@@ -114,9 +114,9 @@
|
|
|
114
114
|
state.sync(); for (const pending of pendingRows) pending.sync();
|
|
115
115
|
try {
|
|
116
116
|
const result = await request("/api/plugins/aun/abilities/owner-challenge", "POST", { aid, ownerAid });
|
|
117
|
-
state.item = { ...state.item, ownerAid, ownerVerified:
|
|
118
|
-
state.challengeExpiresAt = result.expiresAt;
|
|
119
|
-
notify("Owner
|
|
117
|
+
state.item = { ...state.item, ownerAid, ownerVerified: Boolean(result.alreadyVerified), publishAvailable: Boolean(result.alreadyVerified) };
|
|
118
|
+
state.challengeExpiresAt = result.alreadyVerified ? 0 : result.expiresAt;
|
|
119
|
+
notify("Owner 确认通知结果", result.alreadyVerified ? `${ownerAid} 已完成验证,无需重复确认。` : `加密确认通知已通过 AUN 发送给 ${ownerAid}。请直接回复“确认”;仅验证 Owner 身份,不会执行能力。`);
|
|
120
120
|
} catch (error) { notify("Owner 验证发送失败", errorMessage(error), true); }
|
|
121
121
|
finally { state.challengeBusy = false; state.sync(); for (const pending of pendingRows) pending.sync(); }
|
|
122
122
|
};
|
|
@@ -153,17 +153,18 @@
|
|
|
153
153
|
cell(`operation_id: ${entry.operationId}\nkey: ${entry.key}\nconfirmationFingerprint: ${entry.confirmationFingerprint}`, true);
|
|
154
154
|
cell(`path: ${entry.path}\ndefinition_id: ${entry.definitionId}`, true);
|
|
155
155
|
cell(JSON.stringify(entry.params, null, 2), true);
|
|
156
|
-
const details = cell(""); const contract = document.createElement("pre"); contract.textContent = `effects: ${JSON.stringify(entry.effects)}\n执行目标: ${JSON.stringify(entry.executionTarget, null, 2)}\n
|
|
156
|
+
const details = cell(""); const contract = document.createElement("pre"); contract.textContent = `effects: ${JSON.stringify(entry.effects)}\n执行目标: ${JSON.stringify(entry.executionTarget, null, 2)}\nOwner 审批截止时间: ${new Date(entry.confirmationExpiresAt).toLocaleString()} (${entry.confirmationExpiresAt})\n调用最晚启动时间: ${new Date(entry.expiresAt).toLocaleString()} (${entry.expiresAt})\n状态: ${entry.state}`; details.append(contract);
|
|
157
157
|
const expiry = document.createElement("p"); details.append(expiry);
|
|
158
158
|
const actions = cell(""); const proof = document.createElement("p");
|
|
159
159
|
const approve = document.createElement("button"); approve.type = "button"; approve.textContent = "批准一次";
|
|
160
160
|
const reject = document.createElement("button"); reject.type = "button"; reject.textContent = "拒绝";
|
|
161
161
|
const pending = { sync: () => {
|
|
162
162
|
const verified = ownerVerified(entry.providerAid);
|
|
163
|
-
const
|
|
163
|
+
const confirmationExpired = !Number.isFinite(entry.confirmationExpiresAt) || entry.confirmationExpiresAt <= Date.now();
|
|
164
|
+
const messageExpired = !Number.isFinite(entry.expiresAt) || entry.expiresAt <= Date.now();
|
|
164
165
|
const unavailable = confirming || !verified || entry.state !== "pending_confirmation";
|
|
165
|
-
approve.disabled = unavailable ||
|
|
166
|
-
expiry.textContent =
|
|
166
|
+
approve.disabled = unavailable || confirmationExpired || messageExpired; reject.disabled = unavailable || confirmationExpired;
|
|
167
|
+
expiry.textContent = confirmationExpired ? "Owner 审批申请已过期" : messageExpired ? "调用已超过最晚启动时间;不能再执行,但仍可拒绝" : "可在调用截止前批准一次;审批记录最长保留 12 小时";
|
|
167
168
|
proof.textContent = verified ? "当前会话的 Owner 已验证" : "此 Provider 的 Owner 未验证,请先验证 Owner";
|
|
168
169
|
} };
|
|
169
170
|
const confirm = async (approved) => {
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<header><h1>AUN 身份</h1><button id="refresh" type="button">刷新</button></header>
|
|
7
7
|
<p id="status" role="status"></p>
|
|
8
8
|
<div class="table-scroll"><table><thead><tr><th>AID</th><th>来源</th><th>已关联 Agent</th><th>操作</th></tr></thead><tbody id="identities"></tbody></table></div>
|
|
9
|
-
<section><h2>Agent 能力发布</h2><p>默认不发布。请为每个 AID 明确选择公开能力,并验证宿主本地委托策略中的 Owner AID;此配置不代表 AUN 协议中的 ownership
|
|
10
|
-
<section><h2>待确认能力调用</h2><p
|
|
9
|
+
<section><h2>Agent 能力发布</h2><p>默认不发布。请为每个 AID 明确选择公开能力,并验证宿主本地委托策略中的 Owner AID;此配置不代表 AUN 协议中的 ownership 关系。验证通知通过 AUN 加密发送给指定 Owner,Owner 直接回复“确认”即可,不需要复制命令或令牌。发布能力后,每次调用仍须单独确认。</p><div class="table-scroll"><table><thead><tr><th>Provider AID</th><th>关联 Agent / 版本</th><th>宿主委托 Owner</th><th>可发布能力</th><th>操作</th></tr></thead><tbody id="abilities"></tbody></table></div></section>
|
|
10
|
+
<section><h2>待确认能力调用</h2><p>有效请求会出现在此表格,Peer 同时收到 pending 回执,Owner 会收到加密通知。Owner 审批记录保留 12 小时;协议 TTL 仍是调用最晚启动时间,超过后不能再执行。核对认证调用方、原始参数、契约指纹、影响和执行目标;没有当前会话的 Owner 证明时,不能确认执行。</p><div class="table-scroll"><table class="ability-pending-table"><thead><tr><th>Provider AID</th><th>认证调用方</th><th>操作与确认指纹</th><th>能力路径 / 契约版本</th><th>原始参数</th><th>影响、执行目标与有效期</th><th>逐次确认</th></tr></thead><tbody id="ability-pending"></tbody></table></div></section>
|
|
11
11
|
<form id="create-form"><h2>创建 AID</h2><div class="fields"><label>新 AID<input name="aid" required placeholder="例如:example.agentid.cn"></label><label>Owner AID<input name="ownerAid" placeholder="例如:owner.agentid.cn"><small class="hint">留空时使用已配置的默认 Owner;未配置时需填写。Owner 保存为本地归属配置;不代表 AUN 协议中的 Owner 关系已激活。</small></label></div><button type="submit">创建</button></form>
|
|
12
12
|
<form id="config-form"><h2>通道配置</h2><div class="fields"><label>允许的来源<input name="allow_from" placeholder="留空接受所有 Peer,填写 AID 后限制"></label><label>AUN 存储路径<input name="aun_path"></label><label>加密种子<input name="encryption_seed" type="password" autocomplete="new-password" placeholder="留空则不修改"></label></div><button type="submit">保存配置</button></form>
|
|
13
13
|
</main>
|
package/package.json
CHANGED