@agentunion/fastaun-browser 0.5.19 → 0.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +176 -78
  2. package/_packed_docs/CHANGELOG.md +176 -78
  3. package/_packed_docs/INDEX.md +288 -269
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
  7. package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
  8. package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
  9. package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
  10. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  11. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
  12. package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
  13. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  14. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
  18. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  19. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  20. package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
  21. package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
  22. package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
  23. package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
  24. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
  25. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +70 -63
  26. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
  27. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
  28. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
  29. package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
  30. package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
  31. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  32. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
  33. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
  34. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +105 -92
  35. package/_packed_docs/sdk/INDEX.md +197 -167
  36. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  37. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  38. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
  39. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  40. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +363 -0
  41. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +553 -0
  42. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
  43. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
  44. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  45. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  46. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
  47. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
  48. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
  49. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
  50. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
  51. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
  52. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
  53. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
  54. package/dist/agent-md-schema.d.ts +16 -4
  55. package/dist/agent-md-schema.d.ts.map +1 -1
  56. package/dist/agent-md-schema.js +65 -30
  57. package/dist/agent-md-schema.js.map +1 -1
  58. package/dist/agent-md.d.ts +35 -3
  59. package/dist/agent-md.d.ts.map +1 -1
  60. package/dist/agent-md.js +384 -150
  61. package/dist/agent-md.js.map +1 -1
  62. package/dist/aid-store.d.ts +59 -0
  63. package/dist/aid-store.d.ts.map +1 -1
  64. package/dist/aid-store.js +202 -16
  65. package/dist/aid-store.js.map +1 -1
  66. package/dist/aid.d.ts +4 -1
  67. package/dist/aid.d.ts.map +1 -1
  68. package/dist/aid.js +24 -42
  69. package/dist/aid.js.map +1 -1
  70. package/dist/auth.d.ts +1 -1
  71. package/dist/auth.d.ts.map +1 -1
  72. package/dist/auth.js +89 -18
  73. package/dist/auth.js.map +1 -1
  74. package/dist/bundle.js +6410 -1848
  75. package/dist/cert-utils.d.ts +50 -1
  76. package/dist/cert-utils.d.ts.map +1 -1
  77. package/dist/cert-utils.js +123 -56
  78. package/dist/cert-utils.js.map +1 -1
  79. package/dist/client/delivery.d.ts +108 -16
  80. package/dist/client/delivery.d.ts.map +1 -1
  81. package/dist/client/delivery.js +1411 -184
  82. package/dist/client/delivery.js.map +1 -1
  83. package/dist/client/rpc-pipeline.d.ts +4 -1
  84. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  85. package/dist/client/rpc-pipeline.js +203 -117
  86. package/dist/client/rpc-pipeline.js.map +1 -1
  87. package/dist/client/v2-e2ee.d.ts +22 -3
  88. package/dist/client/v2-e2ee.d.ts.map +1 -1
  89. package/dist/client/v2-e2ee.js +1312 -684
  90. package/dist/client/v2-e2ee.js.map +1 -1
  91. package/dist/client.d.ts +23 -2
  92. package/dist/client.d.ts.map +1 -1
  93. package/dist/client.js +419 -101
  94. package/dist/client.js.map +1 -1
  95. package/dist/config.d.ts +4 -0
  96. package/dist/config.d.ts.map +1 -1
  97. package/dist/config.js +1 -0
  98. package/dist/config.js.map +1 -1
  99. package/dist/crypto.d.ts.map +1 -1
  100. package/dist/crypto.js +8 -6
  101. package/dist/crypto.js.map +1 -1
  102. package/dist/events.d.ts +24 -3
  103. package/dist/events.d.ts.map +1 -1
  104. package/dist/events.js +171 -29
  105. package/dist/events.js.map +1 -1
  106. package/dist/facades.d.ts +42 -1
  107. package/dist/facades.d.ts.map +1 -1
  108. package/dist/facades.js +368 -12
  109. package/dist/facades.js.map +1 -1
  110. package/dist/index.d.ts +6 -4
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +1 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/keystore/index.d.ts +14 -1
  115. package/dist/keystore/index.d.ts.map +1 -1
  116. package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
  117. package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
  118. package/dist/keystore/indexeddb-identity-store.js +30 -13
  119. package/dist/keystore/indexeddb-identity-store.js.map +1 -1
  120. package/dist/keystore/indexeddb-shared.d.ts +19 -2
  121. package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
  122. package/dist/keystore/indexeddb-shared.js +193 -6
  123. package/dist/keystore/indexeddb-shared.js.map +1 -1
  124. package/dist/keystore/indexeddb-token-store.d.ts +15 -2
  125. package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
  126. package/dist/keystore/indexeddb-token-store.js +308 -27
  127. package/dist/keystore/indexeddb-token-store.js.map +1 -1
  128. package/dist/org-protocol.d.ts +45 -0
  129. package/dist/org-protocol.d.ts.map +1 -1
  130. package/dist/org-protocol.js +827 -21
  131. package/dist/org-protocol.js.map +1 -1
  132. package/dist/ownership-v21.d.ts +43 -9
  133. package/dist/ownership-v21.d.ts.map +1 -1
  134. package/dist/ownership-v21.js +183 -81
  135. package/dist/ownership-v21.js.map +1 -1
  136. package/dist/ownership-workflow.d.ts +36 -1
  137. package/dist/ownership-workflow.d.ts.map +1 -1
  138. package/dist/ownership-workflow.js +610 -89
  139. package/dist/ownership-workflow.js.map +1 -1
  140. package/dist/seq-tracker.d.ts +2 -0
  141. package/dist/seq-tracker.d.ts.map +1 -1
  142. package/dist/seq-tracker.js +3 -2
  143. package/dist/seq-tracker.js.map +1 -1
  144. package/dist/tools/cross-sdk-agent.d.ts +61 -3
  145. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  146. package/dist/tools/cross-sdk-agent.js +524 -45
  147. package/dist/tools/cross-sdk-agent.js.map +1 -1
  148. package/dist/transport.d.ts +4 -0
  149. package/dist/transport.d.ts.map +1 -1
  150. package/dist/transport.js +64 -30
  151. package/dist/transport.js.map +1 -1
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/package.json +1 -1
@@ -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,8 +89,9 @@ 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 + 1)
85
- throw new ValidationError('ownership target version must be base plus one');
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))
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)
88
97
  throw new ValidationError('transfer target must differ from current owner');
