@agentunion/fastaun-browser 0.5.18 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +172 -79
  2. package/_packed_docs/CHANGELOG.md +172 -79
  3. package/_packed_docs/INDEX.md +244 -116
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +98 -69
  5. package/_packed_docs/agent.md/SCHEMA.md +245 -205
  6. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  7. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +42 -15
  8. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
  9. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +45 -134
  10. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
  11. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +5 -20
  12. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +687 -668
  13. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +19 -28
  14. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  18. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  19. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  20. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +68 -61
  21. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  22. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +279 -285
  23. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  24. package/_packed_docs/sdk/09-group-rpc-manual.md +271 -235
  25. package/_packed_docs/sdk/09-message-rpc-manual.md +47 -6
  26. package/_packed_docs/sdk/09-storage-rpc-manual.md +70 -68
  27. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +63 -34
  28. package/_packed_docs/sdk/INDEX.md +105 -34
  29. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  30. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  31. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
  32. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  33. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
  34. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
  35. 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
  36. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
  37. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  38. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. package/dist/agent-md-schema.d.ts +17 -3
  48. package/dist/agent-md-schema.d.ts.map +1 -1
  49. package/dist/agent-md-schema.js +94 -26
  50. package/dist/agent-md-schema.js.map +1 -1
  51. package/dist/agent-md.d.ts +54 -3
  52. package/dist/agent-md.d.ts.map +1 -1
  53. package/dist/agent-md.js +711 -65
  54. package/dist/agent-md.js.map +1 -1
  55. package/dist/aid-store.d.ts +58 -46
  56. package/dist/aid-store.d.ts.map +1 -1
  57. package/dist/aid-store.js +254 -248
  58. package/dist/aid-store.js.map +1 -1
  59. package/dist/aid.d.ts +4 -1
  60. package/dist/aid.d.ts.map +1 -1
  61. package/dist/aid.js +27 -33
  62. package/dist/aid.js.map +1 -1
  63. package/dist/auth.d.ts +4 -1
  64. package/dist/auth.d.ts.map +1 -1
  65. package/dist/auth.js +426 -39
  66. package/dist/auth.js.map +1 -1
  67. package/dist/bundle.js +38179 -32054
  68. package/dist/cert-utils.d.ts +50 -2
  69. package/dist/cert-utils.d.ts.map +1 -1
  70. package/dist/cert-utils.js +105 -47
  71. package/dist/cert-utils.js.map +1 -1
  72. package/dist/client/delivery.d.ts +22 -1
  73. package/dist/client/delivery.d.ts.map +1 -1
  74. package/dist/client/delivery.js +389 -78
  75. package/dist/client/delivery.js.map +1 -1
  76. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  77. package/dist/client/rpc-pipeline.js +103 -77
  78. package/dist/client/rpc-pipeline.js.map +1 -1
  79. package/dist/client/v2-e2ee.d.ts +1 -0
  80. package/dist/client/v2-e2ee.d.ts.map +1 -1
  81. package/dist/client/v2-e2ee.js +182 -89
  82. package/dist/client/v2-e2ee.js.map +1 -1
  83. package/dist/client.d.ts +100 -1
  84. package/dist/client.d.ts.map +1 -1
  85. package/dist/client.js +1220 -25
  86. package/dist/client.js.map +1 -1
  87. package/dist/crypto.d.ts.map +1 -1
  88. package/dist/crypto.js +8 -6
  89. package/dist/crypto.js.map +1 -1
  90. package/dist/errors.d.ts.map +1 -1
  91. package/dist/errors.js +3 -2
  92. package/dist/errors.js.map +1 -1
  93. package/dist/events.d.ts +9 -2
  94. package/dist/events.d.ts.map +1 -1
  95. package/dist/events.js +72 -13
  96. package/dist/events.js.map +1 -1
  97. package/dist/facades.d.ts +82 -2
  98. package/dist/facades.d.ts.map +1 -1
  99. package/dist/facades.js +100 -6
  100. package/dist/facades.js.map +1 -1
  101. package/dist/index.d.ts +7 -3
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +3 -2
  104. package/dist/index.js.map +1 -1
  105. package/dist/keystore/index.d.ts +17 -0
  106. package/dist/keystore/index.d.ts.map +1 -1
  107. package/dist/keystore/indexeddb-shared.d.ts +10 -1
  108. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  109. package/dist/keystore/indexeddb-shared.js +111 -0
  110. package/dist/keystore/indexeddb-shared.js.map +1 -1
  111. package/dist/keystore/indexeddb-token-store.d.ts +7 -1
  112. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  113. package/dist/keystore/indexeddb-token-store.js +30 -1
  114. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  115. package/dist/org-protocol.d.ts +98 -0
  116. package/dist/org-protocol.d.ts.map +1 -0
  117. package/dist/org-protocol.js +1802 -0
  118. package/dist/org-protocol.js.map +1 -0
  119. package/dist/ownership-proof.d.ts +93 -5
  120. package/dist/ownership-proof.d.ts.map +1 -1
  121. package/dist/ownership-proof.js +239 -53
  122. package/dist/ownership-proof.js.map +1 -1
  123. package/dist/ownership-v21.d.ts +194 -0
  124. package/dist/ownership-v21.d.ts.map +1 -0
  125. package/dist/ownership-v21.js +678 -0
  126. package/dist/ownership-v21.js.map +1 -0
  127. package/dist/ownership-workflow.d.ts +151 -0
  128. package/dist/ownership-workflow.d.ts.map +1 -0
  129. package/dist/ownership-workflow.js +1229 -0
  130. package/dist/ownership-workflow.js.map +1 -0
  131. package/dist/seq-tracker.d.ts +1 -0
  132. package/dist/seq-tracker.d.ts.map +1 -1
  133. package/dist/seq-tracker.js +10 -2
  134. package/dist/seq-tracker.js.map +1 -1
  135. package/dist/tools/cross-sdk-agent.d.ts +83 -0
  136. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  137. package/dist/tools/cross-sdk-agent.js +415 -7
  138. package/dist/tools/cross-sdk-agent.js.map +1 -1
  139. package/dist/transport.d.ts +7 -0
  140. package/dist/transport.d.ts.map +1 -1
  141. package/dist/transport.js +90 -2
  142. package/dist/transport.js.map +1 -1
  143. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  144. package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
  145. package/dist/v2/e2ee/decrypt.js +9 -1
  146. package/dist/v2/e2ee/decrypt.js.map +1 -1
  147. package/dist/version.d.ts +1 -1
  148. package/dist/version.js +1 -1
  149. package/package.json +1 -1
package/dist/agent-md.js CHANGED
@@ -1,9 +1,43 @@
1
1
  import { AID } from './aid.js';
