@agentunion/fastaun-browser 0.5.19 → 0.5.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +129 -78
  2. package/_packed_docs/CHANGELOG.md +129 -78
  3. package/_packed_docs/INDEX.md +201 -195
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
  5. package/_packed_docs/agent.md/SCHEMA.md +7 -7
  6. package/_packed_docs/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
  7. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
  8. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
  9. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
  10. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
  11. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  12. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
  13. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  14. package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
  15. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  16. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
  17. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  18. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
  19. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  20. package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
  21. package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
  22. package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
  23. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
  24. package/_packed_docs/sdk/INDEX.md +92 -80
  25. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  26. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  27. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
  28. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  29. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +300 -0
  30. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +382 -0
  31. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
  32. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
  33. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  34. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  35. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
  36. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
  37. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
  38. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
  39. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
  40. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
  41. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
  42. package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
  43. package/dist/agent-md-schema.d.ts +15 -4
  44. package/dist/agent-md-schema.d.ts.map +1 -1
  45. package/dist/agent-md-schema.js +64 -30
  46. package/dist/agent-md-schema.js.map +1 -1
  47. package/dist/agent-md.d.ts +10 -2
  48. package/dist/agent-md.d.ts.map +1 -1
  49. package/dist/agent-md.js +278 -121
  50. package/dist/agent-md.js.map +1 -1
  51. package/dist/aid-store.d.ts +46 -0
  52. package/dist/aid-store.d.ts.map +1 -1
  53. package/dist/aid-store.js +149 -10
  54. package/dist/aid-store.js.map +1 -1
  55. package/dist/aid.d.ts +4 -1
  56. package/dist/aid.d.ts.map +1 -1
  57. package/dist/aid.js +23 -42
  58. package/dist/aid.js.map +1 -1
  59. package/dist/auth.d.ts +1 -1
  60. package/dist/auth.d.ts.map +1 -1
  61. package/dist/auth.js +85 -18
  62. package/dist/auth.js.map +1 -1
  63. package/dist/bundle.js +1581 -781
  64. package/dist/cert-utils.d.ts +49 -1
  65. package/dist/cert-utils.d.ts.map +1 -1
  66. package/dist/cert-utils.js +102 -56
  67. package/dist/cert-utils.js.map +1 -1
  68. package/dist/client/delivery.d.ts +7 -0
  69. package/dist/client/delivery.d.ts.map +1 -1
  70. package/dist/client/delivery.js +243 -28
  71. package/dist/client/delivery.js.map +1 -1
  72. package/dist/client/rpc-pipeline.d.ts.map +1 -1
  73. package/dist/client/rpc-pipeline.js +89 -77
  74. package/dist/client/rpc-pipeline.js.map +1 -1
  75. package/dist/client/v2-e2ee.d.ts.map +1 -1
  76. package/dist/client/v2-e2ee.js +99 -80
  77. package/dist/client/v2-e2ee.js.map +1 -1
  78. package/dist/client.d.ts.map +1 -1
  79. package/dist/client.js +31 -17
  80. package/dist/client.js.map +1 -1
  81. package/dist/crypto.d.ts.map +1 -1
  82. package/dist/crypto.js +8 -6
  83. package/dist/crypto.js.map +1 -1
  84. package/dist/events.d.ts +5 -2
  85. package/dist/events.d.ts.map +1 -1
  86. package/dist/events.js +33 -14
  87. package/dist/events.js.map +1 -1
  88. package/dist/facades.js +5 -5
  89. package/dist/facades.js.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js.map +1 -1
  93. package/dist/org-protocol.d.ts.map +1 -1
  94. package/dist/org-protocol.js +72 -12
  95. package/dist/org-protocol.js.map +1 -1
  96. package/dist/ownership-v21.d.ts +25 -8
  97. package/dist/ownership-v21.d.ts.map +1 -1
  98. package/dist/ownership-v21.js +142 -73
  99. package/dist/ownership-v21.js.map +1 -1
  100. package/dist/ownership-workflow.d.ts +6 -1
  101. package/dist/ownership-workflow.d.ts.map +1 -1
  102. package/dist/ownership-workflow.js +221 -58
  103. package/dist/ownership-workflow.js.map +1 -1
  104. package/dist/tools/cross-sdk-agent.d.ts +2 -1
  105. package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
  106. package/dist/tools/cross-sdk-agent.js +91 -8
  107. package/dist/tools/cross-sdk-agent.js.map +1 -1
  108. package/dist/transport.d.ts +1 -0
  109. package/dist/transport.d.ts.map +1 -1
  110. package/dist/transport.js +2 -0
  111. package/dist/transport.js.map +1 -1
  112. package/dist/version.d.ts +1 -1
  113. package/dist/version.js +1 -1
  114. package/package.json +1 -1
