@agentunion/fastaun 0.5.19 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +129 -78
  2. package/_packed_docs/CHANGELOG.md +129 -78
  3. package/_packed_docs/INDEX.md +201 -195
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  7. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
  8. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  9. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  10. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
  13. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  14. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  15. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  16. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
  17. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  18. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
  19. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  20. package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
  21. package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
  22. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  23. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
  24. package/_packed_docs/sdk/INDEX.md +92 -80
  25. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  26. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  27. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
  28. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  29. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
  30. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
  31. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
  33. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  34. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  35. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
  36. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
  37. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
  38. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
  39. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
  40. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
  41. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
  42. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
  43. package/dist/agent-md-schema.d.ts +13 -1
  44. package/dist/agent-md-schema.js +61 -33
  45. package/dist/agent-md-schema.js.map +1 -1
  46. package/dist/agent-md.d.ts +8 -1
  47. package/dist/agent-md.js +243 -119
  48. package/dist/agent-md.js.map +1 -1
  49. package/dist/aid-store.d.ts +29 -0
  50. package/dist/aid-store.js +179 -34
  51. package/dist/aid-store.js.map +1 -1
  52. package/dist/aid.js +18 -13
  53. package/dist/aid.js.map +1 -1
  54. package/dist/auth.js +74 -2
  55. package/dist/auth.js.map +1 -1
  56. package/dist/cert-utils.d.ts +43 -1
  57. package/dist/cert-utils.js +144 -56
  58. package/dist/cert-utils.js.map +1 -1
  59. package/dist/client/delivery.d.ts +9 -0
  60. package/dist/client/delivery.js +179 -11
  61. package/dist/client/delivery.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +1 -0
  63. package/dist/client/rpc-pipeline.js +125 -82
  64. package/dist/client/rpc-pipeline.js.map +1 -1
  65. package/dist/client/v2-e2ee.js +95 -89
  66. package/dist/client/v2-e2ee.js.map +1 -1
  67. package/dist/client.js +48 -19
  68. package/dist/client.js.map +1 -1
  69. package/dist/events.d.ts +5 -3
  70. package/dist/events.js +25 -8
  71. package/dist/events.js.map +1 -1
  72. package/dist/facades.js +5 -5
  73. package/dist/facades.js.map +1 -1
  74. package/dist/index.d.ts +2 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/keystore/aid-db.d.ts +1 -1
  77. package/dist/keystore/aid-db.js +11 -2
  78. package/dist/keystore/aid-db.js.map +1 -1
  79. package/dist/keystore/index.d.ts +1 -1
  80. package/dist/keystore/local-token-store.d.ts +1 -1
  81. package/dist/keystore/local-token-store.js +2 -2
  82. package/dist/keystore/local-token-store.js.map +1 -1
  83. package/dist/org-protocol.js +77 -13
  84. package/dist/org-protocol.js.map +1 -1
  85. package/dist/ownership-v21.d.ts +26 -8
  86. package/dist/ownership-v21.js +147 -70
  87. package/dist/ownership-v21.js.map +1 -1
  88. package/dist/ownership-workflow.d.ts +5 -0
  89. package/dist/ownership-workflow.js +366 -56
  90. package/dist/ownership-workflow.js.map +1 -1
  91. package/dist/tools/cross-sdk-agent.d.ts +2 -1
  92. package/dist/tools/cross-sdk-agent.js +109 -6
  93. package/dist/tools/cross-sdk-agent.js.map +1 -1
  94. package/dist/transport.d.ts +1 -0
  95. package/dist/transport.js +10 -0
  96. package/dist/transport.js.map +1 -1
  97. package/dist/version.d.ts +1 -1
  98. package/dist/version.js +1 -1
  99. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
  import { updateOwnershipAgentMd } from './agent-md.js';
3
3
  import { parseFileFrontmatter, validateAgentMdCertificate, validateAgentMdDocument } from './agent-md-schema.js';
4
- import { buildAgentMdSignatureBlock, certFingerprint, isStrictBase64, parseAgentMdTailSignature, publicKeyFingerprint, publicKeyMatchesFingerprint, signBytes, verifySignatureWithCert } from './cert-utils.js';
4
+ import { certFingerprint, isStrictBase64, parseAgentMdTailSignature, publicKeyMatchesFingerprint, signDocument, verifyDocument } from './cert-utils.js';
5
5
  import { StateError, ValidationError } from './errors.js';
6
6
  const AID_RE = /^[a-z0-9_][a-z0-9_-]*(?:\.[a-z0-9_][a-z0-9_-]*)+$/;
7
7
  export const OWNERSHIP_UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
