agent-bridge-aun 1.0.41 → 1.0.44

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.
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.abilityDefinitionId = abilityDefinitionId;
4
+ exports.publishAbilityCatalog = publishAbilityCatalog;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const indexPath = "/.well-known/ability/index.json";
7
+ const abilityPathPattern = /^(?!protocol\/|definitions\/)[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?(?:\/[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?)+$/;
8
+ const encoder = new TextEncoder();
9
+ function canonical(value) {
10
+ if (Array.isArray(value))
11
+ return `[${value.map(canonical).join(",")}]`;
12
+ if (!value || typeof value !== "object")
13
+ return JSON.stringify(value);
14
+ return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonical(item)}`).join(",")}}`;
15
+ }
16
+ function id(value) { return (0, node_crypto_1.createHash)("sha256").update(canonical(value)).digest("hex"); }
17
+ function abilityDefinitionId(definition) { return id(definition); }
18
+ function same(left, right) { return Buffer.from(left).equals(Buffer.from(right)); }
19
+ async function verifyPublicDocument(aid, path, expected) {
20
+ if (!/^[a-z0-9][a-z0-9._-]*\.[a-z0-9.-]+$/i.test(aid))
21
+ throw new Error("能力公开校验的 AID 无效");
22
+ const origin = new URL(`https://${aid}`);
23
+ const issuer = aid.slice(aid.indexOf(".") + 1);
24
+ const providerLabel = aid.slice(0, aid.indexOf("."));
25
+ let url = new URL(path, origin);
26
+ const signal = AbortSignal.timeout(15_000);
27
+ try {
28
+ for (let redirects = 0;; redirects++) {
29
+ const response = await fetch(url.href, { redirect: "manual", credentials: "omit", cache: "no-store", signal });
30
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
31
+ const location = response.headers.get("location");
32
+ await response.body?.cancel();
33
+ if (!location || redirects >= 5)
34
+ throw new Error("重定向缺少地址或超过 5 次");
35
+ const next = new URL(location, url);
36
+ const sameProvider = next.origin === origin.origin;
37
+ const issuerStorage = next.hostname === `storage.${issuer}` && ["", "9470"].includes(next.port) && next.pathname.startsWith(`/${providerLabel}/`);
38
+ const storageDownload = url.hostname === `storage.${issuer}` && ["", "9470"].includes(url.port) && url.pathname.startsWith(`/${providerLabel}/`) && next.origin === url.origin && next.pathname === "/api/storage/download";
39
+ if (next.protocol !== "https:" || next.username || next.password || next.hash || (!sameProvider && !issuerStorage && !storageDownload))
40
+ throw new Error("重定向不属于同一 Provider 或 issuer HTTPS 存储");
41
+ url = next;
42
+ continue;
43
+ }
44
+ if (!response.ok || !response.body) {
45
+ await response.body?.cancel();
46
+ throw new Error(`HTTP ${response.status}`);
47
+ }
48
+ const chunks = [];
49
+ let size = 0;
50
+ const reader = response.body.getReader();
51
+ try {
52
+ for (;;) {
53
+ const { done, value } = await reader.read();
54
+ if (done)
55
+ break;
56
+ size += value.byteLength;
57
+ if (size > 128 * 1024) {
58
+ await reader.cancel();
59
+ throw new Error("公开资料超过 128 KiB");
60
+ }
61
+ chunks.push(value);
62
+ }
63
+ }
64
+ finally {
65
+ reader.releaseLock();
66
+ }
67
+ if (!same(Buffer.concat(chunks), expected))
68
+ throw new Error("公开内容与发布内容不一致");
69
+ return;
70
+ }
71
+ }
72
+ catch (error) {
73
+ throw new Error(`能力资料匿名公开校验失败:${path}(${error instanceof Error ? error.message : "网络错误"})`, { cause: error });
74
+ }
75
+ }
76
+ async function readIndex(storage, aid) {
77
+ try {
78
+ const bytes = await storage.readBytes(indexPath);
79
+ const index = JSON.parse(Buffer.from(bytes).toString("utf8"));
80
+ if (index.schema !== "aun.ability.index.v1" || index.publisher !== aid || !Number.isSafeInteger(index.catalog_revision) || index.catalog_revision < 0 || !Array.isArray(index.capabilities))
81
+ throw new Error("已有 AUN 能力索引格式无效或 publisher 不匹配");
82
+ if (index.capabilities.some((entry) => !entry || typeof entry !== "object" || typeof entry.path !== "string" || typeof entry.definition_id !== "string"))
83
+ throw new Error("已有 AUN 能力索引包含无效条目");
84
+ return index;
85
+ }
86
+ catch (error) {
87
+ if (error.code === "ENOENT" || error.status === 404)
88
+ return { schema: "aun.ability.index.v1", publisher: aid, version: "1.0.0", catalog_revision: 0, capabilities: [] };
89
+ throw error;
90
+ }
91
+ }
92
+ async function publishAbilityCatalog(input) {
93
+ const { storage, aid, pluginId, pluginVersion } = input;
94
+ const definitions = new Map();
95
+ for (const definition of input.definitions) {
96
+ const path = definition.path;
97
+ if (typeof path !== "string" || !abilityPathPattern.test(path) || definitions.has(path))
98
+ throw new Error("Ability path 无效或重复");
99
+ if (definition.contract_revision !== undefined && (typeof definition.contract_revision !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(definition.contract_revision)))
100
+ throw new Error(`Ability ${path} 的契约修订号无效`);
101
+ if (definition.auth_required !== true || definition.require_confirm !== true || definition.idempotency !== "keyed" || !Array.isArray(definition.allowed_modes) || !definition.allowed_modes.includes("explicit"))
102
+ throw new Error(`Ability ${path} 的授权、确认或幂等字段不符合协议`);
103
+ definitions.set(path, definition);
104
+ }
105
+ const selected = new Set(input.selectedPaths);
106
+ if ([...selected].some((path) => !definitions.has(path)))
107
+ throw new Error("发布选择包含当前 Agent 未声明的 Ability path");
108
+ const oldIndex = await readIndex(storage, aid);
109
+ const oldLedger = input.ledger;
110
+ const otherEntries = oldIndex.capabilities.filter((entry) => {
111
+ const owned = oldLedger[String(entry.path)];
112
+ return !owned || ![owned.definition_id, ...(owned.previous_definition_ids ?? [])].includes(String(entry.definition_id));
113
+ });
114
+ const nextLedger = {};
115
+ const published = [];
116
+ for (const path of [...selected].sort()) {
117
+ const definition = definitions.get(path);
118
+ const definitionId = abilityDefinitionId(definition);
119
+ const releaseId = id({ aid, path, definition_id: definitionId, pluginId, pluginVersion });
120
+ const detailPath = `/ability/definitions/${definitionId}/${path}.json`;
121
+ const { contract_revision: _revision, ...publicDefinition } = definition;
122
+ const detail = { ...publicDefinition, path, definition_id: definitionId, description: definition.summary };
123
+ const bytes = encoder.encode(`${canonical(detail)}\n`);
124
+ try {
125
+ const existing = await storage.readBytes(detailPath);
126
+ if (!same(existing, bytes))
127
+ throw new Error(`L3 内容冲突:${path}`);
128
+ }
129
+ catch (error) {
130
+ if (error.code !== "ENOENT" && error.status !== 404)
131
+ throw error;
132
+ await storage.writeBytes(detailPath, bytes, { contentType: "application/json", public: true, overwrite: false });
133
+ }
134
+ await verifyPublicDocument(aid, detailPath, bytes);
135
+ nextLedger[path] = { definition_id: definitionId, release_id: releaseId, plugin_id: pluginId, plugin_version: pluginVersion };
136
+ published.push({ ...publicDefinition, definition_id: definitionId, release_id: releaseId, plugin_version: pluginVersion, detail_ref: `${aid}:${detailPath}` });
137
+ }
138
+ const capabilities = [...otherEntries, ...published].sort((left, right) => String(left.path).localeCompare(String(right.path)));
139
+ const changed = canonical(capabilities) !== canonical(oldIndex.capabilities);
140
+ const index = {
141
+ ...oldIndex,
142
+ schema: "aun.ability.index.v1",
143
+ publisher: aid,
144
+ version: "1.0.0",
145
+ catalog_revision: changed ? oldIndex.catalog_revision + 1 : oldIndex.catalog_revision,
146
+ ...(changed ? { updated_at: new Date().toISOString() } : {}),
147
+ capabilities,
148
+ };
149
+ if (changed) {
150
+ const pendingLedger = structuredClone(oldLedger);
151
+ for (const [path, record] of Object.entries(nextLedger)) {
152
+ const old = pendingLedger[path];
153
+ pendingLedger[path] = { ...record, ...(old ? { previous_definition_ids: [...new Set([old.definition_id, ...(old.previous_definition_ids ?? [])])].filter(value => value !== record.definition_id) } : {}) };
154
+ }
155
+ await input.beforeIndexWrite?.(pendingLedger);
156
+ await storage.writeBytes(indexPath, encoder.encode(`${JSON.stringify(index, null, 2)}\n`), { contentType: "application/json", public: true, overwrite: true });
157
+ const confirmed = await readIndex(storage, aid);
158
+ if (canonical(confirmed) !== canonical(index))
159
+ throw new Error("L1 能力索引写后读回不一致,发布状态未确认");
160
+ }
161
+ if (changed || index.capabilities.length > 0 || Object.keys(oldLedger).length > 0) {
162
+ await verifyPublicDocument(aid, indexPath, await storage.readBytes(indexPath));
163
+ }
164
+ return { index, ledger: nextLedger };
165
+ }