@agentunion/fastaun 0.5.21 → 0.5.23

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 (75) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/_packed_docs/CHANGELOG.md +58 -1
  3. package/_packed_docs/INDEX.md +469 -453
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +169 -164
  5. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +307 -67
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +41 -15
  7. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +5 -5
  8. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
  9. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +669 -666
  10. package/_packed_docs/sdk/0.5.22-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +212 -0
  11. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +199 -0
  12. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +16 -0
  13. package/_packed_docs/sdk/09-group-rpc-manual.md +10 -0
  14. package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
  15. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +3 -1
  16. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +2 -0
  17. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +138 -134
  18. package/_packed_docs/sdk/INDEX.md +255 -239
  19. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +939 -930
  20. 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 +312 -363
  21. 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 +322 -553
  22. 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 +4 -4
  23. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +484 -480
  24. 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 +190 -97
  25. 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 +158 -127
  26. package/dist/agent-md.d.ts +16 -2
  27. package/dist/agent-md.js +59 -23
  28. package/dist/agent-md.js.map +1 -1
  29. package/dist/aid-store.d.ts +1 -0
  30. package/dist/aid-store.js +43 -8
  31. package/dist/aid-store.js.map +1 -1
  32. package/dist/auth.d.ts +7 -0
  33. package/dist/auth.js +222 -70
  34. package/dist/auth.js.map +1 -1
  35. package/dist/client/delivery.d.ts +28 -0
  36. package/dist/client/delivery.js +399 -44
  37. package/dist/client/delivery.js.map +1 -1
  38. package/dist/client/group-state.js +3 -0
  39. package/dist/client/group-state.js.map +1 -1
  40. package/dist/client/rpc-pipeline.js +28 -2
  41. package/dist/client/rpc-pipeline.js.map +1 -1
  42. package/dist/client/v2-e2ee.js +109 -39
  43. package/dist/client/v2-e2ee.js.map +1 -1
  44. package/dist/client.d.ts +3 -0
  45. package/dist/client.js +114 -26
  46. package/dist/client.js.map +1 -1
  47. package/dist/group-fs.d.ts +1 -0
  48. package/dist/group-fs.js +37 -19
  49. package/dist/group-fs.js.map +1 -1
  50. package/dist/org-protocol.d.ts +11 -0
  51. package/dist/org-protocol.js +154 -19
  52. package/dist/org-protocol.js.map +1 -1
  53. package/dist/ownership-v21.d.ts +21 -3
  54. package/dist/ownership-v21.js +135 -37
  55. package/dist/ownership-v21.js.map +1 -1
  56. package/dist/ownership-workflow.d.ts +7 -2
  57. package/dist/ownership-workflow.js +173 -17
  58. package/dist/ownership-workflow.js.map +1 -1
  59. package/dist/storage/lowlevel.d.ts +6 -0
  60. package/dist/storage/lowlevel.js +24 -7
  61. package/dist/storage/lowlevel.js.map +1 -1
  62. package/dist/storage/read-cache.d.ts +70 -0
  63. package/dist/storage/read-cache.js +625 -0
  64. package/dist/storage/read-cache.js.map +1 -0
  65. package/dist/storage/vfs.d.ts +2 -0
  66. package/dist/storage/vfs.js +50 -15
  67. package/dist/storage/vfs.js.map +1 -1
  68. package/dist/tools/cross-sdk-agent.d.ts +21 -1
  69. package/dist/tools/cross-sdk-agent.js +301 -19
  70. package/dist/tools/cross-sdk-agent.js.map +1 -1
  71. package/dist/transport.js +6 -2
  72. package/dist/transport.js.map +1 -1
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/package.json +1 -1
@@ -114,7 +114,7 @@ export function parseOwnershipMessage(content, context) {
114
114
  const ns = context.ns;
115
115
  if (ns.protocol !== '2.1')
116
116
  throw ownershipFailure('unsupported_protocol', 'NS ownership protocol is unavailable');
117
- 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);
117
+ 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) || message.operation === 'bind' && ns.relation_version === message.target_relation_version && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending');
118
118
  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);
119
119
  if (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target : !base && !target)
120
120
  throw ownershipFailure('conflict', 'ownership current NS relation does not match this task');
