@agentunion/fastaun-browser 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 (114) 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 +15 -4
  44. package/dist/agent-md-schema.d.ts.map +1 -1
  45. package/dist/agent-md-schema.js +64 -30
  46. package/dist/agent-md-schema.js.map +1 -1
  47. package/dist/agent-md.d.ts +10 -2
  48. package/dist/agent-md.d.ts.map +1 -1
  49. package/dist/agent-md.js +278 -121
  50. package/dist/agent-md.js.map +1 -1
  51. package/dist/aid-store.d.ts +46 -0
  52. package/dist/aid-store.d.ts.map +1 -1
  53. package/dist/aid-store.js +149 -10
  54. package/dist/aid-store.js.map +1 -1
  55. package/dist/aid.d.ts +4 -1
  56. package/dist/aid.d.ts.map +1 -1
  57. package/dist/aid.js +23 -42
  58. package/dist/aid.js.map +1 -1
  59. package/dist/auth.d.ts +1 -1
  60. package/dist/auth.d.ts.map +1 -1
  61. package/dist/auth.js +85 -18
  62. package/dist/auth.js.map +1 -1
  63. package/dist/bundle.js +1581 -781
  64. package/dist/cert-utils.d.ts +49 -1
  65. package/dist/cert-utils.d.ts.map +1 -1
  66. package/dist/cert-utils.js +102 -56
  67. package/dist/cert-utils.js.map +1 -1
  68. package/dist/client/delivery.d.ts +7 -0
  69. package/dist/client/delivery.d.ts.map +1 -1
  70. package/dist/client/delivery.js +243 -28
  71. package/dist/client/delivery.js.map +1 -1
  72. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  73. package/dist/client/rpc-pipeline.js +89 -77
  74. package/dist/client/rpc-pipeline.js.map +1 -1
  75. package/dist/client/v2-e2ee.d.ts.map +1 -1
  76. package/dist/client/v2-e2ee.js +99 -80
  77. package/dist/client/v2-e2ee.js.map +1 -1
  78. package/dist/client.d.ts.map +1 -1
  79. package/dist/client.js +31 -17
  80. package/dist/client.js.map +1 -1
  81. package/dist/crypto.d.ts.map +1 -1
  82. package/dist/crypto.js +8 -6
  83. package/dist/crypto.js.map +1 -1
  84. package/dist/events.d.ts +5 -2
  85. package/dist/events.d.ts.map +1 -1
  86. package/dist/events.js +33 -14
  87. package/dist/events.js.map +1 -1
  88. package/dist/facades.js +5 -5
  89. package/dist/facades.js.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/org-protocol.d.ts.map +1 -1
  94. package/dist/org-protocol.js +72 -12
  95. package/dist/org-protocol.js.map +1 -1
  96. package/dist/ownership-v21.d.ts +25 -8
  97. package/dist/ownership-v21.d.ts.map +1 -1
  98. package/dist/ownership-v21.js +142 -73
  99. package/dist/ownership-v21.js.map +1 -1
  100. package/dist/ownership-workflow.d.ts +6 -1
  101. package/dist/ownership-workflow.d.ts.map +1 -1
  102. package/dist/ownership-workflow.js +221 -58
  103. package/dist/ownership-workflow.js.map +1 -1
  104. package/dist/tools/cross-sdk-agent.d.ts +2 -1
  105. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  106. package/dist/tools/cross-sdk-agent.js +91 -8
  107. package/dist/tools/cross-sdk-agent.js.map +1 -1
  108. package/dist/transport.d.ts +1 -0
  109. package/dist/transport.d.ts.map +1 -1
  110. package/dist/transport.js +2 -0
  111. package/dist/transport.js.map +1 -1
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +1 -1
package/dist/agent-md.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { AID } from './aid.js';
2
2
  import { parseDocument } from 'yaml';
3
- import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
4
- import { parseFileFrontmatter, validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
3
+ import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, parseAgentMdTailSignatureForRead } from './cert-utils.js';
4
+ import { parseFileFrontmatter, validateAgentMdDocument, validateAgentMdDocumentForRead, verifyAgentMdUpload, verifyAgentMdLegacyBodyUpload } from './agent-md-schema.js';
5
5
  import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
6
6
  import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError } from './errors.js';
7
7
  import { isJsonObject } from './types.js';
8
- import { parseOwnershipFile } from './ownership-v21.js';
8
+ import { ownershipAid, parseOwnershipFile } from './ownership-v21.js';
9
9
  const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
10
10
  export function buildOwnershipReleaseAgentMd(content, release) {
11
11
  return updateOwnershipAgentMd(content, { ownership_release: release });
@@ -327,7 +327,7 @@ export class AgentMdManager {
327
327
  const target = this._safeAid(aid);
328
328
  const gatewayUrl = await this._resolveGateway(target);
329
329
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
330
- const headers = { Accept: 'text/markdown', 'X-AUN-Ownership-Check': '1', ...(viaGatewayHost ? { Host: target } : {}) };
330
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', 'X-AUN-Ownership-Check': '1', ...(viaGatewayHost ? { Host: target } : {}) };
331
331
  let url = this._url(target, gatewayUrl);
332
332
  if (viaGatewayHost) {
333
333
  const parsed = new URL(gatewayUrl);
@@ -380,7 +380,7 @@ export class AgentMdManager {
380
380
  const token = await this._uploadAccessToken(target, gatewayUrl);
381
381
  const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
382
382
  method: 'PUT', cache: 'no-store', redirect: 'error', body: content,
383
- headers: { 'Content-Type': 'text/markdown; charset=utf-8', Authorization: `Bearer ${token}`,
383
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', 'Content-Type': 'text/markdown; charset=utf-8', Authorization: `Bearer ${token}`,
384
384
  ...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }) },
385
385
  });