@@ -148,8 +157,9 @@ 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));
152
- if (message.action !== 'operation.stopped' && (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target : !base && !target))
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);
161
+ const cleaned = message.action === 'sub.cleaned' && Boolean(task) && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid;
162
+ if (message.action !== 'operation.stopped' && (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target && !cleaned : !base && !target))
153
163
  throw failure('conflict', 'ownership current NS relation does not match this task');
154
164
  }
155
165
  }
@@ -199,7 +209,7 @@ export class OwnershipWorkflow {
199
209
  const store = this.deps.store;
200
210
  for (const name of ['loadOwnershipTask', 'listOwnershipTasks', 'compareAndSwapOwnershipTask', 'acquireOwnershipTaskLease', 'renewOwnershipTaskLease', 'releaseOwnershipTaskLease']) {
201
211
  if (typeof store[name] !== 'function')
202
- throw failure('persistence_unavailable', 'ownership durable task storage is unavailable');
212
+ throw storageFailure('ownership durable task storage is unavailable');
203
213
  }
204
214
  return store;
205
215
  }
@@ -208,17 +218,21 @@ export class OwnershipWorkflow {
208
218
  return await this.storage().loadOwnershipTask(aid, operationId);
209
219
  }
210
220
  catch (error) {
211
- throw failure('persistence_unavailable', `ownership task read failed: ${String(error)}`);
221
+ throw storageFailure(`ownership task read failed: ${String(error)}`);
212
222
  }
213
223
  }
214
- async record(snapshot) {
224
+ async record(snapshot, legacy = false) {
215
225
  const task = snapshot.record;
226
+ if (snapshot.record.reset === true && snapshot.revision > 0)
227
+ throw failure('conflict', 'ownership operation was reset');
216
228
  const localAid = this.deps.currentAid();
217
229
  try {
230
+ if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1)
231
+ throw new Error('invalid persisted task revision');
218
232
  if (task.schema_version !== 1 || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(task.status) || !['pending', 'accept', 'reject'].includes(task.local_decision) || typeof task.step !== 'string')
219
233
  throw new Error('invalid task state');
220
234
  parseOwnershipMessage({ ...this.message(task, task.operation === 'bind' ? 'bind.request' : task.operation === 'transfer' ? 'transfer.request' : 'relation.confirmed') });
221
- if (!participants(task).includes(task.initiator_aid) || JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, localAid)) || !task.local_roles.length)
235
+ if (!participants(task).includes(task.initiator_aid) || task.operation !== 'bind' && task.initiator_aid !== task.owner_aid || JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, localAid)) || !task.local_roles.length)
222
236
  throw new Error('invalid local task identity');
223
237
  if (!task.approvals || typeof task.approvals !== 'object' || Array.isArray(task.approvals) || Object.entries(task.approvals).some(([aid, decision]) => !participants(task).includes(aid) || !['pending', 'accept', 'reject'].includes(decision)))
224
238
  throw new Error('invalid task approvals');
@@ -226,6 +240,54 @@ export class OwnershipWorkflow {
226
240
  throw new Error('incomplete task approvals or initiator decision');
227
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')
228
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
+ }
267
+ const base = task.base_agent_md;
268
+ if (!base && !legacy && !task.legacy_task && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
269
+ throw new Error('missing original agent.md context');
270
+ if (base) {
271
+ if (typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || typeof base.etag !== 'string' || base.exists && !base.etag || base.sha256 !== await ownershipSha256(base.raw))
272
+ throw new Error('invalid original agent.md bytes or ETag');
273
+ const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: task.sub_aid }).statement : undefined;
274
+ if ((statement?.relation_version ?? 0) !== task.base_relation_version || statement && task.operation !== 'bind' && statement.owner_aid !== task.owner_aid || (statement ? !this.sameStatement(statement, base.statement ?? undefined) : base.statement !== null))
275
+ throw new Error('invalid original agent.md statement');
276
+ }
277
+ if (task.base_owner_md) {
278
+ const prior = task.base_owner_md;
279
+ if (typeof prior.exists !== 'boolean' || typeof prior.raw !== 'string' || prior.sha256 !== (prior.exists ? await ownershipSha256(prior.raw) : '') || !prior.exists && prior.raw !== '')
280
+ throw new Error('invalid original owner.md context');
281
+ if (base?.statement && !prior.exists && (task.operation === 'transfer' || task.operation === 'release' && task.local_roles.includes('owner')))
282
+ throw new Error('missing original owner.md relation');
283
+ if (prior.exists) {
284
+ const file = parseOwnershipFile(prior.raw, 'owner', { expectedSubAid: task.sub_aid });
285
+ if (!file.statement || file.statement.owner_aid !== base?.statement?.owner_aid || file.statement.relation_version !== base?.statement?.relation_version)
286
+ throw new Error('invalid original owner.md statement');
287
+ }
288
+ }
289
+ if (base?.statement && !task.base_owner_md && !(task.operation === 'release' && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && task.cleanup_authorization_operation_id && !task.files.cleanup))
290
+ throw new Error('missing original owner.md context');
229
291
  if (task.approvals[localAid] !== task.local_decision)
230
292
  throw new Error('task decision and approval differ');
231
293
  if (task.status === 'completed' && (task.verified_relation_version !== task.target_relation_version || task.operation !== 'release' && task.local_decision !== 'accept'))