@@ -560,17 +560,38 @@ export class OwnershipWorkflow {
560
560
  if (!base || typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || base.sha256 !== ownershipSha256(base.raw) || typeof base.etag !== 'string' || (base.exists ? !base.etag : base.raw !== '' || base.etag !== '' || record.operation !== 'bind' || record.base_relation_version !== 0))
561
561
  throw new Error('ownership original agent.md evidence is corrupt');
562
562
  const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
563
- if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version)
563
+ if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version && !(base.pending && statement?.relation_version === record.target_relation_version && statement.owner_aid === record.owner_aid) || base.pending && record.operation !== 'bind')
564
564
  throw new Error('ownership original statement is corrupt');
565
565
  const owner = record.base_owner_md;
566
566
  if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
567
567
  throw new Error('ownership original owner.md evidence is corrupt');
568
568
  if (owner.exists) {
569
569
  const declaration = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: statement?.owner_aid ?? record.owner_aid });
570
- if (declaration.statement?.relation_version !== record.base_relation_version)
570
+ if (!base.pending && declaration.statement?.relation_version !== record.base_relation_version)
571
571
  throw new Error('ownership original owner statement is corrupt');
572
572
  }
573
573
  }
574
+ if (record.pending_agent_md) {
575
+ const pending = record.pending_agent_md;
576
+ const statement = parseOwnershipFile(pending.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
577
+ if (record.operation !== 'bind' || pending.sha256 !== ownershipSha256(pending.raw) || !pending.etag || statement?.owner_aid !== record.owner_aid || statement.relation_version !== record.target_relation_version)
578
+ throw new Error('ownership pending original is corrupt');
579
+ }
580
+ if (record.owner_proposal) {
581
+ const proposal = record.owner_proposal, file = parseOwnershipFile(proposal.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
582
+ if (!this.ownerFirst(record) || proposal.sha256 !== ownershipSha256(proposal.raw) || file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version)
583
+ throw new Error('ownership owner proposal is corrupt');
584
+ }
585
+ if (record.owner_first !== undefined && (record.owner_first !== true || record.operation !== 'bind' || record.initiator_aid !== record.owner_aid || record.sub_aid === record.owner_aid) || record.owner_first && record.request_verified && this.local() === record.sub_aid && !record.owner_proposal)
586
+ throw new Error('ownership owner-first context is corrupt');
587
+ if (record.rejection_cleanup) {
588
+ const cleanup = record.rejection_cleanup;
589
+ if (record.operation !== 'bind' || record.status !== 'rejected' || !record.local_roles.includes('sub') || !record.files.agent || typeof cleanup.cleaned !== 'boolean' || !cleanup.files || Object.keys(cleanup.files).some((key) => key !== 'agent'))
590
+ throw new Error('ownership rejection cleanup is corrupt');
591
+ const file = cleanup.files.agent;
592
+ if (file && (file.hash !== ownershipSha256(file.raw) || file.condition.if_match !== record.files.agent.published_etag || Object.keys(file.condition).length !== 1 || parseOwnershipFile(file.raw, 'agent', { expectedSubAid: record.sub_aid }).statement))
593
+ throw new Error('ownership rejection cleanup original is corrupt');
594
+ }
574
595
  for (const item of record.inbox)
575
596
  parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record });