386
386
  if (!response.ok) {
@@ -408,30 +408,23 @@ export class AgentMdManager {
408
408
  if (!current.isPrivateKeyValid()) {
409
409
  throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
410
410
  }
411
- if (content !== undefined && content !== null) {
412
- const text = String(content ?? '');
413
- if (text.length === 0)
414
- throw new ValidationError('uploadAgentMd requires non-empty content');
415
- validateAgentMdDocument(text, { expectedAid: target });
416
- await this.saveRecord(target, {
417
- content: text,
418
- local_etag: await AgentMdManager.contentEtag(text),
419
- fetched_at: Date.now(),
420
- });
421
- }
422
- const localContent = await this.readContent(target);
411
+ const localContent = content ?? (String((await this.loadRecord(target, true))?.local_draft_content ?? '') || await this.readContent(target));
423
412
  if (localContent === null || localContent.length === 0) {
424
413
  throw new ValidationError('uploadAgentMd requires local agent.md content');
425
414
  }
426
- const parsed = validateAgentMdDocument(localContent, { expectedAid: target });
415
+ const parsed = this._parseBodyUpdate(localContent, target);
427
416
  if (['group', 'org'].includes(String(parsed.frontmatter.type)))
428
417
  return await this.uploadAs(target, localContent, current);
429
- const signedResult = await current.signAgentMd(localContent);
418
+ if (parsed.signatures.length > 1) {
419
+ await this._verifyBodyUpload(target, localContent, current);
420
+ return await this._uploadPrepared(target, localContent, current);
421
+ }
422
+ const signedResult = await current.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : localContent);
430
423
  if (!signedResult.ok) {
431
424
  throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
432
425
  }
433
426
  const signed = signedResult.data.signed;
434
- await verifyAgentMdUpload(signed, current.certPem, target);
427
+ await this._verifyBodyUpload(target, signed, current);
435
428
  return await this._uploadPrepared(target, signed, current);
436
429
  }
437
430
  async uploadAs(targetAid, content, signer) {
@@ -439,19 +432,33 @@ export class AgentMdManager {
439
432
  const rawContent = String(content ?? '');
440
433
  if (!rawContent.trim())
441
434
  throw new ValidationError('uploadAgentMd requires non-empty content');
442
- const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
435
+ const parsed = this._parseBodyUpdate(rawContent, target);
436
+ if (!['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signatures.length > 1) {
437
+ await this._verifyBodyUpload(target, rawContent, signer);
438
+ return await this._uploadPrepared(target, rawContent, signer);
439
+ }
443
440
  if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
444
- const identity = await this._entityDocumentSigner(target, rawContent);
445
- await verifyAgentMdUpload(rawContent, identity.certPem, target);
446
- return await this._uploadPrepared(target, rawContent, identity);
441
+ await verifyAgentMdUpload(rawContent, signer.certPem, target, undefined, async (aid, fingerprint) => {
442
+ if (!this._entitySignerResolver)
443
+ throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
444
+ return await this._entitySignerResolver(aid, fingerprint);
445
+ });
446
+ return await this._uploadPrepared(target, rawContent, signer);
447
+ }
448
+ if (['group', 'org'].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
449
+ if (!this._entityIdentityGetter)
450
+ throw new StateError('entity signing identity is unavailable');
451
+ signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
452
+ if (signer.aid !== target)
453
+ throw new StateError('entity signing identity does not match target');
447
454
  }
448
455
  if (!signer?.isPrivateKeyValid())
449
456
  throw new StateError('uploadAgentMd requires a valid signer identity');
450
- const signedResult = await signer.signAgentMd(rawContent);
457
+ const signedResult = await signer.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : rawContent);
451
458
  if (!signedResult.ok)
452
459
  throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
453
460
  const signed = signedResult.data.signed;
454
- await verifyAgentMdUpload(signed, signer.certPem, target);
461
+ await this._verifyBodyUpload(target, signed, signer);
455
462
  return await this._uploadPrepared(target, signed, signer);
456
463
  }
