@agentunion/fastaun-browser 0.5.18 → 0.5.19

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 (118) hide show
  1. package/CHANGELOG.md +44 -2
  2. package/_packed_docs/CHANGELOG.md +44 -2
  3. package/_packed_docs/INDEX.md +136 -14
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +42 -16
  5. package/_packed_docs/agent.md/SCHEMA.md +245 -205
  6. 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
  7. 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
  8. 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
  9. 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
  10. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +28 -9
  11. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +22 -27
  12. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  13. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +5 -2
  14. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +250 -283
  15. package/_packed_docs/sdk/09-group-rpc-manual.md +46 -14
  16. package/_packed_docs/sdk/09-message-rpc-manual.md +40 -2
  17. package/_packed_docs/sdk/09-storage-rpc-manual.md +74 -64
  18. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +25 -5
  19. package/_packed_docs/sdk/INDEX.md +76 -17
  20. package/dist/agent-md-schema.d.ts +3 -0
  21. package/dist/agent-md-schema.d.ts.map +1 -1
  22. package/dist/agent-md-schema.js +45 -11
  23. package/dist/agent-md-schema.js.map +1 -1
  24. package/dist/agent-md.d.ts +45 -2
  25. package/dist/agent-md.d.ts.map +1 -1
  26. package/dist/agent-md.js +519 -30
  27. package/dist/agent-md.js.map +1 -1
  28. package/dist/aid-store.d.ts +12 -46
  29. package/dist/aid-store.d.ts.map +1 -1
  30. package/dist/aid-store.js +113 -246
  31. package/dist/aid-store.js.map +1 -1
  32. package/dist/aid.d.ts.map +1 -1
  33. package/dist/aid.js +20 -7
  34. package/dist/aid.js.map +1 -1
  35. package/dist/auth.d.ts +4 -1
  36. package/dist/auth.d.ts.map +1 -1
  37. package/dist/auth.js +343 -23
  38. package/dist/auth.js.map +1 -1
  39. package/dist/bundle.js +37330 -32005
  40. package/dist/cert-utils.d.ts +1 -1
  41. package/dist/cert-utils.d.ts.map +1 -1
  42. package/dist/cert-utils.js +19 -7
  43. package/dist/cert-utils.js.map +1 -1
  44. package/dist/client/delivery.d.ts +15 -1
  45. package/dist/client/delivery.d.ts.map +1 -1
  46. package/dist/client/delivery.js +147 -51
  47. package/dist/client/delivery.js.map +1 -1
  48. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  49. package/dist/client/rpc-pipeline.js +19 -5
  50. package/dist/client/rpc-pipeline.js.map +1 -1
  51. package/dist/client/v2-e2ee.d.ts +1 -0
  52. package/dist/client/v2-e2ee.d.ts.map +1 -1
  53. package/dist/client/v2-e2ee.js +104 -30
  54. package/dist/client/v2-e2ee.js.map +1 -1
  55. package/dist/client.d.ts +100 -1
  56. package/dist/client.d.ts.map +1 -1
  57. package/dist/client.js +1204 -23
  58. package/dist/client.js.map +1 -1
  59. package/dist/errors.d.ts.map +1 -1
  60. package/dist/errors.js +3 -2
  61. package/dist/errors.js.map +1 -1
  62. package/dist/events.d.ts +4 -0
  63. package/dist/events.d.ts.map +1 -1
  64. package/dist/events.js +41 -1
  65. package/dist/events.js.map +1 -1
  66. package/dist/facades.d.ts +82 -2
  67. package/dist/facades.d.ts.map +1 -1
  68. package/dist/facades.js +98 -4
  69. package/dist/facades.js.map +1 -1
  70. package/dist/index.d.ts +7 -3
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +3 -2
  73. package/dist/index.js.map +1 -1
  74. package/dist/keystore/index.d.ts +17 -0
  75. package/dist/keystore/index.d.ts.map +1 -1
  76. package/dist/keystore/indexeddb-shared.d.ts +10 -1
  77. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  78. package/dist/keystore/indexeddb-shared.js +111 -0
  79. package/dist/keystore/indexeddb-shared.js.map +1 -1
  80. package/dist/keystore/indexeddb-token-store.d.ts +7 -1
  81. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  82. package/dist/keystore/indexeddb-token-store.js +30 -1
  83. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  84. package/dist/org-protocol.d.ts +98 -0
  85. package/dist/org-protocol.d.ts.map +1 -0
  86. package/dist/org-protocol.js +1742 -0
  87. package/dist/org-protocol.js.map +1 -0
  88. package/dist/ownership-proof.d.ts +93 -5
  89. package/dist/ownership-proof.d.ts.map +1 -1
  90. package/dist/ownership-proof.js +239 -53
  91. package/dist/ownership-proof.js.map +1 -1
  92. package/dist/ownership-v21.d.ts +177 -0
  93. package/dist/ownership-v21.d.ts.map +1 -0
  94. package/dist/ownership-v21.js +609 -0
  95. package/dist/ownership-v21.js.map +1 -0
  96. package/dist/ownership-workflow.d.ts +146 -0
  97. package/dist/ownership-workflow.d.ts.map +1 -0
  98. package/dist/ownership-workflow.js +1066 -0
  99. package/dist/ownership-workflow.js.map +1 -0
  100. package/dist/seq-tracker.d.ts +1 -0
  101. package/dist/seq-tracker.d.ts.map +1 -1
  102. package/dist/seq-tracker.js +10 -2
  103. package/dist/seq-tracker.js.map +1 -1
  104. package/dist/tools/cross-sdk-agent.d.ts +82 -0
  105. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  106. package/dist/tools/cross-sdk-agent.js +332 -7
  107. package/dist/tools/cross-sdk-agent.js.map +1 -1
  108. package/dist/transport.d.ts +6 -0
  109. package/dist/transport.d.ts.map +1 -1
  110. package/dist/transport.js +88 -2
  111. package/dist/transport.js.map +1 -1
  112. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  113. package/dist/v2/e2ee/decrypt.d.ts.map +1 -1
  114. package/dist/v2/e2ee/decrypt.js +9 -1
  115. package/dist/v2/e2ee/decrypt.js.map +1 -1
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.js +1 -1
  118. package/package.json +1 -1