576
597
  for (const key of record.received) {
@@ -778,7 +799,8 @@ export class OwnershipWorkflow {
778
799
  const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
779
800
  const released = record.operation === 'release' && ns.lock === 'unlocked' && !ns.owner_aid && ns.relation_version === record.base_relation_version;
780
801
  const committed = record.operation !== 'release' && ns.lock === 'locked' && ns.owner_aid === targetOwner(record) && ns.relation_version === record.target_relation_version;
781
- if (!base && !released && !committed)
802
+ const pending = record.operation === 'bind' && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.target_relation_version && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement?.owner_aid === record.owner_aid;
803
+ if (!base && !released && !committed && !pending)
782
804
  throw ownershipFailure('conflict', 'legacy relationship no longer matches this task');
783
805
  if (!committed)
784
806
  await this.captureBase(record, ns);
@@ -879,6 +901,8 @@ export class OwnershipWorkflow {
879
901
  };
880
902
  if (context.operation === 'transfer')
881
903
  record.new_owner_aid = context.new_owner_aid;
904
+ if (context.operation === 'bind' && initiator === context.owner_aid && context.owner_aid !== context.sub_aid)
905
+ record.owner_first = true;
882
906
  record.approvals[this.local()] = decision;
883
907
  return record;
884
908
  }
@@ -927,7 +951,7 @@ export class OwnershipWorkflow {
927
951
  await this.recordError(active, error);
928
952
  return ownershipTaskResult(record);
929
953
  }
930
- let base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
954
+ let base = this.isBase(record, ns);
931
955
  if (base && record.request_verified) {
932
956
  try {
933
957
  await this.assertBaseMaterial(active, ns);
@@ -958,11 +982,13 @@ export class OwnershipWorkflow {
958
982
  ns = await this.readNs(subAid);
959
983
  if (operation === 'bind' && ns.lock !== 'unlocked')
960
984
  throw ownershipFailure('owner_locked', 'the current owner relation is locked');
985
+ if (operation === 'bind' && subAid !== this.local() && (ns.pending || ns.ownership_status === 'pending'))
986
+ throw ownershipFailure('conflict', 'pending ownership must resume its original operation');
961
987
  if (!ns.exists && operation !== 'bind')
962
988
  throw ownershipFailure('conflict', 'original agent.md is missing');
963
989
  if (operation === 'bind' && (ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type : subAid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
964
990
  throw ownershipFailure('invalid', 'human cannot declare an owner');
965
- if (operation === 'bind' && ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
991
+ if (operation === 'bind' && ns.exists && !(ns.pending || ns.ownership_status === 'pending') && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
966
992
  throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
967
993
  if (operation !== 'bind') {
968
994
  const consensus = await this.deps.files.checkConsensus(subAid);
@@ -999,6 +1025,8 @@ export class OwnershipWorkflow {
999
1025
  if (record.local_decision !== 'pending') {
1000
1026
  if (record.local_decision !== decision)
1001
1027
  throw ownershipFailure('conflict', 'the persisted local approval cannot be changed');
1028
+ if (decision === 'accept' && record.status === 'stopped' && record.operation === 'transfer' && this.local() === record.sub_aid && record.step === 'agent' && record.code === 'conflict' && record.reason === 'conflict: target owner declaration does not match this task' && !await this.recoverTargetDeclaration(handle))
1029
+ return ownershipTaskResult(record);
1002
1030
  await this.drive(handle);
1003
1031
  return ownershipTaskResult(record);
1004
1032
  }
@@ -1007,12 +1035,13 @@ export class OwnershipWorkflow {
1007
1035
  if (record.status !== 'pending')
1008
1036
  throw ownershipFailure('conflict', 'ownership approval stage has ended');
1009
1037
  const ns = await this.readNs(record.sub_aid);
1010
- if (ns.relation_version !== record.base_relation_version || (record.operation === 'bind' ? ns.lock !== 'unlocked' : ns.lock !== 'locked' || ns.owner_aid !== record.owner_aid))
1038
+ if (!this.isBase(record, ns))
1011
1039
  throw ownershipFailure('conflict', 'the original relation changed before approval');
1012
1040
  await this.assertBaseMaterial(handle, ns);
1013
1041
  record.local_decision = decision;
1014
1042
  record.approvals[this.local()] = decision;
1015
- this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
1043
+ if (decision !== 'accept' || !this.ownerFirst(record))
1044
+ this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
1016
1045
  if (decision === 'reject') {
1017
1046
  record.status = 'rejected';
1018
1047
  record.step = 'approval';
@@ -1026,6 +1055,42 @@ export class OwnershipWorkflow {
1026
1055
  return ownershipTaskResult(record);
1027
1056
  });
1028
1057
  }
1058
+ async recoverTargetDeclaration(handle) {
1059
+ const record = handle.record, local = this.local(), base = record.base_agent_md;
1060
+ if (!base || record.new_owner_aid === local || record.files.agent || record.files.owner || record.owner_proposal || record.legacy_task || record.legacy_recovery || record.approvals[local] !== 'accept' || record.authorization_sha256 === undefined || record.expires_at === undefined)
1061
+ return false;
1062
+ const began = Date.now();
1063
+ const sameBase = (ns) => this.isBase(record, ns) && ns.exists === base.exists && ns.content === base.raw && ownershipSha256(ns.content) === base.sha256 && ns.etag === base.etag;
1064
+ try {
1065
+ if (!sameBase(await this.readNs(record.sub_aid)))
1066
+ return false;
1067
+ await this.deps.files.requireTransfer(this.fileTask(handle));
1068
+ const target = await this.deps.files.readOwner(targetOwner(record), record.sub_aid);
1069
+ if (!target.exists)
1070
+ return false;
1071
+ const declaration = await this.deps.files.verifyFile(target.content, 'owner', record.sub_aid, targetOwner(record));
1072
+ if (declaration.action !== 'declare' || declaration.statement.relation_version !== record.target_relation_version)
1073
+ return false;
1074
+ if (!sameBase(await this.readNs(record.sub_aid)))
1075
+ return false;
1076
+ await this.deps.files.requireTransfer(this.fileTask(handle));
1077
+ const after = await this.deps.files.readOwner(targetOwner(record), record.sub_aid);
1078
+ if (!after.exists || after.content !== target.content || after.sha256 !== target.sha256 || after.version !== target.version || after.revision !== target.revision || Date.now() - began > 30_000 || this.local() !== local)
1079
+ return false;
1080
+ this.assertActive(handle);
1081
+ }
1082
+ catch (error) {
1083
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
1084
+ throw error;
1085
+ return false;
1086
+ }
1087
+ record.status = 'pending';
1088
+ record.retryable = true;
1089
+ delete record.code;
1090
+ delete record.reason;
1091
+ await this.save(handle);
1092
+ return true;
1093
+ }
1029
1094
  enqueue(record, to, action, reason) {
1030
1095
  if (to === this.local())
1031
1096
  return;
@@ -1038,11 +1103,13 @@ export class OwnershipWorkflow {
1038
1103
  payload.new_owner_aid = record.new_owner_aid;
1039
1104
  if (reason !== undefined)
1040
1105
  payload.reason = reason;
1041
- record.outbox.push({ to, payload });
1106
+ record.outbox.push({ to, payload, message_id: `m-${randomUUID().replace(/-/g, '')}` });
1042
1107
  record.notices.push(key);
1043
1108
  }
1044
1109
  validateOutgoing(record, item) {
1045
1110
  try {
1111
+ if (item.message_id !== undefined && (typeof item.message_id !== 'string' || !/^m-[0-9a-f]{32}$/.test(item.message_id)))
1112
+ throw new Error('ownership outgoing message_id is corrupt');
1046
1113
  const message = parseOwnershipMessage(item.payload, { authenticated_from_aid: this.local(), local_aid: item.to, task: { ...record, local_decision: 'accept' } });
1047
1114
  const rejected = ['bind.reject', 'transfer.reject'].includes(message.action);
1048
1115
  if (message.action !== 'operation.stopped' && record.local_decision !== (rejected ? 'reject' : 'accept') || record.approvals[this.local()] !== record.local_decision)
@@ -1067,10 +1134,10 @@ export class OwnershipWorkflow {
1067
1134
  continue;
1068
1135
  }
1069
1136
  const ns = await this.readNs(record.sub_aid);
1070
- const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
1137
+ const base = this.isBase(record, ns);
1071
1138
  const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.base_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
1072
1139
  const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
1073
- if (requiresTarget ? !target : !base) {
1140
+ if (requiresTarget ? !target : !base && !(record.operation === 'bind' && target && ['owner.ready', 'bind.accept'].includes(message.payload.action))) {
1074
1141
  record.outbox.shift();
1075
1142
  await this.save(handle);
1076
1143
  continue;
@@ -1084,8 +1151,12 @@ export class OwnershipWorkflow {
1084
1151
  if (expiresAt <= Math.trunc(Date.now() / 1000))
1085
1152
  throw ownershipFailure('conflict', 'transfer authorization expired');
1086
1153
  }
1154
+ if (!message.message_id) {
1155
+ message.message_id = `m-${randomUUID().replace(/-/g, '')}`;
1156
+ await this.save(handle);
1157
+ }
1087
1158
  this.assertActive(handle);
1088
- await this.deps.send(message.to, message.payload);
1159
+ await this.deps.send(message.to, message.payload, message.message_id);
1089
1160
  handle.record.outbox.shift();
1090
1161
  await this.save(handle);
1091
1162
  }
@@ -1098,11 +1169,35 @@ export class OwnershipWorkflow {
1098
1169
  const currentOwner = ns.lock === 'locked' ? file?.statement?.owner_aid : null;
1099
1170
  if (ns.lock === 'locked' && (!file?.statement || file.statement.relation_version !== ns.relation_version) || ns.owner_aid !== undefined && ns.owner_aid !== currentOwner)
1100
1171
  throw ownershipFailure('unavailable', 'NS owner metadata and signed agent.md do not agree', true);
1101
- return { ...ns, owner_aid: currentOwner };
1172
+ if (!file?.statement)
1173
+ return { ...ns, owner_aid: currentOwner };
1174
+ const consensus = await this.deps.files.checkConsensus(subAid);
1175
+ if (consensus.status === 'invalid' || consensus.status === 'unavailable' || consensus.status === 'not_applicable')
1176
+ throw ownershipFailure(consensus.code ?? consensus.status, consensus.reason ?? 'ownership materials cannot be verified', consensus.retryable);
1177
+ if (consensus.relation_version !== ns.relation_version || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== ownershipSha256(ns.content))
1178
+ throw ownershipFailure('unavailable', 'agent.md changed during ownership verification', true);
1179
+ return { ...ns, lock: consensus.lock, owner_aid: consensus.owner_aid };
1180
+ }
1181
+ isPending(record, ns) {
1182
+ if (record.operation !== 'bind' || ns.lock !== 'unlocked' || !(ns.pending || ns.ownership_status === 'pending') || ns.relation_version !== record.target_relation_version)
1183
+ return false;
1184
+ const saved = record.files.agent;
1185
+ const candidate = saved ? { raw: saved.raw, sha256: saved.hash, etag: saved.published_etag } : record.pending_agent_md ?? (record.base_agent_md?.pending ? record.base_agent_md : undefined);
1186
+ return Boolean(candidate && ns.content === candidate.raw && ownershipSha256(ns.content) === candidate.sha256 && (!candidate.etag || ns.etag === candidate.etag));
1187
+ }
1188
+ isPendingBase(record, ns) {
1189
+ const base = record.base_agent_md;
1190
+ return record.operation === 'bind' && Boolean(base?.pending) && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.base_relation_version && ns.content === base.raw && ns.etag === base.etag && ownershipSha256(ns.content) === base.sha256;
1191
+ }
1192
+ isBase(record, ns) {
1193
+ return this.isPending(record, ns) || this.isPendingBase(record, ns) || ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
1102
1194
  }
1103
1195
  async captureBase(record, ns) {
1104
1196
  const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
1105
- record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement };
1197
+ const pending = record.operation === 'bind' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.target_relation_version && statement?.owner_aid === record.owner_aid;
1198
+ record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement, ...(record.operation === 'bind' && (ns.pending || ns.ownership_status === 'pending') ? { pending: true } : {}) };
1199
+ if (pending)
1200
+ record.pending_agent_md = { raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag };
1106
1201
  if (!statement) {
1107
1202
  record.base_owner_md = { exists: false, raw: '', sha256: '' };
1108
1203
  return;
@@ -1117,6 +1212,12 @@ export class OwnershipWorkflow {
1117
1212
  async assertBaseMaterial(handle, ns) {
1118
1213
  const record = handle.record;
1119
1214
  this.assertAgentBaseMaterial(record, ns);
1215
+ if (record.owner_proposal) {
1216
+ const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
1217
+ if (!current.exists || current.content !== record.owner_proposal.raw || current.sha256 !== record.owner_proposal.sha256)
1218
+ throw ownershipFailure('conflict', 'owner binding proposal changed');
1219
+ await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
1220
+ }
1120
1221
  const original = record.base_owner_md;
1121
1222
  if (!original)
1122
1223
  throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is missing');
@@ -1148,9 +1249,13 @@ export class OwnershipWorkflow {
1148
1249
  throw ownershipFailure('conflict', 'the original owner statement was replaced');
1149
1250
  }
1150
1251
  assertAgentBaseMaterial(record, ns) {
1252
+ if (this.isPending(record, ns))
1253
+ return;
1151
1254
  const base = record.base_agent_md;
1152
1255
  if (!base || typeof base.raw !== 'string')
1153
1256
  throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
1257
+ if (base.pending && (ns.content !== base.raw || ns.etag !== base.etag))
1258
+ throw ownershipFailure('conflict', 'the pending ownership original was replaced');
1154
1259
  const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
1155
1260
  const same = base.statement ? Boolean(statement && Object.keys(base.statement).every((key) => base.statement[key] === statement[key])) : statement === null && ns.content === base.raw && ownershipSha256(ns.content) === base.sha256;
1156
1261
  if (ns.exists !== base.exists || !same)
@@ -1161,6 +1266,10 @@ export class OwnershipWorkflow {
1161
1266
  while (record.inbox.length) {
1162
1267
  const item = record.inbox[0];
1163
1268
  const ns = await this.readNs(record.sub_aid);
1269
+ if (record.operation === 'bind' && record.request_verified && !record.pending_agent_md && item.payload.action === 'bind.accept' && item.from === record.sub_aid && (ns.pending || ns.ownership_status === 'pending') && ns.lock === 'unlocked' && ns.relation_version === record.target_relation_version && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement?.owner_aid === record.owner_aid) {
1270
+ record.pending_agent_md = { raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag };
1271
+ await this.save(handle);
1272
+ }
1164
1273
  if (record.request_verified && ns.relation_version === record.base_relation_version && !['relation.confirmed', 'sub.cleaned'].includes(item.payload.action))
1165
1274
  await this.assertBaseMaterial(handle, ns);
1166
1275
  try {
@@ -1191,6 +1300,17 @@ export class OwnershipWorkflow {
1191
1300
  record.legacy_recovery.owner_confirmed = true;
1192
1301
  }
1193
1302
  if (['bind.request', 'transfer.request'].includes(action)) {
1303
+ if (this.ownerFirst(record)) {
1304
+ if (ns.lock !== 'unlocked' || ns.pending || ns.ownership_status === 'pending')
1305
+ throw ownershipFailure('conflict', 'owner binding requires the original unlocked relation');
1306
+ const proposal = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
1307
+ if (!proposal.exists)
1308
+ throw ownershipFailure('unavailable', 'owner binding proposal is missing', true);
1309
+ const file = await this.deps.files.verifyFile(proposal.content, 'owner', record.sub_aid, record.owner_aid);
1310
+ if (file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version || record.owner_proposal && (record.owner_proposal.raw !== proposal.content || record.owner_proposal.sha256 !== proposal.sha256))
1311
+ throw ownershipFailure('conflict', 'owner binding proposal does not match this operation');
1312
+ record.owner_proposal = { raw: proposal.content, sha256: proposal.sha256 };
1313
+ }
1194
1314
  if (!record.request_verified)
1195
1315
  await this.captureBase(record, ns);
1196
1316
  record.request_verified = true;
@@ -1309,8 +1429,9 @@ export class OwnershipWorkflow {
1309
1429
  fileTask(handle) {
1310
1430
  const record = handle.record;
1311
1431
  const task = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid,
1432
+ stage: record.operation === 'bind' && record.initiator_aid === record.sub_aid && !record.legacy_recovery && !record.legacy_task,
1312
1433
  new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
1313
- files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at, base_agent_md: record.base_agent_md, base_owner_md: record.base_owner_md,
1434
+ files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at, base_agent_md: record.base_agent_md, base_owner_md: record.base_owner_md, pending_agent_md: record.pending_agent_md, owner_proposal: record.owner_proposal,
1314
1435
  assertActive: () => this.assertActive(handle),
1315
1436
  persist: async () => {
1316
1437
  record.authorization_sha256 = task.authorization_sha256;
@@ -1326,6 +1447,9 @@ export class OwnershipWorkflow {
1326
1447
  if (!current || this.isReset(current.record) || current.revision !== handle.revision)
1327
1448
  throw ownershipFailure('conflict', 'ownership task changed before external action');
1328
1449
  }
1450
+ ownerFirst(record) {
1451
+ return record.operation === 'bind' && record.initiator_aid === record.owner_aid && record.owner_aid !== record.sub_aid && Boolean(record.owner_first || record.owner_proposal) && !record.legacy_recovery && !record.legacy_task;
1452
+ }
1329
1453
  async drive(handle) {
1330
1454
  if (this.isReset(handle.record))
1331
1455
  return;
@@ -1339,7 +1463,34 @@ export class OwnershipWorkflow {
1339
1463
  }
1340
1464
  try {
1341
1465
  await this.processInbox(handle);
1466
+ if (this.ownerFirst(record) && record.status === 'pending' && record.request_verified && this.local() === record.owner_aid) {
1467
+ const ns = await this.readNs(record.sub_aid);
1468
+ if (this.isBase(record, ns)) {
1469
+ await this.assertBaseMaterial(handle, ns);
1470
+ record.step = 'owner';
1471
+ await this.save(handle);
1472
+ await this.deps.files.declareSubaid(this.fileTask(handle));
1473
+ }
1474
+ }
1475
+ if (record.operation === 'bind' && (record.initiator_aid === record.sub_aid || this.ownerFirst(record)) && record.status === 'pending' && record.request_verified && record.local_decision === 'accept' && this.local() === record.sub_aid && !record.legacy_recovery && !record.legacy_task) {
1476
+ record.step = 'agent';
1477
+ await this.save(handle);
1478
+ await this.deps.files.declareOwneraid(this.fileTask(handle));
1479
+ }
1342
1480
  await this.flush(handle);
1481
+ if (this.ownerFirst(record) && record.status === 'rejected' && this.local() === record.owner_aid && record.files.owner)
1482
+ await this.deps.files.cancelOwnerProposal(this.fileTask(handle));
1483
+ if (record.operation === 'bind' && record.status === 'rejected' && this.local() === record.sub_aid && record.files.agent && !record.rejection_cleanup?.cleaned) {
1484
+ if (!record.rejection_cleanup) {
1485
+ record.rejection_cleanup = { files: {}, cleaned: false };
1486
+ await this.save(handle);
1487
+ }
1488
+ const task = this.fileTask(handle);
1489
+ task.files = record.rejection_cleanup.files;
1490
+ await this.deps.files.cancelPendingBind(task, record.files.agent);
1491
+ record.rejection_cleanup.cleaned = true;
1492
+ await this.save(handle);
1493
+ }
1343
1494
  if (record.status === 'rejected' || record.status === 'stopped' || record.status === 'error')
1344
1495
  return;
1345
1496
  if (!record.request_verified)
@@ -1376,7 +1527,7 @@ export class OwnershipWorkflow {
1376
1527
  return;
1377
1528
  }
1378
1529
  const ns = await this.readNs(record.sub_aid);
1379
- if (ns.relation_version !== record.base_relation_version || record.operation === 'bind' && ns.lock !== 'unlocked' || record.operation === 'transfer' && (ns.lock !== 'locked' || ns.owner_aid !== record.owner_aid)) {
1530
+ if (!this.isBase(record, ns)) {
1380
1531
  throw ownershipFailure('conflict', 'the original relation was replaced');
1381
1532
  }
1382
1533
  await this.assertBaseMaterial(handle, ns);
@@ -1412,7 +1563,8 @@ export class OwnershipWorkflow {
1412
1563
  record.step = 'owner';
1413
1564
  await this.save(handle);
1414
1565
  await this.deps.files.declareSubaid(this.fileTask(handle));
1415
- this.enqueue(record, record.sub_aid, 'owner.ready');
1566
+ if (!this.ownerFirst(record))
1567
+ this.enqueue(record, record.sub_aid, 'owner.ready');
1416
1568
  await this.save(handle);
1417
1569
  await this.flush(handle);
1418
1570
  }
@@ -1512,6 +1664,10 @@ export class OwnershipWorkflow {
1512
1664
  }
1513
1665
  if (record.operation !== 'release' && (record.local_decision !== 'accept' || this.local() === record.sub_aid && (!record.files.agent || record.files.agent.hash !== result.agent_md_sha256) || this.local() === targetOwner(record) && (!record.files.owner || record.files.owner.hash !== result.owner_md_sha256)))
1514
1666
  return false;
1667
+ if (record.owner_proposal && record.owner_proposal.sha256 !== result.owner_md_sha256)
1668
+ throw ownershipFailure('conflict', 'owner binding proposal changed before acceptance');
1669
+ if (record.operation === 'bind' && record.pending_agent_md && (record.pending_agent_md.sha256 !== result.agent_md_sha256 || record.pending_agent_md.etag !== result.agent_md_etag))
1670
+ throw ownershipFailure('conflict', 'pending ownership original changed before acceptance');
1515
1671
  if (record.status !== 'completed' && record.operation !== 'release' && this.local() === record.sub_aid && record.files.agent?.published_etag !== undefined && record.files.agent.published_etag !== result.agent_md_etag)
1516
1672
  throw ownershipFailure('conflict', 'published agent.md ETag changed');
1517
1673
  if (record.status !== 'completed') {