@agentunion/fastaun-browser 0.5.19 → 0.5.21

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