@@ -81,8 +81,8 @@ export function parseOwnershipMessage(content) {
81
81
  ownershipAid(message.sub_aid, 'sub_aid');
82
82
  ownershipAid(message.owner_aid, 'owner_aid');
83
83
  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');
84
+ if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + (message.operation === 'release' ? 0 : 1))
85
+ throw new ValidationError('ownership target version does not match operation');
86
86
  if (message.operation === 'transfer') {
87
87
  if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
88
88
  throw new ValidationError('transfer target must differ from current owner');
@@ -149,7 +149,8 @@ export function validateOwnershipMessage(message, context) {
149
149
  throw failure('unsupported_protocol', 'NS ownership protocol is unavailable');
150
150
  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
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))
152
+ const cleaned = message.action === 'sub.cleaned' && Boolean(task) && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid;
153
+ if (message.action !== 'operation.stopped' && (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target && !cleaned : !base && !target))
153
154
  throw failure('conflict', 'ownership current NS relation does not match this task');
154
155
  }
155
156
  }
@@ -215,10 +216,12 @@ export class OwnershipWorkflow {
215
216
  const task = snapshot.record;
216
217
  const localAid = this.deps.currentAid();
217
218
  try {
219
+ if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1)
220
+ throw new Error('invalid persisted task revision');
218
221
  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
222
  throw new Error('invalid task state');
220
223
  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)
224
+ 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
225
  throw new Error('invalid local task identity');
223
226
  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
227
  throw new Error('invalid task approvals');
@@ -226,6 +229,30 @@ export class OwnershipWorkflow {
226
229
  throw new Error('incomplete task approvals or initiator decision');
227
230
  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
231
  throw new Error('invalid task journals');
232
+ const base = task.base_agent_md;
233
+ if (!base && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
234
+ throw new Error('missing original agent.md context');
235
+ if (base) {
236
+ if (typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || typeof base.etag !== 'string' || base.exists && !base.etag || base.sha256 !== await ownershipSha256(base.raw))
237
+ throw new Error('invalid original agent.md bytes or ETag');
238
+ const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: task.sub_aid }).statement : undefined;
239
+ 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))
240
+ throw new Error('invalid original agent.md statement');
241
+ }
242
+ if (task.base_owner_md) {
243
+ const prior = task.base_owner_md;
244
+ if (typeof prior.exists !== 'boolean' || typeof prior.raw !== 'string' || prior.sha256 !== (prior.exists ? await ownershipSha256(prior.raw) : '') || !prior.exists && prior.raw !== '')
245
+ throw new Error('invalid original owner.md context');
246
+ if (base?.statement && !prior.exists && (task.operation === 'transfer' || task.operation === 'release' && task.local_roles.includes('owner')))
247
+ throw new Error('missing original owner.md relation');
248
+ if (prior.exists) {
249
+ const file = parseOwnershipFile(prior.raw, 'owner', { expectedSubAid: task.sub_aid });
250
+ if (!file.statement || file.statement.owner_aid !== base?.statement?.owner_aid || file.statement.relation_version !== base?.statement?.relation_version)
251
+ throw new Error('invalid original owner.md statement');
252
+ }
253
+ }
254
+ 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))
255
+ throw new Error('missing original owner.md context');
229
256
  if (task.approvals[localAid] !== task.local_decision)