@@ -239,20 +301,23 @@ export class OwnershipWorkflow {
239
301
  if (!['agent', 'owner', 'release', 'transfer', 'cleanup'].includes(key) || !prepared || typeof prepared.raw !== 'string' || !/^[0-9a-f]{64}$/.test(prepared.sha256) || await ownershipSha256(prepared.raw) !== prepared.sha256 || !prepared.condition || typeof prepared.condition !== 'object' || Array.isArray(prepared.condition))
240
302
  throw new Error('invalid prepared file bytes');
241
303
  const condition = prepared.condition;
242
- if (Object.keys(condition).length !== 1 || (agentFile ? !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*') : typeof condition.expected_revision !== 'string' || !condition.expected_revision))
304
+ if (agentFile ? Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*') : Object.keys(condition).length !== 0)
243
305
  throw new Error('invalid prepared file condition');
306
+ const original = prepared.original_owner;
307
+ if (!agentFile && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? await ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
308
+ throw new Error('invalid prepared owner original');
244
309
  if (key === 'cleanup' && task.operation !== 'release' || key === 'release' && task.operation !== 'release' || key === 'transfer' && task.operation !== 'transfer' || ['agent', 'owner'].includes(key) && task.operation === 'release')
245
310
  throw new Error('invalid prepared file operation');
246
311
  const ownerAid = key === 'owner' || key === 'agent' ? targetOwner(task) : task.owner_aid;
247
312
  const file = parseOwnershipFile(prepared.raw, agentFile ? 'agent' : 'owner', { expectedSubAid: task.sub_aid, expectedOwnerAid: key === 'cleanup' ? undefined : ownerAid });
248
- 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')))
249
314
  throw new Error('invalid prepared file local role');
250
315
  if (key === 'cleanup' ? Boolean(file.statement) : file.statement?.relation_version !== (key === 'owner' || key === 'agent' ? task.target_relation_version : task.base_relation_version))
251
316
  throw new Error('invalid prepared file generation');
252
317
  const fileExpiry = prepared.expires_at;
253
318
  if (!agentFile && file.action !== (key === 'owner' ? 'declare' : key === 'release' ? 'release' : 'transfer_owner') || key === 'transfer' && (typeof fileExpiry !== 'number' || !Number.isSafeInteger(fileExpiry) || fileExpiry <= 0 || fileExpiry !== task.expires_at || file.transfer?.new_owner_aid !== task.new_owner_aid || file.transfer?.target_relation_version !== task.target_relation_version || file.transfer?.expires_at !== fileExpiry) || key !== 'transfer' && fileExpiry !== undefined)
254
319
  throw new Error('invalid prepared file authorization');
255
- if (prepared.published_revision !== undefined && (agentFile || typeof prepared.published_revision !== 'string' || !prepared.published_revision) || prepared.published_etag !== undefined && (!agentFile || typeof prepared.published_etag !== 'string' || !prepared.published_etag) || prepared.cleaned !== undefined && typeof prepared.cleaned !== 'boolean')
320
+ if (prepared.published_etag !== undefined && (!agentFile || typeof prepared.published_etag !== 'string' || !prepared.published_etag) || prepared.cleaned !== undefined && typeof prepared.cleaned !== 'boolean')
256
321
  throw new Error('invalid prepared file result');
257
322
  }
258
323
  if (task.release_cleanup !== undefined) {
@@ -266,19 +331,27 @@ export class OwnershipWorkflow {
266
331
  }
267
332
  if (task.status === 'completed') {
268
333
  const consensus = task.verified_consensus;
269
- if (!consensus || consensus.protocol !== '2.1' || consensus.sub_aid !== task.sub_aid || 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))
334
+ const cleaned = task.operation === 'release' && consensus?.status === 'unbound' && consensus.relation_version === 0 && Boolean(await this.cleanupEvidence(task, consensus));
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))
270
336
  throw new Error('invalid persisted consensus evidence');
271
337
  if (task.operation === 'release') {
272
- if (consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
338
+ if (!cleaned && consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
273
339
  throw new Error('invalid persisted release evidence');
274
340
  }
275
341
  else {
276
- if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) || typeof consensus.owner_md_revision !== 'string' || !consensus.owner_md_revision || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256))
342
+ if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256))
277
343
  throw new Error('invalid persisted binding evidence');
278
- 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)
279
345
  throw new Error('persisted consensus differs from local original files');
280
346
  }
281
347
  }
348
+ for (const key of task.received) {
349
+ if (typeof key !== 'string' || key.split('|').length !== 2)
350
+ throw new Error('invalid received journal');
351
+ const [fromAid, action] = key.split('|');
352
+ const message = parseOwnershipMessage(this.message(task, action));
353
+ validateOwnershipMessage(message, { authenticated_from_aid: fromAid, local_aid: localAid, task });
354
+ }
282
355
  for (const item of task.inbox) {
283
356
  parseOwnershipMessage(item.payload);
284
357
  validateOwnershipMessage(item.payload, { authenticated_from_aid: item.from_aid, local_aid: localAid, task });
@@ -287,6 +360,11 @@ export class OwnershipWorkflow {
287
360
  }
288
361
  for (const item of task.outbox) {
289
362
  parseOwnershipMessage(item.payload);
363
+ const action = item.payload.action;
364
+ if (['bind.accept', 'transfer.accept', 'owner.ready', 'transfer.ready'].includes(action) && task.local_decision !== 'accept' || ['bind.reject', 'transfer.reject'].includes(action) && task.local_decision !== 'reject')
365
+ throw new Error('invalid outbox local approval');
366
+ if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action) && !task.request_validated)
367
+ throw new Error('invalid outbox received request');
290
368
  validateOwnershipMessage(item.payload, { authenticated_from_aid: localAid, local_aid: item.to_aid, task });
291
369
  if (item.key !== `${item.to_aid}|${item.payload.action}` || typeof item.message_id !== 'string' || !/^m-[0-9a-f]{32}$/.test(item.message_id))
292
370
  throw new Error('invalid outbox target');
@@ -294,7 +372,9 @@ export class OwnershipWorkflow {
294
372
  return task;
295
373
  }
296
374
  catch (error) {
297
- 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 });
298
378
  }
299
379
  }
