@agentunion/fastaun 0.5.19 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +176 -78
  2. package/_packed_docs/CHANGELOG.md +176 -78
  3. package/_packed_docs/INDEX.md +288 -269
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  7. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  8. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  9. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  10. 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
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  14. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
  18. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  19. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  20. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  21. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  22. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  23. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  24. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  25. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
  26. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
  27. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
  28. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
  29. package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
  30. package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
  31. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  32. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  33. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  34. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
  35. package/_packed_docs/sdk/INDEX.md +197 -167
  36. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  37. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  38. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
  39. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  40. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
  41. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
  42. 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
  43. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
  44. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  45. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. package/dist/agent-md-schema.d.ts +13 -1
  55. package/dist/agent-md-schema.js +63 -33
  56. package/dist/agent-md-schema.js.map +1 -1
  57. package/dist/agent-md.d.ts +30 -3
  58. package/dist/agent-md.js +293 -152
  59. package/dist/agent-md.js.map +1 -1
  60. package/dist/aid-store.d.ts +42 -0
  61. package/dist/aid-store.js +224 -44
  62. package/dist/aid-store.js.map +1 -1
  63. package/dist/aid.js +18 -13
  64. package/dist/aid.js.map +1 -1
  65. package/dist/auth.js +78 -2
  66. package/dist/auth.js.map +1 -1
  67. package/dist/cert-utils.d.ts +44 -1
  68. package/dist/cert-utils.js +157 -56
  69. package/dist/cert-utils.js.map +1 -1
  70. package/dist/client/delivery.d.ts +68 -1
  71. package/dist/client/delivery.js +1196 -135
  72. package/dist/client/delivery.js.map +1 -1
  73. package/dist/client/rpc-pipeline.d.ts +5 -1
  74. package/dist/client/rpc-pipeline.js +240 -111
  75. package/dist/client/rpc-pipeline.js.map +1 -1
  76. package/dist/client/v2-e2ee.d.ts +12 -2
  77. package/dist/client/v2-e2ee.js +842 -379
  78. package/dist/client/v2-e2ee.js.map +1 -1
  79. package/dist/client.d.ts +26 -2
  80. package/dist/client.js +420 -75
  81. package/dist/client.js.map +1 -1
  82. package/dist/config.d.ts +4 -0
  83. package/dist/config.js +1 -0
  84. package/dist/config.js.map +1 -1
  85. package/dist/events.d.ts +23 -4
  86. package/dist/events.js +171 -15
  87. package/dist/events.js.map +1 -1
  88. package/dist/facades.d.ts +43 -1
  89. package/dist/facades.js +368 -12
  90. package/dist/facades.js.map +1 -1
  91. package/dist/index.d.ts +7 -4
  92. package/dist/index.js +1 -1
  93. package/dist/index.js.map +1 -1
  94. package/dist/keystore/aid-db.d.ts +7 -2
  95. package/dist/keystore/aid-db.js +194 -43
  96. package/dist/keystore/aid-db.js.map +1 -1
  97. package/dist/keystore/index.d.ts +15 -1
  98. package/dist/keystore/local-token-store.d.ts +10 -2
  99. package/dist/keystore/local-token-store.js +81 -16
  100. package/dist/keystore/local-token-store.js.map +1 -1
  101. package/dist/org-protocol.d.ts +45 -0
  102. package/dist/org-protocol.js +829 -22
  103. package/dist/org-protocol.js.map +1 -1
  104. package/dist/ownership-v21.d.ts +40 -9
  105. package/dist/ownership-v21.js +174 -79
  106. package/dist/ownership-v21.js.map +1 -1
  107. package/dist/ownership-workflow.d.ts +22 -0
  108. package/dist/ownership-workflow.js +861 -66
  109. package/dist/ownership-workflow.js.map +1 -1
  110. package/dist/secret-store/file-store.js +84 -0
  111. package/dist/secret-store/file-store.js.map +1 -1
  112. package/dist/seq-tracker.d.ts +1 -0
  113. package/dist/seq-tracker.js +2 -2
  114. package/dist/seq-tracker.js.map +1 -1
  115. package/dist/tools/cross-sdk-agent.d.ts +67 -3
  116. package/dist/tools/cross-sdk-agent.js +575 -42
  117. package/dist/tools/cross-sdk-agent.js.map +1 -1
  118. package/dist/transport.d.ts +4 -0
  119. package/dist/transport.js +108 -44
  120. package/dist/transport.js.map +1 -1
  121. package/dist/types.d.ts +1 -0
  122. package/dist/v2/e2ee/encrypt-group.js +1 -0
  123. package/dist/v2/e2ee/encrypt-group.js.map +1 -1
  124. package/dist/v2/e2ee/encrypt-p2p.js +1 -0
  125. package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
  126. package/dist/v2/e2ee/types.d.ts +1 -0
  127. package/dist/version.d.ts +1 -1
  128. package/dist/version.js +1 -1
  129. package/package.json +1 -1
package/dist/agent-md.js CHANGED
@@ -3,15 +3,26 @@ 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 fields = relationshipFields(meta);
24
+ return Object.keys(fields).length ? fields : undefined;
25
+ }
15
26
  const AGENT_MD_NEGATIVE_CACHE_TTL_MS = 60_000;