230
257
  throw new Error('task decision and approval differ');
231
258
  if (task.status === 'completed' && (task.verified_relation_version !== task.target_relation_version || task.operation !== 'release' && task.local_decision !== 'accept'))
@@ -239,8 +266,11 @@ export class OwnershipWorkflow {
239
266
  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
267
  throw new Error('invalid prepared file bytes');
241
268
  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))
269
+ 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
270
  throw new Error('invalid prepared file condition');
271
+ const original = prepared.original_owner;
272
+ if (!agentFile && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? await ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
273
+ throw new Error('invalid prepared owner original');
244
274
  if (key === 'cleanup' && task.operation !== 'release' || key === 'release' && task.operation !== 'release' || key === 'transfer' && task.operation !== 'transfer' || ['agent', 'owner'].includes(key) && task.operation === 'release')
245
275
  throw new Error('invalid prepared file operation');
246
276
  const ownerAid = key === 'owner' || key === 'agent' ? targetOwner(task) : task.owner_aid;
@@ -252,7 +282,7 @@ export class OwnershipWorkflow {
252
282
  const fileExpiry = prepared.expires_at;
253
283
  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
284
  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')
285
+ if (prepared.published_etag !== undefined && (!agentFile || typeof prepared.published_etag !== 'string' || !prepared.published_etag) || prepared.cleaned !== undefined && typeof prepared.cleaned !== 'boolean')
256
286
  throw new Error('invalid prepared file result');
257
287
  }
258
288
  if (task.release_cleanup !== undefined) {
@@ -266,19 +296,27 @@ export class OwnershipWorkflow {
266
296
  }
267
297
  if (task.status === 'completed') {
268
298
  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))
299
+ 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))
270
301
  throw new Error('invalid persisted consensus evidence');
271
302
  if (task.operation === 'release') {
272
- if (consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
303
+ if (!cleaned && consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
273
304
  throw new Error('invalid persisted release evidence');
274
305
  }
275
306
  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))
307
+ if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256))
277
308
  throw new Error('invalid persisted binding evidence');
278
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)
279
310
  throw new Error('persisted consensus differs from local original files');
280
311
  }
281
312
  }
313
+ for (const key of task.received) {
314
+ if (typeof key !== 'string' || key.split('|').length !== 2)
315
+ throw new Error('invalid received journal');
316
+ const [fromAid, action] = key.split('|');
317
+ const message = parseOwnershipMessage(this.message(task, action));
318
+ validateOwnershipMessage(message, { authenticated_from_aid: fromAid, local_aid: localAid, task });
319
+ }
282
320
  for (const item of task.inbox) {
283
321
  parseOwnershipMessage(item.payload);
284
322
  validateOwnershipMessage(item.payload, { authenticated_from_aid: item.from_aid, local_aid: localAid, task });
@@ -287,6 +325,11 @@ export class OwnershipWorkflow {
287
325
  }
288
326
  for (const item of task.outbox) {
289
327
  parseOwnershipMessage(item.payload);
328
+ const action = item.payload.action;
329
+ 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')
330
+ throw new Error('invalid outbox local approval');
331
+ if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action) && !task.request_validated)
332
+ throw new Error('invalid outbox received request');
290
333
  validateOwnershipMessage(item.payload, { authenticated_from_aid: localAid, local_aid: item.to_aid, task });
291
334
  if (item.key !== `${item.to_aid}|${item.payload.action}` || typeof item.message_id !== 'string' || !/^m-[0-9a-f]{32}$/.test(item.message_id))
292
335
  throw new Error('invalid outbox target');
