@agentunion/fastaun-browser 0.5.19 → 0.5.21
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/CHANGELOG.md +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
- package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +16 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +65 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +35 -3
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +384 -150
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +59 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +202 -16
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +4 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +24 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +89 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +6410 -1848
- package/dist/cert-utils.d.ts +50 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +123 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +108 -16
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1411 -184
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +203 -117
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +22 -3
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +1312 -684
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +419 -101
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +24 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +171 -29
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +42 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +14 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +30 -13
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +19 -2
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +193 -6
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +15 -2
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +308 -27
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +827 -21
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +43 -9
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +183 -81
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +36 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +610 -89
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/seq-tracker.d.ts +2 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +3 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +61 -3
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +524 -45
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +64 -30
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent-md.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AID } from './aid.js';
|
|
2
2
|
import { parseDocument } from 'yaml';
|
|
3
|
-
import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
|
|
4
|
-
import { parseFileFrontmatter, validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
3
|
+
import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, parseAgentMdTailSignatureForRead } from './cert-utils.js';
|
|
4
|
+
import { parseFileFrontmatter, validateAgentMdDocument, validateAgentMdDocumentForRead, verifyAgentMdUpload, verifyAgentMdLegacyBodyUpload } from './agent-md-schema.js';
|
|
5
5
|
import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
|
|
6
6
|
import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError } from './errors.js';
|
|
7
7
|
import { isJsonObject } from './types.js';
|
|
8
|
-
import { parseOwnershipFile } from './ownership-v21.js';
|
|
8
|
+
import { ownershipAid, parseOwnershipFile } from './ownership-v21.js';
|
|
9
9
|
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
10
10
|
export function buildOwnershipReleaseAgentMd(content, release) {
|
|
11
11
|
return updateOwnershipAgentMd(content, { ownership_release: release });
|
|
@@ -18,7 +18,7 @@ export function buildOwnershipAgentMdV21(content, statement) {
|
|
|
18
18
|
}
|
|
19
19
|
function updateOwnershipAgentMd(content, fields) {
|
|
20
20
|
const payload = parseAgentMdTailSignature(String(content ?? '')).payload;
|
|
21
|
-
const match =
|
|
21
|
+
const match = /^\ufeff?---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---([\s\S]*)$/.exec(payload);
|
|
22
22
|
if (!match)
|
|
23
23
|
throw new ValidationError('agent.md must start with YAML frontmatter');
|
|
24
24
|
const document = parseDocument(match[1], { uniqueKeys: true });
|
|
@@ -34,7 +34,7 @@ function updateOwnershipAgentMd(content, fields) {
|
|
|
34
34
|
for (const [name, value] of Object.entries(fields))
|
|
35
35
|
document.set(name, value);
|
|
36
36
|
const yamlText = document.toString().replace(/\r\n/g, '\n');
|
|
37
|
-
const updated =
|
|
37
|
+
const updated = `${payload.startsWith('\ufeff') ? '\ufeff' : ''}---\n${yamlText.endsWith('\n') ? yamlText : `${yamlText}\n`}---${match[2]}`;
|
|
38
38
|
validateAgentMdDocument(updated, { expectedAid: String(frontmatter.aid ?? '') });
|
|
39
39
|
return updated;
|
|
40
40
|
}
|
|
@@ -206,6 +206,10 @@ function requestWithNodeHost(input, init, timeoutMs) {
|
|
|
206
206
|
}
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
|
+
export function readAgentMdFrontmatter(content) {
|
|
210
|
+
const payload = parseAgentMdTailSignatureForRead(content).payload;
|
|
211
|
+
return payload.replace(/^\ufeff/, '').split(/\r\n|\n|\r/, 1)[0]?.trim() === '---' ? parseFileFrontmatter(payload, true, false) : {};
|
|
212
|
+
}
|
|
209
213
|
async function fetchWithTimeout(input, init, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
|
|
210
214
|
if (new Headers(init.headers).has('host')) {
|
|
211
215
|
return await requestWithNodeHost(input, init, timeoutMs);
|
|
@@ -327,7 +331,7 @@ export class AgentMdManager {
|
|
|
327
331
|
const target = this._safeAid(aid);
|
|
328
332
|
const gatewayUrl = await this._resolveGateway(target);
|
|
329
333
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
330
|
-
const headers = { Accept: 'text/markdown', 'X-AUN-Ownership-Check': '1', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
334
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', 'X-AUN-Ownership-Check': '1', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
331
335
|
let url = this._url(target, gatewayUrl);
|
|
332
336
|
if (viaGatewayHost) {
|
|
333
337
|
const parsed = new URL(gatewayUrl);
|
|
@@ -344,10 +348,19 @@ export class AgentMdManager {
|
|
|
344
348
|
if (fallback && fallback !== url)
|
|
345
349
|
response = await read(fallback);
|
|
346
350
|
}
|
|
347
|
-
if (!response.ok && response.status !== 404)
|
|
348
|
-
throw new StateError(`NS ownership check unavailable: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
|
|
349
351
|
if (headerValue(response.headers, 'X-AUN-Ownership-Status') === 'not_applicable')
|
|
350
352
|
throw new StateError('group/org uses its lifecycle owner relationship', { stringCode: 'not_applicable' });
|
|
353
|
+
if (response.status === 400) {
|
|
354
|
+
let failure;
|
|
355
|
+
try {
|
|
356
|
+
failure = await response.json();
|
|
357
|
+
}
|
|
358
|
+
catch { }
|
|
359
|
+
if (isJsonObject(failure) && failure.error === 'invalid')
|
|
360
|
+
throw new ValidationError('NS ownership material is invalid', { stringCode: 'invalid' });
|
|
361
|
+
}
|
|
362
|
+
if (!response.ok && response.status !== 404)
|
|
363
|
+
throw new StateError(`NS ownership check unavailable: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
|
|
351
364
|
const protocol = headerValue(response.headers, 'X-AUN-Ownership-Protocol');
|
|
352
365
|
const version = headerValue(response.headers, 'X-AUN-Ownership-Version');
|
|
353
366
|
const lock = headerValue(response.headers, 'X-AUN-Ownership-Lock');
|
|
@@ -369,7 +382,7 @@ export class AgentMdManager {
|
|
|
369
382
|
}
|
|
370
383
|
return { sub_aid: target, exists: response.ok, content, etag, protocol, relation_version: Number(version), lock: lock, observed_at: Math.trunc(Date.now() / 1000) };
|
|
371
384
|
}
|
|
372
|
-
async submitOwnershipRaw(aid, content, condition) {
|
|
385
|
+
async submitOwnershipRaw(aid, content, condition, guard) {
|
|
373
386
|
const target = this._safeAid(aid);
|
|
374
387
|
if (this._currentAid()?.aid !== target)
|
|
375
388
|
throw new ValidationError('ownership upload requires its own identity', { stringCode: 'forbidden' });
|
|
@@ -378,15 +391,44 @@ export class AgentMdManager {
|
|
|
378
391
|
throw new ValidationError('ownership upload requires its original document condition');
|
|
379
392
|
const gatewayUrl = await this._resolveGateway(target);
|
|
380
393
|
const token = await this._uploadAccessToken(target, gatewayUrl);
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
394
|
+
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
395
|
+
let url = this._url(target, gatewayUrl);
|
|
396
|
+
if (viaGatewayHost) {
|
|
397
|
+
const parsed = new URL(gatewayUrl);
|
|
398
|
+
const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
|
|
399
|
+
url = `${agentMdHttpScheme(gatewayUrl)}://${parsed.hostname}${port}/agent.md`;
|
|
400
|
+
}
|
|
401
|
+
const submit = async (targetUrl) => {
|
|
402
|
+
await guard?.();
|
|
403
|
+
return fetchWithTimeout(targetUrl, {
|
|
404
|
+
method: 'PUT', cache: 'no-store', redirect: 'error', body: content,
|
|
405
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', 'Content-Type': 'text/markdown; charset=utf-8', Authorization: `Bearer ${token}`,
|
|
406
|
+
...(viaGatewayHost ? { Host: target } : {}),
|
|
407
|
+
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }) },
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
let response;
|
|
411
|
+
try {
|
|
412
|
+
response = await submit(url);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
if (!viaGatewayHost)
|
|
416
|
+
throw error;
|
|
417
|
+
const fallback = nameServiceAgentMdUrl(target, gatewayUrl);
|
|
418
|
+
if (!fallback || fallback === url)
|
|
419
|
+
throw error;
|
|
420
|
+
url = fallback;
|
|
421
|
+
response = await submit(url);
|
|
422
|
+
}
|
|
423
|
+
if (response.status === 404 && !headerValue(response.headers, 'X-AUN-Ownership-Protocol') && headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable' && viaGatewayHost) {
|
|
424
|
+
const fallback = nameServiceAgentMdUrl(target, gatewayUrl);
|
|
425
|
+
if (fallback && fallback !== url)
|
|
426
|
+
response = await submit(fallback);
|
|
427
|
+
}
|
|
386
428
|
if (!response.ok) {
|
|
387
429
|
const reason = (await response.text()).trim();
|
|
388
430
|
const code = response.status === 409 || response.status === 412 ? 'conflict' : response.status === 401 || response.status === 403 ? 'forbidden' : response.status === 428 ? 'unsupported_protocol' : response.status >= 500 ? 'unavailable' : 'invalid';
|
|
389
|
-
throw new StateError(`
|
|
431
|
+
throw new StateError(`agent.md upload failed: HTTP ${response.status}${reason ? ` ${reason}` : ''}`, { stringCode: /owner_locked/.test(reason) ? 'owner_locked' : code, retryable: code === 'unavailable' });
|
|
390
432
|
}
|
|
391
433
|
return await this.readOwnershipFresh(target);
|
|
392
434
|
}
|
|
@@ -408,30 +450,23 @@ export class AgentMdManager {
|
|
|
408
450
|
if (!current.isPrivateKeyValid()) {
|
|
409
451
|
throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
|
|
410
452
|
}
|
|
411
|
-
|
|
412
|
-
const text = String(content ?? '');
|
|
413
|
-
if (text.length === 0)
|
|
414
|
-
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
415
|
-
validateAgentMdDocument(text, { expectedAid: target });
|
|
416
|
-
await this.saveRecord(target, {
|
|
417
|
-
content: text,
|
|
418
|
-
local_etag: await AgentMdManager.contentEtag(text),
|
|
419
|
-
fetched_at: Date.now(),
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
const localContent = await this.readContent(target);
|
|
453
|
+
const localContent = content ?? (String((await this.loadRecord(target, true))?.local_draft_content ?? '') || await this.readContent(target));
|
|
423
454
|
if (localContent === null || localContent.length === 0) {
|
|
424
455
|
throw new ValidationError('uploadAgentMd requires local agent.md content');
|
|
425
456
|
}
|
|
426
|
-
const parsed =
|
|
457
|
+
const parsed = this._parseBodyUpdate(localContent, target);
|
|
427
458
|
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
428
459
|
return await this.uploadAs(target, localContent, current);
|
|
429
|
-
|
|
460
|
+
if (parsed.signatures.length > 1) {
|
|
461
|
+
await this._verifyBodyUpload(target, localContent, current);
|
|
462
|
+
return await this._uploadPrepared(target, localContent, current);
|
|
463
|
+
}
|
|
464
|
+
const signedResult = await current.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : localContent);
|
|
430
465
|
if (!signedResult.ok) {
|
|
431
466
|
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
432
467
|
}
|
|
433
468
|
const signed = signedResult.data.signed;
|
|
434
|
-
await
|
|
469
|
+
await this._verifyBodyUpload(target, signed, current);
|
|
435
470
|
return await this._uploadPrepared(target, signed, current);
|
|
436
471
|
}
|
|
437
472
|
async uploadAs(targetAid, content, signer) {
|
|
@@ -439,19 +474,33 @@ export class AgentMdManager {
|
|
|
439
474
|
const rawContent = String(content ?? '');
|
|
440
475
|
if (!rawContent.trim())
|
|
441
476
|
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
442
|
-
const parsed =
|
|
477
|
+
const parsed = this._parseBodyUpdate(rawContent, target);
|
|
478
|
+
if (!['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signatures.length > 1) {
|
|
479
|
+
await this._verifyBodyUpload(target, rawContent, signer);
|
|
480
|
+
return await this._uploadPrepared(target, rawContent, signer);
|
|
481
|
+
}
|
|
443
482
|
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
483
|
+
await verifyAgentMdUpload(rawContent, signer.certPem, target, undefined, async (aid, fingerprint) => {
|
|
484
|
+
if (!this._entitySignerResolver)
|
|
485
|
+
throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
486
|
+
return await this._entitySignerResolver(aid, fingerprint);
|
|
487
|
+
});
|
|
488
|
+
return await this._uploadPrepared(target, rawContent, signer);
|
|
489
|
+
}
|
|
490
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
|
|
491
|
+
if (!this._entityIdentityGetter)
|
|
492
|
+
throw new StateError('entity signing identity is unavailable');
|
|
493
|
+
signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
|
|
494
|
+
if (signer.aid !== target)
|
|
495
|
+
throw new StateError('entity signing identity does not match target');
|
|
447
496
|
}
|
|
448
497
|
if (!signer?.isPrivateKeyValid())
|
|
449
498
|
throw new StateError('uploadAgentMd requires a valid signer identity');
|
|
450
|
-
const signedResult = await signer.signAgentMd(rawContent);
|
|
499
|
+
const signedResult = await signer.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : rawContent);
|
|
451
500
|
if (!signedResult.ok)
|
|
452
501
|
throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
|
|
453
502
|
const signed = signedResult.data.signed;
|
|
454
|
-
await
|
|
503
|
+
await this._verifyBodyUpload(target, signed, signer);
|
|
455
504
|
return await this._uploadPrepared(target, signed, signer);
|
|
456
505
|
}
|
|
457
506
|
async pendingUpload(aid) {
|
|
@@ -467,10 +516,55 @@ export class AgentMdManager {
|
|
|
467
516
|
const condition = pending.condition;
|
|
468
517
|
if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
|
|
469
518
|
throw new StateError('pending agent.md requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
519
|
+
const pendingDocument = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
|
|
520
|
+
const meta = pendingDocument.frontmatter;
|
|
521
|
+
const priorRelation = this._isLegacyBodyRelation(meta) && pending.relation && Object.keys(pending.relation).length === 4 && ['relation_version', 'lock', 'owner_aid', 'statement'].every((field) => Object.hasOwn(pending.relation, field));
|
|
522
|
+
if (this._isLegacyBodyRelation(meta) && pending.relation?.protocol !== 'legacy' && !priorRelation)
|
|
523
|
+
throw new StateError('legacy ownership pending agent.md requires its original relationship snapshot', { stringCode: 'unsupported_protocol' });
|
|
473
524
|
try {
|
|
525
|
+
if (priorRelation) {
|
|
526
|
+
const relation = pending.relation, statement = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid }).statement;
|
|
527
|
+
if (!statement || !this._sameBodyRelation(statement, relation.statement) || statement.relation_version !== relation.relation_version || statement.owner_aid !== relation.owner_aid || !['locked', 'unlocked'].includes(String(relation.lock)) || pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
528
|
+
throw new StateError('pending original relationship context is corrupt', { stringCode: 'persistence_unavailable' });
|
|
529
|
+
const identity = signer ?? this._currentAid();
|
|
530
|
+
if (!identity || identity.aid !== aid || this._currentAid()?.aid !== aid || this._ownerAid() !== aid)
|
|
531
|
+
throw new StateError('body recovery requires its current subject identity', { stringCode: 'forbidden' });
|
|
532
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
533
|
+
const remote = await this.readAgentMdFresh(aid);
|
|
534
|
+
if (!remote.exists || !this._sameBodyRelation(this._bodyRelationFields(meta), this._bodyRelationFields(validateAgentMdDocumentForRead(remote.content, { expectedAid: aid, validateOwnership: false }).frontmatter)))
|
|
535
|
+
throw new StateError('pending agent.md relationship material changed', { stringCode: 'conflict' });
|
|
536
|
+
if (remote.etag === pending.content_etag && remote.content === pending.content) {
|
|
537
|
+
await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
if (!condition.if_match || remote.etag !== condition.if_match)
|
|
541
|
+
throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
|
|
542
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
if (pending.relation?.protocol === 'legacy') {
|
|
546
|
+
const relation = pending.relation;
|
|
547
|
+
if (!condition.if_match || condition.if_match !== relation.original_etag || typeof relation.original_content !== 'string' || relation.original_content_etag !== await AgentMdManager.contentEtag(relation.original_content) || pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
548
|
+
throw new StateError('pending legacy original bytes or document condition are corrupt', { stringCode: 'persistence_unavailable' });
|
|
549
|
+
const original = await this._verifyLegacyBodyOriginal(aid, relation.original_content);
|
|
550
|
+
if (!this._isLegacyBodyRelation(original) || !this._sameBodyRelation(this._bodyRelationFields(original), relation.fields))
|
|
551
|
+
throw new StateError('pending legacy original relationship snapshot is corrupt', { stringCode: 'persistence_unavailable' });
|
|
552
|
+
if (!this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
|
|
553
|
+
throw new StateError('legacy relationship material changed', { stringCode: 'conflict' });
|
|
554
|
+
const identity = signer ?? this._currentAid();
|
|
555
|
+
if (!identity || identity.aid.toLowerCase() !== aid.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== aid.toLowerCase())
|
|
556
|
+
throw new StateError('legacy body recovery requires its current subject identity', { stringCode: 'forbidden' });
|
|
557
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
558
|
+
const remote = await this._readLegacyBodyFresh(aid);
|
|
559
|
+
if (remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
560
|
+
await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
if (remote.etag !== condition.if_match || remote.content !== relation.original_content)
|
|
564
|
+
throw new StateError('pending legacy original document changed', { stringCode: 'conflict' });
|
|
565
|
+
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
474
568
|
if (['group', 'org'].includes(String(meta.type))) {
|
|
475
569
|
const identity = signer ?? this._currentAid();
|
|
476
570
|
if (!identity)
|
|
@@ -485,12 +579,10 @@ export class AgentMdManager {
|
|
|
485
579
|
if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
486
580
|
throw new StateError('pending entity original bytes are corrupt', { stringCode: 'persistence_unavailable' });
|
|
487
581
|
const documentSigner = await this._entityDocumentSigner(aid, pending.content);
|
|
488
|
-
await
|
|
489
|
-
if (identity.aid.toLowerCase() !== aid.toLowerCase())
|
|
490
|
-
await this._validateEntityUploadAuthorization(pending.authorization, aid, pending.content, identity, String(context.entity_cert_fingerprint));
|
|
582
|
+
await this._verifyEntityDocument(aid, pending.content);
|
|
491
583
|
const remote = await this._readEntityFresh(aid);
|
|
492
584
|
if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
493
|
-
await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
585
|
+
await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
494
586
|
return true;
|
|
495
587
|
}
|
|
496
588
|
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
@@ -498,38 +590,29 @@ export class AgentMdManager {
|
|
|
498
590
|
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
499
591
|
return true;
|
|
500
592
|
}
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
if (candidate.statement && !relation)
|
|
507
|
-
throw fail('persistence_unavailable', 'pending ownership body update requires its original relationship context');
|
|
508
|
-
if (relation) {
|
|
509
|
-
if (remote.relation_version !== relation.relation_version || remote.lock !== relation.lock)
|
|
510
|
-
throw fail('conflict', 'pending agent.md original relationship changed');
|
|
511
|
-
if (!sameStatement(candidate.statement, relation.statement))
|
|
512
|
-
throw fail('unsupported_protocol', 'ownership changes require their persisted 2.1 task');
|
|
513
|
-
if (remote.exists && (remote.relation_version > 0 || candidate.statement)) {
|
|
514
|
-
const current = parseOwnershipFile(remote.content, 'agent', { expectedSubAid: aid });
|
|
515
|
-
if (!sameStatement(current.statement, relation.statement) || (remote.owner_aid ?? current.statement?.owner_aid ?? null) !== relation.owner_aid)
|
|
516
|
-
throw fail('conflict', 'pending agent.md relationship statement changed');
|
|
517
|
-
}
|
|
593
|
+
const identity = signer ?? this._currentAid();
|
|
594
|
+
if (pendingDocument.signatures.length > 1) {
|
|
595
|
+
if (!identity)
|
|
596
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
597
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
518
598
|
}
|
|
519
|
-
if (
|
|
520
|
-
throw
|
|
599
|
+
if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
|
|
600
|
+
throw new StateError('pending agent.md original bytes are corrupt', { stringCode: 'persistence_unavailable' });
|
|
601
|
+
const remote = await this.readAgentMdFresh(aid);
|
|
602
|
+
if (remote.exists && this._isLegacyBodyRelation(validateAgentMdDocumentForRead(remote.content, { expectedAid: aid, validateOwnership: false }).frontmatter))
|
|
603
|
+
throw new StateError('legacy relationship material cannot be removed or replaced', { stringCode: 'conflict' });
|
|
521
604
|
if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
|
|
522
|
-
const saved = await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
605
|
+
const saved = await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
523
606
|
if (!saved.aid)
|
|
524
607
|
throw new StateError('agent.md pending completion persistence failed');
|
|
525
608
|
return true;
|
|
526
609
|
}
|
|
527
610
|
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
528
611
|
throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
|
|
529
|
-
const identity = signer ?? this._currentAid();
|
|
530
612
|
if (!identity)
|
|
531
613
|
throw new StateError('pending agent.md requires a signer identity');
|
|
532
|
-
|
|
614
|
+
if (pendingDocument.signatures.length <= 1)
|
|
615
|
+
await this._verifyBodyUpload(aid, pending.content, identity);
|
|
533
616
|
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
534
617
|
return true;
|
|
535
618
|
}
|
|
@@ -538,33 +621,146 @@ export class AgentMdManager {
|
|
|
538
621
|
throw error;
|
|
539
622
|
}
|
|
540
623
|
}
|
|
624
|
+
_parseBodyUpdate(content, target) {
|
|
625
|
+
const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false });
|
|
626
|
+
return validateAgentMdDocument(this._isLegacyBodyRelation(parsed.frontmatter) && parsed.signatures.length <= 1 ? parsed.payload : content, { expectedAid: target, validateOwnership: false });
|
|
627
|
+
}
|
|
628
|
+
_isLegacyBodyRelation(meta) {
|
|
629
|
+
if (['group', 'org'].includes(String(meta.type)))
|
|
630
|
+
return false;
|
|
631
|
+
return ['owner', 'ownership', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(meta, field));
|
|
632
|
+
}
|
|
633
|
+
_bodyRelationFields(meta) {
|
|
634
|
+
return Object.fromEntries(['owner', 'ownership', 'ownership_proof', 'ownership_release'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]]));
|
|
635
|
+
}
|
|
636
|
+
_sameBodyRelation(first, second) {
|
|
637
|
+
const stable = (value) => Array.isArray(value) ? value.map(stable) : isJsonObject(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])) : value;
|
|
638
|
+
return JSON.stringify(stable(first)) === JSON.stringify(stable(second));
|
|
639
|
+
}
|
|
640
|
+
async readAgentMdFresh(aid) {
|
|
641
|
+
const target = this._safeAid(aid);
|
|
642
|
+
const gateway = await this._resolveGateway(target);
|
|
643
|
+
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
644
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
645
|
+
let url = this._url(target, gateway);
|
|
646
|
+
if (viaGatewayHost) {
|
|
647
|
+
const parsed = new URL(gateway);
|
|
648
|
+
const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
|
|
649
|
+
url = `${agentMdHttpScheme(gateway)}://${parsed.hostname}${port}/agent.md`;
|
|
650
|
+
}
|
|
651
|
+
let response;
|
|
652
|
+
try {
|
|
653
|
+
response = await fetchWithTimeout(url, { method: 'GET', headers, cache: 'no-store', redirect: 'error' });
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
const fallback = viaGatewayHost ? nameServiceAgentMdUrl(target, gateway) : '';
|
|
657
|
+
if (!fallback || fallback === url)
|
|
658
|
+
throw error;
|
|
659
|
+
response = await fetchWithTimeout(fallback, { method: 'GET', headers, cache: 'no-store', redirect: 'error' });
|
|
660
|
+
}
|
|
661
|
+
if (response.status === 404)
|
|
662
|
+
return { exists: false, content: '', etag: '', last_modified: '', status: 404 };
|
|
663
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
664
|
+
if (response.status !== 200 || !etag)
|
|
665
|
+
throw new StateError('agent.md original document or ETag is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
666
|
+
let content;
|
|
667
|
+
try {
|
|
668
|
+
content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
|
|
672
|
+
}
|
|
673
|
+
return { exists: true, content, etag, last_modified: headerValue(response.headers, 'Last-Modified'), status: 200 };
|
|
674
|
+
}
|
|
675
|
+
async _readLegacyBodyFresh(target) {
|
|
676
|
+
const fresh = await this.readAgentMdFresh(target);
|
|
677
|
+
if (!fresh.exists)
|
|
678
|
+
throw new StateError('legacy relationship cannot be created by an ordinary body update', { stringCode: 'unsupported_protocol' });
|
|
679
|
+
return fresh;
|
|
680
|
+
}
|
|
681
|
+
async _verifyLegacyBodyOriginal(target, content) {
|
|
682
|
+
const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false, requireSignature: true });
|
|
683
|
+
const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
|
|
684
|
+
const verified = await this._verifyPeerDocument(peer, content);
|
|
685
|
+
if (!verified.ok || verified.data.status !== 'verified')
|
|
686
|
+
throw new ClientSignatureError('legacy original agent.md signature verification failed', { stringCode: 'invalid' });
|
|
687
|
+
return parsed.frontmatter;
|
|
688
|
+
}
|
|
689
|
+
async _verifyPeerDocument(peer, content) {
|
|
690
|
+
const parsed = parseAgentMdTailSignatureForRead(content);
|
|
691
|
+
const additionalCertificates = new Map();
|
|
692
|
+
if (parsed.fields)
|
|
693
|
+
additionalCertificates.set(`${parsed.fields.signer_aid ?? peer.aid};${parsed.fields.cert_fingerprint}`, peer);
|
|
694
|
+
for (const block of parsed.signatures.slice(1))
|
|
695
|
+
additionalCertificates.set(`${block.fields.signer_aid};${block.fields.cert_fingerprint}`, await this._resolvePeer(block.fields.signer_aid, block.fields.cert_fingerprint, Number(block.fields.timestamp)));
|
|
696
|
+
return await peer.verifyAgentMd(content, (signer, fingerprint) => {
|
|
697
|
+
const certificate = additionalCertificates.get(`${signer};${fingerprint}`);
|
|
698
|
+
if (!certificate)
|
|
699
|
+
throw new StateError('additional signing certificate is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
700
|
+
return certificate;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
async _verifyBodyUpload(target, content, signer) {
|
|
704
|
+
const meta = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
705
|
+
await (this._isLegacyBodyRelation(meta) ? verifyAgentMdLegacyBodyUpload : verifyAgentMdUpload)(content, signer.certPem, target, undefined, async (aid, fingerprint, timestamp) => {
|
|
706
|
+
if (this._entitySignerResolver)
|
|
707
|
+
return await this._entitySignerResolver(aid, fingerprint);
|
|
708
|
+
return await this._resolvePeer(aid, fingerprint, timestamp);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
541
711
|
async _uploadPrepared(target, signed, signer, originalCondition) {
|
|
542
|
-
const meta = validateAgentMdDocument(signed, { expectedAid: target }).frontmatter;
|
|
712
|
+
const meta = validateAgentMdDocument(signed, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
713
|
+
if (this._isLegacyBodyRelation(meta))
|
|
714
|
+
validateOwnershipFrontmatter(Object.fromEntries(['aid', 'type', 'owner'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]])), target);
|
|
715
|
+
if (!['group', 'org'].includes(String(meta.type)))
|
|
716
|
+
await this._verifyBodyUpload(target, signed, signer);
|
|
717
|
+
if (!originalCondition) {
|
|
718
|
+
const draft = await this.saveRecord(target, { local_draft_content: signed });
|
|
719
|
+
if (draft.local_draft_content !== signed)
|
|
720
|
+
throw new StateError('agent.md local draft persistence failed', { stringCode: 'persistence_unavailable' });
|
|
721
|
+
}
|
|
543
722
|
const entity = ['group', 'org'].includes(String(meta.type));
|
|
544
723
|
const uploader = entity ? this._currentAid() ?? signer : signer;
|
|
545
724
|
const entityContext = entity ? await this._entityContext(target, meta, uploader) : undefined;
|
|
546
725
|
if (entity)
|
|
547
|
-
await
|
|
726
|
+
await this._verifyEntityDocument(target, signed);
|
|
548
727
|
let condition = originalCondition;
|
|
549
|
-
|
|
728
|
+
const pendingRelation = originalCondition ? (await this.pendingUpload(target))?.relation : undefined;
|
|
729
|
+
let relation = entityContext ?? (pendingRelation?.protocol === 'legacy' ? pendingRelation : {});
|
|
550
730
|
if (!condition) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
731
|
+
let fresh;
|
|
732
|
+
if (!entity && this._isLegacyBodyRelation(meta))
|
|
733
|
+
fresh = await this._readLegacyBodyFresh(target);
|
|
734
|
+
else
|
|
735
|
+
fresh = entity ? await this._readEntityFresh(target) : await this.readAgentMdFresh(target);
|
|
736
|
+
const freshMeta = !entity && (!('exists' in fresh) || fresh.exists) && fresh.content ? validateAgentMdDocumentForRead(fresh.content, { expectedAid: target, validateOwnership: false }).frontmatter : undefined;
|
|
737
|
+
const legacy = !entity && (this._isLegacyBodyRelation(meta) || freshMeta && this._isLegacyBodyRelation(freshMeta));
|
|
738
|
+
if (legacy) {
|
|
739
|
+
if (!freshMeta || !this._isLegacyBodyRelation(freshMeta))
|
|
740
|
+
throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
|
|
741
|
+
if (!this._sameBodyRelation(this._bodyRelationFields(meta), this._bodyRelationFields(freshMeta)))
|
|
742
|
+
throw new StateError('legacy relationship material changed', { stringCode: 'conflict' });
|
|
743
|
+
if (!fresh.etag)
|
|
744
|
+
throw new StateError('legacy original document ETag is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
745
|
+
await this._verifyLegacyBodyOriginal(target, fresh.content);
|
|
746
|
+
condition = { if_match: fresh.etag };
|
|
747
|
+
relation = { protocol: 'legacy', original_content: fresh.content, original_content_etag: await AgentMdManager.contentEtag(fresh.content), original_etag: fresh.etag, fields: this._bodyRelationFields(freshMeta) };
|
|
748
|
+
}
|
|
749
|
+
else {
|
|
750
|
+
condition = 'exists' in fresh && fresh.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
|
|
558
751
|
}
|
|
559
752
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
753
|
+
if (relation?.protocol === 'legacy') {
|
|
754
|
+
if (signer.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.certFingerprint !== signer.certFingerprint)
|
|
755
|
+
throw new StateError('legacy body update requires its current subject identity', { stringCode: 'forbidden' });
|
|
756
|
+
if (!this._isLegacyBodyRelation(meta) || !this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
|
|
757
|
+
throw new StateError('legacy relationship material changed', { stringCode: 'conflict' });
|
|
758
|
+
await this._verifyBodyUpload(target, signed, signer);
|
|
566
759
|
}
|
|
760
|
+
const entityAuthorization = '';
|
|
567
761
|
const pending = await this.saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signed, pending_content_etag: await AgentMdManager.contentEtag(signed), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization });
|
|
762
|
+
if (relation?.protocol === 'legacy' && !this._sameBodyRelation(pending.pending_relation, relation))
|
|
763
|
+
throw new StateError('legacy original relationship snapshot persistence failed', { stringCode: 'persistence_unavailable' });
|
|
568
764
|
const savedCondition = pending.pending_condition;
|
|
569
765
|
if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
|
|
570
766
|
throw new StateError('agent.md pending upload persistence failed');
|
|
@@ -573,11 +769,12 @@ export class AgentMdManager {
|
|
|
573
769
|
const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
|
|
574
770
|
if (delegated && !owner?.isPrivateKeyValid())
|
|
575
771
|
throw new StateError('owner agent.md upload requires a loaded owner identity');
|
|
576
|
-
const authorization = entity ?
|
|
772
|
+
const authorization = entity ? '' : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
|
|
577
773
|
const result = await this._uploadHttp(target, signed, authorization, condition);
|
|
578
774
|
const remoteEtag = String(result.etag ?? '').trim();
|
|
775
|
+
const contentEtag = await AgentMdManager.contentEtag(signed);
|
|
579
776
|
const saved = await this.saveRecord(target, {
|
|
580
|
-
content: signed, local_etag:
|
|
777
|
+
content: signed, local_etag: contentEtag, remote_etag: remoteEtag || undefined, ...(remoteEtag === contentEtag ? { local_draft_content: '' } : {}),
|
|
581
778
|
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
|
|
582
779
|
remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
|
|
583
780
|
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
@@ -595,12 +792,17 @@ export class AgentMdManager {
|
|
|
595
792
|
const fresh = await this._readEntityFresh(this._safeAid(target));
|
|
596
793
|
if (fresh.ownership_status !== 'not_applicable' || !['current', 'stale'].includes(fresh.entity_status ?? '') || !fresh.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(fresh.entity_cert_fingerprint ?? ''))
|
|
597
794
|
throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
598
|
-
|
|
795
|
+
try {
|
|
796
|
+
return { owner_aid: this._safeAid(ownershipAid(fresh.owner_aid, 'owner_aid')), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
800
|
+
}
|
|
599
801
|
}
|
|
600
802
|
async _readEntityFresh(target) {
|
|
601
803
|
const gateway = await this._resolveGateway(target);
|
|
602
804
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
603
|
-
const headers = { 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
805
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', 'X-AUN-Ownership-Check': '1', 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
|
|
604
806
|
let url = this._url(target, gateway);
|
|
605
807
|
if (viaGatewayHost) {
|
|
606
808
|
const parsed = new URL(gateway);
|
|
@@ -656,8 +858,8 @@ export class AgentMdManager {
|
|
|
656
858
|
}
|
|
657
859
|
async _entityContext(target, meta, signer) {
|
|
658
860
|
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
659
|
-
if (
|
|
660
|
-
throw new StateError('group/org agent.md requires
|
|
861
|
+
if (signer.aid.toLowerCase() !== owner || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase())
|
|
862
|
+
throw new StateError('group/org agent.md requires current owner uploader', { stringCode: 'forbidden' });
|
|
661
863
|
const context = await this._entityCurrentContext(target, meta, true);
|
|
662
864
|
if (signer.aid.toLowerCase() === owner)
|
|
663
865
|
await this._verifyEntityOwnerSigner(signer);
|
|
@@ -683,6 +885,17 @@ export class AgentMdManager {
|
|
|
683
885
|
throw new StateError('entity signing certificate changed', { stringCode: 'conflict' });
|
|
684
886
|
return signer;
|
|
685
887
|
}
|
|
888
|
+
async _verifyEntityDocument(target, content) {
|
|
889
|
+
const primary = await this._entityDocumentSigner(target, content);
|
|
890
|
+
await verifyAgentMdUpload(content, primary.certPem, target, undefined, async (signer, fingerprint) => {
|
|
891
|
+
if (!this._entitySignerResolver)
|
|
892
|
+
throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
893
|
+
const material = await this._entitySignerResolver(signer, fingerprint);
|
|
894
|
+
if (material.aid !== signer || material.certFingerprint !== fingerprint)
|
|
895
|
+
throw new StateError('entity additional signing certificate changed', { stringCode: 'conflict' });
|
|
896
|
+
return material;
|
|
897
|
+
});
|
|
898
|
+
}
|
|
686
899
|
async _verifyEntityOwnerSigner(owner) {
|
|
687
900
|
if (!this._entitySignerResolver)
|
|
688
901
|
throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
|
|
@@ -694,15 +907,15 @@ export class AgentMdManager {
|
|
|
694
907
|
async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
|
|
695
908
|
const target = this._safeAid(String(aid ?? this._ownerAid() ?? '').trim());
|
|
696
909
|
const contentResult = await this._downloadHttp(target, timeoutMs);
|
|
697
|
-
const parsed =
|
|
910
|
+
const parsed = parseAgentMdTailSignatureForRead(contentResult.content);
|
|
698
911
|
let entity = false;
|
|
699
912
|
try {
|
|
700
|
-
entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload).type));
|
|
913
|
+
entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload, false, false).type));
|
|
701
914
|
}
|
|
702
915
|
catch { }
|
|
703
916
|
let frontmatter;
|
|
704
917
|
try {
|
|
705
|
-
frontmatter =
|
|
918
|
+
frontmatter = validateAgentMdDocumentForRead(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
|
|
706
919
|
}
|
|
707
920
|
catch (error) {
|
|
708
921
|
if (entity)
|
|
@@ -710,10 +923,9 @@ export class AgentMdManager {
|
|
|
710
923
|
throw error;
|
|
711
924
|
}
|
|
712
925
|
const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
const
|
|
716
|
-
const verified = await peer.verifyAgentMd(contentResult.content);
|
|
926
|
+
const signerAid = entity ? (parsed.fields?.signer_aid ?? target) : target;
|
|
927
|
+
const peer = await this._resolvePeer(signerAid, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
|
|
928
|
+
const verified = await this._verifyPeerDocument(peer, contentResult.content);
|
|
717
929
|
if (!verified.ok)
|
|
718
930
|
throw new AUNError(verified.error.message);
|
|
719
931
|
const signature = { ...verified.data };
|
|
@@ -767,6 +979,7 @@ export class AgentMdManager {
|
|
|
767
979
|
return {
|
|
768
980
|
aid: target,
|
|
769
981
|
content: contentResult.content,
|
|
982
|
+
frontmatter: readAgentMdFrontmatter(contentResult.content),
|
|
770
983
|
verification,
|
|
771
984
|
signature,
|
|
772
985
|
cert_pem: peer.certPem,
|
|
@@ -967,11 +1180,36 @@ export class AgentMdManager {
|
|
|
967
1180
|
const root = this.root;
|
|
968
1181
|
return await this._readStorage(this._contentKey(aid), root);
|
|
969
1182
|
}
|
|
1183
|
+
async ownType() {
|
|
1184
|
+
try {
|
|
1185
|
+
const target = this._ownerAid();
|
|
1186
|
+
const current = this._currentAid();
|
|
1187
|
+
if (!target || !current?.isCertValid() || current.aid.toLowerCase() !== target.toLowerCase())
|
|
1188
|
+
return '';
|
|
1189
|
+
const record = await this.loadRecord(target);
|
|
1190
|
+
const content = String(record?.local_draft_content ?? '') || String(record?.content ?? '') || await this.readContent(target) || '';
|
|
1191
|
+
if (!content)
|
|
1192
|
+
return '';
|
|
1193
|
+
const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false, requireSignature: true });
|
|
1194
|
+
const type = parseFileFrontmatter(parsed.payload, false, false).type;
|
|
1195
|
+
if (typeof type !== 'string' || !type.trim())
|
|
1196
|
+
return '';
|
|
1197
|
+
const verified = await current.verifyAgentMd(content, async (signer, fingerprint) => {
|
|
1198
|
+
if (signer.toLowerCase() === current.aid.toLowerCase() && await certMatchesFingerprint(current.certPem, fingerprint))
|
|
1199
|
+
return current;
|
|
1200
|
+
throw new StateError('自身 agent.md 签名证书不在本地可信身份中');
|
|
1201
|
+
});
|
|
1202
|
+
return verified.ok && verified.data.status === 'verified' ? type.trim() : '';
|
|
1203
|
+
}
|
|
1204
|
+
catch {
|
|
1205
|
+
return '';
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
970
1208
|
async writeContent(aid, content) {
|
|
971
1209
|
const root = this.root;
|
|
972
1210
|
await this._writeStorage(this._contentKey(aid), String(content ?? ''), root);
|
|
973
1211
|
}
|
|
974
|
-
async loadRecord(aid) {
|
|
1212
|
+
async loadRecord(aid, strict = false) {
|
|
975
1213
|
const target = String(aid ?? '').trim();
|
|
976
1214
|
if (!target)
|
|
977
1215
|
return null;
|
|
@@ -979,7 +1217,7 @@ export class AgentMdManager {
|
|
|
979
1217
|
const generation = this._storageGeneration;
|
|
980
1218
|
try {
|
|
981
1219
|
const loaded = await this._withLock(async () => {
|
|
982
|
-
const record = await this._readRecordUnlocked(target, root);
|
|
1220
|
+
const record = await this._readRecordUnlocked(target, root, strict);
|
|
983
1221
|
const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
984
1222
|
try {
|
|
985
1223
|
const content = await this._readStorage(this._contentKey(target), root);
|
|
@@ -1005,6 +1243,8 @@ export class AgentMdManager {
|
|
|
1005
1243
|
return { ...loaded };
|
|
1006
1244
|
}
|
|
1007
1245
|
catch (err) {
|
|
1246
|
+
if (strict)
|
|
1247
|
+
throw err;
|
|
1008
1248
|
this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1009
1249
|
return null;
|
|
1010
1250
|
}
|
|
@@ -1015,10 +1255,12 @@ export class AgentMdManager {
|
|
|
1015
1255
|
return {};
|
|
1016
1256
|
const root = this.root;
|
|
1017
1257
|
const generation = this._storageGeneration;
|
|
1258
|
+
let strictDraft = false;
|
|
1018
1259
|
try {
|
|
1019
1260
|
const inputFields = { ...fields };
|
|
1261
|
+
strictDraft = typeof inputFields.local_draft_content === 'string';
|
|
1020
1262
|
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
1021
|
-
if (hasContent) {
|
|
1263
|
+
if (hasContent && !strictDraft) {
|
|
1022
1264
|
const text = String(inputFields.content ?? '');
|
|
1023
1265
|
await this._writeStorage(this._contentKey(target), text, root);
|
|
1024
1266
|
if (!inputFields.local_etag)
|
|
@@ -1028,8 +1270,18 @@ export class AgentMdManager {
|
|
|
1028
1270
|
}
|
|
1029
1271
|
delete inputFields.content;
|
|
1030
1272
|
const record = await this._withLock(async () => {
|
|
1031
|
-
const next = { ...(await this._readRecordUnlocked(target, root)), aid: target };
|
|
1273
|
+
const next = { ...(await this._readRecordUnlocked(target, root, strictDraft)), aid: target };
|
|
1274
|
+
if (hasContent && strictDraft) {
|
|
1275
|
+
const text = String(fields.content ?? '');
|
|
1276
|
+
await this._writeStorage(this._contentKey(target), text, root);
|
|
1277
|
+
if (!inputFields.local_etag)
|
|
1278
|
+
inputFields.local_etag = await AgentMdManager.contentEtag(text);
|
|
1279
|
+
if (!inputFields.fetched_at)
|
|
1280
|
+
inputFields.fetched_at = Date.now();
|
|
1281
|
+
}
|
|
1032
1282
|
for (const [key, value] of Object.entries(inputFields)) {
|
|
1283
|
+
if (key === 'local_draft_content' && value === '' && hasContent && next.local_draft_content !== fields.content)
|
|
1284
|
+
continue;
|
|
1033
1285
|
if (value !== undefined && value !== null)
|
|
1034
1286
|
next[key] = value;
|
|
1035
1287
|
}
|
|
@@ -1045,6 +1297,8 @@ export class AgentMdManager {
|
|
|
1045
1297
|
return { ...loaded };
|
|
1046
1298
|
}
|
|
1047
1299
|
catch (err) {
|
|
1300
|
+
if (strictDraft)
|
|
1301
|
+
throw err;
|
|
1048
1302
|
this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1049
1303
|
return {};
|
|
1050
1304
|
}
|
|
@@ -1105,7 +1359,8 @@ export class AgentMdManager {
|
|
|
1105
1359
|
return String(await this._gatewayResolver?.(aid) ?? '');
|
|
1106
1360
|
}
|
|
1107
1361
|
async _resolvePeer(aid, certFingerprint, signedAt) {
|
|
1108
|
-
const
|
|
1362
|
+
const normalizedFp = normalizeFingerprintHex(certFingerprint);
|
|
1363
|
+
const expectedFp = normalizedFp ? `sha256:${normalizedFp}` : String(certFingerprint ?? '').trim().toLowerCase();
|
|
1109
1364
|
const current = this._currentAid();
|
|
1110
1365
|
if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
1111
1366
|
if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
|
|
@@ -1115,6 +1370,8 @@ export class AgentMdManager {
|
|
|
1115
1370
|
if (!(peer instanceof AID)) {
|
|
1116
1371
|
throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
|
|
1117
1372
|
}
|
|
1373
|
+
if (peer.aid.toLowerCase() !== aid.toLowerCase())
|
|
1374
|
+
throw new ClientSignatureError(`agent.md signer does not match requested AID: ${aid}`, { stringCode: 'invalid' });
|
|
1118
1375
|
return peer;
|
|
1119
1376
|
}
|
|
1120
1377
|
async _accessToken(aid, gatewayUrl) {
|
|
@@ -1130,52 +1387,21 @@ export class AgentMdManager {
|
|
|
1130
1387
|
throw new StateError('authenticate did not return access_token');
|
|
1131
1388
|
return token;
|
|
1132
1389
|
}
|
|
1133
|
-
async
|
|
1134
|
-
const invalid = new StateError('pending entity original V2 authorization is missing or invalid', { stringCode: 'persistence_unavailable' });
|
|
1135
|
-
try {
|
|
1136
|
-
if (typeof authorization !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(authorization) || authorization.includes('=') && authorization.length % 4 !== 0)
|
|
1137
|
-
throw invalid;
|
|
1138
|
-
const binary = globalThis.atob(authorization.replace(/-/g, '+').replace(/_/g, '/'));
|
|
1139
|
-
const text = new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(binary, char => char.charCodeAt(0)));
|
|
1140
|
-
if (base64UrlEncode(text) !== authorization.replace(/=+$/, ''))
|
|
1141
|
-
throw invalid;
|
|
1142
|
-
const value = JSON.parse(text);
|
|
1143
|
-
const lexical = parseDocument(text, { uniqueKeys: true });
|
|
1144
|
-
if (lexical.errors.length || lexical.get('version', true)?.source !== '2' || !/^[1-9][0-9]*$/.test(lexical.get('timestamp', true)?.source ?? ''))
|
|
1145
|
-
throw invalid;
|
|
1146
|
-
const fields = ['version', 'target_aid', 'uploader_aid', 'body_sha256', 'entity_cert_fingerprint', 'timestamp', 'nonce', 'cert_fingerprint', 'signature'];
|
|
1147
|
-
if (!isJsonObject(value) || Object.keys(value).length !== fields.length || fields.some(field => !(field in value)) || value.version !== 2)
|
|
1148
|
-
throw invalid;
|
|
1149
|
-
const timestamp = value.timestamp, nonce = value.nonce;
|
|
1150
|
-
if (typeof timestamp !== 'number' || !Number.isSafeInteger(timestamp) || timestamp <= 0 || typeof nonce !== 'string' || !nonce.trim() || new TextEncoder().encode(nonce).length > 128)
|
|
1151
|
-
throw invalid;
|
|
1152
|
-
if (value.target_aid !== target.toLowerCase() || value.uploader_aid !== owner.aid.toLowerCase() || value.body_sha256 !== await sha256Hex(content) || value.entity_cert_fingerprint !== entityFingerprint || value.cert_fingerprint !== owner.certFingerprint || typeof value.signature !== 'string')
|
|
1153
|
-
throw invalid;
|
|
1154
|
-
const verified = await owner.verify(`AUN-AGENTMD-OWNER-AUTH-V2;${value.uploader_aid};${value.target_aid};${value.body_sha256};${entityFingerprint};${timestamp};${nonce}`, value.signature);
|
|
1155
|
-
if (!verified.ok || !verified.data?.valid)
|
|
1156
|
-
throw invalid;
|
|
1157
|
-
return timestamp;
|
|
1158
|
-
}
|
|
1159
|
-
catch {
|
|
1160
|
-
throw invalid;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
async _ownerUploadAuthorization(target, content, owner, entityFingerprint = '') {
|
|
1390
|
+
async _ownerUploadAuthorization(target, content, owner) {
|
|
1164
1391
|
const bodySha256 = await sha256Hex(content);
|
|
1165
1392
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
1166
1393
|
const nonce = randomOwnerAuthorizationNonce();
|
|
1167
|
-
const canonical =
|
|
1394
|
+
const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
|
|
1168
1395
|
const signed = await owner.sign(canonical);
|
|
1169
1396
|
if (!signed.ok || !signed.data) {
|
|
1170
1397
|
const message = signed.error?.message || 'owner authorization signing failed';
|
|
1171
1398
|
throw new ClientSignatureError(message);
|
|
1172
1399
|
}
|
|
1173
1400
|
return base64UrlEncode(JSON.stringify({
|
|
1174
|
-
version:
|
|
1401
|
+
version: 1,
|
|
1175
1402
|
target_aid: target.toLowerCase(),
|
|
1176
1403
|
uploader_aid: owner.aid.toLowerCase(),
|
|
1177
1404
|
body_sha256: bodySha256,
|
|
1178
|
-
...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
|
|
1179
1405
|
timestamp,
|
|
1180
1406
|
nonce,
|
|
1181
1407
|
cert_fingerprint: owner.certFingerprint,
|
|
@@ -1190,15 +1416,15 @@ export class AgentMdManager {
|
|
|
1190
1416
|
throw new StateError('agent.md upload requires its original document condition', { stringCode: 'persistence_unavailable' });
|
|
1191
1417
|
const gatewayUrl = await this._resolveGateway(aid);
|
|
1192
1418
|
const authorization = String(ownerAuthorization ?? '').trim();
|
|
1193
|
-
const
|
|
1419
|
+
const meta = validateAgentMdDocument(content, { expectedAid: aid, validateOwnership: false }).frontmatter;
|
|
1420
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
1194
1421
|
// 浏览器禁止覆盖 Host;动态 AID URL 由浏览器/Docker 的通配解析路由。
|
|
1195
1422
|
const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
|
|
1196
|
-
const headers = {
|
|
1423
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1',
|
|
1197
1424
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
1198
|
-
...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
1199
1425
|
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
|
|
1200
1426
|
...(viaGatewayHost ? { Host: this._safeAid(aid) } : {}),
|
|
1201
|
-
...(authorization ? {
|
|
1427
|
+
...(!entity && authorization ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
|
|
1202
1428
|
};
|
|
1203
1429
|
if (!authorization || entity) {
|
|
1204
1430
|
const token = await this._uploadAccessToken(aid, gatewayUrl);
|
|
@@ -1223,7 +1449,7 @@ export class AgentMdManager {
|
|
|
1223
1449
|
}
|
|
1224
1450
|
let response;
|
|
1225
1451
|
try {
|
|
1226
|
-
response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content });
|
|
1452
|
+
response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
1227
1453
|
}
|
|
1228
1454
|
catch (error) {
|
|
1229
1455
|
if (!viaGatewayHost)
|
|
@@ -1231,12 +1457,12 @@ export class AgentMdManager {
|
|
|
1231
1457
|
const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
|
|
1232
1458
|
if (!fallbackUrl || fallbackUrl === url)
|
|
1233
1459
|
throw error;
|
|
1234
|
-
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
|
|
1460
|
+
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
1235
1461
|
}
|
|
1236
1462
|
if (response.status === 404 && viaGatewayHost) {
|
|
1237
1463
|
const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
|
|
1238
1464
|
if (fallbackUrl && fallbackUrl !== url) {
|
|
1239
|
-
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
|
|
1465
|
+
response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
|
|
1240
1466
|
}
|
|
1241
1467
|
}
|
|
1242
1468
|
if (response.status === 404) {
|
|
@@ -1260,13 +1486,13 @@ export class AgentMdManager {
|
|
|
1260
1486
|
const cached = await this.loadRecord(target) ?? {};
|
|
1261
1487
|
const gatewayUrl = await this._resolveGateway(target);
|
|
1262
1488
|
const url = this._url(target, gatewayUrl);
|
|
1263
|
-
const headers = { Accept: 'text/markdown' };
|
|
1489
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
|
|
1264
1490
|
const cachedEtag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
|
|
1265
1491
|
const cachedLastModified = String(cached.last_modified ?? '').trim();
|
|
1266
1492
|
let response = await fetchWithTimeout(url, {
|
|
1267
1493
|
method: 'GET',
|
|
1268
1494
|
headers,
|
|
1269
|
-
redirect: '
|
|
1495
|
+
redirect: 'error',
|
|
1270
1496
|
}, timeoutMs);
|
|
1271
1497
|
if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
|
|
1272
1498
|
return {
|
|
@@ -1279,8 +1505,8 @@ export class AgentMdManager {
|
|
|
1279
1505
|
if (response.status === 304) {
|
|
1280
1506
|
response = await fetchWithTimeout(url, {
|
|
1281
1507
|
method: 'GET',
|
|
1282
|
-
headers: { Accept: 'text/markdown' },
|
|
1283
|
-
redirect: '
|
|
1508
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
1509
|
+
redirect: 'error',
|
|
1284
1510
|
}, timeoutMs);
|
|
1285
1511
|
}
|
|
1286
1512
|
if (response.status === 404) {
|
|
@@ -1310,7 +1536,7 @@ export class AgentMdManager {
|
|
|
1310
1536
|
const gatewayUrl = await this._resolveGateway(target);
|
|
1311
1537
|
const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
|
|
1312
1538
|
method: 'HEAD',
|
|
1313
|
-
headers: { Accept: 'text/markdown' },
|
|
1539
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
1314
1540
|
}, HEAD_HTTP_TIMEOUT_MS);
|
|
1315
1541
|
const etag = headerValue(response.headers, 'ETag');
|
|
1316
1542
|
const lastModified = headerValue(response.headers, 'Last-Modified');
|
|
@@ -1405,14 +1631,22 @@ export class AgentMdManager {
|
|
|
1405
1631
|
payload.aid = this._safeAid(aid);
|
|
1406
1632
|
await this._writeStorage(this._metaKey(aid), `${JSON.stringify(payload, null, 2)}\n`, root);
|
|
1407
1633
|
}
|
|
1408
|
-
async _readRecordUnlocked(aid, root = this.root) {
|
|
1409
|
-
const raw = await this._readStorage(this._metaKey(aid), root);
|
|
1410
|
-
if (raw === null)
|
|
1411
|
-
return {};
|
|
1634
|
+
async _readRecordUnlocked(aid, root = this.root, strict = false) {
|
|
1412
1635
|
try {
|
|
1413
|
-
|
|
1636
|
+
const raw = await this._readStorage(this._metaKey(aid), root);
|
|
1637
|
+
if (raw === null)
|
|
1638
|
+
return {};
|
|
1639
|
+
const parsed = JSON.parse(raw);
|
|
1640
|
+
if (strict && !isRecord(parsed))
|
|
1641
|
+
throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
|
|
1642
|
+
return this._normalizeRecord(aid, parsed);
|
|
1414
1643
|
}
|
|
1415
1644
|
catch (err) {
|
|
1645
|
+
if (strict) {
|
|
1646
|
+
if (err instanceof StateError)
|
|
1647
|
+
throw err;
|
|
1648
|
+
throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
|
|
1649
|
+
}
|
|
1416
1650
|
this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1417
1651
|
return {};
|
|
1418
1652
|
}
|