@agentunion/fastaun 0.5.18 → 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 (122) hide show
  1. package/CHANGELOG.md +172 -79
  2. package/_packed_docs/CHANGELOG.md +172 -79
  3. package/_packed_docs/INDEX.md +244 -116
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +98 -69
  5. package/_packed_docs/agent.md/SCHEMA.md +245 -205
  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/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 +42 -15
  8. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +4 -4
  9. package/_packed_docs/protocol/01-/350/272/253/344/273/275/344/270/216/345/207/255/350/257/201/345/215/217/350/256/256-auth.md +45 -134
  10. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1072 -1072
  11. package/_packed_docs/protocol/07-/351/224/231/350/257/257/347/240/201/344/270/216/347/212/266/346/200/201/346/234/272.md +5 -20
  12. package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +687 -668
  13. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +19 -28
  14. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +443 -0
  15. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
  16. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +658 -0
  17. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
  18. package/_packed_docs/protocol/agent.md/SCHEMA.md +4 -0
  19. package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
  20. package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +68 -61
  21. package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
  22. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +279 -285
  23. package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
  24. package/_packed_docs/sdk/09-group-rpc-manual.md +271 -235
  25. package/_packed_docs/sdk/09-message-rpc-manual.md +47 -6
  26. package/_packed_docs/sdk/09-storage-rpc-manual.md +70 -68
  27. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +63 -34
  28. package/_packed_docs/sdk/INDEX.md +105 -34
  29. package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
  30. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
  31. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +922 -0
  32. package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
  33. 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
  34. 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
  35. 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
  36. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +476 -0
  37. package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
  38. package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. package/dist/agent-md-schema.d.ts +15 -1
  48. package/dist/agent-md-schema.js +87 -24
  49. package/dist/agent-md-schema.js.map +1 -1
  50. package/dist/agent-md.d.ts +54 -2
  51. package/dist/agent-md.js +637 -53
  52. package/dist/agent-md.js.map +1 -1
  53. package/dist/aid-store.d.ts +38 -59
  54. package/dist/aid-store.js +300 -265
  55. package/dist/aid-store.js.map +1 -1
  56. package/dist/aid.js +30 -11
  57. package/dist/aid.js.map +1 -1
  58. package/dist/auth.d.ts +6 -1
  59. package/dist/auth.js +500 -15
  60. package/dist/auth.js.map +1 -1
  61. package/dist/cert-utils.d.ts +44 -2
  62. package/dist/cert-utils.js +149 -49
  63. package/dist/cert-utils.js.map +1 -1
  64. package/dist/client/delivery.d.ts +11 -1
  65. package/dist/client/delivery.js +321 -62
  66. package/dist/client/delivery.js.map +1 -1
  67. package/dist/client/rpc-pipeline.d.ts +1 -0
  68. package/dist/client/rpc-pipeline.js +142 -82
  69. package/dist/client/rpc-pipeline.js.map +1 -1
  70. package/dist/client/v2-e2ee.d.ts +1 -0
  71. package/dist/client/v2-e2ee.js +166 -96
  72. package/dist/client/v2-e2ee.js.map +1 -1
  73. package/dist/client.d.ts +84 -1
  74. package/dist/client.js +780 -30
  75. package/dist/client.js.map +1 -1
  76. package/dist/errors.js +4 -3
  77. package/dist/errors.js.map +1 -1
  78. package/dist/events.d.ts +9 -3
  79. package/dist/events.js +69 -11
  80. package/dist/events.js.map +1 -1
  81. package/dist/facades.d.ts +83 -3
  82. package/dist/facades.js +100 -6
  83. package/dist/facades.js.map +1 -1
  84. package/dist/index.d.ts +9 -4
  85. package/dist/index.js +3 -2
  86. package/dist/index.js.map +1 -1
  87. package/dist/keystore/aid-db.d.ts +5 -0
  88. package/dist/keystore/aid-db.js +57 -0
  89. package/dist/keystore/aid-db.js.map +1 -1
  90. package/dist/keystore/index.d.ts +8 -0
  91. package/dist/keystore/local-identity-store.js +10 -3
  92. package/dist/keystore/local-identity-store.js.map +1 -1
  93. package/dist/keystore/local-token-store.d.ts +6 -1
  94. package/dist/keystore/local-token-store.js +24 -1
  95. package/dist/keystore/local-token-store.js.map +1 -1
  96. package/dist/org-protocol.d.ts +98 -0
  97. package/dist/org-protocol.js +1753 -0
  98. package/dist/org-protocol.js.map +1 -0
  99. package/dist/ownership-proof.d.ts +82 -5
  100. package/dist/ownership-proof.js +327 -31
  101. package/dist/ownership-proof.js.map +1 -1
  102. package/dist/ownership-v21.d.ts +196 -0
  103. package/dist/ownership-v21.js +712 -0
  104. package/dist/ownership-v21.js.map +1 -0
  105. package/dist/ownership-workflow.d.ts +87 -0
  106. package/dist/ownership-workflow.js +1182 -0
  107. package/dist/ownership-workflow.js.map +1 -0
  108. package/dist/seq-tracker.d.ts +1 -0
  109. package/dist/seq-tracker.js +10 -1
  110. package/dist/seq-tracker.js.map +1 -1
  111. package/dist/tools/cross-sdk-agent.d.ts +90 -1
  112. package/dist/tools/cross-sdk-agent.js +441 -7
  113. package/dist/tools/cross-sdk-agent.js.map +1 -1
  114. package/dist/transport.d.ts +8 -0
  115. package/dist/transport.js +118 -4
  116. package/dist/transport.js.map +1 -1
  117. package/dist/v2/e2ee/decrypt.d.ts +1 -0
  118. package/dist/v2/e2ee/decrypt.js +9 -1
  119. package/dist/v2/e2ee/decrypt.js.map +1 -1
  120. package/dist/version.d.ts +1 -1
  121. package/dist/version.js +1 -1
  122. package/package.json +1 -1