@@ -394,7 +437,7 @@ export class OwnershipWorkflow {
394
437
  if (!saved && !initial)
395
438
  throw failure('operation_not_found', 'ownership operation was not found');
396
439
  if (!saved && initial) {
397
- const existing = (await this.records(aid)).find((task) => task.sub_aid === subAid && this.ongoing(task));
440
+ const existing = (await this.records(aid, subAid)).find((task) => task.sub_aid === subAid && this.ongoing(task));
398
441
  if (existing) {
399
442
  if (!this.sameIntent(existing, initial) || existing.base_relation_version !== initial.base_relation_version || existing.target_relation_version !== initial.target_relation_version)
400
443
  throw failure('conflict', 'another local operation for this sub is pending');
@@ -435,7 +478,7 @@ export class OwnershipWorkflow {
435
478
  sameIntent(task, intent) {
436
479
  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
480
  }
438
- async records(aid) {
481
+ async records(aid, subAid) {
439
482
  let snapshots;
440
483
  try {
441
484
  snapshots = await this.storage().listOwnershipTasks(aid);
@@ -443,32 +486,39 @@ export class OwnershipWorkflow {
443
486
  catch (error) {
444
487
  throw failure('persistence_unavailable', `ownership task list failed: ${String(error)}`);
445
488
  }
446
- return await Promise.all(snapshots.map((snapshot) => this.record(snapshot)));
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)));
447
490
  }
448
491
  async observe(subAid) {
449
492
  this.requireApplicable(subAid);
450
493
  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;
494
+ const file = snapshot.exists ? await this.deps.files.verify(snapshot.content, 'agent', subAid) : null;
454
495
  if (file)
455
496
  requireOwnershipApplicable(file.frontmatter.type);
456
- if (snapshot.lock === 'unlocked' && snapshot.relation_version === 0 && file?.statement)
497
+ if ((file?.statement?.relation_version ?? 0) !== snapshot.relation_version)
457
498
  throw failure('unavailable', 'NS relation metadata does not match agent.md', true);
458
499
  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
500
  throw failure('unavailable', 'NS lock and current signed agent.md differ', true);
460
501
  return { ...snapshot, file, owner_aid: snapshot.lock === 'locked' ? file.statement.owner_aid : null };
461
502
  }
462
503
  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);
504
+ const base = task.base_agent_md;
505
+ 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));
506
+ }
507
+ async captureBase(task, ns) {
508
+ if (!task.base_agent_md)
509
+ task.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: await ownershipSha256(ns.content), etag: ns.etag, statement: ns.file?.statement ?? null };
510
+ if (!task.base_owner_md) {
511
+ const prior = ns.file?.statement ? await this.deps.files.readOwner(ns.file.statement.owner_aid, task.sub_aid) : null;
512
+ task.base_owner_md = prior ? { exists: prior.exists, raw: prior.content, sha256: prior.sha256 } : { exists: false, raw: '', sha256: '' };
513
+ }
464
514
  }
465
515
  isTarget(task, ns) {
466
516
  return ns.relation_version === task.target_relation_version && (task.operation === 'release' ? ns.lock === 'unlocked' : ns.lock === 'locked' && ns.owner_aid === targetOwner(task));
467
517
  }
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); } };
518
+ fileTask(context, expectedStatement) {
519
+ const task = { ...context.task, files: context.task.files, expected_agent_statement: expectedStatement,
520
+ 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); },
521
+ 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
522
  return task;
473
523
  }
474
524
  async start(operation, subAid, ownerAid, newOwnerAid, requestedId, guard) {
@@ -502,7 +552,7 @@ export class OwnershipWorkflow {
502
552
  }
503
553
  }