package/dist/agent-md.js CHANGED
@@ -1,9 +1,43 @@
1
1
  import { AID } from './aid.js';
2
+ import { parseDocument } from 'yaml';
2
3
  import { certMatchesFingerprint, parseAgentMdTailSignature } from './cert-utils.js';
3
- import { validateAgentMdDocument, verifyAgentMdUpload } from './agent-md-schema.js';
4
+ import { parseFileFrontmatter, validateAgentMdDocument, verifyAgentMdUpload } 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 { 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 = { Accept: 'text/markdown', '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: { '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) {
@@ -292,34 +423,296 @@ export class AgentMdManager {
292
423
  if (localContent === null || localContent.length === 0) {
293
424
  throw new ValidationError('uploadAgentMd requires local agent.md content');
294
425
  }
295
- validateAgentMdDocument(localContent, { expectedAid: target });
426
+ const parsed = validateAgentMdDocument(localContent, { expectedAid: target });
427
+ if (['group', 'org'].includes(String(parsed.frontmatter.type)))
428
+ return await this.uploadAs(target, localContent, current);
296
429
  const signedResult = await current.signAgentMd(localContent);
297
430
  if (!signedResult.ok) {
298
431
  throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
299
432
  }
300
433
  const signed = signedResult.data.signed;
301
434
  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: '',
314
- });
315
- return result;
435
+ return await this._uploadPrepared(target, signed, current);
436
+ }
437
+ async uploadAs(targetAid, content, signer) {
438
+ const target = this._safeAid(String(targetAid ?? '').trim());
439
+ const rawContent = String(content ?? '');
440
+ if (!rawContent.trim())
441
+ throw new ValidationError('uploadAgentMd requires non-empty content');
442
+ const parsed = validateAgentMdDocument(rawContent, { expectedAid: target });
443
+ 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);
447
+ }
448
+ if (!signer?.isPrivateKeyValid())
449
+ throw new StateError('uploadAgentMd requires a valid signer identity');
450
+ const signedResult = await signer.signAgentMd(rawContent);
451
+ if (!signedResult.ok)
452
+ throw new ClientSignatureError(signedResult.error.message || 'agent.md signing failed');
453
+ const signed = signedResult.data.signed;
454
+ await verifyAgentMdUpload(signed, signer.certPem, target);
455
+ return await this._uploadPrepared(target, signed, signer);
456
+ }
457
+ async pendingUpload(aid) {
458
+ const record = await this.loadRecord(aid);
459
+ if (record?.pending_operation !== 'replace' || !String(record.pending_content ?? '').trim())
460
+ return null;
461
+ 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 };
462
+ }
463
+ async resumePendingUpload(aid, signer) {
464
+ const pending = await this.pendingUpload(aid);
465
+ if (!pending)
466
+ return false;
467
+ const condition = pending.condition;
468
+ if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
469
+ 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' });
473
+ try {
474
+ if (['group', 'org'].includes(String(meta.type))) {
475
+ const identity = signer ?? this._currentAid();
476
+ if (!identity)
477
+ throw new StateError('entity upload recovery requires current owner', { stringCode: 'forbidden' });
478
+ const context = await this._entityContext(aid, meta, identity);
479
+ if (!pending.relation?.entity_cert_fingerprint)
480
+ throw new StateError('pending entity upload requires original owner and certificate context', { stringCode: 'persistence_unavailable' });
481
+ if (pending.relation.owner_aid !== context.owner_aid)
482
+ throw new StateError('pending entity upload belongs to a different owner', { stringCode: 'forbidden' });
483
+ if (pending.relation.entity_cert_fingerprint !== context.entity_cert_fingerprint)
484
+ throw new StateError('pending entity upload belongs to an old rekey generation', { stringCode: 'conflict' });
485
+ if (pending.content_etag !== await AgentMdManager.contentEtag(pending.content))
486
+ throw new StateError('pending entity original bytes are corrupt', { stringCode: 'persistence_unavailable' });
487
+ 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));
491
+ const remote = await this._readEntityFresh(aid);
492
+ 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: '' });
494
+ return true;
495
+ }
496
+ if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
497
+ throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
498
+ await this._uploadPrepared(aid, pending.content, identity, condition);
499
+ return true;
500
+ }
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
+ }
518
+ }
519
+ if (!relation && (remote.relation_version !== 0 || remote.lock !== 'unlocked'))
520
+ throw fail('unavailable', 'pending agent.md cannot overwrite relationship history', true);
521
+ 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: '' });
523
+ if (!saved.aid)
524
+ throw new StateError('agent.md pending completion persistence failed');
525
+ return true;
526
+ }
527
+ if (condition.if_match && (!remote.exists || remote.etag !== condition.if_match) || condition.if_none_match === '*' && remote.exists)
528
+ throw new StateError('pending agent.md original document condition changed', { stringCode: 'conflict' });
529
+ const identity = signer ?? this._currentAid();
530
+ if (!identity)
531
+ throw new StateError('pending agent.md requires a signer identity');
532
+ await verifyAgentMdUpload(pending.content, identity.certPem, aid);
533
+ await this._uploadPrepared(aid, pending.content, identity, condition);
534
+ return true;
535
+ }
536
+ catch (error) {
537
+ await this.saveRecord(aid, { pending_error: error instanceof Error ? error.message : String(error) });
538
+ throw error;
539
+ }
540
+ }
541
+ async _uploadPrepared(target, signed, signer, originalCondition) {
542
+ const meta = validateAgentMdDocument(signed, { expectedAid: target }).frontmatter;
543
+ const entity = ['group', 'org'].includes(String(meta.type));
544
+ const uploader = entity ? this._currentAid() ?? signer : signer;
545
+ const entityContext = entity ? await this._entityContext(target, meta, uploader) : undefined;
546
+ if (entity)
547
+ await verifyAgentMdUpload(signed, (await this._entityDocumentSigner(target, signed)).certPem, target);
548
+ let condition = originalCondition;
549
+ let relation = entityContext ?? (await this.pendingUpload(target))?.relation;
550
+ 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 };
558
+ }
559
+ }
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' });
566
+ }
567
+ 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 });
568
+ const savedCondition = pending.pending_condition;
569
+ if (pending.pending_content !== signed || savedCondition?.if_match !== condition.if_match || savedCondition?.if_none_match !== condition.if_none_match || pending.pending_authorization !== entityAuthorization)
570
+ throw new StateError('agent.md pending upload persistence failed');
571
+ try {
572
+ const owner = this._currentAid();
573
+ const delegated = target.toLowerCase() !== this._ownerAid().toLowerCase();
574
+ if (delegated && !owner?.isPrivateKeyValid())
575
+ throw new StateError('owner agent.md upload requires a loaded owner identity');
576
+ const authorization = entity ? entityAuthorization : delegated ? await this._ownerUploadAuthorization(target, signed, owner) : '';
577
+ const result = await this._uploadHttp(target, signed, authorization, condition);
578
+ const remoteEtag = String(result.etag ?? '').trim();
579
+ const saved = await this.saveRecord(target, {
580
+ content: signed, local_etag: await AgentMdManager.contentEtag(signed), remote_etag: remoteEtag || undefined,
581
+ last_modified: String(result.last_modified ?? result.lastModified ?? '').trim(), fetched_at: Date.now(), checked_at: Date.now(),
582
+ remote_status: remoteEtag ? 'found' : 'unknown', last_error: '',
583
+ pending_operation: '', pending_content: '', pending_content_etag: '', pending_error: '', pending_condition: {}, pending_relation: {}, pending_authorization: '',
584
+ });
585
+ if (!saved.aid)
586
+ throw new StateError('agent.md upload completion persistence failed');
587
+ return { ...result };
588
+ }
589
+ catch (error) {
590
+ await this.saveRecord(target, { pending_error: error instanceof Error ? error.message : String(error) });
591
+ throw error;
592
+ }
593
+ }
594
+ async readEntityAuthorityFresh(target) {
595
+ const fresh = await this._readEntityFresh(this._safeAid(target));
596
+ 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
+ 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 };
599
+ }
600
+ async _readEntityFresh(target) {
601
+ const gateway = await this._resolveGateway(target);
602
+ const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
603
+ const headers = { 'Cache-Control': 'no-cache', ...(viaGatewayHost ? { Host: target } : {}) };
604
+ let url = this._url(target, gateway);
605
+ if (viaGatewayHost) {
606
+ const parsed = new URL(gateway);
607
+ const port = parsed.port && parsed.port !== '443' && parsed.port !== '20001' ? `:${parsed.port}` : '';
608
+ url = `${agentMdHttpScheme(gateway)}://${parsed.hostname}${port}/agent.md`;
609
+ }
610
+ let response;
611
+ try {
612
+ response = await fetchWithTimeout(url, { method: 'GET', cache: 'no-store', redirect: 'error', headers });
613
+ }
614
+ catch (error) {
615
+ throw new StateError(`entity agent.md fresh read unavailable: ${String(error)}`, { stringCode: 'unavailable', retryable: true });
616
+ }
617
+ if (response.status !== 200 && response.status !== 404)
618
+ throw new StateError(`entity agent.md fresh read failed: HTTP ${response.status}`, { stringCode: 'unavailable', retryable: true });
619
+ const etag = headerValue(response.headers, 'ETag');
620
+ if (response.status === 200 && !etag)
621
+ throw new StateError('entity agent.md fresh read has no ETag', { stringCode: 'unavailable', retryable: true });
622
+ let content = '';
623
+ if (response.status === 200) {
624
+ try {
625
+ content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
626
+ }
627
+ catch {
628
+ throw new ValidationError('entity agent.md must be valid UTF-8', { stringCode: 'invalid' });
629
+ }
630
+ }
631
+ const facts = {};
632
+ 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']]) {
633
+ const value = headerValue(response.headers, header);
634
+ if (value)
635
+ facts[field] = value;
636
+ }
637
+ return { exists: response.status === 200, content, etag, ...facts };
638
+ }
639
+ async _entityCurrentContext(target, meta, uploading = false) {
640
+ const owner = String(meta.owner ?? '').trim().toLowerCase();
641
+ if (!this._entityOwnerResolver)
642
+ throw new StateError('entity owner resolver is unavailable', { stringCode: 'unavailable', retryable: true });
643
+ const freshOwner = String(await this._entityOwnerResolver(target, String(meta.type))).trim().toLowerCase();
644
+ if (!freshOwner)
645
+ throw new StateError('entity current owner is unavailable', { stringCode: 'unavailable', retryable: true });
646
+ if (freshOwner !== owner)
647
+ throw new StateError('entity current owner changed', { stringCode: uploading ? 'forbidden' : 'conflict' });
648
+ if (!this._entityIdentityGetter)
649
+ throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
650
+ const entity = await this._entityIdentityGetter(target, String(meta.type));
651
+ if (!entity.certFingerprint)
652
+ throw new StateError('entity current certificate is unavailable', { stringCode: 'unavailable', retryable: true });
653
+ if (meta.entity_cert_fingerprint !== entity.certFingerprint)
654
+ throw new StateError('entity agent.md certificate generation changed', { stringCode: 'conflict' });
655
+ return { type: meta.type, owner_aid: owner, entity_cert_fingerprint: entity.certFingerprint };
656
+ }
657
+ async _entityContext(target, meta, signer) {
658
+ 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' });
661
+ const context = await this._entityCurrentContext(target, meta, true);
662
+ if (signer.aid.toLowerCase() === owner)
663
+ await this._verifyEntityOwnerSigner(signer);
664
+ else if (signer.certFingerprint !== context.entity_cert_fingerprint)
665
+ throw new StateError('entity uploader certificate generation changed', { stringCode: 'conflict' });
666
+ return context;
667
+ }
668
+ async _entityDocumentSigner(target, content) {
669
+ const parsed = validateAgentMdDocument(content, { expectedAid: target, requireSignature: true });
670
+ const signerAid = parsed.signature.signer_aid ?? target;
671
+ let signer;
672
+ if (signerAid.toLowerCase() === target.toLowerCase()) {
673
+ if (!this._entityIdentityGetter)
674
+ throw new StateError('entity current certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
675
+ signer = await this._entityIdentityGetter(target, String(parsed.frontmatter.type));
676
+ }
677
+ else {
678
+ if (!this._entitySignerResolver)
679
+ throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
680
+ signer = await this._entitySignerResolver(signerAid, parsed.signature.cert_fingerprint);
681
+ }
682
+ if (signer.aid.toLowerCase() !== signerAid.toLowerCase() || signer.certFingerprint !== parsed.signature.cert_fingerprint)
683
+ throw new StateError('entity signing certificate changed', { stringCode: 'conflict' });
684
+ return signer;
685
+ }
686
+ async _verifyEntityOwnerSigner(owner) {
687
+ if (!this._entitySignerResolver)
688
+ throw new StateError('entity owner signing certificate resolver is unavailable', { stringCode: 'unavailable', retryable: true });
689
+ const fresh = await this._entitySignerResolver(owner.aid, owner.certFingerprint);
690
+ if (fresh.aid.toLowerCase() !== owner.aid.toLowerCase() || fresh.certFingerprint !== owner.certFingerprint) {
691
+ throw new StateError('entity owner signing certificate changed', { stringCode: 'conflict' });
692
+ }
316
693
  }
317
694
  async download(aid, timeoutMs = DEFAULT_HTTP_TIMEOUT_MS) {
318
695
  const target = this._safeAid(String(aid ?? this._ownerAid() ?? '').trim());
319
696
  const contentResult = await this._downloadHttp(target, timeoutMs);
320
697
  const parsed = parseAgentMdTailSignature(contentResult.content);
698
+ let entity = false;
699
+ try {
700
+ entity = ['group', 'org'].includes(String(parseFileFrontmatter(parsed.payload).type));
701
+ }
702
+ catch { }
703
+ let frontmatter;
704
+ try {
705
+ frontmatter = validateAgentMdDocument(contentResult.content, { expectedAid: target, validateOwnership: false, ...(entity ? { requireSignature: true } : {}) }).frontmatter;
706
+ }
707
+ catch (error) {
708
+ if (entity)
709
+ throw new ValidationError(error instanceof Error ? error.message : String(error), { stringCode: 'invalid' });
710
+ throw error;
711
+ }
321
712
  const expectedFp = parsed.fields?.cert_fingerprint || parsed.fields?.public_key_fingerprint || null;
322
- const peer = await this._resolvePeer(target, expectedFp);
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);
323
716
  const verified = await peer.verifyAgentMd(contentResult.content);
324
717
  if (!verified.ok)
325
718
  throw new AUNError(verified.error.message);
@@ -327,9 +720,30 @@ export class AgentMdManager {
327
720
  const statusText = String(signature.status ?? 'invalid');
328
721
  const reason = String(signature.reason ?? '').trim();
329
722
  if (statusText === 'invalid') {
330
- throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`);
723
+ throw new ClientSignatureError(`agent.md verification failed for ${target}${reason ? `: ${reason}` : ''}`, entity ? { stringCode: 'invalid' } : undefined);
724
+ }
725
+ let ownershipStatus;
726
+ if (entity)
727
+ ownershipStatus = 'not_applicable';
728
+ else {
729
+ try {
730
+ validateOwnershipFrontmatter(frontmatter, target);
731
+ const rawProof = (frontmatter.ownership_proof ?? {});
732
+ const subjectSignature = rawProof.subject_signature;
733
+ const ownerSignature = rawProof.owner_signature;
734
+ const release = frontmatter.ownership_release;
735
+ const subjectCertPem = subjectSignature ? (await this._resolvePeer(target, String(subjectSignature.cert_fingerprint), isJsonObject(subjectSignature.statement) ? Number(subjectSignature.statement.signed_at) : undefined)).certPem : undefined;
736
+ const ownerAid = String(ownerSignature ? frontmatter.owner : release?.statement && typeof release.statement === 'object' ? release.statement.owner_aid : '');
737
+ const ownerFingerprint = String(ownerSignature?.cert_fingerprint ?? release?.cert_fingerprint ?? '');
738
+ const ownerStatement = (ownerSignature ?? release)?.statement;
739
+ const ownerCertPem = ownerFingerprint ? (await this._resolvePeer(ownerAid, ownerFingerprint, isJsonObject(ownerStatement) ? Number(ownerStatement.signed_at) : undefined)).certPem : undefined;
740
+ ownershipStatus = await verifyOwnershipFrontmatterStatus(frontmatter, subjectCertPem, ownerCertPem);
741
+ }
742
+ catch {
743
+ ownershipStatus = 'invalid';
744
+ }
331
745
  }
332
- const verification = { status: statusText };
746
+ const verification = { status: statusText, ownership_status: ownershipStatus };
333
747
  if (reason)
334
748
  verification.reason = reason;
335
749
  const localEtag = await AgentMdManager.contentEtag(contentResult.content);
@@ -690,14 +1104,14 @@ export class AgentMdManager {
690
1104
  async _resolveGateway(aid) {
691
1105
  return String(await this._gatewayResolver?.(aid) ?? '');
692
1106
  }
693
- async _resolvePeer(aid, certFingerprint) {
1107
+ async _resolvePeer(aid, certFingerprint, signedAt) {
694
1108
  const expectedFp = String(certFingerprint ?? '').trim().toLowerCase();
695
1109
  const current = this._currentAid();
696
1110
  if (current && current.aid.toLowerCase() === aid.toLowerCase()) {
697
1111
  if (!expectedFp || await certMatchesFingerprint(current.certPem, expectedFp))
698
1112
  return current;
699
1113
  }
700
- const peer = await this._peerResolver?.(aid, expectedFp || null);
1114
+ const peer = await this._peerResolver?.(aid, expectedFp || null, signedAt);
701
1115
  if (!(peer instanceof AID)) {
702
1116
  throw new StateError(`agent.md peer resolver did not return AID for ${aid}`);
703
1117
  }
@@ -709,22 +1123,87 @@ export class AgentMdManager {
709
1123
  throw new StateError('authenticate did not return access_token');
710
1124
  return token;
711
1125
  }
1126
+ async _uploadAccessToken(aid, gatewayUrl) {
1127
+ const resolver = this._uploadAccessTokenResolver ?? this._accessTokenResolver;
1128
+ const token = String(await resolver?.(aid, gatewayUrl) ?? '').trim();
1129
+ if (!token)
1130
+ throw new StateError('authenticate did not return access_token');
1131
+ return token;
1132
+ }
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 = '') {
1164
+ const bodySha256 = await sha256Hex(content);
1165
+ const timestamp = Math.floor(Date.now() / 1000);
1166
+ 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}`;
1168
+ const signed = await owner.sign(canonical);
1169
+ if (!signed.ok || !signed.data) {
1170
+ const message = signed.error?.message || 'owner authorization signing failed';
1171
+ throw new ClientSignatureError(message);
1172
+ }
1173
+ return base64UrlEncode(JSON.stringify({
1174
+ version: entityFingerprint ? 2 : 1,
1175
+ target_aid: target.toLowerCase(),
1176
+ uploader_aid: owner.aid.toLowerCase(),
1177
+ body_sha256: bodySha256,
1178
+ ...(entityFingerprint ? { entity_cert_fingerprint: entityFingerprint } : {}),
1179
+ timestamp,
1180
+ nonce,
1181
+ cert_fingerprint: owner.certFingerprint,
1182
+ signature: signed.data.signature,
1183
+ }));
1184
+ }
712
1185
  _url(aid, gatewayUrl) {
713
1186
  return `${agentMdHttpScheme(gatewayUrl)}://${this._safeAid(aid)}/agent.md`;
714
1187
  }
715
- async _uploadHttp(aid, content, ownerOperationId) {
1188
+ async _uploadHttp(aid, content, ownerAuthorization = '', condition) {
1189
+ if (!condition || Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*'))
1190
+ throw new StateError('agent.md upload requires its original document condition', { stringCode: 'persistence_unavailable' });
716
1191
  const gatewayUrl = await this._resolveGateway(aid);
717
- const token = await this._accessToken(aid, gatewayUrl);
1192
+ const authorization = String(ownerAuthorization ?? '').trim();
1193
+ const entity = ['group', 'org'].includes(String(validateAgentMdDocument(content, { expectedAid: aid }).frontmatter.type));
718
1194
  // 浏览器禁止覆盖 Host;动态 AID URL 由浏览器/Docker 的通配解析路由。
719
1195
  const viaGatewayHost = this._uploadViaGatewayHost && nodeBuiltin('node:http') !== null;
720
1196
  const headers = {
721
- Authorization: `Bearer ${token}`,
722
1197
  'Content-Type': 'text/markdown; charset=utf-8',
1198
+ ...(!entity ? { 'X-AUN-Ownership-Check': '1' } : {}),
1199
+ ...(condition.if_match ? { 'If-Match': condition.if_match } : { 'If-None-Match': '*' }),
723
1200
  ...(viaGatewayHost ? { Host: this._safeAid(aid) } : {}),
1201
+ ...(authorization ? { [entity ? 'AUN-AGENTMD-OWNER-AUTHORIZATION' : 'X-AUN-AgentMD-Owner-Authorization']: authorization } : {}),
724
1202
  };
725
- const operationId = String(ownerOperationId ?? '').trim();
726
- if (operationId)
727
- headers['X-AUN-Owner-Operation-ID'] = operationId;
1203
+ if (!authorization || entity) {
1204
+ const token = await this._uploadAccessToken(aid, gatewayUrl);
1205
+ headers.Authorization = `Bearer ${token}`;
1206
+ }
728
1207
  let url = this._url(aid, gatewayUrl);
729
1208
  if (viaGatewayHost) {
730
1209
  try {
@@ -765,6 +1244,10 @@ export class AgentMdManager {
765
1244
  }
766
1245
  if (!response.ok) {
767
1246
  const message = (await response.text()).trim();
1247
+ if (entity) {
1248
+ 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';
1249
+ throw new StateError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, { stringCode: code, retryable: code === 'unavailable' });
1250
+ }
768
1251
  throw new AUNError(`upload agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
769
1252
  }
770
1253
  const payload = await response.json();
@@ -806,9 +1289,15 @@ export class AgentMdManager {
806
1289
  }
807
1290
  if (!response.ok) {
808
1291
  const message = (await response.text()).trim();
809
- throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`);
1292
+ throw new AUNError(`download agent.md failed: HTTP ${response.status}${message ? ` - ${message}` : ''}`, response.status >= 500 ? { stringCode: 'unavailable', retryable: true } : undefined);
1293
+ }
1294
+ let content;
1295
+ try {
1296
+ content = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(await response.arrayBuffer());
1297
+ }
1298
+ catch {
1299
+ throw new ValidationError('agent.md must be valid UTF-8', { stringCode: 'invalid' });
810
1300
  }
811
- const content = await response.text();
812
1301
  return {
813
1302
  content,
814
1303
  etag: headerValue(response.headers, 'ETag') || cachedEtag,