@agentunion/fastaun 0.5.20 → 0.5.22
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.
- package/CHANGELOG.md +104 -0
- package/_packed_docs/CHANGELOG.md +104 -0
- package/_packed_docs/INDEX.md +59 -34
- package/_packed_docs/KITE_DOCS_GUIDE.md +59 -52
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/344/270/216/346/215/242owner/347/274/226/346/216/222/346/226/271/346/241/210.md +530 -0
- package/_packed_docs/agent.md/working/AID/347/261/273/345/236/213-owner/345/205/263/347/263/273/345/256/214/346/225/264/350/256/276/350/256/241/344/270/216/345/256/236/346/226/275/350/256/241/345/210/222.md +685 -0
- package/_packed_docs/agent.md/working/agent.md-schema/344/270/216/347/255/276/345/217/221/351/252/214/347/255/276/346/265/201/347/250/213/346/276/204/346/270/205.md +349 -0
- package/_packed_docs/agent.md/working//346/234/254/345/234/260Docker/346/265/213/350/257/225Bug/346/270/205/345/215/225.md +40 -0
- 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
- 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
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +1433 -1392
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +2 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +33 -22
- package/_packed_docs/sdk/0.5.21-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +368 -0
- package/_packed_docs/sdk/0.5.21-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +350 -0
- 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
- 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
- package/_packed_docs/sdk/01-/345/277/253/351/200/237/345/274/200/345/247/213.md +1 -1
- package/_packed_docs/sdk/02-WebSocket/345/215/217/350/256/256.md +5 -1
- package/_packed_docs/sdk/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +64 -64
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +4 -1
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +42 -39
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +163 -14
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +48 -47
- package/_packed_docs/sdk/09-group-rpc-manual.md +15 -2
- package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +73 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +90 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +33 -25
- package/_packed_docs/sdk/INDEX.md +67 -33
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +74 -57
- 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 -300
- 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 -382
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -708
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +28 -20
- 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
- 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
- package/dist/agent-md-schema.js +6 -4
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +22 -2
- package/dist/agent-md.js +92 -68
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +13 -0
- package/dist/aid-store.js +50 -15
- package/dist/aid-store.js.map +1 -1
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +22 -2
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.js +13 -0
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +87 -1
- package/dist/client/delivery.js +1387 -139
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +3 -0
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.js +149 -37
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +12 -2
- package/dist/client/v2-e2ee.js +831 -304
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +26 -2
- package/dist/client.js +412 -68
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +18 -1
- package/dist/events.js +148 -9
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +43 -1
- package/dist/facades.js +363 -7
- package/dist/facades.js.map +1 -1
- package/dist/group-fs.d.ts +1 -0
- package/dist/group-fs.js +37 -19
- package/dist/group-fs.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +6 -1
- package/dist/keystore/aid-db.js +184 -42
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +14 -0
- package/dist/keystore/local-token-store.d.ts +9 -1
- package/dist/keystore/local-token-store.js +79 -14
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +46 -0
- package/dist/org-protocol.js +802 -12
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +36 -5
- package/dist/ownership-v21.js +168 -52
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +24 -2
- package/dist/ownership-workflow.js +682 -41
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/secret-store/file-store.js +84 -0
- package/dist/secret-store/file-store.js.map +1 -1
- package/dist/seq-tracker.d.ts +1 -0
- package/dist/seq-tracker.js +2 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +6 -0
- package/dist/storage/lowlevel.js +24 -7
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/read-cache.d.ts +70 -0
- package/dist/storage/read-cache.js +625 -0
- package/dist/storage/read-cache.js.map +1 -0
- package/dist/storage/vfs.d.ts +2 -0
- package/dist/storage/vfs.js +50 -15
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +86 -3
- package/dist/tools/cross-sdk-agent.js +758 -46
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.js +104 -46
- package/dist/transport.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/v2/e2ee/encrypt-group.js +1 -0
- package/dist/v2/e2ee/encrypt-group.js.map +1 -1
- package/dist/v2/e2ee/encrypt-p2p.js +1 -0
- package/dist/v2/e2ee/encrypt-p2p.js.map +1 -1
- package/dist/v2/e2ee/types.d.ts +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,13 @@ import { isScalar, parseDocument } from 'yaml';
|
|
|
3
3
|
import { StateError, ValidationError } from './errors.js';
|
|
4
4
|
import { OWNERSHIP_UUID_V4_RE, ownershipAid, ownershipFailure, ownershipInteger, ownershipSha256, parseOwnershipFile, requireOwnershipApplicable } from './ownership-v21.js';
|
|
5
5
|
export const OWNERSHIP_SDK_MESSAGE_TYPE = 'aun.ownership.sdk.v1';
|
|
6
|
+
class OwnershipTaskDecodeError extends StateError {
|
|
7
|
+
operationId;
|
|
8
|
+
constructor(operationId, reason) {
|
|
9
|
+
super(reason, { stringCode: 'persistence_unavailable' });
|
|
10
|
+
this.operationId = operationId;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
6
13
|
function participants(task) {
|
|
7
14
|
return [...new Set([task.owner_aid, task.sub_aid, ...(task.new_owner_aid ? [task.new_owner_aid] : [])])];
|
|
8
15
|
}
|
|
@@ -43,7 +50,8 @@ export function parseOwnershipMessage(content, context) {
|
|
|
43
50
|
ownershipAid(message.sub_aid, 'sub_aid');
|
|
44
51
|
ownershipAid(message.owner_aid, 'owner_aid');
|
|
45
52
|
ownershipInteger(message.base_relation_version, 'base_relation_version', 0);
|
|
46
|
-
|
|
53
|
+
const target = ownershipInteger(message.target_relation_version, 'target_relation_version');
|
|
54
|
+
if (target !== message.base_relation_version + 1 && !(message.operation === 'release' && target === message.base_relation_version))
|
|
47
55
|
throw new ValidationError('ownership target version does not match the operation');
|
|
48
56
|
if (message.operation === 'transfer') {
|
|
49
57
|
if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
|
|
@@ -106,8 +114,8 @@ export function parseOwnershipMessage(content, context) {
|
|
|
106
114
|
const ns = context.ns;
|
|
107
115
|
if (ns.protocol !== '2.1')
|
|
108
116
|
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.
|
|
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
|
+
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);
|
|
111
119
|
if (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target : !base && !target)
|
|
112
120
|
throw ownershipFailure('conflict', 'ownership current NS relation does not match this task');
|
|
113
121
|
}
|
|
@@ -166,6 +174,95 @@ export class OwnershipWorkflow {
|
|
|
166
174
|
throw ownershipFailure('invalid', 'ownership rejection reason must be a string');
|
|
167
175
|
return this.decide(operationId, 'reject', reason);
|
|
168
176
|
}
|
|
177
|
+
async resetOwnershipTask() {
|
|
178
|
+
if (!this.deps.store.loadOwnershipTaskSource || !this.deps.store.resetOwnershipTask)
|
|
179
|
+
throw ownershipFailure('persistence_unavailable', 'atomic ownership task reset storage is unavailable');
|
|
180
|
+
const local = this.local(), check = () => { if (this.local() !== local)
|
|
181
|
+
throw ownershipFailure('conflict', 'ownership reset identity changed'); };
|
|
182
|
+
const reset = [], skipped = [];
|
|
183
|
+
for (const snapshot of this.list(true)) {
|
|
184
|
+
check();
|
|
185
|
+
if (!OWNERSHIP_UUID_V4_RE.test(snapshot.operation_id)) {
|
|
186
|
+
skipped.push(snapshot.operation_id);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
let source = this.taskSource(snapshot.operation_id);
|
|
190
|
+
for (let attempt = 0; source; attempt++) {
|
|
191
|
+
let task;
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(source.source);
|
|
194
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
195
|
+
task = parsed;
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
198
|
+
if (task && Object.hasOwn(task, 'task_reset')) {
|
|
199
|
+
try {
|
|
200
|
+
this.decode({ operation_id: source.operation_id, revision: source.revision, task });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
skipped.push(snapshot.operation_id);
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
let initiator;
|
|
208
|
+
try {
|
|
209
|
+
initiator = ownershipAid(task?.initiator_aid, 'initiator_aid');
|
|
210
|
+
}
|
|
211
|
+
catch { }
|
|
212
|
+
if (!task || !initiator || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(task.status)) {
|
|
213
|
+
skipped.push(snapshot.operation_id);
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
if (initiator !== local || ['completed', 'rejected', 'stopped'].includes(task.status))
|
|
217
|
+
break;
|
|
218
|
+
let subject;
|
|
219
|
+
try {
|
|
220
|
+
subject = ownershipAid(task.sub_aid, 'sub_aid');
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
try {
|
|
224
|
+
const original = source, result = await this.runSerialized(subject ?? source.operation_id, () => { check(); return this.resetSource(original, subject); });
|
|
225
|
+
check();
|
|
226
|
+
if (result.created)
|
|
227
|
+
reset.push(source.operation_id);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
check();
|
|
232
|
+
if (attempt > 0 || !(error instanceof StateError) || error.stringCode !== 'conflict')
|
|
233
|
+
throw error;
|
|
234
|
+
source = this.taskSource(snapshot.operation_id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
check();
|
|
239
|
+
return { reset_operation_ids: [...new Set(reset)].sort(), skipped_operation_ids: [...new Set(skipped)].sort() };
|
|
240
|
+
}
|
|
241
|
+
async resetTask(operationId) {
|
|
242
|
+
if (typeof operationId !== 'string' || !OWNERSHIP_UUID_V4_RE.test(operationId))
|
|
243
|
+
throw ownershipFailure('invalid', 'ownership operation_id is invalid');
|
|
244
|
+
const source = this.taskSource(operationId);
|
|
245
|
+
if (!source)
|
|
246
|
+
throw ownershipFailure('operation_not_found', 'no local ownership task matches operation_id');
|
|
247
|
+
let task = {};
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(source.source);
|
|
250
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
251
|
+
task = parsed;
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
if (Object.hasOwn(task, 'task_reset')) {
|
|
255
|
+
this.decode({ operation_id: operationId, revision: source.revision, task });
|
|
256
|
+
return { operation_id: operationId, reset: true };
|
|
257
|
+
}
|
|
258
|
+
let subject;
|
|
259
|
+
try {
|
|
260
|
+
subject = ownershipAid(task.sub_aid, 'sub_aid');
|
|
261
|
+
}
|
|
262
|
+
catch { }
|
|
263
|
+
await this.runSerialized(subject ?? operationId, () => this.resetSource(source, subject));
|
|
264
|
+
return { operation_id: operationId, reset: true };
|
|
265
|
+
}
|
|
169
266
|
async checkOwnershipConsensus(subAid = this.local()) {
|
|
170
267
|
return this.deps.files.checkConsensus(ownershipAid(subAid, 'sub_aid'));
|
|
171
268
|
}
|
|
@@ -188,8 +285,12 @@ export class OwnershipWorkflow {
|
|
|
188
285
|
return true;
|
|
189
286
|
let message;
|
|
190
287
|
try {
|
|
288
|
+
const saved = this.load(String(body.operation_id));
|
|
289
|
+
if (saved && this.isReset(saved.record))
|
|
290
|
+
return true;
|
|
291
|
+
const task = saved && !saved.record.base_agent_md && saved.record.request_verified && !['completed', 'stopped', 'rejected'].includes(saved.record.status) ? this.legacyTaskView(saved.record) : saved?.record;
|
|
191
292
|
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 :
|
|
293
|
+
task: task ?? (['bind.request', 'transfer.request'].includes(String(body.action)) || body.operation === 'release' && body.action === 'relation.confirmed' ? undefined : saved?.record) });
|
|
193
294
|
}
|
|
194
295
|
catch (error) {
|
|
195
296
|
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
@@ -209,12 +310,14 @@ export class OwnershipWorkflow {
|
|
|
209
310
|
return this.runSerialized(message.sub_aid, async () => {
|
|
210
311
|
let handle = this.load(message.operation_id);
|
|
211
312
|
if (handle) {
|
|
313
|
+
const task = !handle.record.base_agent_md && handle.record.request_verified && !['completed', 'stopped', 'rejected'].includes(handle.record.status) ? this.legacyTaskView(handle.record) : handle.record;
|
|
212
314
|
try {
|
|
213
|
-
parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(), task
|
|
315
|
+
parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(), task });
|
|
214
316
|
}
|
|
215
317
|
catch {
|
|
216
318
|
return true;
|
|
217
319
|
}
|
|
320
|
+
await this.upgradeLegacy(handle);
|
|
218
321
|
}
|
|
219
322
|
else {
|
|
220
323
|
if (!['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
|
|
@@ -249,6 +352,10 @@ export class OwnershipWorkflow {
|
|
|
249
352
|
for (const snapshot of this.list(true)) {
|
|
250
353
|
try {
|
|
251
354
|
const record = this.decode(snapshot).record;
|
|
355
|
+
if (this.isReset(record))
|
|
356
|
+
continue;
|
|
357
|
+
if (!record.base_agent_md && record.request_verified && ['completed', 'stopped', 'rejected'].includes(record.status))
|
|
358
|
+
continue;
|
|
252
359
|
await this.runSerialized(record.sub_aid, async () => {
|
|
253
360
|
const handle = this.load(record.operation_id);
|
|
254
361
|
await this.drive(handle);
|
|
@@ -290,11 +397,116 @@ export class OwnershipWorkflow {
|
|
|
290
397
|
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
291
398
|
}
|
|
292
399
|
}
|
|
400
|
+
taskSource(operationId) {
|
|
401
|
+
try {
|
|
402
|
+
if (!this.deps.store.loadOwnershipTaskSource || !this.deps.store.resetOwnershipTask)
|
|
403
|
+
throw new Error('atomic ownership task reset storage is unavailable');
|
|
404
|
+
return this.deps.store.loadOwnershipTaskSource(this.local(), operationId);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async resetSource(source, subject) {
|
|
411
|
+
const task = { protocol: '2.1', operation_id: source.operation_id, local_aid: this.local(), status: 'stopped', step: 'reset',
|
|
412
|
+
...(subject ? { sub_aid: subject } : {}), task_reset: { source: source.source, source_sha256: ownershipSha256(source.source), revision: source.revision, reset_at: Math.trunc(Date.now() / 1000) } };
|
|
413
|
+
try {
|
|
414
|
+
const saved = this.deps.store.resetOwnershipTask(this.local(), source.operation_id, source.revision, source.source, task);
|
|
415
|
+
this.decode(saved);
|
|
416
|
+
if (!this.isReset(saved.task))
|
|
417
|
+
throw new Error('ownership reset marker was not saved');
|
|
418
|
+
const committed = this.taskSource(source.operation_id);
|
|
419
|
+
if (!committed || committed.revision !== saved.revision || JSON.stringify(JSON.parse(committed.source)) !== JSON.stringify(saved.task))
|
|
420
|
+
throw new Error('ownership reset marker readback failed');
|
|
421
|
+
return { operation_id: source.operation_id, reset: true, created: true };
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
if (!(error instanceof Error) || !/snapshot|revision conflict|already reset/.test(error.message))
|
|
425
|
+
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
426
|
+
const winner = this.taskSource(source.operation_id);
|
|
427
|
+
if (winner) {
|
|
428
|
+
let parsed;
|
|
429
|
+
try {
|
|
430
|
+
parsed = JSON.parse(winner.source);
|
|
431
|
+
}
|
|
432
|
+
catch { }
|
|
433
|
+
if (parsed && typeof parsed === 'object' && Object.hasOwn(parsed, 'task_reset')) {
|
|
434
|
+
this.decode({ operation_id: winner.operation_id, revision: winner.revision, task: parsed });
|
|
435
|
+
return { operation_id: source.operation_id, reset: true, created: false };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
throw ownershipFailure(error instanceof Error && /revision|snapshot|conflict/.test(error.message) ? 'conflict' : 'persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
isReset(record) { return Object.hasOwn(record, 'task_reset'); }
|
|
442
|
+
async resetUnusableTasks(subAid, ownerAid) {
|
|
443
|
+
for (const snapshot of this.list(true)) {
|
|
444
|
+
if (this.isReset(snapshot.task)) {
|
|
445
|
+
this.decode(snapshot);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (snapshot.task.sub_aid !== subAid)
|
|
449
|
+
continue;
|
|
450
|
+
if (!this.isReset(snapshot.task) && ['completed', 'rejected', 'stopped'].includes(snapshot.task.status))
|
|
451
|
+
continue;
|
|
452
|
+
let record;
|
|
453
|
+
let unusable = false;
|
|
454
|
+
try {
|
|
455
|
+
record = this.decode(snapshot).record;
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
if (this.isReset(snapshot.task) || !(error instanceof OwnershipTaskDecodeError) || error.operationId !== snapshot.operation_id)
|
|
459
|
+
throw error;
|
|
460
|
+
unusable = true;
|
|
461
|
+
record = snapshot.task;
|
|
462
|
+
}
|
|
463
|
+
if (!unusable && !this.isReset(record) && !record.base_agent_md && record.request_verified && !['completed', 'stopped', 'rejected'].includes(record.status) && record.operation === 'bind' && record.owner_aid === ownerAid && record.initiator_aid === this.local()) {
|
|
464
|
+
try {
|
|
465
|
+
this.legacyTaskView(record);
|
|
466
|
+
}
|
|
467
|
+
catch {
|
|
468
|
+
unusable = true;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (!unusable)
|
|
472
|
+
continue;
|
|
473
|
+
const source = this.taskSource(snapshot.operation_id);
|
|
474
|
+
if (!source || source.revision !== snapshot.revision)
|
|
475
|
+
throw ownershipFailure('conflict', 'ownership task advanced before automatic reset');
|
|
476
|
+
let parsed;
|
|
477
|
+
try {
|
|
478
|
+
parsed = JSON.parse(source.source);
|
|
479
|
+
}
|
|
480
|
+
catch { }
|
|
481
|
+
if (!parsed || typeof parsed !== 'object' || parsed.sub_aid !== subAid)
|
|
482
|
+
throw ownershipFailure('persistence_unavailable', 'ownership task subject cannot be established for automatic reset');
|
|
483
|
+
if (Object.hasOwn(parsed, 'task_reset'))
|
|
484
|
+
throw ownershipFailure('persistence_unavailable', 'ownership reset marker is corrupt');
|
|
485
|
+
if (['completed', 'rejected', 'stopped'].includes(parsed.status))
|
|
486
|
+
continue;
|
|
487
|
+
await this.resetSource(source, subAid);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
293
490
|
decode(snapshot) {
|
|
294
491
|
const record = snapshot.task;
|
|
295
492
|
try {
|
|
493
|
+
if (this.isReset(record)) {
|
|
494
|
+
const task = snapshot.task, reset = task.task_reset;
|
|
495
|
+
if (task.protocol !== '2.1' || task.operation_id !== snapshot.operation_id || !OWNERSHIP_UUID_V4_RE.test(snapshot.operation_id) || task.local_aid !== this.local() || task.status !== 'stopped' || task.step !== 'reset' || Object.keys(task).some((key) => !['protocol', 'operation_id', 'local_aid', 'status', 'step', 'sub_aid', 'task_reset'].includes(key)) || !reset || typeof reset !== 'object' || Array.isArray(reset) || Object.keys(reset).length !== 4 || typeof reset.source !== 'string' || reset.source_sha256 !== ownershipSha256(reset.source) || !Number.isSafeInteger(reset.revision) || Number(reset.revision) < 1 || Number(reset.revision) + 1 !== snapshot.revision || !Number.isSafeInteger(reset.reset_at) || Number(reset.reset_at) <= 0)
|
|
496
|
+
throw new Error('ownership reset marker is corrupt');
|
|
497
|
+
let subject;
|
|
498
|
+
try {
|
|
499
|
+
subject = ownershipAid(JSON.parse(reset.source).sub_aid, 'sub_aid');
|
|
500
|
+
}
|
|
501
|
+
catch { }
|
|
502
|
+
if (task.sub_aid !== subject)
|
|
503
|
+
throw new Error('ownership reset subject changed');
|
|
504
|
+
return { record, revision: snapshot.revision };
|
|
505
|
+
}
|
|
296
506
|
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
507
|
throw new Error('invalid ownership task');
|
|
508
|
+
if (!record.base_agent_md && record.request_verified && !record.legacy_task)
|
|
509
|
+
return { record, revision: snapshot.revision };
|
|
298
510
|
ownershipAid(record.sub_aid, 'sub_aid');
|
|
299
511
|
ownershipAid(record.owner_aid, 'owner_aid');
|
|
300
512
|
ownershipAid(record.initiator_aid, 'initiator_aid');
|
|
@@ -304,28 +516,82 @@ export class OwnershipWorkflow {
|
|
|
304
516
|
}
|
|
305
517
|
else if (record.new_owner_aid !== undefined)
|
|
306
518
|
throw new Error('unexpected transfer target');
|
|
307
|
-
|
|
519
|
+
const baseVersion = ownershipInteger(record.base_relation_version, 'base_relation_version', 0), targetVersion = ownershipInteger(record.target_relation_version, 'target_relation_version');
|
|
520
|
+
if (targetVersion !== baseVersion + 1 && !(record.operation === 'release' && targetVersion === baseVersion) || !participants(record).includes(record.initiator_aid) || record.operation !== 'bind' && record.initiator_aid !== record.owner_aid)
|
|
308
521
|
throw new Error('invalid fixed ownership context');
|
|
309
522
|
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
523
|
throw new Error('ownership approval or pending state is corrupt');
|
|
311
524
|
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
525
|
throw new Error('ownership initiator or approval participants are corrupt');
|
|
313
|
-
if (record.
|
|
526
|
+
if (record.legacy_recovery !== undefined) {
|
|
527
|
+
const recovery = record.legacy_recovery, original = recovery.task, checkpoint = recovery.checkpoint;
|
|
528
|
+
if (!original || record.request_verified !== true || recovery.task_sha256 !== ownershipSha256(JSON.stringify(original)) || !Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || !checkpoint || !Number.isSafeInteger(checkpoint.observed_at) || checkpoint.observed_at <= 0 || typeof recovery.owner_confirmed !== 'boolean')
|
|
529
|
+
throw new Error('legacy recovery checkpoint is corrupt');
|
|
530
|
+
this.legacyView(original);
|
|
531
|
+
if (['operation_id', 'operation', 'sub_aid', 'owner_aid', 'initiator_aid', 'base_relation_version', 'target_relation_version'].some((key) => original[key] !== record[key]) || JSON.stringify(checkpoint.agent_md) !== JSON.stringify(record.base_agent_md) || original.local_decision !== 'pending' && original.local_decision !== record.local_decision || participants(record).some((name) => original.approvals[name] !== 'pending' && original.approvals[name] !== record.approvals[name]))
|
|
532
|
+
throw new Error('legacy recovery original context or approval changed');
|
|
533
|
+
const owner = checkpoint.owner_md;
|
|
534
|
+
if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
|
|
535
|
+
throw new Error('legacy recovery owner observation is corrupt');
|
|
536
|
+
if (owner.exists) {
|
|
537
|
+
const file = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
|
|
538
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
539
|
+
throw new Error('legacy recovery owner observation changed');
|
|
540
|
+
}
|
|
541
|
+
const originalOwner = original.files.owner, pinned = record.files.owner;
|
|
542
|
+
if ((originalOwner || owner.exists) && (!pinned || pinned.raw !== (originalOwner?.raw ?? owner.raw) || pinned.hash !== (originalOwner?.hash ?? owner.sha256) || JSON.stringify(pinned.original_owner) !== JSON.stringify(owner)) || originalOwner && (!owner.exists || originalOwner.raw !== owner.raw || originalOwner.hash !== owner.sha256))
|
|
543
|
+
throw new Error('legacy recovery owner candidate changed');
|
|
544
|
+
if (this.local() === record.owner_aid ? !recovery.owner_confirmed : recovery.owner_confirmed ? !record.received.includes(`${record.owner_aid}|${record.operation_id}|owner.ready`) : Boolean(record.files.agent) || record.status === 'completed')
|
|
545
|
+
throw new Error('legacy recovery owner confirmation is corrupt');
|
|
546
|
+
}
|
|
547
|
+
if (record.legacy_task) {
|
|
548
|
+
const recovery = record.legacy_task, original = recovery.task;
|
|
549
|
+
if (!Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || recovery.task_sha256 !== ownershipSha256(JSON.stringify(original)))
|
|
550
|
+
throw new Error('legacy task source is corrupt');
|
|
551
|
+
this.priorView(original);
|
|
552
|
+
if (['operation_id', 'operation', 'sub_aid', 'owner_aid', 'new_owner_aid', 'initiator_aid', 'base_relation_version', 'target_relation_version'].some((key) => original[key] !== record[key]) || Object.entries(original.approvals).some(([aid, decision]) => decision !== 'pending' && record.approvals[aid] !== decision) || JSON.stringify(recovery.checkpoint.agent_md) !== JSON.stringify(record.base_agent_md ?? null) || JSON.stringify(recovery.checkpoint.owner_md) !== JSON.stringify(record.base_owner_md ?? null) || !record.base_agent_md && record.status !== 'completed')
|
|
553
|
+
throw new Error('legacy task fixed context changed');
|
|
554
|
+
for (const [key, saved] of Object.entries(original.files))
|
|
555
|
+
if (record.files[key]?.raw !== saved.raw || record.files[key]?.hash !== saved.hash || key === 'agent' && JSON.stringify(record.files[key]?.condition) !== JSON.stringify(saved.condition))
|
|
556
|
+
throw new Error('legacy task signed candidate changed');
|
|
557
|
+
}
|
|
558
|
+
if (record.request_verified && (record.base_agent_md || !record.legacy_task)) {
|
|
314
559
|
const base = record.base_agent_md;
|
|
315
|
-
if (!base || base.exists !==
|
|
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))
|
|
316
561
|
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)
|
|
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 && !(base.pending && statement?.relation_version === record.target_relation_version && statement.owner_aid === record.owner_aid) || base.pending && record.operation !== 'bind')
|
|
319
564
|
throw new Error('ownership original statement is corrupt');
|
|
320
565
|
const owner = record.base_owner_md;
|
|
321
566
|
if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
|
|
322
567
|
throw new Error('ownership original owner.md evidence is corrupt');
|
|
323
568
|
if (owner.exists) {
|
|
324
569
|
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)
|
|
570
|
+
if (!base.pending && declaration.statement?.relation_version !== record.base_relation_version)
|
|
326
571
|
throw new Error('ownership original owner statement is corrupt');
|
|
327
572
|
}
|
|
328
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
|
+
}
|
|
329
595
|
for (const item of record.inbox)
|
|
330
596
|
parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record });
|
|
331
597
|
for (const key of record.received) {
|
|
@@ -371,7 +637,7 @@ export class OwnershipWorkflow {
|
|
|
371
637
|
if (record.status === 'completed') {
|
|
372
638
|
const evidence = record.completion_evidence;
|
|
373
639
|
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))
|
|
640
|
+
if (!evidence || evidence.sub_aid !== record.sub_aid || !cleaned && (evidence.relation_version !== (record.operation === 'release' ? record.base_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
641
|
throw new Error('ownership target acceptance evidence is corrupt');
|
|
376
642
|
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
643
|
throw new Error('ownership target file evidence is missing');
|
|
@@ -381,8 +647,195 @@ export class OwnershipWorkflow {
|
|
|
381
647
|
return { record, revision: snapshot.revision };
|
|
382
648
|
}
|
|
383
649
|
catch (error) {
|
|
384
|
-
|
|
650
|
+
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
651
|
+
throw error;
|
|
652
|
+
throw new OwnershipTaskDecodeError(snapshot.operation_id, error instanceof Error ? error.message : String(error));
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
legacyTaskView(record) {
|
|
656
|
+
return record.operation === 'bind' && record.base_relation_version === 0 && record.initiator_aid === record.sub_aid && Object.keys(record.files ?? {}).every((key) => key === 'owner') ? this.legacyView(record) : this.priorView(record);
|
|
657
|
+
}
|
|
658
|
+
legacyView(record) {
|
|
659
|
+
if (Object.hasOwn(record, 'legacy_recovery') || record.protocol !== '2.1' || record.operation !== 'bind' || record.base_relation_version !== 0 || record.target_relation_version !== 1 || record.initiator_aid !== record.sub_aid || record.request_verified !== true || record.status !== 'pending' && !(record.status === 'error' && record.retryable === true) || record.base_agent_md != null || record.base_owner_md != null || !record.files || typeof record.files !== 'object' || Array.isArray(record.files) || Object.keys(record.files).some((key) => key !== 'owner') || record.local_decision === 'reject' || Object.values(record.approvals ?? {}).includes('reject'))
|
|
660
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
|
|
661
|
+
const view = JSON.parse(JSON.stringify(record));
|
|
662
|
+
view.request_verified = false;
|
|
663
|
+
view.files = {};
|
|
664
|
+
view.inbox = record.inbox?.map((item) => ({ from: item.from, payload: this.legacyMessage(item.payload) }));
|
|
665
|
+
view.outbox = record.outbox?.map((item) => ({ to: item.to, payload: this.legacyMessage(item.payload) }));
|
|
666
|
+
this.decode({ operation_id: record.operation_id, revision: 1, task: view });
|
|
667
|
+
for (const notice of record.notices) {
|
|
668
|
+
if (typeof notice !== 'string' || notice.split('|').length !== 2)
|
|
669
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership notice is corrupt');
|
|
670
|
+
const [to, action] = notice.split('|');
|
|
671
|
+
this.validateOutgoing(view, { to: to, payload: { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: record.operation_id, operation: record.operation, action: action, sub_aid: record.sub_aid, owner_aid: record.owner_aid, base_relation_version: 0, target_relation_version: 1 } });
|
|
672
|
+
}
|
|
673
|
+
const saved = record.files.owner;
|
|
674
|
+
if (saved) {
|
|
675
|
+
if (this.local() !== record.owner_aid || record.local_decision !== 'accept' || typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !saved.condition || Object.keys(saved.condition).length !== 1 || typeof saved.condition.expected_revision !== 'string' || !saved.condition.expected_revision || saved.published_revision !== undefined && (typeof saved.published_revision !== 'string' || !saved.published_revision))
|
|
676
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner candidate cannot be recovered automatically');
|
|
677
|
+
const file = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
|
|
678
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
679
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner candidate does not match this task');
|
|
680
|
+
}
|
|
681
|
+
return view;
|
|
682
|
+
}
|
|
683
|
+
legacyMessage(content) {
|
|
684
|
+
if (!content || typeof content !== 'object' || Array.isArray(content))
|
|
685
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership message is corrupt');
|
|
686
|
+
return { ...content };
|
|
687
|
+
}
|
|
688
|
+
async upgradeLegacy(handle) {
|
|
689
|
+
if (Boolean(handle.record.base_agent_md) || !handle.record.request_verified || ['completed', 'stopped', 'rejected'].includes(handle.record.status))
|
|
690
|
+
return;
|
|
691
|
+
if (handle.record.operation !== 'bind' || handle.record.base_relation_version !== 0 || handle.record.initiator_aid !== handle.record.sub_aid || Object.keys(handle.record.files ?? {}).some((key) => key !== 'owner'))
|
|
692
|
+
return this.upgradePrior(handle);
|
|
693
|
+
const original = JSON.parse(JSON.stringify(handle.record));
|
|
694
|
+
const record = this.legacyView(original);
|
|
695
|
+
for (const snapshot of this.list(true)) {
|
|
696
|
+
if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
|
|
697
|
+
continue;
|
|
698
|
+
const other = this.decode(snapshot).record;
|
|
699
|
+
if (!other.base_agent_md && other.request_verified && !['completed', 'stopped', 'rejected'].includes(other.status))
|
|
700
|
+
this.legacyTaskView(other);
|
|
701
|
+
if (other.status === 'pending' || other.status === 'error' && other.retryable)
|
|
702
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
703
|
+
}
|
|
704
|
+
const ns = await this.readNs(record.sub_aid);
|
|
705
|
+
if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
706
|
+
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
707
|
+
if (ns.lock === 'locked' && ns.owner_aid === record.owner_aid && ns.relation_version === record.target_relation_version)
|
|
708
|
+
return this.upgradePrior(handle);
|
|
709
|
+
if (ns.lock !== 'unlocked' || ns.relation_version !== 0 || ns.owner_aid || ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement)
|
|
710
|
+
throw ownershipFailure('conflict', 'legacy ownership relation has changed');
|
|
711
|
+
const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
712
|
+
if (current.exists) {
|
|
713
|
+
const file = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
|
|
714
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
715
|
+
throw ownershipFailure('conflict', 'legacy owner declaration has changed');
|
|
716
|
+
}
|
|
717
|
+
const candidate = original.files.owner;
|
|
718
|
+
if (candidate) {
|
|
719
|
+
await this.deps.files.verifyFile(candidate.raw, 'owner', record.sub_aid, record.owner_aid);
|
|
720
|
+
if (!current.exists || current.content !== candidate.raw || current.sha256 !== candidate.hash)
|
|
721
|
+
throw ownershipFailure('conflict', 'legacy owner candidate was replaced');
|
|
722
|
+
if (candidate.published_revision !== undefined && current.revision !== candidate.published_revision)
|
|
723
|
+
throw ownershipFailure(current.revision ? 'conflict' : 'unavailable', 'legacy published owner revision changed', !current.revision);
|
|
385
724
|
}
|
|
725
|
+
else if (this.local() === record.owner_aid && current.exists)
|
|
726
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner declaration lacks a local task candidate');
|
|
727
|
+
const owner = { exists: current.exists, raw: current.content, sha256: current.exists ? ownershipSha256(current.content) : '', ...(candidate && current.revision ? { revision: current.revision } : {}) };
|
|
728
|
+
record.status = 'pending';
|
|
729
|
+
record.step = 'approval';
|
|
730
|
+
record.retryable = true;
|
|
731
|
+
record.request_verified = true;
|
|
732
|
+
delete record.code;
|
|
733
|
+
delete record.reason;
|
|
734
|
+
record.inbox = [];
|
|
735
|
+
record.received = [];
|
|
736
|
+
record.outbox = [];
|
|
737
|
+
record.notices = [];
|
|
738
|
+
await this.captureBase(record, ns);
|
|
739
|
+
if (candidate || current.exists && this.local() === record.sub_aid)
|
|
740
|
+
record.files.owner = { raw: candidate?.raw ?? current.content, hash: candidate?.hash ?? current.sha256, condition: {}, original_owner: owner };
|
|
741
|
+
record.legacy_recovery = { task: original, revision: handle.revision, task_sha256: ownershipSha256(JSON.stringify(original)), checkpoint: { observed_at: Math.trunc(Date.now() / 1000), agent_md: record.base_agent_md, owner_md: owner }, owner_confirmed: this.local() === record.owner_aid };
|
|
742
|
+
if (this.local() === record.sub_aid)
|
|
743
|
+
this.enqueue(record, record.owner_aid, 'bind.request');
|
|
744
|
+
else if (record.local_decision === 'accept')
|
|
745
|
+
this.enqueue(record, record.sub_aid, 'bind.accept');
|
|
746
|
+
const after = await this.readNs(record.sub_aid), afterOwner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
747
|
+
if (after.exists !== ns.exists || after.content !== ns.content || after.etag !== ns.etag || after.relation_version !== 0 || after.lock !== 'unlocked' || afterOwner.exists !== current.exists || afterOwner.content !== current.content || afterOwner.sha256 !== current.sha256)
|
|
748
|
+
throw ownershipFailure('conflict', 'legacy recovery observations changed');
|
|
749
|
+
const recovered = { record, revision: handle.revision };
|
|
750
|
+
this.decode({ operation_id: record.operation_id, revision: handle.revision + 1, task: record });
|
|
751
|
+
try {
|
|
752
|
+
await this.save(recovered);
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (error instanceof StateError && error.stringCode === 'persistence_unavailable' && error.message.includes('active ownership task already exists'))
|
|
756
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
handle.record = record;
|
|
760
|
+
handle.revision = recovered.revision;
|
|
761
|
+
}
|
|
762
|
+
priorView(original) {
|
|
763
|
+
if (original.base_agent_md != null || original.base_owner_md != null || original.legacy_task || original.legacy_recovery || !original.request_verified || !['pending', 'error'].includes(original.status) || original.status === 'error' && !original.retryable || Object.values(original.approvals ?? {}).includes('reject'))
|
|
764
|
+
throw ownershipFailure('unsupported_protocol', 'legacy task context cannot be recovered');
|
|
765
|
+
const record = structuredClone(original);
|
|
766
|
+
record.request_verified = false;
|
|
767
|
+
for (const [key, saved] of Object.entries(record.files ?? {})) {
|
|
768
|
+
const owner = key === 'owner' ? targetOwner(record) : record.owner_aid;
|
|
769
|
+
if (this.local() !== (key === 'agent' ? record.sub_aid : owner))
|
|
770
|
+
throw ownershipFailure('unsupported_protocol', 'legacy file local role is invalid');
|
|
771
|
+
if (key !== 'agent' && key !== 'cleanup') {
|
|
772
|
+
const condition = saved.condition;
|
|
773
|
+
if (!saved.original_owner && (!condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision))
|
|
774
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner condition is invalid');
|
|
775
|
+
saved.condition = {};
|
|
776
|
+
saved.original_owner ??= { exists: false, raw: '', sha256: '' };
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
this.decode({ operation_id: original.operation_id, revision: 1, task: record });
|
|
781
|
+
}
|
|
782
|
+
catch (error) {
|
|
783
|
+
throw ownershipFailure('unsupported_protocol', error instanceof Error ? error.message : String(error));
|
|
784
|
+
}
|
|
785
|
+
return record;
|
|
786
|
+
}
|
|
787
|
+
async upgradePrior(handle) {
|
|
788
|
+
const original = structuredClone(handle.record), record = this.priorView(original);
|
|
789
|
+
for (const snapshot of this.list(true)) {
|
|
790
|
+
if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
|
|
791
|
+
continue;
|
|
792
|
+
const other = this.decode(snapshot).record;
|
|
793
|
+
if (other.status === 'pending' || other.status === 'error' && other.retryable)
|
|
794
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
795
|
+
}
|
|
796
|
+
const ns = await this.readNs(record.sub_aid);
|
|
797
|
+
if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
798
|
+
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
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);
|
|
800
|
+
const released = record.operation === 'release' && ns.lock === 'unlocked' && !ns.owner_aid && ns.relation_version === record.base_relation_version;
|
|
801
|
+
const committed = record.operation !== 'release' && ns.lock === 'locked' && ns.owner_aid === targetOwner(record) && ns.relation_version === record.target_relation_version;
|
|
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)
|
|
804
|
+
throw ownershipFailure('conflict', 'legacy relationship no longer matches this task');
|
|
805
|
+
if (!committed)
|
|
806
|
+
await this.captureBase(record, ns);
|
|
807
|
+
for (const [key, saved] of Object.entries(record.files)) {
|
|
808
|
+
const owner = key === 'owner' || key === 'agent' ? targetOwner(record) : record.owner_aid;
|
|
809
|
+
await this.deps.files.verifyFile(saved.raw, key === 'agent' ? 'agent' : 'owner', record.sub_aid, owner);
|
|
810
|
+
if (key === 'agent')
|
|
811
|
+
continue;
|
|
812
|
+
const current = await this.deps.files.readOwner(owner, record.sub_aid);
|
|
813
|
+
const source = original.files[key];
|
|
814
|
+
const revision = source.condition.expected_revision;
|
|
815
|
+
if (revision && (current.content === saved.raw && source.published_revision !== undefined || !committed && current.content !== saved.raw)) {
|
|
816
|
+
if (!current.revision)
|
|
817
|
+
throw ownershipFailure('unavailable', 'legacy owner original revision is unavailable', true);
|
|
818
|
+
if (current.revision !== (current.content === saved.raw ? source.published_revision : revision))
|
|
819
|
+
throw ownershipFailure('conflict', 'legacy owner original revision changed');
|
|
820
|
+
}
|
|
821
|
+
const currentStatement = current.exists ? parseOwnershipFile(current.content, 'owner', { expectedSubAid: record.sub_aid }).statement : undefined;
|
|
822
|
+
const expected = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid }).statement;
|
|
823
|
+
const same = expected && currentStatement && Object.keys(expected).every((field) => expected[field] === currentStatement[field]);
|
|
824
|
+
if (current.content !== saved.raw && (source.published_revision !== undefined || current.exists && (key === 'owner' || !same)) && !committed)
|
|
825
|
+
throw ownershipFailure('conflict', 'legacy signed owner file was replaced');
|
|
826
|
+
saved.original_owner = { exists: current.exists, raw: current.content, sha256: current.exists ? current.sha256 : '', ...(revision && current.revision ? { revision: current.revision } : {}) };
|
|
827
|
+
}
|
|
828
|
+
record.request_verified = true;
|
|
829
|
+
record.legacy_task = { task: original, revision: handle.revision, task_sha256: ownershipSha256(JSON.stringify(original)), checkpoint: { agent_md: record.base_agent_md ?? null, owner_md: record.base_owner_md ?? null } };
|
|
830
|
+
const recovered = { record, revision: handle.revision };
|
|
831
|
+
if (committed) {
|
|
832
|
+
if (!await this.confirm(recovered))
|
|
833
|
+
throw ownershipFailure('conflict', 'legacy committed target lacks matching local signed evidence');
|
|
834
|
+
}
|
|
835
|
+
else
|
|
836
|
+
await this.save(recovered);
|
|
837
|
+
handle.record = record;
|
|
838
|
+
handle.revision = recovered.revision;
|
|
386
839
|
}
|
|
387
840
|
async save(handle) {
|
|
388
841
|
try {
|
|
@@ -448,23 +901,44 @@ export class OwnershipWorkflow {
|
|
|
448
901
|
};
|
|
449
902
|
if (context.operation === 'transfer')
|
|
450
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;
|
|
451
906
|
record.approvals[this.local()] = decision;
|
|
452
907
|
return record;
|
|
453
908
|
}
|
|
454
909
|
async start(operation, subAid, ownerAid, newOwnerAid, operationId) {
|
|
455
910
|
this.requireApplicable(subAid);
|
|
456
911
|
return this.runSerialized(subAid, async () => {
|
|
912
|
+
if (operation === 'bind' && !operationId)
|
|
913
|
+
await this.resetUnusableTasks(subAid, ownerAid);
|
|
457
914
|
const existing = operationId ? this.load(operationId) : null;
|
|
458
915
|
if (existing) {
|
|
916
|
+
if (this.isReset(existing.record))
|
|
917
|
+
throw ownershipFailure('conflict', 'ownership task was reset');
|
|
918
|
+
await this.upgradeLegacy(existing);
|
|
459
919
|
const record = existing.record;
|
|
920
|
+
if (!record.base_agent_md && record.request_verified)
|
|
921
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
|
|
460
922
|
if (record.operation !== operation || record.sub_aid !== subAid || record.owner_aid !== ownerAid || record.new_owner_aid !== newOwnerAid || record.initiator_aid !== this.local())
|
|
461
923
|
throw ownershipFailure('conflict', 'saved registration task context changed');
|
|
462
924
|
await this.drive(existing);
|
|
463
925
|
return ownershipTaskResult(record);
|
|
464
926
|
}
|
|
465
927
|
let ns;
|
|
466
|
-
for (const snapshot of this.list()) {
|
|
467
|
-
|
|
928
|
+
for (const snapshot of this.list(true)) {
|
|
929
|
+
if (snapshot.task.sub_aid !== subAid)
|
|
930
|
+
continue;
|
|
931
|
+
const active = this.decode(snapshot);
|
|
932
|
+
if (this.isReset(active.record))
|
|
933
|
+
continue;
|
|
934
|
+
if (!active.record.base_agent_md && active.record.request_verified && ['completed', 'stopped', 'rejected'].includes(active.record.status))
|
|
935
|
+
continue;
|
|
936
|
+
if (!active.record.base_agent_md && active.record.request_verified && (active.record.operation !== operation || active.record.owner_aid !== ownerAid || active.record.new_owner_aid !== newOwnerAid || active.record.initiator_aid !== this.local()))
|
|
937
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
938
|
+
await this.upgradeLegacy(active);
|
|
939
|
+
const record = active.record;
|
|
940
|
+
if (!record.base_agent_md && record.request_verified)
|
|
941
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
|
|
468
942
|
if (record.sub_aid !== subAid || record.status !== 'pending' && !(record.status === 'error' && record.retryable))
|
|
469
943
|
continue;
|
|
470
944
|
const same = record.operation === operation && record.owner_aid === ownerAid && record.new_owner_aid === newOwnerAid && record.initiator_aid === this.local();
|
|
@@ -477,7 +951,7 @@ export class OwnershipWorkflow {
|
|
|
477
951
|
await this.recordError(active, error);
|
|
478
952
|
return ownershipTaskResult(record);
|
|
479
953
|
}
|
|
480
|
-
let base =
|
|
954
|
+
let base = this.isBase(record, ns);
|
|
481
955
|
if (base && record.request_verified) {
|
|
482
956
|
try {
|
|
483
957
|
await this.assertBaseMaterial(active, ns);
|
|
@@ -488,7 +962,7 @@ export class OwnershipWorkflow {
|
|
|
488
962
|
base = false;
|
|
489
963
|
}
|
|
490
964
|
}
|
|
491
|
-
const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.
|
|
965
|
+
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);
|
|
492
966
|
if (!base && !target) {
|
|
493
967
|
await this.recordError(active, ownershipFailure('conflict', 'the operation was superseded by a different relation result'));
|
|
494
968
|
const result = ownershipTaskResult(record);
|
|
@@ -508,11 +982,13 @@ export class OwnershipWorkflow {
|
|
|
508
982
|
ns = await this.readNs(subAid);
|
|
509
983
|
if (operation === 'bind' && ns.lock !== 'unlocked')
|
|
510
984
|
throw ownershipFailure('owner_locked', 'the current owner relation is locked');
|
|
511
|
-
if (
|
|
512
|
-
throw ownershipFailure('
|
|
513
|
-
if (
|
|
985
|
+
if (operation === 'bind' && subAid !== this.local() && (ns.pending || ns.ownership_status === 'pending'))
|
|
986
|
+
throw ownershipFailure('conflict', 'pending ownership must resume its original operation');
|
|
987
|
+
if (!ns.exists && operation !== 'bind')
|
|
988
|
+
throw ownershipFailure('conflict', 'original agent.md is missing');
|
|
989
|
+
if (operation === 'bind' && (ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type : subAid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
514
990
|
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
515
|
-
if (operation === 'bind' && 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)
|
|
516
992
|
throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
|
|
517
993
|
if (operation !== 'bind') {
|
|
518
994
|
const consensus = await this.deps.files.checkConsensus(subAid);
|
|
@@ -536,14 +1012,21 @@ export class OwnershipWorkflow {
|
|
|
536
1012
|
const initial = this.load(operationId);
|
|
537
1013
|
if (!initial)
|
|
538
1014
|
throw ownershipFailure('operation_not_found', 'no local ownership request matches operation_id');
|
|
1015
|
+
if (this.isReset(initial.record))
|
|
1016
|
+
throw ownershipFailure('conflict', 'ownership task was reset');
|
|
539
1017
|
return this.runSerialized(initial.record.sub_aid, async () => {
|
|
540
1018
|
const handle = this.load(operationId);
|
|
1019
|
+
await this.upgradeLegacy(handle);
|
|
541
1020
|
const record = handle.record;
|
|
1021
|
+
if (!record.base_agent_md && record.request_verified)
|
|
1022
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
|
|
542
1023
|
if (record.operation === 'release' || record.initiator_aid === this.local() || !record.request_verified)
|
|
543
1024
|
throw ownershipFailure('forbidden', 'this task is not a verified local approval request');
|
|
544
1025
|
if (record.local_decision !== 'pending') {
|
|
545
1026
|
if (record.local_decision !== decision)
|
|
546
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);
|
|
547
1030
|
await this.drive(handle);
|
|
548
1031
|
return ownershipTaskResult(record);
|
|
549
1032
|
}
|
|
@@ -552,12 +1035,13 @@ export class OwnershipWorkflow {
|
|
|
552
1035
|
if (record.status !== 'pending')
|
|
553
1036
|
throw ownershipFailure('conflict', 'ownership approval stage has ended');
|
|
554
1037
|
const ns = await this.readNs(record.sub_aid);
|
|
555
|
-
if (
|
|
1038
|
+
if (!this.isBase(record, ns))
|
|
556
1039
|
throw ownershipFailure('conflict', 'the original relation changed before approval');
|
|
557
1040
|
await this.assertBaseMaterial(handle, ns);
|
|
558
1041
|
record.local_decision = decision;
|
|
559
1042
|
record.approvals[this.local()] = decision;
|
|
560
|
-
|
|
1043
|
+
if (decision !== 'accept' || !this.ownerFirst(record))
|
|
1044
|
+
this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
|
|
561
1045
|
if (decision === 'reject') {
|
|
562
1046
|
record.status = 'rejected';
|
|
563
1047
|
record.step = 'approval';
|
|
@@ -571,6 +1055,42 @@ export class OwnershipWorkflow {
|
|
|
571
1055
|
return ownershipTaskResult(record);
|
|
572
1056
|
});
|
|
573
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
|
+
}
|
|
574
1094
|
enqueue(record, to, action, reason) {
|
|
575
1095
|
if (to === this.local())
|
|
576
1096
|
return;
|
|
@@ -583,11 +1103,13 @@ export class OwnershipWorkflow {
|
|
|
583
1103
|
payload.new_owner_aid = record.new_owner_aid;
|
|
584
1104
|
if (reason !== undefined)
|
|
585
1105
|
payload.reason = reason;
|
|
586
|
-
record.outbox.push({ to, payload });
|
|
1106
|
+
record.outbox.push({ to, payload, message_id: `m-${randomUUID().replace(/-/g, '')}` });
|
|
587
1107
|
record.notices.push(key);
|
|
588
1108
|
}
|
|
589
1109
|
validateOutgoing(record, item) {
|
|
590
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');
|
|
591
1113
|
const message = parseOwnershipMessage(item.payload, { authenticated_from_aid: this.local(), local_aid: item.to, task: { ...record, local_decision: 'accept' } });
|
|
592
1114
|
const rejected = ['bind.reject', 'transfer.reject'].includes(message.action);
|
|
593
1115
|
if (message.action !== 'operation.stopped' && record.local_decision !== (rejected ? 'reject' : 'accept') || record.approvals[this.local()] !== record.local_decision)
|
|
@@ -598,7 +1120,7 @@ export class OwnershipWorkflow {
|
|
|
598
1120
|
throw new Error('ownership outgoing action lacks participant approvals');
|
|
599
1121
|
}
|
|
600
1122
|
catch (error) {
|
|
601
|
-
throw
|
|
1123
|
+
throw new OwnershipTaskDecodeError(record.operation_id, error instanceof Error ? error.message : String(error));
|
|
602
1124
|
}
|
|
603
1125
|
}
|
|
604
1126
|
async flush(handle) {
|
|
@@ -612,10 +1134,10 @@ export class OwnershipWorkflow {
|
|
|
612
1134
|
continue;
|
|
613
1135
|
}
|
|
614
1136
|
const ns = await this.readNs(record.sub_aid);
|
|
615
|
-
const base =
|
|
616
|
-
const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.
|
|
1137
|
+
const base = this.isBase(record, ns);
|
|
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);
|
|
617
1139
|
const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
|
|
618
|
-
if (requiresTarget ? !target : !base) {
|
|
1140
|
+
if (requiresTarget ? !target : !base && !(record.operation === 'bind' && target && ['owner.ready', 'bind.accept'].includes(message.payload.action))) {
|
|
619
1141
|
record.outbox.shift();
|
|
620
1142
|
await this.save(handle);
|
|
621
1143
|
continue;
|
|
@@ -629,7 +1151,12 @@ export class OwnershipWorkflow {
|
|
|
629
1151
|
if (expiresAt <= Math.trunc(Date.now() / 1000))
|
|
630
1152
|
throw ownershipFailure('conflict', 'transfer authorization expired');
|
|
631
1153
|
}
|
|
632
|
-
|
|
1154
|
+
if (!message.message_id) {
|
|
1155
|
+
message.message_id = `m-${randomUUID().replace(/-/g, '')}`;
|
|
1156
|
+
await this.save(handle);
|
|
1157
|
+
}
|
|
1158
|
+
this.assertActive(handle);
|
|
1159
|
+
await this.deps.send(message.to, message.payload, message.message_id);
|
|
633
1160
|
handle.record.outbox.shift();
|
|
634
1161
|
await this.save(handle);
|
|
635
1162
|
}
|
|
@@ -642,11 +1169,35 @@ export class OwnershipWorkflow {
|
|
|
642
1169
|
const currentOwner = ns.lock === 'locked' ? file?.statement?.owner_aid : null;
|
|
643
1170
|
if (ns.lock === 'locked' && (!file?.statement || file.statement.relation_version !== ns.relation_version) || ns.owner_aid !== undefined && ns.owner_aid !== currentOwner)
|
|
644
1171
|
throw ownershipFailure('unavailable', 'NS owner metadata and signed agent.md do not agree', true);
|
|
645
|
-
|
|
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);
|
|
646
1194
|
}
|
|
647
1195
|
async captureBase(record, ns) {
|
|
648
|
-
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
|
|
649
|
-
record.
|
|
1196
|
+
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
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 };
|
|
650
1201
|
if (!statement) {
|
|
651
1202
|
record.base_owner_md = { exists: false, raw: '', sha256: '' };
|
|
652
1203
|
return;
|
|
@@ -661,6 +1212,12 @@ export class OwnershipWorkflow {
|
|
|
661
1212
|
async assertBaseMaterial(handle, ns) {
|
|
662
1213
|
const record = handle.record;
|
|
663
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
|
+
}
|
|
664
1221
|
const original = record.base_owner_md;
|
|
665
1222
|
if (!original)
|
|
666
1223
|
throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is missing');
|
|
@@ -692,10 +1249,14 @@ export class OwnershipWorkflow {
|
|
|
692
1249
|
throw ownershipFailure('conflict', 'the original owner statement was replaced');
|
|
693
1250
|
}
|
|
694
1251
|
assertAgentBaseMaterial(record, ns) {
|
|
1252
|
+
if (this.isPending(record, ns))
|
|
1253
|
+
return;
|
|
695
1254
|
const base = record.base_agent_md;
|
|
696
|
-
if (!base)
|
|
1255
|
+
if (!base || typeof base.raw !== 'string')
|
|
697
1256
|
throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
|
|
698
|
-
|
|
1257
|
+
if (base.pending && (ns.content !== base.raw || ns.etag !== base.etag))
|
|
1258
|
+
throw ownershipFailure('conflict', 'the pending ownership original was replaced');
|
|
1259
|
+
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
699
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;
|
|
700
1261
|
if (ns.exists !== base.exists || !same)
|
|
701
1262
|
throw ownershipFailure('conflict', 'the original ownership statement was replaced');
|
|
@@ -705,6 +1266,10 @@ export class OwnershipWorkflow {
|
|
|
705
1266
|
while (record.inbox.length) {
|
|
706
1267
|
const item = record.inbox[0];
|
|
707
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
|
+
}
|
|
708
1273
|
if (record.request_verified && ns.relation_version === record.base_relation_version && !['relation.confirmed', 'sub.cleaned'].includes(item.payload.action))
|
|
709
1274
|
await this.assertBaseMaterial(handle, ns);
|
|
710
1275
|
try {
|
|
@@ -723,7 +1288,29 @@ export class OwnershipWorkflow {
|
|
|
723
1288
|
continue;
|
|
724
1289
|
}
|
|
725
1290
|
const action = item.payload.action;
|
|
1291
|
+
if (action === 'owner.ready' && record.legacy_recovery && record.status === 'pending') {
|
|
1292
|
+
const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1293
|
+
if (!owner.exists)
|
|
1294
|
+
throw ownershipFailure(record.files.owner ? 'conflict' : 'unavailable', 'legacy owner confirmation declaration is missing', !record.files.owner);
|
|
1295
|
+
const file = await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
1296
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version || record.files.owner && (record.files.owner.raw !== owner.content || record.files.owner.hash !== owner.sha256))
|
|
1297
|
+
throw ownershipFailure('conflict', 'legacy owner confirmation declaration changed');
|
|
1298
|
+
if (!record.files.owner)
|
|
1299
|
+
record.files.owner = { raw: owner.content, hash: owner.sha256, condition: {}, original_owner: { exists: true, raw: owner.content, sha256: owner.sha256 } };
|
|
1300
|
+
record.legacy_recovery.owner_confirmed = true;
|
|
1301
|
+
}
|
|
726
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
|
+
}
|
|
727
1314
|
if (!record.request_verified)
|
|
728
1315
|
await this.captureBase(record, ns);
|
|
729
1316
|
record.request_verified = true;
|
|
@@ -842,8 +1429,10 @@ export class OwnershipWorkflow {
|
|
|
842
1429
|
fileTask(handle) {
|
|
843
1430
|
const record = handle.record;
|
|
844
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,
|
|
845
1433
|
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,
|
|
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,
|
|
1435
|
+
assertActive: () => this.assertActive(handle),
|
|
847
1436
|
persist: async () => {
|
|
848
1437
|
record.authorization_sha256 = task.authorization_sha256;
|
|
849
1438
|
record.expires_at = task.expires_at;
|
|
@@ -853,15 +1442,55 @@ export class OwnershipWorkflow {
|
|
|
853
1442
|
} };
|
|
854
1443
|
return task;
|
|
855
1444
|
}
|
|
1445
|
+
assertActive(handle) {
|
|
1446
|
+
const current = this.load(handle.record.operation_id);
|
|
1447
|
+
if (!current || this.isReset(current.record) || current.revision !== handle.revision)
|
|
1448
|
+
throw ownershipFailure('conflict', 'ownership task changed before external action');
|
|
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
|
+
}
|
|
856
1453
|
async drive(handle) {
|
|
1454
|
+
if (this.isReset(handle.record))
|
|
1455
|
+
return;
|
|
1456
|
+
await this.upgradeLegacy(handle);
|
|
857
1457
|
const record = handle.record;
|
|
1458
|
+
if (!record.base_agent_md && record.request_verified)
|
|
1459
|
+
return;
|
|
858
1460
|
if (record.status === 'error' && record.retryable) {
|
|
859
1461
|
record.status = 'pending';
|
|
860
1462
|
await this.save(handle);
|
|
861
1463
|
}
|
|
862
1464
|
try {
|
|
863
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
|
+
}
|
|
864
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
|
+
}
|
|
865
1494
|
if (record.status === 'rejected' || record.status === 'stopped' || record.status === 'error')
|
|
866
1495
|
return;
|
|
867
1496
|
if (!record.request_verified)
|
|
@@ -898,7 +1527,7 @@ export class OwnershipWorkflow {
|
|
|
898
1527
|
return;
|
|
899
1528
|
}
|
|
900
1529
|
const ns = await this.readNs(record.sub_aid);
|
|
901
|
-
if (
|
|
1530
|
+
if (!this.isBase(record, ns)) {
|
|
902
1531
|
throw ownershipFailure('conflict', 'the original relation was replaced');
|
|
903
1532
|
}
|
|
904
1533
|
await this.assertBaseMaterial(handle, ns);
|
|
@@ -907,6 +1536,8 @@ export class OwnershipWorkflow {
|
|
|
907
1536
|
const approved = participants(record).every((name) => record.approvals[name] === 'accept');
|
|
908
1537
|
if (record.operation === 'bind' && !approved || record.operation === 'transfer' && this.local() === record.owner_aid && !approved)
|
|
909
1538
|
return;
|
|
1539
|
+
if (record.legacy_recovery && this.local() === record.sub_aid && !record.legacy_recovery.owner_confirmed)
|
|
1540
|
+
return;
|
|
910
1541
|
delete record.code;
|
|
911
1542
|
delete record.reason;
|
|
912
1543
|
record.retryable = true;
|
|
@@ -932,11 +1563,17 @@ export class OwnershipWorkflow {
|
|
|
932
1563
|
record.step = 'owner';
|
|
933
1564
|
await this.save(handle);
|
|
934
1565
|
await this.deps.files.declareSubaid(this.fileTask(handle));
|
|
935
|
-
this.
|
|
1566
|
+
if (!this.ownerFirst(record))
|
|
1567
|
+
this.enqueue(record, record.sub_aid, 'owner.ready');
|
|
936
1568
|
await this.save(handle);
|
|
937
1569
|
await this.flush(handle);
|
|
938
1570
|
}
|
|
939
1571
|
if (this.local() === record.sub_aid) {
|
|
1572
|
+
if (record.legacy_recovery && record.files.owner) {
|
|
1573
|
+
const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid), pinned = record.files.owner;
|
|
1574
|
+
if (!current.exists || current.content !== pinned.raw || current.sha256 !== pinned.hash)
|
|
1575
|
+
throw ownershipFailure('conflict', 'legacy owner declaration changed before subject publication');
|
|
1576
|
+
}
|
|
940
1577
|
record.step = 'agent';
|
|
941
1578
|
await this.save(handle);
|
|
942
1579
|
await this.deps.files.declareOwneraid(this.fileTask(handle));
|
|
@@ -987,7 +1624,7 @@ export class OwnershipWorkflow {
|
|
|
987
1624
|
record.cleanup_status = 'cleaned';
|
|
988
1625
|
cleaned = true;
|
|
989
1626
|
}
|
|
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));
|
|
1627
|
+
const reached = cleaned || result.relation_version === (record.operation === 'release' ? record.base_relation_version : record.target_relation_version) && (record.operation === 'release' ? result.status === 'released' && result.lock === 'unlocked' : result.status === 'consensus' && result.owner_aid === targetOwner(record));
|
|
991
1628
|
if (!reached)
|
|
992
1629
|
return false;
|
|
993
1630
|
if (record.operation === 'release' && !cleaned) {
|
|
@@ -1027,6 +1664,10 @@ export class OwnershipWorkflow {
|
|
|
1027
1664
|
}
|
|
1028
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)))
|
|
1029
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');
|
|
1030
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)
|
|
1031
1672
|
throw ownershipFailure('conflict', 'published agent.md ETag changed');
|
|
1032
1673
|
if (record.status !== 'completed') {
|
|
@@ -1106,7 +1747,7 @@ export class OwnershipWorkflow {
|
|
|
1106
1747
|
}
|
|
1107
1748
|
await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: record.sub_aid, owner_aid: targetOwner(record),
|
|
1108
1749
|
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) });
|
|
1750
|
+
files: record.release_cleanup.files, expected_agent_statement: original, assertActive: () => this.assertActive(handle), persist: () => this.save(handle) });
|
|
1110
1751
|
record.release_cleanup.cleaned = true;
|
|
1111
1752
|
await this.save(handle);
|
|
1112
1753
|
this.emit(record, 'ownership.progress');
|
|
@@ -1169,8 +1810,8 @@ export class OwnershipWorkflow {
|
|
|
1169
1810
|
const payload = { ...ownershipTaskResult(record), local_roles: [...record.local_roles], ...extra };
|
|
1170
1811
|
if (event === 'ownership.request')
|
|
1171
1812
|
payload.requester_aid = record.initiator_aid;
|
|
1172
|
-
if (record.
|
|
1173
|
-
payload.relation_version = record.
|
|
1813
|
+
if (record.completion_evidence !== undefined)
|
|
1814
|
+
payload.relation_version = record.completion_evidence.relation_version;
|
|
1174
1815
|
queueMicrotask(() => { try {
|
|
1175
1816
|
const pending = this.deps.emit(event, payload);
|
|
1176
1817
|
if (pending)
|