@agentunion/fastaun-browser 0.5.20 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/_packed_docs/CHANGELOG.md +47 -0
  3. package/_packed_docs/INDEX.md +453 -440
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
  5. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  7. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  8. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  9. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
  10. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -658
  13. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  14. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  15. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  16. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  17. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  18. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
  19. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
  20. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +147 -14
  21. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
  22. package/_packed_docs/sdk/09-group-rpc-manual.md +5 -2
  23. package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
  24. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  25. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  26. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
  27. package/_packed_docs/sdk/INDEX.md +239 -221
  28. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
  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 +106 -43
  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 +180 -9
  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 -708
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -476
  33. 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 +1 -1
  34. 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 +2 -2
  35. package/dist/agent-md-schema.d.ts +1 -0
  36. package/dist/agent-md-schema.d.ts.map +1 -1
  37. package/dist/agent-md-schema.js +5 -4
  38. package/dist/agent-md-schema.js.map +1 -1
  39. package/dist/agent-md.d.ts +25 -1
  40. package/dist/agent-md.d.ts.map +1 -1
  41. package/dist/agent-md.js +148 -71
  42. package/dist/agent-md.js.map +1 -1
  43. package/dist/aid-store.d.ts +13 -0
  44. package/dist/aid-store.d.ts.map +1 -1
  45. package/dist/aid-store.js +55 -8
  46. package/dist/aid-store.js.map +1 -1
  47. package/dist/aid.d.ts.map +1 -1
  48. package/dist/aid.js +2 -1
  49. package/dist/aid.js.map +1 -1
  50. package/dist/auth.d.ts.map +1 -1
  51. package/dist/auth.js +4 -0
  52. package/dist/auth.js.map +1 -1
  53. package/dist/bundle.js +4981 -1219
  54. package/dist/cert-utils.d.ts +1 -0
  55. package/dist/cert-utils.d.ts.map +1 -1
  56. package/dist/cert-utils.js +21 -0
  57. package/dist/cert-utils.js.map +1 -1
  58. package/dist/client/delivery.d.ts +101 -16
  59. package/dist/client/delivery.d.ts.map +1 -1
  60. package/dist/client/delivery.js +1172 -160
  61. package/dist/client/delivery.js.map +1 -1
  62. package/dist/client/rpc-pipeline.d.ts +4 -1
  63. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  64. package/dist/client/rpc-pipeline.js +122 -48
  65. package/dist/client/rpc-pipeline.js.map +1 -1
  66. package/dist/client/v2-e2ee.d.ts +22 -3
  67. package/dist/client/v2-e2ee.d.ts.map +1 -1
  68. package/dist/client/v2-e2ee.js +1258 -649
  69. package/dist/client/v2-e2ee.js.map +1 -1
  70. package/dist/client.d.ts +23 -2
  71. package/dist/client.d.ts.map +1 -1
  72. package/dist/client.js +391 -87
  73. package/dist/client.js.map +1 -1
  74. package/dist/config.d.ts +4 -0
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +1 -0
  77. package/dist/config.js.map +1 -1
  78. package/dist/events.d.ts +19 -1
  79. package/dist/events.d.ts.map +1 -1
  80. package/dist/events.js +146 -23
  81. package/dist/events.js.map +1 -1
  82. package/dist/facades.d.ts +42 -1
  83. package/dist/facades.d.ts.map +1 -1
  84. package/dist/facades.js +363 -7
  85. package/dist/facades.js.map +1 -1
  86. package/dist/index.d.ts +5 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +1 -1
  89. package/dist/index.js.map +1 -1
  90. package/dist/keystore/index.d.ts +14 -1
  91. package/dist/keystore/index.d.ts.map +1 -1
  92. package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
  93. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  94. package/dist/keystore/indexeddb-identity-store.js +30 -13
  95. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  96. package/dist/keystore/indexeddb-shared.d.ts +19 -2
  97. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  98. package/dist/keystore/indexeddb-shared.js +193 -6
  99. package/dist/keystore/indexeddb-shared.js.map +1 -1
  100. package/dist/keystore/indexeddb-token-store.d.ts +15 -2
  101. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  102. package/dist/keystore/indexeddb-token-store.js +308 -27
  103. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  104. package/dist/org-protocol.d.ts +45 -0
  105. package/dist/org-protocol.d.ts.map +1 -1
  106. package/dist/org-protocol.js +757 -11
  107. package/dist/org-protocol.js.map +1 -1
  108. package/dist/ownership-v21.d.ts +21 -4
  109. package/dist/ownership-v21.d.ts.map +1 -1
  110. package/dist/ownership-v21.js +49 -16
  111. package/dist/ownership-v21.js.map +1 -1
  112. package/dist/ownership-workflow.d.ts +30 -0
  113. package/dist/ownership-workflow.d.ts.map +1 -1
  114. package/dist/ownership-workflow.js +412 -54
  115. package/dist/ownership-workflow.js.map +1 -1
  116. package/dist/seq-tracker.d.ts +2 -0
  117. package/dist/seq-tracker.d.ts.map +1 -1
  118. package/dist/seq-tracker.js +3 -2
  119. package/dist/seq-tracker.js.map +1 -1
  120. package/dist/tools/cross-sdk-agent.d.ts +60 -3
  121. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  122. package/dist/tools/cross-sdk-agent.js +433 -37
  123. package/dist/tools/cross-sdk-agent.js.map +1 -1
  124. package/dist/transport.d.ts +3 -0
  125. package/dist/transport.d.ts.map +1 -1
  126. package/dist/transport.js +62 -30
  127. package/dist/transport.js.map +1 -1
  128. package/dist/version.d.ts +1 -1
  129. package/dist/version.js +1 -1
  130. package/package.json +1 -1