16
27
  export function buildOwnershipReleaseAgentMd(content, release) {
17
28
  return updateOwnershipAgentMd(content, { ownership_release: release });
@@ -21,7 +32,7 @@ export function buildOwnershipAgentMd(content, owner, proof) {
21
32
  }
22
33
  export function updateOwnershipAgentMd(content, fields) {
23
34
  const payload = parseAgentMdTailSignature(String(content ?? '')).payload;
24
- const match = /^---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---([\s\S]*)$/.exec(payload);
35
+ const match = /^\ufeff?---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---([\s\S]*)$/.exec(payload);
25
36
  if (!match)
26
37
  throw new ValidationError('agent.md must start with YAML frontmatter');
27
38
  const document = parseDocument(match[1], { uniqueKeys: true });
@@ -37,7 +48,7 @@ export function updateOwnershipAgentMd(content, fields) {
37
48
  for (const [name, value] of Object.entries(fields))
38
49
  document.set(name, value);
39
50
  const yamlText = document.toString().replace(/\r\n/g, '\n');
40
- const updated = `---\n${yamlText.endsWith('\n') ? yamlText : `${yamlText}\n`}---${match[2]}`;
51
+ const updated = `${payload.startsWith('\ufeff') ? '\ufeff' : ''}---\n${yamlText.endsWith('\n') ? yamlText : `${yamlText}\n`}---${match[2]}`;
41
52
  validateAgentMdDocument(updated, { expectedAid: String(frontmatter.aid ?? '') });
42
53
  return updated;
43
54
  }
@@ -251,12 +262,12 @@ export class AgentMdManager {
251
262
  this._atomicWriteText(filePath, String(content ?? ''));
252
263
  return filePath;
253
264
  }
254
- loadRecord(aid) {
265
+ _loadRecord(aid, strict = false) {
255
266
  const target = String(aid ?? '').trim();
256
267
  if (!target)
257
268
  return null;
258
269
  try {
259
- const record = this._withRecordLock(target, () => this._readRecordUnlocked(target));
270
+ const record = this._withRecordLock(target, () => this._readRecordUnlocked(target, strict));
260
271
  const loaded = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
261
272
  try {
262
273
  const content = this.readContent(target);
@@ -270,7 +281,7 @@ export class AgentMdManager {
270
281
  }
271
282
  if (Object.keys(loaded).length <= 1)
272
283
  return null;
273
- if (Object.keys(record).length === 0 && typeof loaded.content === 'string') {
284
+ if (Object.keys(record).length === 0 && typeof loaded.content === 'string' && !fs.existsSync(this.metaPath(target))) {
274
285
  this._withRecordLock(target, () => this._writeRecordUnlocked(target, {
275
286
  aid: target,
276
287
  local_etag: loaded.local_etag,
@@ -281,11 +292,16 @@ export class AgentMdManager {
281
292
  return { ...loaded };
282
293
  }
283
294
  catch (err) {
295
+ if (strict)
296
+ throw err;
284
297
  this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
285
298
  return null;
286
299
  }
287
300
  }
288
- saveRecord(aid, fields) {
301
+ loadRecord(aid) {
302
+ return this._loadRecord(aid);
303
+ }
304
+ _saveRecord(aid, fields, strict = false) {
289
305
  const target = String(aid ?? '').trim();
290
306
  if (!target)
291
307
  return {};
@@ -294,6 +310,7 @@ export class AgentMdManager {
294
310
  const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
295
311
  let savedTo = '';
296
312
  const record = this._withRecordLock(target, () => {
313
+ const next = { ...this._readRecordUnlocked(target, true), aid: target };
297
314
  if (hasContent) {
298
315
  const content = String(inputFields.content ?? '');
299
316
  savedTo = this.writeContent(target, content);
@@ -303,7 +320,6 @@ export class AgentMdManager {
303
320
  inputFields.fetched_at = Date.now();
304
321
  }
305
322
  delete inputFields.content;
306
- const next = { ...this._readRecordUnlocked(target), aid: target };
307
323
  for (const [key, value] of Object.entries(inputFields)) {
308
324
  if (value !== undefined && value !== null)
309
325
  next[key] = value;
@@ -319,7 +335,7 @@ export class AgentMdManager {
319
335
  loaded.saved_to = savedTo;
320
336
  }
321
337
  else {
322
- const current = this.loadRecord(target);
338
+ const current = this._loadRecord(target, strict);
323
339
  if (current && typeof current.content === 'string')
324
340
  loaded.content = current.content;
325
341
  }
@@ -327,11 +343,27 @@ export class AgentMdManager {
327
343
  return { ...loaded };
328
344
  }
329
345
  catch (err) {
346
+ if (strict)
347
+ throw err;
330
348
  this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
331
349
  return {};
332
350
  }
333
351
  }
334
- async upload(content) {
352
+ saveRecord(aid, fields) {
353
+ return this._saveRecord(aid, fields, Object.prototype.hasOwnProperty.call(fields, 'local_draft_content'));
354
+ }
355
+ _clearConfirmedDraft(aid, content) {
356
+ this._withRecordLock(aid, () => {
357
+ const record = this._readRecordUnlocked(aid, true);
358
+ if (record.local_draft_content !== content)
359
+ return;
360
+ record.local_draft_content = '';
361
+ record.updated_at = Date.now();
362
+ this._writeRecordUnlocked(aid, record);
363
+ });
364
+ this._cache.delete(aid);
365
+ }
366
+ async upload(content, originalCondition) {
335
367
  const target = this._ownerAid();
336
368
  if (!target)
337
369
  throw new ValidationError('uploadAgentMd requires local AID');
@@ -339,28 +371,57 @@ export class AgentMdManager {
339
371
  if (!current?.isPrivateKeyValid()) {
340
372
  throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
341
373
  }
342
- const rawContent = content === undefined || content === null ? this.readContent(target) : String(content);
374
+ const rawContent = content === undefined || content === null ? String(this._loadRecord(target, true)?.local_draft_content ?? '') || this.readContent(target) : String(content);
343
375
  if (!rawContent.trim())
344
376
  throw new ValidationError('uploadAgentMd requires non-empty content');
345
- const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
377
+ const parsed = validateAgentMdDocument(rawContent, { expectedAid: target, validateOwnership: false });
346
378
  if (['group', 'org'].includes(String(parsed.frontmatter.type)))
347
379
  return await this.uploadAs(target, rawContent, current);
380
+ if (parseAgentMdTailSignature(rawContent).signatures.length > 1) {
381
+ await this._verifyDocument(rawContent, target, current, true, true);
382
+ return await this._uploadPrepared(target, rawContent, current, originalCondition);
383
+ }
348
384
  const signed = current.signAgentMd(rawContent);
349
385
  if (!signed.ok || !signed.data) {
350
386
  const message = signed.error?.message ?? 'agent.md signing failed';
351
387
  throw new ClientSignatureError(message);
352
388
  }
353
389
  const signedContent = signed.data.signed;
354
- verifyAgentMdUpload(signedContent, current.certPem, target);
355
- return await this._uploadPrepared(target, signedContent, current);
390
+ verifyAgentMdBodyUpdate(signedContent, current.certPem, target);
391
+ return await this._uploadPrepared(target, signedContent, current, originalCondition);
392
+ }
393
+ async readAgentMdFresh(aid) {
394
+ const target = this.safeAid(aid), gateway = await this._resolveGateway(target);
395
+ const response = await requestText(this._url(target, gateway, this._uploadViaGatewayHost), {
396
+ method: 'GET', verifySsl: this._verifySsl,
397
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', ...(this._uploadViaGatewayHost ? { Host: target } : {}) },
398
+ timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true,
399
+ });
400
+ if (response.status === 404)
401
+ return { exists: false, content: '', etag: '' };
402
+ if (response.status !== 200)
403
+ throw ownershipFailure('unavailable', `agent.md fresh read failed: HTTP ${response.status}`, true);
404
+ const etag = headerValue(response.headers, 'ETag');
405
+ if (!etag)
406
+ throw ownershipFailure('unavailable', 'agent.md fresh read has no ETag', true);
407
+ return { exists: true, content: response.text, etag };
356
408
  }
357
409
  async readOwnershipFresh(aid) {
358
410
  const target = this.safeAid(aid);
359
411
  const response = await this._ownershipRequest(target, 'GET');
412
+ if ([200, 400, 404].includes(response.status) && headerValue(response.headers, 'X-AUN-Ownership-Status') === 'not_applicable')
413
+ throw ownershipFailure('not_applicable', 'group/org uses its lifecycle owner relationship');
414
+ if (response.status === 400) {
415
+ let value;
416
+ try {
417
+ value = JSON.parse(response.text);
418
+ }
419
+ catch { }
420
+ if (isJsonObject(value) && value.error === 'invalid')
421
+ throw ownershipFailure('invalid', 'NS rejected the current agent.md: HTTP 400');
422
+ }
360
423
  if (response.status !== 200 && response.status !== 404)
361
424
  throw ownershipFailure('unavailable', `NS fresh read failed: HTTP ${response.status}`, true);
362
- if (headerValue(response.headers, 'X-AUN-Ownership-Status') === 'not_applicable')
363
- throw ownershipFailure('not_applicable', 'group/org uses its lifecycle owner relationship');
364
425
  let type;
365
426
  if (response.status === 200) {
366
427
  try {
@@ -380,7 +441,7 @@ export class AgentMdManager {
380
441
  throw ownershipFailure('unavailable', 'NS fresh metadata is incomplete', true);
381
442
  return { sub_aid: target, exists: response.status === 200, content: response.status === 200 ? response.text : '', etag, protocol, relation_version: Number(version), lock: lock, observed_at: Math.floor(Date.now() / 1000) };
382
443
  }
383
- async submitOwnershipRaw(aid, raw, condition) {
444
+ async submitOwnershipRaw(aid, raw, condition, assertActive) {
384
445
  const target = this.safeAid(aid), current = this._currentAid();
385
446
  if (!current?.isPrivateKeyValid() || current.aid !== target || this._ownerAid() !== target)
386
447
  throw ownershipFailure('forbidden', 'raw agent.md submission requires its own authenticated identity');
@@ -388,24 +449,25 @@ export class AgentMdManager {
388
449
  throw new ValidationError('raw agent.md requires one original condition');
389
450
  parseOwnershipFile(raw, 'agent', { expectedSubAid: target });
390
451
  verifyAgentMdUpload(raw, current.certPem, target);
391
- const response = await this._ownershipRequest(target, 'PUT', raw, condition);
452
+ const response = await this._ownershipRequest(target, 'PUT', raw, condition, false, assertActive);
392
453
  if (response.status === 409 || response.status === 412)
393
454
  throw ownershipFailure('conflict', `NS original condition failed: HTTP ${response.status}`);
394
455
  if (response.status === 428)
395
456
  throw ownershipFailure('unsupported_protocol', 'NS does not support the required original document condition');
396
457
  if (response.status === 401 || response.status === 403)
397
458
  throw ownershipFailure('forbidden', `NS self submission denied: HTTP ${response.status}`);
459
+ if (response.status === 400)
460
+ throw ownershipFailure('invalid', 'NS rejected the submitted agent.md: HTTP 400');
398
461
  if (response.status < 200 || response.status >= 300)
399
462
  throw ownershipFailure('unavailable', `NS raw submission failed: HTTP ${response.status}`, true);
400
463
  const fresh = await this.readOwnershipFresh(target);
401
464
  if (fresh.content !== raw)
402
465
  throw ownershipFailure('unavailable', 'NS raw submission cannot be confirmed from the current document', true);
403
- 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
466
  return fresh;
405
467
  }
406
- async _ownershipRequest(target, method, raw, condition) {
468
+ async _ownershipRequest(target, method, raw, condition, legacyRead = false, assertActive) {
407
469
  const gatewayUrl = await this._resolveGateway(target);
408
- const headers = { Accept: 'text/markdown', 'Cache-Control': 'no-cache', 'X-AUN-Ownership-Check': '1' };
470
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', ...(!legacyRead ? { 'X-AUN-Ownership-Check': '1' } : {}) };
409
471
  if (method === 'PUT') {
410
472
  headers['Content-Type'] = 'text/markdown; charset=utf-8';
411
473
  headers.Authorization = `Bearer ${await this._uploadAccessToken(target, gatewayUrl)}`;
@@ -417,11 +479,13 @@ export class AgentMdManager {
417
479
  const viaGateway = this._uploadViaGatewayHost;
418
480
  if (viaGateway)
419
481
  headers.Host = target;
420
- const request = (url) => requestText(url, { method, verifySsl: this._verifySsl, headers, body: raw, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true });
421
- let response = await request(this._url(target, gatewayUrl, viaGateway));
482
+ const request = (url) => { if (method === 'PUT')
483
+ assertActive?.(); return requestText(url, { method, verifySsl: this._verifySsl, headers, body: raw, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true }); };
484
+ const primary = this._url(target, gatewayUrl, viaGateway);
485
+ let response = await request(primary);
422
486
  if (response.status === 404 && !headerValue(response.headers, 'X-AUN-Ownership-Protocol') && headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable') {
423
487
  const fallback = nameServiceAgentMdUrl(target, gatewayUrl);
424
- if (fallback && fallback !== this._url(target, gatewayUrl, viaGateway)) {
488
+ if (fallback && fallback !== primary) {
425
489
  headers.Host = target;
426
490
  response = await request(fallback);
427
491
  }
@@ -433,31 +497,42 @@ export class AgentMdManager {
433
497
  const rawContent = String(content ?? '');
434
498
  if (!rawContent.trim())
435
499
  throw new ValidationError('uploadAgentMd requires non-empty content');
436
- const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
500
+ const parsed = validateAgentMdDocument(rawContent, { expectedAid: target, validateOwnership: false });
437
501
  if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
438
502
  const identity = await this._entitySigningIdentity(target, parsed.frontmatter, parsed.signature, signer);
439
- verifyAgentMdUpload(rawContent, identity.certPem, target);
503
+ await this._verifyDocument(rawContent, target, identity, true);
440
504
  return await this._uploadPrepared(target, rawContent, identity);
441
505
  }
506
+ if (['group', 'org'].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
507
+ const identity = await this._entityIdentityGetter?.(target, String(parsed.frontmatter.type));
508
+ if (!identity?.isPrivateKeyValid())
509
+ throw new StateError('entity agent.md signing requires its local private key');
510
+ signer = identity;
511
+ }
442
512
  if (!signer?.isPrivateKeyValid())
443
513
  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);
514
+ if (parseAgentMdTailSignature(rawContent).signatures.length > 1) {
515
+ await this._verifyDocument(rawContent, target, signer, true, true);
516
+ return await this._uploadPrepared(target, rawContent, signer);
517
+ }
446
518
  const signed = signer.signAgentMd(rawContent);
447
519
  if (!signed.ok || !signed.data) {
448
520
  const message = signed.error?.message ?? 'agent.md signing failed';
449
521
  throw new ClientSignatureError(message);
450
522
  }
451
523
  const signedContent = signed.data.signed;
452
- verifyAgentMdUpload(signedContent, signer.certPem, target);
524
+ verifyAgentMdBodyUpdate(signedContent, signer.certPem, target);
453
525
  return await this._uploadPrepared(target, signedContent, signer);
454
526
  }
455
- pendingUpload(aid) {
456
- const record = this.loadRecord(aid);
527
+ _pendingUpload(aid, strict = false) {
528
+ const record = this._loadRecord(aid, strict);
457
529
  if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
458
530
  return null;
459
531
  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
532
  }
533
+ pendingUpload(aid) {
534
+ return this._pendingUpload(aid, true);
535
+ }
461
536
  async resumePendingUpload(aid, signer) {
462
537
  const pending = this.pendingUpload(aid);
463
538
  if (!pending)
@@ -467,9 +542,32 @@ export class AgentMdManager {
467
542
  throw ownershipFailure('persistence_unavailable', 'pending agent.md requires its original document condition');
468
543
  const parsed = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
469
544
  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
545
  try {
546
+ if (pending.relation?.legacy === true)
547
+ return await this._resumeLegacyBodyUpload(aid, pending, signer ?? this._currentAid());
548
+ if (legacyRelationship(meta) && pending.relation && Object.keys(pending.relation).length === 4 && ['relation_version', 'lock', 'owner_aid', 'statement'].every((field) => Object.hasOwn(pending.relation, field))) {
549
+ const relation = pending.relation, statement = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid }).statement;
550
+ if (!statement || !isDeepStrictEqual(statement, relation.statement) || statement.relation_version !== relation.relation_version || statement.owner_aid !== relation.owner_aid || !['locked', 'unlocked'].includes(String(relation.lock)) || pending.content_etag !== AgentMdManager.contentEtag(pending.content))
551
+ throw ownershipFailure('persistence_unavailable', 'pending original relationship context is corrupt');
552
+ const identity = signer ?? this._currentAid();
553
+ if (!identity || identity.aid !== aid || this._currentAid()?.aid !== aid || this._ownerAid() !== aid)
554
+ throw ownershipFailure('forbidden', 'body recovery requires its current subject identity');
555
+ await this._verifyDocument(pending.content, aid, identity, true, true);
556
+ const remote = await this.readAgentMdFresh(aid);
557
+ if (!remote.exists || !isDeepStrictEqual(legacyRelationship(meta), legacyRelationship(validateAgentMdReadDocument(remote.content, { expectedAid: aid }).frontmatter)))
558
+ throw ownershipFailure('conflict', 'pending agent.md relationship material changed');
559
+ if (remote.etag === pending.content_etag && remote.content === pending.content) {
560
+ 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: '' });
561
+ this._clearConfirmedDraft(aid, pending.content);
562
+ return true;
563
+ }
564
+ if (!condition.if_match || remote.etag !== condition.if_match)
565
+ throw ownershipFailure('conflict', 'pending agent.md original document condition changed');
566
+ await this._uploadPrepared(aid, pending.content, identity, condition);
567
+ return true;
568
+ }
569
+ if (legacyRelationship(meta))
570
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership pending agent.md requires its original relationship snapshot');
473
571
  if (['group', 'org'].includes(String(meta.type))) {
474
572
  if (!parsed.signature)
475
573
  throw ownershipFailure('invalid', 'pending entity agent.md requires a signature');
@@ -483,13 +581,13 @@ export class AgentMdManager {
483
581
  throw ownershipFailure('conflict', 'pending entity upload belongs to an old rekey generation');
484
582
  if (pending.content_etag !== AgentMdManager.contentEtag(pending.content))
485
583
  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));
584
+ await this._verifyDocument(pending.content, aid, identity, true);
490
585
  const remote = await this._readEntityFresh(aid);
491
586
  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: '' });
587
+ 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: '' });
588
+ if (!saved.aid)
589
+ throw new StateError('agent.md pending completion persistence failed');
590
+ this._clearConfirmedDraft(aid, pending.content);
493
591
  return true;
494
592
  }
495
593
  if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
@@ -497,38 +595,31 @@ export class AgentMdManager {
497
595
  await this._uploadPrepared(aid, pending.content, identity, condition);
498
596
  return true;
499
597
  }
500
- const remote = await this.readOwnershipFresh(aid);
501
- const fail = (code, reason, retryable = false) => ownershipFailure(code, reason, retryable);
502
- const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
503
- const relation = pending.relation && Object.keys(pending.relation).length ? pending.relation : undefined;
504
- const sameStatement = (first, second) => first == null && second == null || first !== null && second !== null && typeof first === 'object' && typeof second === 'object' && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((field) => first[field] === second[field]);
505
- if (candidate.statement && !relation)
506
- throw fail('persistence_unavailable', 'pending ownership body update requires its original relationship context');
507
- if (relation) {
508
- if (remote.relation_version !== relation.relation_version || remote.lock !== relation.lock)
509
- throw fail('conflict', 'pending agent.md original relationship changed');
510
- if (!sameStatement(candidate.statement, relation.statement))
511
- throw fail('unsupported_protocol', 'ownership changes require their persisted 2.1 task');
512
- if (remote.exists && (remote.relation_version > 0 || candidate.statement)) {
513
- const current = parseOwnershipFile(remote.content, 'agent', { expectedSubAid: aid });
514
- if (!sameStatement(current.statement, relation.statement) || (remote.owner_aid ?? current.statement?.owner_aid ?? null) !== relation.owner_aid)
515
- throw fail('conflict', 'pending agent.md relationship statement changed');
516
- }
598
+ const multipleSignatures = parseAgentMdTailSignature(pending.content).signatures.length > 1;
599
+ const identity = signer ?? this._currentAid();
600
+ if (multipleSignatures) {
601
+ if (!identity)
602
+ throw new StateError('pending agent.md requires a signer identity');
603
+ await this._verifyDocument(pending.content, aid, identity, true, true);
517
604
  }
518
- if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
519
- throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
605
+ if (pending.content_etag !== AgentMdManager.contentEtag(pending.content))
606
+ throw ownershipFailure('persistence_unavailable', 'pending agent.md original bytes are corrupt');
607
+ if (!identity)
608
+ throw new StateError('pending agent.md requires a signer identity');
609
+ if (!multipleSignatures)
610
+ verifyAgentMdBodyUpdate(pending.content, identity.certPem, aid);
611
+ const remote = await this.readAgentMdFresh(aid);
520
612
  if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
521
613
  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
614
  if (!saved.aid)
523
615
  throw new StateError('agent.md pending completion persistence failed');
616
+ this._clearConfirmedDraft(aid, pending.content);
524
617
  return true;
525
618
  }
526
619
  if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
527
620
  throw ownershipFailure('conflict', 'pending agent.md original document condition changed');
528
- const identity = signer ?? this._currentAid();
529
- if (!identity)
530
- throw new StateError('pending agent.md requires a signer identity');
531
- verifyAgentMdUpload(pending.content, identity.certPem, aid);
621
+ if (remote.exists && legacyRelationship(validateAgentMdReadDocument(remote.content, { expectedAid: aid }).frontmatter))
622
+ throw ownershipFailure('conflict', 'legacy relationship material cannot be removed or replaced');
532
623
  await this._uploadPrepared(aid, pending.content, identity, condition);
533
624
  return true;
534
625
  }
@@ -538,32 +629,38 @@ export class AgentMdManager {
538
629
  }
539
630
  }
540
631
  async _uploadPrepared(target, signedContent, signer, originalCondition) {
541
- const meta = validateAgentMdDocument(signedContent, { expectedAid: target }).frontmatter;
632
+ const meta = validateAgentMdDocument(signedContent, { expectedAid: target, validateOwnership: false }).frontmatter;
542
633
  const entity = ['group', 'org'].includes(String(meta.type));
634
+ const legacy = legacyRelationship(meta);
635
+ const draftStrict = !originalCondition || !this._pendingUpload(target, true);
636
+ if (legacy && (signer.aid !== target || this._currentAid()?.aid !== target || this._ownerAid() !== target))
637
+ throw ownershipFailure('forbidden', 'legacy relationship body updates require current subject identity');
638
+ if (draftStrict && this._saveRecord(target, { local_draft_content: signedContent }, true).local_draft_content !== signedContent)
639
+ throw new StateError('agent.md local draft persistence failed');
543
640
  const entityContext = entity ? await this._entityContext(target, meta, signer) : undefined;
544
641
  let condition = originalCondition;
545
- let relation = entityContext ?? (this.pendingUpload(target))?.relation;
642
+ let relation = entityContext ?? (this._pendingUpload(target, draftStrict))?.relation;
546
643
  if (!condition) {
547
- const fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
644
+ const fresh = entity ? await this._readEntityFresh(target) : legacy ? await this._readLegacyBodyBase(target) : await this.readAgentMdFresh(target);
548
645
  condition = fresh.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
549
646
  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 };
647
+ if (legacy) {
648
+ if (!fresh.exists || !fresh.etag || !isDeepStrictEqual(legacy, legacyRelationship(validateAgentMdReadDocument(fresh.content, { expectedAid: target }).frontmatter)))
649
+ throw ownershipFailure('conflict', 'legacy relationship material must remain unchanged');
650
+ relation = { legacy: true, base_content: fresh.content, base_content_etag: AgentMdManager.contentEtag(fresh.content), base_etag: fresh.etag, fields: legacy };
651
+ }
652
+ else {
653
+ if (fresh.exists && legacyRelationship(validateAgentMdReadDocument(fresh.content, { expectedAid: target }).frontmatter))
654
+ throw ownershipFailure('conflict', 'legacy relationship material cannot be removed or replaced');
655
+ relation = undefined;
656
+ }
554
657
  }
555
658
  }
556
659
  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 });
660
+ const entityAuthorization = '';
661
+ 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
662
  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)
663
+ 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
664
  throw new StateError('agent.md pending upload persistence failed');
568
665
  try {
569
666
  const owner = this._currentAid();
@@ -573,24 +670,71 @@ export class AgentMdManager {
573
670
  const authorization = entity ? entityAuthorization : delegated ? this._ownerUploadAuthorization(target, signedContent, owner) : '';
574
671
  const result = await this._uploadHttp(target, signedContent, authorization, condition);
575
672
  const remoteEtag = String(result.etag ?? '').trim();
576
- const saved = this.saveRecord(target, {
673
+ const saved = this._saveRecord(target, {
577
674
  content: signedContent, local_etag: AgentMdManager.contentEtag(signedContent), remote_etag: remoteEtag || undefined,
578
675
  last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
579
676
  remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
580
677
  pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
581
- });
678
+ }, draftStrict);
582
679
  if (!saved.aid)
583
680
  throw new StateError('agent.md upload completion persistence failed');
681
+ if (remoteEtag === AgentMdManager.contentEtag(signedContent))
682
+ this._clearConfirmedDraft(target, signedContent);
584
683
  return { ...result };
585
684
  }
586
685
  catch (error) {
587
- this.saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) });
686
+ this._saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) }, draftStrict);
588
687
  throw error;
589
688
  }
590
689
  }
690
+ async _verifyLegacyBodyContent(target, content) {
691
+ const parsed = validateAgentMdReadDocument(content, { expectedAid: target, requireSignature: true });
692
+ if (!legacyRelationship(parsed.frontmatter))
693
+ throw ownershipFailure('unsupported_protocol', 'stored agent.md does not contain a legacy relationship');
694
+ const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
695
+ const verified = await this._verifyDocument(content, target, peer);
696
+ if (verified.status !== 'verified')
697
+ throw ownershipFailure('invalid', 'stored legacy agent.md signature is invalid');
698
+ return parsed;
699
+ }
700
+ async _readLegacyBodyBase(target) {
701
+ const response = await this.readAgentMdFresh(target);
702
+ if (response.exists)
703
+ await this._verifyLegacyBodyContent(target, response.content);
704
+ return response;
705
+ }
706
+ async _resumeLegacyBodyUpload(target, pending, identity) {
707
+ const relation = pending.relation;
708
+ if (!identity || identity.aid !== target || this._currentAid()?.aid !== target || this._ownerAid() !== target)
709
+ throw ownershipFailure('forbidden', 'legacy relationship recovery requires current subject identity');
710
+ 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))
711
+ throw ownershipFailure('persistence_unavailable', 'pending legacy relationship original bytes or condition are missing');
712
+ if (parseAgentMdTailSignature(pending.content).signatures.length > 1)
713
+ await this._verifyDocument(pending.content, target, identity, true, true);
714
+ else
715
+ verifyAgentMdBodyUpdate(pending.content, identity.certPem, target);
716
+ const base = await this._verifyLegacyBodyContent(target, relation.base_content);
717
+ const candidate = validateAgentMdDocument(pending.content, { expectedAid: target, validateOwnership: false });
718
+ if (!isDeepStrictEqual(legacyRelationship(base.frontmatter), relation.fields) || !isDeepStrictEqual(relationshipFields(candidate.frontmatter), relation.fields))
719
+ throw ownershipFailure('persistence_unavailable', 'pending legacy relationship material changed');
720
+ const remote = await this._readLegacyBodyBase(target);
721
+ if (!remote.exists)
722
+ throw ownershipFailure('conflict', 'pending legacy agent.md original document is missing');
723
+ if (remote.content === pending.content && remote.etag === pending.content_etag) {
724
+ 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: '' });
725
+ if (!saved.aid)
726
+ throw new StateError('agent.md pending completion persistence failed');
727
+ this._clearConfirmedDraft(target, pending.content);
728
+ return true;
729
+ }
730
+ if (remote.content !== relation.base_content || remote.etag !== relation.base_etag)
731
+ throw ownershipFailure('conflict', 'pending legacy agent.md original document changed');
732
+ await this._uploadPrepared(target, pending.content, identity, pending.condition);
733
+ return true;
734
+ }
591
735
  async _readEntityFresh(target) {
592
736
  const gateway = await this._resolveGateway(target);
593
- const headers = { 'Cache-Control': 'no-cache', ...(this._uploadViaGatewayHost ? { Host: target } : {}) };
737
+ const headers = { 'X-AUN-Ownership-Check': '1', 'X-AUN-Signature-Format': 'multi-v1', 'Cache-Control': 'no-cache', ...(this._uploadViaGatewayHost ? { Host: target } : {}) };
594
738
  const request = (url) => requestText(url, { method: 'GET', verifySsl: this._verifySsl, headers, timeoutMs: DEFAULT_HTTP_TIMEOUT_MS, redirectsLeft: 0, strictUtf8: true });
595
739
  const response = await request(this._url(target, gateway, this._uploadViaGatewayHost));
596
740
  if (response.status !== 200 && response.status !== 404)
@@ -604,19 +748,23 @@ export class AgentMdManager {
604
748
  }
605
749
  async readEntityAuthorityFresh(target) {
606
750
  const observed = await this._readEntityFresh(this.safeAid(target));
607
- if (!observed.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
751
+ try {
752
+ ownershipAid(observed.owner_aid, 'owner_aid');
753
+ }
754
+ catch {
755
+ throw ownershipFailure('unavailable', 'entity owner authority metadata is unavailable', true);
756
+ }
757
+ if (!/^sha256:[0-9a-f]{64}$/.test(observed.entity_cert_fingerprint))
608
758
  throw ownershipFailure('unavailable', 'entity owner authority metadata is unavailable', true);
609
759
  return { owner_aid: observed.owner_aid, entity_cert_fingerprint: observed.entity_cert_fingerprint };
610
760
  }
611
761
  async _entityContext(target, meta, signer) {
612
762
  const owner = String(meta.owner ?? '').trim().toLowerCase();
613
763
  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');
764
+ if (signer.aid.toLowerCase() !== target.toLowerCase() || !uploader || uploader.aid.toLowerCase() !== owner || uploader.aid.toLowerCase() !== this._ownerAid().toLowerCase())
765
+ throw ownershipFailure('forbidden', 'group/org agent.md requires current owner identity');
616
766
  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)
767
+ if (signer.certFingerprint !== context.entity_cert_fingerprint)
620
768
  throw ownershipFailure('conflict', 'entity signing certificate changed');
621
769
  if (uploader.aid.toLowerCase() === target.toLowerCase()) {
622
770
  if (uploader.certFingerprint !== context.entity_cert_fingerprint)
@@ -636,6 +784,25 @@ export class AgentMdManager {
636
784
  throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
637
785
  return await this._entitySignerResolver(signer, signature.cert_fingerprint);
638
786
  }
787
+ async _verifyDocument(content, target, identity, current = false, bodyUpdate = false) {
788
+ const certificates = new Map();
789
+ const parsed = parseAgentMdTailSignature(content, !current);
790
+ const meta = validateAgentMdReadDocument(content, { expectedAid: target }).frontmatter;
791
+ const historicalEntityOwner = !current && ['group', 'org'].includes(String(meta.type)) && parsed.signatures.length === 1 && parsed.fields?.signer_aid !== undefined && parsed.fields.signer_aid === meta.owner && parsed.fields.signer_aid !== meta.aid;
792
+ 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));
793
+ for (const item of parsed.signatures.slice(1)) {
794
+ const fields = item.fields;
795
+ 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));
796
+ certificates.set(`${fields.signer_aid};${fields.cert_fingerprint}`, peer.certPem);
797
+ }
798
+ const resolver = (aid, fingerprint) => certificates.get(`${aid};${fingerprint}`);
799
+ if (current)
800
+ (bodyUpdate ? verifyAgentMdBodyUpdate : verifyAgentMdUpload)(content, identity.certPem, target, undefined, resolver);
801
+ const result = verifyDocument(content, { expectedAid: historicalEntityOwner ? String(meta.owner) : target, primaryCertPem: identity.certPem, requireCurrentValidity: current, readCompatibility, certificateResolver: resolver });
802
+ if (result.status === 'invalid')
803
+ throw ownershipFailure('invalid', result.reason ?? 'document signature verification failed');
804
+ return result;
805
+ }
639
806
  async _verifyEntityOwnerSigner(owner) {
640
807
  if (!this._entitySignerResolver)
641
808
  throw ownershipFailure('unavailable', 'entity owner signing certificate resolver is unavailable', true);
@@ -663,52 +830,21 @@ export class AgentMdManager {
663
830
  throw ownershipFailure('conflict', 'entity agent.md certificate generation changed');
664
831
  return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
665
832
  }
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 = '') {
833
+ _ownerUploadAuthorization(target, signedContent, owner) {
697
834
  const bodySha256 = crypto.createHash('sha256').update(signedContent, 'utf-8').digest('hex');
698
835
  const timestamp = Math.floor(Date.now() / 1000);
699
836
  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}`;
837
+ const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
701
838
  const signed = owner.sign(canonical);
702
839
  if (!signed.ok || !signed.data) {
703
840
  const message = signed.error?.message ?? 'owner authorization signing failed';
704
841
  throw new ClientSignatureError(message);
705
842
  }
706
843
  const payload = {
707
- version: entityFingerprint ? 2 : 1,
844
+ version: 1,
708
845
  target_aid: target.toLowerCase(),
709
846
  uploader_aid: owner.aid.toLowerCase(),
710
847
  body_sha256: bodySha256,
711
- ...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
712
848
  timestamp,
713
849
  nonce,
714
850
  cert_fingerprint: owner.certFingerprint,
@@ -721,13 +857,14 @@ export class AgentMdManager {
721
857
  throw ownershipFailure('persistence_unavailable', 'agent.md upload requires its original document condition');
722
858
  const gatewayUrl = await this._resolveGateway(target);
723
859
  const authorization = String(ownerAuthorization ?? '').trim();
724
- const entity = ['group', 'org'].includes(String(validateAgentMdDocument(signedContent, { expectedAid: target }).frontmatter.type));
860
+ const meta = validateAgentMdDocument(signedContent, { expectedAid: target, validateOwnership: false }).frontmatter;
861
+ const entity = ['group', 'org'].includes(String(meta.type));
725
862
  const uploadHeaders = {
863
+ 'X-AUN-Signature-Format': 'multi-v1',
726
864
  'Content-Type': 'text/markdown; charset=utf-8',
727
- ...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
728
865
  ...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
729
866
  ...(this._uploadViaGatewayHost ? { Host: target } : {}),
730
- ...(authorization ? { [entity ? 'AUN-AGENTMD-OWNER-AUTHORIZATION' : 'X-AUN-AgentMD-Owner-Authorization']: authorization } : {}),
867
+ ...(authorization && !entity ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
731
868
  };
732
869
  if (!authorization || entity) {
733
870
  const token = await this._uploadAccessToken(entity ? this._ownerAid() : target, gatewayUrl);
@@ -985,14 +1122,14 @@ export class AgentMdManager {
985
1122
  return String(await this._gatewayResolver?.(aid) ?? '');
986
1123
  }
987
1124
  async _resolvePeer(aid, certFingerprint, signedAt) {
988
- const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
1125
+ const expectedFp = certFingerprint ? `sha256:${normalizeFingerprintHex(certFingerprint)}` : '';
989
1126
  const current = this._currentAid();
990
- if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
991
- if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp))
1127
+ if (signedAt === undefined && current && current.aid.toLowerCase() === aid.toLowerCase()) {
1128
+ if (!expectedFp || certMatchesFingerprint(current.certPem, expectedFp) || publicKeyMatchesFingerprint(current.certPem, expectedFp))
992
1129
  return current;
993
1130
  }
994
1131
  const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
995
- if (!(peer instanceof AID)) {
1132
+ if (!(peer instanceof AID) || peer.aid.toLowerCase() !== aid.toLowerCase()) {
996
1133
  throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
997
1134
  }
998
1135
  return peer;
@@ -1016,7 +1153,7 @@ export class AgentMdManager {
1016
1153
  const response = await requestText(this._url(target, gatewayUrl), {
1017
1154
  method: 'HEAD',
1018
1155
  verifySsl: this._verifySsl,
1019
- headers: { Accept: 'text/markdown' },
1156
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
1020
1157
  timeoutMs: HEAD_HTTP_TIMEOUT_MS,
1021
1158
  });
1022
1159
  if (response.status === 404) {
@@ -1054,7 +1191,7 @@ export class AgentMdManager {
1054
1191
  this._log.debug(`downloadAgentMd enter: aid=${target}`);
1055
1192
  const gatewayUrl = await this._resolveGateway(target);
1056
1193
  const cached = this.loadRecord(target) ?? {};
1057
- const headers = { Accept: 'text/markdown' };
1194
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
1058
1195
  const etag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
1059
1196
  const lastModified = String(cached.last_modified ?? '').trim();
1060
1197
  let response = await requestText(this._url(target, gatewayUrl), {
@@ -1062,6 +1199,7 @@ export class AgentMdManager {
1062
1199
  verifySsl: this._verifySsl,
1063
1200
  headers,
1064
1201
  timeoutMs,
1202
+ redirectsLeft: 0,
1065
1203
  strictUtf8: true,
1066
1204
  });
1067
1205
  let reusedCachedNotModified = false;
@@ -1081,8 +1219,9 @@ export class AgentMdManager {
1081
1219
  response = await requestText(this._url(target, gatewayUrl), {
1082
1220
  method: 'GET',
1083
1221
  verifySsl: this._verifySsl,
1084
- headers: { Accept: 'text/markdown' },
1222
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
1085
1223
  timeoutMs,
1224
+ redirectsLeft: 0,
1086
1225
  strictUtf8: true,
1087
1226
  });
1088
1227
  }
@@ -1095,29 +1234,24 @@ export class AgentMdManager {
1095
1234
  throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
1096
1235
  }
1097
1236
  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);
1237
+ const parsed = parseAgentMdTailSignature(content, true);
1101
1238
  let entity = false;
1102
1239
  let frontmatter = {};
1103
1240
  try {
1104
- frontmatter = parseFileFrontmatter(parsed.payload);
1241
+ frontmatter = validateAgentMdReadDocument(content).frontmatter;
1105
1242
  entity = ['group', 'org'].includes(String(frontmatter.type));
1106
1243
  }
1107
1244
  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
- }
1245
+ try {
1246
+ frontmatter = validateAgentMdReadDocument(content, { expectedAid: target, requireSignature: entity }).frontmatter;
1247
+ }
1248
+ catch (error) {
1249
+ throw ownershipFailure('invalid', error instanceof Error ? error.message : String(error));
1115
1250
  }
1116
- if (entity)
1117
- await this._entityCurrentContext(target, frontmatter);
1118
1251
  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);
1252
+ const peer = await this._resolvePeer(entity ? parsed.fields.signer_aid ?? target : target, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
1253
+ const generic = await this._verifyDocument(content, target, peer);
1254
+ const verified = resultOk({ ...generic, aid: target, cert_fingerprint: parsed.fields?.cert_fingerprint, timestamp: Number(parsed.fields?.timestamp) });
1121
1255
  if (!verified.ok || !verified.data) {
1122
1256
  const message = verified.error?.message ?? 'agent.md verification failed';
1123
1257
  throw new AUNError(message);
@@ -1128,8 +1262,6 @@ export class AgentMdManager {
1128
1262
  if (statusText === 'invalid') {
1129
1263
  throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : {});
1130
1264
  }
1131
- if (!entity)
1132
- frontmatter = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
1133
1265
  let ownershipStatus;
1134
1266
  if (entity)
1135
1267
  ownershipStatus = 'not_applicable';
@@ -1180,6 +1312,7 @@ export class AgentMdManager {
1180
1312
  return {
1181
1313
  aid: target,
1182
1314
  content,
1315
+ frontmatter,
1183
1316
  verification,
1184
1317
  signature,
1185
1318
  cert_pem: peer.certPem,
@@ -1328,14 +1461,17 @@ export class AgentMdManager {
1328
1461
  payload.aid = this.safeAid(aid);
1329
1462
  this._atomicWriteText(this.metaPath(aid), `${JSON.stringify(payload, null, 2)}\n`);
1330
1463
  }
1331
- _readRecordUnlocked(aid) {
1464
+ _readRecordUnlocked(aid, strict = false) {
1332
1465
  const filePath = this.metaPath(aid);
1333
1466
  if (!fs.existsSync(filePath))
1334
1467
  return {};
1335
1468
  try {
1336
1469
  const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
1337
- if (!isJsonObject(parsed))
1470
+ if (!isJsonObject(parsed)) {
1471
+ if (strict)
1472
+ throw ownershipFailure('persistence_unavailable', `agent.md metadata is not a mapping: aid=${aid}`);
1338
1473
  return {};
1474
+ }
1339
1475
  const record = {};
1340
1476
  for (const [key, value] of Object.entries(parsed)) {
1341
1477
  if (key !== 'content')
@@ -1348,6 +1484,11 @@ export class AgentMdManager {
1348
1484
  return record;
1349
1485
  }
1350
1486
  catch (err) {
1487
+ if (strict) {
1488
+ if (err instanceof AUNError && err.stringCode === 'persistence_unavailable')
1489
+ throw err;
1490
+ throw ownershipFailure('persistence_unavailable', `agent.md metadata is damaged: aid=${aid}`);
1491
+ }
1351
1492
  this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
1352
1493
  return {};
1353
1494
  }