@agentunion/fastaun 0.5.19 → 0.5.20
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 +129 -78
- package/_packed_docs/CHANGELOG.md +129 -78
- package/_packed_docs/INDEX.md +201 -195
- package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- 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 +1072 -1072
- 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 +443 -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 +658 -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/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
- package/_packed_docs/sdk/INDEX.md +92 -80
- 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 +922 -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 +300 -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 +382 -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 +476 -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 +13 -1
- package/dist/agent-md-schema.js +61 -33
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +8 -1
- package/dist/agent-md.js +243 -119
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +29 -0
- package/dist/aid-store.js +179 -34
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +18 -13
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +74 -2
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +43 -1
- package/dist/cert-utils.js +144 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +9 -0
- package/dist/client/delivery.js +179 -11
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.js +125 -82
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.js +95 -89
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.js +48 -19
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +5 -3
- package/dist/events.js +25 -8
- package/dist/events.js.map +1 -1
- package/dist/facades.js +5 -5
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +1 -1
- package/dist/keystore/aid-db.js +11 -2
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +1 -1
- package/dist/keystore/local-token-store.d.ts +1 -1
- package/dist/keystore/local-token-store.js +2 -2
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.js +77 -13
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +26 -8
- package/dist/ownership-v21.js +147 -70
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +5 -0
- package/dist/ownership-workflow.js +366 -56
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +2 -1
- package/dist/tools/cross-sdk-agent.js +109 -6
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +10 -0
- 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
|
@@ -3,15 +3,29 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as http from 'node:http';
|
|
4
4
|
import * as https from 'node:https';
|
|
5
5
|
import * as path from 'node:path';
|
|
6
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
6
7
|
import { parseDocument } from 'yaml';
|
|
7
8
|
import { AID } from './aid.js';
|
|
8
|
-
import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
|
|
9
|
-
import { parseFileFrontmatter, validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
9
|
+
import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, publicKeyMatchesFingerprint, verifyDocument } from './cert-utils.js';
|
|
10
|
+
import { parseFileFrontmatter, validateAgentMdDocument, validateAgentMdReadDocument, verifyAgentMdBodyUpdate, verifyAgentMdUpload } from './agent-md-schema.js';
|
|
10
11
|
import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
|
|
11
12
|
import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError, } from './errors.js';
|
|
12
13
|
import { isJsonObject } from './types.js';
|
|
13
|
-
import { ownershipFailure, parseOwnershipFile } from './ownership-v21.js';
|
|
14
|
+
import { ownershipAid, ownershipFailure, parseOwnershipFile } from './ownership-v21.js';
|
|
15
|
+
import { resultOk } from './result.js';
|
|
14
16
|
const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
|
|
17
|
+
function relationshipFields(meta) {
|
|
18
|
+
return Object.fromEntries(['owner', 'ownership', 'ownership_proof', 'ownership_release'].filter((key) => Object.hasOwn(meta, key)).map((key) => [key, meta[key]]));
|
|
19
|
+
}
|
|
20
|
+
function legacyRelationship(meta) {
|
|
21
|
+
if (['group', 'org'].includes(String(meta.type)))
|
|
22
|
+
return undefined;
|
|
23
|
+
const statement = meta.ownership?.statement;
|
|
24
|
+
if (statement && typeof statement === 'object' && Object.hasOwn(statement, 'relation_version'))
|
|
25
|
+
return undefined;
|
|
26
|
+
const fields = relationshipFields(meta);
|
|
27
|
+
return Object.keys(fields).length ? fields : undefined;
|
|
28
|
+
}
|
|
15
29
|
const AGENT_MD_NEGATIVE_CACHE_TTL_MS = 60_000;
|
|
16
30
|
export function buildOwnershipReleaseAgentMd(content, release) {
|
|
17
31
|
return updateOwnershipAgentMd(content, { ownership_release: release });
|
|
@@ -251,12 +265,12 @@ export class AgentMdManager {
|
|
|
251
265
|
this._atomicWriteText(filePath, String(content ?? ''));
|
|
252
266
|
return filePath;
|
|
253
267
|
}
|
|
254
|
-
|
|
268
|
+
_loadRecord(aid, strict = false) {
|
|
255
269
|
const target = String(aid ?? '').trim();
|
|
256
270
|
if (!target)
|
|
257
271
|
return null;
|
|
258
272
|
try {
|
|
259
|
-
const record = this._withRecordLock(target, () => this._readRecordUnlocked(target));
|
|
273
|
+
const record = this._withRecordLock(target, () => this._readRecordUnlocked(target, strict));
|
|
260
274
|
const loaded = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
|
|
261
275
|
try {
|
|
262
276
|
const content = this.readContent(target);
|
|
@@ -270,7 +284,7 @@ export class AgentMdManager {
|
|
|
270
284
|
}
|
|
271
285
|
if (Object.keys(loaded).length <= 1)
|
|
272
286
|
return null;
|
|
273
|
-
if (Object.keys(record).length === 0 && typeof loaded.content === 'string') {
|
|
287
|
+
if (Object.keys(record).length === 0 && typeof loaded.content === 'string' && !fs.existsSync(this.metaPath(target))) {
|
|
274
288
|
this._withRecordLock(target, () => this._writeRecordUnlocked(target, {
|
|
275
289
|
aid: target,
|
|
276
290
|
local_etag: loaded.local_etag,
|
|
@@ -281,11 +295,16 @@ export class AgentMdManager {
|
|
|
281
295
|
return { ...loaded };
|
|
282
296
|
}
|
|
283
297
|
catch (err) {
|
|
298
|
+
if (strict)
|
|
299
|
+
throw err;
|
|
284
300
|
this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
285
301
|
return null;
|
|
286
302
|
}
|
|
287
303
|
}
|
|
288
|
-
|
|
304
|
+
loadRecord(aid) {
|
|
305
|
+
return this._loadRecord(aid);
|
|
306
|
+
}
|
|
307
|
+
_saveRecord(aid, fields, strict = false) {
|
|
289
308
|
const target = String(aid ?? '').trim();
|
|
290
309
|
if (!target)
|
|
291
310
|
return {};
|
|
@@ -294,6 +313,7 @@ export class AgentMdManager {
|
|
|
294
313
|
const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
|
|
295
314
|
let savedTo = '';
|
|
296
315
|
const record = this._withRecordLock(target, () => {
|
|
316
|
+
const next = { ...this._readRecordUnlocked(target, true), aid: target };
|
|
297
317
|
if (hasContent) {
|
|
298
318
|
const content = String(inputFields.content ?? '');
|
|
299
319
|
savedTo = this.writeContent(target, content);
|
|
@@ -303,7 +323,6 @@ export class AgentMdManager {
|
|
|
303
323
|
inputFields.fetched_at = Date.now();
|
|
304
324
|
}
|
|
305
325
|
delete inputFields.content;
|
|
306
|
-
const next = { ...this._readRecordUnlocked(target), aid: target };
|
|
307
326
|
for (const [key, value] of Object.entries(inputFields)) {
|
|
308
327
|
if (value !== undefined && value !== null)
|
|
309
328
|
next[key] = value;
|
|
@@ -319,7 +338,7 @@ export class AgentMdManager {
|
|
|
319
338
|
loaded.saved_to = savedTo;
|
|
320
339
|
}
|
|
321
340
|
else {
|
|
322
|
-
const current = this.
|
|
341
|
+
const current = this._loadRecord(target, strict);
|
|
323
342
|
if (current && typeof current.content === 'string')
|
|
324
343
|
loaded.content = current.content;
|
|
325
344
|
}
|
|
@@ -327,10 +346,26 @@ export class AgentMdManager {
|
|
|
327
346
|
return { ...loaded };
|
|
328
347
|
}
|
|
329
348
|
catch (err) {
|
|
349
|
+
if (strict)
|
|
350
|
+
throw err;
|
|
330
351
|
this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
|
|
331
352
|
return {};
|
|
332
353
|
}
|
|
333
354
|
}
|
|
355
|
+
saveRecord(aid, fields) {
|
|
356
|
+
return this._saveRecord(aid, fields, Object.prototype.hasOwnProperty.call(fields, 'local_draft_content'));
|
|
357
|
+
}
|
|
358
|
+
_clearConfirmedDraft(aid, content) {
|
|
359
|
+
this._withRecordLock(aid, () => {
|
|
360
|
+
const record = this._readRecordUnlocked(aid, true);
|
|
361
|
+
if (record.local_draft_content !== content)
|
|
362
|
+
return;
|
|
363
|
+
record.local_draft_content = '';
|
|
364
|
+
record.updated_at = Date.now();
|
|
365
|
+
this._writeRecordUnlocked(aid, record);
|
|
366
|
+
});
|
|
367
|
+
this._cache.delete(aid);
|
|
368
|
+
}
|
|
334
369
|
async upload(content) {
|
|
335
370
|
const target = this._ownerAid();
|
|
336
371
|
if (!target)
|
|
@@ -339,19 +374,23 @@ export class AgentMdManager {
|
|
|
339
374
|
if (!current?.isPrivateKeyValid()) {
|
|
340
375
|
throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
|
|
341
376
|
}
|
|
342
|
-
const rawContent = content === undefined || content === null ? this.readContent(target) : String(content);
|
|
377
|
+
const rawContent = content === undefined || content === null ? String(this._loadRecord(target, true)?.local_draft_content ?? '') || this.readContent(target) : String(content);
|
|
343
378
|
if (!rawContent.trim())
|
|
344
379
|
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
345
|
-
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
|
|
380
|
+
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target, validateOwnership: false });
|
|
346
381
|
if (['group', 'org'].includes(String(parsed.frontmatter.type)))
|
|
347
382
|
return await this.uploadAs(target, rawContent, current);
|
|
383
|
+
if (parseAgentMdTailSignature(rawContent).signatures.length > 1) {
|
|
384
|
+
await this._verifyDocument(rawContent, target, current, true, true);
|
|
385
|
+
return await this._uploadPrepared(target, rawContent, current);
|
|
386
|
+
}
|
|
348
387
|
const signed = current.signAgentMd(rawContent);
|
|
349
388
|
if (!signed.ok || !signed.data) {
|
|
350
389
|
const message = signed.error?.message ?? 'agent.md signing failed';
|
|
351
390
|
throw new ClientSignatureError(message);
|
|
352
391
|
}
|
|
353
392
|
const signedContent = signed.data.signed;
|
|
354
|
-
|
|
393
|
+
verifyAgentMdBodyUpdate(signedContent, current.certPem, target);
|
|
355
394
|
return await this._uploadPrepared(target, signedContent, current);
|
|
356
395
|
}
|
|
357
396
|
async readOwnershipFresh(aid) {
|
|
@@ -395,6 +434,8 @@ export class AgentMdManager {
|
|
|
395
434
|
throw ownershipFailure('unsupported_protocol', 'NS does not support the required original document condition');
|
|
396
435
|
if (response.status === 401 || response.status === 403)
|
|
397
436
|
throw ownershipFailure('forbidden', `NS self submission denied: HTTP ${response.status}`);
|
|
437
|
+
if (response.status === 400)
|
|
438
|
+
throw ownershipFailure('invalid', 'NS rejected the submitted agent.md: HTTP 400');
|
|
398
439
|
if (response.status < 200 || response.status >= 300)
|
|
399
440
|
throw ownershipFailure('unavailable', `NS raw submission failed: HTTP ${response.status}`, true);
|
|
400
441
|
const fresh = await this.readOwnershipFresh(target);
|
|
@@ -403,9 +444,9 @@ export class AgentMdManager {
|
|
|
403
444
|
this.saveRecord(target, { content: raw, local_etag: AgentMdManager.contentEtag(raw), remote_etag: fresh.etag, fetched_at: Date.now(), checked_at: Date.now(), remote_status: 'found' });
|
|
404
445
|
return fresh;
|
|
405
446
|
}
|
|
406
|
-
async _ownershipRequest(target, method, raw, condition) {
|
|
447
|
+
async _ownershipRequest(target, method, raw, condition, legacyRead = false) {
|
|
407
448
|
const gatewayUrl = await this._resolveGateway(target);
|
|
408
|
-
const headers = { Accept: 'text/markdown', 'Cache-Control': 'no-cache', 'X-AUN-Ownership-Check': '1' };
|
|
449
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', ...(!legacyRead ? { 'X-AUN-Ownership-Check': '1' } : {}) };
|
|
409
450
|
if (method === 'PUT') {
|
|
410
451
|
headers['Content-Type'] = 'text/markdown; charset=utf-8';
|
|
411
452
|
headers.Authorization = `Bearer ${await this._uploadAccessToken(target, gatewayUrl)}`;
|
|
@@ -433,31 +474,42 @@ export class AgentMdManager {
|
|
|
433
474
|
const rawContent = String(content ?? '');
|
|
434
475
|
if (!rawContent.trim())
|
|
435
476
|
throw new ValidationError('uploadAgentMd requires non-empty content');
|
|
436
|
-
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
|
|
477
|
+
const parsed = validateAgentMdDocument(rawContent, { expectedAid: target, validateOwnership: false });
|
|
437
478
|
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
|
|
438
479
|
const identity = await this._entitySigningIdentity(target, parsed.frontmatter, parsed.signature, signer);
|
|
439
|
-
|
|
480
|
+
await this._verifyDocument(rawContent, target, identity, true);
|
|
440
481
|
return await this._uploadPrepared(target, rawContent, identity);
|
|
441
482
|
}
|
|
483
|
+
if (['group', 'org'].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
|
|
484
|
+
const identity = await this._entityIdentityGetter?.(target, String(parsed.frontmatter.type));
|
|
485
|
+
if (!identity?.isPrivateKeyValid())
|
|
486
|
+
throw new StateError('entity agent.md signing requires its local private key');
|
|
487
|
+
signer = identity;
|
|
488
|
+
}
|
|
442
489
|
if (!signer?.isPrivateKeyValid())
|
|
443
490
|
throw new StateError('uploadAgentMd requires a valid signer identity');
|
|
444
|
-
if (
|
|
445
|
-
await this.
|
|
491
|
+
if (parseAgentMdTailSignature(rawContent).signatures.length > 1) {
|
|
492
|
+
await this._verifyDocument(rawContent, target, signer, true, true);
|
|
493
|
+
return await this._uploadPrepared(target, rawContent, signer);
|
|
494
|
+
}
|
|
446
495
|
const signed = signer.signAgentMd(rawContent);
|
|
447
496
|
if (!signed.ok || !signed.data) {
|
|
448
497
|
const message = signed.error?.message ?? 'agent.md signing failed';
|
|
449
498
|
throw new ClientSignatureError(message);
|
|
450
499
|
}
|
|
451
500
|
const signedContent = signed.data.signed;
|
|
452
|
-
|
|
501
|
+
verifyAgentMdBodyUpdate(signedContent, signer.certPem, target);
|
|
453
502
|
return await this._uploadPrepared(target, signedContent, signer);
|
|
454
503
|
}
|
|
455
|
-
|
|
456
|
-
const record = this.
|
|
504
|
+
_pendingUpload(aid, strict = false) {
|
|
505
|
+
const record = this._loadRecord(aid, strict);
|
|
457
506
|
if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
|
|
458
507
|
return null;
|
|
459
508
|
return { content: String(record.pending_content), content_etag: String(record.pending_content_etag ?? ''), last_error: String(record.pending_error ?? ''), condition: record.pending_condition, relation: record.pending_relation, authorization: record.pending_authorization };
|
|
460
509
|
}
|
|
510
|
+
pendingUpload(aid) {
|
|
511
|
+
return this._pendingUpload(aid, true);
|
|
512
|
+
}
|
|
461
513
|
async resumePendingUpload(aid, signer) {
|
|
462
514
|
const pending = this.pendingUpload(aid);
|
|
463
515
|
if (!pending)
|
|
@@ -467,9 +519,11 @@ export class AgentMdManager {
|
|
|
467
519
|
throw ownershipFailure('persistence_unavailable', 'pending agent.md requires its original document condition');
|
|
468
520
|
const parsed = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
|
|
469
521
|
const meta = parsed.frontmatter;
|
|
470
|
-
if ('ownership_proof' in meta || 'ownership_release' in meta)
|
|
471
|
-
throw ownershipFailure('unsupported_protocol', 'legacy ownership pending agent.md cannot be recovered through ordinary upload');
|
|
472
522
|
try {
|
|
523
|
+
if (pending.relation?.legacy === true)
|
|
524
|
+
return await this._resumeLegacyBodyUpload(aid, pending, signer ?? this._currentAid());
|
|
525
|
+
if (legacyRelationship(meta))
|
|
526
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership pending agent.md requires its original relationship snapshot');
|
|
473
527
|
if (['group', 'org'].includes(String(meta.type))) {
|
|
474
528
|
if (!parsed.signature)
|
|
475
529
|
throw ownershipFailure('invalid', 'pending entity agent.md requires a signature');
|
|
@@ -483,13 +537,13 @@ export class AgentMdManager {
|
|
|
483
537
|
throw ownershipFailure('conflict', 'pending entity upload belongs to an old rekey generation');
|
|
484
538
|
if (pending.content_etag !== AgentMdManager.contentEtag(pending.content))
|
|
485
539
|
throw ownershipFailure('persistence_unavailable', 'pending entity original bytes are corrupt');
|
|
486
|
-
|
|
487
|
-
const uploader = this._currentAid();
|
|
488
|
-
if (uploader.aid.toLowerCase() !== aid.toLowerCase())
|
|
489
|
-
this._validateEntityUploadAuthorization(pending.authorization, aid, pending.content, uploader, String(context.entity_cert_fingerprint));
|
|
540
|
+
await this._verifyDocument(pending.content, aid, identity, true);
|
|
490
541
|
const remote = await this._readEntityFresh(aid);
|
|
491
542
|
if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
492
|
-
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: '' });
|
|
543
|
+
const saved = 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: '' });
|
|
544
|
+
if (!saved.aid)
|
|
545
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
546
|
+
this._clearConfirmedDraft(aid, pending.content);
|
|
493
547
|
return true;
|
|
494
548
|
}
|
|
495
549
|
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
@@ -497,6 +551,13 @@ export class AgentMdManager {
|
|
|
497
551
|
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
498
552
|
return true;
|
|
499
553
|
}
|
|
554
|
+
const multipleSignatures = parseAgentMdTailSignature(pending.content).signatures.length > 1;
|
|
555
|
+
const identity = signer ?? this._currentAid();
|
|
556
|
+
if (multipleSignatures) {
|
|
557
|
+
if (!identity)
|
|
558
|
+
throw new StateError('pending agent.md requires a signer identity');
|
|
559
|
+
await this._verifyDocument(pending.content, aid, identity, true);
|
|
560
|
+
}
|
|
500
561
|
const remote = await this.readOwnershipFresh(aid);
|
|
501
562
|
const fail = (code, reason, retryable = false) => ownershipFailure(code, reason, retryable);
|
|
502
563
|
const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
|
|
@@ -521,14 +582,15 @@ export class AgentMdManager {
|
|
|
521
582
|
const saved = 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: '' });
|
|
522
583
|
if (!saved.aid)
|
|
523
584
|
throw new StateError('agent.md pending completion persistence failed');
|
|
585
|
+
this._clearConfirmedDraft(aid, pending.content);
|
|
524
586
|
return true;
|
|
525
587
|
}
|
|
526
588
|
if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
|
|
527
589
|
throw ownershipFailure('conflict', 'pending agent.md original document condition changed');
|
|
528
|
-
const identity = signer ?? this._currentAid();
|
|
529
590
|
if (!identity)
|
|
530
591
|
throw new StateError('pending agent.md requires a signer identity');
|
|
531
|
-
|
|
592
|
+
if (!multipleSignatures)
|
|
593
|
+
verifyAgentMdUpload(pending.content, identity.certPem, aid);
|
|
532
594
|
await this._uploadPrepared(aid, pending.content, identity, condition);
|
|
533
595
|
return true;
|
|
534
596
|
}
|
|
@@ -538,32 +600,45 @@ export class AgentMdManager {
|
|
|
538
600
|
}
|
|
539
601
|
}
|
|
540
602
|
async _uploadPrepared(target, signedContent, signer, originalCondition) {
|
|
541
|
-
const meta = validateAgentMdDocument(signedContent, { expectedAid: target }).frontmatter;
|
|
603
|
+
const meta = validateAgentMdDocument(signedContent, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
542
604
|
const entity = ['group', 'org'].includes(String(meta.type));
|
|
605
|
+
const legacy = legacyRelationship(meta);
|
|
606
|
+
const draftStrict = !originalCondition;
|
|
607
|
+
if (legacy && (signer.aid !== target || this._currentAid()?.aid !== target || this._ownerAid() !== target))
|
|
608
|
+
throw ownershipFailure('forbidden', 'legacy relationship body updates require current subject identity');
|
|
609
|
+
if (legacy && Object.hasOwn(meta, 'owner'))
|
|
610
|
+
validateOwnershipFrontmatter({ aid: meta.aid, type: meta.type, owner: meta.owner }, target);
|
|
611
|
+
if (!entity && !legacy)
|
|
612
|
+
parseOwnershipFile(signedContent, 'agent', { expectedSubAid: target });
|
|
613
|
+
if (draftStrict && this._saveRecord(target, { local_draft_content: signedContent }, true).local_draft_content !== signedContent)
|
|
614
|
+
throw new StateError('agent.md local draft persistence failed');
|
|
543
615
|
const entityContext = entity ? await this._entityContext(target, meta, signer) : undefined;
|
|
544
616
|
let condition = originalCondition;
|
|
545
|
-
let relation = entityContext ?? (this.
|
|
617
|
+
let relation = entityContext ?? (this._pendingUpload(target, draftStrict))?.relation;
|
|
546
618
|
if (!condition) {
|
|
547
|
-
const fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
|
|
619
|
+
const fresh = entity ? await this._readEntityFresh(target) : legacy ? await this._readLegacyBodyBase(target) : await this.readOwnershipFresh(target);
|
|
548
620
|
condition = fresh.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
|
|
549
621
|
if (!entity) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
622
|
+
if (legacy) {
|
|
623
|
+
if (!fresh.exists || !fresh.etag || !isDeepStrictEqual(legacy, legacyRelationship(validateAgentMdReadDocument(fresh.content, { expectedAid: target }).frontmatter)))
|
|
624
|
+
throw ownershipFailure('unsupported_protocol', 'legacy relationship material must remain unchanged');
|
|
625
|
+
relation = { legacy: true, base_content: fresh.content, base_content_etag: AgentMdManager.contentEtag(fresh.content), base_etag: fresh.etag, fields: legacy };
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
if (fresh.exists && legacyRelationship(validateAgentMdReadDocument(fresh.content, { expectedAid: target }).frontmatter))
|
|
629
|
+
throw ownershipFailure('unsupported_protocol', 'legacy relationship material cannot be removed or replaced');
|
|
630
|
+
const authority = fresh;
|
|
631
|
+
const candidate = parseOwnershipFile(signedContent, 'agent', { expectedSubAid: target });
|
|
632
|
+
const statement = fresh.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
|
|
633
|
+
relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
|
|
634
|
+
}
|
|
554
635
|
}
|
|
555
636
|
}
|
|
556
637
|
const uploader = this._currentAid();
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
if (entityOwnerUpload) {
|
|
560
|
-
const timestamp = this._validateEntityUploadAuthorization(entityAuthorization, target, signedContent, uploader, String(entityContext.entity_cert_fingerprint));
|
|
561
|
-
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300)
|
|
562
|
-
throw ownershipFailure('conflict', 'pending entity V2 authorization expired; explicit upload is required');
|
|
563
|
-
}
|
|
564
|
-
const pending = this.saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signedContent, pending_content_etag: AgentMdManager.contentEtag(signedContent), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization });
|
|
638
|
+
const entityAuthorization = '';
|
|
639
|
+
const pending = this._saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signedContent, pending_content_etag: AgentMdManager.contentEtag(signedContent), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization }, draftStrict);
|
|
565
640
|
const savedCondition = pending.pending_condition;
|
|
566
|
-
if (pending.pending_content !== signedContent || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
|
|
641
|
+
if (pending.pending_content !== signedContent || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization || legacy && !isDeepStrictEqual(pending.pending_relation, relation))
|
|
567
642
|
throw new StateError('agent.md pending upload persistence failed');
|
|
568
643
|
try {
|
|
569
644
|
const owner = this._currentAid();
|
|
@@ -573,24 +648,77 @@ export class AgentMdManager {
|
|
|
573
648
|
const authorization = entity ? entityAuthorization : delegated ? this._ownerUploadAuthorization(target, signedContent, owner) : '';
|
|
574
649
|
const result = await this._uploadHttp(target, signedContent, authorization, condition);
|
|
575
650
|
const remoteEtag = String(result.etag ?? '').trim();
|
|
576
|
-
const saved = this.
|
|
651
|
+
const saved = this._saveRecord(target, {
|
|
577
652
|
content: signedContent, local_etag: AgentMdManager.contentEtag(signedContent), remote_etag: remoteEtag || undefined,
|
|
578
653
|
last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
|
|
579
654
|
remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
|
|
580
655
|
pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
|
|
581
|
-
});
|
|
656
|
+
}, draftStrict);
|
|
582
657
|
if (!saved.aid)
|
|
583
658
|
throw new StateError('agent.md upload completion persistence failed');
|
|
659
|
+
if (remoteEtag === AgentMdManager.contentEtag(signedContent))
|
|
660
|
+
this._clearConfirmedDraft(target, signedContent);
|
|
584
661
|
return { ...result };
|
|
585
662
|
}
|
|
586
663
|
catch (error) {
|
|
587
|
-
this.
|
|
664
|
+
this._saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) }, draftStrict);
|
|
588
665
|
throw error;
|
|
589
666
|
}
|
|
590
667
|
}
|
|
668
|
+
async _verifyLegacyBodyContent(target, content) {
|
|
669
|
+
const parsed = validateAgentMdReadDocument(content, { expectedAid: target, requireSignature: true });
|
|
670
|
+
if (!legacyRelationship(parsed.frontmatter))
|
|
671
|
+
throw ownershipFailure('unsupported_protocol', 'stored agent.md does not contain a legacy relationship');
|
|
672
|
+
const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
|
|
673
|
+
const verified = await this._verifyDocument(content, target, peer);
|
|
674
|
+
if (verified.status !== 'verified')
|
|
675
|
+
throw ownershipFailure('invalid', 'stored legacy agent.md signature is invalid');
|
|
676
|
+
return parsed;
|
|
677
|
+
}
|
|
678
|
+
async _readLegacyBodyBase(target) {
|
|
679
|
+
const response = await this._ownershipRequest(target, 'GET', undefined, undefined, true);
|
|
680
|
+
if (response.status === 404)
|
|
681
|
+
return { exists: false, content: '', etag: '' };
|
|
682
|
+
if (response.status !== 200)
|
|
683
|
+
throw ownershipFailure('unavailable', `legacy agent.md fresh read failed: HTTP ${response.status}`, true);
|
|
684
|
+
await this._verifyLegacyBodyContent(target, response.text);
|
|
685
|
+
const etag = headerValue(response.headers, 'ETag');
|
|
686
|
+
if (!etag)
|
|
687
|
+
throw ownershipFailure('unavailable', 'legacy agent.md original ETag is missing', true);
|
|
688
|
+
return { exists: true, content: response.text, etag };
|
|
689
|
+
}
|
|
690
|
+
async _resumeLegacyBodyUpload(target, pending, identity) {
|
|
691
|
+
const relation = pending.relation;
|
|
692
|
+
if (!identity || identity.aid !== target || this._currentAid()?.aid !== target || this._ownerAid() !== target)
|
|
693
|
+
throw ownershipFailure('forbidden', 'legacy relationship recovery requires current subject identity');
|
|
694
|
+
if (typeof relation.base_content !== 'string' || relation.base_content_etag !== AgentMdManager.contentEtag(relation.base_content) || typeof relation.base_etag !== 'string' || !relation.base_etag || pending.condition?.if_match !== relation.base_etag || pending.content_etag !== AgentMdManager.contentEtag(pending.content))
|
|
695
|
+
throw ownershipFailure('persistence_unavailable', 'pending legacy relationship original bytes or condition are missing');
|
|
696
|
+
if (parseAgentMdTailSignature(pending.content).signatures.length > 1)
|
|
697
|
+
await this._verifyDocument(pending.content, target, identity, true, true);
|
|
698
|
+
else
|
|
699
|
+
verifyAgentMdBodyUpdate(pending.content, identity.certPem, target);
|
|
700
|
+
const base = await this._verifyLegacyBodyContent(target, relation.base_content);
|
|
701
|
+
const candidate = validateAgentMdDocument(pending.content, { expectedAid: target, validateOwnership: false });
|
|
702
|
+
if (!isDeepStrictEqual(legacyRelationship(base.frontmatter), relation.fields) || !isDeepStrictEqual(relationshipFields(candidate.frontmatter), relation.fields))
|
|
703
|
+
throw ownershipFailure('persistence_unavailable', 'pending legacy relationship material changed');
|
|
704
|
+
const remote = await this._readLegacyBodyBase(target);
|
|
705
|
+
if (!remote.exists)
|
|
706
|
+
throw ownershipFailure('conflict', 'pending legacy agent.md original document is missing');
|
|
707
|
+
if (remote.content === pending.content && remote.etag === pending.content_etag) {
|
|
708
|
+
const saved = this.saveRecord(target, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
|
|
709
|
+
if (!saved.aid)
|
|
710
|
+
throw new StateError('agent.md pending completion persistence failed');
|
|
711
|
+
this._clearConfirmedDraft(target, pending.content);
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
if (remote.content !== relation.base_content || remote.etag !== relation.base_etag)
|
|
715
|
+
throw ownershipFailure('conflict', 'pending legacy agent.md original document changed');
|
|
716
|
+
await this._uploadPrepared(target, pending.content, identity, pending.condition);
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
591
719
|
async _readEntityFresh(target) {
|
|
592
720
|
const gateway = await this._resolveGateway(target);
|
|
593
|
-
const headers = { 'Cache-Control': 'no-cache', ...(this._uploadViaGatewayHost ? { Host: target } : {}) };
|
|
721
|
+
const headers = { 'X-AUN-Ownership-Check': '1', 'X-AUN-Signature-Format': 'multi-v1', 'Cache-Control': 'no-cache', ...(this._uploadViaGatewayHost ? { Host: target } : {}) };
|
|
594
722
|
const request = (url) => requestText(url, { method: 'GET', verifySsl: this._verifySsl, headers, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true });
|
|
595
723
|
const response = await request(this._url(target, gateway, this._uploadViaGatewayHost));
|
|
596
724
|
if (response.status !== 200 && response.status !== 404)
|
|
@@ -604,19 +732,23 @@ export class AgentMdManager {
|
|
|
604
732
|
}
|
|
605
733
|
async readEntityAuthorityFresh(target) {
|
|
606
734
|
const observed = await this._readEntityFresh(this.safeAid(target));
|
|
607
|
-
|
|
735
|
+
try {
|
|
736
|
+
ownershipAid(observed.owner_aid, 'owner_aid');
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
throw ownershipFailure('unavailable', 'entity owner authority metadata is unavailable', true);
|
|
740
|
+
}
|
|
741
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
|
|
608
742
|
throw ownershipFailure('unavailable', 'entity owner authority metadata is unavailable', true);
|
|
609
743
|
return { owner_aid: observed.owner_aid, entity_cert_fingerprint: observed.entity_cert_fingerprint };
|
|
610
744
|
}
|
|
611
745
|
async _entityContext(target, meta, signer) {
|
|
612
746
|
const owner = String(meta.owner ?? '').trim().toLowerCase();
|
|
613
747
|
const uploader = this._currentAid();
|
|
614
|
-
if (
|
|
615
|
-
throw ownershipFailure('forbidden', 'group/org agent.md requires
|
|
748
|
+
if (signer.aid.toLowerCase() !== target.toLowerCase() || !uploader || uploader.aid.toLowerCase() !== owner || uploader.aid.toLowerCase() !== this._ownerAid().toLowerCase())
|
|
749
|
+
throw ownershipFailure('forbidden', 'group/org agent.md requires current owner identity');
|
|
616
750
|
const context = await this._entityCurrentContext(target, meta, true);
|
|
617
|
-
if (signer.
|
|
618
|
-
await this._verifyEntityOwnerSigner(signer);
|
|
619
|
-
else if (signer.certFingerprint !== context.entity_cert_fingerprint)
|
|
751
|
+
if (signer.certFingerprint !== context.entity_cert_fingerprint)
|
|
620
752
|
throw ownershipFailure('conflict', 'entity signing certificate changed');
|
|
621
753
|
if (uploader.aid.toLowerCase() === target.toLowerCase()) {
|
|
622
754
|
if (uploader.certFingerprint !== context.entity_cert_fingerprint)
|
|
@@ -636,6 +768,24 @@ export class AgentMdManager {
|
|
|
636
768
|
throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
|
|
637
769
|
return await this._entitySignerResolver(signer, signature.cert_fingerprint);
|
|
638
770
|
}
|
|
771
|
+
async _verifyDocument(content, target, identity, current = false, bodyUpdate = false) {
|
|
772
|
+
const certificates = new Map();
|
|
773
|
+
const parsed = parseAgentMdTailSignature(content, !current);
|
|
774
|
+
const meta = validateAgentMdReadDocument(content).frontmatter;
|
|
775
|
+
const readCompatibility = !current && (!['group', 'org'].includes(String(meta.type)) || meta.type === 'group' && !Object.hasOwn(meta, 'entity_cert_fingerprint') && parsed.signatures.length === 1 && (parsed.fields?.signer_aid === undefined || parsed.fields.signer_aid === meta.aid));
|
|
776
|
+
for (const item of parsed.signatures.slice(1)) {
|
|
777
|
+
const fields = item.fields;
|
|
778
|
+
const peer = current && this._entitySignerResolver ? await this._entitySignerResolver(fields.signer_aid, fields.cert_fingerprint) : await this._resolvePeer(fields.signer_aid, fields.cert_fingerprint, Number(fields.timestamp));
|
|
779
|
+
certificates.set(`${fields.signer_aid};${fields.cert_fingerprint}`, peer.certPem);
|
|
780
|
+
}
|
|
781
|
+
const resolver = (aid, fingerprint) => certificates.get(`${aid};${fingerprint}`);
|
|
782
|
+
if (current)
|
|
783
|
+
(bodyUpdate ? verifyAgentMdBodyUpdate : verifyAgentMdUpload)(content, identity.certPem, target, undefined, resolver);
|
|
784
|
+
const result = verifyDocument(content, { expectedAid: target, primaryCertPem: identity.certPem, requireCurrentValidity: current, readCompatibility, certificateResolver: resolver });
|
|
785
|
+
if (result.status === 'invalid')
|
|
786
|
+
throw ownershipFailure('invalid', result.reason ?? 'document signature verification failed');
|
|
787
|
+
return result;
|
|
788
|
+
}
|
|
639
789
|
async _verifyEntityOwnerSigner(owner) {
|
|
640
790
|
if (!this._entitySignerResolver)
|
|
641
791
|
throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
|
|
@@ -663,52 +813,21 @@ export class AgentMdManager {
|
|
|
663
813
|
throw ownershipFailure('conflict', 'entity agent.md certificate generation changed');
|
|
664
814
|
return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
|
|
665
815
|
}
|
|
666
|
-
|
|
667
|
-
const invalid = ownershipFailure('persistence_unavailable', 'pending entity original V2 authorization is missing or invalid');
|
|
668
|
-
try {
|
|
669
|
-
if (typeof authorization !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(authorization) || authorization.includes('=') && authorization.length % 4 !== 0)
|
|
670
|
-
throw invalid;
|
|
671
|
-
const raw = Buffer.from(authorization, 'base64url');
|
|
672
|
-
if (raw.toString('base64url') !== authorization.replace(/=+$/, ''))
|
|
673
|
-
throw invalid;
|
|
674
|
-
const text = new TextDecoder('utf-8', { fatal: true }).decode(raw);
|
|
675
|
-
const value = JSON.parse(text);
|
|
676
|
-
const lexical = parseDocument(text, { uniqueKeys: true });
|
|
677
|
-
if (lexical.errors.length || lexical.get('version', true)?.source !== '2' || !/^[1-9][0-9]*$/.test(lexical.get('timestamp', true)?.source ?? ''))
|
|
678
|
-
throw invalid;
|
|
679
|
-
const fields = ['version', 'target_aid', 'uploader_aid', 'body_sha256', 'entity_cert_fingerprint', 'timestamp', 'nonce', 'cert_fingerprint', 'signature'];
|
|
680
|
-
if (!isJsonObject(value) || Object.keys(value).length !== fields.length || fields.some(field => !(field in value)) || value.version !== 2)
|
|
681
|
-
throw invalid;
|
|
682
|
-
const timestamp = value.timestamp, nonce = value.nonce;
|
|
683
|
-
if (typeof timestamp !== 'number' || !Number.isSafeInteger(timestamp) || timestamp <= 0 || typeof nonce !== 'string' || !nonce.trim() || Buffer.byteLength(nonce, 'utf8') > 128)
|
|
684
|
-
throw invalid;
|
|
685
|
-
if (value.target_aid !== target.toLowerCase() || value.uploader_aid !== owner.aid.toLowerCase() || value.body_sha256 !== crypto.createHash('sha256').update(content, 'utf-8').digest('hex') || value.entity_cert_fingerprint !== entityFingerprint || value.cert_fingerprint !== owner.certFingerprint || typeof value.signature !== 'string')
|
|
686
|
-
throw invalid;
|
|
687
|
-
const verified = owner.verify(`AUN-AGENTMD-OWNER-AUTH-V2;${value.uploader_aid};${value.target_aid};${value.body_sha256};${entityFingerprint};${timestamp};${nonce}`, value.signature);
|
|
688
|
-
if (!verified.ok || !verified.data?.valid)
|
|
689
|
-
throw invalid;
|
|
690
|
-
return timestamp;
|
|
691
|
-
}
|
|
692
|
-
catch {
|
|
693
|
-
throw invalid;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
_ownerUploadAuthorization(target, signedContent, owner, entityFingerprint = '') {
|
|
816
|
+
_ownerUploadAuthorization(target, signedContent, owner) {
|
|
697
817
|
const bodySha256 = crypto.createHash('sha256').update(signedContent, 'utf-8').digest('hex');
|
|
698
818
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
699
819
|
const nonce = crypto.randomBytes(16).toString('hex');
|
|
700
|
-
const canonical =
|
|
820
|
+
const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
|
|
701
821
|
const signed = owner.sign(canonical);
|
|
702
822
|
if (!signed.ok || !signed.data) {
|
|
703
823
|
const message = signed.error?.message ?? 'owner authorization signing failed';
|
|
704
824
|
throw new ClientSignatureError(message);
|
|
705
825
|
}
|
|
706
826
|
const payload = {
|
|
707
|
-
version:
|
|
827
|
+
version: 1,
|
|
708
828
|
target_aid: target.toLowerCase(),
|
|
709
829
|
uploader_aid: owner.aid.toLowerCase(),
|
|
710
830
|
body_sha256: bodySha256,
|
|
711
|
-
...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
|
|
712
831
|
timestamp,
|
|
713
832
|
nonce,
|
|
714
833
|
cert_fingerprint: owner.certFingerprint,
|
|
@@ -721,13 +840,15 @@ export class AgentMdManager {
|
|
|
721
840
|
throw ownershipFailure('persistence_unavailable', 'agent.md upload requires its original document condition');
|
|
722
841
|
const gatewayUrl = await this._resolveGateway(target);
|
|
723
842
|
const authorization = String(ownerAuthorization ?? '').trim();
|
|
724
|
-
const
|
|
843
|
+
const meta = validateAgentMdDocument(signedContent, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
844
|
+
const entity = ['group', 'org'].includes(String(meta.type));
|
|
725
845
|
const uploadHeaders = {
|
|
846
|
+
'X-AUN-Signature-Format': 'multi-v1',
|
|
726
847
|
'Content-Type': 'text/markdown; charset=utf-8',
|
|
727
|
-
...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
848
|
+
...(!entity && !legacyRelationship(meta) ? { 'X-AUN-Ownership-Check': '1' } : {}),
|
|
728
849
|
...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
|
|
729
850
|
...(this._uploadViaGatewayHost ? { Host: target } : {}),
|
|
730
|
-
...(authorization
|
|
851
|
+
...(authorization && !entity ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
|
|
731
852
|
};
|
|
732
853
|
if (!authorization || entity) {
|
|
733
854
|
const token = await this._uploadAccessToken(entity ? this._ownerAid() : target, gatewayUrl);
|
|
@@ -985,14 +1106,14 @@ export class AgentMdManager {
|
|
|
985
1106
|
return String(await this._gatewayResolver?.(aid) ?? '');
|
|
986
1107
|
}
|
|
987
1108
|
async _resolvePeer(aid, certFingerprint, signedAt) {
|
|
988
|
-
const expectedFp =
|
|
1109
|
+
const expectedFp = certFingerprint ? `sha256:${normalizeFingerprintHex(certFingerprint)}` : '';
|
|
989
1110
|
const current = this._currentAid();
|
|
990
|
-
if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
991
|
-
if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp))
|
|
1111
|
+
if (signedAt === undefined && current && current.aid.toLowerCase() === aid.toLowerCase()) {
|
|
1112
|
+
if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp) || publicKeyMatchesFingerprint(current.certPem, expectedFp))
|
|
992
1113
|
return current;
|
|
993
1114
|
}
|
|
994
1115
|
const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
|
|
995
|
-
if (!(peer instanceof AID)) {
|
|
1116
|
+
if (!(peer instanceof AID) || peer.aid.toLowerCase() !== aid.toLowerCase()) {
|
|
996
1117
|
throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
|
|
997
1118
|
}
|
|
998
1119
|
return peer;
|
|
@@ -1016,7 +1137,7 @@ export class AgentMdManager {
|
|
|
1016
1137
|
const response = await requestText(this._url(target, gatewayUrl), {
|
|
1017
1138
|
method: 'HEAD',
|
|
1018
1139
|
verifySsl: this._verifySsl,
|
|
1019
|
-
headers: { Accept: 'text/markdown' },
|
|
1140
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
1020
1141
|
timeoutMs: HEAD_HTTP_TIMEOUT_MS,
|
|
1021
1142
|
});
|
|
1022
1143
|
if (response.status === 404) {
|
|
@@ -1054,7 +1175,7 @@ export class AgentMdManager {
|
|
|
1054
1175
|
this._log.debug(`downloadAgentMd enter: aid=${target}`);
|
|
1055
1176
|
const gatewayUrl = await this._resolveGateway(target);
|
|
1056
1177
|
const cached = this.loadRecord(target) ?? {};
|
|
1057
|
-
const headers = { Accept: 'text/markdown' };
|
|
1178
|
+
const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
|
|
1058
1179
|
const etag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
|
|
1059
1180
|
const lastModified = String(cached.last_modified ?? '').trim();
|
|
1060
1181
|
let response = await requestText(this._url(target, gatewayUrl), {
|
|
@@ -1062,6 +1183,7 @@ export class AgentMdManager {
|
|
|
1062
1183
|
verifySsl: this._verifySsl,
|
|
1063
1184
|
headers,
|
|
1064
1185
|
timeoutMs,
|
|
1186
|
+
redirectsLeft: 0,
|
|
1065
1187
|
strictUtf8: true,
|
|
1066
1188
|
});
|
|
1067
1189
|
let reusedCachedNotModified = false;
|
|
@@ -1081,8 +1203,9 @@ export class AgentMdManager {
|
|
|
1081
1203
|
response = await requestText(this._url(target, gatewayUrl), {
|
|
1082
1204
|
method: 'GET',
|
|
1083
1205
|
verifySsl: this._verifySsl,
|
|
1084
|
-
headers: { Accept: 'text/markdown' },
|
|
1206
|
+
headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
|
|
1085
1207
|
timeoutMs,
|
|
1208
|
+
redirectsLeft: 0,
|
|
1086
1209
|
strictUtf8: true,
|
|
1087
1210
|
});
|
|
1088
1211
|
}
|
|
@@ -1095,29 +1218,24 @@ export class AgentMdManager {
|
|
|
1095
1218
|
throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
|
|
1096
1219
|
}
|
|
1097
1220
|
const content = response.text;
|
|
1098
|
-
|
|
1099
|
-
throw ownershipFailure('conflict', 'entity agent.md is not from the current lifecycle generation');
|
|
1100
|
-
const parsed = parseAgentMdTailSignature(content);
|
|
1221
|
+
const parsed = parseAgentMdTailSignature(content, true);
|
|
1101
1222
|
let entity = false;
|
|
1102
1223
|
let frontmatter = {};
|
|
1103
1224
|
try {
|
|
1104
|
-
frontmatter =
|
|
1225
|
+
frontmatter = validateAgentMdReadDocument(content).frontmatter;
|
|
1105
1226
|
entity = ['group', 'org'].includes(String(frontmatter.type));
|
|
1106
1227
|
}
|
|
1107
1228
|
catch { }
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
throw ownershipFailure('invalid', error instanceof Error ? error.message : String(error));
|
|
1114
|
-
}
|
|
1229
|
+
try {
|
|
1230
|
+
frontmatter = validateAgentMdReadDocument(content, { expectedAid: target, requireSignature: entity }).frontmatter;
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
throw ownershipFailure('invalid', error instanceof Error ? error.message : String(error));
|
|
1115
1234
|
}
|
|
1116
|
-
if (entity)
|
|
1117
|
-
await this._entityCurrentContext(target, frontmatter);
|
|
1118
1235
|
const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
|
|
1119
|
-
const peer =
|
|
1120
|
-
const
|
|
1236
|
+
const peer = await this._resolvePeer(entity ? parsed.fields.signer_aid ?? target : target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
|
|
1237
|
+
const generic = await this._verifyDocument(content, target, peer);
|
|
1238
|
+
const verified = resultOk({ ...generic, aid: target, cert_fingerprint: parsed.fields?.cert_fingerprint, timestamp: Number(parsed.fields?.timestamp) });
|
|
1121
1239
|
if (!verified.ok || !verified.data) {
|
|
1122
1240
|
const message = verified.error?.message ?? 'agent.md verification failed';
|
|
1123
1241
|
throw new AUNError(message);
|
|
@@ -1128,8 +1246,6 @@ export class AgentMdManager {
|
|
|
1128
1246
|
if (statusText === 'invalid') {
|
|
1129
1247
|
throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : {});
|
|
1130
1248
|
}
|
|
1131
|
-
if (!entity)
|
|
1132
|
-
frontmatter = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
|
|
1133
1249
|
let ownershipStatus;
|
|
1134
1250
|
if (entity)
|
|
1135
1251
|
ownershipStatus = 'not_applicable';
|
|
@@ -1328,14 +1444,17 @@ export class AgentMdManager {
|
|
|
1328
1444
|
payload.aid = this.safeAid(aid);
|
|
1329
1445
|
this._atomicWriteText(this.metaPath(aid), `${JSON.stringify(payload, null, 2)}\n`);
|
|
1330
1446
|
}
|
|
1331
|
-
_readRecordUnlocked(aid) {
|
|
1447
|
+
_readRecordUnlocked(aid, strict = false) {
|
|
1332
1448
|
const filePath = this.metaPath(aid);
|
|
1333
1449
|
if (!fs.existsSync(filePath))
|
|
1334
1450
|
return {};
|
|
1335
1451
|
try {
|
|
1336
1452
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1337
|
-
if (!isJsonObject(parsed))
|
|
1453
|
+
if (!isJsonObject(parsed)) {
|
|
1454
|
+
if (strict)
|
|
1455
|
+
throw ownershipFailure('persistence_unavailable', `agent.md metadata is not a mapping: aid=${aid}`);
|
|
1338
1456
|
return {};
|
|
1457
|
+
}
|
|
1339
1458
|
const record = {};
|
|
1340
1459
|
for (const [key, value] of Object.entries(parsed)) {
|
|
1341
1460
|
if (key !== 'content')
|
|
@@ -1348,6 +1467,11 @@ export class AgentMdManager {
|
|
|
1348
1467
|
return record;
|
|
1349
1468
|
}
|
|
1350
1469
|
catch (err) {
|
|
1470
|
+
if (strict) {
|
|
1471
|
+
if (err instanceof AUNError && err.stringCode === 'persistence_unavailable')
|
|
1472
|
+
throw err;
|
|
1473
|
+
throw ownershipFailure('persistence_unavailable', `agent.md metadata is damaged: aid=${aid}`);
|
|
1474
|
+
}
|
|
1351
1475
|
this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
|
|
1352
1476
|
return {};
|
|
1353
1477
|
}
|