@agentunion/fastaun 0.5.20 → 0.5.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/_packed_docs/CHANGELOG.md +47 -0
- package/_packed_docs/INDEX.md +453 -440
- package/_packed_docs/KITE_DOCS_GUIDE.md +164 -161
- 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/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/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 +666 -658
- 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/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 +147 -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 +5 -2
- package/_packed_docs/sdk/09-message-rpc-manual.md +250 -160
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +71 -0
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +88 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +134 -130
- package/_packed_docs/sdk/INDEX.md +239 -221
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -922
- 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 +106 -43
- 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 +180 -9
- 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 +480 -476
- 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 +1 -1
- 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 +2 -2
- 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 +84 -67
- 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.js +4 -0
- 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 +59 -1
- package/dist/client/delivery.js +1018 -125
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.js +123 -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 +751 -294
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +26 -2
- package/dist/client.js +379 -63
- 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/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 +45 -0
- package/dist/org-protocol.js +754 -11
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +16 -3
- package/dist/ownership-v21.js +35 -17
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +17 -0
- package/dist/ownership-workflow.js +511 -26
- 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/tools/cross-sdk-agent.d.ts +66 -3
- package/dist/tools/cross-sdk-agent.js +466 -36
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.js +98 -44
- 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)
|
|
@@ -107,7 +115,7 @@ export function parseOwnershipMessage(content, context) {
|
|
|
107
115
|
if (ns.protocol !== '2.1')
|
|
108
116
|
throw ownershipFailure('unsupported_protocol', 'NS ownership protocol is unavailable');
|
|
109
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);
|
|
110
|
-
const target = message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === message.
|
|
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,17 +516,50 @@ 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;
|
|
562
|
+
const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
318
563
|
if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version)
|
|
319
564
|
throw new Error('ownership original statement is corrupt');
|
|
320
565
|
const owner = record.base_owner_md;
|
|
@@ -371,7 +616,7 @@ export class OwnershipWorkflow {
|
|
|
371
616
|
if (record.status === 'completed') {
|
|
372
617
|
const evidence = record.completion_evidence;
|
|
373
618
|
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))
|
|
619
|
+
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
620
|
throw new Error('ownership target acceptance evidence is corrupt');
|
|
376
621
|
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
622
|
throw new Error('ownership target file evidence is missing');
|
|
@@ -381,8 +626,194 @@ export class OwnershipWorkflow {
|
|
|
381
626
|
return { record, revision: snapshot.revision };
|
|
382
627
|
}
|
|
383
628
|
catch (error) {
|
|
384
|
-
|
|
629
|
+
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
630
|
+
throw error;
|
|
631
|
+
throw new OwnershipTaskDecodeError(snapshot.operation_id, error instanceof Error ? error.message : String(error));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
legacyTaskView(record) {
|
|
635
|
+
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);
|
|
636
|
+
}
|
|
637
|
+
legacyView(record) {
|
|
638
|
+
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'))
|
|
639
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
|
|
640
|
+
const view = JSON.parse(JSON.stringify(record));
|
|
641
|
+
view.request_verified = false;
|
|
642
|
+
view.files = {};
|
|
643
|
+
view.inbox = record.inbox?.map((item) => ({ from: item.from, payload: this.legacyMessage(item.payload) }));
|
|
644
|
+
view.outbox = record.outbox?.map((item) => ({ to: item.to, payload: this.legacyMessage(item.payload) }));
|
|
645
|
+
this.decode({ operation_id: record.operation_id, revision: 1, task: view });
|
|
646
|
+
for (const notice of record.notices) {
|
|
647
|
+
if (typeof notice !== 'string' || notice.split('|').length !== 2)
|
|
648
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership notice is corrupt');
|
|
649
|
+
const [to, action] = notice.split('|');
|
|
650
|
+
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 } });
|
|
651
|
+
}
|
|
652
|
+
const saved = record.files.owner;
|
|
653
|
+
if (saved) {
|
|
654
|
+
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))
|
|
655
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner candidate cannot be recovered automatically');
|
|
656
|
+
const file = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
|
|
657
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
658
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner candidate does not match this task');
|
|
659
|
+
}
|
|
660
|
+
return view;
|
|
661
|
+
}
|
|
662
|
+
legacyMessage(content) {
|
|
663
|
+
if (!content || typeof content !== 'object' || Array.isArray(content))
|
|
664
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership message is corrupt');
|
|
665
|
+
return { ...content };
|
|
666
|
+
}
|
|
667
|
+
async upgradeLegacy(handle) {
|
|
668
|
+
if (Boolean(handle.record.base_agent_md) || !handle.record.request_verified || ['completed', 'stopped', 'rejected'].includes(handle.record.status))
|
|
669
|
+
return;
|
|
670
|
+
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'))
|
|
671
|
+
return this.upgradePrior(handle);
|
|
672
|
+
const original = JSON.parse(JSON.stringify(handle.record));
|
|
673
|
+
const record = this.legacyView(original);
|
|
674
|
+
for (const snapshot of this.list(true)) {
|
|
675
|
+
if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
|
|
676
|
+
continue;
|
|
677
|
+
const other = this.decode(snapshot).record;
|
|
678
|
+
if (!other.base_agent_md && other.request_verified && !['completed', 'stopped', 'rejected'].includes(other.status))
|
|
679
|
+
this.legacyTaskView(other);
|
|
680
|
+
if (other.status === 'pending' || other.status === 'error' && other.retryable)
|
|
681
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
682
|
+
}
|
|
683
|
+
const ns = await this.readNs(record.sub_aid);
|
|
684
|
+
if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
685
|
+
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
686
|
+
if (ns.lock === 'locked' && ns.owner_aid === record.owner_aid && ns.relation_version === record.target_relation_version)
|
|
687
|
+
return this.upgradePrior(handle);
|
|
688
|
+
if (ns.lock !== 'unlocked' || ns.relation_version !== 0 || ns.owner_aid || ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement)
|
|
689
|
+
throw ownershipFailure('conflict', 'legacy ownership relation has changed');
|
|
690
|
+
const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
691
|
+
if (current.exists) {
|
|
692
|
+
const file = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
|
|
693
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
694
|
+
throw ownershipFailure('conflict', 'legacy owner declaration has changed');
|
|
695
|
+
}
|
|
696
|
+
const candidate = original.files.owner;
|
|
697
|
+
if (candidate) {
|
|
698
|
+
await this.deps.files.verifyFile(candidate.raw, 'owner', record.sub_aid, record.owner_aid);
|
|
699
|
+
if (!current.exists || current.content !== candidate.raw || current.sha256 !== candidate.hash)
|
|
700
|
+
throw ownershipFailure('conflict', 'legacy owner candidate was replaced');
|
|
701
|
+
if (candidate.published_revision !== undefined && current.revision !== candidate.published_revision)
|
|
702
|
+
throw ownershipFailure(current.revision ? 'conflict' : 'unavailable', 'legacy published owner revision changed', !current.revision);
|
|
703
|
+
}
|
|
704
|
+
else if (this.local() === record.owner_aid && current.exists)
|
|
705
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner declaration lacks a local task candidate');
|
|
706
|
+
const owner = { exists: current.exists, raw: current.content, sha256: current.exists ? ownershipSha256(current.content) : '', ...(candidate && current.revision ? { revision: current.revision } : {}) };
|
|
707
|
+
record.status = 'pending';
|
|
708
|
+
record.step = 'approval';
|
|
709
|
+
record.retryable = true;
|
|
710
|
+
record.request_verified = true;
|
|
711
|
+
delete record.code;
|
|
712
|
+
delete record.reason;
|
|
713
|
+
record.inbox = [];
|
|
714
|
+
record.received = [];
|
|
715
|
+
record.outbox = [];
|
|
716
|
+
record.notices = [];
|
|
717
|
+
await this.captureBase(record, ns);
|
|
718
|
+
if (candidate || current.exists && this.local() === record.sub_aid)
|
|
719
|
+
record.files.owner = { raw: candidate?.raw ?? current.content, hash: candidate?.hash ?? current.sha256, condition: {}, original_owner: owner };
|
|
720
|
+
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 };
|
|
721
|
+
if (this.local() === record.sub_aid)
|
|
722
|
+
this.enqueue(record, record.owner_aid, 'bind.request');
|
|
723
|
+
else if (record.local_decision === 'accept')
|
|
724
|
+
this.enqueue(record, record.sub_aid, 'bind.accept');
|
|
725
|
+
const after = await this.readNs(record.sub_aid), afterOwner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
726
|
+
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)
|
|
727
|
+
throw ownershipFailure('conflict', 'legacy recovery observations changed');
|
|
728
|
+
const recovered = { record, revision: handle.revision };
|
|
729
|
+
this.decode({ operation_id: record.operation_id, revision: handle.revision + 1, task: record });
|
|
730
|
+
try {
|
|
731
|
+
await this.save(recovered);
|
|
385
732
|
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
if (error instanceof StateError && error.stringCode === 'persistence_unavailable' && error.message.includes('active ownership task already exists'))
|
|
735
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
736
|
+
throw error;
|
|
737
|
+
}
|
|
738
|
+
handle.record = record;
|
|
739
|
+
handle.revision = recovered.revision;
|
|
740
|
+
}
|
|
741
|
+
priorView(original) {
|
|
742
|
+
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'))
|
|
743
|
+
throw ownershipFailure('unsupported_protocol', 'legacy task context cannot be recovered');
|
|
744
|
+
const record = structuredClone(original);
|
|
745
|
+
record.request_verified = false;
|
|
746
|
+
for (const [key, saved] of Object.entries(record.files ?? {})) {
|
|
747
|
+
const owner = key === 'owner' ? targetOwner(record) : record.owner_aid;
|
|
748
|
+
if (this.local() !== (key === 'agent' ? record.sub_aid : owner))
|
|
749
|
+
throw ownershipFailure('unsupported_protocol', 'legacy file local role is invalid');
|
|
750
|
+
if (key !== 'agent' && key !== 'cleanup') {
|
|
751
|
+
const condition = saved.condition;
|
|
752
|
+
if (!saved.original_owner && (!condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision))
|
|
753
|
+
throw ownershipFailure('unsupported_protocol', 'legacy owner condition is invalid');
|
|
754
|
+
saved.condition = {};
|
|
755
|
+
saved.original_owner ??= { exists: false, raw: '', sha256: '' };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
this.decode({ operation_id: original.operation_id, revision: 1, task: record });
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
throw ownershipFailure('unsupported_protocol', error instanceof Error ? error.message : String(error));
|
|
763
|
+
}
|
|
764
|
+
return record;
|
|
765
|
+
}
|
|
766
|
+
async upgradePrior(handle) {
|
|
767
|
+
const original = structuredClone(handle.record), record = this.priorView(original);
|
|
768
|
+
for (const snapshot of this.list(true)) {
|
|
769
|
+
if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
|
|
770
|
+
continue;
|
|
771
|
+
const other = this.decode(snapshot).record;
|
|
772
|
+
if (other.status === 'pending' || other.status === 'error' && other.retryable)
|
|
773
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
774
|
+
}
|
|
775
|
+
const ns = await this.readNs(record.sub_aid);
|
|
776
|
+
if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
777
|
+
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
778
|
+
const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
|
|
779
|
+
const released = record.operation === 'release' && ns.lock === 'unlocked' && !ns.owner_aid && ns.relation_version === record.base_relation_version;
|
|
780
|
+
const committed = record.operation !== 'release' && ns.lock === 'locked' && ns.owner_aid === targetOwner(record) && ns.relation_version === record.target_relation_version;
|
|
781
|
+
if (!base && !released && !committed)
|
|
782
|
+
throw ownershipFailure('conflict', 'legacy relationship no longer matches this task');
|
|
783
|
+
if (!committed)
|
|
784
|
+
await this.captureBase(record, ns);
|
|
785
|
+
for (const [key, saved] of Object.entries(record.files)) {
|
|
786
|
+
const owner = key === 'owner' || key === 'agent' ? targetOwner(record) : record.owner_aid;
|
|
787
|
+
await this.deps.files.verifyFile(saved.raw, key === 'agent' ? 'agent' : 'owner', record.sub_aid, owner);
|
|
788
|
+
if (key === 'agent')
|
|
789
|
+
continue;
|
|
790
|
+
const current = await this.deps.files.readOwner(owner, record.sub_aid);
|
|
791
|
+
const source = original.files[key];
|
|
792
|
+
const revision = source.condition.expected_revision;
|
|
793
|
+
if (revision && (current.content === saved.raw && source.published_revision !== undefined || !committed && current.content !== saved.raw)) {
|
|
794
|
+
if (!current.revision)
|
|
795
|
+
throw ownershipFailure('unavailable', 'legacy owner original revision is unavailable', true);
|
|
796
|
+
if (current.revision !== (current.content === saved.raw ? source.published_revision : revision))
|
|
797
|
+
throw ownershipFailure('conflict', 'legacy owner original revision changed');
|
|
798
|
+
}
|
|
799
|
+
const currentStatement = current.exists ? parseOwnershipFile(current.content, 'owner', { expectedSubAid: record.sub_aid }).statement : undefined;
|
|
800
|
+
const expected = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid }).statement;
|
|
801
|
+
const same = expected && currentStatement && Object.keys(expected).every((field) => expected[field] === currentStatement[field]);
|
|
802
|
+
if (current.content !== saved.raw && (source.published_revision !== undefined || current.exists && (key === 'owner' || !same)) && !committed)
|
|
803
|
+
throw ownershipFailure('conflict', 'legacy signed owner file was replaced');
|
|
804
|
+
saved.original_owner = { exists: current.exists, raw: current.content, sha256: current.exists ? current.sha256 : '', ...(revision && current.revision ? { revision: current.revision } : {}) };
|
|
805
|
+
}
|
|
806
|
+
record.request_verified = true;
|
|
807
|
+
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 } };
|
|
808
|
+
const recovered = { record, revision: handle.revision };
|
|
809
|
+
if (committed) {
|
|
810
|
+
if (!await this.confirm(recovered))
|
|
811
|
+
throw ownershipFailure('conflict', 'legacy committed target lacks matching local signed evidence');
|
|
812
|
+
}
|
|
813
|
+
else
|
|
814
|
+
await this.save(recovered);
|
|
815
|
+
handle.record = record;
|
|
816
|
+
handle.revision = recovered.revision;
|
|
386
817
|
}
|
|
387
818
|
async save(handle) {
|
|
388
819
|
try {
|
|
@@ -454,17 +885,36 @@ export class OwnershipWorkflow {
|
|
|
454
885
|
async start(operation, subAid, ownerAid, newOwnerAid, operationId) {
|
|
455
886
|
this.requireApplicable(subAid);
|
|
456
887
|
return this.runSerialized(subAid, async () => {
|
|
888
|
+
if (operation === 'bind' && !operationId)
|
|
889
|
+
await this.resetUnusableTasks(subAid, ownerAid);
|
|
457
890
|
const existing = operationId ? this.load(operationId) : null;
|
|
458
891
|
if (existing) {
|
|
892
|
+
if (this.isReset(existing.record))
|
|
893
|
+
throw ownershipFailure('conflict', 'ownership task was reset');
|
|
894
|
+
await this.upgradeLegacy(existing);
|
|
459
895
|
const record = existing.record;
|
|
896
|
+
if (!record.base_agent_md && record.request_verified)
|
|
897
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
|
|
460
898
|
if (record.operation !== operation || record.sub_aid !== subAid || record.owner_aid !== ownerAid || record.new_owner_aid !== newOwnerAid || record.initiator_aid !== this.local())
|
|
461
899
|
throw ownershipFailure('conflict', 'saved registration task context changed');
|
|
462
900
|
await this.drive(existing);
|
|
463
901
|
return ownershipTaskResult(record);
|
|
464
902
|
}
|
|
465
903
|
let ns;
|
|
466
|
-
for (const snapshot of this.list()) {
|
|
467
|
-
|
|
904
|
+
for (const snapshot of this.list(true)) {
|
|
905
|
+
if (snapshot.task.sub_aid !== subAid)
|
|
906
|
+
continue;
|
|
907
|
+
const active = this.decode(snapshot);
|
|
908
|
+
if (this.isReset(active.record))
|
|
909
|
+
continue;
|
|
910
|
+
if (!active.record.base_agent_md && active.record.request_verified && ['completed', 'stopped', 'rejected'].includes(active.record.status))
|
|
911
|
+
continue;
|
|
912
|
+
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()))
|
|
913
|
+
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
914
|
+
await this.upgradeLegacy(active);
|
|
915
|
+
const record = active.record;
|
|
916
|
+
if (!record.base_agent_md && record.request_verified)
|
|
917
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
|
|
468
918
|
if (record.sub_aid !== subAid || record.status !== 'pending' && !(record.status === 'error' && record.retryable))
|
|
469
919
|
continue;
|
|
470
920
|
const same = record.operation === operation && record.owner_aid === ownerAid && record.new_owner_aid === newOwnerAid && record.initiator_aid === this.local();
|
|
@@ -488,7 +938,7 @@ export class OwnershipWorkflow {
|
|
|
488
938
|
base = false;
|
|
489
939
|
}
|
|
490
940
|
}
|
|
491
|
-
const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.
|
|
941
|
+
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
942
|
if (!base && !target) {
|
|
493
943
|
await this.recordError(active, ownershipFailure('conflict', 'the operation was superseded by a different relation result'));
|
|
494
944
|
const result = ownershipTaskResult(record);
|
|
@@ -508,11 +958,11 @@ export class OwnershipWorkflow {
|
|
|
508
958
|
ns = await this.readNs(subAid);
|
|
509
959
|
if (operation === 'bind' && ns.lock !== 'unlocked')
|
|
510
960
|
throw ownershipFailure('owner_locked', 'the current owner relation is locked');
|
|
511
|
-
if (!ns.exists)
|
|
512
|
-
throw ownershipFailure('
|
|
513
|
-
if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type === 'human')
|
|
961
|
+
if (!ns.exists && operation !== 'bind')
|
|
962
|
+
throw ownershipFailure('conflict', 'original agent.md is missing');
|
|
963
|
+
if (operation === 'bind' && (ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type : subAid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
514
964
|
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
515
|
-
if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
|
|
965
|
+
if (operation === 'bind' && ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
|
|
516
966
|
throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
|
|
517
967
|
if (operation !== 'bind') {
|
|
518
968
|
const consensus = await this.deps.files.checkConsensus(subAid);
|
|
@@ -536,9 +986,14 @@ export class OwnershipWorkflow {
|
|
|
536
986
|
const initial = this.load(operationId);
|
|
537
987
|
if (!initial)
|
|
538
988
|
throw ownershipFailure('operation_not_found', 'no local ownership request matches operation_id');
|
|
989
|
+
if (this.isReset(initial.record))
|
|
990
|
+
throw ownershipFailure('conflict', 'ownership task was reset');
|
|
539
991
|
return this.runSerialized(initial.record.sub_aid, async () => {
|
|
540
992
|
const handle = this.load(operationId);
|
|
993
|
+
await this.upgradeLegacy(handle);
|
|
541
994
|
const record = handle.record;
|
|
995
|
+
if (!record.base_agent_md && record.request_verified)
|
|
996
|
+
throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
|
|
542
997
|
if (record.operation === 'release' || record.initiator_aid === this.local() || !record.request_verified)
|
|
543
998
|
throw ownershipFailure('forbidden', 'this task is not a verified local approval request');
|
|
544
999
|
if (record.local_decision !== 'pending') {
|
|
@@ -598,7 +1053,7 @@ export class OwnershipWorkflow {
|
|
|
598
1053
|
throw new Error('ownership outgoing action lacks participant approvals');
|
|
599
1054
|
}
|
|
600
1055
|
catch (error) {
|
|
601
|
-
throw
|
|
1056
|
+
throw new OwnershipTaskDecodeError(record.operation_id, error instanceof Error ? error.message : String(error));
|
|
602
1057
|
}
|
|
603
1058
|
}
|
|
604
1059
|
async flush(handle) {
|
|
@@ -613,7 +1068,7 @@ export class OwnershipWorkflow {
|
|
|
613
1068
|
}
|
|
614
1069
|
const ns = await this.readNs(record.sub_aid);
|
|
615
1070
|
const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
|
|
616
|
-
const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.
|
|
1071
|
+
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
1072
|
const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
|
|
618
1073
|
if (requiresTarget ? !target : !base) {
|
|
619
1074
|
record.outbox.shift();
|
|
@@ -629,6 +1084,7 @@ export class OwnershipWorkflow {
|
|
|
629
1084
|
if (expiresAt <= Math.trunc(Date.now() / 1000))
|
|
630
1085
|
throw ownershipFailure('conflict', 'transfer authorization expired');
|
|
631
1086
|
}
|
|
1087
|
+
this.assertActive(handle);
|
|
632
1088
|
await this.deps.send(message.to, message.payload);
|
|
633
1089
|
handle.record.outbox.shift();
|
|
634
1090
|
await this.save(handle);
|
|
@@ -645,7 +1101,7 @@ export class OwnershipWorkflow {
|
|
|
645
1101
|
return { ...ns, owner_aid: currentOwner };
|
|
646
1102
|
}
|
|
647
1103
|
async captureBase(record, ns) {
|
|
648
|
-
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
|
|
1104
|
+
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
649
1105
|
record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement };
|
|
650
1106
|
if (!statement) {
|
|
651
1107
|
record.base_owner_md = { exists: false, raw: '', sha256: '' };
|
|
@@ -693,9 +1149,9 @@ export class OwnershipWorkflow {
|
|
|
693
1149
|
}
|
|
694
1150
|
assertAgentBaseMaterial(record, ns) {
|
|
695
1151
|
const base = record.base_agent_md;
|
|
696
|
-
if (!base)
|
|
1152
|
+
if (!base || typeof base.raw !== 'string')
|
|
697
1153
|
throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
|
|
698
|
-
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
|
|
1154
|
+
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
699
1155
|
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
1156
|
if (ns.exists !== base.exists || !same)
|
|
701
1157
|
throw ownershipFailure('conflict', 'the original ownership statement was replaced');
|
|
@@ -723,6 +1179,17 @@ export class OwnershipWorkflow {
|
|
|
723
1179
|
continue;
|
|
724
1180
|
}
|
|
725
1181
|
const action = item.payload.action;
|
|
1182
|
+
if (action === 'owner.ready' && record.legacy_recovery && record.status === 'pending') {
|
|
1183
|
+
const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1184
|
+
if (!owner.exists)
|
|
1185
|
+
throw ownershipFailure(record.files.owner ? 'conflict' : 'unavailable', 'legacy owner confirmation declaration is missing', !record.files.owner);
|
|
1186
|
+
const file = await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
1187
|
+
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))
|
|
1188
|
+
throw ownershipFailure('conflict', 'legacy owner confirmation declaration changed');
|
|
1189
|
+
if (!record.files.owner)
|
|
1190
|
+
record.files.owner = { raw: owner.content, hash: owner.sha256, condition: {}, original_owner: { exists: true, raw: owner.content, sha256: owner.sha256 } };
|
|
1191
|
+
record.legacy_recovery.owner_confirmed = true;
|
|
1192
|
+
}
|
|
726
1193
|
if (['bind.request', 'transfer.request'].includes(action)) {
|
|
727
1194
|
if (!record.request_verified)
|
|
728
1195
|
await this.captureBase(record, ns);
|
|
@@ -844,6 +1311,7 @@ export class OwnershipWorkflow {
|
|
|
844
1311
|
const task = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid,
|
|
845
1312
|
new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
|
|
846
1313
|
files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at, base_agent_md: record.base_agent_md, base_owner_md: record.base_owner_md,
|
|
1314
|
+
assertActive: () => this.assertActive(handle),
|
|
847
1315
|
persist: async () => {
|
|
848
1316
|
record.authorization_sha256 = task.authorization_sha256;
|
|
849
1317
|
record.expires_at = task.expires_at;
|
|
@@ -853,8 +1321,18 @@ export class OwnershipWorkflow {
|
|
|
853
1321
|
} };
|
|
854
1322
|
return task;
|
|
855
1323
|
}
|
|
1324
|
+
assertActive(handle) {
|
|
1325
|
+
const current = this.load(handle.record.operation_id);
|
|
1326
|
+
if (!current || this.isReset(current.record) || current.revision !== handle.revision)
|
|
1327
|
+
throw ownershipFailure('conflict', 'ownership task changed before external action');
|
|
1328
|
+
}
|
|
856
1329
|
async drive(handle) {
|
|
1330
|
+
if (this.isReset(handle.record))
|
|
1331
|
+
return;
|
|
1332
|
+
await this.upgradeLegacy(handle);
|
|
857
1333
|
const record = handle.record;
|
|
1334
|
+
if (!record.base_agent_md && record.request_verified)
|
|
1335
|
+
return;
|
|
858
1336
|
if (record.status === 'error' && record.retryable) {
|
|
859
1337
|
record.status = 'pending';
|
|
860
1338
|
await this.save(handle);
|
|
@@ -907,6 +1385,8 @@ export class OwnershipWorkflow {
|
|
|
907
1385
|
const approved = participants(record).every((name) => record.approvals[name] === 'accept');
|
|
908
1386
|
if (record.operation === 'bind' && !approved || record.operation === 'transfer' && this.local() === record.owner_aid && !approved)
|
|
909
1387
|
return;
|
|
1388
|
+
if (record.legacy_recovery && this.local() === record.sub_aid && !record.legacy_recovery.owner_confirmed)
|
|
1389
|
+
return;
|
|
910
1390
|
delete record.code;
|
|
911
1391
|
delete record.reason;
|
|
912
1392
|
record.retryable = true;
|
|
@@ -937,6 +1417,11 @@ export class OwnershipWorkflow {
|
|
|
937
1417
|
await this.flush(handle);
|
|
938
1418
|
}
|
|
939
1419
|
if (this.local() === record.sub_aid) {
|
|
1420
|
+
if (record.legacy_recovery && record.files.owner) {
|
|
1421
|
+
const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid), pinned = record.files.owner;
|
|
1422
|
+
if (!current.exists || current.content !== pinned.raw || current.sha256 !== pinned.hash)
|
|
1423
|
+
throw ownershipFailure('conflict', 'legacy owner declaration changed before subject publication');
|
|
1424
|
+
}
|
|
940
1425
|
record.step = 'agent';
|
|
941
1426
|
await this.save(handle);
|
|
942
1427
|
await this.deps.files.declareOwneraid(this.fileTask(handle));
|
|
@@ -987,7 +1472,7 @@ export class OwnershipWorkflow {
|
|
|
987
1472
|
record.cleanup_status = 'cleaned';
|
|
988
1473
|
cleaned = true;
|
|
989
1474
|
}
|
|
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));
|
|
1475
|
+
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
1476
|
if (!reached)
|
|
992
1477
|
return false;
|
|
993
1478
|
if (record.operation === 'release' && !cleaned) {
|
|
@@ -1106,7 +1591,7 @@ export class OwnershipWorkflow {
|
|
|
1106
1591
|
}
|
|
1107
1592
|
await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: record.sub_aid, owner_aid: targetOwner(record),
|
|
1108
1593
|
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) });
|
|
1594
|
+
files: record.release_cleanup.files, expected_agent_statement: original, assertActive: () => this.assertActive(handle), persist: () => this.save(handle) });
|
|
1110
1595
|
record.release_cleanup.cleaned = true;
|
|
1111
1596
|
await this.save(handle);
|
|
1112
1597
|
this.emit(record, 'ownership.progress');
|
|
@@ -1169,8 +1654,8 @@ export class OwnershipWorkflow {
|
|
|
1169
1654
|
const payload = { ...ownershipTaskResult(record), local_roles: [...record.local_roles], ...extra };
|
|
1170
1655
|
if (event === 'ownership.request')
|
|
1171
1656
|
payload.requester_aid = record.initiator_aid;
|
|
1172
|
-
if (record.
|
|
1173
|
-
payload.relation_version = record.
|
|
1657
|
+
if (record.completion_evidence !== undefined)
|
|
1658
|
+
payload.relation_version = record.completion_evidence.relation_version;
|
|
1174
1659
|
queueMicrotask(() => { try {
|
|
1175
1660
|
const pending = this.deps.emit(event, payload);
|
|
1176
1661
|
if (pending)
|