@@ -108,7 +108,10 @@ export function parseOwnershipFile(content, role, context = {}) {
108
108
  if (raw.length - new TextEncoder().encode(parsed.payload).length > 2048)
109
109
  throw new ValidationError('ownership file signature exceeds 2048 bytes');
110
110
  const fields = parsed.fields;
111
- if (fields) {
111
+ if (fields?.signer_aid !== undefined)
112
+ throw new ValidationError('ownership main signature contains unknown signer_aid');
113
+ for (const item of parsed.signatures) {
114
+ const fields = item.fields;
112
115
  if (!/^sha256:[0-9a-f]{64}$/.test(fields.cert_fingerprint) || 'public_key_fingerprint' in fields && !/^sha256:[0-9a-f]{64}$/.test(fields.public_key_fingerprint))
113
116
  throw new ValidationError('ownership file fingerprint is invalid');
114
117
  if (!/^[1-9][0-9]*$/.test(fields.timestamp) || !Number.isSafeInteger(Number(fields.timestamp)))
@@ -187,8 +190,18 @@ export function verifyOwnershipFile(content, role, certificate, context = {}) {
187
190
  throw new ValidationError('ownership certificate fingerprint mismatch');
188
191
  if ('public_key_fingerprint' in fields && !publicKeyMatchesFingerprint(certificate.cert_pem, fields.public_key_fingerprint))
189
192
  throw new ValidationError('ownership public key fingerprint mismatch');
190
- if (!verifySignatureWithCert(certificate.cert_pem, Buffer.from(fields.signature, 'base64'), Buffer.from(parsed.payload, 'utf8')))
191
- throw new ValidationError('ownership file signature verification failed');
193
+ const additional = new Map();
194
+ for (const item of parseAgentMdTailSignature(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(parsed.raw)).signatures.slice(1)) {
195
+ const fields = item.fields, extra = context.certificateResolver?.(fields.signer_aid, fields.cert_fingerprint, Number(fields.timestamp));
196
+ if (extra && (extra.chain_valid === false || extra.revoked === true || typeof extra.state === 'string' && extra.state !== 'active_signing'))
197
+ throw new ValidationError('ownership additional signing certificate is not currently verified and active_signing', { stringCode: 'invalid' });
198
+ if (!extra || extra.chain_valid !== true || extra.revoked !== false || extra.revocation_checked !== true || extra.state !== 'active_signing' || !Number.isSafeInteger(extra.observed_at) || extra.observed_at <= 0 || extra.observed_at > now + 5 || extra.observed_at < now - 30 || !extra.cert_pem?.trim())
199
+ throw new StateError('ownership additional certificate current status is unavailable', { stringCode: 'unavailable', retryable: true });
200
+ additional.set(`${fields.signer_aid};${fields.cert_fingerprint}`, extra.cert_pem);
201
+ }
202
+ const verified = verifyDocument(new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(parsed.raw), { expectedAid: signer, primaryCertPem: certificate.cert_pem, requireCurrentValidity: true, now, certificateResolver: (aid, fingerprint) => additional.get(`${aid};${fingerprint}`) });
203
+ if (!verified.valid)
204
+ throw new ValidationError(verified.reason ?? 'ownership file signature verification failed');
192
205
  return parsed;
193
206
  }
194
207
  export function signOwnershipFile(identity, content, role, context = {}) {
@@ -204,8 +217,7 @@ export function signOwnershipFile(identity, content, role, context = {}) {
204
217
  throw new ValidationError('ownership payload must end with a newline before signature');
205
218
  const now = Math.trunc(context.now ?? Date.now() / 1000);
206
219
  validateAgentMdCertificate(identity.certPem, identity.aid, now, { now });
207
- const signature = signBytes(identity.privateKeyPem, Buffer.from(parsed.payload, 'utf8')).toString('base64');
208
- return parsed.payload + buildAgentMdSignatureBlock(identity.certFingerprint, now, signature, publicKeyFingerprint(identity.certPem), role === 'agent' ? 'AUN-SIGNATURE-AGENTMD-V2' : 'AUN-SIGNATURE-OWNERSHIP-V2');
220
+ return signDocument(identity, parsed.payload, { timestamp: now, tag: role === 'agent' ? 'AGENTMD-V2' : 'OWNERSHIP-V2' }).signed;
209
221
  }
210
222
  export function ownershipSha256(content) {
211
223
  return createHash('sha256').update(content).digest('hex');
@@ -225,14 +237,13 @@ export class OwnershipFiles {
225
237
  async checkConsensus(subAid, target) {
226
238
  ownershipAid(subAid, 'sub_aid');
227
239
  const began = this.deps.now();
228
- const result = { status: 'unavailable', sub_aid: subAid, owner_aid: null, relation_version: 0, lock: 'unlocked', agent_md_etag: '', agent_md_sha256: '', owner_md_revision: '', owner_md_sha256: '' };
240
+ const result = { status: 'unavailable', sub_aid: subAid, owner_aid: null, relation_version: 0, lock: 'unlocked', agent_md_etag: '', agent_md_sha256: '', owner_md_sha256: '' };
229
241
  try {
230
242
  const before = await this.readAgent(subAid);
231
243
  Object.assign(result, { relation_version: before.relation_version, lock: before.lock, agent_md_etag: before.etag, agent_md_sha256: before.exists ? ownershipSha256(before.content) : '' });
232
- const released = before.lock === 'unlocked' && before.relation_version > 0;
233
- const agent = before.exists && !released ? await this.verifyFile(before.content, 'agent', subAid) : undefined;
244
+ const agent = before.exists ? await this.verifyFile(before.content, 'agent', subAid) : undefined;
234
245
  const statement = agent?.statement;
235
- if (before.lock === 'unlocked' && before.relation_version === 0 && statement)
246
+ if (before.relation_version !== (statement?.relation_version ?? 0))
236
247
  throw ownershipFailure('unavailable', 'NS relation metadata does not match agent.md', true);
237
248
  if (before.lock === 'locked' && (!statement || statement.relation_version !== before.relation_version))
238
249
  throw ownershipFailure('unavailable', 'NS locked relation does not match agent.md', true);
@@ -240,29 +251,36 @@ export class OwnershipFiles {
240
251
  if (before.owner_aid !== undefined && before.owner_aid !== currentOwner)
241
252
  throw ownershipFailure('unavailable', 'NS owner metadata does not match agent.md', true);
242
253
  result.owner_aid = currentOwner;
243
- result.status = before.lock === 'unlocked' ? before.relation_version === 0 ? 'unbound' : 'released' : 'single_sided';
244
- const ownerAid = released ? undefined : currentOwner ?? target?.owner_aid ?? statement?.owner_aid;
254
+ result.status = statement ? 'single_sided' : 'unbound';
255
+ const ownerAid = statement?.owner_aid ?? target?.owner_aid;
256
+ let ownerBefore;
245
257
  if (ownerAid) {
246
258
  const owner = await this.readOwner(ownerAid, subAid);
247
- result.owner_md_revision = owner.revision;
248
- result.owner_md_sha256 = owner.sha256 ?? '';
259
+ ownerBefore = owner;
260
+ result.owner_md_sha256 = owner.exists ? owner.sha256 ?? '' : '';
249
261
  if (owner.exists) {
250
262
  const declaration = await this.verifyFile(owner.content, 'owner', subAid, ownerAid);
251
- if (before.lock === 'locked') {
263
+ if (statement) {
252
264
  if (declaration.statement.relation_version !== before.relation_version)
253
265
  throw new ValidationError('ownership relation_version mismatch');
254
- if (declaration.action === 'release')
255
- throw ownershipFailure('unavailable', 'NS has not confirmed the current release', true);
256
- result.status = 'consensus';
266
+ result.status = declaration.action === 'release' ? 'released' : 'consensus';
257
267
  }
258
268
  else if (target && declaration.action === 'declare' && declaration.statement.relation_version === target.relation_version && target.relation_version === before.relation_version + 1) {
259
269
  result.status = 'single_sided';
260
270
  }
261
271
  }
262
- else if (before.lock === 'locked')
263
- throw ownershipFailure('unavailable', 'NS has not confirmed owner object absence', true);
272
+ else if (statement)
273
+ result.status = 'released';
264
274
  }
275
+ if (statement && before.lock !== (result.status === 'released' ? 'unlocked' : 'locked'))
276
+ throw ownershipFailure('unavailable', 'NS lock does not match current ownership materials', true);
277
+ result.owner_aid = result.status === 'consensus' ? statement.owner_aid : null;
265
278
  const after = await this.readAgent(subAid);
279
+ if (ownerAid && ownerBefore) {
280
+ const ownerAfter = await this.readOwner(ownerAid, subAid);
281
+ if (ownerBefore.exists !== ownerAfter.exists || ownerBefore.content !== ownerAfter.content || ownerBefore.sha256 !== ownerAfter.sha256)
282
+ throw ownershipFailure('unavailable', 'owner.md changed during consensus check', true);
283
+ }
266
284
  if (before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.content !== after.content || before.owner_aid !== after.owner_aid || this.deps.now() - began > 30)
267
285
  throw ownershipFailure('unavailable', 'NS relation changed or verification window elapsed during consensus check', true);
268
286
  return result;
@@ -316,11 +334,15 @@ export class OwnershipFiles {
316
334
  return this.publishAgent(task, async (agent) => {
317
335
  if (agent.lock === 'locked')
318
336
  throw ownershipFailure('owner_locked', 'NS has not confirmed release');
337
+ const parsed = parseOwnershipFile(agent.content, 'agent', { expectedSubAid: task.sub_aid });
338
+ if (!parsed.statement && agent.relation_version === 0)
339
+ return {};
319
340
  if (agent.relation_version !== task.target_relation_version)
320
341
  throw ownershipFailure('conflict', 'release relation_version changed');
321
- const parsed = parseOwnershipFile(agent.content, 'agent', { expectedSubAid: task.sub_aid });
322
342
  if (parsed.statement && (parsed.statement.owner_aid !== task.owner_aid || parsed.statement.relation_version !== task.base_relation_version))
323
343
  throw ownershipFailure('conflict', 'agent.md no longer contains the released relation');
344
+ if (task.expected_agent_statement && Object.keys(task.expected_agent_statement).some((field) => task.expected_agent_statement[field] !== parsed.statement?.[field]))
345
+ throw ownershipFailure('conflict', 'agent.md no longer contains the approved statement');
324
346
  return {};
325
347
  }, true);
326
348
  }
@@ -338,8 +360,8 @@ export class OwnershipFiles {
338
360
  async cleanupOwnerFile(task) {
339
361
  this.requireLocal(task.owner_aid);
340
362
  const source = this.savedFile(task, task.operation === 'release' ? 'release' : 'transfer');
341
- if (!source?.published_revision)
342
- throw ownershipFailure('conflict', 'original published owner revision is unavailable');
363
+ if (!source)
364
+ throw ownershipFailure('conflict', 'original owner file is unavailable');
343
365
  const consensus = await this.checkConsensus(task.sub_aid);
344
366
  if (consensus.status === 'not_applicable')
345
367
  throw ownershipFailure('not_applicable', consensus.reason ?? 'group/org use their dedicated lifecycle ownership');
@@ -347,19 +369,24 @@ export class OwnershipFiles {
347
369
  throw new ValidationError(consensus.reason ?? 'ownership consensus is invalid', { stringCode: 'invalid' });
348
370
  if (consensus.status === 'unavailable')
349
371
  throw ownershipFailure(consensus.code ?? 'unavailable', consensus.reason ?? 'ownership consensus is unavailable', true);
350
- if (consensus.relation_version !== task.target_relation_version || task.operation === 'release' && consensus.status !== 'released' || task.operation === 'transfer' && (consensus.status !== 'consensus' || consensus.owner_aid !== task.new_owner_aid))
351
- throw ownershipFailure('conflict', 'NS has not confirmed this task target');
372
+ if (task.operation === 'release' && consensus.status === 'released')
373
+ return;
374
+ if (task.operation === 'release' ? consensus.status !== 'unbound' && consensus.status !== 'consensus' : consensus.relation_version !== task.target_relation_version || consensus.status !== 'consensus' || consensus.owner_aid !== task.new_owner_aid)
375
+ throw ownershipFailure('conflict', 'current materials do not permit this task cleanup');
352
376
  const current = await this.readOwner(task.owner_aid, task.sub_aid);
353
- if (current.exists && (current.revision !== source.published_revision || current.sha256 !== source.hash))
354
- throw ownershipFailure('conflict', 'owner file revision changed before cleanup');
377
+ if (current.exists && (current.content !== source.raw || current.sha256 !== source.hash))
378
+ throw ownershipFailure('conflict', 'owner file changed before cleanup');
355
379
  if (!task.files.cleanup) {
356
- task.files.cleanup = { raw: source.raw, hash: source.hash, condition: { expected_revision: source.published_revision } };
380
+ task.files.cleanup = { raw: source.raw, hash: source.hash, condition: {} };
357
381
  await task.persist();
358
382
  }
359
383
  else
360
384
  this.savedFile(task, 'cleanup');
361
- if (current.exists)
362
- await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid, source.published_revision);
385
+ const beforeDelete = await this.readOwner(task.owner_aid, task.sub_aid);
386
+ if (beforeDelete.exists && (beforeDelete.content !== source.raw || beforeDelete.sha256 !== source.hash))
387
+ throw ownershipFailure('conflict', 'owner file changed before cleanup');
388
+ if (beforeDelete.exists)
389
+ await this.deps.deleteOwnerMd(task.owner_aid, task.sub_aid);
363
390
  task.files.cleanup.deleted = true;
364
391
  await task.persist();
365
392
  }
@@ -379,7 +406,7 @@ export class OwnershipFiles {
379
406
  async readOwner(ownerAid, subAid) {
380
407
  const value = await this.deps.readOwnerMd(ownerAid, subAid);
381
408
  const now = this.deps.now();
382
- if (value.owner_aid !== ownerAid || value.sub_aid !== undefined && value.sub_aid !== subAid || value.bucket !== 'default' || value.path !== `sub_aids/${subAid}/owner.md` || typeof value.exists !== 'boolean' || typeof value.revision !== 'string' || !value.revision || !Number.isSafeInteger(value.observed_at) || value.observed_at > now + 5 || value.observed_at < now - 30 || value.authoritative !== true || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}` || value.service_aid !== value.authenticated_service_aid || typeof value.content !== 'string' || value.exists && !/^[0-9a-f]{64}$/.test(value.sha256 ?? ''))
409
+ if (value.owner_aid !== ownerAid || value.sub_aid !== undefined && value.sub_aid !== subAid || value.bucket !== 'default' || value.path !== `sub_aids/${subAid}/owner.md` || typeof value.exists !== 'boolean' || !Number.isSafeInteger(value.observed_at) || value.observed_at > now + 5 || value.observed_at < now - 30 || value.authoritative !== true || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}` || value.service_aid !== value.authenticated_service_aid || typeof value.content !== 'string' || value.exists && !/^[0-9a-f]{64}$/.test(value.sha256 ?? ''))
383
410
  throw ownershipFailure('unavailable', 'Storage authoritative observation is incomplete or changed', true);
384
411
  if (value.exists && value.sha256 !== ownershipSha256(value.content))
385
412
  throw ownershipFailure('unavailable', 'Storage complete file digest does not match authoritative observation', true);
@@ -396,13 +423,19 @@ export class OwnershipFiles {
396
423
  const parsed = parseOwnershipFile(raw, role, context);
397
424
  const signer = role === 'agent' ? subAid : String(parsed.frontmatter.owner_aid);
398
425
  const certificate = await this.deps.resolveCertificate(signer, parsed.signature.cert_fingerprint, role === 'agent' && parsed.frontmatter.type === 'group' ? 'group_identity' : undefined);
399
- return verifyOwnershipFile(raw, role, certificate, { ...context, now: this.deps.now() });
426
+ const additional = new Map();
427
+ for (const item of parseAgentMdTailSignature(raw).signatures.slice(1))
428
+ additional.set(`${item.fields.signer_aid};${item.fields.cert_fingerprint}`, await this.deps.resolveCertificate(item.fields.signer_aid, item.fields.cert_fingerprint));
429
+ return verifyOwnershipFile(raw, role, certificate, { ...context, now: this.deps.now(), certificateResolver: (aid, fingerprint) => additional.get(`${aid};${fingerprint}`) });
400
430
  }
401
431
  async requireTransfer(task) {
402
432
  const owner = await this.readOwner(task.owner_aid, task.sub_aid);
403
433
  if (!owner.exists)
404
434
  throw ownershipFailure('unavailable', 'original owner authorization is missing', true);
405
435
  const authorization = await this.verifyFile(owner.content, 'owner', task.sub_aid, task.owner_aid);
436
+ const saved = task.files.transfer;
437
+ if (saved && (saved.raw !== owner.content || saved.hash !== owner.sha256))
438
+ throw ownershipFailure('conflict', 'fixed transfer authorization changed');
406
439
  if (authorization.action !== 'transfer_owner' || authorization.statement.relation_version !== task.base_relation_version || authorization.transfer.new_owner_aid !== task.new_owner_aid || authorization.transfer.target_relation_version !== task.target_relation_version)
407
440
  throw ownershipFailure('conflict', 'transfer authorization does not match this task');
408
441
  const expiresAt = authorization.transfer.expires_at;
@@ -429,17 +462,22 @@ export class OwnershipFiles {
429
462
  }
430
463
  async assertBase(task, agent) {
431
464
  ownershipInteger(task.base_relation_version, 'base_relation_version', 0);
432
- if (ownershipInteger(task.target_relation_version, 'target_relation_version') !== task.base_relation_version + 1 || agent.relation_version !== task.base_relation_version)
465
+ if (ownershipInteger(task.target_relation_version, 'target_relation_version') !== task.base_relation_version + (task.operation === 'release' ? 0 : 1) || agent.relation_version !== task.base_relation_version)
433
466
  throw ownershipFailure('conflict', 'NS relation_version changed');
434
467
  if (!agent.exists)
435
468
  throw ownershipFailure('unavailable', 'initial agent.md is not published', true);
436
- const parsed = agent.lock === 'unlocked' && agent.relation_version > 0 ? parseOwnershipFile(agent.content, 'agent', { expectedSubAid: task.sub_aid }) : await this.verifyFile(agent.content, 'agent', task.sub_aid);
437
- if (agent.lock === 'unlocked' && agent.relation_version === 0 && parsed.statement)
469
+ const parsed = await this.verifyFile(agent.content, 'agent', task.sub_aid);
470
+ const base = task.base_agent_md;
471
+ if (base && (agent.exists !== base.exists || (base.statement ? !parsed.statement || Object.keys(base.statement).some((field) => base.statement[field] !== parsed.statement[field]) : parsed.statement || agent.content !== base.raw || ownershipSha256(agent.content) !== base.sha256)))
472
+ throw ownershipFailure('conflict', 'the original ownership statement was replaced');
473
+ if (agent.relation_version !== (parsed.statement?.relation_version ?? 0))
438
474
  throw ownershipFailure('unavailable', 'NS relation metadata does not match agent.md', true);
439
475
  if (task.operation === 'bind' && parsed.frontmatter.type === 'human')
440
476
  throw ownershipFailure('invalid', 'human cannot declare an owner');
441
477
  if (task.operation === 'bind' && agent.lock !== 'unlocked')
442
478
  throw ownershipFailure('owner_locked', 'an existing owner relation is locked');
479
+ if (task.operation === 'bind' && parsed.statement?.owner_aid === this.targetOwner(task))
480
+ throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
443
481
  if (task.operation !== 'bind' && (agent.lock !== 'locked' || parsed.statement?.owner_aid !== task.owner_aid || parsed.statement.relation_version !== task.base_relation_version))
444
482
  throw ownershipFailure('conflict', 'the original relation is no longer current');
445
483
  }
@@ -451,25 +489,35 @@ export class OwnershipFiles {
451
489
  }
452
490
  savedFile(task, key) {
453
491
  const saved = task.files[key];
454
- if (saved && (typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !this.validSavedCondition(key, saved.condition) || saved.published_revision !== undefined && (typeof saved.published_revision !== 'string' || !saved.published_revision)))
492
+ if (saved && (typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !this.validSavedCondition(key, saved.condition) || saved.published_etag !== undefined && (typeof saved.published_etag !== 'string' || !saved.published_etag)))
455
493
  throw ownershipFailure('persistence_unavailable', 'saved ownership file is corrupt');
494
+ const original = saved?.original_owner;
495
+ if (saved && key !== 'agent' && key !== 'cleanup' && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
496
+ throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is corrupt');
456
497
  return saved;
457
498
  }
458
499
  validSavedCondition(key, condition) {
459
- if (!condition || typeof condition !== 'object' || Object.keys(condition).length !== 1)
500
+ if (!condition || typeof condition !== 'object')
501
+ return false;
502
+ if (key !== 'agent')
503
+ return Object.keys(condition).length === 0;
504
+ if (Object.keys(condition).length !== 1)
460
505
  return false;
461
506
  if (key === 'agent')
462
507
  return typeof condition.if_match === 'string' && condition.if_match.length > 0 || condition.if_none_match === '*';
463
- return typeof condition.expected_revision === 'string' && condition.expected_revision.length > 0;
508
+ return false;
464
509
  }
465
510
  async publishOwner(task, key, ownerAid, prepare) {
466
511
  let saved = this.savedFile(task, key);
467
512
  await this.readAgent(task.sub_aid);
468
513
  const current = await this.readOwner(ownerAid, task.sub_aid);
469
514
  if (!saved) {
515
+ const base = task.base_owner_md;
516
+ if (base && ownerAid === task.base_agent_md?.statement?.owner_aid && (current.exists !== base.exists || current.content !== base.raw || (current.exists ? current.sha256 : '') !== base.sha256))
517
+ throw ownershipFailure('conflict', 'original owner.md material changed before publication');
470
518
  const ownership = await prepare(current);
471
519
  const raw = signOwnershipFile(this.deps.currentAid(), `---\nowner_aid: ${ownerAid}\nsub_aid: ${task.sub_aid}\nownership: ${JSON.stringify(ownership)}\n---\n`, 'owner', { now: this.deps.now(), expectedOwnerAid: ownerAid, expectedSubAid: task.sub_aid });
472
- saved = { raw, hash: ownershipSha256(raw), condition: { expected_revision: current.revision } };
520
+ saved = { raw, hash: ownershipSha256(raw), condition: {}, original_owner: { exists: current.exists, raw: current.content, sha256: current.exists ? ownershipSha256(current.content) : '' } };
473
521
  if (key === 'transfer')
474
522
  saved.expires_at = ownership.transfer.expires_at;
475
523
  task.files[key] = saved;
@@ -477,23 +525,25 @@ export class OwnershipFiles {
477
525
  }
478
526
  await this.verifyFile(saved.raw, 'owner', task.sub_aid, ownerAid);
479
527
  if (current.exists && current.sha256 === saved.hash && current.content === saved.raw) {
480
- if (saved.published_revision !== undefined && current.revision !== saved.published_revision)
481
- throw ownershipFailure('conflict', 'owner file published revision changed');
482
- saved.published_revision = current.revision;
483
528
  await task.persist();
484
529
  return current;
485
530
  }
486
- if (current.revision !== saved.condition.expected_revision)
487
- throw ownershipFailure('conflict', 'owner file original revision changed');
531
+ const original = saved.original_owner;
532
+ if (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? ownershipSha256(original.raw) : ''))
533
+ throw ownershipFailure('persistence_unavailable', 'owner file original material is missing or corrupt');
534
+ if (current.exists !== original.exists || current.content !== original.raw || (current.exists ? current.sha256 : '') !== original.sha256)
535
+ throw ownershipFailure('conflict', 'owner file original material changed');
488
536
  await this.assertBase(task, await this.readAgent(task.sub_aid));
489
537
  if (key === 'owner' && task.operation === 'transfer')
490
538
  await this.requireTransfer(task);
491
539
  if (key === 'transfer' && saved.expires_at <= this.deps.now())
492
540
  throw ownershipFailure('conflict', 'transfer authorization expired');
493
- const published = await this.deps.writeOwnerMd(ownerAid, task.sub_aid, saved.raw, saved.condition.expected_revision);
494
- if (!published.exists || !published.revision || published.sha256 !== saved.hash || published.content !== saved.raw)
541
+ const beforeWrite = await this.readOwner(ownerAid, task.sub_aid);
542
+ if (beforeWrite.exists !== original.exists || beforeWrite.content !== original.raw || (beforeWrite.exists ? beforeWrite.sha256 : '') !== original.sha256)
543
+ throw ownershipFailure('conflict', 'owner file original material changed before publication');
544
+ const published = await this.deps.writeOwnerMd(ownerAid, task.sub_aid, saved.raw);
545
+ if (!published.exists || published.sha256 !== saved.hash || published.content !== saved.raw)
495
546
  throw ownershipFailure('unavailable', 'owner publication could not be verified', true);
496
- saved.published_revision = published.revision;
497
547
  await task.persist();
498
548
  return published;
499
549
  }
@@ -511,10 +561,10 @@ export class OwnershipFiles {
511
561
  await task.persist();
512
562
  }
513
563
  await this.verifyFile(saved.raw, 'agent', task.sub_aid);
514
- if (current.exists && ownershipSha256(current.content) === saved.hash && current.content === saved.raw && current.relation_version === task.target_relation_version && current.lock === (cleanup ? 'unlocked' : 'locked')) {
515
- if (saved.published_revision !== undefined && current.etag !== saved.published_revision)
564
+ if (current.exists && ownershipSha256(current.content) === saved.hash && current.content === saved.raw && current.relation_version === (cleanup ? 0 : task.target_relation_version) && current.lock === (cleanup ? 'unlocked' : 'locked')) {
565
+ if (saved.published_etag !== undefined && current.etag !== saved.published_etag)
516
566
  throw ownershipFailure('conflict', 'agent.md published ETag changed');
517
- saved.published_revision = current.etag;
567
+ saved.published_etag = current.etag;
518
568
  await task.persist();
519
569
  return current;
520
570
  }
@@ -532,13 +582,16 @@ export class OwnershipFiles {
532
582
  if (!target.exists)
533
583
  throw ownershipFailure('unavailable', 'target owner declaration is missing', true);
534
584
  const declaration = await this.verifyFile(target.content, 'owner', task.sub_aid, this.targetOwner(task));
585
+ const savedOwner = task.files.owner;
586
+ if (savedOwner && (savedOwner.raw !== target.content || savedOwner.hash !== target.sha256))
587
+ throw ownershipFailure('conflict', 'fixed target owner declaration changed');
535
588
  if (declaration.action !== 'declare' || declaration.statement.relation_version !== task.target_relation_version)
536
589
  throw ownershipFailure('conflict', 'target owner declaration changed');
537
590
  }
538
591
  const published = await this.deps.submitAgentMdRaw(task.sub_aid, saved.raw, saved.condition);
539
- if (published.content !== saved.raw || published.relation_version !== task.target_relation_version || published.lock !== (cleanup ? 'unlocked' : 'locked'))
592
+ if (published.content !== saved.raw || published.relation_version !== (cleanup ? 0 : task.target_relation_version) || published.lock !== (cleanup ? 'unlocked' : 'locked'))
540
593
  throw ownershipFailure('unavailable', 'agent.md publication could not be verified', true);
541
- saved.published_revision = published.etag;
594
+ saved.published_etag = published.etag;
542
595
  await task.persist();
543
596
  return published;
544
597
  }
@@ -557,21 +610,24 @@ export class OwnershipStorageAdapter {
557
610
  return result;
558
611
  }
559
612
  catch (error) {
613
+ const reading = method === 'storage.head_object' || method === 'storage.get_object';
560
614
  const code = error && typeof error === 'object' ? error.code : undefined;
561
615
  if (error instanceof ValidationError && code !== -32601 || error instanceof StateError && error.stringCode === 'invalid')
562
616
  throw new ValidationError(error.message, { stringCode: 'invalid' });
563
617
  if (error instanceof StateError && ['unavailable', 'unsupported_protocol', 'conflict'].includes(error.stringCode))
564
618
  throw ownershipFailure(error.stringCode, error.message, error.stringCode !== 'conflict');
619
+ if (reading && error instanceof StateError && error.stringCode === 'forbidden')
620
+ throw ownershipFailure('unavailable', error.message, true);
565
621
  if (error instanceof StateError && error.stringCode)
566
622
  throw error;
567
- throw ownershipFailure(code === -32009 ? 'conflict' : code === -32004 ? 'forbidden' : code === -32601 ? 'unsupported_protocol' : 'unavailable', error instanceof Error ? error.message : String(error), ![-32009, -32004].includes(code ?? 0));
623
+ throw ownershipFailure(code === -32009 ? 'conflict' : code === -32004 ? reading ? 'unavailable' : 'forbidden' : code === -32601 ? 'unsupported_protocol' : 'unavailable', error instanceof Error ? error.message : String(error), code !== -32009 && (reading || code !== -32004));
568
624
  }
569
625
  }
570
626
  metadata(value, ownerAid, path) {
571
627
  const now = this.now();
572
628
  const expectedService = `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}`;
573
- if (value.consistency !== 'authoritative' || value.owner_aid !== ownerAid || value.bucket !== 'default' || value.path !== path || typeof value.exists !== 'boolean' || typeof value.revision !== 'string' || !value.revision || !Number.isSafeInteger(value.observed_at) || Number(value.observed_at) < now - 30 || Number(value.observed_at) > now + 5 || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== undefined && value.service_aid !== expectedService || value.authenticated_service_aid !== undefined && value.authenticated_service_aid !== expectedService || value.exists && (typeof value.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(value.sha256) || typeof value.object_id !== 'string' || !value.object_id || !Number.isSafeInteger(value.version) || Number(value.version) < 1))
574
- throw ownershipFailure('unavailable', 'Storage authoritative namespace, revision or freshness metadata is incomplete', true);
629
+ if (value.consistency !== 'authoritative' || value.owner_aid !== ownerAid || value.bucket !== 'default' || value.path !== path || typeof value.exists !== 'boolean' || !Number.isSafeInteger(value.observed_at) || Number(value.observed_at) < now - 30 || Number(value.observed_at) > now + 5 || value.recovery_ready !== undefined && value.recovery_ready !== true || value.service_aid !== undefined && value.service_aid !== expectedService || value.authenticated_service_aid !== undefined && value.authenticated_service_aid !== expectedService || value.exists && (typeof value.sha256 !== 'string' || !/^[0-9a-f]{64}$/.test(value.sha256)))
630
+ throw ownershipFailure('unavailable', 'Storage authoritative namespace or freshness metadata is incomplete', true);
575
631
  }
576
632
  async readObject(ownerAid, path) {
577
633
  ownershipAid(ownerAid, 'owner_aid');
@@ -582,9 +638,9 @@ export class OwnershipStorageAdapter {
582
638
  let content = '';
583
639
  let final = before;
584
640
  if (before.exists) {
585
- const object = await this.call('storage.get_object', { ...params, expected_revision: before.revision });
641
+ const object = await this.call('storage.get_object', params);
586
642
  this.metadata(object, ownerAid, path);
587
- if (object.exists !== true || object.revision !== before.revision || object.sha256 !== before.sha256 || object.object_id !== before.object_id || object.version !== before.version || typeof object.content !== 'string')
643
+ if (object.exists !== true || object.sha256 !== before.sha256 || typeof object.content !== 'string')
588
644
  throw ownershipFailure('unavailable', 'Storage object changed or full Base64 content is unavailable', true);
589
645
  if (object.content !== '' && !isStrictBase64(object.content))
590
646
  throw ownershipFailure('unavailable', 'Storage Base64 content is unavailable', true);
@@ -599,33 +655,54 @@ export class OwnershipStorageAdapter {
599
655
  }
600
656
  final = await this.call('storage.head_object', params);
601
657
  this.metadata(final, ownerAid, path);
602
- if (final.exists !== true || final.revision !== before.revision || final.sha256 !== before.sha256 || final.object_id !== before.object_id || final.version !== before.version || this.now() - began > 30)
603
- throw ownershipFailure('unavailable', 'Storage revision changed during authoritative read', true);
658
+ if (final.exists !== true || final.sha256 !== before.sha256 || this.now() - began > 30)
659
+ throw ownershipFailure('unavailable', 'Storage material changed during authoritative read', true);
604
660
  }
605
661
  const service = `st.${ownerAid.slice(ownerAid.indexOf('.') + 1)}`;
606
- return { owner_aid: ownerAid, bucket: 'default', path, exists: Boolean(final.exists), revision: String(final.revision), observed_at: Number(final.observed_at),
607
- content, sha256: final.exists ? String(final.sha256) : undefined, authoritative: true, recovery_ready: final.recovery_ready, service_aid: service, authenticated_service_aid: service };
662
+ return { owner_aid: ownerAid, bucket: 'default', path, exists: Boolean(final.exists), observed_at: Number(final.observed_at),
663
+ content, sha256: final.exists ? String(final.sha256) : undefined, authoritative: true, recovery_ready: final.recovery_ready, service_aid: service, authenticated_service_aid: service, version: final.exists && Number.isSafeInteger(final.version) && Number(final.version) > 0 ? final.version : undefined };
608
664
  }
609
665
  async readOwnerMd(ownerAid, subAid) {
610
666
  ownershipAid(subAid, 'sub_aid');
611
667
  return this.readObject(ownerAid, `sub_aids/${subAid}/owner.md`);
612
668
  }
613
- async writeOwnerMd(ownerAid, subAid, raw, originalRevision) {
669
+ async writeOwnerMd(ownerAid, subAid, raw) {
614
670
  this.requireLocal(ownerAid);
615
671
  ownershipAid(subAid, 'sub_aid');
616
- if (typeof originalRevision !== 'string' || !originalRevision)
617
- throw ownershipFailure('invalid', 'original Storage revision is required');
618
- await this.call('storage.put_object', { owner_aid: ownerAid, bucket: 'default', object_key: `sub_aids/${subAid}/owner.md`, content: Buffer.from(raw, 'utf8').toString('base64'), content_type: 'text/markdown; charset=utf-8', is_private: false, overwrite: true, expected_revision: originalRevision });
672
+ const path = `sub_aids/${subAid}/owner.md`;
673
+ const before = await this.readOwnerMd(ownerAid, subAid);
674
+ const bytes = new TextEncoder().encode(raw);
675
+ const expectedVersion = before.exists ? before.version : 0;
676
+ if (typeof expectedVersion !== 'number' || !Number.isSafeInteger(expectedVersion) || expectedVersion < (before.exists ? 1 : 0))
677
+ throw ownershipFailure('unavailable', 'Storage owner.md version is unavailable', true);
678
+ const session = await this.call('storage.create_upload_session', { owner_aid: ownerAid, bucket: 'default', object_key: path, size_bytes: bytes.length, content_type: 'text/markdown; charset=utf-8', expected_version: expectedVersion, overwrite: true });
679
+ const uploadURL = String(session.upload_url ?? ''), sessionID = String(session.session_id ?? '');
680
+ if (!uploadURL || !sessionID)
681
+ throw ownershipFailure('unavailable', 'Storage owner.md upload session is incomplete', true);
682
+ const headers = {};
683
+ if (session.headers && typeof session.headers === 'object')
684
+ for (const [key, value] of Object.entries(session.headers))
685
+ if (value !== undefined && value !== null && key.toLowerCase() !== 'content-type')
686
+ headers[key] = String(value);
687
+ headers['Content-Type'] = 'text/markdown; charset=utf-8';
688
+ if (this.deps.httpPut)
689
+ await this.deps.httpPut(uploadURL, bytes, headers);
690
+ else {
691
+ const response = await fetch(uploadURL, { method: 'PUT', body: bytes, headers });
692
+ if (!response.ok)
693
+ throw ownershipFailure('unavailable', `Storage owner.md HTTP PUT failed: ${response.status}`, true);
694
+ }
695
+ const complete = { owner_aid: ownerAid, bucket: 'default', object_key: path, session_id: sessionID, size_bytes: bytes.length, sha256: ownershipSha256(bytes), content_type: 'text/markdown; charset=utf-8', is_private: false, overwrite: true };
696
+ complete.expected_version = expectedVersion;
697
+ await this.call('storage.complete_upload', complete);
619
698
  return this.readOwnerMd(ownerAid, subAid);
620
699
  }
621
- async deleteOwnerMd(ownerAid, subAid, originalRevision) {
700
+ async deleteOwnerMd(ownerAid, subAid) {
622
701
  this.requireLocal(ownerAid);
623
702
  ownershipAid(subAid, 'sub_aid');
624
- if (typeof originalRevision !== 'string' || !originalRevision)
625
- throw ownershipFailure('invalid', 'original Storage revision is required');
626
- await this.call('storage.delete_object', { owner_aid: ownerAid, bucket: 'default', object_key: `sub_aids/${subAid}/owner.md`, expected_revision: originalRevision });
703
+ await this.call('storage.delete_object', { owner_aid: ownerAid, bucket: 'default', object_key: `sub_aids/${subAid}/owner.md` });
627
704
  if ((await this.readOwnerMd(ownerAid, subAid)).exists)
628
- throw ownershipFailure('unavailable', 'Storage conditional deletion could not be confirmed', true);
705
+ throw ownershipFailure('unavailable', 'Storage deletion could not be confirmed', true);
629
706
  }
630
707
  requireLocal(ownerAid) {
631
708
  if (ownershipAid(ownerAid, 'owner_aid') !== this.deps.currentAid())