@@ -0,0 +1,1182 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { isScalar, parseDocument } from 'yaml';
3
+ import { StateError, ValidationError } from './errors.js';
4
+ import { OWNERSHIP_UUID_V4_RE, ownershipAid, ownershipFailure, ownershipInteger, ownershipSha256, parseOwnershipFile, requireOwnershipApplicable } from './ownership-v21.js';
5
+ export const OWNERSHIP_SDK_MESSAGE_TYPE = 'aun.ownership.sdk.v1';
6
+ function participants(task) {
7
+ return [...new Set([task.owner_aid, task.sub_aid, ...(task.new_owner_aid ? [task.new_owner_aid] : [])])];
8
+ }
9
+ function targetOwner(task) {
10
+ return task.operation === 'transfer' ? task.new_owner_aid : task.owner_aid;
11
+ }
12
+ export function parseOwnershipMessage(content, context) {
13
+ let value = content;
14
+ if (typeof content === 'string') {
15
+ try {
16
+ value = JSON.parse(content);
17
+ }
18
+ catch {
19
+ throw new ValidationError('ownership message must be JSON');
20
+ }
21
+ const document = parseDocument(content, { uniqueKeys: true });
22
+ if (document.errors.length)
23
+ throw new ValidationError('ownership message has duplicate or invalid fields');
24
+ for (const field of ['base_relation_version', 'target_relation_version']) {
25
+ const node = document.get(field, true);
26
+ if (!isScalar(node) || !/^(?:0|[1-9][0-9]*)$/.test(node.source ?? ''))
27
+ throw new ValidationError(`${field} must use an integer`);
28
+ }
29
+ }
30
+ if (!value || typeof value !== 'object' || Array.isArray(value))
31
+ throw new ValidationError('ownership message must be an object');
32
+ const message = value;
33
+ const required = ['type', 'operation_id', 'operation', 'action', 'sub_aid', 'owner_aid', 'base_relation_version', 'target_relation_version'];
34
+ const optional = ['new_owner_aid', 'reason'];
35
+ if (required.some((key) => !(key in message)) || Object.keys(message).some((key) => !required.includes(key) && !optional.includes(key)))
36
+ throw new ValidationError('ownership message contains unknown or missing fields');
37
+ if (message.type !== OWNERSHIP_SDK_MESSAGE_TYPE)
38
+ throw ownershipFailure('unsupported_protocol', 'legacy ownership control is unsupported');
39
+ if (typeof message.operation_id !== 'string' || !OWNERSHIP_UUID_V4_RE.test(message.operation_id))
40
+ throw new ValidationError('operation_id must be a UUIDv4');
41
+ if (!['bind', 'release', 'transfer'].includes(message.operation))
42
+ throw new ValidationError('ownership operation is invalid');
43
+ ownershipAid(message.sub_aid, 'sub_aid');
44
+ ownershipAid(message.owner_aid, 'owner_aid');
45
+ ownershipInteger(message.base_relation_version, 'base_relation_version', 0);
46
+ if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + (message.operation === 'release' ? 0 : 1))
47
+ throw new ValidationError('ownership target version does not match the operation');
48
+ if (message.operation === 'transfer') {
49
+ if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
50
+ throw new ValidationError('transfer target must differ from current owner');
51
+ }
52
+ else if ('new_owner_aid' in message)
53
+ throw new ValidationError('new_owner_aid is only allowed for transfer');
54
+ const allowed = {
55
+ bind: ['bind.request', 'bind.accept', 'bind.reject', 'owner.ready', 'relation.confirmed', 'operation.stopped'],
56
+ release: ['relation.confirmed', 'sub.cleaned', 'operation.stopped'],
57
+ transfer: ['transfer.request', 'transfer.accept', 'transfer.reject', 'transfer.ready', 'owner.ready', 'relation.confirmed', 'operation.stopped'],
58
+ };
59
+ if (!allowed[message.operation].includes(message.action))
60
+ throw new ValidationError('ownership action does not match operation');
61
+ if ('reason' in message && (typeof message.reason !== 'string' || !['bind.reject', 'transfer.reject', 'operation.stopped'].includes(message.action)))
62
+ throw new ValidationError('ownership reason is only allowed for reject or stopped');
63
+ const from = ownershipAid(context.authenticated_from_aid, 'authenticated_from_aid');
64
+ const local = ownershipAid(context.local_aid, 'local_aid');
65
+ if (from === local || !participants(message).includes(from) || !participants(message).includes(local))
66
+ throw ownershipFailure('forbidden', 'ownership sender or receiver is not a remote participant');
67
+ const task = context.task;
68
+ if (task && ['operation_id', 'operation', 'sub_aid', 'owner_aid', 'new_owner_aid', 'base_relation_version', 'target_relation_version'].some((key) => task[key] !== message[key]))
69
+ throw ownershipFailure('conflict', 'ownership fixed task context changed');
70
+ const initiator = task?.initiator_aid;
71
+ let direction = false;
72
+ switch (message.action) {
73
+ case 'bind.request':
74
+ direction = (!initiator || from === initiator) && [message.owner_aid, message.sub_aid].includes(from);
75
+ break;
76
+ case 'bind.accept':
77
+ case 'bind.reject':
78
+ direction = Boolean(initiator && local === initiator && from !== initiator);
79
+ break;
80
+ case 'transfer.request':
81
+ case 'transfer.ready':
82
+ direction = from === message.owner_aid && [message.sub_aid, message.new_owner_aid].includes(local);
83
+ break;
84
+ case 'transfer.accept':
85
+ case 'transfer.reject':
86
+ direction = Boolean(task && local === message.owner_aid && [message.sub_aid, message.new_owner_aid].includes(from));
87
+ break;
88
+ case 'owner.ready':
89
+ direction = Boolean(task && from === targetOwner(message) && local === message.sub_aid);
90
+ break;
91
+ case 'relation.confirmed':
92
+ direction = message.operation === 'release' ? from === message.owner_aid && local === message.sub_aid : Boolean(task && from === message.sub_aid && local !== message.sub_aid);
93
+ break;
94
+ case 'sub.cleaned':
95
+ direction = Boolean(task && from === message.sub_aid && local === message.owner_aid);
96
+ break;
97
+ case 'operation.stopped':
98
+ direction = Boolean(task);
99
+ break;
100
+ }
101
+ if (!direction)
102
+ throw ownershipFailure('forbidden', 'ownership action has an invalid authenticated direction');
103
+ if (['owner.ready', 'transfer.ready'].includes(message.action) && task?.local_decision !== 'accept')
104
+ throw ownershipFailure('forbidden', 'ownership local approval is required');
105
+ if (context.ns) {
106
+ const ns = context.ns;
107
+ if (ns.protocol !== '2.1')
108
+ throw ownershipFailure('unsupported_protocol', 'NS ownership protocol is unavailable');
109
+ 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);
110
+ const target = message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === message.target_relation_version || ns.relation_version === 0) : ns.relation_version === message.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(message);
111
+ if (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target : !base && !target)
112
+ throw ownershipFailure('conflict', 'ownership current NS relation does not match this task');
113
+ }
114
+ return message;
115
+ }
116
+ function localRoles(task, local) {
117
+ const roles = [];
118
+ if (task.owner_aid === local)
119
+ roles.push('owner');
120
+ if (task.sub_aid === local)
121
+ roles.push('sub');
122
+ if (task.new_owner_aid === local)
123
+ roles.push('new_owner');
124
+ return roles;
125
+ }
126
+ export function ownershipTaskResult(task) {
127
+ const result = {
128
+ operation_id: task.operation_id, operation: task.operation, sub_aid: task.sub_aid, owner_aid: task.owner_aid,
129
+ base_relation_version: task.base_relation_version, target_relation_version: task.target_relation_version, status: task.status, step: task.step,
130
+ };
131
+ if (task.operation === 'transfer')
132
+ result.new_owner_aid = task.new_owner_aid;
133
+ for (const field of ['code', 'reason', 'retryable'])
134
+ if (task[field] !== undefined)
135
+ Object.assign(result, { [field]: task[field] });
136
+ return result;
137
+ }
138
+ export class OwnershipWorkflow {
139
+ deps;
140
+ static serial = new Map();
141
+ constructor(deps) {
142
+ this.deps = deps;
143
+ }
144
+ async bindOwneraid(ownerAid) {
145
+ return this.start('bind', this.local(), ownershipAid(ownerAid, 'owner_aid'));
146
+ }
147
+ async bindSubaid(subAid) {
148
+ return this.start('bind', ownershipAid(subAid, 'sub_aid'), this.local());
149
+ }
150
+ async releaseSubaid(subAid) {
151
+ return this.start('release', ownershipAid(subAid, 'sub_aid'), this.local());
152
+ }
153
+ async transferSubaid(subAid, newOwnerAid) {
154
+ return this.start('transfer', ownershipAid(subAid, 'sub_aid'), this.local(), ownershipAid(newOwnerAid, 'new_owner_aid'));
155
+ }
156
+ async resumeRegistration(ownerAid, operationId) {
157
+ if (typeof operationId !== 'string' || !OWNERSHIP_UUID_V4_RE.test(operationId))
158
+ throw ownershipFailure('invalid', 'registration operation_id is invalid');
159
+ return this.start('bind', this.local(), ownershipAid(ownerAid, 'owner_aid'), undefined, operationId);
160
+ }
161
+ async acceptOwnershipRequest(operationId) {
162
+ return this.decide(operationId, 'accept');
163
+ }
164
+ async rejectOwnershipRequest(operationId, reason) {
165
+ if (reason !== undefined && typeof reason !== 'string')
166
+ throw ownershipFailure('invalid', 'ownership rejection reason must be a string');
167
+ return this.decide(operationId, 'reject', reason);
168
+ }
169
+ async checkOwnershipConsensus(subAid = this.local()) {
170
+ return this.deps.files.checkConsensus(ownershipAid(subAid, 'sub_aid'));
171
+ }
172
+ async receive(from, content) {
173
+ let body = content;
174
+ if (typeof content === 'string') {
175
+ try {
176
+ body = JSON.parse(content);
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ }
182
+ if (!body || typeof body !== 'object' || Array.isArray(body))
183
+ return false;
184
+ const type = body.type;
185
+ if (typeof type !== 'string' || !type.startsWith('aun.ownership.'))
186
+ return false;
187
+ if (type !== OWNERSHIP_SDK_MESSAGE_TYPE)
188
+ return true;
189
+ let message;
190
+ try {
191
+ message = parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(),
192
+ task: ['bind.request', 'transfer.request'].includes(String(body.action)) || body.operation === 'release' && body.action === 'relation.confirmed' ? undefined : this.load(String(body.operation_id))?.record });
193
+ }
194
+ catch (error) {
195
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
196
+ throw error;
197
+ return true;
198
+ }
199
+ let observationError;
200
+ try {
201
+ this.requireApplicable(message.sub_aid);
202
+ await this.deps.files.readAgent(message.sub_aid);
203
+ }
204
+ catch (error) {
205
+ if (error instanceof StateError && error.stringCode === 'not_applicable')
206
+ return true;
207
+ observationError = error;
208
+ }
209
+ return this.runSerialized(message.sub_aid, async () => {
210
+ let handle = this.load(message.operation_id);
211
+ if (handle) {
212
+ try {
213
+ parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(), task: handle.record });
214
+ }
215
+ catch {
216
+ return true;
217
+ }
218
+ }
219
+ else {
220
+ if (!['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
221
+ return true;
222
+ handle = { record: this.newRecord(message, from, 'pending'), revision: null };
223
+ }
224
+ if (message.action === 'sub.cleaned') {
225
+ const began = Date.now();
226
+ const before = await this.readNs(message.sub_aid), after = await this.readNs(message.sub_aid);
227
+ if (Date.now() - began > 30_000 || before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid || ownershipSha256(before.content) !== ownershipSha256(after.content))
228
+ throw ownershipFailure('unavailable', 'NS changed during sub cleanup verification', true);
229
+ if (!before.exists || before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
230
+ return true;
231
+ const file = parseOwnershipFile(before.content, 'agent', { expectedSubAid: message.sub_aid });
232
+ if (Object.hasOwn(file.frontmatter, 'owner') || Object.hasOwn(file.frontmatter, 'ownership'))
233
+ return true;
234
+ }
235
+ const key = `${from}|${message.operation_id}|${message.action}`;
236
+ if (!handle.record.received.includes(key) && !handle.record.inbox.some((item) => `${item.from}|${item.payload.operation_id}|${item.payload.action}` === key)) {
237
+ handle.record.inbox.push({ from, payload: message });
238
+ await this.save(handle);
239
+ }
240
+ if (observationError)
241
+ await this.recordError(handle, observationError);
242
+ else
243
+ await this.drive(handle);
244
+ return true;
245
+ });
246
+ }
247
+ async resume() {
248
+ const failed = [];
249
+ for (const snapshot of this.list(true)) {
250
+ try {
251
+ const record = this.decode(snapshot).record;
252
+ await this.runSerialized(record.sub_aid, async () => {
253
+ const handle = this.load(record.operation_id);
254
+ await this.drive(handle);
255
+ if (handle.record.request_verified && handle.record.local_decision === 'pending' && handle.record.status === 'pending' && handle.record.operation !== 'release')
256
+ this.emit(handle.record, 'ownership.request');
257
+ });
258
+ }
259
+ catch (error) {
260
+ failed.push(`${snapshot.operation_id}: ${error instanceof Error ? error.message : String(error)}`);
261
+ }
262
+ }
263
+ if (failed.length)
264
+ throw ownershipFailure('persistence_unavailable', failed.join('; '));
265
+ }
266
+ local() { return ownershipAid(this.deps.currentAid(), 'local_aid'); }
267
+ requireApplicable(subAid) {
268
+ if (subAid === this.local())
269
+ requireOwnershipApplicable(this.deps.currentType?.());
270
+ }
271
+ store() {
272
+ if (!this.deps.store.loadOwnershipTask || !this.deps.store.listOwnershipTasks || !this.deps.store.compareAndSwapOwnershipTask)
273
+ throw ownershipFailure('persistence_unavailable', 'atomic ownership task storage is unavailable');
274
+ return this.deps.store;
275
+ }
276
+ list(isolateErrors = false) {
277
+ try {
278
+ return this.store().listOwnershipTasks(this.local(), isolateErrors);
279
+ }
280
+ catch (error) {
281
+ throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
282
+ }
283
+ }
284
+ load(operationId) {
285
+ try {
286
+ const snapshot = this.store().loadOwnershipTask(this.local(), operationId);
287
+ return snapshot ? this.decode(snapshot) : null;
288
+ }
289
+ catch (error) {
290
+ throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
291
+ }
292
+ }
293
+ decode(snapshot) {
294
+ const record = snapshot.task;
295
+ try {
296
+ if (record.protocol !== '2.1' || typeof record.operation_id !== 'string' || !OWNERSHIP_UUID_V4_RE.test(record.operation_id) || record.operation_id !== snapshot.operation_id || !['bind', 'release', 'transfer'].includes(record.operation) || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(record.status) || typeof record.step !== 'string')
297
+ throw new Error('invalid ownership task');
298
+ ownershipAid(record.sub_aid, 'sub_aid');
299
+ ownershipAid(record.owner_aid, 'owner_aid');
300
+ ownershipAid(record.initiator_aid, 'initiator_aid');
301
+ if (record.operation === 'transfer') {
302
+ if (ownershipAid(record.new_owner_aid, 'new_owner_aid') === record.owner_aid)
303
+ throw new Error('invalid transfer task');
304
+ }
305
+ else if (record.new_owner_aid !== undefined)
306
+ throw new Error('unexpected transfer target');
307
+ if (ownershipInteger(record.target_relation_version, 'target_relation_version') !== ownershipInteger(record.base_relation_version, 'base_relation_version', 0) + (record.operation === 'release' ? 0 : 1) || !participants(record).includes(record.initiator_aid) || record.operation !== 'bind' && record.initiator_aid !== record.owner_aid)
308
+ throw new Error('invalid fixed ownership context');
309
+ if (JSON.stringify(record.local_roles) !== JSON.stringify(localRoles(record, this.local())) || !record.local_roles.length || !['pending', 'accept', 'reject'].includes(record.local_decision) || !record.approvals || participants(record).some((aid) => !['pending', 'accept', 'reject'].includes(record.approvals[aid])) || record.approvals[this.local()] !== record.local_decision || !record.files || typeof record.files !== 'object' || Array.isArray(record.files) || !Array.isArray(record.inbox) || !Array.isArray(record.outbox) || !Array.isArray(record.received) || !Array.isArray(record.notices) || typeof record.request_verified !== 'boolean' || typeof record.request_notified !== 'boolean')
310
+ throw new Error('ownership approval or pending state is corrupt');
311
+ if (Object.keys(record.approvals).length !== participants(record).length || record.approvals[record.initiator_aid] !== 'accept' || record.initiator_aid === this.local() && record.local_decision !== 'accept')
312
+ throw new Error('ownership initiator or approval participants are corrupt');
313
+ if (record.request_verified) {
314
+ const base = record.base_agent_md;
315
+ if (!base || base.exists !== true || typeof base.raw !== 'string' || base.sha256 !== ownershipSha256(base.raw) || typeof base.etag !== 'string' || !base.etag)
316
+ throw new Error('ownership original agent.md evidence is corrupt');
317
+ const statement = parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
318
+ if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version)
319
+ throw new Error('ownership original statement is corrupt');
320
+ const owner = record.base_owner_md;
321
+ if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
322
+ throw new Error('ownership original owner.md evidence is corrupt');
323
+ if (owner.exists) {
324
+ const declaration = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: statement?.owner_aid ?? record.owner_aid });
325
+ if (declaration.statement?.relation_version !== record.base_relation_version)
326
+ throw new Error('ownership original owner statement is corrupt');
327
+ }
328
+ }
329
+ for (const item of record.inbox)
330
+ parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record });
331
+ for (const key of record.received) {
332
+ if (typeof key !== 'string' || key.split('|').length !== 3)
333
+ throw new Error('ownership received key is corrupt');
334
+ const [from, operationId, action] = key.split('|');
335
+ parseOwnershipMessage({ type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation: record.operation, action,
336
+ sub_aid: record.sub_aid, owner_aid: record.owner_aid, ...(record.operation === 'transfer' ? { new_owner_aid: record.new_owner_aid } : {}),
337
+ base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version }, { authenticated_from_aid: from, local_aid: this.local(), task: record });
338
+ }
339
+ for (const item of record.outbox)
340
+ this.validateOutgoing(record, item);
341
+ if (record.status === 'completed' && (record.verified_relation_version !== record.target_relation_version || record.operation !== 'release' && record.local_decision !== 'accept'))
342
+ throw new Error('ownership completion evidence or local approval is corrupt');
343
+ const transferEvidenceRequired = record.operation === 'transfer' && (record.authorization_sha256 !== undefined || record.expires_at !== undefined || (record.status === 'completed' || ['owner', 'agent'].includes(record.step) || record.files.agent || record.files.owner) && record.local_roles.some((role) => role === 'sub' || role === 'new_owner'));
344
+ if (transferEvidenceRequired && (!/^[0-9a-f]{64}$/.test(record.authorization_sha256 ?? '') || !Number.isSafeInteger(record.expires_at) || (record.expires_at ?? 0) <= 0))
345
+ throw new Error('ownership transfer authorization evidence is corrupt');
346
+ for (const [key, saved] of Object.entries(record.files)) {
347
+ if (!['agent', 'owner', 'release', 'transfer', 'cleanup'].includes(key) || !saved || typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !saved.condition || typeof saved.condition !== 'object' || Object.keys(saved.condition).length !== (key === 'agent' ? 1 : 0) || saved.published_etag !== undefined && (key !== 'agent' || typeof saved.published_etag !== 'string' || !saved.published_etag))
348
+ throw new Error('ownership saved file is corrupt');
349
+ if (key === 'agent' && !(typeof saved.condition.if_match === 'string' && saved.condition.if_match || saved.condition.if_none_match === '*'))
350
+ throw new Error('ownership original file condition is corrupt');
351
+ const original = saved.original_owner;
352
+ if (key !== 'agent' && key !== 'cleanup' && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
353
+ throw new Error('ownership original owner material is corrupt');
354
+ const owner = ['agent', 'owner'].includes(key) ? targetOwner(record) : record.owner_aid;
355
+ const file = parseOwnershipFile(saved.raw, key === 'agent' ? 'agent' : 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: owner });
356
+ const version = ['release', 'transfer', 'cleanup'].includes(key) ? record.base_relation_version : record.target_relation_version;
357
+ if (key === 'agent' && record.operation === 'release' ? Boolean(file.statement) : file.statement?.relation_version !== version)
358
+ throw new Error('ownership saved file relation is corrupt');
359
+ if (key === 'owner' && file.action !== 'declare' || key === 'release' && file.action !== 'release' || key === 'transfer' && (file.action !== 'transfer_owner' || file.transfer?.new_owner_aid !== record.new_owner_aid || file.transfer?.target_relation_version !== record.target_relation_version || file.transfer?.expires_at !== saved.expires_at))
360
+ throw new Error('ownership saved file action is corrupt');
361
+ }
362
+ if (record.release_cleanup !== undefined) {
363
+ const cleanup = record.release_cleanup;
364
+ if (!['bind', 'transfer'].includes(record.operation) || record.status !== 'completed' || !record.local_roles.includes('sub') || record.local_decision !== 'accept' || typeof cleanup.cleaned !== 'boolean' || !cleanup.files || typeof cleanup.files !== 'object' || Array.isArray(cleanup.files))
365
+ throw new Error('ownership historical cleanup context is corrupt');
366
+ for (const [key, saved] of Object.entries(cleanup.files)) {
367
+ if (key !== 'agent' || !saved || saved.hash !== ownershipSha256(saved.raw) || typeof saved.condition?.if_match !== 'string' || !saved.condition.if_match || Object.keys(saved.condition).length !== 1 || parseOwnershipFile(saved.raw, 'agent', { expectedSubAid: record.sub_aid }).statement)
368
+ throw new Error('ownership historical cleanup file is corrupt');
369
+ }
370
+ }
371
+ if (record.status === 'completed') {
372
+ const evidence = record.completion_evidence;
373
+ const cleaned = record.operation === 'release' && evidence?.status === 'unbound' && evidence.relation_version === 0 && record.cleanup_status === 'cleaned' && Boolean(this.cleanupEvidence(record, evidence));
374
+ if (!evidence || evidence.sub_aid !== record.sub_aid || !cleaned && (evidence.relation_version !== record.target_relation_version || evidence.status !== (record.operation === 'release' ? 'released' : 'consensus')) || evidence.lock !== (record.operation === 'release' ? 'unlocked' : 'locked') || evidence.owner_aid !== (record.operation === 'release' ? null : targetOwner(record)) || typeof evidence.agent_md_etag !== 'string' || !evidence.agent_md_etag || !/^[0-9a-f]{64}$/.test(evidence.agent_md_sha256) || typeof evidence.owner_md_sha256 !== 'string' || record.operation !== 'release' && !/^[0-9a-f]{64}$/.test(evidence.owner_md_sha256))
375
+ throw new Error('ownership target acceptance evidence is corrupt');
376
+ if (record.operation !== 'release' && (record.local_roles.includes('sub') && record.files.agent?.hash !== evidence.agent_md_sha256 || this.local() === targetOwner(record) && record.files.owner?.hash !== evidence.owner_md_sha256 || record.operation === 'transfer' && record.local_roles.includes('owner') && !record.files.transfer))
377
+ throw new Error('ownership target file evidence is missing');
378
+ if (record.operation === 'release' && record.local_roles.includes('owner') && !record.files.release)
379
+ throw new Error('ownership release file evidence is missing');
380
+ }
381
+ return { record, revision: snapshot.revision };
382
+ }
383
+ catch (error) {
384
+ throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
385
+ }
386
+ }
387
+ async save(handle) {
388
+ try {
389
+ const snapshot = this.store().compareAndSwapOwnershipTask(this.local(), handle.record.operation_id, handle.revision, handle.record);
390
+ handle.revision = snapshot.revision;
391
+ }
392
+ catch (error) {
393
+ throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
394
+ }
395
+ }
396
+ async recordError(handle, error) {
397
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
398
+ throw error;
399
+ const record = handle.record;
400
+ if (record.status === 'error' && record.retryable)
401
+ record.status = 'pending';
402
+ let code = error instanceof ValidationError ? 'invalid' : error instanceof StateError && error.stringCode ? error.stringCode : 'unavailable';
403
+ if (record.status === 'pending' && ['conflict', 'owner_locked', 'forbidden'].includes(code)) {
404
+ try {
405
+ if (await this.confirm(handle)) {
406
+ await this.flush(handle);
407
+ await this.cleanup(handle);
408
+ return;
409
+ }
410
+ }
411
+ catch (confirmationError) {
412
+ if (confirmationError instanceof StateError && confirmationError.stringCode === 'persistence_unavailable')
413
+ throw confirmationError;
414
+ error = confirmationError;
415
+ code = error instanceof ValidationError ? 'invalid' : error instanceof StateError && error.stringCode ? error.stringCode : 'unavailable';
416
+ }
417
+ }
418
+ record.code = code;
419
+ record.reason = error instanceof Error ? error.message : String(error);
420
+ record.retryable = record.status === 'pending' && ['unavailable', 'unsupported_protocol'].includes(code);
421
+ if (record.status === 'pending' && code === 'invalid')
422
+ record.status = 'error';
423
+ else if (record.status === 'pending' && ['conflict', 'owner_locked', 'forbidden'].includes(code))
424
+ record.status = 'stopped';
425
+ await this.save(handle);
426
+ this.emit(record, record.status === 'stopped' ? 'ownership.stopped' : 'ownership.error');
427
+ }
428
+ async runSerialized(subAid, work) {
429
+ const key = `${this.local()}|${subAid}`;
430
+ const before = OwnershipWorkflow.serial.get(key) ?? Promise.resolve();
431
+ const next = before.catch(() => { }).then(work);
432
+ OwnershipWorkflow.serial.set(key, next);
433
+ try {
434
+ return await next;
435
+ }
436
+ finally {
437
+ if (OwnershipWorkflow.serial.get(key) === next)
438
+ OwnershipWorkflow.serial.delete(key);
439
+ }
440
+ }
441
+ newRecord(context, initiator, decision) {
442
+ const record = {
443
+ protocol: '2.1', operation_id: context.operation_id, operation: context.operation, sub_aid: context.sub_aid, owner_aid: context.owner_aid,
444
+ base_relation_version: context.base_relation_version, target_relation_version: context.target_relation_version, status: 'pending', step: 'approval', retryable: true,
445
+ initiator_aid: initiator, local_roles: localRoles(context, this.local()), local_decision: decision,
446
+ approvals: Object.fromEntries(participants(context).map((name) => [name, name === initiator ? 'accept' : 'pending'])),
447
+ files: {}, inbox: [], received: [], outbox: [], notices: [], request_verified: initiator === this.local(), request_notified: false,
448
+ };
449
+ if (context.operation === 'transfer')
450
+ record.new_owner_aid = context.new_owner_aid;
451
+ record.approvals[this.local()] = decision;
452
+ return record;
453
+ }
454
+ async start(operation, subAid, ownerAid, newOwnerAid, operationId) {
455
+ this.requireApplicable(subAid);
456
+ return this.runSerialized(subAid, async () => {
457
+ const existing = operationId ? this.load(operationId) : null;
458
+ if (existing) {
459
+ const record = existing.record;
460
+ if (record.operation !== operation || record.sub_aid !== subAid || record.owner_aid !== ownerAid || record.new_owner_aid !== newOwnerAid || record.initiator_aid !== this.local())
461
+ throw ownershipFailure('conflict', 'saved registration task context changed');
462
+ await this.drive(existing);
463
+ return ownershipTaskResult(record);
464
+ }
465
+ let ns;
466
+ for (const snapshot of this.list()) {
467
+ const active = this.decode(snapshot), record = active.record;
468
+ if (record.sub_aid !== subAid || record.status !== 'pending' && !(record.status === 'error' && record.retryable))
469
+ continue;
470
+ const same = record.operation === operation && record.owner_aid === ownerAid && record.new_owner_aid === newOwnerAid && record.initiator_aid === this.local();
471
+ try {
472
+ ns = await this.readNs(subAid);
473
+ }
474
+ catch (error) {
475
+ if (!same)
476
+ throw error;
477
+ await this.recordError(active, error);
478
+ return ownershipTaskResult(record);
479
+ }
480
+ 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);
481
+ if (base && record.request_verified) {
482
+ try {
483
+ await this.assertBaseMaterial(active, ns);
484
+ }
485
+ catch (error) {
486
+ if (!(error instanceof StateError) || error.stringCode !== 'conflict')
487
+ throw error;
488
+ base = false;
489
+ }
490
+ }
491
+ const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.target_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
492
+ if (!base && !target) {
493
+ await this.recordError(active, ownershipFailure('conflict', 'the operation was superseded by a different relation result'));
494
+ const result = ownershipTaskResult(record);
495
+ if (same && result.status !== 'stopped')
496
+ return result;
497
+ if (result.status !== 'stopped' && result.status !== 'completed')
498
+ throw ownershipFailure(record.code ?? 'unavailable', record.reason ?? 'the original target cannot be confirmed', record.retryable);
499
+ continue;
500
+ }
501
+ if (!same || operationId && operationId !== record.operation_id)
502
+ throw ownershipFailure('conflict', 'a different active ownership intent already exists');
503
+ await this.drive(active);
504
+ return ownershipTaskResult(record);
505
+ }
506
+ if (operation === 'transfer' && newOwnerAid === ownerAid)
507
+ throw ownershipFailure('invalid', 'new owner must differ from original owner');
508
+ ns = await this.readNs(subAid);
509
+ if (operation === 'bind' && ns.lock !== 'unlocked')
510
+ throw ownershipFailure('owner_locked', 'the current owner relation is locked');
511
+ if (!ns.exists)
512
+ throw ownershipFailure('unavailable', 'initial agent.md is not published', true);
513
+ if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type === 'human')
514
+ throw ownershipFailure('invalid', 'human cannot declare an owner');
515
+ if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
516
+ throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
517
+ if (operation !== 'bind') {
518
+ const consensus = await this.deps.files.checkConsensus(subAid);
519
+ if (consensus.status !== 'consensus' || consensus.owner_aid !== ownerAid || consensus.relation_version !== ns.relation_version)
520
+ throw ownershipFailure(consensus.status === 'invalid' ? 'invalid' : consensus.status === 'unavailable' ? consensus.code ?? 'unavailable' : 'forbidden', 'the current original owner consensus cannot be verified', consensus.status === 'unavailable');
521
+ }
522
+ const targetVersion = ownershipInteger(ns.relation_version + (operation === 'release' ? 0 : 1), 'target_relation_version');
523
+ const record = this.newRecord({ operation_id: operationId ?? randomUUID(), operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid, base_relation_version: ns.relation_version, target_relation_version: targetVersion }, this.local(), 'accept');
524
+ await this.captureBase(record, ns);
525
+ if (operation !== 'release')
526
+ for (const to of participants(record))
527
+ if (to !== this.local())
528
+ this.enqueue(record, to, operation === 'bind' ? 'bind.request' : 'transfer.request');
529
+ const handle = { record, revision: null };
530
+ await this.save(handle);
531
+ await this.drive(handle);
532
+ return ownershipTaskResult(record);
533
+ });
534
+ }
535
+ async decide(operationId, decision, reason) {
536
+ const initial = this.load(operationId);
537
+ if (!initial)
538
+ throw ownershipFailure('operation_not_found', 'no local ownership request matches operation_id');
539
+ return this.runSerialized(initial.record.sub_aid, async () => {
540
+ const handle = this.load(operationId);
541
+ const record = handle.record;
542
+ if (record.operation === 'release' || record.initiator_aid === this.local() || !record.request_verified)
543
+ throw ownershipFailure('forbidden', 'this task is not a verified local approval request');
544
+ if (record.local_decision !== 'pending') {
545
+ if (record.local_decision !== decision)
546
+ throw ownershipFailure('conflict', 'the persisted local approval cannot be changed');
547
+ await this.drive(handle);
548
+ return ownershipTaskResult(record);
549
+ }
550
+ if (record.status === 'error' && record.retryable)
551
+ record.status = 'pending';
552
+ if (record.status !== 'pending')
553
+ throw ownershipFailure('conflict', 'ownership approval stage has ended');
554
+ const ns = await this.readNs(record.sub_aid);
555
+ if (ns.relation_version !== record.base_relation_version || (record.operation === 'bind' ? ns.lock !== 'unlocked' : ns.lock !== 'locked' || ns.owner_aid !== record.owner_aid))
556
+ throw ownershipFailure('conflict', 'the original relation changed before approval');
557
+ await this.assertBaseMaterial(handle, ns);
558
+ record.local_decision = decision;
559
+ record.approvals[this.local()] = decision;
560
+ this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
561
+ if (decision === 'reject') {
562
+ record.status = 'rejected';
563
+ record.step = 'approval';
564
+ record.retryable = false;
565
+ if (reason !== undefined)
566
+ record.reason = reason;
567
+ }
568
+ await this.save(handle);
569
+ this.emit(record, decision === 'reject' ? 'ownership.rejected' : 'ownership.progress');
570
+ await this.drive(handle);
571
+ return ownershipTaskResult(record);
572
+ });
573
+ }
574
+ enqueue(record, to, action, reason) {
575
+ if (to === this.local())
576
+ return;
577
+ const key = `${to}|${action}`;
578
+ if (record.notices.includes(key))
579
+ return;
580
+ const payload = { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: record.operation_id, operation: record.operation, action,
581
+ sub_aid: record.sub_aid, owner_aid: record.owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version };
582
+ if (record.operation === 'transfer')
583
+ payload.new_owner_aid = record.new_owner_aid;
584
+ if (reason !== undefined)
585
+ payload.reason = reason;
586
+ record.outbox.push({ to, payload });
587
+ record.notices.push(key);
588
+ }
589
+ validateOutgoing(record, item) {
590
+ try {
591
+ const message = parseOwnershipMessage(item.payload, { authenticated_from_aid: this.local(), local_aid: item.to, task: { ...record, local_decision: 'accept' } });
592
+ const rejected = ['bind.reject', 'transfer.reject'].includes(message.action);
593
+ if (message.action !== 'operation.stopped' && record.local_decision !== (rejected ? 'reject' : 'accept') || record.approvals[this.local()] !== record.local_decision)
594
+ throw new Error('ownership outgoing action lacks local approval');
595
+ if (['bind.request', 'transfer.request'].includes(message.action) && record.initiator_aid !== this.local())
596
+ throw new Error('ownership outgoing request is not from the initiator');
597
+ if ((message.action === 'transfer.ready' || message.action === 'owner.ready' && record.operation === 'bind') && !participants(record).every((aid) => record.approvals[aid] === 'accept'))
598
+ throw new Error('ownership outgoing action lacks participant approvals');
599
+ }
600
+ catch (error) {
601
+ throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
602
+ }
603
+ }
604
+ async flush(handle) {
605
+ while (handle.record.outbox.length) {
606
+ const message = handle.record.outbox[0];
607
+ this.validateOutgoing(handle.record, message);
608
+ const record = handle.record;
609
+ if (['rejected', 'stopped', 'error'].includes(record.status) && !['bind.reject', 'transfer.reject', 'operation.stopped'].includes(message.payload.action)) {
610
+ record.outbox.shift();
611
+ await this.save(handle);
612
+ continue;
613
+ }
614
+ const ns = await this.readNs(record.sub_aid);
615
+ 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);
616
+ const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.target_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
617
+ const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
618
+ if (requiresTarget ? !target : !base) {
619
+ record.outbox.shift();
620
+ await this.save(handle);
621
+ continue;
622
+ }
623
+ if (base && record.request_verified)
624
+ await this.assertBaseMaterial(handle, ns);
625
+ if (message.payload.action === 'transfer.ready' && record.status !== 'completed') {
626
+ const expiresAt = record.files.transfer?.expires_at;
627
+ if (typeof expiresAt !== 'number' || !Number.isSafeInteger(expiresAt) || expiresAt <= 0)
628
+ throw ownershipFailure('persistence_unavailable', 'persisted transfer authorization expiry is missing or invalid');
629
+ if (expiresAt <= Math.trunc(Date.now() / 1000))
630
+ throw ownershipFailure('conflict', 'transfer authorization expired');
631
+ }
632
+ await this.deps.send(message.to, message.payload);
633
+ handle.record.outbox.shift();
634
+ await this.save(handle);
635
+ }
636
+ }
637
+ async readNs(subAid) {
638
+ const ns = await this.deps.files.readAgent(subAid);
639
+ const file = ns.exists ? await this.deps.files.verifyFile(ns.content, 'agent', subAid) : undefined;
640
+ if (ns.relation_version !== (file?.statement?.relation_version ?? 0))
641
+ throw ownershipFailure('unavailable', 'NS relation metadata does not match agent.md', true);
642
+ const currentOwner = ns.lock === 'locked' ? file?.statement?.owner_aid : null;
643
+ if (ns.lock === 'locked' && (!file?.statement || file.statement.relation_version !== ns.relation_version) || ns.owner_aid !== undefined && ns.owner_aid !== currentOwner)
644
+ throw ownershipFailure('unavailable', 'NS owner metadata and signed agent.md do not agree', true);
645
+ return { ...ns, owner_aid: currentOwner };
646
+ }
647
+ async captureBase(record, ns) {
648
+ const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
649
+ record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement };
650
+ if (!statement) {
651
+ record.base_owner_md = { exists: false, raw: '', sha256: '' };
652
+ return;
653
+ }
654
+ const owner = await this.deps.files.readOwner(statement.owner_aid, record.sub_aid);
655
+ if ((record.operation === 'transfer' || record.operation === 'release' && ns.lock === 'locked') && !owner.exists)
656
+ throw ownershipFailure('unavailable', 'original owner.md is unavailable', true);
657
+ if (owner.exists)
658
+ await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, statement.owner_aid);
659
+ record.base_owner_md = { exists: owner.exists, raw: owner.content, sha256: owner.exists ? ownershipSha256(owner.content) : '' };
660
+ }
661
+ async assertBaseMaterial(handle, ns) {
662
+ const record = handle.record;
663
+ this.assertAgentBaseMaterial(record, ns);
664
+ const original = record.base_owner_md;
665
+ if (!original)
666
+ throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is missing');
667
+ const originalOwnerAid = record.base_agent_md.statement?.owner_aid;
668
+ if (!originalOwnerAid)
669
+ return;
670
+ const current = await this.deps.files.readOwner(originalOwnerAid, record.sub_aid);
671
+ if (current.exists === original.exists && current.content === original.raw && (current.exists ? current.sha256 : '') === original.sha256)
672
+ return;
673
+ if (!current.exists)
674
+ throw ownershipFailure('conflict', 'the original owner.md was replaced');
675
+ const key = record.operation === 'bind' ? 'owner' : record.operation === 'release' ? 'release' : 'transfer';
676
+ const saved = record.files[key];
677
+ if (saved && saved.raw === current.content && saved.hash === current.sha256)
678
+ return;
679
+ if (record.operation !== 'bind' && this.local() !== record.owner_aid && record.local_decision === 'accept') {
680
+ const owner = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, originalOwnerAid);
681
+ const previous = original.exists ? parseOwnershipFile(original.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: originalOwnerAid }) : undefined;
682
+ const sameStatement = previous?.statement && Object.keys(previous.statement).every((field) => previous.statement[field] === owner.statement?.[field]);
683
+ const allowed = sameStatement && (record.operation === 'release' ? owner.action === 'release' : owner.action === 'transfer_owner' && owner.transfer?.new_owner_aid === record.new_owner_aid && owner.transfer?.target_relation_version === record.target_relation_version);
684
+ if (allowed && !saved) {
685
+ if (record.operation === 'transfer')
686
+ await this.deps.files.requireTransfer(this.fileTask(handle));
687
+ record.files[key] = { raw: current.content, hash: current.sha256, condition: {}, original_owner: original, ...(owner.transfer ? { expires_at: owner.transfer.expires_at } : {}) };
688
+ await this.save(handle);
689
+ return;
690
+ }
691
+ }
692
+ throw ownershipFailure('conflict', 'the original owner statement was replaced');
693
+ }
694
+ assertAgentBaseMaterial(record, ns) {
695
+ const base = record.base_agent_md;
696
+ if (!base)
697
+ throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
698
+ const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
699
+ 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;
700
+ if (ns.exists !== base.exists || !same)
701
+ throw ownershipFailure('conflict', 'the original ownership statement was replaced');
702
+ }
703
+ async processInbox(handle) {
704
+ const record = handle.record;
705
+ while (record.inbox.length) {
706
+ const item = record.inbox[0];
707
+ const ns = await this.readNs(record.sub_aid);
708
+ if (record.request_verified && ns.relation_version === record.base_relation_version && !['relation.confirmed', 'sub.cleaned'].includes(item.payload.action))
709
+ await this.assertBaseMaterial(handle, ns);
710
+ try {
711
+ parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record, ns });
712
+ }
713
+ catch (error) {
714
+ record.inbox.shift();
715
+ record.received.push(`${item.from}|${item.payload.operation_id}|${item.payload.action}`);
716
+ if (record.status === 'pending' && ['bind.request', 'transfer.request'].includes(item.payload.action)) {
717
+ record.status = 'stopped';
718
+ record.step = 'conflict';
719
+ record.retryable = false;
720
+ }
721
+ await this.save(handle);
722
+ this.emit(record, 'ownership.error', { code: error instanceof StateError ? error.stringCode : 'invalid', reason: error instanceof Error ? error.message : String(error), retryable: false });
723
+ continue;
724
+ }
725
+ const action = item.payload.action;
726
+ if (['bind.request', 'transfer.request'].includes(action)) {
727
+ if (!record.request_verified)
728
+ await this.captureBase(record, ns);
729
+ record.request_verified = true;
730
+ }
731
+ if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action)) {
732
+ if (record.approvals[item.from] === 'pending' && record.status === 'pending') {
733
+ const rejected = action.endsWith('.reject');
734
+ if (rejected) {
735
+ try {
736
+ if (await this.confirm(handle)) {
737
+ record.inbox.shift();
738
+ record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
739
+ await this.save(handle);
740
+ continue;
741
+ }
742
+ const observed = await this.deps.files.checkConsensus(record.sub_aid, { owner_aid: targetOwner(record), relation_version: record.target_relation_version });
743
+ const targetReached = observed.status === 'consensus' && observed.owner_aid === targetOwner(record) && observed.relation_version === record.target_relation_version;
744
+ if (targetReached) {
745
+ record.code = 'unavailable';
746
+ record.reason = 'ownership target is committed but local evidence is incomplete';
747
+ record.retryable = true;
748
+ record.inbox.shift();
749
+ record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
750
+ await this.save(handle);
751
+ continue;
752
+ }
753
+ }
754
+ catch (error) {
755
+ if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
756
+ throw error;
757
+ try {
758
+ const ns = await this.readNs(record.sub_aid);
759
+ if (ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record)) {
760
+ record.code = 'unavailable';
761
+ record.reason = 'ownership target is committed but local evidence is incomplete';
762
+ record.retryable = true;
763
+ record.inbox.shift();
764
+ record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
765
+ await this.save(handle);
766
+ continue;
767
+ }
768
+ }
769
+ catch (observationError) {
770
+ if (observationError instanceof StateError && observationError.stringCode === 'persistence_unavailable')
771
+ throw observationError;
772
+ }
773
+ record.code = error instanceof StateError ? error.stringCode : 'unavailable';
774
+ record.reason = error instanceof Error ? error.message : String(error);
775
+ record.retryable = true;
776
+ await this.save(handle);
777
+ throw error;
778
+ }
779
+ }
780
+ record.approvals[item.from] = rejected ? 'reject' : 'accept';
781
+ if (rejected) {
782
+ record.status = 'rejected';
783
+ record.step = 'approval';
784
+ record.retryable = false;
785
+ if (item.payload.reason !== undefined)
786
+ record.reason = item.payload.reason;
787
+ if (record.operation === 'transfer')
788
+ for (const to of participants(record))
789
+ if (to !== item.from)
790
+ this.enqueue(record, to, 'operation.stopped', item.payload.reason);
791
+ }
792
+ }
793
+ }
794
+ if (action === 'operation.stopped' && record.status === 'pending' && !(await this.confirm(handle))) {
795
+ record.status = 'stopped';
796
+ record.step = 'stopped';
797
+ record.retryable = false;
798
+ }
799
+ if (action === 'sub.cleaned') {
800
+ const file = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid });
801
+ if (!file.statement)
802
+ record.cleanup_status = 'cleaned';
803
+ }
804
+ if (record.operation === 'release' && !record.request_verified) {
805
+ if (ns.relation_version === 0) {
806
+ const authorizationId = this.cleanupEvidence(record, { agent_md_etag: ns.etag, agent_md_sha256: ownershipSha256(ns.content), owner_md_sha256: '' }, ns.content);
807
+ if (!authorizationId)
808
+ throw ownershipFailure('conflict', 'no local cleanup evidence can verify the release request');
809
+ const authorization = this.load(authorizationId).record;
810
+ const saved = authorization.files.agent;
811
+ const statement = parseOwnershipFile(saved.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
812
+ const etag = saved.published_etag ?? authorization.completion_evidence?.agent_md_etag;
813
+ if (!etag)
814
+ throw ownershipFailure('persistence_unavailable', 'historical approved agent.md ETag is missing');
815
+ record.base_agent_md = { exists: true, raw: saved.raw, sha256: saved.hash, etag, statement };
816
+ const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
817
+ if (owner.exists)
818
+ await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
819
+ record.base_owner_md = { exists: owner.exists, raw: owner.content, sha256: owner.exists ? ownershipSha256(owner.content) : '' };
820
+ record.cleanup_authorization_id = authorizationId;
821
+ if (!(await this.confirm(handle)))
822
+ throw ownershipFailure('conflict', 'no local cleanup evidence can verify the release request');
823
+ }
824
+ else
825
+ await this.captureBase(record, ns);
826
+ record.request_verified = true;
827
+ }
828
+ record.inbox.shift();
829
+ record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
830
+ const request = record.request_verified && record.local_decision === 'pending' && record.operation !== 'release' && !record.request_notified;
831
+ if (request)
832
+ record.request_notified = true;
833
+ await this.save(handle);
834
+ if (request)
835
+ this.emit(record, 'ownership.request');
836
+ if (record.status === 'rejected')
837
+ this.emit(record, 'ownership.rejected');
838
+ if (record.status === 'stopped')
839
+ this.emit(record, 'ownership.stopped');
840
+ }
841
+ }
842
+ fileTask(handle) {
843
+ const record = handle.record;
844
+ const task = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid,
845
+ new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
846
+ 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,
847
+ persist: async () => {
848
+ record.authorization_sha256 = task.authorization_sha256;
849
+ record.expires_at = task.expires_at;
850
+ if (record.operation === 'transfer' && task.authorization_sha256 !== undefined && task.expires_at !== undefined && record.step === 'approval')
851
+ record.step = this.local() === targetOwner(record) ? 'owner' : 'agent';
852
+ await this.save(handle);
853
+ } };
854
+ return task;
855
+ }
856
+ async drive(handle) {
857
+ const record = handle.record;
858
+ if (record.status === 'error' && record.retryable) {
859
+ record.status = 'pending';
860
+ await this.save(handle);
861
+ }
862
+ try {
863
+ await this.processInbox(handle);
864
+ await this.flush(handle);
865
+ if (record.status === 'rejected' || record.status === 'stopped' || record.status === 'error')
866
+ return;
867
+ if (!record.request_verified)
868
+ return;
869
+ if (record.status === 'completed') {
870
+ const observed = await this.deps.files.checkConsensus(record.sub_aid);
871
+ await this.resumeSubCleanup(handle, observed);
872
+ if (await this.confirm(handle, observed))
873
+ await this.cleanup(handle);
874
+ await this.flush(handle);
875
+ return;
876
+ }
877
+ if (record.operation === 'release') {
878
+ if (await this.confirm(handle)) {
879
+ await this.cleanup(handle);
880
+ await this.flush(handle);
881
+ return;
882
+ }
883
+ if (this.local() === record.owner_aid) {
884
+ record.step = 'release';
885
+ await this.save(handle);
886
+ await this.deps.files.revokeSubaid(this.fileTask(handle));
887
+ }
888
+ if (await this.confirm(handle))
889
+ await this.cleanup(handle);
890
+ await this.flush(handle);
891
+ return;
892
+ }
893
+ if (record.local_decision !== 'accept')
894
+ return;
895
+ if (await this.confirm(handle)) {
896
+ await this.cleanup(handle);
897
+ await this.flush(handle);
898
+ return;
899
+ }
900
+ const ns = await this.readNs(record.sub_aid);
901
+ 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)) {
902
+ throw ownershipFailure('conflict', 'the original relation was replaced');
903
+ }
904
+ await this.assertBaseMaterial(handle, ns);
905
+ if (record.expires_at !== undefined && record.expires_at <= Math.trunc(Date.now() / 1000))
906
+ throw ownershipFailure('conflict', 'transfer authorization expired');
907
+ const approved = participants(record).every((name) => record.approvals[name] === 'accept');
908
+ if (record.operation === 'bind' && !approved || record.operation === 'transfer' && this.local() === record.owner_aid && !approved)
909
+ return;
910
+ delete record.code;
911
+ delete record.reason;
912
+ record.retryable = true;
913
+ if (record.operation === 'transfer' && this.local() === record.owner_aid) {
914
+ record.step = 'transfer';
915
+ await this.save(handle);
916
+ await this.deps.files.transferOwneraid(this.fileTask(handle));
917
+ for (const to of participants(record))
918
+ this.enqueue(record, to, 'transfer.ready');
919
+ await this.save(handle);
920
+ await this.flush(handle);
921
+ }
922
+ if (record.operation === 'transfer') {
923
+ const original = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
924
+ if (original.exists && (await this.deps.files.verifyFile(original.content, 'owner', record.sub_aid, record.owner_aid)).action === 'declare') {
925
+ if (record.authorization_sha256 !== undefined)
926
+ throw ownershipFailure('conflict', 'transfer authorization was withdrawn');
927
+ return;
928
+ }
929
+ await this.deps.files.requireTransfer(this.fileTask(handle));
930
+ }
931
+ if (this.local() === targetOwner(record)) {
932
+ record.step = 'owner';
933
+ await this.save(handle);
934
+ await this.deps.files.declareSubaid(this.fileTask(handle));
935
+ this.enqueue(record, record.sub_aid, 'owner.ready');
936
+ await this.save(handle);
937
+ await this.flush(handle);
938
+ }
939
+ if (this.local() === record.sub_aid) {
940
+ record.step = 'agent';
941
+ await this.save(handle);
942
+ await this.deps.files.declareOwneraid(this.fileTask(handle));
943
+ }
944
+ const completed = await this.confirm(handle);
945
+ await this.flush(handle);
946
+ if (completed)
947
+ await this.cleanup(handle);
948
+ }
949
+ catch (error) {
950
+ await this.recordError(handle, error);
951
+ }
952
+ }
953
+ async confirm(handle, observed) {
954
+ const record = handle.record;
955
+ const result = observed ?? await this.deps.files.checkConsensus(record.sub_aid, record.operation === 'release' ? undefined : { owner_aid: targetOwner(record), relation_version: record.target_relation_version });
956
+ if (result.status === 'invalid' || result.status === 'unavailable')
957
+ throw ownershipFailure(result.code ?? result.status, result.reason ?? 'ownership consensus cannot be verified', result.retryable);
958
+ if (record.operation === 'release' && record.status === 'completed' && result.status === 'unbound' && result.relation_version === 0)
959
+ return true;
960
+ let cleaned = false;
961
+ if (record.operation === 'release' && result.status === 'unbound' && result.relation_version === 0) {
962
+ const began = Date.now();
963
+ const ns = await this.readNs(record.sub_aid);
964
+ if (!ns.exists || !ns.etag)
965
+ throw ownershipFailure('conflict', 'current signed cleanup document is missing');
966
+ if (ns.etag !== result.agent_md_etag || ownershipSha256(ns.content) !== result.agent_md_sha256)
967
+ throw ownershipFailure('unavailable', 'agent.md changed during cleanup confirmation', true);
968
+ if (record.local_roles.includes('owner') && record.files.release) {
969
+ const source = record.files.release, owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
970
+ if (owner.exists && owner.content === source.raw && owner.sha256 === source.hash) {
971
+ await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
972
+ const after = await this.readNs(record.sub_aid);
973
+ if (after.etag !== ns.etag || after.content !== ns.content || after.relation_version !== 0 || after.lock !== 'unlocked')
974
+ throw ownershipFailure('unavailable', 'agent.md changed during release confirmation', true);
975
+ result.owner_md_sha256 = owner.sha256;
976
+ }
977
+ }
978
+ const authorization = this.cleanupEvidence(record, result, ns.content);
979
+ if (!authorization)
980
+ throw ownershipFailure('conflict', 'no local cleanup original can confirm this release task');
981
+ if (Date.now() - began > 30_000)
982
+ throw ownershipFailure('unavailable', 'cleanup confirmation verification window elapsed', true);
983
+ if (authorization !== record.operation_id)
984
+ record.cleanup_authorization_id = authorization;
985
+ record.local_decision = 'accept';
986
+ record.approvals[this.local()] = 'accept';
987
+ record.cleanup_status = 'cleaned';
988
+ cleaned = true;
989
+ }
990
+ const reached = cleaned || result.relation_version === record.target_relation_version && (record.operation === 'release' ? result.status === 'released' && result.lock === 'unlocked' : result.status === 'consensus' && result.owner_aid === targetOwner(record));
991
+ if (!reached)
992
+ return false;
993
+ if (record.operation === 'release' && !cleaned) {
994
+ const ns = await this.readNs(record.sub_aid);
995
+ const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement;
996
+ if (ns.etag !== result.agent_md_etag || ownershipSha256(ns.content) !== result.agent_md_sha256)
997
+ throw ownershipFailure('unavailable', 'agent.md changed during release confirmation', true);
998
+ if (statement?.owner_aid !== record.owner_aid || statement.relation_version !== record.base_relation_version)
999
+ return false;
1000
+ if (record.base_agent_md)
1001
+ this.assertAgentBaseMaterial(record, ns);
1002
+ if (record.files.release && result.owner_md_sha256 && record.files.release.hash !== result.owner_md_sha256)
1003
+ throw ownershipFailure('conflict', 'current release material does not belong to this task');
1004
+ }
1005
+ if (record.operation === 'release' && record.local_roles.includes('owner') && !record.files.release)
1006
+ throw ownershipFailure('conflict', 'local release file evidence is missing');
1007
+ if (record.operation === 'transfer' && record.local_roles.includes('owner') && !record.files.transfer)
1008
+ throw ownershipFailure('conflict', 'local transfer file evidence is missing');
1009
+ if (record.operation === 'release' && this.local() === record.sub_aid && record.local_decision !== 'accept') {
1010
+ const authorization = await this.cleanupAuthorization(record);
1011
+ if (authorization) {
1012
+ record.local_decision = 'accept';
1013
+ record.approvals[this.local()] = 'accept';
1014
+ record.cleanup_authorization_id = authorization.operation_id;
1015
+ await this.save(handle);
1016
+ }
1017
+ const ns = await this.deps.files.readAgent(record.sub_aid);
1018
+ if (!authorization && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement) {
1019
+ record.step = 'cleanup';
1020
+ if (record.cleanup_status !== 'action_required') {
1021
+ record.cleanup_status = 'action_required';
1022
+ await this.save(handle);
1023
+ this.emit(record, 'ownership.action_required');
1024
+ }
1025
+ return false;
1026
+ }
1027
+ }
1028
+ 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)))
1029
+ return false;
1030
+ 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)
1031
+ throw ownershipFailure('conflict', 'published agent.md ETag changed');
1032
+ if (record.status !== 'completed') {
1033
+ record.status = 'completed';
1034
+ record.step = 'confirmed';
1035
+ record.verified_relation_version = record.target_relation_version;
1036
+ record.completion_evidence = { ...result };
1037
+ delete record.code;
1038
+ delete record.reason;
1039
+ record.retryable = false;
1040
+ if (record.operation === 'release' && this.local() === record.owner_aid)
1041
+ this.enqueue(record, record.sub_aid, 'relation.confirmed');
1042
+ else if (record.operation !== 'release' && this.local() === record.sub_aid)
1043
+ for (const to of participants(record))
1044
+ this.enqueue(record, to, 'relation.confirmed');
1045
+ await this.save(handle);
1046
+ this.emit(record, 'ownership.completed');
1047
+ }
1048
+ return true;
1049
+ }
1050
+ cleanupEvidence(record, evidence, raw) {
1051
+ const release = record.files.release;
1052
+ if (record.local_roles.includes('owner') && release && release.hash === evidence.owner_md_sha256)
1053
+ return record.operation_id;
1054
+ if (!record.local_roles.includes('sub'))
1055
+ return undefined;
1056
+ const matches = (saved) => Boolean(saved && saved.hash === evidence.agent_md_sha256 && saved.hash === ownershipSha256(saved.raw) && (saved.published_etag === undefined || saved.published_etag === evidence.agent_md_etag) && (raw === undefined || raw === saved.raw));
1057
+ if (matches(record.files.agent))
1058
+ return record.operation_id;
1059
+ for (const snapshot of this.list(true)) {
1060
+ if (!['bind', 'transfer'].includes(String(snapshot.task.operation)) || record.cleanup_authorization_id && snapshot.operation_id !== record.cleanup_authorization_id)
1061
+ continue;
1062
+ let candidate;
1063
+ try {
1064
+ candidate = this.decode(snapshot).record;
1065
+ }
1066
+ catch (error) {
1067
+ if (snapshot.task.sub_aid === record.sub_aid)
1068
+ throw error;
1069
+ else
1070
+ continue;
1071
+ }
1072
+ if (candidate.local_decision !== 'accept' || !candidate.local_roles.includes('sub') || candidate.sub_aid !== record.sub_aid || targetOwner(candidate) !== record.owner_aid || candidate.target_relation_version !== record.base_relation_version || !matches(candidate.release_cleanup?.files.agent))
1073
+ continue;
1074
+ const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
1075
+ if (statement?.owner_aid === record.owner_aid && statement.relation_version === record.base_relation_version && (!record.base_agent_md?.statement || Object.keys(record.base_agent_md.statement).every((field) => record.base_agent_md.statement[field] === statement[field])))
1076
+ return candidate.operation_id;
1077
+ }
1078
+ return undefined;
1079
+ }
1080
+ async resumeSubCleanup(handle, observed) {
1081
+ const record = handle.record;
1082
+ if (!['bind', 'transfer'].includes(record.operation) || !record.local_roles.includes('sub') || record.local_decision !== 'accept' || record.release_cleanup?.cleaned)
1083
+ return;
1084
+ if (record.release_cleanup && observed.status === 'unbound' && observed.relation_version === 0) {
1085
+ const saved = record.release_cleanup.files.agent;
1086
+ const ns = await this.readNs(record.sub_aid);
1087
+ if (!saved || saved.raw !== ns.content || saved.hash !== observed.agent_md_sha256 || saved.hash !== ownershipSha256(ns.content) || ns.etag !== observed.agent_md_etag || saved.published_etag !== undefined && saved.published_etag !== ns.etag)
1088
+ throw ownershipFailure('conflict', 'historical cleanup original does not match current material');
1089
+ saved.published_etag = ns.etag;
1090
+ record.release_cleanup.cleaned = true;
1091
+ await this.save(handle);
1092
+ return;
1093
+ }
1094
+ if (observed.status !== 'released' || observed.relation_version !== record.target_relation_version)
1095
+ return;
1096
+ const ns = await this.readNs(record.sub_aid);
1097
+ if (!ns.exists || ns.lock !== 'unlocked' || ns.relation_version !== observed.relation_version || ns.etag !== observed.agent_md_etag || ownershipSha256(ns.content) !== observed.agent_md_sha256)
1098
+ throw ownershipFailure('unavailable', 'NS changed during historical cleanup observation', true);
1099
+ const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement;
1100
+ const original = record.files.agent && parseOwnershipFile(record.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
1101
+ if (!statement || !original || Object.keys(original).some((field) => original[field] !== statement[field]))
1102
+ throw ownershipFailure('conflict', 'historical cleanup fields belong to another relation');
1103
+ if (!record.release_cleanup) {
1104
+ record.release_cleanup = { files: {}, cleaned: false };
1105
+ await this.save(handle);
1106
+ }
1107
+ await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: record.sub_aid, owner_aid: targetOwner(record),
1108
+ base_relation_version: record.target_relation_version, target_relation_version: observed.relation_version,
1109
+ files: record.release_cleanup.files, expected_agent_statement: original, persist: () => this.save(handle) });
1110
+ record.release_cleanup.cleaned = true;
1111
+ await this.save(handle);
1112
+ this.emit(record, 'ownership.progress');
1113
+ }
1114
+ async cleanupAuthorization(task) {
1115
+ const ns = await this.readNs(task.sub_aid);
1116
+ const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: task.sub_aid }).statement : undefined;
1117
+ if (!statement)
1118
+ return undefined;
1119
+ for (const snapshot of this.list(true)) {
1120
+ if (snapshot.task.sub_aid !== task.sub_aid)
1121
+ continue;
1122
+ const candidate = this.decode(snapshot).record;
1123
+ if (['bind', 'transfer'].includes(candidate.operation) && 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 && candidate.files.agent) {
1124
+ const original = parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
1125
+ const fixed = task.base_agent_md?.statement;
1126
+ if (original && Object.keys(original).every((field) => original[field] === statement[field]) && (!fixed || Object.keys(fixed).every((field) => fixed[field] === original[field])))
1127
+ return candidate;
1128
+ }
1129
+ }
1130
+ return undefined;
1131
+ }
1132
+ async cleanup(handle) {
1133
+ const record = handle.record;
1134
+ if (record.operation === 'bind')
1135
+ return;
1136
+ if (record.operation === 'release' && this.local() === record.sub_aid && record.cleanup_status !== 'cleaned') {
1137
+ const ns = await this.readNs(record.sub_aid);
1138
+ if (ns.exists && ns.lock === 'unlocked' && ns.relation_version === 0 && !parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement) {
1139
+ if (!this.cleanupEvidence(record, { agent_md_sha256: ownershipSha256(ns.content), agent_md_etag: ns.etag, owner_md_sha256: '' }, ns.content))
1140
+ throw ownershipFailure('conflict', 'current cleanup does not match a locally saved original');
1141
+ record.cleanup_status = 'cleaned';
1142
+ await this.save(handle);
1143
+ }
1144
+ else {
1145
+ const authorization = await this.cleanupAuthorization(record);
1146
+ if (!authorization || record.cleanup_authorization_id && authorization.operation_id !== record.cleanup_authorization_id) {
1147
+ if (record.cleanup_status !== 'action_required') {
1148
+ record.cleanup_status = 'action_required';
1149
+ await this.save(handle);
1150
+ this.emit(record, 'ownership.action_required');
1151
+ }
1152
+ }
1153
+ else {
1154
+ record.cleanup_authorization_id = authorization.operation_id;
1155
+ await this.save(handle);
1156
+ const task = this.fileTask(handle);
1157
+ task.expected_agent_statement = parseOwnershipFile(authorization.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
1158
+ await this.deps.files.revokeOwneraid(task);
1159
+ record.cleanup_status = 'cleaned';
1160
+ this.enqueue(record, record.owner_aid, 'sub.cleaned');
1161
+ await this.save(handle);
1162
+ }
1163
+ }
1164
+ }
1165
+ if (this.local() === record.owner_aid && !record.files.cleanup?.deleted)
1166
+ await this.deps.files.cleanupOwnerFile(this.fileTask(handle));
1167
+ }
1168
+ emit(record, event, extra = {}) {
1169
+ const payload = { ...ownershipTaskResult(record), local_roles: [...record.local_roles], ...extra };
1170
+ if (event === 'ownership.request')
1171
+ payload.requester_aid = record.initiator_aid;
1172
+ if (record.verified_relation_version !== undefined)
1173
+ payload.relation_version = record.verified_relation_version;
1174
+ queueMicrotask(() => { try {
1175
+ const pending = this.deps.emit(event, payload);
1176
+ if (pending)
1177
+ void pending.catch(() => { });
1178
+ }
1179
+ catch { } });
1180
+ }
1181
+ }
1182
+ //# sourceMappingURL=ownership-workflow.js.map