300
380
  message(task, action, reason) {
@@ -323,7 +403,7 @@ export class OwnershipWorkflow {
323
403
  event(context, event, extra = {}) {
324
404
  context.events.push({ event, payload: { ...ownershipTaskResult(context.task), initiator_aid: context.task.initiator_aid, local_roles: context.task.local_roles,
325
405
  ...(event === 'ownership.request' ? { requester_aid: context.task.initiator_aid } : {}),
326
- ...(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 } });
327
407
  }
328
408
  async save(context) {
329
409
  context.guard?.();
@@ -394,22 +474,32 @@ export class OwnershipWorkflow {
394
474
  if (!saved && !initial)
395
475
  throw failure('operation_not_found', 'ownership operation was not found');
396
476
  if (!saved && initial) {
397
- const existing = (await this.records(aid)).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));
398
478
  if (existing) {
399
479
  if (!this.sameIntent(existing, initial) || existing.base_relation_version !== initial.base_relation_version || existing.target_relation_version !== initial.target_relation_version)
400
480
  throw failure('conflict', 'another local operation for this sub is pending');
401
- try {
402
- await store.releaseOwnershipTaskLease(aid, lease);
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: [] };
403
489
  }
404
- catch (error) {
405
- throw failure('persistence_unavailable', `ownership sub lease release failed: ${String(error)}`);
406
- }
407
- return { result: await this.advance(subAid, existing.operation_id, undefined, events, undefined, guard), events: [] };
408
490
  }
409
491
  }
410
492
  context = { aid, task: saved ? await this.record(saved) : initial, revision: saved?.revision ?? null, lease, events: [], guard };
411
493
  if (context.task.sub_aid !== subAid)
412
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
+ }
413
503
  if (!saved)
414
504
  await this.save(context);
415
505
  await mutate?.(context);
@@ -435,40 +525,198 @@ export class OwnershipWorkflow {
435
525
  sameIntent(task, intent) {
436
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;
437
527
  }
438
- async records(aid) {
528
+ async records(aid, subAid, operations, strictSub = false) {
439
529
  let snapshots;
440
530
  try {
441
- snapshots = await this.storage().listOwnershipTasks(aid);
531
+ snapshots = await this.storage().listOwnershipTasks(aid, true);
442
532
  }
443
533
  catch (error) {
444
- 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
+ }
445
552
  }
446
- return await Promise.all(snapshots.map((snapshot) => this.record(snapshot)));
553
+ return records;
447
554
  }
448
555
  async observe(subAid) {
449
556
  this.requireApplicable(subAid);
450
557
  const snapshot = await this.deps.files.readAgent(subAid);
451
- const file = snapshot.exists ? snapshot.lock === 'unlocked' && snapshot.relation_version > 0
452
- ? parseOwnershipFile(snapshot.content, 'agent', { expectedSubAid: subAid })
453
- : await this.deps.files.verify(snapshot.content, 'agent', subAid) : null;
558
+ const file = snapshot.exists ? await this.deps.files.verify(snapshot.content, 'agent', subAid) : null;
454
559
  if (file)
455
560
  requireOwnershipApplicable(file.frontmatter.type);
456
- if (snapshot.lock === 'unlocked' && snapshot.relation_version === 0 && file?.statement)
561
+ if ((file?.statement?.relation_version ?? 0) !== snapshot.relation_version)
457
562
  throw failure('unavailable', 'NS relation metadata does not match agent.md', true);
458
563
  if (snapshot.lock === 'locked' && (!file?.statement || file.statement.relation_version !== snapshot.relation_version || snapshot.owner_aid !== undefined && snapshot.owner_aid !== file.statement.owner_aid))
459
564
  throw failure('unavailable', 'NS lock and current signed agent.md differ', true);
460
565
  return { ...snapshot, file, owner_aid: snapshot.lock === 'locked' ? file.statement.owner_aid : null };
461
566
  }
462
567
  isBase(task, ns) {
463
- return ns.relation_version === task.base_relation_version && (task.operation === 'bind' ? ns.lock === 'unlocked' : ns.lock === 'locked' && ns.owner_aid === task.owner_aid);
568
+ const base = task.base_agent_md;
569
+ return ns.relation_version === task.base_relation_version && (task.operation === 'bind' ? ns.lock === 'unlocked' : ns.lock === 'locked' && ns.owner_aid === task.owner_aid) && (!base || base.exists === ns.exists && (base.statement ? this.sameStatement(base.statement, ns.file?.statement) : !ns.file?.statement && base.raw === ns.content && base.etag === ns.etag));
570
+ }
571
+ async captureBase(task, ns) {
572
+ if (!task.base_agent_md)
573
+ task.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: await ownershipSha256(ns.content), etag: ns.etag, statement: ns.file?.statement ?? null };
574
+ if (!task.base_owner_md) {
575
+ const prior = ns.file?.statement ? await this.deps.files.readOwner(ns.file.statement.owner_aid, task.sub_aid) : null;
576
+ task.base_owner_md = prior ? { exists: prior.exists, raw: prior.content, sha256: prior.sha256 } : { exists: false, raw: '', sha256: '' };
577
+ }
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);
464
660
  }
465
661
  isTarget(task, ns) {
466
- 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));
467
663
  }
468
- fileTask(context) {
469
- const task = { ...context.task, files: context.task.files,
470
- persist: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; await this.save(context); },
471
- guard: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; await this.save(context); } };
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);
715
+ }
716
+ fileTask(context, expectedStatement) {
717
+ const task = { ...context.task, files: context.task.files, expected_agent_statement: expectedStatement,
718
+ persist: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; context.task.base_agent_md = task.base_agent_md; context.task.base_owner_md = task.base_owner_md; await this.save(context); },
719
+ guard: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; context.task.base_agent_md = task.base_agent_md; context.task.base_owner_md = task.base_owner_md; await this.save(context); } };
472
720
  return task;
473
721
  }
474
722
  async start(operation, subAid, ownerAid, newOwnerAid, requestedId, guard) {
@@ -502,7 +750,11 @@ export class OwnershipWorkflow {
502
750
  }
503
751
  }
504
752
  const intent = { initiator_aid: aid, operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid };
505
- for (const pending of (await this.records(aid)).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');
506
758
  if (this.sameIntent(pending, intent)) {
507
759
  const result = await this.advance(subAid, pending.operation_id, undefined, events, undefined, guard);
508
760
  if (result.status !== 'stopped' || result.code !== 'conflict' || !/relation version or target was replaced/.test(result.reason ?? ''))
@@ -519,12 +771,12 @@ export class OwnershipWorkflow {
519
771
  }
520
772
  const ns = await this.observe(subAid);
521
773
  if (operation === 'bind') {
522
- if (!ns.exists)
523
- throw failure('unavailable', 'initial agent.md is not published', true);
524
774
  if (ns.lock === 'locked')
525
775
  throw failure('owner_locked', 'the sub AID has a current owner lock');
526
- if (ns.file?.frontmatter.type === 'human')
776
+ if ((ns.exists ? ns.file?.frontmatter.type : subAid === aid ? this.deps.currentAidType?.() : undefined) === 'human')
527
777
  throw failure('invalid', 'human cannot declare an owner');
778
+ if (ns.file?.statement?.owner_aid === ownerAid)
779
+ throw failure('conflict', 'current owner release requires sub cleanup before binding again');
528
780
  }
529
781
  else {
530
782
  if (aid !== ownerAid || ns.lock !== 'locked' || ns.owner_aid !== aid)
@@ -533,10 +785,11 @@ export class OwnershipWorkflow {
533
785
  if (consensus.status !== 'consensus' || consensus.owner_aid !== aid || consensus.relation_version !== ns.relation_version)
534
786
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'the current owner relation could not be verified', consensus.status !== 'invalid');
535
787
  }
536
- ownershipInteger(ns.relation_version + 1, 'target_relation_version');
788
+ const targetVersion = ownershipInteger(ns.relation_version + (operation === 'release' ? 0 : 1), 'target_relation_version');
537
789
  const operationId = requestedId ?? crypto.randomUUID();
538
- const payload = { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation, action: operation === 'bind' ? 'bind.request' : operation === 'transfer' ? 'transfer.request' : 'relation.confirmed', sub_aid: subAid, owner_aid: ownerAid, ...(newOwnerAid ? { new_owner_aid: newOwnerAid } : {}), base_relation_version: ns.relation_version, target_relation_version: ns.relation_version + 1 };
790
+ const payload = { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation, action: operation === 'bind' ? 'bind.request' : operation === 'transfer' ? 'transfer.request' : 'relation.confirmed', sub_aid: subAid, owner_aid: ownerAid, ...(newOwnerAid ? { new_owner_aid: newOwnerAid } : {}), base_relation_version: ns.relation_version, target_relation_version: targetVersion };
539
791
  const task = this.create(payload, aid, 'accept');
792
+ await this.captureBase(task, ns);
540
793
  if (!task.local_roles.length)
541
794
  throw failure('forbidden', 'the local AID is not an ownership participant');
542
795
  if (aid !== this.deps.currentAid())
@@ -558,6 +811,128 @@ export class OwnershipWorkflow {
558
811
  async releaseSubaid(subAid) { return await this.start('release', subAid, this.deps.currentAid()); }
559
812
  async transferSubaid(subAid, newOwnerAid) { return await this.start('transfer', subAid, this.deps.currentAid(), newOwnerAid); }
560
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
+ }
561
936
  async checkOwnershipConsensus(subAid = this.deps.currentAid()) { return await this.deps.files.checkConsensus(subAid); }
562
937
  async decide(operationId, decision, reason) {
563
938
  const saved = await this.load(operationId);
@@ -623,6 +998,8 @@ export class OwnershipWorkflow {
623
998
  throw error;
624
999
  }
625
1000
  const snapshot = await this.load(message.operation_id);
1001
+ if (snapshot?.record.reset === true && snapshot.revision > 0)
1002
+ return true;
626
1003
  const prior = snapshot ? await this.record(snapshot) : undefined;
627
1004
  if (!prior && !['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
628
1005
  return true;
@@ -662,34 +1039,54 @@ export class OwnershipWorkflow {
662
1039
  return true;
663
1040
  }
664
1041
  async confirmSubCleaned(task) {
665
- const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid), after = await this.deps.files.readAgent(task.sub_aid);
666
- if (this.now() - began > 30 || before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid || await ownershipSha256(before.content) !== await ownershipSha256(after.content))
667
- throw failure('unavailable', 'NS changed during sub cleanup verification', true);
668
- if (!before.exists || before.relation_version !== task.target_relation_version || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
1042
+ const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid);
1043
+ if (!before.exists || before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
669
1044
  return false;
670
1045
  const file = parseOwnershipFile(before.content, 'agent', { expectedSubAid: task.sub_aid });
671
- return !Object.hasOwn(file.frontmatter, 'owner') && !Object.hasOwn(file.frontmatter, 'ownership');
1046
+ if (Object.hasOwn(file.frontmatter, 'owner') || Object.hasOwn(file.frontmatter, 'ownership'))
1047
+ return false;
1048
+ if (!task.base_agent_md?.statement || task.base_agent_md.statement.owner_aid !== task.owner_aid || task.base_agent_md.statement.sub_aid !== task.sub_aid || task.base_agent_md.statement.relation_version !== task.base_relation_version)
1049
+ return false;
1050
+ let ownerHash = '';
1051
+ if (task.local_roles.includes('owner') && task.files.release) {
1052
+ const original = task.files.release, object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
1053
+ if (object.exists && object.content === original.raw && object.sha256 === original.sha256) {
1054
+ const file = await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
1055
+ if (file.action === 'release' && file.statement && ['version', 'owner_aid', 'sub_aid', 'relation_version'].every((key) => file.statement[key] === task.base_agent_md.statement[key]))
1056
+ ownerHash = object.sha256;
1057
+ }
1058
+ }
1059
+ const confirmed = Boolean(await this.cleanupEvidence(task, { agent_md_etag: before.etag, agent_md_sha256: await ownershipSha256(before.content), owner_md_sha256: ownerHash }, before.content));
1060
+ const after = await this.deps.files.readAgent(task.sub_aid);
1061
+ if (this.now() - began > 30 || before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid || before.content !== after.content)
1062
+ throw failure('unavailable', 'NS changed during sub cleanup verification', true);
1063
+ return confirmed;
672
1064
  }
673
1065
  async resume(guard) {
674
1066
  guard?.();
675
1067
  let snapshots;
676
1068
  try {
677
- snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid());
1069
+ snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid(), true);
678
1070
  }
679
1071
  catch (error) {
680
1072
  throw failure('persistence_unavailable', `ownership task list failed: ${String(error)}`);
681
1073
  }
682
1074
  for (const snapshot of snapshots) {
683
1075
  guard?.();
684
- const task = await this.record(snapshot);
685
- 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))
1076
+ if (snapshot.record.reset === true && snapshot.revision > 0)
686
1077
  continue;
687
1078
  try {
1079
+ const task = await this.record(snapshot);
1080
+ if (!task.base_agent_md && task.request_validated && task.status === 'completed')
1081
+ continue;
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))
1083
+ continue;
688
1084
  await this.execute(task.sub_aid, task.operation_id, undefined, undefined, guard);
689
1085
  }
690
1086
  catch (error) {
1087
+ guard?.();
691
1088
  if (ownershipErrorCode(error) !== 'conflict')
692
- throw error;
1089
+ 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 });
693
1090
  }