504
554
  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))) {
555
+ for (const pending of (await this.records(aid, subAid)).filter((task) => task.sub_aid === subAid && this.ongoing(task))) {
506
556
  if (this.sameIntent(pending, intent)) {
507
557
  const result = await this.advance(subAid, pending.operation_id, undefined, events, undefined, guard);
508
558
  if (result.status !== 'stopped' || result.code !== 'conflict' || !/relation version or target was replaced/.test(result.reason ?? ''))
@@ -525,6 +575,8 @@ export class OwnershipWorkflow {
525
575
  throw failure('owner_locked', 'the sub AID has a current owner lock');
526
576
  if (ns.file?.frontmatter.type === 'human')
527
577
  throw failure('invalid', 'human cannot declare an owner');
578
+ if (ns.file?.statement?.owner_aid === ownerAid)
579
+ throw failure('conflict', 'current owner release requires sub cleanup before binding again');
528
580
  }
529
581
  else {
530
582
  if (aid !== ownerAid || ns.lock !== 'locked' || ns.owner_aid !== aid)
@@ -533,10 +585,11 @@ export class OwnershipWorkflow {
533
585
  if (consensus.status !== 'consensus' || consensus.owner_aid !== aid || consensus.relation_version !== ns.relation_version)
534
586
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'the current owner relation could not be verified', consensus.status !== 'invalid');
535
587
  }
536
- ownershipInteger(ns.relation_version + 1, 'target_relation_version');
588
+ const targetVersion = ownershipInteger(ns.relation_version + (operation === 'release' ? 0 : 1), 'target_relation_version');
537
589
  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 };
590
+ 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
591
  const task = this.create(payload, aid, 'accept');
592
+ await this.captureBase(task, ns);
540
593
  if (!task.local_roles.length)
541
594
  throw failure('forbidden', 'the local AID is not an ownership participant');
542
595
  if (aid !== this.deps.currentAid())
@@ -662,13 +715,28 @@ export class OwnershipWorkflow {
662
715
  return true;
663
716
  }
664
717
  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)
718
+ const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid);
719
+ if (!before.exists || before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
669
720
  return false;
670
721
  const file = parseOwnershipFile(before.content, 'agent', { expectedSubAid: task.sub_aid });
671
- return !Object.hasOwn(file.frontmatter, 'owner') && !Object.hasOwn(file.frontmatter, 'ownership');
722
+ if (Object.hasOwn(file.frontmatter, 'owner') || Object.hasOwn(file.frontmatter, 'ownership'))
723
+ return false;
724
+ 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)
725
+ return false;
726
+ let ownerHash = '';
727
+ if (task.local_roles.includes('owner') && task.files.release) {
728
+ const original = task.files.release, object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
729
+ if (object.exists && object.content === original.raw && object.sha256 === original.sha256) {
730
+ const file = await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
731
+ if (file.action === 'release' && file.statement && ['version', 'owner_aid', 'sub_aid', 'relation_version'].every((key) => file.statement[key] === task.base_agent_md.statement[key]))
732
+ ownerHash = object.sha256;
733
+ }
734
+ }
735
+ 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));
736
+ const after = await this.deps.files.readAgent(task.sub_aid);
737
+ 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)
738
+ throw failure('unavailable', 'NS changed during sub cleanup verification', true);
739
+ return confirmed;
672
740
  }
673
741
  async resume(guard) {
674
742
  guard?.();
@@ -681,15 +749,16 @@ export class OwnershipWorkflow {
681
749
  }
682
750
  for (const snapshot of snapshots) {
683
751
  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))
686
- continue;
687
752
  try {
753
+ const task = await this.record(snapshot);
754
+ 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
+ continue;
688
756
  await this.execute(task.sub_aid, task.operation_id, undefined, undefined, guard);
689
757
  }
690
758
  catch (error) {
759
+ guard?.();
691
760
  if (ownershipErrorCode(error) !== 'conflict')
692
- throw error;
761
+ 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
762
  }
694
763
  }
695
764
  }
@@ -703,6 +772,25 @@ export class OwnershipWorkflow {
703
772
  }
704
773
  const entry = task.inbox[0];
705
774
  const ns = await this.observe(task.sub_aid);