457
464
  async pendingUpload(aid) {
@@ -467,10 +474,34 @@ export class AgentMdManager {
467
474
  const condition = pending.condition;
468
475
  if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
469
476
  throw new StateError('pending agent.md requires its original document condition', { stringCode: 'persistence_unavailable' });
470
- const meta = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false }).frontmatter;
471
- if ('ownership_proof' in meta || 'ownership_release' in meta)
472
- throw new StateError('legacy ownership pending agent.md cannot be recovered through ordinary upload', { stringCode: 'unsupported_protocol' });
477
+ const pendingDocument = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
478
+ const meta = pendingDocument.frontmatter;
479
+ if (this._isLegacyBodyRelation(meta) && pending.relation?.protocol !== 'legacy')
480
+ throw new StateError('legacy ownership pending agent.md requires its original relationship snapshot', { stringCode: 'unsupported_protocol' });
473
481
  try {
482
+ if (pending.relation?.protocol === 'legacy') {
483
+ const relation = pending.relation;
484
+ if (!condition.if_match || condition.if_match !== relation.original_etag || typeof relation.original_content !== 'string' || relation.original_content_etag !== await AgentMdManager.contentEtag(relation.original_content) || pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
485
+ throw new StateError('pending legacy original bytes or document condition are corrupt', { stringCode: 'persistence_unavailable' });
486
+ const original = await this._verifyLegacyBodyOriginal(aid, relation.original_content);
487
+ if (!this._isLegacyBodyRelation(original) || !this._sameBodyRelation(this._bodyRelationFields(original), relation.fields))
488
+ throw new StateError('pending legacy original relationship snapshot is corrupt', { stringCode: 'persistence_unavailable' });
489
+ if (!this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
490
+ throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
491
+ const identity = signer ?? this._currentAid();
492
+ if (!identity || identity.aid.toLowerCase() !== aid.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== aid.toLowerCase())
493
+ throw new StateError('legacy body recovery requires its current subject identity', { stringCode: 'forbidden' });
494
+ await this._verifyBodyUpload(aid, pending.content, identity);
495
+ const remote = await this._readLegacyBodyFresh(aid);
496
+ if (remote.content === pending.content && remote.etag === pending.content_etag) {
497
+ await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
498
+ return true;
499
+ }
500
+ if (remote.etag !== condition.if_match || remote.content !== relation.original_content)
501
+ throw new StateError('pending legacy original document changed', { stringCode: 'conflict' });
502
+ await this._uploadPrepared(aid, pending.content, identity, condition);
503
+ return true;
504
+ }
474
505
  if (['group', 'org'].includes(String(meta.type))) {
475
506
  const identity = signer ?? this._currentAid();
476
507
  if (!identity)
@@ -485,12 +516,10 @@ export class AgentMdManager {
485
516
  if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
486
517
  throw new StateError('pending entity original bytes are corrupt', { stringCode: 'persistence_unavailable' });
487
518
  const documentSigner = await this._entityDocumentSigner(aid, pending.content);
488
- await verifyAgentMdUpload(pending.content, documentSigner.certPem, aid);
489
- if (identity.aid.toLowerCase() !== aid.toLowerCase())
490
- await this._validateEntityUploadAuthorization(pending.authorization, aid, pending.content, identity, String(context.entity_cert_fingerprint));
519
+ await this._verifyEntityDocument(aid, pending.content);
491
520
  const remote = await this._readEntityFresh(aid);
492
521
  if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
493
- await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
522
+ await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
494
523
  return true;
495
524
  }
496
525
  if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
@@ -498,6 +527,12 @@ export class AgentMdManager {
498
527
  await this._uploadPrepared(aid, pending.content, identity, condition);
499
528
  return true;
500
529
  }
530
+ const identity = signer ?? this._currentAid();
531
+ if (pendingDocument.signatures.length > 1) {
532
+ if (!identity)
533
+ throw new StateError('pending agent.md requires a signer identity');
534
+ await this._verifyBodyUpload(aid, pending.content, identity);
535
+ }
501
536
  const remote = await this.readOwnershipFresh(aid);
502
537
  const fail = (code, reason, retryable = false) => new StateError(reason, { stringCode: code, retryable });
503
538
  const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
@@ -519,17 +554,17 @@ export class AgentMdManager {
519
554
  if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
520
555
  throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
521
556
  if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
522
- const saved = await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
557
+ const saved = await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
523
558
  if (!saved.aid)
524
559
  throw new StateError('agent.md pending completion persistence failed');
525
560
  return true;
526
561
  }
527
562
  if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
528
563
  throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
529
- const identity = signer ?? this._currentAid();
530
564
  if (!identity)
531
565
  throw new StateError('pending agent.md requires a signer identity');
532
- await verifyAgentMdUpload(pending.content, identity.certPem, aid);
566
+ if (pendingDocument.signatures.length <= 1)
567
+ await this._verifyBodyUpload(aid, pending.content, identity);
533
568
  await this._uploadPrepared(aid, pending.content, identity, condition);
534
569
  return true;
535
570
  }
@@ -538,33 +573,142 @@ export class AgentMdManager {
538
573
  throw error;
539
574
  }
540
575
  }
576
+ _parseBodyUpdate(content, target) {
577
+ const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false });
578
+ return validateAgentMdDocument(this._isLegacyBodyRelation(parsed.frontmatter) && parsed.signatures.length <= 1 ? parsed.payload : content, { expectedAid: target, validateOwnership: false });
579
+ }
580
+ _isLegacyBodyRelation(meta) {
581
+ if (['group', 'org'].includes(String(meta.type)))
582
+ return false;
583
+ if (Object.hasOwn(meta, 'ownership')) {
584
+ const ownership = meta.ownership;
585
+ const statement = isJsonObject(ownership) ? ownership.statement : undefined;
586
+ return isJsonObject(statement) && statement.version === 2 && !Object.hasOwn(statement, 'relation_version');
587
+ }
588
+ return ['owner', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(meta, field));
589
+ }
590
+ _bodyRelationFields(meta) {
591
+ return Object.fromEntries(['owner', 'ownership', 'ownership_proof', 'ownership_release'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]]));
592
+ }
593
+ _sameBodyRelation(first, second) {
594
+ const stable = (value) => Array.isArray(value) ? value.map(stable) : isJsonObject(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])) : value;
595
+ return JSON.stringify(stable(first)) === JSON.stringify(stable(second));
596
+ }
597
+ async _readLegacyBodyFresh(target) {
598
+ const gateway = await this._resolveGateway(target);
599
+ const response = await fetchWithTimeout(this._url(target, gateway), { method: 'GET', headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache' }, cache: 'no-store', redirect: 'error' });
600
+ if (response.status === 404)
601
+ throw new StateError('legacy relationship cannot be created by an ordinary body update', { stringCode: 'unsupported_protocol' });
602
+ const etag = headerValue(response.headers, 'ETag');
603
+ if (response.status !== 200 || !etag)
604
+ throw new StateError('legacy original document or ETag is unavailable', { stringCode: 'unavailable', retryable: true });
605
+ let content;
606
+ try {
607
+ content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
608
+ }
609
+ catch {
610
+ throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
611
+ }
612
+ return { content, etag, last_modified: headerValue(response.headers, 'Last-Modified'), status: response.status };
613
+ }
614
+ async _verifyLegacyBodyOriginal(target, content) {
615
+ const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false, requireSignature: true });
616
+ const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
617
+ const verified = await this._verifyPeerDocument(peer, content);
618
+ if (!verified.ok || verified.data.status !== 'verified')
619
+ throw new ClientSignatureError('legacy original agent.md signature verification failed', { stringCode: 'invalid' });
620
+ return parsed.frontmatter;
621
+ }
622
+ async _verifyPeerDocument(peer, content) {
623
+ const parsed = parseAgentMdTailSignatureForRead(content);
624
+ const additionalCertificates = new Map();
625
+ if (parsed.fields)
626
+ additionalCertificates.set(`${parsed.fields.signer_aid ?? peer.aid};${parsed.fields.cert_fingerprint}`, peer);
627
+ for (const block of parsed.signatures.slice(1))
628
+ additionalCertificates.set(`${block.fields.signer_aid};${block.fields.cert_fingerprint}`, await this._resolvePeer(block.fields.signer_aid, block.fields.cert_fingerprint, Number(block.fields.timestamp)));
629
+ return await peer.verifyAgentMd(content, (signer, fingerprint) => {
630
+ const certificate = additionalCertificates.get(`${signer};${fingerprint}`);
631
+ if (!certificate)
632
+ throw new StateError('additional signing certificate is unavailable', { stringCode: 'unavailable', retryable: true });
633
+ return certificate;
634
+ });
635
+ }
636
+ async _verifyBodyUpload(target, content, signer) {
637
+ const meta = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
638
+ await (this._isLegacyBodyRelation(meta) ? verifyAgentMdLegacyBodyUpload : verifyAgentMdUpload)(content, signer.certPem, target, undefined, async (aid, fingerprint, timestamp) => {
639
+ if (this._entitySignerResolver)
640
+ return await this._entitySignerResolver(aid, fingerprint);
641
+ return await this._resolvePeer(aid, fingerprint, timestamp);
642
+ });
643
+ }
541
644
  async _uploadPrepared(target, signed, signer, originalCondition) {
542
- const meta = validateAgentMdDocument(signed, { expectedAid: target }).frontmatter;
645
+ const meta = validateAgentMdDocument(signed, { expectedAid: target, validateOwnership: false }).frontmatter;
646
+ if (this._isLegacyBodyRelation(meta))
647
+ validateOwnershipFrontmatter(Object.fromEntries(['aid', 'type', 'owner'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]])), target);
648
+ if (!['group', 'org'].includes(String(meta.type)))
649
+ await this._verifyBodyUpload(target, signed, signer);
650
+ if (!originalCondition) {
651
+ const draft = await this.saveRecord(target, { local_draft_content: signed });
652
+ if (draft.local_draft_content !== signed)
653
+ throw new StateError('agent.md local draft persistence failed', { stringCode: 'persistence_unavailable' });
654
+ }
543
655
  const entity = ['group', 'org'].includes(String(meta.type));