@@ -27,6 +27,14 @@ export function parseOwnershipRegistrationIntent(raw, aid) {
27
27
  function failure(code, reason, retryable = false) {
28
28
  return new StateError(reason, { stringCode: code, retryable });
29
29
  }
30
+ function storageFailure(reason) {
31
+ return Object.assign(failure('persistence_unavailable', reason), { ownershipStorageFailure: true });
32
+ }
33
+ function rawTaskRecord(snapshot) {
34
+ const raw = Object.hasOwn(snapshot, 'raw') ? snapshot.raw : snapshot;
35
+ const record = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw.record : undefined;
36
+ return record && typeof record === 'object' && !Array.isArray(record) ? record : undefined;
37
+ }
30
38
  function participants(task) {
31
39
  return [...new Set([task.owner_aid, task.sub_aid, ...(task.new_owner_aid ? [task.new_owner_aid] : [])])];
32
40
  }
@@ -81,7 +89,8 @@ export function parseOwnershipMessage(content) {
81
89
  ownershipAid(message.sub_aid, 'sub_aid');
82
90
  ownershipAid(message.owner_aid, 'owner_aid');
83
91
  ownershipInteger(message.base_relation_version, 'base_relation_version', 0);
84
- if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + (message.operation === 'release' ? 0 : 1))
92
+ const target = ownershipInteger(message.target_relation_version, 'target_relation_version');
93
+ if (target !== message.base_relation_version + 1 && !(message.operation === 'release' && target === message.base_relation_version))
85
94
  throw new ValidationError('ownership target version does not match operation');
86
95
  if (message.operation === 'transfer') {
87
96
  if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
@@ -148,7 +157,7 @@ export function validateOwnershipMessage(message, context) {
148
157
  if (ns.protocol !== '2.1')
149
158
  throw failure('unsupported_protocol', 'NS ownership protocol is unavailable');
150
159
  const base = ns.relation_version === message.base_relation_version && (message.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === message.owner_aid);
151
- const target = ns.relation_version === message.target_relation_version && (message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === targetOwner(message));
160
+ const target = message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === message.base_relation_version || ns.relation_version === 0) : ns.relation_version === message.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(message);
152
161
  const cleaned = message.action === 'sub.cleaned' && Boolean(task) && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid;
153
162
  if (message.action !== 'operation.stopped' && (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target && !cleaned : !base && !target))
154
163
  throw failure('conflict', 'ownership current NS relation does not match this task');
@@ -200,7 +209,7 @@ export class OwnershipWorkflow {
200
209
  const store = this.deps.store;
201
210
  for (const name of ['loadOwnershipTask', 'listOwnershipTasks', 'compareAndSwapOwnershipTask', 'acquireOwnershipTaskLease', 'renewOwnershipTaskLease', 'releaseOwnershipTaskLease']) {
202
211
  if (typeof store[name] !== 'function')
203
- throw failure('persistence_unavailable', 'ownership durable task storage is unavailable');
212
+ throw storageFailure('ownership durable task storage is unavailable');
204
213
  }
205
214
  return store;
206
215
  }
@@ -209,11 +218,13 @@ export class OwnershipWorkflow {
209
218
  return await this.storage().loadOwnershipTask(aid, operationId);
210
219
  }
211
220
  catch (error) {
212
- throw failure('persistence_unavailable', `ownership task read failed: ${String(error)}`);
221
+ throw storageFailure(`ownership task read failed: ${String(error)}`);
213
222
  }
214
223
  }
215
- async record(snapshot) {
224
+ async record(snapshot, legacy = false) {
216
225
  const task = snapshot.record;
226
+ if (snapshot.record.reset === true && snapshot.revision > 0)
227
+ throw failure('conflict', 'ownership operation was reset');
217
228
  const localAid = this.deps.currentAid();
218
229
  try {
219
230
  if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1)
@@ -229,8 +240,32 @@ export class OwnershipWorkflow {
229
240
  throw new Error('incomplete task approvals or initiator decision');
230
241
  if (!task.files || typeof task.files !== 'object' || Array.isArray(task.files) || !Array.isArray(task.inbox) || !Array.isArray(task.received) || !Array.isArray(task.outbox) || !Array.isArray(task.sent) || !task.hints || typeof task.hints !== 'object' || typeof task.request_validated !== 'boolean')
231
242
  throw new Error('invalid task journals');
243
+ if (!task.base_agent_md && task.request_validated && !legacy && !task.legacy_task)
244
+ return task;
245
+ if (task.legacy_recovery) {
246
+ const recovery = task.legacy_recovery, original = recovery.task;
247
+ await this.validateLegacyBind(original);
248
+ if (!Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || recovery.task_sha256 !== await ownershipSha256(JSON.stringify(original)) || !this.sameIntent(task, original) || task.operation_id !== original.operation_id || task.base_relation_version !== original.base_relation_version || task.target_relation_version !== original.target_relation_version || task.created_at !== original.created_at || task.local_decision !== original.local_decision && original.local_decision !== 'pending' || Object.entries(original.approvals).some(([aid, decision]) => decision !== 'pending' && task.approvals[aid] !== decision) || !recovery.checkpoint || !Number.isSafeInteger(recovery.checkpoint.observed_at) || typeof recovery.owner_confirmed !== 'boolean')
249
+ throw new Error('invalid legacy recovery checkpoint');
250
+ const owner = recovery.checkpoint.owner_md;
251
+ if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? await ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
252
+ throw new Error('invalid legacy owner checkpoint');
253
+ if (task.local_roles.includes('sub') && (owner.exists || recovery.owner_confirmed) && (!task.files.owner || owner.exists && (task.files.owner.raw !== owner.raw || task.files.owner.sha256 !== owner.sha256)))
254
+ throw new Error('missing or changed legacy owner candidate');
255
+ }
256
+ if (task.legacy_task) {
257
+ const recovery = task.legacy_task, original = recovery.task;
258
+ if (!Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || recovery.task_sha256 !== await ownershipSha256(JSON.stringify(original)))
259
+ throw new Error('invalid legacy task source');
260
+ await this.priorView(original);
261
+ if (!this.sameIntent(task, original) || task.operation_id !== original.operation_id || task.base_relation_version !== original.base_relation_version || task.target_relation_version !== original.target_relation_version || task.created_at !== original.created_at || Object.entries(original.approvals).some(([aid, decision]) => decision !== 'pending' && task.approvals[aid] !== decision) || JSON.stringify(recovery.checkpoint.agent_md) !== JSON.stringify(task.base_agent_md ?? null) || JSON.stringify(recovery.checkpoint.owner_md) !== JSON.stringify(task.base_owner_md ?? null) || !task.base_agent_md && task.status !== 'completed')
262
+ throw new Error('legacy task fixed context changed');
263
+ for (const [key, prepared] of Object.entries(original.files))
264
+ if (task.files[key]?.raw !== prepared.raw || task.files[key]?.sha256 !== prepared.sha256 || ['agent', 'cleanup'].includes(key) && JSON.stringify(task.files[key]?.condition) !== JSON.stringify(prepared.condition))
265
+ throw new Error('legacy task signed candidate changed');
266
+ }
232
267
  const base = task.base_agent_md;
233
- if (!base && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
268
+ if (!base && !legacy && !task.legacy_task && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
234
269
  throw new Error('missing original agent.md context');
235
270
  if (base) {
236
271
  if (typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || typeof base.etag !== 'string' || base.exists && !base.etag || base.sha256 !== await ownershipSha256(base.raw))
@@ -275,7 +310,7 @@ export class OwnershipWorkflow {
275
310
  throw new Error('invalid prepared file operation');
276
311
  const ownerAid = key === 'owner' || key === 'agent' ? targetOwner(task) : task.owner_aid;
277
312
  const file = parseOwnershipFile(prepared.raw, agentFile ? 'agent' : 'owner', { expectedSubAid: task.sub_aid, expectedOwnerAid: key === 'cleanup' ? undefined : ownerAid });
278
- if (!task.local_roles.includes(agentFile ? 'sub' : key === 'owner' && task.operation === 'transfer' ? 'new_owner' : 'owner'))
313
+ if (!task.local_roles.includes(agentFile ? 'sub' : key === 'owner' && task.operation === 'transfer' ? 'new_owner' : 'owner') && !(key === 'owner' && task.legacy_recovery && task.local_roles.includes('sub')))
279
314
  throw new Error('invalid prepared file local role');
280
315
  if (key === 'cleanup' ? Boolean(file.statement) : file.statement?.relation_version !== (key === 'owner' || key === 'agent' ? task.target_relation_version : task.base_relation_version))
281
316
  throw new Error('invalid prepared file generation');
@@ -297,7 +332,7 @@ export class OwnershipWorkflow {
297
332
  if (task.status === 'completed') {
298
333
  const consensus = task.verified_consensus;
299
334
  const cleaned = task.operation === 'release' && consensus?.status === 'unbound' && consensus.relation_version === 0 && Boolean(await this.cleanupEvidence(task, consensus));
300
- if (!consensus || consensus.protocol !== '2.1' || consensus.sub_aid !== task.sub_aid || !cleaned && consensus.relation_version !== task.target_relation_version || typeof consensus.agent_md_etag !== 'string' || !consensus.agent_md_etag || !/^[0-9a-f]{64}$/.test(consensus.agent_md_sha256))
335
+ if (!consensus || consensus.protocol !== '2.1' || consensus.sub_aid !== task.sub_aid || !cleaned && consensus.relation_version !== (task.operation === 'release' ? task.base_relation_version : task.target_relation_version) || typeof consensus.agent_md_etag !== 'string' || !consensus.agent_md_etag || !/^[0-9a-f]{64}$/.test(consensus.agent_md_sha256))
301
336
  throw new Error('invalid persisted consensus evidence');
302
337
  if (task.operation === 'release') {
303
338
  if (!cleaned && consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
@@ -306,7 +341,7 @@ export class OwnershipWorkflow {
306
341
  else {
307
342
  if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256))
308
343
  throw new Error('invalid persisted binding evidence');
309
- if (task.local_roles.includes('sub') && task.files.agent?.sha256 !== consensus.agent_md_sha256 || task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') && task.files.owner?.sha256 !== consensus.owner_md_sha256 || task.operation === 'transfer' && task.local_roles.includes('owner') && !task.files.transfer)
344
+ if (task.local_roles.includes('sub') && task.files.agent?.sha256 !== consensus.agent_md_sha256 || (task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') || task.legacy_recovery && task.local_roles.includes('sub')) && task.files.owner?.sha256 !== consensus.owner_md_sha256 || task.operation === 'transfer' && task.local_roles.includes('owner') && !task.files.transfer)
310
345
  throw new Error('persisted consensus differs from local original files');
311
346
  }
312
347
  }
@@ -337,7 +372,9 @@ export class OwnershipWorkflow {
337
372
  return task;
338
373
  }
339
374
  catch (error) {
340
- throw failure('persistence_unavailable', `ownership task is damaged: ${String(error)}`);
375
+ if (error?.ownershipStorageFailure)
376
+ throw error;
377
+ throw Object.assign(failure('persistence_unavailable', `ownership task is damaged: ${String(error)}`), { ownershipTaskDamage: true });
341
378
  }
342
379
  }
343
380
  message(task, action, reason) {
@@ -366,7 +403,7 @@ export class OwnershipWorkflow {
366
403
  event(context, event, extra = {}) {
367
404
  context.events.push({ event, payload: { ...ownershipTaskResult(context.task), initiator_aid: context.task.initiator_aid, local_roles: context.task.local_roles,
368
405
  ...(event === 'ownership.request' ? { requester_aid: context.task.initiator_aid } : {}),
369
- ...(context.task.verified_relation_version === undefined ? {} : { relation_version: context.task.verified_relation_version }), ...extra } });
406
+ ...(context.task.verified_consensus === undefined ? {} : { relation_version: context.task.verified_consensus.relation_version }), ...extra } });
370
407
  }
371
408
  async save(context) {
372
409
  context.guard?.();
@@ -437,22 +474,32 @@ export class OwnershipWorkflow {
437
474
  if (!saved && !initial)
438
475
  throw failure('operation_not_found', 'ownership operation was not found');
439
476
  if (!saved && initial) {
440
- const existing = (await this.records(aid, subAid)).find((task) => task.sub_aid === subAid && this.ongoing(task));
477
+ const existing = (await this.records(aid, subAid, undefined, true)).find((task) => task.sub_aid === subAid && this.ongoing(task));
441
478
  if (existing) {
442
479
  if (!this.sameIntent(existing, initial) || existing.base_relation_version !== initial.base_relation_version || existing.target_relation_version !== initial.target_relation_version)
443
480
  throw failure('conflict', 'another local operation for this sub is pending');
444
- try {
445
- await store.releaseOwnershipTaskLease(aid, lease);
446
- }
447
- catch (error) {
448
- throw failure('persistence_unavailable', `ownership sub lease release failed: ${String(error)}`);
481
+ if (initial.initiator_aid === aid) {
482
+ try {
483
+ await store.releaseOwnershipTaskLease(aid, lease);
484
+ }
485
+ catch (error) {
486
+ throw failure('persistence_unavailable', `ownership sub lease release failed: ${String(error)}`);
487
+ }
488
+ return { result: await this.advance(subAid, existing.operation_id, undefined, events, undefined, guard), events: [] };
449
489
  }
450
- return { result: await this.advance(subAid, existing.operation_id, undefined, events, undefined, guard), events: [] };
451
490
  }
452
491
  }
453
492
  context = { aid, task: saved ? await this.record(saved) : initial, revision: saved?.revision ?? null, lease, events: [], guard };
454
493
  if (context.task.sub_aid !== subAid)
455
494
  throw failure('conflict', 'ownership task sub changed');
495
+ if (saved && !context.task.base_agent_md && context.task.request_validated) {
496
+ if (!this.ongoing(context.task))
497
+ return { result: ownershipTaskResult(context.task), events: context.events };
498
+ if (context.task.operation === 'bind' && context.task.initiator_aid === context.task.sub_aid && context.task.base_relation_version === 0 && context.task.target_relation_version === 1 && Object.keys(context.task.files).every((key) => key === 'owner'))
499
+ await this.recoverLegacyBind(context);
500
+ else
501
+ await this.recoverPrior(context);
502
+ }
456
503
  if (!saved)
457
504
  await this.save(context);
458
505
  await mutate?.(context);
@@ -478,15 +525,32 @@ export class OwnershipWorkflow {
478
525
  sameIntent(task, intent) {
479
526
  return task.initiator_aid === intent.initiator_aid && task.operation === intent.operation && task.sub_aid === intent.sub_aid && task.owner_aid === intent.owner_aid && task.new_owner_aid === intent.new_owner_aid;
480
527
  }
481
- async records(aid, subAid) {
528
+ async records(aid, subAid, operations, strictSub = false) {
482
529
  let snapshots;
483
530
  try {
484
- snapshots = await this.storage().listOwnershipTasks(aid);
531
+ snapshots = await this.storage().listOwnershipTasks(aid, true);
485
532
  }
486
533
  catch (error) {
487
- throw failure('persistence_unavailable', `ownership task list failed: ${String(error)}`);
534
+ throw storageFailure(`ownership task list failed: ${String(error)}`);
535
+ }
536
+ const records = [];
537
+ for (const snapshot of snapshots) {
538
+ if (snapshot.record.reset === true && snapshot.revision > 0)
539
+ continue;
540
+ if (subAid && typeof snapshot.record.sub_aid === 'string' && snapshot.record.sub_aid !== subAid)
541
+ continue;
542
+ if (operations && typeof snapshot.record.operation === 'string' && !operations.includes(snapshot.record.operation))
543
+ continue;
544
+ try {
545
+ records.push(await this.record(snapshot));
546
+ }
547
+ catch (error) {
548
+ await this.deps.emit('ownership.error', { operation_id: snapshot.record.operation_id, sub_aid: snapshot.record.sub_aid, code: ownershipErrorCode(error), reason: String(error), retryable: false });
549
+ if (strictSub && snapshot.record.sub_aid === subAid)
550
+ throw error;
551
+ }
488
552
  }
489
- return await Promise.all(snapshots.filter((snapshot) => !subAid || typeof snapshot.record.sub_aid !== 'string' || snapshot.record.sub_aid === subAid).map((snapshot) => this.record(snapshot)));
553
+ return records;
490
554
  }
491
555
  async observe(subAid) {
492
556
  this.requireApplicable(subAid);
@@ -512,8 +576,142 @@ export class OwnershipWorkflow {
512
576
  task.base_owner_md = prior ? { exists: prior.exists, raw: prior.content, sha256: prior.sha256 } : { exists: false, raw: '', sha256: '' };
513
577
  }
514
578
  }
579
+ async validateLegacyBind(task) {
580
+ const aid = this.deps.currentAid();
581
+ if (task.base_agent_md != null || task.base_owner_md != null || task.schema_version !== 1 || !this.ongoing(task) || task.operation !== 'bind' || task.initiator_aid !== task.sub_aid || task.base_relation_version !== 0 || task.target_relation_version !== 1 || task.new_owner_aid !== undefined || !task.request_validated || task.approvals[aid] !== task.local_decision || Object.values(task.approvals).includes('reject') || !Number.isSafeInteger(task.created_at) || task.created_at <= 0 || task.release_cleanup !== undefined || task.verified_consensus !== undefined || task.cleanup_pending || task.cleanup_done || task.authorization_sha256 !== undefined || task.expires_at !== undefined || task.legacy_recovery !== undefined)
582
+ throw failure('persistence_unavailable', 'legacy bind context cannot be recovered');
583
+ parseOwnershipMessage(this.message(task, 'bind.request'));
584
+ if (JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, aid)) || !task.local_roles.length || JSON.stringify(Object.keys(task.approvals).sort()) !== JSON.stringify(participants(task).sort()) || task.approvals[task.sub_aid] !== 'accept' || Object.values(task.approvals).some((value) => !['pending', 'accept'].includes(value)))
585
+ throw failure('persistence_unavailable', 'legacy bind approval is damaged');
586
+ for (const [key, prepared] of Object.entries(task.files)) {
587
+ const condition = prepared.condition, published = prepared.published_revision;
588
+ if (key !== 'owner' || !task.local_roles.includes('owner') || task.local_decision !== 'accept' || typeof prepared.raw !== 'string' || prepared.sha256 !== await ownershipSha256(prepared.raw) || !condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision || published !== undefined && (typeof published !== 'string' || !published) || prepared.expires_at !== undefined || prepared.cleaned)
589
+ throw failure('persistence_unavailable', 'legacy bind file cannot be reused');
590
+ const file = parseOwnershipFile(prepared.raw, 'owner', { expectedSubAid: task.sub_aid, expectedOwnerAid: task.owner_aid });
591
+ if (file.action !== 'declare' || file.statement?.relation_version !== 1)
592
+ throw failure('persistence_unavailable', 'legacy owner declaration differs');
593
+ }
594
+ const validate = (payload, from, to) => {
595
+ if (!payload || ['bind.reject', 'operation.stopped'].includes(payload.action))
596
+ throw failure('persistence_unavailable', 'legacy bind contains terminal or mixed controls');
597
+ const message = parseOwnershipMessage({ ...payload });
598
+ validateOwnershipMessage(message, { authenticated_from_aid: from, local_aid: to, task });
599
+ };
600
+ for (const key of task.received) {
601
+ if (typeof key !== 'string' || key.split('|').length !== 2)
602
+ throw failure('persistence_unavailable', 'legacy received journal is damaged');
603
+ const [from, action] = key.split('|'), payload = this.message(task, action);
604
+ validate(payload, from, aid);
605
+ }
606
+ for (const item of task.inbox) {
607
+ validate(item.payload, item.from_aid, aid);
608
+ if (!task.received.includes(`${item.from_aid}|${item.payload.action}`))
609
+ throw failure('persistence_unavailable', 'legacy inbox is damaged');
610
+ }
611
+ for (const item of task.outbox) {
612
+ validate(item.payload, aid, item.to_aid);
613
+ if (item.key !== `${item.to_aid}|${item.payload.action}` || !/^m-[0-9a-f]{32}$/.test(item.message_id) || item.payload.action !== 'bind.request' && task.local_decision !== 'accept')
614
+ throw failure('persistence_unavailable', 'legacy outbox is damaged');
615
+ }
616
+ for (const key of task.sent) {
617
+ if (typeof key !== 'string' || key.split('|').length !== 2)
618
+ throw failure('persistence_unavailable', 'legacy sent journal is damaged');
619
+ const [to, action] = key.split('|'), payload = this.message(task, action);
620
+ validate(payload, aid, to);
621
+ }
622
+ }
623
+ async recoverLegacyBind(context) {
624
+ const original = context.task;
625
+ await this.validateLegacyBind(original);
626
+ const before = await this.observe(original.sub_aid);
627
+ if (before.lock === 'locked' && before.owner_aid === original.owner_aid && before.relation_version === original.target_relation_version)
628
+ return this.recoverPrior(context);
629
+ if (before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || before.file?.statement)
630
+ throw failure('conflict', 'legacy bind relationship base changed');
631
+ const owner = await this.deps.files.readOwner(original.owner_aid, original.sub_aid), prepared = original.files.owner;
632
+ if (prepared && (!owner.exists || owner.content !== prepared.raw || owner.sha256 !== prepared.sha256) || !prepared && original.local_roles.includes('owner') && owner.exists)
633
+ throw failure('conflict', 'legacy owner original cannot be confirmed');
634
+ const publishedRevision = prepared?.published_revision;
635
+ if (publishedRevision !== undefined && owner.revision !== publishedRevision)
636
+ throw failure(owner.revision ? 'conflict' : 'unavailable', 'legacy published owner revision changed', !owner.revision);
637
+ if (owner.exists) {
638
+ const file = await this.deps.files.verify(owner.content, 'owner', original.sub_aid, original.owner_aid);
639
+ if (file.action !== 'declare' || file.statement?.relation_version !== 1)
640
+ throw failure('conflict', 'legacy owner declaration changed');
641
+ }
642
+ const serialized = JSON.stringify(original);
643
+ const candidate = prepared ?? (original.local_roles.includes('sub') && owner.exists ? { raw: owner.content, sha256: owner.sha256 } : undefined);
644
+ const originalOwner = { exists: owner.exists, raw: owner.content, sha256: owner.sha256, ...(prepared && owner.revision ? { revision: owner.revision } : {}) };
645
+ const task = { ...original, status: 'pending', step: 'compatibility_checkpoint', retryable: true, files: candidate ? { owner: { raw: candidate.raw, sha256: candidate.sha256, condition: {}, original_owner: originalOwner } } : {}, inbox: [], received: [], outbox: [], sent: [], hints: {}, legacy_recovery: { task: JSON.parse(serialized), revision: context.revision, task_sha256: await ownershipSha256(serialized), checkpoint: { observed_at: before.observed_at, owner_md: originalOwner }, owner_confirmed: false } };
646
+ delete task.code;
647
+ delete task.reason;
648
+ delete task.base_agent_md;
649
+ delete task.base_owner_md;
650
+ await this.captureBase(task, before);
651
+ const afterOwner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid), after = await this.observe(task.sub_aid);
652
+ if (owner.exists !== afterOwner.exists || owner.content !== afterOwner.content || owner.sha256 !== afterOwner.sha256 || before.exists !== after.exists || before.content !== after.content || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid)
653
+ throw failure('conflict', 'legacy compatibility observations changed');
654
+ if (task.local_roles.includes('sub'))
655
+ this.enqueue(task, task.owner_aid, 'bind.request');
656
+ else if (task.local_decision === 'accept')
657
+ this.enqueue(task, task.sub_aid, 'bind.accept');
658
+ context.task = task;
659
+ await this.save(context);
660
+ }
515
661
  isTarget(task, ns) {
516
- return ns.relation_version === task.target_relation_version && (task.operation === 'release' ? ns.lock === 'unlocked' : ns.lock === 'locked' && ns.owner_aid === targetOwner(task));
662
+ return ns.relation_version === (task.operation === 'release' ? task.base_relation_version : task.target_relation_version) && (task.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === targetOwner(task));
663
+ }
664
+ async priorView(original) {
665
+ if (original.base_agent_md != null || original.base_owner_md != null || original.legacy_task || original.legacy_recovery || !original.request_validated || !this.ongoing(original) || Object.values(original.approvals ?? {}).includes('reject'))
666
+ throw failure('persistence_unavailable', 'legacy task context cannot be recovered');
667
+ const task = structuredClone(original);
668
+ for (const [key, prepared] of Object.entries(task.files ?? {})) {
669
+ if (['agent', 'cleanup'].includes(key))
670
+ continue;
671
+ const condition = prepared.condition;
672
+ if (!prepared.original_owner && (!condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision))
673
+ throw failure('persistence_unavailable', 'legacy owner condition is invalid');
674
+ prepared.condition = {};
675
+ prepared.original_owner ??= { exists: false, raw: '', sha256: '' };
676
+ }
677
+ await this.record({ revision: 1, record: task }, true);
678
+ return task;
679
+ }
680
+ async recoverPrior(context) {
681
+ const original = structuredClone(context.task), task = await this.priorView(original);
682
+ const ns = await this.observe(task.sub_aid);
683
+ const base = this.isBase(task, ns), released = task.operation === 'release' && this.isTarget(task, ns);
684
+ const committed = task.operation !== 'release' && this.isTarget(task, ns);
685
+ if (!base && !released && !committed)
686
+ throw failure('conflict', 'legacy relationship no longer matches this task');
687
+ if (!committed)
688
+ await this.captureBase(task, ns);
689
+ for (const [key, prepared] of Object.entries(task.files)) {
690
+ const agent = ['agent', 'cleanup'].includes(key), ownerAid = ['agent', 'owner'].includes(key) ? targetOwner(task) : task.owner_aid;
691
+ await this.deps.files.verify(prepared.raw, agent ? 'agent' : 'owner', task.sub_aid, agent && key === 'cleanup' ? undefined : ownerAid);
692
+ if (agent)
693
+ continue;
694
+ const current = await this.deps.files.readOwner(ownerAid, task.sub_aid), source = original.files[key];
695
+ const revision = source.condition.expected_revision;
696
+ if (revision && (current.content === prepared.raw && source.published_revision !== undefined || !committed && current.content !== prepared.raw)) {
697
+ if (!current.revision)
698
+ throw failure('unavailable', 'legacy owner original revision is unavailable');
699
+ if (current.revision !== (current.content === prepared.raw ? source.published_revision : revision))
700
+ throw failure('conflict', 'legacy owner original revision changed');
701
+ }
702
+ const statement = current.exists ? parseOwnershipFile(current.content, 'owner', { expectedSubAid: task.sub_aid }).statement : undefined;
703
+ if (current.content !== prepared.raw && (source.published_revision !== undefined || current.exists && (key === 'owner' || !this.sameStatement(statement, parseOwnershipFile(prepared.raw, 'owner', { expectedSubAid: task.sub_aid }).statement))) && !committed)
704
+ throw failure('conflict', 'legacy signed owner file was replaced');
705
+ prepared.original_owner = { exists: current.exists, raw: current.content, sha256: current.sha256, ...(revision && current.revision ? { revision: current.revision } : {}) };
706
+ }
707
+ task.legacy_task = { task: original, revision: context.revision, task_sha256: await ownershipSha256(JSON.stringify(original)), checkpoint: { agent_md: task.base_agent_md ?? null, owner_md: task.base_owner_md ?? null } };
708
+ context.task = task;
709
+ if (committed) {
710
+ if (!await this.complete(context, ns))
711
+ throw failure('conflict', 'legacy committed target lacks matching local signed evidence');
712
+ }
713
+ else
714
+ await this.save(context);
517
715
  }
518
716
  fileTask(context, expectedStatement) {
519
717
  const task = { ...context.task, files: context.task.files, expected_agent_statement: expectedStatement,
@@ -552,7 +750,11 @@ export class OwnershipWorkflow {
552
750
  }
553
751
  }
554
752
  const intent = { initiator_aid: aid, operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid };
555
- for (const pending of (await this.records(aid, subAid)).filter((task) => task.sub_aid === subAid && this.ongoing(task))) {
753
+ if (operation === 'bind' && requestedId === undefined)
754
+ await this.prepareBind(subAid, intent, guard);
755
+ for (const pending of (await this.records(aid, subAid, undefined, true)).filter((task) => task.sub_aid === subAid && this.ongoing(task))) {
756
+ if (!pending.base_agent_md && pending.request_validated && !this.sameIntent(pending, intent))
757
+ throw failure('conflict', 'another local legacy operation for this sub is pending');
556
758
  if (this.sameIntent(pending, intent)) {
557
759
  const result = await this.advance(subAid, pending.operation_id, undefined, events, undefined, guard);
558
760
  if (result.status !== 'stopped' || result.code !== 'conflict' || !/relation version or target was replaced/.test(result.reason ?? ''))
@@ -569,11 +771,9 @@ export class OwnershipWorkflow {
569
771
  }
570
772
  const ns = await this.observe(subAid);
571
773
  if (operation === 'bind') {
572
- if (!ns.exists)
573
- throw failure('unavailable', 'initial agent.md is not published', true);
574
774
  if (ns.lock === 'locked')
575
775
  throw failure('owner_locked', 'the sub AID has a current owner lock');
576
- if (ns.file?.frontmatter.type === 'human')
776
+ if ((ns.exists ? ns.file?.frontmatter.type : subAid === aid ? this.deps.currentAidType?.() : undefined) === 'human')
577
777
  throw failure('invalid', 'human cannot declare an owner');
578
778
  if (ns.file?.statement?.owner_aid === ownerAid)
579
779
  throw failure('conflict', 'current owner release requires sub cleanup before binding again');
@@ -611,6 +811,128 @@ export class OwnershipWorkflow {
611
811
  async releaseSubaid(subAid) { return await this.start('release', subAid, this.deps.currentAid()); }
612
812
  async transferSubaid(subAid, newOwnerAid) { return await this.start('transfer', subAid, this.deps.currentAid(), newOwnerAid); }
613
813
  async startRegistrationIntent(ownerAid, operationId, guard) { return await this.start('bind', this.deps.currentAid(), ownerAid, undefined, operationId, guard); }
814
+ async resetTask(operationId, expected) {
815
+ const aid = this.deps.currentAid(), store = this.deps.store;
816
+ if (typeof store.resetOwnershipTask !== 'function')
817
+ throw storageFailure('ownership task reset storage is unavailable');
818
+ let result;
819
+ try {
820
+ result = await store.resetOwnershipTask(aid, operationId, expected);
821
+ }
822
+ catch (error) {
823
+ if (error instanceof StateError)
824
+ throw error;
825
+ throw storageFailure(`ownership task reset failed: ${String(error)}`);
826
+ }
827
+ if (aid !== this.deps.currentAid())
828
+ throw failure('forbidden', 'ownership current identity changed');
829
+ return result;
830
+ }
831
+ async resetOwnershipTask() {
832
+ const aid = this.deps.currentAid(), reset = new Set(), skipped = new Set();
833
+ if (typeof this.deps.store.resetOwnershipTask !== 'function')
834
+ throw storageFailure('ownership task reset storage is unavailable');
835
+ const list = async () => {
836
+ try {
837
+ return await this.storage().listOwnershipTasks(aid, true);
838
+ }
839
+ catch (error) {
840
+ throw storageFailure(`ownership task list failed: ${String(error)}`);
841
+ }
842
+ };
843
+ for (const original of await list()) {
844
+ let snapshot = original;
845
+ for (;;) {
846
+ if (aid !== this.deps.currentAid())
847
+ throw failure('forbidden', 'ownership current identity changed');
848
+ if (snapshot.record.reset === true && snapshot.revision > 0)
849
+ break;
850
+ const operationId = snapshot.record.operation_id;
851
+ if (typeof operationId !== 'string')
852
+ throw storageFailure('ownership task identity cannot be determined for reset');
853
+ if (!OWNERSHIP_UUID_V4_RE.test(operationId)) {
854
+ skipped.add(operationId);
855
+ break;
856
+ }
857
+ const record = rawTaskRecord(snapshot);
858
+ if (record && (Object.hasOwn(record, 'reset') || Object.hasOwn(record, 'reset_archive'))) {
859
+ skipped.add(operationId);
860
+ break;
861
+ }
862
+ let initiator;
863
+ try {
864
+ initiator = ownershipAid(record?.initiator_aid, 'initiator_aid');
865
+ }
866
+ catch { }
867
+ const status = record?.status;
868
+ if (!initiator || typeof status !== 'string' || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(status)) {
869
+ skipped.add(operationId);
870
+ break;
871
+ }
872
+ if (initiator !== aid || status !== 'pending' && status !== 'error')
873
+ break;
874
+ try {
875
+ await this.resetTask(operationId, snapshot);
876
+ reset.add(operationId);
877
+ break;
878
+ }
879
+ catch (error) {
880
+ if (!error?.ownershipResetConflict)
881
+ throw error;
882
+ const current = (await list()).find((row) => row.record.operation_id === operationId);
883
+ if (!current)
884
+ throw storageFailure('ownership task disappeared during reset');
885
+ snapshot = current;
886
+ }
887
+ }
888
+ }
889
+ return { reset_operation_ids: [...reset].sort(), skipped_operation_ids: [...skipped].sort() };
890
+ }
891
+ async prepareBind(subAid, intent, guard) {
892
+ const aid = this.deps.currentAid();
893
+ let snapshots;
894
+ try {
895
+ snapshots = await this.storage().listOwnershipTasks(aid, true);
896
+ }
897
+ catch (error) {
898
+ throw storageFailure(`ownership task list failed: ${String(error)}`);
899
+ }
900
+ for (const snapshot of snapshots) {
901
+ guard?.();
902
+ if (aid !== this.deps.currentAid())
903
+ throw failure('forbidden', 'ownership current identity changed');
904
+ if (snapshot.record.reset === true && snapshot.revision > 0)
905
+ continue;
906
+ if (typeof snapshot.record.sub_aid !== 'string')
907
+ throw storageFailure('ownership task subject cannot be determined for automatic reset');
908
+ if (snapshot.record.sub_aid !== subAid)
909
+ continue;
910
+ const raw = rawTaskRecord(snapshot);
911
+ if (raw && (Object.hasOwn(raw, 'reset') || Object.hasOwn(raw, 'reset_archive')))
912
+ throw storageFailure('ownership reset persistence is damaged');
913
+ let task;
914
+ try {
915
+ task = await this.record(snapshot);
916
+ }
917
+ catch (error) {
918
+ if (!error?.ownershipTaskDamage)
919
+ throw error;
920
+ await this.resetTask(String(snapshot.record.operation_id), snapshot);
921
+ continue;
922
+ }
923
+ if (!task.base_agent_md && task.request_validated && this.sameIntent(task, intent) && (this.ongoing(task) || task.status === 'stopped' && task.code === 'unsupported_protocol')) {
924
+ try {
925
+ if (task.operation === 'bind' && task.initiator_aid === task.sub_aid && task.base_relation_version === 0 && Object.keys(task.files ?? {}).every((key) => key === 'owner'))
926
+ await this.validateLegacyBind(task);
927
+ else
928
+ await this.priorView(task);
929
+ }
930
+ catch {
931
+ await this.resetTask(task.operation_id, snapshot);
932
+ }
933
+ }
934
+ }
935
+ }
614
936
  async checkOwnershipConsensus(subAid = this.deps.currentAid()) { return await this.deps.files.checkConsensus(subAid); }
615
937
  async decide(operationId, decision, reason) {
616
938
  const saved = await this.load(operationId);
@@ -676,6 +998,8 @@ export class OwnershipWorkflow {
676
998
  throw error;
677
999
  }
678
1000
  const snapshot = await this.load(message.operation_id);
1001
+ if (snapshot?.record.reset === true && snapshot.revision > 0)
1002
+ return true;
679
1003
  const prior = snapshot ? await this.record(snapshot) : undefined;
680
1004
  if (!prior && !['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
681
1005
  return true;
@@ -742,15 +1066,19 @@ export class OwnershipWorkflow {
742
1066
  guard?.();
743
1067
  let snapshots;
744
1068
  try {
745
- snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid());
1069
+ snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid(), true);
746
1070
  }
747
1071
  catch (error) {
748
1072
  throw failure('persistence_unavailable', `ownership task list failed: ${String(error)}`);
749
1073
  }
750
1074
  for (const snapshot of snapshots) {
751
1075
  guard?.();
1076
+ if (snapshot.record.reset === true && snapshot.revision > 0)
1077
+ continue;
752
1078
  try {
753
1079
  const task = await this.record(snapshot);
1080
+ if (!task.base_agent_md && task.request_validated && task.status === 'completed')
1081
+ continue;
754
1082
  if (['rejected', 'stopped'].includes(task.status) && !task.outbox.length && !task.inbox.length || task.status === 'completed' && !task.cleanup_pending && !task.outbox.length && !task.inbox.length && !(['bind', 'transfer'].includes(task.operation) && task.local_roles.includes('sub') && task.local_decision === 'accept' && !task.release_cleanup?.cleaned))
755
1083
  continue;
756
1084
  await this.execute(task.sub_aid, task.operation_id, undefined, undefined, guard);
@@ -837,6 +1165,20 @@ export class OwnershipWorkflow {
837
1165
  }
838
1166
  }
839
1167
  else {
1168
+ if (action === 'owner.ready' && task.legacy_recovery) {
1169
+ const owner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
1170
+ const pinned = task.files.owner;
1171
+ if (pinned && (!owner.exists || owner.content !== pinned.raw || owner.sha256 !== pinned.sha256))
1172
+ throw failure('conflict', 'legacy owner candidate was replaced');
1173
+ if (!owner.exists)
1174
+ throw failure('unavailable', 'legacy owner confirmation is unavailable', true);
1175
+ const file = await this.deps.files.verify(owner.content, 'owner', task.sub_aid, task.owner_aid);
1176
+ if (file.action !== 'declare' || file.statement?.relation_version !== task.target_relation_version)
1177
+ throw failure('conflict', 'legacy owner confirmation differs');
1178
+ if (!pinned)
1179
+ task.files.owner = { raw: owner.content, sha256: owner.sha256, condition: {}, original_owner: { exists: true, raw: owner.content, sha256: owner.sha256 } };
1180
+ task.legacy_recovery.owner_confirmed = true;
1181
+ }
840
1182
  if (action === 'transfer.ready' && this.isBase(task, ns)) {
841
1183
  const authorization = await this.deps.files.verifyTransfer(this.fileTask(context));
842
1184
  task.expires_at = authorization.transfer.expires_at;
@@ -856,11 +1198,29 @@ export class OwnershipWorkflow {
856
1198
  async flush(context, ns) {
857
1199
  const task = context.task;
858
1200
  for (const item of [...task.outbox]) {
1201
+ ns = await this.observe(task.sub_aid);
859
1202
  const action = item.payload.action;
860
1203
  const notification = ['relation.confirmed', 'sub.cleaned'].includes(action);
861
1204
  const terminal = ['bind.reject', 'transfer.reject', 'operation.stopped'].includes(action);
862
1205
  const cleaned = action === 'sub.cleaned' && task.status === 'completed' && await this.confirmSubCleaned(task);
863
- if (!terminal && (notification ? !this.isTarget(task, ns) && !cleaned : !this.isBase(task, ns) || task.status === 'completed' || task.status === 'rejected' || task.status === 'stopped')) {
1206
+ let base = this.isBase(task, ns);
1207
+ if (!notification && base && task.request_validated && task.base_agent_md?.statement && task.base_owner_md) {
1208
+ const object = await this.deps.files.readOwner(task.base_agent_md.statement.owner_aid, task.sub_aid), original = task.base_owner_md;
1209
+ const prepared = task.files[task.operation === 'transfer' ? 'transfer' : task.operation === 'release' ? 'release' : 'owner'];
1210
+ base = object.exists === original.exists && object.content === original.raw && object.sha256 === original.sha256 || Boolean(prepared && object.exists && object.content === prepared.raw && object.sha256 === prepared.sha256);
1211
+ if (!base && task.operation === 'transfer' && !task.local_roles.includes('owner') && task.local_decision === 'accept') {
1212
+ try {
1213
+ await this.deps.files.verifyTransfer(this.fileTask(context));
1214
+ base = true;
1215
+ }
1216
+ catch (error) {
1217
+ if (ownershipErrorCode(error) !== 'conflict')
1218
+ throw error;
1219
+ }
1220
+ }
1221
+ }
1222
+ const target = notification && this.isTarget(task, ns) && (task.operation === 'release' ? this.sameStatement(task.base_agent_md?.statement ?? undefined, ns.file?.statement) : !task.files.agent || this.sameStatement(parseOwnershipFile(task.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement, ns.file?.statement));
1223
+ if (notification ? !target && !cleaned : !base || !terminal && (task.status === 'completed' || task.status === 'rejected' || task.status === 'stopped')) {
864
1224
  task.outbox = task.outbox.filter((row) => row.key !== item.key);
865
1225
  await this.save(context);
866
1226
  continue;
@@ -889,7 +1249,7 @@ export class OwnershipWorkflow {
889
1249
  let consensus;
890
1250
  if (task.operation === 'release') {
891
1251
  consensus = await this.deps.files.checkConsensus(task.sub_aid);
892
- if ((cleaned ? consensus.status !== 'unbound' || consensus.relation_version !== 0 : consensus.status !== 'released' || consensus.relation_version !== task.target_relation_version) || consensus.lock !== ns.lock || consensus.owner_aid !== ns.owner_aid || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== await ownershipSha256(ns.content))
1252
+ if ((cleaned ? consensus.status !== 'unbound' || consensus.relation_version !== 0 : consensus.status !== 'released' || consensus.relation_version !== task.base_relation_version) || consensus.lock !== ns.lock || consensus.owner_aid !== ns.owner_aid || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== await ownershipSha256(ns.content))
893
1253
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership release could not be freshly confirmed', consensus.status !== 'invalid');
894
1254
  if (!cleaned && task.files.release && consensus.owner_md_sha256) {
895
1255
  const source = task.files.release;
@@ -939,7 +1299,7 @@ export class OwnershipWorkflow {
939
1299
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership target materials could not be verified', consensus.status !== 'invalid');
940
1300
  if (task.local_roles.includes('sub') && (!task.files.agent || task.files.agent.sha256 !== consensus.agent_md_sha256))
941
1301
  throw failure('conflict', 'NS accepted agent.md does not match this task original bytes');
942
- if (task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') && task.files.owner?.sha256 !== consensus.owner_md_sha256)
1302
+ if ((task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') || task.legacy_recovery && task.local_roles.includes('sub')) && task.files.owner?.sha256 !== consensus.owner_md_sha256)
943
1303
  throw failure('conflict', 'Storage owner.md does not match this task original bytes');
944
1304
  if (task.local_roles.includes('sub') && task.files.agent.published_etag !== undefined && task.files.agent.published_etag !== consensus.agent_md_etag && task.verified_relation_version === undefined)
945
1305
  throw failure('conflict', 'NS accepted agent.md revision changed');
@@ -975,19 +1335,9 @@ export class OwnershipWorkflow {
975
1335
  const matches = async (prepared) => Boolean(prepared && prepared.sha256 === evidence.agent_md_sha256 && prepared.sha256 === await ownershipSha256(prepared.raw) && (raw === undefined || prepared.raw === raw) && (prepared.published_etag === undefined || prepared.published_etag === evidence.agent_md_etag));
976
1336
  if (await matches(task.files.cleanup))
977
1337
  return task.operation_id;
978
- let history;
979
- try {
980
- history = await this.storage().listOwnershipTasks(this.deps.currentAid());
981
- }
982
- catch (error) {
983
- throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
984
- }
985
- for (const snapshot of history) {
986
- if (!['bind', 'transfer'].includes(String(snapshot.record.operation)))
987
- continue;
988
- if (snapshot.record.sub_aid !== task.sub_aid)
1338
+ for (const candidate of await this.records(this.deps.currentAid(), task.sub_aid, ['bind', 'transfer'])) {
1339
+ if (!['bind', 'transfer'].includes(candidate.operation))
989
1340
  continue;
990
- const candidate = await this.record(snapshot);
991
1341
  if (task.cleanup_authorization_operation_id && candidate.operation_id !== task.cleanup_authorization_operation_id || candidate.local_decision !== 'accept' || !candidate.local_roles.includes('sub') || candidate.sub_aid !== task.sub_aid || targetOwner(candidate) !== task.owner_aid || candidate.target_relation_version !== task.base_relation_version || !await matches(candidate.release_cleanup?.files.cleanup))
992
1342
  continue;
993
1343
  const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
@@ -1008,14 +1358,7 @@ export class OwnershipWorkflow {
1008
1358
  throw failure('persistence_unavailable', 'ownership cleanup historical authorization is damaged');
1009
1359
  return true;
1010
1360
  }
1011
- let history;
1012
- try {
1013
- history = await this.storage().listOwnershipTasks(context.aid);
1014
- }
1015
- catch (error) {
1016
- throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
1017
- }
1018
- const authorized = (await Promise.all(history.filter((row) => row.record.sub_aid === task.sub_aid).map((row) => this.record(row)))).find(matches);
1361
+ const authorized = (await this.records(context.aid, task.sub_aid, ['bind', 'transfer'])).find(matches);
1019
1362
  if (!authorized)
1020
1363
  return false;
1021
1364
  task.cleanup_authorization_operation_id = authorized.operation_id;
@@ -1147,13 +1490,28 @@ export class OwnershipWorkflow {
1147
1490
  const accepted = participants(task).every((aid) => task.approvals[aid] === 'accept');
1148
1491
  if (task.operation === 'bind') {
1149
1492
  if (task.local_roles.includes('owner') && accepted) {
1150
- await this.deps.files.declareSubaid(this.fileTask(context));
1493
+ if (task.legacy_recovery && !task.files.owner) {
1494
+ const owner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid), checkpoint = task.legacy_recovery.checkpoint.owner_md;
1495
+ if (owner.exists !== checkpoint.exists || owner.content !== checkpoint.raw || owner.sha256 !== checkpoint.sha256)
1496
+ throw failure('conflict', 'legacy owner checkpoint was replaced');
1497
+ }
1498
+ const prepared = await this.deps.files.declareSubaid(this.fileTask(context));
1151
1499
  this.enqueue(task, task.sub_aid, 'owner.ready');
1152
- if (task.local_roles.includes('sub'))
1500
+ if (task.local_roles.includes('sub')) {
1501
+ if (task.legacy_recovery) {
1502
+ const owner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
1503
+ if (!owner.exists || owner.content !== prepared.raw || owner.sha256 !== prepared.sha256)
1504
+ throw failure('conflict', 'legacy local owner confirmation differs');
1505
+ const file = await this.deps.files.verify(owner.content, 'owner', task.sub_aid, task.owner_aid);
1506
+ if (file.action !== 'declare' || file.statement?.relation_version !== task.target_relation_version)
1507
+ throw failure('conflict', 'legacy local owner declaration differs');
1508
+ task.legacy_recovery.owner_confirmed = true;
1509
+ }
1153
1510
  task.hints['owner.ready'] = true;
1511
+ }
1154
1512
  await this.save(context);
1155
1513
  }
1156
- if (task.local_roles.includes('sub') && (task.hints['owner.ready'] || task.approvals[task.owner_aid] === 'accept' || task.files.agent)) {
1514
+ if (task.local_roles.includes('sub') && (task.hints['owner.ready'] || !task.legacy_recovery && task.approvals[task.owner_aid] === 'accept' || task.files.agent) && (!task.legacy_recovery || task.legacy_recovery.owner_confirmed && accepted || task.files.agent)) {
1157
1515
  await this.deps.files.declareOwneraid(this.fileTask(context));
1158
1516
  }
1159
1517
  task.step = accepted ? 'awaiting_confirmation' : 'awaiting_peer_approval';