775
+ if (!task.base_agent_md && !task.request_validated && task.local_decision === 'pending' && !Object.keys(task.files).length) {
776
+ if (this.isBase(task, ns) || task.operation === 'release' && ns.relation_version === task.base_relation_version && ns.file?.statement?.owner_aid === task.owner_aid)
777
+ await this.captureBase(task, ns);
778
+ else if (task.operation === 'release' && ns.relation_version === 0 && !ns.file?.statement && task.local_roles.includes('sub')) {
779
+ for (const candidate of await this.records(context.aid, task.sub_aid)) {
780
+ const original = candidate.files.agent, cleanup = candidate.release_cleanup?.files.cleanup;
781
+ 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)
782
+ continue;
783
+ const statement = parseOwnershipFile(original.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
784
+ if (!statement || !cleanup.condition?.if_match)
785
+ continue;
786
+ task.base_agent_md = { exists: true, raw: original.raw, sha256: original.sha256, etag: cleanup.condition.if_match, statement };
787
+ task.cleanup_authorization_operation_id = candidate.operation_id;
788
+ break;
789
+ }
790
+ }
791
+ if (task.base_agent_md)
792
+ await this.save(context);
793
+ }
706
794
  try {
707
795
  validateOwnershipMessage(entry.payload, { authenticated_from_aid: entry.from_aid, local_aid: context.aid, task, ns });
708
796
  }
@@ -719,6 +807,11 @@ export class OwnershipWorkflow {
719
807
  }