544
656
  const uploader = entity ? this._currentAid() ?? signer : signer;
545
657
  const entityContext = entity ? await this._entityContext(target, meta, uploader) : undefined;
546
658
  if (entity)
547
- await verifyAgentMdUpload(signed, (await this._entityDocumentSigner(target, signed)).certPem, target);
659
+ await this._verifyEntityDocument(target, signed);
548
660
  let condition = originalCondition;
549
661
  let relation = entityContext ?? (await this.pendingUpload(target))?.relation;
550
662
  if (!condition) {
551
- const fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
552
- condition = fresh.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
553
- if (!entity) {
663
+ let fresh;
664
+ if (!entity && this._isLegacyBodyRelation(meta))
665
+ fresh = await this._readLegacyBodyFresh(target);
666
+ else {
667
+ try {
668
+ fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
669
+ }
670
+ catch (error) {
671
+ if (entity || !['unavailable', 'unsupported_protocol'].includes(String(error?.stringCode)))
672
+ throw error;
673
+ const legacy = await this._readLegacyBodyFresh(target);
674
+ const legacyMeta = validateAgentMdDocumentForRead(legacy.content, { expectedAid: target, validateOwnership: false }).frontmatter;
675
+ if (!this._isLegacyBodyRelation(legacyMeta))
676
+ throw error;
677
+ fresh = legacy;
678
+ }
679
+ }
680
+ const freshMeta = !entity && (!('exists' in fresh) || fresh.exists) && fresh.content ? validateAgentMdDocumentForRead(fresh.content, { expectedAid: target, validateOwnership: false }).frontmatter : undefined;
681
+ const legacy = !entity && (this._isLegacyBodyRelation(meta) || freshMeta && this._isLegacyBodyRelation(freshMeta));
682
+ if (legacy) {
683
+ if (!freshMeta || !this._isLegacyBodyRelation(freshMeta) || !this._sameBodyRelation(this._bodyRelationFields(meta), this._bodyRelationFields(freshMeta)))
684
+ throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
685
+ if (!fresh.etag)
686
+ throw new StateError('legacy original document ETag is unavailable', { stringCode: 'unavailable', retryable: true });
687
+ await this._verifyLegacyBodyOriginal(target, fresh.content);
688
+ condition = { if_match: fresh.etag };
689
+ relation = { protocol: 'legacy', original_content: fresh.content, original_content_etag: await AgentMdManager.contentEtag(fresh.content), original_etag: fresh.etag, fields: this._bodyRelationFields(freshMeta) };
690
+ }
691
+ else {
554
692
  const authority = fresh;
555
- const candidate = parseOwnershipFile(signed, 'agent', { expectedSubAid: target });
556
- const statement = fresh.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
557
- relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
693
+ condition = authority.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
694
+ if (!entity) {
695
+ const candidate = parseOwnershipFile(signed, 'agent', { expectedSubAid: target });
696
+ const statement = authority.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
697
+ relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
698
+ }
558
699
  }
559
700
  }
560
- const ownerUpload = entity && uploader.aid.toLowerCase() !== target.toLowerCase();
561
- const entityAuthorization = ownerUpload ? originalCondition ? (await this.pendingUpload(target))?.authorization : await this._ownerUploadAuthorization(target, signed, uploader, String(entityContext.entity_cert_fingerprint)) : '';
562
- if (ownerUpload) {
563
- const timestamp = await this._validateEntityUploadAuthorization(entityAuthorization, target, signed, uploader, String(entityContext.entity_cert_fingerprint));
564
- if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300)
565
- throw new StateError('pending entity V2 authorization expired; explicit upload is required', { stringCode: 'conflict' });
701
+ if (relation?.protocol === 'legacy') {
702
+ if (signer.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.certFingerprint !== signer.certFingerprint)
703
+ throw new StateError('legacy body update requires its current subject identity', { stringCode: 'forbidden' });
704
+ if (!this._isLegacyBodyRelation(meta) || !this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
705
+ throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
706
+ await this._verifyBodyUpload(target, signed, signer);
566
707
  }
708
+ const entityAuthorization = '';
567
709
  const pending = await this.saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signed, pending_content_etag: await AgentMdManager.contentEtag(signed), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization });
710
+ if (relation?.protocol === 'legacy' && !this._sameBodyRelation(pending.pending_relation, relation))
711
+ throw new StateError('legacy original relationship snapshot persistence failed', { stringCode: 'persistence_unavailable' });
568
712
  const savedCondition = pending.pending_condition;
569
713
  if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
570
714
  throw new StateError('agent.md pending upload persistence failed');
@@ -573,11 +717,12 @@ export class AgentMdManager {
573
717
  const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
574
718
  if (delegated && !owner?.isPrivateKeyValid())
575
719
  throw new StateError('owner agent.md upload requires a loaded owner identity');
576
- const authorization = entity ? entityAuthorization : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
577
- const result = await this._uploadHttp(target, signed, authorization, condition);
720
+ const authorization = entity ? '' : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
721
+ const result = await this._uploadHttp(target, signed, authorization, condition, relation?.protocol === 'legacy');
578
722
  const remoteEtag = String(result.etag ?? '').trim();
723
+ const contentEtag = await AgentMdManager.contentEtag(signed);
579
724
  const saved = await this.saveRecord(target, {
580
- content: signed, local_etag: await AgentMdManager.contentEtag(signed), remote_etag: remoteEtag || undefined,
725
+ content: signed, local_etag: contentEtag, remote_etag: remoteEtag || undefined, ...(remoteEtag === contentEtag ? { local_draft_content: '' } : {}),
581
726
  last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
582
727
  remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
583
728
  pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
@@ -595,12 +740,17 @@ export class AgentMdManager {
595
740
  const fresh = await this._readEntityFresh(this._safeAid(target));
596
741
  if (fresh.ownership_status !== 'not_applicable' || !['current', 'stale'].includes(fresh.entity_status ?? '') || !fresh.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(fresh.entity_cert_fingerprint ?? ''))
597
742
  throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
598
- return { owner_aid: this._safeAid(fresh.owner_aid), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
743
+ try {
744
+ return { owner_aid: this._safeAid(ownershipAid(fresh.owner_aid, 'owner_aid')), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
745
+ }
746
+ catch {
747
+ throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
748
+ }
599
749
  }
600
750
  async _readEntityFresh(target) {
601
751
  const gateway = await this._resolveGateway(target);
602
752
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
603
- const headers = { 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
753
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', 'X-AUN-Ownership-Check': '1', 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
604
754
  let url = this._url(target, gateway);
605
755
  if (viaGatewayHost) {
606
756
  const parsed = new URL(gateway);
@@ -656,8 +806,8 @@ export class AgentMdManager {
656
806
  }
657
807
  async _entityContext(target, meta, signer) {
658
808
  const owner = String(meta.owner ?? '').trim().toLowerCase();
659
- if (![owner, target.toLowerCase()].includes(signer.aid.toLowerCase()) || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase())
660
- throw new StateError('group/org agent.md requires entity or current owner uploader', { stringCode: 'forbidden' });
809
+ if (signer.aid.toLowerCase() !== owner || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase())
810
+ throw new StateError('group/org agent.md requires current owner uploader', { stringCode: 'forbidden' });
661
811
  const context = await this._entityCurrentContext(target, meta, true);
662
812
  if (signer.aid.toLowerCase() === owner)
663
813
  await this._verifyEntityOwnerSigner(signer);
@@ -683,6 +833,17 @@ export class AgentMdManager {
683
833
  throw new StateError('entity signing certificate changed', { stringCode: 'conflict' });
684
834
  return signer;
685
835
  }
836
+ async _verifyEntityDocument(target, content) {
837
+ const primary = await this._entityDocumentSigner(target, content);
838
+ await verifyAgentMdUpload(content, primary.certPem, target, undefined, async (signer, fingerprint) => {
839
+ if (!this._entitySignerResolver)
840
+ throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
841
+ const material = await this._entitySignerResolver(signer, fingerprint);
842
+ if (material.aid !== signer || material.certFingerprint !== fingerprint)
843
+ throw new StateError('entity additional signing certificate changed', { stringCode: 'conflict' });
844
+ return material;
845
+ });
846
+ }
686
847
  async _verifyEntityOwnerSigner(owner) {
687
848
  if (!this._entitySignerResolver)
688
849
  throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
@@ -694,15 +855,15 @@ export class AgentMdManager {
694
855
  async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
695
856
  const target = this._safeAid(String(aid ?? this._ownerAid() ?? '').trim());
696
857
  const contentResult = await this._downloadHttp(target, timeoutMs);
697
- const parsed = parseAgentMdTailSignature(contentResult.content);
858
+ const parsed = parseAgentMdTailSignatureForRead(contentResult.content);
698
859
  let entity = false;
699
860
  try {
700
- entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload).type));
861
+ entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload, false, false).type));
701
862
  }
702
863
  catch { }
703
864
  let frontmatter;
704
865
  try {
705
- frontmatter = validateAgentMdDocument(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
866
+ frontmatter = validateAgentMdDocumentForRead(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
706
867
  }
707
868
  catch (error) {
708
869
  if (entity)
@@ -710,10 +871,9 @@ export class AgentMdManager {
710
871
  throw error;
711
872
  }
712
873
  const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
713
- if (entity)
714
- await this._entityCurrentContext(target, frontmatter);
715
- const peer = entity ? await this._entityDocumentSigner(target, contentResult.content) : await this._resolvePeer(target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
716
- const verified = await peer.verifyAgentMd(contentResult.content);
874
+ const signerAid = entity ? (parsed.fields?.signer_aid ?? target) : target;
875
+ const peer = await this._resolvePeer(signerAid, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
876
+ const verified = await this._verifyPeerDocument(peer, contentResult.content);
717
877
  if (!verified.ok)
718
878
  throw new AUNError(verified.error.message);
719
879
  const signature = { ...verified.data };
@@ -971,7 +1131,7 @@ export class AgentMdManager {
971
1131
  const root = this.root;
972
1132
  await this._writeStorage(this._contentKey(aid), String(content ?? ''), root);
973
1133
  }
974
- async loadRecord(aid) {
1134
+ async loadRecord(aid, strict = false) {
975
1135
  const target = String(aid ?? '').trim();
976
1136
  if (!target)
977
1137
  return null;
@@ -979,7 +1139,7 @@ export class AgentMdManager {
979
1139
  const generation = this._storageGeneration;
980
1140
  try {
981
1141
  const loaded = await this._withLock(async () => {
982
- const record = await this._readRecordUnlocked(target, root);
1142
+ const record = await this._readRecordUnlocked(target, root, strict);
983
1143
  const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
984
1144
  try {
985
1145
  const content = await this._readStorage(this._contentKey(target), root);
@@ -1005,6 +1165,8 @@ export class AgentMdManager {
1005
1165
  return { ...loaded };
1006
1166
  }
1007
1167
  catch (err) {
1168
+ if (strict)
1169
+ throw err;
1008
1170
  this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
1009
1171
  return null;
1010
1172
  }
@@ -1015,10 +1177,12 @@ export class AgentMdManager {
1015
1177
  return {};
1016
1178
  const root = this.root;
1017
1179
  const generation = this._storageGeneration;
1180
+ let strictDraft = false;
1018
1181
  try {
1019
1182
  const inputFields = { ...fields };
1183
+ strictDraft = typeof inputFields.local_draft_content === 'string';
1020
1184
  const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
1021
- if (hasContent) {
1185
+ if (hasContent && !strictDraft) {
1022
1186
  const text = String(inputFields.content ?? '');
1023
1187
  await this._writeStorage(this._contentKey(target), text, root);
1024
1188
  if (!inputFields.local_etag)
@@ -1028,8 +1192,18 @@ export class AgentMdManager {
1028
1192
  }
1029
1193
  delete inputFields.content;
1030
1194
  const record = await this._withLock(async () => {
1031
- const next = { ...(await this._readRecordUnlocked(target, root)), aid: target };
1195
+ const next = { ...(await this._readRecordUnlocked(target, root, strictDraft)), aid: target };
1196
+ if (hasContent && strictDraft) {
1197
+ const text = String(fields.content ?? '');
1198
+ await this._writeStorage(this._contentKey(target), text, root);
1199
+ if (!inputFields.local_etag)
1200
+ inputFields.local_etag = await AgentMdManager.contentEtag(text);
1201
+ if (!inputFields.fetched_at)
1202
+ inputFields.fetched_at = Date.now();
1203
+ }
1032
1204
  for (const [key, value] of Object.entries(inputFields)) {
1205
+ if (key === 'local_draft_content' && value === '' && hasContent && next.local_draft_content !== fields.content)
1206
+ continue;
1033
1207
  if (value !== undefined && value !== null)
1034
1208
  next[key] = value;
1035
1209
  }
@@ -1045,6 +1219,8 @@ export class AgentMdManager {
1045
1219
  return { ...loaded };
1046
1220
  }
1047
1221
  catch (err) {
1222
+ if (strictDraft)
1223
+ throw err;
1048
1224
  this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
1049
1225
  return {};
1050
1226
  }
@@ -1105,7 +1281,8 @@ export class AgentMdManager {
1105
1281
  return String(await this._gatewayResolver?.(aid) ?? '');
1106
1282
  }
1107
1283
  async _resolvePeer(aid, certFingerprint, signedAt) {
1108
- const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
1284
+ const normalizedFp = normalizeFingerprintHex(certFingerprint);
1285
+ const expectedFp = normalizedFp ? `sha256:${normalizedFp}` : String(certFingerprint ?? '').trim().toLowerCase();
1109
1286
  const current = this._currentAid();
1110
1287
  if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
1111
1288
  if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
@@ -1115,6 +1292,8 @@ export class AgentMdManager {
1115
1292
  if (!(peer instanceof AID)) {
1116
1293
  throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
1117
1294
  }
1295
+ if (peer.aid.toLowerCase() !== aid.toLowerCase())
1296
+ throw new ClientSignatureError(`agent.md signer does not match requested AID: ${aid}`, { stringCode: 'invalid' });
1118
1297
  return peer;
1119
1298
  }
1120
1299
  async _accessToken(aid, gatewayUrl) {
@@ -1130,52 +1309,21 @@ export class AgentMdManager {
1130
1309
  throw new StateError('authenticate did not return access_token');
1131
1310
  return token;
1132
1311
  }
1133
- async _validateEntityUploadAuthorization(authorization, target, content, owner, entityFingerprint) {
1134
- const invalid = new StateError('pending entity original V2 authorization is missing or invalid', { stringCode: 'persistence_unavailable' });
1135
- try {
1136
- if (typeof authorization !== 'string' || !/^[A-Za-z0-9_-]+={0,2}$/.test(authorization) || authorization.includes('=') && authorization.length % 4 !== 0)
1137
- throw invalid;
1138
- const binary = globalThis.atob(authorization.replace(/-/g, '+').replace(/_/g, '/'));
1139
- const text = new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(binary, char => char.charCodeAt(0)));
1140
- if (base64UrlEncode(text) !== authorization.replace(/=+$/, ''))
1141
- throw invalid;
1142
- const value = JSON.parse(text);
1143
- const lexical = parseDocument(text, { uniqueKeys: true });
1144
- if (lexical.errors.length || lexical.get('version', true)?.source !== '2' || !/^[1-9][0-9]*$/.test(lexical.get('timestamp', true)?.source ?? ''))
1145
- throw invalid;
1146
- const fields = ['version', 'target_aid', 'uploader_aid', 'body_sha256', 'entity_cert_fingerprint', 'timestamp', 'nonce', 'cert_fingerprint', 'signature'];
1147
- if (!isJsonObject(value) || Object.keys(value).length !== fields.length || fields.some(field => !(field in value)) || value.version !== 2)
1148
- throw invalid;
1149
- const timestamp = value.timestamp, nonce = value.nonce;
1150
- if (typeof timestamp !== 'number' || !Number.isSafeInteger(timestamp) || timestamp <= 0 || typeof nonce !== 'string' || !nonce.trim() || new TextEncoder().encode(nonce).length > 128)
1151
- throw invalid;
1152
- if (value.target_aid !== target.toLowerCase() || value.uploader_aid !== owner.aid.toLowerCase() || value.body_sha256 !== await sha256Hex(content) || value.entity_cert_fingerprint !== entityFingerprint || value.cert_fingerprint !== owner.certFingerprint || typeof value.signature !== 'string')
1153
- throw invalid;
1154
- const verified = await owner.verify(`AUN-AGENTMD-OWNER-AUTH-V2;${value.uploader_aid};${value.target_aid};${value.body_sha256};${entityFingerprint};${timestamp};${nonce}`, value.signature);
1155
- if (!verified.ok || !verified.data?.valid)
1156
- throw invalid;
1157
- return timestamp;
1158
- }
1159
- catch {
1160
- throw invalid;
1161
- }
1162
- }
1163
- async _ownerUploadAuthorization(target, content, owner, entityFingerprint = '') {
1312
+ async _ownerUploadAuthorization(target, content, owner) {
1164
1313
  const bodySha256 = await sha256Hex(content);
1165
1314
  const timestamp = Math.floor(Date.now() / 1000);
1166
1315
  const nonce = randomOwnerAuthorizationNonce();
1167
- 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}`;
1316
+ const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
1168
1317
  const signed = await owner.sign(canonical);
1169
1318
  if (!signed.ok || !signed.data) {
1170
1319
  const message = signed.error?.message || 'owner authorization signing failed';
1171
1320
  throw new ClientSignatureError(message);
1172
1321
  }
1173
1322
  return base64UrlEncode(JSON.stringify({
1174
- version: entityFingerprint ? 2 : 1,
1323
+ version: 1,
1175
1324
  target_aid: target.toLowerCase(),
1176
1325
  uploader_aid: owner.aid.toLowerCase(),
1177
1326
  body_sha256: bodySha256,
1178
- ...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
1179
1327
  timestamp,
1180
1328
  nonce,
1181
1329
  cert_fingerprint: owner.certFingerprint,
@@ -1185,20 +1333,21 @@ export class AgentMdManager {
1185
1333
  _url(aid, gatewayUrl) {
1186
1334
  return `${agentMdHttpScheme(gatewayUrl)}://${this._safeAid(aid)}/agent.md`;
1187
1335
  }
1188
- async _uploadHttp(aid, content, ownerAuthorization = '', condition) {
1336
+ async _uploadHttp(aid, content, ownerAuthorization = '', condition, legacyBodyUpdate = false) {
1189
1337
  if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
1190
1338
  throw new StateError('agent.md upload requires its original document condition', { stringCode: 'persistence_unavailable' });
1191
1339
  const gatewayUrl = await this._resolveGateway(aid);
1192
1340
  const authorization = String(ownerAuthorization ?? '').trim();
1193
- const entity = ['group', 'org'].includes(String(validateAgentMdDocument(content, { expectedAid: aid }).frontmatter.type));
1341
+ const meta = validateAgentMdDocument(content, { expectedAid: aid, validateOwnership: false }).frontmatter;
1342
+ const entity = ['group', 'org'].includes(String(meta.type));
1194
1343
  // 浏览器禁止覆盖 Host;动态 AID URL 由浏览器/Docker 的通配解析路由。
1195
1344
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
1196
- const headers = {
1345
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1',
1197
1346
  'Content-Type': 'text/markdown; charset=utf-8',
1198
- ...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
1347
+ ...(!entity && !(legacyBodyUpdate && this._isLegacyBodyRelation(meta)) ? { 'X-AUN-Ownership-Check': '1' } : {}),
1199
1348
  ...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
1200
1349
  ...(viaGatewayHost ? { Host: this._safeAid(aid) } : {}),
1201
- ...(authorization ? { [entity ? 'AUN-AGENTMD-OWNER-AUTHORIZATION' : 'X-AUN-AgentMD-Owner-Authorization']: authorization } : {}),
1350
+ ...(!entity && authorization ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
1202
1351
  };
1203
1352
  if (!authorization || entity) {
1204
1353
  const token = await this._uploadAccessToken(aid, gatewayUrl);
@@ -1223,7 +1372,7 @@ export class AgentMdManager {
1223
1372
  }
1224
1373
  let response;
1225
1374
  try {
1226
- response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content });
1375
+ response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content, redirect: 'error' });
1227
1376
  }
1228
1377
  catch (error) {
1229
1378
  if (!viaGatewayHost)
@@ -1231,12 +1380,12 @@ export class AgentMdManager {
1231
1380
  const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
1232
1381
  if (!fallbackUrl || fallbackUrl === url)
1233
1382
  throw error;
1234
- response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
1383
+ response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
1235
1384
  }
1236
1385
  if (response.status === 404 && viaGatewayHost) {
1237
1386
  const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
1238
1387
  if (fallbackUrl && fallbackUrl !== url) {
1239
- response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
1388
+ response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
1240
1389
  }
1241
1390
  }
1242
1391
  if (response.status === 404) {
@@ -1260,13 +1409,13 @@ export class AgentMdManager {
1260
1409
  const cached = await this.loadRecord(target) ?? {};
1261
1410
  const gatewayUrl = await this._resolveGateway(target);
1262
1411
  const url = this._url(target, gatewayUrl);
1263
- const headers = { Accept: 'text/markdown' };
1412
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
1264
1413
  const cachedEtag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
1265
1414
  const cachedLastModified = String(cached.last_modified ?? '').trim();
1266
1415
  let response = await fetchWithTimeout(url, {
1267
1416
  method: 'GET',
1268
1417
  headers,
1269
- redirect: 'follow',
1418
+ redirect: 'error',
1270
1419
  }, timeoutMs);
1271
1420
  if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
1272
1421
  return {
@@ -1279,8 +1428,8 @@ export class AgentMdManager {
1279
1428
  if (response.status === 304) {
1280
1429
  response = await fetchWithTimeout(url, {
1281
1430
  method: 'GET',
1282
- headers: { Accept: 'text/markdown' },
1283
- redirect: 'follow',
1431
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
1432
+ redirect: 'error',
1284
1433
  }, timeoutMs);
1285
1434
  }
1286
1435
  if (response.status === 404) {
@@ -1310,7 +1459,7 @@ export class AgentMdManager {
1310
1459
  const gatewayUrl = await this._resolveGateway(target);
1311
1460
  const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
1312
1461
  method: 'HEAD',
1313
- headers: { Accept: 'text/markdown' },
1462
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
1314
1463
  }, HEAD_HTTP_TIMEOUT_MS);
1315
1464
  const etag = headerValue(response.headers, 'ETag');
1316
1465
  const lastModified = headerValue(response.headers, 'Last-Modified');
@@ -1405,14 +1554,22 @@ export class AgentMdManager {
1405
1554
  payload.aid = this._safeAid(aid);
1406
1555
  await this._writeStorage(this._metaKey(aid), `${JSON.stringify(payload, null, 2)}\n`, root);
1407
1556
  }
1408
- async _readRecordUnlocked(aid, root = this.root) {
1409
- const raw = await this._readStorage(this._metaKey(aid), root);
1410
- if (raw === null)
1411
- return {};
1557
+ async _readRecordUnlocked(aid, root = this.root, strict = false) {
1412
1558
  try {
1413
- return this._normalizeRecord(aid, JSON.parse(raw));
1559
+ const raw = await this._readStorage(this._metaKey(aid), root);
1560
+ if (raw === null)
1561
+ return {};
1562
+ const parsed = JSON.parse(raw);
1563
+ if (strict && !isRecord(parsed))
1564
+ throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
1565
+ return this._normalizeRecord(aid, parsed);
1414
1566
  }
1415
1567
  catch (err) {
1568
+ if (strict) {
1569
+ if (err instanceof StateError)
1570
+ throw err;
1571
+ throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
1572
+ }
1416
1573
  this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
1417
1574
  return {};
1418
1575
  }