694
1091
  }
695
1092
  }
@@ -703,6 +1100,25 @@ export class OwnershipWorkflow {
703
1100
  }
704
1101
  const entry = task.inbox[0];
705
1102
  const ns = await this.observe(task.sub_aid);
1103
+ if (!task.base_agent_md && !task.request_validated && task.local_decision === 'pending' && !Object.keys(task.files).length) {
1104
+ if (this.isBase(task, ns) || task.operation === 'release' && ns.relation_version === task.base_relation_version && ns.file?.statement?.owner_aid === task.owner_aid)
1105
+ await this.captureBase(task, ns);
1106
+ else if (task.operation === 'release' && ns.relation_version === 0 && !ns.file?.statement && task.local_roles.includes('sub')) {
1107
+ for (const candidate of await this.records(context.aid, task.sub_aid)) {
1108
+ const original = candidate.files.agent, cleanup = candidate.release_cleanup?.files.cleanup;
1109
+ if (!['bind', 'transfer'].includes(candidate.operation) || candidate.local_decision !== 'accept' || !candidate.local_roles.includes('sub') || targetOwner(candidate) !== task.owner_aid || candidate.target_relation_version !== task.base_relation_version || !original || !cleanup || cleanup.raw !== ns.content || cleanup.sha256 !== await ownershipSha256(ns.content) || cleanup.published_etag !== undefined && cleanup.published_etag !== ns.etag)
1110
+ continue;
1111
+ const statement = parseOwnershipFile(original.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
1112
+ if (!statement || !cleanup.condition?.if_match)
1113
+ continue;
1114
+ task.base_agent_md = { exists: true, raw: original.raw, sha256: original.sha256, etag: cleanup.condition.if_match, statement };
1115
+ task.cleanup_authorization_operation_id = candidate.operation_id;
1116
+ break;
1117
+ }
1118
+ }
1119
+ if (task.base_agent_md)
1120
+ await this.save(context);
1121
+ }
706
1122
  try {
707
1123
  validateOwnershipMessage(entry.payload, { authenticated_from_aid: entry.from_aid, local_aid: context.aid, task, ns });
708
1124
  }
@@ -719,6 +1135,11 @@ export class OwnershipWorkflow {
719
1135
  }
720
1136
  else if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action)) {
721
1137
  const decision = action.endsWith('.accept') ? 'accept' : 'reject';
1138
+ if (decision === 'reject' && await this.complete(context, ns)) {
1139
+ task.inbox.shift();
1140
+ await this.save(context);
1141
+ continue;
1142
+ }
722
1143
  if (!task.approvals[entry.from_aid] || task.approvals[entry.from_aid] === 'pending') {
723
1144
  task.approvals[entry.from_aid] = decision;
724
1145
  if (decision === 'reject') {
@@ -744,6 +1165,20 @@ export class OwnershipWorkflow {
744
1165
  }
745
1166
  }
746
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
+ }
747
1182
  if (action === 'transfer.ready' && this.isBase(task, ns)) {
748
1183
  const authorization = await this.deps.files.verifyTransfer(this.fileTask(context));
749
1184
  task.expires_at = authorization.transfer.expires_at;
@@ -753,7 +1188,7 @@ export class OwnershipWorkflow {
753
1188
  }
754
1189
  else if (action === 'sub.cleaned') {
755
1190
  const file = ns.file;
756
- if (ns.relation_version === task.target_relation_version && ns.lock === 'unlocked' && !ns.owner_aid && file && !file.statement && !Object.hasOwn(file.frontmatter, 'owner') && !Object.hasOwn(file.frontmatter, 'ownership'))
1191
+ if (ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid && file && !file.statement && !Object.hasOwn(file.frontmatter, 'owner') && !Object.hasOwn(file.frontmatter, 'ownership'))
757
1192
  task.hints[action] = true;
758
1193
  }
759
1194
  task.inbox.shift();
@@ -763,10 +1198,29 @@ export class OwnershipWorkflow {
763
1198
  async flush(context, ns) {
764
1199
  const task = context.task;
765
1200
  for (const item of [...task.outbox]) {
1201
+ ns = await this.observe(task.sub_aid);
766
1202
  const action = item.payload.action;
767
1203
  const notification = ['relation.confirmed', 'sub.cleaned'].includes(action);
768
1204
  const terminal = ['bind.reject', 'transfer.reject', 'operation.stopped'].includes(action);
769
- if (!terminal && (notification ? !this.isTarget(task, ns) : !this.isBase(task, ns) || task.status === 'completed' || task.status === 'rejected' || task.status === 'stopped')) {
1205
+ const cleaned = action === 'sub.cleaned' && task.status === 'completed' && await this.confirmSubCleaned(task);
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')) {
770
1224
  task.outbox = task.outbox.filter((row) => row.key !== item.key);
771
1225
  await this.save(context);
772
1226
  continue;
@@ -789,19 +1243,43 @@ export class OwnershipWorkflow {
789
1243
  }
790
1244
  async complete(context, ns) {
791
1245
  const task = context.task;
792
- if (!this.isTarget(task, ns))
1246
+ const began = Date.now(), cleaned = task.operation === 'release' && ns.exists && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
1247
+ if (!this.isTarget(task, ns) && !cleaned)
793
1248
  return false;
794
1249
  let consensus;
795
1250
  if (task.operation === 'release') {
796
1251
  consensus = await this.deps.files.checkConsensus(task.sub_aid);
797
- if (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)
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))
798
1253
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership release could not be freshly confirmed', consensus.status !== 'invalid');
1254
+ if (!cleaned && task.files.release && consensus.owner_md_sha256) {
1255
+ const source = task.files.release;
1256
+ if (source.sha256 !== consensus.owner_md_sha256)
1257
+ throw failure('conflict', 'current release material does not belong to this task');
1258
+ }
1259
+ if (cleaned) {
1260
+ const source = task.files.release;
1261
+ if (task.local_roles.includes('owner') && source) {
1262
+ const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
1263
+ if (object.exists && object.content === source.raw && object.sha256 === source.sha256) {
1264
+ await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
1265
+ consensus.owner_md_sha256 = object.sha256;
1266
+ }
1267
+ }
1268
+ const authorization = await this.cleanupEvidence(task, consensus, ns.content);
1269
+ if (!authorization)
1270
+ throw failure('conflict', 'no local cleanup original can confirm this release task');
1271
+ const after = await this.observe(task.sub_aid);
1272
+ if (Date.now() - began > 30_000 || after.etag !== ns.etag || after.content !== ns.content || !after.exists || after.relation_version !== 0 || after.lock !== 'unlocked' || after.owner_aid)
1273
+ throw failure('unavailable', 'NS changed during cleanup confirmation', true);
1274
+ if (authorization !== task.operation_id)
1275
+ task.cleanup_authorization_operation_id = authorization;
1276
+ }
799
1277
  const prior = ns.file?.statement;
800
- if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
1278
+ if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version || !this.sameStatement(prior, task.base_agent_md?.statement ?? undefined)))
801
1279
  throw failure('conflict', 'ownership released document does not match this generation');
802
1280
  if (task.local_roles.includes('owner') && !task.files.release)
803
1281
  throw failure('conflict', 'ownership completion lacks this task original owner file');
804
- if (prior && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && !await this.authorizeCleanup(context)) {
1282
+ if (prior && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && !await this.authorizeCleanup(context, prior)) {
805
1283
  task.status = 'pending';
806
1284
  task.step = 'cleanup_action_required';
807
1285
  task.cleanup_pending = true;
@@ -821,16 +1299,14 @@ export class OwnershipWorkflow {
821
1299
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership target materials could not be verified', consensus.status !== 'invalid');
822
1300
  if (task.local_roles.includes('sub') && (!task.files.agent || task.files.agent.sha256 !== consensus.agent_md_sha256))
823
1301
  throw failure('conflict', 'NS accepted agent.md does not match this task original bytes');
824
- 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)
825
1303
  throw failure('conflict', 'Storage owner.md does not match this task original bytes');
826
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)
827
1305
  throw failure('conflict', 'NS accepted agent.md revision changed');
828
- if (task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') && task.files.owner.published_revision !== undefined && task.files.owner.published_revision !== consensus.owner_md_revision && task.verified_relation_version === undefined)
829
- throw failure('conflict', 'Storage owner.md revision changed');
830
1306
  }
831
1307
  task.status = 'completed';
832
1308
  task.step = task.operation === 'release' ? 'released' : 'confirmed';
833
- task.verified_relation_version = consensus.relation_version;
1309
+ task.verified_relation_version = task.target_relation_version;
834
1310
  task.verified_consensus = { ...consensus, protocol: '2.1' };
835
1311
  delete task.code;
836
1312
  delete task.reason;
@@ -850,23 +1326,39 @@ export class OwnershipWorkflow {
850
1326
  await this.save(context);
851
1327
  return true;
852
1328
  }
853
- async authorizeCleanup(context) {
1329
+ async cleanupEvidence(task, evidence, raw) {
1330
+ const release = task.files.release;
1331
+ if (task.local_roles.includes('owner') && release && release.sha256 === evidence.owner_md_sha256 && release.sha256 === await ownershipSha256(release.raw))
1332
+ return task.operation_id;
1333
+ if (!task.local_roles.includes('sub'))
1334
+ return undefined;
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));
1336
+ if (await matches(task.files.cleanup))
1337
+ return task.operation_id;
1338
+ for (const candidate of await this.records(this.deps.currentAid(), task.sub_aid, ['bind', 'transfer'])) {
1339
+ if (!['bind', 'transfer'].includes(candidate.operation))
1340
+ continue;
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))
1342
+ continue;
1343
+ const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
1344
+ if (statement?.owner_aid === task.owner_aid && statement.sub_aid === task.sub_aid && statement.relation_version === task.base_relation_version && this.sameStatement(statement, task.base_agent_md?.statement ?? undefined))
1345
+ return candidate.operation_id;
1346
+ }
1347
+ return undefined;
1348
+ }
1349
+ sameStatement(left, right) {
1350
+ return Boolean(left && right && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((key) => left[key] === right[key]));
1351
+ }
1352
+ async authorizeCleanup(context, statement) {
854
1353
  const task = context.task;
855
- const matches = (record) => ['bind', 'transfer'].includes(record.operation) && record.sub_aid === task.sub_aid && record.target_relation_version === task.base_relation_version && record.local_decision === 'accept' && record.local_roles.includes('sub') && targetOwner(record) === task.owner_aid;
1354
+ const matches = (record) => ['bind', 'transfer'].includes(record.operation) && record.sub_aid === task.sub_aid && record.target_relation_version === task.base_relation_version && record.local_decision === 'accept' && record.local_roles.includes('sub') && targetOwner(record) === task.owner_aid && Boolean(record.files.agent && this.sameStatement(parseOwnershipFile(record.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement, statement));
856
1355
  if (task.cleanup_authorization_operation_id) {
857
1356
  const saved = await this.load(task.cleanup_authorization_operation_id);
858
1357
  if (!saved || !matches(await this.record(saved)))
859
1358
  throw failure('persistence_unavailable', 'ownership cleanup historical authorization is damaged');
860
1359
  return true;
861
1360
  }
862
- let history;
863
- try {
864
- history = await this.storage().listOwnershipTasks(context.aid);
865
- }
866
- catch (error) {
867
- throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
868
- }
869
- const authorized = (await Promise.all(history.map((row) => this.record(row)))).find(matches);
1361
+ const authorized = (await this.records(context.aid, task.sub_aid, ['bind', 'transfer'])).find(matches);
870
1362
  if (!authorized)
871
1363
  return false;
872
1364
  task.cleanup_authorization_operation_id = authorized.operation_id;
@@ -875,13 +1367,23 @@ export class OwnershipWorkflow {
875
1367
  }
876
1368
  async resumeSubCleanup(context, ns) {
877
1369
  const task = context.task;
878
- if (!['bind', 'transfer'].includes(task.operation) || !task.local_roles.includes('sub') || task.local_decision !== 'accept' || ns.lock !== 'unlocked' || ns.relation_version !== task.target_relation_version + 1 || task.release_cleanup?.cleaned)
1370
+ if (!['bind', 'transfer'].includes(task.operation) || !task.local_roles.includes('sub') || task.local_decision !== 'accept' || ns.lock !== 'unlocked' || task.release_cleanup?.cleaned)
1371
+ return;
1372
+ if (ns.relation_version === 0 && !ns.file?.statement) {
1373
+ const prepared = task.release_cleanup?.files.cleanup;
1374
+ if (!prepared || !prepared.condition?.if_match || prepared.raw !== ns.content || prepared.sha256 !== await ownershipSha256(ns.content) || prepared.published_etag !== undefined && prepared.published_etag !== ns.etag)
1375
+ throw failure('conflict', 'historical cleanup original file was not confirmed');
1376
+ task.release_cleanup.cleaned = true;
1377
+ await this.save(context);
1378
+ return;
1379
+ }
1380
+ if (ns.relation_version !== task.target_relation_version)
879
1381
  return;
880
1382
  const consensus = await this.deps.files.checkConsensus(task.sub_aid);
881
1383
  if (consensus.status !== 'released' || consensus.relation_version !== ns.relation_version || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== await ownershipSha256(ns.content))
882
1384
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'NS changed during historical cleanup observation', consensus.status !== 'invalid');
883
1385
  const statement = ns.file?.statement;
884
- if (statement && (statement.owner_aid !== targetOwner(task) || statement.relation_version !== task.target_relation_version))
1386
+ if (!statement || !task.files.agent || !this.sameStatement(statement, parseOwnershipFile(task.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement))
885
1387
  throw failure('conflict', 'historical cleanup fields belong to another relation');
886
1388
  if (!task.release_cleanup) {
887
1389
  task.release_cleanup = { files: {}, cleaned: false };
@@ -889,7 +1391,7 @@ export class OwnershipWorkflow {
889
1391
  }
890
1392
  if (statement || task.release_cleanup.files.cleanup)
891
1393
  await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: task.sub_aid, owner_aid: targetOwner(task),
892
- base_relation_version: task.target_relation_version, target_relation_version: ns.relation_version, files: task.release_cleanup.files,
1394
+ base_relation_version: task.target_relation_version, target_relation_version: ns.relation_version, files: task.release_cleanup.files, expected_agent_statement: statement,
893
1395
  persist: () => this.save(context), guard: () => this.save(context) });
894
1396
  task.release_cleanup.cleaned = true;
895
1397
  await this.save(context);
@@ -899,7 +1401,9 @@ export class OwnershipWorkflow {
899
1401
  const task = context.task;
900
1402
  if (!task.cleanup_pending || task.cleanup_done)
901
1403
  return;
902
- if (!this.isTarget(task, ns)) {
1404
+ const releaseOwner = task.operation === 'release' && task.local_roles.includes('owner');
1405
+ const cleaned = task.operation === 'release' && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
1406
+ if (!this.isTarget(task, ns) && !cleaned && !releaseOwner) {
903
1407
  task.cleanup_pending = false;
904
1408
  task.step = 'cleanup_superseded';
905
1409
  await this.save(context);
@@ -907,7 +1411,9 @@ export class OwnershipWorkflow {
907
1411
  }
908
1412
  if (task.operation === 'release' && task.local_roles.includes('sub')) {
909
1413
  const prior = ns.file?.statement;
910
- if (prior && !await this.authorizeCleanup(context)) {
1414
+ if (prior && !this.sameStatement(prior, task.base_agent_md?.statement ?? undefined))
1415
+ throw failure('conflict', 'ownership cleanup original relation was replaced');
1416
+ if (prior && !await this.authorizeCleanup(context, prior)) {
911
1417
  task.step = 'cleanup_action_required';
912
1418
  await this.save(context);
913
1419
  this.event(context, 'ownership.action_required');
@@ -915,18 +1421,18 @@ export class OwnershipWorkflow {
915
1421
  }
916
1422
  if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
917
1423
  throw failure('conflict', 'ownership cleanup current fields differ');
918
- if (prior)
919
- await this.deps.files.revokeOwneraid(this.fileTask(context));
1424
+ if (prior || task.files.cleanup)
1425
+ await this.deps.files.revokeOwneraid(this.fileTask(context, prior));
920
1426
  this.enqueue(task, task.owner_aid, 'sub.cleaned');
921
1427
  }
922
1428
  if (task.local_roles.includes('owner') && (task.operation === 'release' || task.operation === 'transfer')) {
1429
+ if (releaseOwner && this.isTarget(task, ns) && ns.file?.statement)
1430
+ return;
923
1431
  const prepared = task.files[task.operation === 'release' ? 'release' : 'transfer'];
924
- if (prepared && !prepared.published_revision) {
1432
+ if (prepared) {
925
1433
  const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
926
- if (object.exists && object.sha256 === prepared.sha256 && object.content === prepared.raw) {
927
- prepared.published_revision = object.revision;
928
- await this.save(context);
929
- }
1434
+ if (object.exists && (object.sha256 !== prepared.sha256 || object.content !== prepared.raw))
1435
+ throw failure('conflict', 'ownership cleanup original owner file has been replaced');
930
1436
  }
931
1437
  await this.deps.files.cleanupOwnerFile(this.fileTask(context));
932
1438
  }
@@ -962,7 +1468,7 @@ export class OwnershipWorkflow {
962
1468
  delete task.reason;
963
1469
  task.retryable = true;
964
1470
  if (await this.complete(context, ns)) {
965
- if (task.status === 'completed' && task.operation === 'release' && task.local_roles.includes('sub')) {
1471
+ if (task.status === 'completed' && task.operation === 'release') {
966
1472
  await this.cleanup(context, ns);
967
1473
  ns = await this.observe(task.sub_aid);
968
1474
  }
@@ -984,13 +1490,28 @@ export class OwnershipWorkflow {
984
1490
  const accepted = participants(task).every((aid) => task.approvals[aid] === 'accept');
985
1491
  if (task.operation === 'bind') {
986
1492
  if (task.local_roles.includes('owner') && accepted) {
987
- 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));
988
1499
  this.enqueue(task, task.sub_aid, 'owner.ready');
989
- 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
+ }
990
1510
  task.hints['owner.ready'] = true;
1511
+ }
991
1512
  await this.save(context);
992
1513
  }
993
- 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)) {
994
1515
  await this.deps.files.declareOwneraid(this.fileTask(context));
995
1516
  }
996
1517
  task.step = accepted ? 'awaiting_confirmation' : 'awaiting_peer_approval';