720
808
  else if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action)) {
721
809
  const decision = action.endsWith('.accept') ? 'accept' : 'reject';
810
+ if (decision === 'reject' && await this.complete(context, ns)) {
811
+ task.inbox.shift();
812
+ await this.save(context);
813
+ continue;
814
+ }
722
815
  if (!task.approvals[entry.from_aid] || task.approvals[entry.from_aid] === 'pending') {
723
816
  task.approvals[entry.from_aid] = decision;
724
817
  if (decision === 'reject') {
@@ -753,7 +846,7 @@ export class OwnershipWorkflow {
753
846
  }
754
847
  else if (action === 'sub.cleaned') {
755
848
  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'))
849
+ 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
850
  task.hints[action] = true;
758
851
  }
759
852
  task.inbox.shift();
@@ -766,7 +859,8 @@ export class OwnershipWorkflow {
766
859
  const action = item.payload.action;
767
860
  const notification = ['relation.confirmed', 'sub.cleaned'].includes(action);
768
861
  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')) {
862
+ 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')) {
770
864
  task.outbox = task.outbox.filter((row) => row.key !== item.key);
771
865
  await this.save(context);
772
866
  continue;
@@ -789,19 +883,43 @@ export class OwnershipWorkflow {
789
883
  }
790
884
  async complete(context, ns) {
791
885
  const task = context.task;
792
- if (!this.isTarget(task, ns))
886
+ const began = Date.now(), cleaned = task.operation === 'release' && ns.exists && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
887
+ if (!this.isTarget(task, ns) && !cleaned)
793
888
  return false;
794
889
  let consensus;
795
890
  if (task.operation === 'release') {
796
891
  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)
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))
798
893
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership release could not be freshly confirmed', consensus.status !== 'invalid');
894
+ if (!cleaned && task.files.release && consensus.owner_md_sha256) {
895
+ const source = task.files.release;
896
+ if (source.sha256 !== consensus.owner_md_sha256)
897
+ throw failure('conflict', 'current release material does not belong to this task');
898
+ }
899
+ if (cleaned) {
900
+ const source = task.files.release;
901
+ if (task.local_roles.includes('owner') && source) {
902
+ const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
903
+ if (object.exists && object.content === source.raw && object.sha256 === source.sha256) {
904
+ await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
905
+ consensus.owner_md_sha256 = object.sha256;
906
+ }
907
+ }
908
+ const authorization = await this.cleanupEvidence(task, consensus, ns.content);
909
+ if (!authorization)
910
+ throw failure('conflict', 'no local cleanup original can confirm this release task');
911
+ const after = await this.observe(task.sub_aid);
912
+ 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)
913
+ throw failure('unavailable', 'NS changed during cleanup confirmation', true);
914
+ if (authorization !== task.operation_id)
915
+ task.cleanup_authorization_operation_id = authorization;
916
+ }
799
917
  const prior = ns.file?.statement;
800
- if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
918
+ 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
919
  throw failure('conflict', 'ownership released document does not match this generation');
802
920
  if (task.local_roles.includes('owner') && !task.files.release)
803
921
  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)) {
922
+ if (prior && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && !await this.authorizeCleanup(context, prior)) {
805
923
  task.status = 'pending';
806
924
  task.step = 'cleanup_action_required';
807
925
  task.cleanup_pending = true;
@@ -825,12 +943,10 @@ export class OwnershipWorkflow {
825
943
  throw failure('conflict', 'Storage owner.md does not match this task original bytes');
826
944
  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
945
  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
946
  }
831
947
  task.status = 'completed';
832
948
  task.step = task.operation === 'release' ? 'released' : 'confirmed';
833
- task.verified_relation_version = consensus.relation_version;
949
+ task.verified_relation_version = task.target_relation_version;
834
950
  task.verified_consensus = { ...consensus, protocol: '2.1' };
835
951
  delete task.code;
836
952
  delete task.reason;
@@ -850,9 +966,42 @@ export class OwnershipWorkflow {
850
966
  await this.save(context);
851
967
  return true;
852
968
  }
853
- async authorizeCleanup(context) {
969
+ async cleanupEvidence(task, evidence, raw) {
970
+ const release = task.files.release;
971
+ if (task.local_roles.includes('owner') && release && release.sha256 === evidence.owner_md_sha256 && release.sha256 === await ownershipSha256(release.raw))
972
+ return task.operation_id;
973
+ if (!task.local_roles.includes('sub'))
974
+ return undefined;
975
+ 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
+ if (await matches(task.files.cleanup))
977
+ 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)
989
+ continue;
990
+ const candidate = await this.record(snapshot);
991
+ 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
+ continue;
993
+ const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
994
+ 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))
995
+ return candidate.operation_id;
996
+ }
997
+ return undefined;
998
+ }
999
+ sameStatement(left, right) {
1000
+ return Boolean(left && right && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((key) => left[key] === right[key]));
1001
+ }
1002
+ async authorizeCleanup(context, statement) {
854
1003
  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;
1004
+ 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
1005
  if (task.cleanup_authorization_operation_id) {
857
1006
  const saved = await this.load(task.cleanup_authorization_operation_id);
858
1007
  if (!saved || !matches(await this.record(saved)))
@@ -866,7 +1015,7 @@ export class OwnershipWorkflow {
866
1015
  catch (error) {
867
1016
  throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
868
1017
  }
869
- const authorized = (await Promise.all(history.map((row) => this.record(row)))).find(matches);
1018
+ const authorized = (await Promise.all(history.filter((row) => row.record.sub_aid === task.sub_aid).map((row) => this.record(row)))).find(matches);
870
1019
  if (!authorized)
871
1020
  return false;
872
1021
  task.cleanup_authorization_operation_id = authorized.operation_id;
@@ -875,13 +1024,23 @@ export class OwnershipWorkflow {
875
1024
  }
876
1025
  async resumeSubCleanup(context, ns) {
877
1026
  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)
1027
+ if (!['bind', 'transfer'].includes(task.operation) || !task.local_roles.includes('sub') || task.local_decision !== 'accept' || ns.lock !== 'unlocked' || task.release_cleanup?.cleaned)
1028
+ return;
1029
+ if (ns.relation_version === 0 && !ns.file?.statement) {
1030
+ const prepared = task.release_cleanup?.files.cleanup;
1031
+ 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)
1032
+ throw failure('conflict', 'historical cleanup original file was not confirmed');
1033
+ task.release_cleanup.cleaned = true;
1034
+ await this.save(context);
1035
+ return;
1036
+ }
1037
+ if (ns.relation_version !== task.target_relation_version)
879
1038
  return;
880
1039
  const consensus = await this.deps.files.checkConsensus(task.sub_aid);
881
1040
  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
1041
  throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'NS changed during historical cleanup observation', consensus.status !== 'invalid');
883
1042
  const statement = ns.file?.statement;
884
- if (statement && (statement.owner_aid !== targetOwner(task) || statement.relation_version !== task.target_relation_version))
1043
+ if (!statement || !task.files.agent || !this.sameStatement(statement, parseOwnershipFile(task.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement))
885
1044
  throw failure('conflict', 'historical cleanup fields belong to another relation');
886
1045
  if (!task.release_cleanup) {
887
1046
  task.release_cleanup = { files: {}, cleaned: false };
@@ -889,7 +1048,7 @@ export class OwnershipWorkflow {
889
1048
  }
890
1049
  if (statement || task.release_cleanup.files.cleanup)
891
1050
  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,
1051
+ base_relation_version: task.target_relation_version, target_relation_version: ns.relation_version, files: task.release_cleanup.files, expected_agent_statement: statement,
893
1052
  persist: () => this.save(context), guard: () => this.save(context) });
894
1053
  task.release_cleanup.cleaned = true;
895
1054
  await this.save(context);
@@ -899,7 +1058,9 @@ export class OwnershipWorkflow {
899
1058
  const task = context.task;
900
1059
  if (!task.cleanup_pending || task.cleanup_done)
901
1060
  return;
902
- if (!this.isTarget(task, ns)) {
1061
+ const releaseOwner = task.operation === 'release' && task.local_roles.includes('owner');
1062
+ const cleaned = task.operation === 'release' && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
1063
+ if (!this.isTarget(task, ns) && !cleaned && !releaseOwner) {
903
1064
  task.cleanup_pending = false;
904
1065
  task.step = 'cleanup_superseded';
905
1066
  await this.save(context);
@@ -907,7 +1068,9 @@ export class OwnershipWorkflow {
907
1068
  }
908
1069
  if (task.operation === 'release' && task.local_roles.includes('sub')) {
909
1070
  const prior = ns.file?.statement;
910
- if (prior && !await this.authorizeCleanup(context)) {
1071
+ if (prior && !this.sameStatement(prior, task.base_agent_md?.statement ?? undefined))
1072
+ throw failure('conflict', 'ownership cleanup original relation was replaced');
1073
+ if (prior && !await this.authorizeCleanup(context, prior)) {
911
1074
  task.step = 'cleanup_action_required';
912
1075
  await this.save(context);
913
1076
  this.event(context, 'ownership.action_required');
@@ -915,18 +1078,18 @@ export class OwnershipWorkflow {
915
1078
  }
916
1079
  if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
917
1080
  throw failure('conflict', 'ownership cleanup current fields differ');
918
- if (prior)
919
- await this.deps.files.revokeOwneraid(this.fileTask(context));
1081
+ if (prior || task.files.cleanup)
1082
+ await this.deps.files.revokeOwneraid(this.fileTask(context, prior));
920
1083
  this.enqueue(task, task.owner_aid, 'sub.cleaned');
921
1084
  }
922
1085
  if (task.local_roles.includes('owner') && (task.operation === 'release' || task.operation === 'transfer')) {
1086
+ if (releaseOwner && this.isTarget(task, ns) && ns.file?.statement)
1087
+ return;
923
1088
  const prepared = task.files[task.operation === 'release' ? 'release' : 'transfer'];
924
- if (prepared && !prepared.published_revision) {
1089
+ if (prepared) {
925
1090
  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
- }
1091
+ if (object.exists && (object.sha256 !== prepared.sha256 || object.content !== prepared.raw))
1092
+ throw failure('conflict', 'ownership cleanup original owner file has been replaced');
930
1093
  }
931
1094
  await this.deps.files.cleanupOwnerFile(this.fileTask(context));
932
1095
  }
@@ -962,7 +1125,7 @@ export class OwnershipWorkflow {
962
1125
  delete task.reason;
963
1126
  task.retryable = true;
964
1127
  if (await this.complete(context, ns)) {
965
- if (task.status === 'completed' && task.operation === 'release' && task.local_roles.includes('sub')) {
1128
+ if (task.status === 'completed' && task.operation === 'release') {
966
1129
  await this.cleanup(context, ns);
967
1130
  ns = await this.observe(task.sub_aid);
968
1131
  }