2
- import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
3
- import { validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
2
+ import { parseDocument } from 'yaml';
3
+ import { certMatchesFingerprint, normalizeFingerprintHex, parseAgentMdTailSignature, parseAgentMdTailSignatureForRead } from './cert-utils.js';
4
+ import { parseFileFrontmatter, validateAgentMdDocument, validateAgentMdDocumentForRead, verifyAgentMdUpload, verifyAgentMdLegacyBodyUpload } from './agent-md-schema.js';
5
+ import { validateOwnershipFrontmatter, verifyOwnershipFrontmatterStatus } from './ownership-proof.js';
4
6
  import { AUNError, ClientSignatureError, NotFoundError, StateError, ValidationError } from './errors.js';
5
7
  import { isJsonObject } from './types.js';
8
+ import { ownershipAid, parseOwnershipFile } from './ownership-v21.js';
6
9
  const DEFAULT_HTTP_TIMEOUT_MS = 30_000;
10
+ export function buildOwnershipReleaseAgentMd(content, release) {
11
+ return updateOwnershipAgentMd(content, { ownership_release: release });
12
+ }
13
+ export function buildOwnershipAgentMd(content, owner, proof) {
14
+ return updateOwnershipAgentMd(content, { owner, ownership_proof: proof });
15
+ }
16
+ export function buildOwnershipAgentMdV21(content, statement) {
17
+ return updateOwnershipAgentMd(content, statement ? { owner: statement.owner_aid, ownership: { statement } } : {});
18
+ }
19
+ function updateOwnershipAgentMd(content, fields) {
20
+ const payload = parseAgentMdTailSignature(String(content ?? '')).payload;
21
+ const match = /^---(?:\r\n|\n|\r)([\s\S]*?)(?:\r\n|\n|\r)---([\s\S]*)$/.exec(payload);
22
+ if (!match)
23
+ throw new ValidationError('agent.md must start with YAML frontmatter');
24
+ const document = parseDocument(match[1], { uniqueKeys: true });
25
+ if (document.errors.length)
26
+ throw new ValidationError(`agent.md frontmatter is invalid: ${document.errors[0].message}`);
27
+ const frontmatter = document.toJS();
28
+ if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter))
29
+ throw new ValidationError('agent.md frontmatter must be a mapping');
30
+ document.delete('owner');
31
+ document.delete('ownership_proof');
32
+ document.delete('ownership_release');
33
+ document.delete('ownership');
34
+ for (const [name, value] of Object.entries(fields))
35
+ document.set(name, value);
36
+ const yamlText = document.toString().replace(/\r\n/g, '\n');
37
+ const updated = `---\n${yamlText.endsWith('\n') ? yamlText : `${yamlText}\n`}---${match[2]}`;
38
+ validateAgentMdDocument(updated, { expectedAid: String(frontmatter.aid ?? '') });
39
+ return updated;
40
+ }
7
41
  export function buildDefaultAgentMd(aid, options = {}) {
8
42
  const target = String(aid ?? '').trim();
9
43
  const type = String(options.type ?? 'agent').trim() || 'agent';
@@ -16,6 +50,10 @@ export function buildDefaultAgentMd(aid, options = {}) {
16
50
  `aid: ${JSON.stringify(target)}`,
17
51
  `name: ${JSON.stringify(name)}`,
18
52
  `type: ${JSON.stringify(type)}`,
53
+ ...(options.exttype ? [`exttype: ${JSON.stringify(options.exttype)}`] : []),
54
+ ...(options.owner ? [`owner: ${JSON.stringify(options.owner)}`] : []),
55
+ ...(options.entityCertFingerprint ? [`entity_cert_fingerprint: ${JSON.stringify(options.entityCertFingerprint)}`] : []),
56
+ ...(options.ownershipProof ? [`ownership_proof: ${JSON.stringify(options.ownershipProof)}`] : []),
19
57
  'version: "1.0.0"',
20
58
  `description: ${JSON.stringify(description)}`,
21
59
  ...(avatar ? [`avatar: ${JSON.stringify(avatar)}`] : []),
@@ -44,6 +82,24 @@ function nodeBuiltin(name) {
44
82
  return null;
45
83
  }
46
84
  }
85
+ async function sha256Hex(value) {
86
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
87
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
88
+ }
89
+ function randomOwnerAuthorizationNonce() {
90
+ if (typeof globalThis.crypto.randomUUID === 'function')
91
+ return globalThis.crypto.randomUUID().replaceAll('-', '');
92
+ const bytes = new Uint8Array(16);
93
+ globalThis.crypto.getRandomValues(bytes);
94
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join('');
95
+ }
96
+ function base64UrlEncode(value) {
97
+ const bytes = new TextEncoder().encode(value);
98
+ let binary = '';
99
+ for (const byte of bytes)
100
+ binary += String.fromCharCode(byte);
101
+ return globalThis.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
102
+ }
47
103
  function requestHeaders(init) {
48
104
  const out = {};
49
105
  try {
@@ -231,7 +287,11 @@ export class AgentMdManager {
231
287
  _gatewayResolver;
232
288
  _peerResolver;
233
289
  _accessTokenResolver;
290
+ _uploadAccessTokenResolver;
234
291
  _aidValidator;
292
+ _entityOwnerResolver;
293
+ _entityIdentityGetter;
294
+ _entitySignerResolver;
235
295
  _cache = new Map();
236
296
  _memoryStorage = new Map();
237
297
  _fetchInflight = new Set();
@@ -250,7 +310,11 @@ export class AgentMdManager {
250
310
  this._gatewayResolver = opts.gatewayResolver;
251
311
  this._peerResolver = opts.peerResolver;
252
312
  this._accessTokenResolver = opts.accessTokenResolver;
313
+ this._uploadAccessTokenResolver = opts.uploadAccessTokenResolver;
253
314
  this._aidValidator = opts.aidValidator;
315
+ this._entityOwnerResolver = opts.entityOwnerResolver;
316
+ this._entityIdentityGetter = opts.entityIdentityGetter;
317
+ this._entitySignerResolver = opts.entitySignerResolver;
254
318
  }
255
319
  get root() {
256
320
  return this._agentMdPath || this._defaultRoot();
@@ -259,6 +323,73 @@ export class AgentMdManager {
259
323
  this._aunPath = String(aunPath ?? '');
260
324
  this.setRoot(null);
261
325
  }
326
+ async readOwnershipFresh(aid) {
327
+ const target = this._safeAid(aid);
328
+ const gatewayUrl = await this._resolveGateway(target);
329
+ const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
330
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache', 'X-AUN-Ownership-Check': '1', ...(viaGatewayHost ? { Host: target } : {}) };
331
+ let url = this._url(target, gatewayUrl);
332
+ if (viaGatewayHost) {
333
+ const parsed = new URL(gatewayUrl);
334
+ const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
335
+ url = `${agentMdHttpScheme(gatewayUrl)}://${parsed.hostname}${port}/agent.md`;
336
+ }
337
+ const read = (targetUrl) => fetchWithTimeout(targetUrl, {
338
+ method: 'GET', cache: 'no-store', redirect: 'error',
339
+ headers,
340
+ });
341
+ let response = await read(url);
342
+ if (response.status === 404 && !headerValue(response.headers, 'X-AUN-Ownership-Protocol') && headerValue(response.headers, 'X-AUN-Ownership-Status') !== 'not_applicable' && viaGatewayHost) {
343
+ const fallback = nameServiceAgentMdUrl(target, gatewayUrl);
344
+ if (fallback && fallback !== url)
345
+ response = await read(fallback);
346
+ }
347
+ if (!response.ok && response.status !== 404)
348
+ throw new StateError(`NS ownership check unavailable: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
349
+ if (headerValue(response.headers, 'X-AUN-Ownership-Status') === 'not_applicable')
350
+ throw new StateError('group/org uses its lifecycle owner relationship', { stringCode: 'not_applicable' });
351
+ const protocol = headerValue(response.headers, 'X-AUN-Ownership-Protocol');
352
+ const version = headerValue(response.headers, 'X-AUN-Ownership-Version');
353
+ const lock = headerValue(response.headers, 'X-AUN-Ownership-Lock');
354
+ const etag = headerValue(response.headers, 'ETag');
355
+ if (protocol !== '2.1')
356
+ throw new StateError('NS ownership protocol metadata unavailable', { stringCode: 'unsupported_protocol', retryable: true });
357
+ if (!/^(0|[1-9][0-9]*)$/.test(version) || !Number.isSafeInteger(Number(version)) || !['locked', 'unlocked'].includes(lock))
358
+ throw new StateError('NS ownership relationship metadata unavailable', { stringCode: 'unavailable', retryable: true });
359
+ if (response.status === 404 && (Number(version) !== 0 || lock !== 'unlocked') || response.ok && !etag)
360
+ throw new StateError('NS ownership document metadata unavailable', { stringCode: 'unavailable', retryable: true });
361
+ let content = '';
362
+ if (response.ok) {
363
+ try {
364
+ content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
365
+ }
366
+ catch {
367
+ throw new ValidationError('NS ownership file must be valid UTF-8', { stringCode: 'invalid' });
368
+ }
369
+ }
370
+ return { sub_aid: target, exists: response.ok, content, etag, protocol, relation_version: Number(version), lock: lock, observed_at: Math.trunc(Date.now() / 1000) };
371
+ }
372
+ async submitOwnershipRaw(aid, content, condition) {
373
+ const target = this._safeAid(aid);
374
+ if (this._currentAid()?.aid !== target)
375
+ throw new ValidationError('ownership upload requires its own identity', { stringCode: 'forbidden' });
376
+ parseOwnershipFile(content, 'agent', { expectedSubAid: target });
377
+ if (Boolean(condition.if_match) === Boolean(condition.if_none_match === '*'))
378
+ throw new ValidationError('ownership upload requires its original document condition');
379
+ const gatewayUrl = await this._resolveGateway(target);
380
+ 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: { 'X-AUN-Signature-Format': 'multi-v1', 'Content-Type': 'text/markdown; charset=utf-8', Authorization: `Bearer ${token}`,
384
+ ...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }) },
385
+ });
386
+ if (!response.ok) {
387
+ const reason = (await response.text()).trim();
388
+ 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' });
390
+ }
391
+ return await this.readOwnershipFresh(target);
392
+ }
262
393
  setRoot(root) {
263
394
  this._agentMdPath = String(root ?? '').trim() || this._defaultRoot();
264
395
  this._storageGeneration += 1;
@@ -268,7 +399,7 @@ export class AgentMdManager {
268
399
  this._snapshotLoads.clear();
269
400
  return this._agentMdPath;
270
401
  }
271
- async upload(content, ownerOperationId) {
402
+ async upload(content) {
272
403
  const target = this._ownerAid();
273
404
  const current = this._currentAid();
274
405
  if (!target || !current) {
@@ -277,59 +408,502 @@ export class AgentMdManager {
277
408
  if (!current.isPrivateKeyValid()) {
278
409
  throw new StateError('uploadAgentMd requires loaded AID with a valid private key');
279
410
  }
280
- if (content !== undefined && content !== null) {
281
- const text = String(content ?? '');
282
- if (text.length === 0)
283
- throw new ValidationError('uploadAgentMd requires non-empty content');
284
- validateAgentMdDocument(text, { expectedAid: target });
285
- await this.saveRecord(target, {
286
- content: text,
287
- local_etag: await AgentMdManager.contentEtag(text),
288
- fetched_at: Date.now(),
289
- });
290
- }
291
- const localContent = await this.readContent(target);
411
+ const localContent = content ?? (String((await this.loadRecord(target, true))?.local_draft_content ?? '') || await this.readContent(target));
292
412
  if (localContent === null || localContent.length === 0) {
293
413
  throw new ValidationError('uploadAgentMd requires local agent.md content');
294
414
  }
295
- validateAgentMdDocument(localContent, { expectedAid: target });
296
- const signedResult = await current.signAgentMd(localContent);
415
+ const parsed = this._parseBodyUpdate(localContent, target);
416
+ if (['group', 'org'].includes(String(parsed.frontmatter.type)))
417
+ return await this.uploadAs(target, localContent, current);
418
+ if (parsed.signatures.length > 1) {
419
+ await this._verifyBodyUpload(target, localContent, current);
420
+ return await this._uploadPrepared(target, localContent, current);
421
+ }
422
+ const signedResult = await current.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : localContent);
297
423
  if (!signedResult.ok) {
298
424
  throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
299
425
  }
300
426
  const signed = signedResult.data.signed;
301
- await verifyAgentMdUpload(signed, current.certPem, target);
302
- const result = await this._uploadHttp(target, signed, ownerOperationId);
303
- const localEtag = await AgentMdManager.contentEtag(signed);
304
- const remoteEtag = String(result.etag ?? '').trim();
305
- await this.saveRecord(target, {
306
- content: signed,
307
- local_etag: localEtag,
308
- remote_etag: remoteEtag || undefined,
309
- last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(),
310
- fetched_at: Date.now(),
311
- checked_at: Date.now(),
312
- remote_status: remoteEtag ? 'found' : 'unknown',
313
- last_error: '',
427
+ await this._verifyBodyUpload(target, signed, current);
428
+ return await this._uploadPrepared(target, signed, current);
429
+ }
430
+ async uploadAs(targetAid, content, signer) {
431
+ const target = this._safeAid(String(targetAid ?? '').trim());
432
+ const rawContent = String(content ?? '');
433
+ if (!rawContent.trim())
434
+ throw new ValidationError('uploadAgentMd requires non-empty content');
435
+ const parsed = this._parseBodyUpdate(rawContent, target);
436
+ if (!['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signatures.length > 1) {
437
+ await this._verifyBodyUpload(target, rawContent, signer);
438
+ return await this._uploadPrepared(target, rawContent, signer);
439
+ }
440
+ if (['group', 'org'].includes(String(parsed.frontmatter.type)) && parsed.signature) {
441
+ await verifyAgentMdUpload(rawContent, signer.certPem, target, undefined, async (aid, fingerprint) => {
442
+ if (!this._entitySignerResolver)
443
+ throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
444
+ return await this._entitySignerResolver(aid, fingerprint);
445
+ });
446
+ return await this._uploadPrepared(target, rawContent, signer);
447
+ }
448
+ if (['group', 'org'].includes(String(parsed.frontmatter.type)) && signer.aid !== target) {
449
+ if (!this._entityIdentityGetter)
450
+ throw new StateError('entity signing identity is unavailable');
451
+ signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
452
+ if (signer.aid !== target)
453
+ throw new StateError('entity signing identity does not match target');
454
+ }
455
+ if (!signer?.isPrivateKeyValid())
456
+ throw new StateError('uploadAgentMd requires a valid signer identity');
457
+ const signedResult = await signer.signAgentMd(this._isLegacyBodyRelation(parsed.frontmatter) ? parsed.payload : rawContent);
458
+ if (!signedResult.ok)
459
+ throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
460
+ const signed = signedResult.data.signed;
461
+ await this._verifyBodyUpload(target, signed, signer);
462
+ return await this._uploadPrepared(target, signed, signer);
463
+ }
464
+ async pendingUpload(aid) {
465
+ const record = await this.loadRecord(aid);
466
+ if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
467
+ return null;
468
+ 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 };
469
+ }
470
+ async resumePendingUpload(aid, signer) {
471
+ const pending = await this.pendingUpload(aid);
472
+ if (!pending)
473
+ return false;
474
+ const condition = pending.condition;
475
+ if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
476
+ throw new StateError('pending agent.md requires its original document condition', { stringCode: 'persistence_unavailable' });
477
+ const pendingDocument = validateAgentMdDocument(pending.content, { expectedAid: aid, validateOwnership: false });
478
+ const meta = pendingDocument.frontmatter;
479
+ if (this._isLegacyBodyRelation(meta) && pending.relation?.protocol !== 'legacy')
480
+ throw new StateError('legacy ownership pending agent.md requires its original relationship snapshot', { stringCode: 'unsupported_protocol' });
481
+ try {
482
+ if (pending.relation?.protocol === 'legacy') {
483
+ const relation = pending.relation;
484
+ if (!condition.if_match || condition.if_match !== relation.original_etag || typeof relation.original_content !== 'string' || relation.original_content_etag !== await AgentMdManager.contentEtag(relation.original_content) || pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
485
+ throw new StateError('pending legacy original bytes or document condition are corrupt', { stringCode: 'persistence_unavailable' });
486
+ const original = await this._verifyLegacyBodyOriginal(aid, relation.original_content);
487
+ if (!this._isLegacyBodyRelation(original) || !this._sameBodyRelation(this._bodyRelationFields(original), relation.fields))
488
+ throw new StateError('pending legacy original relationship snapshot is corrupt', { stringCode: 'persistence_unavailable' });
489
+ if (!this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
490
+ throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
491
+ const identity = signer ?? this._currentAid();
492
+ if (!identity || identity.aid.toLowerCase() !== aid.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== aid.toLowerCase())
493
+ throw new StateError('legacy body recovery requires its current subject identity', { stringCode: 'forbidden' });
494
+ await this._verifyBodyUpload(aid, pending.content, identity);
495
+ const remote = await this._readLegacyBodyFresh(aid);
496
+ if (remote.content === pending.content && remote.etag === pending.content_etag) {
497
+ await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
498
+ return true;
499
+ }
500
+ if (remote.etag !== condition.if_match || remote.content !== relation.original_content)
501
+ throw new StateError('pending legacy original document changed', { stringCode: 'conflict' });
502
+ await this._uploadPrepared(aid, pending.content, identity, condition);
503
+ return true;
504
+ }
505
+ if (['group', 'org'].includes(String(meta.type))) {
506
+ const identity = signer ?? this._currentAid();
507
+ if (!identity)
508
+ throw new StateError('entity upload recovery requires current owner', { stringCode: 'forbidden' });
509
+ const context = await this._entityContext(aid, meta, identity);
510
+ if (!pending.relation?.entity_cert_fingerprint)
511
+ throw new StateError('pending entity upload requires original owner and certificate context', { stringCode: 'persistence_unavailable' });
512
+ if (pending.relation.owner_aid !== context.owner_aid)
513
+ throw new StateError('pending entity upload belongs to a different owner', { stringCode: 'forbidden' });
514
+ if (pending.relation.entity_cert_fingerprint !== context.entity_cert_fingerprint)
515
+ throw new StateError('pending entity upload belongs to an old rekey generation', { stringCode: 'conflict' });
516
+ if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
517
+ throw new StateError('pending entity original bytes are corrupt', { stringCode: 'persistence_unavailable' });
518
+ const documentSigner = await this._entityDocumentSigner(aid, pending.content);
519
+ await this._verifyEntityDocument(aid, pending.content);
520
+ const remote = await this._readEntityFresh(aid);
521
+ if (remote.exists && remote.content === pending.content && remote.etag === pending.content_etag) {
522
+ await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
523
+ return true;
524
+ }
525
+ if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
526
+ throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
527
+ await this._uploadPrepared(aid, pending.content, identity, condition);
528
+ return true;
529
+ }
530
+ const identity = signer ?? this._currentAid();
531
+ if (pendingDocument.signatures.length > 1) {
532
+ if (!identity)
533
+ throw new StateError('pending agent.md requires a signer identity');
534
+ await this._verifyBodyUpload(aid, pending.content, identity);
535
+ }
536
+ const remote = await this.readOwnershipFresh(aid);
537
+ const fail = (code, reason, retryable = false) => new StateError(reason, { stringCode: code, retryable });
538
+ const candidate = parseOwnershipFile(pending.content, 'agent', { expectedSubAid: aid });
539
+ const relation = pending.relation && Object.keys(pending.relation).length ? pending.relation : undefined;
540
+ 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]);
541
+ if (candidate.statement && !relation)
542
+ throw fail('persistence_unavailable', 'pending ownership body update requires its original relationship context');
543
+ if (relation) {
544
+ if (remote.relation_version !== relation.relation_version || remote.lock !== relation.lock)
545
+ throw fail('conflict', 'pending agent.md original relationship changed');
546
+ if (!sameStatement(candidate.statement, relation.statement))
547
+ throw fail('unsupported_protocol', 'ownership changes require their persisted 2.1 task');
548
+ if (remote.exists && (remote.relation_version > 0 || candidate.statement)) {
549
+ const current = parseOwnershipFile(remote.content, 'agent', { expectedSubAid: aid });
550
+ if (!sameStatement(current.statement, relation.statement) || (remote.owner_aid ?? current.statement?.owner_aid ?? null) !== relation.owner_aid)
551
+ throw fail('conflict', 'pending agent.md relationship statement changed');
552
+ }
553
+ }
554
+ if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
555
+ throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
556
+ if (remote.exists && remote.etag === pending.content_etag && remote.content === pending.content) {
557
+ const saved = await this.saveRecord(aid, { content: pending.content, remote_etag: remote.etag, local_draft_content: '', pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '' });
558
+ if (!saved.aid)
559
+ throw new StateError('agent.md pending completion persistence failed');
560
+ return true;
561
+ }
562
+ if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
563
+ throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
564
+ if (!identity)
565
+ throw new StateError('pending agent.md requires a signer identity');
566
+ if (pendingDocument.signatures.length <= 1)
567
+ await this._verifyBodyUpload(aid, pending.content, identity);
568
+ await this._uploadPrepared(aid, pending.content, identity, condition);
569
+ return true;
570
+ }
571
+ catch (error) {
572
+ await this.saveRecord(aid, { pending_error: error instanceof Error ? error.message : String(error) });
573
+ throw error;
574
+ }
575
+ }
576
+ _parseBodyUpdate(content, target) {
577
+ const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false });
578
+ return validateAgentMdDocument(this._isLegacyBodyRelation(parsed.frontmatter) && parsed.signatures.length <= 1 ? parsed.payload : content, { expectedAid: target, validateOwnership: false });
579
+ }
580
+ _isLegacyBodyRelation(meta) {
581
+ if (['group', 'org'].includes(String(meta.type)))
582
+ return false;
583
+ if (Object.hasOwn(meta, 'ownership')) {
584
+ const ownership = meta.ownership;
585
+ const statement = isJsonObject(ownership) ? ownership.statement : undefined;
586
+ return isJsonObject(statement) && statement.version === 2 && !Object.hasOwn(statement, 'relation_version');
587
+ }
588
+ return ['owner', 'ownership_proof', 'ownership_release'].some((field) => Object.hasOwn(meta, field));
589
+ }
590
+ _bodyRelationFields(meta) {
591
+ return Object.fromEntries(['owner', 'ownership', 'ownership_proof', 'ownership_release'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]]));
592
+ }
593
+ _sameBodyRelation(first, second) {
594
+ const stable = (value) => Array.isArray(value) ? value.map(stable) : isJsonObject(value) ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable(value[key])])) : value;
595
+ return JSON.stringify(stable(first)) === JSON.stringify(stable(second));
596
+ }
597
+ async _readLegacyBodyFresh(target) {
598
+ const gateway = await this._resolveGateway(target);
599
+ const response = await fetchWithTimeout(this._url(target, gateway), { method: 'GET', headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown', 'Cache-Control': 'no-cache' }, cache: 'no-store', redirect: 'error' });
600
+ if (response.status === 404)
601
+ throw new StateError('legacy relationship cannot be created by an ordinary body update', { stringCode: 'unsupported_protocol' });
602
+ const etag = headerValue(response.headers, 'ETag');
603
+ if (response.status !== 200 || !etag)
604
+ throw new StateError('legacy original document or ETag is unavailable', { stringCode: 'unavailable', retryable: true });
605
+ let content;
606
+ try {
607
+ content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
608
+ }
609
+ catch {
610
+ throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
611
+ }
612
+ return { content, etag, last_modified: headerValue(response.headers, 'Last-Modified'), status: response.status };
613
+ }
614
+ async _verifyLegacyBodyOriginal(target, content) {
615
+ const parsed = validateAgentMdDocumentForRead(content, { expectedAid: target, validateOwnership: false, requireSignature: true });
616
+ const peer = await this._resolvePeer(target, parsed.signature.cert_fingerprint, Number(parsed.signature.timestamp));
617
+ const verified = await this._verifyPeerDocument(peer, content);
618
+ if (!verified.ok || verified.data.status !== 'verified')
619
+ throw new ClientSignatureError('legacy original agent.md signature verification failed', { stringCode: 'invalid' });
620
+ return parsed.frontmatter;
621
+ }
622
+ async _verifyPeerDocument(peer, content) {
623
+ const parsed = parseAgentMdTailSignatureForRead(content);
624
+ const additionalCertificates = new Map();
625
+ if (parsed.fields)
626
+ additionalCertificates.set(`${parsed.fields.signer_aid ?? peer.aid};${parsed.fields.cert_fingerprint}`, peer);
627
+ for (const block of parsed.signatures.slice(1))
628
+ additionalCertificates.set(`${block.fields.signer_aid};${block.fields.cert_fingerprint}`, await this._resolvePeer(block.fields.signer_aid, block.fields.cert_fingerprint, Number(block.fields.timestamp)));
629
+ return await peer.verifyAgentMd(content, (signer, fingerprint) => {
630
+ const certificate = additionalCertificates.get(`${signer};${fingerprint}`);
631
+ if (!certificate)
632
+ throw new StateError('additional signing certificate is unavailable', { stringCode: 'unavailable', retryable: true });
633
+ return certificate;
634
+ });
635
+ }
636
+ async _verifyBodyUpload(target, content, signer) {
637
+ const meta = validateAgentMdDocument(content, { expectedAid: target, validateOwnership: false }).frontmatter;
638
+ await (this._isLegacyBodyRelation(meta) ? verifyAgentMdLegacyBodyUpload : verifyAgentMdUpload)(content, signer.certPem, target, undefined, async (aid, fingerprint, timestamp) => {
639
+ if (this._entitySignerResolver)
640
+ return await this._entitySignerResolver(aid, fingerprint);
641
+ return await this._resolvePeer(aid, fingerprint, timestamp);
642
+ });
643
+ }
644
+ async _uploadPrepared(target, signed, signer, originalCondition) {
645
+ const meta = validateAgentMdDocument(signed, { expectedAid: target, validateOwnership: false }).frontmatter;
646
+ if (this._isLegacyBodyRelation(meta))
647
+ validateOwnershipFrontmatter(Object.fromEntries(['aid', 'type', 'owner'].filter((field) => Object.hasOwn(meta, field)).map((field) => [field, meta[field]])), target);
648
+ if (!['group', 'org'].includes(String(meta.type)))
649
+ await this._verifyBodyUpload(target, signed, signer);
650
+ if (!originalCondition) {
651
+ const draft = await this.saveRecord(target, { local_draft_content: signed });
652
+ if (draft.local_draft_content !== signed)
653
+ throw new StateError('agent.md local draft persistence failed', { stringCode: 'persistence_unavailable' });
654
+ }
655
+ const entity = ['group', 'org'].includes(String(meta.type));
656
+ const uploader = entity ? this._currentAid() ?? signer : signer;
657
+ const entityContext = entity ? await this._entityContext(target, meta, uploader) : undefined;
658
+ if (entity)
659
+ await this._verifyEntityDocument(target, signed);
660
+ let condition = originalCondition;
661
+ let relation = entityContext ?? (await this.pendingUpload(target))?.relation;
662
+ if (!condition) {
663
+ let fresh;
664
+ if (!entity && this._isLegacyBodyRelation(meta))
665
+ fresh = await this._readLegacyBodyFresh(target);
666
+ else {
667
+ try {
668
+ fresh = entity ? await this._readEntityFresh(target) : await this.readOwnershipFresh(target);
669
+ }
670
+ catch (error) {
671
+ if (entity || !['unavailable', 'unsupported_protocol'].includes(String(error?.stringCode)))
672
+ throw error;
673
+ const legacy = await this._readLegacyBodyFresh(target);
674
+ const legacyMeta = validateAgentMdDocumentForRead(legacy.content, { expectedAid: target, validateOwnership: false }).frontmatter;
675
+ if (!this._isLegacyBodyRelation(legacyMeta))
676
+ throw error;
677
+ fresh = legacy;
678
+ }
679
+ }
680
+ const freshMeta = !entity && (!('exists' in fresh) || fresh.exists) && fresh.content ? validateAgentMdDocumentForRead(fresh.content, { expectedAid: target, validateOwnership: false }).frontmatter : undefined;
681
+ const legacy = !entity && (this._isLegacyBodyRelation(meta) || freshMeta && this._isLegacyBodyRelation(freshMeta));
682
+ if (legacy) {
683
+ if (!freshMeta || !this._isLegacyBodyRelation(freshMeta) || !this._sameBodyRelation(this._bodyRelationFields(meta), this._bodyRelationFields(freshMeta)))
684
+ throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
685
+ if (!fresh.etag)
686
+ throw new StateError('legacy original document ETag is unavailable', { stringCode: 'unavailable', retryable: true });
687
+ await this._verifyLegacyBodyOriginal(target, fresh.content);
688
+ condition = { if_match: fresh.etag };
689
+ relation = { protocol: 'legacy', original_content: fresh.content, original_content_etag: await AgentMdManager.contentEtag(fresh.content), original_etag: fresh.etag, fields: this._bodyRelationFields(freshMeta) };
690
+ }
691
+ else {
692
+ const authority = fresh;
693
+ condition = authority.exists ? { if_match: fresh.etag } : { if_none_match: '*' };
694
+ if (!entity) {
695
+ const candidate = parseOwnershipFile(signed, 'agent', { expectedSubAid: target });
696
+ const statement = authority.exists && (authority.relation_version > 0 || candidate.statement) ? parseOwnershipFile(fresh.content, 'agent', { expectedSubAid: target }).statement : undefined;
697
+ relation = { relation_version: authority.relation_version, lock: authority.lock, owner_aid: authority.owner_aid ?? statement?.owner_aid ?? null, statement: statement ?? null };
698
+ }
699
+ }
700
+ }
701
+ if (relation?.protocol === 'legacy') {
702
+ if (signer.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.aid.toLowerCase() !== target.toLowerCase() || this._currentAid()?.certFingerprint !== signer.certFingerprint)
703
+ throw new StateError('legacy body update requires its current subject identity', { stringCode: 'forbidden' });
704
+ if (!this._isLegacyBodyRelation(meta) || !this._sameBodyRelation(this._bodyRelationFields(meta), relation.fields))
705
+ throw new StateError('legacy relationship changes require their original protocol', { stringCode: 'unsupported_protocol' });
706
+ await this._verifyBodyUpload(target, signed, signer);
707
+ }
708
+ const entityAuthorization = '';
709
+ const pending = await this.saveRecord(target, { pending_operation: 'replace', pending_target_aid: target, pending_content: signed, pending_content_etag: await AgentMdManager.contentEtag(signed), pending_error: '', pending_condition: condition, pending_relation: relation, pending_authorization: entityAuthorization });
710
+ if (relation?.protocol === 'legacy' && !this._sameBodyRelation(pending.pending_relation, relation))
711
+ throw new StateError('legacy original relationship snapshot persistence failed', { stringCode: 'persistence_unavailable' });
712
+ const savedCondition = pending.pending_condition;
713
+ if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
714
+ throw new StateError('agent.md pending upload persistence failed');
715
+ try {
716
+ const owner = this._currentAid();
717
+ const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
718
+ if (delegated && !owner?.isPrivateKeyValid())
719
+ throw new StateError('owner agent.md upload requires a loaded owner identity');
720
+ const authorization = entity ? '' : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
721
+ const result = await this._uploadHttp(target, signed, authorization, condition, relation?.protocol === 'legacy');
722
+ const remoteEtag = String(result.etag ?? '').trim();
723
+ const contentEtag = await AgentMdManager.contentEtag(signed);
724
+ const saved = await this.saveRecord(target, {
725
+ content: signed, local_etag: contentEtag, remote_etag: remoteEtag || undefined, ...(remoteEtag === contentEtag ? { local_draft_content: '' } : {}),
726
+ last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
727
+ remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
728
+ pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
729
+ });
730
+ if (!saved.aid)
731
+ throw new StateError('agent.md upload completion persistence failed');
732
+ return { ...result };
733
+ }
734
+ catch (error) {
735
+ await this.saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) });
736
+ throw error;
737
+ }
738
+ }
739
+ async readEntityAuthorityFresh(target) {
740
+ const fresh = await this._readEntityFresh(this._safeAid(target));
741
+ if (fresh.ownership_status !== 'not_applicable' || !['current', 'stale'].includes(fresh.entity_status ?? '') || !fresh.owner_aid || !/^sha256:[0-9a-f]{64}$/.test(fresh.entity_cert_fingerprint ?? ''))
742
+ throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
743
+ try {
744
+ return { owner_aid: this._safeAid(ownershipAid(fresh.owner_aid, 'owner_aid')), entity_cert_fingerprint: fresh.entity_cert_fingerprint };
745
+ }
746
+ catch {
747
+ throw new StateError('entity current owner authority is unavailable', { stringCode: 'unavailable', retryable: true });
748
+ }
749
+ }
750
+ async _readEntityFresh(target) {
751
+ const gateway = await this._resolveGateway(target);
752
+ const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
753
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', 'X-AUN-Ownership-Check': '1', 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
754
+ let url = this._url(target, gateway);
755
+ if (viaGatewayHost) {
756
+ const parsed = new URL(gateway);
757
+ const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
758
+ url = `${agentMdHttpScheme(gateway)}://${parsed.hostname}${port}/agent.md`;
759
+ }
760
+ let response;
761
+ try {
762
+ response = await fetchWithTimeout(url, { method: 'GET', cache: 'no-store', redirect: 'error', headers });
763
+ }
764
+ catch (error) {
765
+ throw new StateError(`entity agent.md fresh read unavailable: ${String(error)}`, { stringCode: 'unavailable', retryable: true });
766
+ }
767
+ if (response.status !== 200 && response.status !== 404)
768
+ throw new StateError(`entity agent.md fresh read failed: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
769
+ const etag = headerValue(response.headers, 'ETag');
770
+ if (response.status === 200 && !etag)
771
+ throw new StateError('entity agent.md fresh read has no ETag', { stringCode: 'unavailable', retryable: true });
772
+ let content = '';
773
+ if (response.status === 200) {
774
+ try {
775
+ content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
776
+ }
777
+ catch {
778
+ throw new ValidationError('entity agent.md must be valid UTF-8', { stringCode: 'invalid' });
779
+ }
780
+ }
781
+ const facts = {};
782
+ for (const [field, header] of [['ownership_status', 'X-AUN-Ownership-Status'], ['entity_status', 'X-AUN-Entity-Verification-Status'], ['owner_aid', 'X-AUN-Entity-Owner-AID'], ['entity_cert_fingerprint', 'X-AUN-Entity-Cert-Fingerprint']]) {
783
+ const value = headerValue(response.headers, header);
784
+ if (value)
785
+ facts[field] = value;
786
+ }
787
+ return { exists: response.status === 200, content, etag, ...facts };
788
+ }
789
+ async _entityCurrentContext(target, meta, uploading = false) {
790
+ const owner = String(meta.owner ?? '').trim().toLowerCase();
791
+ if (!this._entityOwnerResolver)
792
+ throw new StateError('entity owner resolver is unavailable', { stringCode: 'unavailable', retryable: true });
793
+ const freshOwner = String(await this._entityOwnerResolver(target, String(meta.type))).trim().toLowerCase();
794
+ if (!freshOwner)
795
+ throw new StateError('entity current owner is unavailable', { stringCode: 'unavailable', retryable: true });
796
+ if (freshOwner !== owner)
797
+ throw new StateError('entity current owner changed', { stringCode: uploading ? 'forbidden' : 'conflict' });
798
+ if (!this._entityIdentityGetter)
799
+ throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
800
+ const entity = await this._entityIdentityGetter(target, String(meta.type));
801
+ if (!entity.certFingerprint)
802
+ throw new StateError('entity current certificate is unavailable', { stringCode: 'unavailable', retryable: true });
803
+ if (meta.entity_cert_fingerprint !== entity.certFingerprint)
804
+ throw new StateError('entity agent.md certificate generation changed', { stringCode: 'conflict' });
805
+ return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
806
+ }
807
+ async _entityContext(target, meta, signer) {
808
+ const owner = String(meta.owner ?? '').trim().toLowerCase();
809
+ if (signer.aid.toLowerCase() !== owner || signer.aid.toLowerCase() !== this._ownerAid().toLowerCase())
810
+ throw new StateError('group/org agent.md requires current owner uploader', { stringCode: 'forbidden' });
811
+ const context = await this._entityCurrentContext(target, meta, true);
812
+ if (signer.aid.toLowerCase() === owner)
813
+ await this._verifyEntityOwnerSigner(signer);
814
+ else if (signer.certFingerprint !== context.entity_cert_fingerprint)
815
+ throw new StateError('entity uploader certificate generation changed', { stringCode: 'conflict' });
816
+ return context;
817
+ }
818
+ async _entityDocumentSigner(target, content) {
819
+ const parsed = validateAgentMdDocument(content, { expectedAid: target, requireSignature: true });
820
+ const signerAid = parsed.signature.signer_aid ?? target;
821
+ let signer;
822
+ if (signerAid.toLowerCase() === target.toLowerCase()) {
823
+ if (!this._entityIdentityGetter)
824
+ throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
825
+ signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
826
+ }
827
+ else {
828
+ if (!this._entitySignerResolver)
829
+ throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
830
+ signer = await this._entitySignerResolver(signerAid, parsed.signature.cert_fingerprint);
831
+ }
832
+ if (signer.aid.toLowerCase() !== signerAid.toLowerCase() || signer.certFingerprint !== parsed.signature.cert_fingerprint)
833
+ throw new StateError('entity signing certificate changed', { stringCode: 'conflict' });
834
+ return signer;
835
+ }
836
+ async _verifyEntityDocument(target, content) {
837
+ const primary = await this._entityDocumentSigner(target, content);
838
+ await verifyAgentMdUpload(content, primary.certPem, target, undefined, async (signer, fingerprint) => {
839
+ if (!this._entitySignerResolver)
840
+ throw new StateError('entity additional signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
841
+ const material = await this._entitySignerResolver(signer, fingerprint);
842
+ if (material.aid !== signer || material.certFingerprint !== fingerprint)
843
+ throw new StateError('entity additional signing certificate changed', { stringCode: 'conflict' });
844
+ return material;
314
845
  });
315
- return result;
846
+ }
847
+ async _verifyEntityOwnerSigner(owner) {
848
+ if (!this._entitySignerResolver)
849
+ throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
850
+ const fresh = await this._entitySignerResolver(owner.aid, owner.certFingerprint);
851
+ if (fresh.aid.toLowerCase() !== owner.aid.toLowerCase() || fresh.certFingerprint !== owner.certFingerprint) {
852
+ throw new StateError('entity owner signing certificate changed', { stringCode: 'conflict' });
853
+ }
316
854
  }
317
855
  async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
318
856
  const target = this._safeAid(String(aid ?? this._ownerAid() ?? '').trim());
319
857
  const contentResult = await this._downloadHttp(target, timeoutMs);
320
- const parsed = parseAgentMdTailSignature(contentResult.content);
858
+ const parsed = parseAgentMdTailSignatureForRead(contentResult.content);
859
+ let entity = false;
860
+ try {
861
+ entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload, false, false).type));
862
+ }
863
+ catch { }
864
+ let frontmatter;
865
+ try {
866
+ frontmatter = validateAgentMdDocumentForRead(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
867
+ }
868
+ catch (error) {
869
+ if (entity)
870
+ throw new ValidationError(error instanceof Error ? error.message : String(error), { stringCode: 'invalid' });
871
+ throw error;
872
+ }
321
873
  const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
322
- const peer = await this._resolvePeer(target, expectedFp);
323
- const verified = await peer.verifyAgentMd(contentResult.content);
874
+ const signerAid = entity ? (parsed.fields?.signer_aid ?? target) : target;
875
+ const peer = await this._resolvePeer(signerAid, expectedFp, parsed.fields?.timestamp ? Number(parsed.fields.timestamp) : undefined);
876
+ const verified = await this._verifyPeerDocument(peer, contentResult.content);
324
877
  if (!verified.ok)
325
878
  throw new AUNError(verified.error.message);
326
879
  const signature = { ...verified.data };
327
880
  const statusText = String(signature.status ?? 'invalid');
328
881
  const reason = String(signature.reason ?? '').trim();
329
882
  if (statusText === 'invalid') {
330
- throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`);
883
+ throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : undefined);
884
+ }
885
+ let ownershipStatus;
886
+ if (entity)
887
+ ownershipStatus = 'not_applicable';
888
+ else {
889
+ try {
890
+ validateOwnershipFrontmatter(frontmatter, target);
891
+ const rawProof = (frontmatter.ownership_proof ?? {});
892
+ const subjectSignature = rawProof.subject_signature;
893
+ const ownerSignature = rawProof.owner_signature;
894
+ const release = frontmatter.ownership_release;
895
+ const subjectCertPem = subjectSignature ? (await this._resolvePeer(target, String(subjectSignature.cert_fingerprint), isJsonObject(subjectSignature.statement) ? Number(subjectSignature.statement.signed_at) : undefined)).certPem : undefined;
896
+ const ownerAid = String(ownerSignature ? frontmatter.owner : release?.statement && typeof release.statement === 'object' ? release.statement.owner_aid : '');
897
+ const ownerFingerprint = String(ownerSignature?.cert_fingerprint ?? release?.cert_fingerprint ?? '');
898
+ const ownerStatement = (ownerSignature ?? release)?.statement;
899
+ const ownerCertPem = ownerFingerprint ? (await this._resolvePeer(ownerAid, ownerFingerprint, isJsonObject(ownerStatement) ? Number(ownerStatement.signed_at) : undefined)).certPem : undefined;
900
+ ownershipStatus = await verifyOwnershipFrontmatterStatus(frontmatter, subjectCertPem, ownerCertPem);
901
+ }
902
+ catch {
903
+ ownershipStatus = 'invalid';
904
+ }
331
905
  }
332
- const verification = { status: statusText };
906
+ const verification = { status: statusText, ownership_status: ownershipStatus };
333
907
  if (reason)
334
908
  verification.reason = reason;
335
909
  const localEtag = await AgentMdManager.contentEtag(contentResult.content);
@@ -557,7 +1131,7 @@ export class AgentMdManager {
557
1131
  const root = this.root;
558
1132
  await this._writeStorage(this._contentKey(aid), String(content ?? ''), root);
559
1133
  }
560
- async loadRecord(aid) {
1134
+ async loadRecord(aid, strict = false) {
561
1135
  const target = String(aid ?? '').trim();
562
1136
  if (!target)
563
1137
  return null;
@@ -565,7 +1139,7 @@ export class AgentMdManager {
565
1139
  const generation = this._storageGeneration;
566
1140
  try {
567
1141
  const loaded = await this._withLock(async () => {
568
- const record = await this._readRecordUnlocked(target, root);
1142
+ const record = await this._readRecordUnlocked(target, root, strict);
569
1143
  const next = Object.keys(record).length > 0 ? { ...record, aid: target } : { aid: target };
570
1144
  try {
571
1145
  const content = await this._readStorage(this._contentKey(target), root);
@@ -591,6 +1165,8 @@ export class AgentMdManager {
591
1165
  return { ...loaded };
592
1166
  }
593
1167
  catch (err) {
1168
+ if (strict)
1169
+ throw err;
594
1170
  this._log.debug(`agent.md cache load skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
595
1171
  return null;
596
1172
  }
@@ -601,10 +1177,12 @@ export class AgentMdManager {
601
1177
  return {};
602
1178
  const root = this.root;
603
1179
  const generation = this._storageGeneration;
1180
+ let strictDraft = false;
604
1181
  try {
605
1182
  const inputFields = { ...fields };
1183
+ strictDraft = typeof inputFields.local_draft_content === 'string';
606
1184
  const hasContent = Object.prototype.hasOwnProperty.call(inputFields, 'content') && inputFields.content !== undefined && inputFields.content !== null;
607
- if (hasContent) {
1185
+ if (hasContent && !strictDraft) {
608
1186
  const text = String(inputFields.content ?? '');
609
1187
  await this._writeStorage(this._contentKey(target), text, root);
610
1188
  if (!inputFields.local_etag)
@@ -614,8 +1192,18 @@ export class AgentMdManager {
614
1192
  }
615
1193
  delete inputFields.content;
616
1194
  const record = await this._withLock(async () => {
617
- const next = { ...(await this._readRecordUnlocked(target, root)), aid: target };
1195
+ const next = { ...(await this._readRecordUnlocked(target, root, strictDraft)), aid: target };
1196
+ if (hasContent && strictDraft) {
1197
+ const text = String(fields.content ?? '');
1198
+ await this._writeStorage(this._contentKey(target), text, root);
1199
+ if (!inputFields.local_etag)
1200
+ inputFields.local_etag = await AgentMdManager.contentEtag(text);
1201
+ if (!inputFields.fetched_at)
1202
+ inputFields.fetched_at = Date.now();
1203
+ }
618
1204
  for (const [key, value] of Object.entries(inputFields)) {
1205
+ if (key === 'local_draft_content' && value === '' && hasContent && next.local_draft_content !== fields.content)
1206
+ continue;
619
1207
  if (value !== undefined && value !== null)
620
1208
  next[key] = value;
621
1209
  }
@@ -631,6 +1219,8 @@ export class AgentMdManager {
631
1219
  return { ...loaded };
632
1220
  }
633
1221
  catch (err) {
1222
+ if (strictDraft)
1223
+ throw err;
634
1224
  this._log.debug(`agent.md cache save skipped: aid=${target} err=${err instanceof Error ? err.message : String(err)}`);
635
1225
  return {};
636
1226
  }
@@ -690,17 +1280,20 @@ export class AgentMdManager {
690
1280
  async _resolveGateway(aid) {
691
1281
  return String(await this._gatewayResolver?.(aid) ?? '');
692
1282
  }
693
- async _resolvePeer(aid, certFingerprint) {
694
- const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
1283
+ async _resolvePeer(aid, certFingerprint, signedAt) {
1284
+ const normalizedFp = normalizeFingerprintHex(certFingerprint);
1285
+ const expectedFp = normalizedFp ? `sha256:${normalizedFp}` : String(certFingerprint ?? '').trim().toLowerCase();
695
1286
  const current = this._currentAid();
696
1287
  if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
697
1288
  if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
698
1289
  return current;
699
1290
  }
700
- const peer = await this._peerResolver?.(aid, expectedFp || null);
1291
+ const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
701
1292
  if (!(peer instanceof AID)) {
702
1293
  throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
703
1294
  }
1295
+ if (peer.aid.toLowerCase() !== aid.toLowerCase())
1296
+ throw new ClientSignatureError(`agent.md signer does not match requested AID: ${aid}`, { stringCode: 'invalid' });
704
1297
  return peer;
705
1298
  }
706
1299
  async _accessToken(aid, gatewayUrl) {
@@ -709,22 +1302,57 @@ export class AgentMdManager {
709
1302
  throw new StateError('authenticate did not return access_token');
710
1303
  return token;
711
1304
  }
1305
+ async _uploadAccessToken(aid, gatewayUrl) {
1306
+ const resolver = this._uploadAccessTokenResolver ?? this._accessTokenResolver;
1307
+ const token = String(await resolver?.(aid, gatewayUrl) ?? '').trim();
1308
+ if (!token)
1309
+ throw new StateError('authenticate did not return access_token');
1310
+ return token;
1311
+ }
1312
+ async _ownerUploadAuthorization(target, content, owner) {
1313
+ const bodySha256 = await sha256Hex(content);
1314
+ const timestamp = Math.floor(Date.now() / 1000);
1315
+ const nonce = randomOwnerAuthorizationNonce();
1316
+ const canonical = `AUN-AGENT-MD-UPLOAD-V1;${owner.aid.toLowerCase()};${target.toLowerCase()};${bodySha256};${timestamp};${nonce}`;
1317
+ const signed = await owner.sign(canonical);
1318
+ if (!signed.ok || !signed.data) {
1319
+ const message = signed.error?.message || 'owner authorization signing failed';
1320
+ throw new ClientSignatureError(message);
1321
+ }
1322
+ return base64UrlEncode(JSON.stringify({
1323
+ version: 1,
1324
+ target_aid: target.toLowerCase(),
1325
+ uploader_aid: owner.aid.toLowerCase(),
1326
+ body_sha256: bodySha256,
1327
+ timestamp,
1328
+ nonce,
1329
+ cert_fingerprint: owner.certFingerprint,
1330
+ signature: signed.data.signature,
1331
+ }));
1332
+ }
712
1333
  _url(aid, gatewayUrl) {
713
1334
  return `${agentMdHttpScheme(gatewayUrl)}://${this._safeAid(aid)}/agent.md`;
714
1335
  }
715
- async _uploadHttp(aid, content, ownerOperationId) {
1336
+ async _uploadHttp(aid, content, ownerAuthorization = '', condition, legacyBodyUpdate = false) {
1337
+ if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
1338
+ throw new StateError('agent.md upload requires its original document condition', { stringCode: 'persistence_unavailable' });
716
1339
  const gatewayUrl = await this._resolveGateway(aid);
717
- const token = await this._accessToken(aid, gatewayUrl);
1340
+ const authorization = String(ownerAuthorization ?? '').trim();
1341
+ const meta = validateAgentMdDocument(content, { expectedAid: aid, validateOwnership: false }).frontmatter;
1342
+ const entity = ['group', 'org'].includes(String(meta.type));
718
1343
  // 浏览器禁止覆盖 Host;动态 AID URL 由浏览器/Docker 的通配解析路由。
719
1344
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
720
- const headers = {
721
- Authorization: `Bearer ${token}`,
1345
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1',
722
1346
  'Content-Type': 'text/markdown; charset=utf-8',
1347
+ ...(!entity && !(legacyBodyUpdate && this._isLegacyBodyRelation(meta)) ? { 'X-AUN-Ownership-Check': '1' } : {}),
1348
+ ...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
723
1349
  ...(viaGatewayHost ? { Host: this._safeAid(aid) } : {}),
1350
+ ...(!entity && authorization ? { 'X-AUN-AgentMD-Owner-Authorization': authorization } : {}),
724
1351
  };
725
- const operationId = String(ownerOperationId ?? '').trim();
726
- if (operationId)
727
- headers['X-AUN-Owner-Operation-ID'] = operationId;
1352
+ if (!authorization || entity) {
1353
+ const token = await this._uploadAccessToken(aid, gatewayUrl);
1354
+ headers.Authorization = `Bearer ${token}`;
1355
+ }
728
1356
  let url = this._url(aid, gatewayUrl);
729
1357
  if (viaGatewayHost) {
730
1358
  try {
@@ -744,7 +1372,7 @@ export class AgentMdManager {
744
1372
  }
745
1373
  let response;
746
1374
  try {
747
- response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content });
1375
+ response = await fetchWithTimeout(url, { method: 'PUT', headers, body: content, redirect: 'error' });
748
1376
  }
749
1377
  catch (error) {
750
1378
  if (!viaGatewayHost)
@@ -752,12 +1380,12 @@ export class AgentMdManager {
752
1380
  const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
753
1381
  if (!fallbackUrl || fallbackUrl === url)
754
1382
  throw error;
755
- response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
1383
+ response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
756
1384
  }
757
1385
  if (response.status === 404 && viaGatewayHost) {
758
1386
  const fallbackUrl = nameServiceAgentMdUrl(aid, gatewayUrl);
759
1387
  if (fallbackUrl && fallbackUrl !== url) {
760
- response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content });
1388
+ response = await fetchWithTimeout(fallbackUrl, { method: 'PUT', headers, body: content, redirect: 'error' });
761
1389
  }
762
1390
  }
763
1391
  if (response.status === 404) {
@@ -765,6 +1393,10 @@ export class AgentMdManager {
765
1393
  }
766
1394
  if (!response.ok) {
767
1395
  const message = (await response.text()).trim();
1396
+ if (entity) {
1397
+ 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';
1398
+ throw new StateError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, { stringCode: code, retryable: code === 'unavailable' });
1399
+ }
768
1400
  throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
769
1401
  }
770
1402
  const payload = await response.json();
@@ -777,13 +1409,13 @@ export class AgentMdManager {
777
1409
  const cached = await this.loadRecord(target) ?? {};
778
1410
  const gatewayUrl = await this._resolveGateway(target);
779
1411
  const url = this._url(target, gatewayUrl);
780
- const headers = { Accept: 'text/markdown' };
1412
+ const headers = { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' };
781
1413
  const cachedEtag = String(cached.remote_etag ?? cached.etag ?? cached.local_etag ?? '').trim();
782
1414
  const cachedLastModified = String(cached.last_modified ?? '').trim();
783
1415
  let response = await fetchWithTimeout(url, {
784
1416
  method: 'GET',
785
1417
  headers,
786
- redirect: 'follow',
1418
+ redirect: 'error',
787
1419
  }, timeoutMs);
788
1420
  if (response.status === 304 && cached.content !== undefined && cached.content !== null) {
789
1421
  return {
@@ -796,8 +1428,8 @@ export class AgentMdManager {
796
1428
  if (response.status === 304) {
797
1429
  response = await fetchWithTimeout(url, {
798
1430
  method: 'GET',
799
- headers: { Accept: 'text/markdown' },
800
- redirect: 'follow',
1431
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
1432
+ redirect: 'error',
801
1433
  }, timeoutMs);
802
1434
  }
803
1435
  if (response.status === 404) {
@@ -806,9 +1438,15 @@ export class AgentMdManager {
806
1438
  }
807
1439
  if (!response.ok) {
808
1440
  const message = (await response.text()).trim();
809
- throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
1441
+ throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, response.status >= 500 ? { stringCode: 'unavailable', retryable: true } : undefined);
1442
+ }
1443
+ let content;
1444
+ try {
1445
+ content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
1446
+ }
1447
+ catch {
1448
+ throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
810
1449
  }
811
- const content = await response.text();
812
1450
  return {
813
1451
  content,
814
1452
  etag: headerValue(response.headers, 'ETag') || cachedEtag,
@@ -821,7 +1459,7 @@ export class AgentMdManager {
821
1459
  const gatewayUrl = await this._resolveGateway(target);
822
1460
  const response = await fetchWithTimeout(this._url(target, gatewayUrl), {
823
1461
  method: 'HEAD',
824
- headers: { Accept: 'text/markdown' },
1462
+ headers: { 'X-AUN-Signature-Format': 'multi-v1', Accept: 'text/markdown' },
825
1463
  }, HEAD_HTTP_TIMEOUT_MS);
826
1464
  const etag = headerValue(response.headers, 'ETag');
827
1465
  const lastModified = headerValue(response.headers, 'Last-Modified');
@@ -916,14 +1554,22 @@ export class AgentMdManager {
916
1554
  payload.aid = this._safeAid(aid);
917
1555
  await this._writeStorage(this._metaKey(aid), `${JSON.stringify(payload, null, 2)}\n`, root);
918
1556
  }
919
- async _readRecordUnlocked(aid, root = this.root) {
920
- const raw = await this._readStorage(this._metaKey(aid), root);
921
- if (raw === null)
922
- return {};
1557
+ async _readRecordUnlocked(aid, root = this.root, strict = false) {
923
1558
  try {
924
- return this._normalizeRecord(aid, JSON.parse(raw));
1559
+ const raw = await this._readStorage(this._metaKey(aid), root);
1560
+ if (raw === null)
1561
+ return {};
1562
+ const parsed = JSON.parse(raw);
1563
+ if (strict && !isRecord(parsed))
1564
+ throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
1565
+ return this._normalizeRecord(aid, parsed);
925
1566
  }
926
1567
  catch (err) {
1568
+ if (strict) {
1569
+ if (err instanceof StateError)
1570
+ throw err;
1571
+ throw new StateError('agent.md metadata is damaged', { stringCode: 'persistence_unavailable' });
1572
+ }
927
1573
  this._log.warn(`agent.md metadata damaged, ignoring: aid=${aid} err=${err instanceof Error ? err.message : String(err)}`);
928
1574
  return {};
929
1575
  }