@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +129 -78
  2. package/_packed_docs/CHANGELOG.md +129 -78
  3. package/_packed_docs/INDEX.md +201 -195
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. 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
  7. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
  8. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  9. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  10. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
  13. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  14. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  15. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  16. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
  17. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  18. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
  19. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  20. package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
  21. package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
  22. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  23. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
  24. package/_packed_docs/sdk/INDEX.md +92 -80
  25. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  26. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  27. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
  28. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  29. 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
  30. 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
  31. 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
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
  33. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  34. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. package/dist/agent-md-schema.d.ts +13 -1
  44. package/dist/agent-md-schema.js +61 -33
  45. package/dist/agent-md-schema.js.map +1 -1
  46. package/dist/agent-md.d.ts +8 -1
  47. package/dist/agent-md.js +243 -119
  48. package/dist/agent-md.js.map +1 -1
  49. package/dist/aid-store.d.ts +29 -0
  50. package/dist/aid-store.js +179 -34
  51. package/dist/aid-store.js.map +1 -1
  52. package/dist/aid.js +18 -13
  53. package/dist/aid.js.map +1 -1
  54. package/dist/auth.js +74 -2
  55. package/dist/auth.js.map +1 -1
  56. package/dist/cert-utils.d.ts +43 -1
  57. package/dist/cert-utils.js +144 -56
  58. package/dist/cert-utils.js.map +1 -1
  59. package/dist/client/delivery.d.ts +9 -0
  60. package/dist/client/delivery.js +179 -11
  61. package/dist/client/delivery.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +1 -0
  63. package/dist/client/rpc-pipeline.js +125 -82
  64. package/dist/client/rpc-pipeline.js.map +1 -1
  65. package/dist/client/v2-e2ee.js +95 -89
  66. package/dist/client/v2-e2ee.js.map +1 -1
  67. package/dist/client.js +48 -19
  68. package/dist/client.js.map +1 -1
  69. package/dist/events.d.ts +5 -3
  70. package/dist/events.js +25 -8
  71. package/dist/events.js.map +1 -1
  72. package/dist/facades.js +5 -5
  73. package/dist/facades.js.map +1 -1
  74. package/dist/index.d.ts +2 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/keystore/aid-db.d.ts +1 -1
  77. package/dist/keystore/aid-db.js +11 -2
  78. package/dist/keystore/aid-db.js.map +1 -1
  79. package/dist/keystore/index.d.ts +1 -1
  80. package/dist/keystore/local-token-store.d.ts +1 -1
  81. package/dist/keystore/local-token-store.js +2 -2
  82. package/dist/keystore/local-token-store.js.map +1 -1
  83. package/dist/org-protocol.js +77 -13
  84. package/dist/org-protocol.js.map +1 -1
  85. package/dist/ownership-v21.d.ts +26 -8
  86. package/dist/ownership-v21.js +147 -70
  87. package/dist/ownership-v21.js.map +1 -1
  88. package/dist/ownership-workflow.d.ts +5 -0
  89. package/dist/ownership-workflow.js +366 -56
  90. package/dist/ownership-workflow.js.map +1 -1
  91. package/dist/tools/cross-sdk-agent.d.ts +2 -1
  92. package/dist/tools/cross-sdk-agent.js +109 -6
  93. package/dist/tools/cross-sdk-agent.js.map +1 -1
  94. package/dist/transport.d.ts +1 -0
  95. package/dist/transport.js +10 -0
  96. package/dist/transport.js.map +1 -1
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. 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
- loadRecord(aid) {
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
- saveRecord(aid, fields) {
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.loadRecord(target);
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
- verifyAgentMdUpload(signedContent, current.certPem, target);
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
- verifyAgentMdUpload(rawContent, identity.certPem, target);
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 (['group', 'org'].includes(String(parsed.frontmatter.type)))
445
- await this._entityContext(target, parsed.frontmatter, signer);
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
- verifyAgentMdUpload(signedContent, signer.certPem, target);
501
+ verifyAgentMdBodyUpdate(signedContent, signer.certPem, target);
453
502
  return await this._uploadPrepared(target, signedContent, signer);
454
503
  }
455
- pendingUpload(aid) {
456
- const record = this.loadRecord(aid);
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
- verifyAgentMdUpload(pending.content, identity.certPem, aid);
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
- verifyAgentMdUpload(pending.content, identity.certPem, aid);
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.pendingUpload(target))?.relation;
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
- const authority = fresh;
551
- const candidate = parseOwnershipFile(signedContent, 'agent', { expectedSubAid: target });
552
- const statement = fresh.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
553
- relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
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 entityOwnerUpload = entity && uploader.aid.toLowerCase() !== target.toLowerCase();
558
- const entityAuthorization = entityOwnerUpload ? originalCondition ? (this.pendingUpload(target))?.authorization : this._ownerUploadAuthorization(target, signedContent, uploader, String(entityContext.entity_cert_fingerprint)) : '';
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.saveRecord(target, {
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.saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) });
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
- if (!observed.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
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 (![owner, target.toLowerCase()].includes(signer.aid.toLowerCase()) || !uploader || ![owner, target.toLowerCase()].includes(uploader.aid.toLowerCase()) || uploader.aid.toLowerCase() !== this._ownerAid().toLowerCase())
615
- throw ownershipFailure('forbidden', 'group/org agent.md requires entity or current owner identity');
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.aid.toLowerCase() === owner)
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
- _validateEntityUploadAuthorization(authorization, target, content, owner, entityFingerprint) {
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 = entityFingerprint ? `AUN-AGENTMD-OWNER-AUTH-V2;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${entityFingerprint};${timestamp};${nonce}` : `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
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: entityFingerprint ? 2 : 1,
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 entity = ['group', 'org'].includes(String(validateAgentMdDocument(signedContent, { expectedAid: target }).frontmatter.type));
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 ? { [entity ? 'AUN-AGENTMD-OWNER-AUTHORIZATION' : 'X-AUN-AgentMD-Owner-Authorization']: 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 = String(certFingerprint ?? '').trim().toLowerCase();
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
- if (headerValue(response.headers, 'X-AUN-Entity-Verification-Status') === 'stale')
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 = parseFileFrontmatter(parsed.payload);
1225
+ frontmatter = validateAgentMdReadDocument(content).frontmatter;
1105
1226
  entity = ['group', 'org'].includes(String(frontmatter.type));
1106
1227
  }
1107
1228
  catch { }
1108
- if (entity) {
1109
- try {
1110
- frontmatter = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false, requireSignature: true }).frontmatter;
1111
- }
1112
- catch (error) {
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 = entity ? await this._entitySigningIdentity(target, frontmatter, parsed.fields) : await this._resolvePeer(target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
1120
- const verified = peer.verifyAgentMd(content);
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
  }