@agentunion/fastaun-browser 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.d.ts +1 -0
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +5 -4
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +25 -1
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +148 -71
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +13 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +55 -8
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +2 -1
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +4 -0
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +4981 -1219
- package/dist/cert-utils.d.ts +1 -0
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +21 -0
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +101 -16
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1172 -160
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +4 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +122 -48
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts +22 -3
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +1258 -649
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts +23 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +391 -87
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +19 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +146 -23
- package/dist/events.js.map +1 -1
- package/dist/facades.d.ts +42 -1
- package/dist/facades.d.ts.map +1 -1
- package/dist/facades.js +363 -7
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/index.d.ts +14 -1
- package/dist/keystore/index.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.d.ts +4 -0
- package/dist/keystore/indexeddb-identity-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-identity-store.js +30 -13
- package/dist/keystore/indexeddb-identity-store.js.map +1 -1
- package/dist/keystore/indexeddb-shared.d.ts +19 -2
- package/dist/keystore/indexeddb-shared.d.ts.map +1 -1
- package/dist/keystore/indexeddb-shared.js +193 -6
- package/dist/keystore/indexeddb-shared.js.map +1 -1
- package/dist/keystore/indexeddb-token-store.d.ts +15 -2
- package/dist/keystore/indexeddb-token-store.d.ts.map +1 -1
- package/dist/keystore/indexeddb-token-store.js +308 -27
- package/dist/keystore/indexeddb-token-store.js.map +1 -1
- package/dist/org-protocol.d.ts +45 -0
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +757 -11
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +21 -4
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +49 -16
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +30 -0
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +412 -54
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/seq-tracker.d.ts +2 -0
- package/dist/seq-tracker.d.ts.map +1 -1
- package/dist/seq-tracker.js +3 -2
- package/dist/seq-tracker.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +60 -3
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +433 -37
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +3 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +62 -30
- package/dist/transport.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -27,6 +27,14 @@ export function parseOwnershipRegistrationIntent(raw, aid) {
|
|
|
27
27
|
function failure(code, reason, retryable = false) {
|
|
28
28
|
return new StateError(reason, { stringCode: code, retryable });
|
|
29
29
|
}
|
|
30
|
+
function storageFailure(reason) {
|
|
31
|
+
return Object.assign(failure('persistence_unavailable', reason), { ownershipStorageFailure: true });
|
|
32
|
+
}
|
|
33
|
+
function rawTaskRecord(snapshot) {
|
|
34
|
+
const raw = Object.hasOwn(snapshot, 'raw') ? snapshot.raw : snapshot;
|
|
35
|
+
const record = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw.record : undefined;
|
|
36
|
+
return record && typeof record === 'object' && !Array.isArray(record) ? record : undefined;
|
|
37
|
+
}
|
|
30
38
|
function participants(task) {
|
|
31
39
|
return [...new Set([task.owner_aid, task.sub_aid, ...(task.new_owner_aid ? [task.new_owner_aid] : [])])];
|
|
32
40
|
}
|
|
@@ -81,7 +89,8 @@ export function parseOwnershipMessage(content) {
|
|
|
81
89
|
ownershipAid(message.sub_aid, 'sub_aid');
|
|
82
90
|
ownershipAid(message.owner_aid, 'owner_aid');
|
|
83
91
|
ownershipInteger(message.base_relation_version, 'base_relation_version', 0);
|
|
84
|
-
|
|
92
|
+
const target = ownershipInteger(message.target_relation_version, 'target_relation_version');
|
|
93
|
+
if (target !== message.base_relation_version + 1 && !(message.operation === 'release' && target === message.base_relation_version))
|
|
85
94
|
throw new ValidationError('ownership target version does not match operation');
|
|
86
95
|
if (message.operation === 'transfer') {
|
|
87
96
|
if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
|
|
@@ -148,7 +157,7 @@ export function validateOwnershipMessage(message, context) {
|
|
|
148
157
|
if (ns.protocol !== '2.1')
|
|
149
158
|
throw failure('unsupported_protocol', 'NS ownership protocol is unavailable');
|
|
150
159
|
const base = ns.relation_version === message.base_relation_version && (message.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === message.owner_aid);
|
|
151
|
-
const target =
|
|
160
|
+
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);
|
|
152
161
|
const cleaned = message.action === 'sub.cleaned' && Boolean(task) && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid;
|
|
153
162
|
if (message.action !== 'operation.stopped' && (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target && !cleaned : !base && !target))
|
|
154
163
|
throw failure('conflict', 'ownership current NS relation does not match this task');
|
|
@@ -200,7 +209,7 @@ export class OwnershipWorkflow {
|
|
|
200
209
|
const store = this.deps.store;
|
|
201
210
|
for (const name of ['loadOwnershipTask', 'listOwnershipTasks', 'compareAndSwapOwnershipTask', 'acquireOwnershipTaskLease', 'renewOwnershipTaskLease', 'releaseOwnershipTaskLease']) {
|
|
202
211
|
if (typeof store[name] !== 'function')
|
|
203
|
-
throw
|
|
212
|
+
throw storageFailure('ownership durable task storage is unavailable');
|
|
204
213
|
}
|
|
205
214
|
return store;
|
|
206
215
|
}
|
|
@@ -209,11 +218,13 @@ export class OwnershipWorkflow {
|
|
|
209
218
|
return await this.storage().loadOwnershipTask(aid, operationId);
|
|
210
219
|
}
|
|
211
220
|
catch (error) {
|
|
212
|
-
throw
|
|
221
|
+
throw storageFailure(`ownership task read failed: ${String(error)}`);
|
|
213
222
|
}
|
|
214
223
|
}
|
|
215
|
-
async record(snapshot) {
|
|
224
|
+
async record(snapshot, legacy = false) {
|
|
216
225
|
const task = snapshot.record;
|
|
226
|
+
if (snapshot.record.reset === true && snapshot.revision > 0)
|
|
227
|
+
throw failure('conflict', 'ownership operation was reset');
|
|
217
228
|
const localAid = this.deps.currentAid();
|
|
218
229
|
try {
|
|
219
230
|
if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1)
|
|
@@ -229,8 +240,32 @@ export class OwnershipWorkflow {
|
|
|
229
240
|
throw new Error('incomplete task approvals or initiator decision');
|
|
230
241
|
if (!task.files || typeof task.files !== 'object' || Array.isArray(task.files) || !Array.isArray(task.inbox) || !Array.isArray(task.received) || !Array.isArray(task.outbox) || !Array.isArray(task.sent) || !task.hints || typeof task.hints !== 'object' || typeof task.request_validated !== 'boolean')
|
|
231
242
|
throw new Error('invalid task journals');
|
|
243
|
+
if (!task.base_agent_md && task.request_validated && !legacy && !task.legacy_task)
|
|
244
|
+
return task;
|
|
245
|
+
if (task.legacy_recovery) {
|
|
246
|
+
const recovery = task.legacy_recovery, original = recovery.task;
|
|
247
|
+
await this.validateLegacyBind(original);
|
|
248
|
+
if (!Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || recovery.task_sha256 !== await ownershipSha256(JSON.stringify(original)) || !this.sameIntent(task, original) || task.operation_id !== original.operation_id || task.base_relation_version !== original.base_relation_version || task.target_relation_version !== original.target_relation_version || task.created_at !== original.created_at || task.local_decision !== original.local_decision && original.local_decision !== 'pending' || Object.entries(original.approvals).some(([aid, decision]) => decision !== 'pending' && task.approvals[aid] !== decision) || !recovery.checkpoint || !Number.isSafeInteger(recovery.checkpoint.observed_at) || typeof recovery.owner_confirmed !== 'boolean')
|
|
249
|
+
throw new Error('invalid legacy recovery checkpoint');
|
|
250
|
+
const owner = recovery.checkpoint.owner_md;
|
|
251
|
+
if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? await ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
|
|
252
|
+
throw new Error('invalid legacy owner checkpoint');
|
|
253
|
+
if (task.local_roles.includes('sub') && (owner.exists || recovery.owner_confirmed) && (!task.files.owner || owner.exists && (task.files.owner.raw !== owner.raw || task.files.owner.sha256 !== owner.sha256)))
|
|
254
|
+
throw new Error('missing or changed legacy owner candidate');
|
|
255
|
+
}
|
|
256
|
+
if (task.legacy_task) {
|
|
257
|
+
const recovery = task.legacy_task, original = recovery.task;
|
|
258
|
+
if (!Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || recovery.task_sha256 !== await ownershipSha256(JSON.stringify(original)))
|
|
259
|
+
throw new Error('invalid legacy task source');
|
|
260
|
+
await this.priorView(original);
|
|
261
|
+
if (!this.sameIntent(task, original) || task.operation_id !== original.operation_id || task.base_relation_version !== original.base_relation_version || task.target_relation_version !== original.target_relation_version || task.created_at !== original.created_at || Object.entries(original.approvals).some(([aid, decision]) => decision !== 'pending' && task.approvals[aid] !== decision) || JSON.stringify(recovery.checkpoint.agent_md) !== JSON.stringify(task.base_agent_md ?? null) || JSON.stringify(recovery.checkpoint.owner_md) !== JSON.stringify(task.base_owner_md ?? null) || !task.base_agent_md && task.status !== 'completed')
|
|
262
|
+
throw new Error('legacy task fixed context changed');
|
|
263
|
+
for (const [key, prepared] of Object.entries(original.files))
|
|
264
|
+
if (task.files[key]?.raw !== prepared.raw || task.files[key]?.sha256 !== prepared.sha256 || ['agent', 'cleanup'].includes(key) && JSON.stringify(task.files[key]?.condition) !== JSON.stringify(prepared.condition))
|
|
265
|
+
throw new Error('legacy task signed candidate changed');
|
|
266
|
+
}
|
|
232
267
|
const base = task.base_agent_md;
|
|
233
|
-
if (!base && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
|
|
268
|
+
if (!base && !legacy && !task.legacy_task && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
|
|
234
269
|
throw new Error('missing original agent.md context');
|
|
235
270
|
if (base) {
|
|
236
271
|
if (typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || typeof base.etag !== 'string' || base.exists && !base.etag || base.sha256 !== await ownershipSha256(base.raw))
|
|
@@ -275,7 +310,7 @@ export class OwnershipWorkflow {
|
|
|
275
310
|
throw new Error('invalid prepared file operation');
|
|
276
311
|
const ownerAid = key === 'owner' || key === 'agent' ? targetOwner(task) : task.owner_aid;
|
|
277
312
|
const file = parseOwnershipFile(prepared.raw, agentFile ? 'agent' : 'owner', { expectedSubAid: task.sub_aid, expectedOwnerAid: key === 'cleanup' ? undefined : ownerAid });
|
|
278
|
-
if (!task.local_roles.includes(agentFile ? 'sub' : key === 'owner' && task.operation === 'transfer' ? 'new_owner' : 'owner'))
|
|
313
|
+
if (!task.local_roles.includes(agentFile ? 'sub' : key === 'owner' && task.operation === 'transfer' ? 'new_owner' : 'owner') && !(key === 'owner' && task.legacy_recovery && task.local_roles.includes('sub')))
|
|
279
314
|
throw new Error('invalid prepared file local role');
|
|
280
315
|
if (key === 'cleanup' ? Boolean(file.statement) : file.statement?.relation_version !== (key === 'owner' || key === 'agent' ? task.target_relation_version : task.base_relation_version))
|
|
281
316
|
throw new Error('invalid prepared file generation');
|
|
@@ -297,7 +332,7 @@ export class OwnershipWorkflow {
|
|
|
297
332
|
if (task.status === 'completed') {
|
|
298
333
|
const consensus = task.verified_consensus;
|
|
299
334
|
const cleaned = task.operation === 'release' && consensus?.status === 'unbound' && consensus.relation_version === 0 && Boolean(await this.cleanupEvidence(task, consensus));
|
|
300
|
-
if (!consensus || consensus.protocol !== '2.1' || consensus.sub_aid !== task.sub_aid || !cleaned && consensus.relation_version !== task.target_relation_version || typeof consensus.agent_md_etag !== 'string' || !consensus.agent_md_etag || !/^[0-9a-f]{64}$/.test(consensus.agent_md_sha256))
|
|
335
|
+
if (!consensus || consensus.protocol !== '2.1' || consensus.sub_aid !== task.sub_aid || !cleaned && consensus.relation_version !== (task.operation === 'release' ? task.base_relation_version : task.target_relation_version) || typeof consensus.agent_md_etag !== 'string' || !consensus.agent_md_etag || !/^[0-9a-f]{64}$/.test(consensus.agent_md_sha256))
|
|
301
336
|
throw new Error('invalid persisted consensus evidence');
|
|
302
337
|
if (task.operation === 'release') {
|
|
303
338
|
if (!cleaned && consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
|
|
@@ -306,7 +341,7 @@ export class OwnershipWorkflow {
|
|
|
306
341
|
else {
|
|
307
342
|
if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256))
|
|
308
343
|
throw new Error('invalid persisted binding evidence');
|
|
309
|
-
if (task.local_roles.includes('sub') && task.files.agent?.sha256 !== consensus.agent_md_sha256 || task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') && task.files.owner?.sha256 !== consensus.owner_md_sha256 || task.operation === 'transfer' && task.local_roles.includes('owner') && !task.files.transfer)
|
|
344
|
+
if (task.local_roles.includes('sub') && task.files.agent?.sha256 !== consensus.agent_md_sha256 || (task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') || task.legacy_recovery && task.local_roles.includes('sub')) && task.files.owner?.sha256 !== consensus.owner_md_sha256 || task.operation === 'transfer' && task.local_roles.includes('owner') && !task.files.transfer)
|
|
310
345
|
throw new Error('persisted consensus differs from local original files');
|
|
311
346
|
}
|
|
312
347
|
}
|
|
@@ -337,7 +372,9 @@ export class OwnershipWorkflow {
|
|
|
337
372
|
return task;
|
|
338
373
|
}
|
|
339
374
|
catch (error) {
|
|
340
|
-
|
|
375
|
+
if (error?.ownershipStorageFailure)
|
|
376
|
+
throw error;
|
|
377
|
+
throw Object.assign(failure('persistence_unavailable', `ownership task is damaged: ${String(error)}`), { ownershipTaskDamage: true });
|
|
341
378
|
}
|
|
342
379
|
}
|
|
343
380
|
message(task, action, reason) {
|
|
@@ -366,7 +403,7 @@ export class OwnershipWorkflow {
|
|
|
366
403
|
event(context, event, extra = {}) {
|
|
367
404
|
context.events.push({ event, payload: { ...ownershipTaskResult(context.task), initiator_aid: context.task.initiator_aid, local_roles: context.task.local_roles,
|
|
368
405
|
...(event === 'ownership.request' ? { requester_aid: context.task.initiator_aid } : {}),
|
|
369
|
-
...(context.task.
|
|
406
|
+
...(context.task.verified_consensus === undefined ? {} : { relation_version: context.task.verified_consensus.relation_version }), ...extra } });
|
|
370
407
|
}
|
|
371
408
|
async save(context) {
|
|
372
409
|
context.guard?.();
|
|
@@ -437,22 +474,32 @@ export class OwnershipWorkflow {
|
|
|
437
474
|
if (!saved && !initial)
|
|
438
475
|
throw failure('operation_not_found', 'ownership operation was not found');
|
|
439
476
|
if (!saved && initial) {
|
|
440
|
-
const existing = (await this.records(aid, subAid)).find((task) => task.sub_aid === subAid && this.ongoing(task));
|
|
477
|
+
const existing = (await this.records(aid, subAid, undefined, true)).find((task) => task.sub_aid === subAid && this.ongoing(task));
|
|
441
478
|
if (existing) {
|
|
442
479
|
if (!this.sameIntent(existing, initial) || existing.base_relation_version !== initial.base_relation_version || existing.target_relation_version !== initial.target_relation_version)
|
|
443
480
|
throw failure('conflict', 'another local operation for this sub is pending');
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
481
|
+
if (initial.initiator_aid === aid) {
|
|
482
|
+
try {
|
|
483
|
+
await store.releaseOwnershipTaskLease(aid, lease);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
throw failure('persistence_unavailable', `ownership sub lease release failed: ${String(error)}`);
|
|
487
|
+
}
|
|
488
|
+
return { result: await this.advance(subAid, existing.operation_id, undefined, events, undefined, guard), events: [] };
|
|
449
489
|
}
|
|
450
|
-
return { result: await this.advance(subAid, existing.operation_id, undefined, events, undefined, guard), events: [] };
|
|
451
490
|
}
|
|
452
491
|
}
|
|
453
492
|
context = { aid, task: saved ? await this.record(saved) : initial, revision: saved?.revision ?? null, lease, events: [], guard };
|
|
454
493
|
if (context.task.sub_aid !== subAid)
|
|
455
494
|
throw failure('conflict', 'ownership task sub changed');
|
|
495
|
+
if (saved && !context.task.base_agent_md && context.task.request_validated) {
|
|
496
|
+
if (!this.ongoing(context.task))
|
|
497
|
+
return { result: ownershipTaskResult(context.task), events: context.events };
|
|
498
|
+
if (context.task.operation === 'bind' && context.task.initiator_aid === context.task.sub_aid && context.task.base_relation_version === 0 && context.task.target_relation_version === 1 && Object.keys(context.task.files).every((key) => key === 'owner'))
|
|
499
|
+
await this.recoverLegacyBind(context);
|
|
500
|
+
else
|
|
501
|
+
await this.recoverPrior(context);
|
|
502
|
+
}
|
|
456
503
|
if (!saved)
|
|
457
504
|
await this.save(context);
|
|
458
505
|
await mutate?.(context);
|
|
@@ -478,15 +525,32 @@ export class OwnershipWorkflow {
|
|
|
478
525
|
sameIntent(task, intent) {
|
|
479
526
|
return task.initiator_aid === intent.initiator_aid && task.operation === intent.operation && task.sub_aid === intent.sub_aid && task.owner_aid === intent.owner_aid && task.new_owner_aid === intent.new_owner_aid;
|
|
480
527
|
}
|
|
481
|
-
async records(aid, subAid) {
|
|
528
|
+
async records(aid, subAid, operations, strictSub = false) {
|
|
482
529
|
let snapshots;
|
|
483
530
|
try {
|
|
484
|
-
snapshots = await this.storage().listOwnershipTasks(aid);
|
|
531
|
+
snapshots = await this.storage().listOwnershipTasks(aid, true);
|
|
485
532
|
}
|
|
486
533
|
catch (error) {
|
|
487
|
-
throw
|
|
534
|
+
throw storageFailure(`ownership task list failed: ${String(error)}`);
|
|
535
|
+
}
|
|
536
|
+
const records = [];
|
|
537
|
+
for (const snapshot of snapshots) {
|
|
538
|
+
if (snapshot.record.reset === true && snapshot.revision > 0)
|
|
539
|
+
continue;
|
|
540
|
+
if (subAid && typeof snapshot.record.sub_aid === 'string' && snapshot.record.sub_aid !== subAid)
|
|
541
|
+
continue;
|
|
542
|
+
if (operations && typeof snapshot.record.operation === 'string' && !operations.includes(snapshot.record.operation))
|
|
543
|
+
continue;
|
|
544
|
+
try {
|
|
545
|
+
records.push(await this.record(snapshot));
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
await this.deps.emit('ownership.error', { operation_id: snapshot.record.operation_id, sub_aid: snapshot.record.sub_aid, code: ownershipErrorCode(error), reason: String(error), retryable: false });
|
|
549
|
+
if (strictSub && snapshot.record.sub_aid === subAid)
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
488
552
|
}
|
|
489
|
-
return
|
|
553
|
+
return records;
|
|
490
554
|
}
|
|
491
555
|
async observe(subAid) {
|
|
492
556
|
this.requireApplicable(subAid);
|
|
@@ -512,8 +576,142 @@ export class OwnershipWorkflow {
|
|
|
512
576
|
task.base_owner_md = prior ? { exists: prior.exists, raw: prior.content, sha256: prior.sha256 } : { exists: false, raw: '', sha256: '' };
|
|
513
577
|
}
|
|
514
578
|
}
|
|
579
|
+
async validateLegacyBind(task) {
|
|
580
|
+
const aid = this.deps.currentAid();
|
|
581
|
+
if (task.base_agent_md != null || task.base_owner_md != null || task.schema_version !== 1 || !this.ongoing(task) || task.operation !== 'bind' || task.initiator_aid !== task.sub_aid || task.base_relation_version !== 0 || task.target_relation_version !== 1 || task.new_owner_aid !== undefined || !task.request_validated || task.approvals[aid] !== task.local_decision || Object.values(task.approvals).includes('reject') || !Number.isSafeInteger(task.created_at) || task.created_at <= 0 || task.release_cleanup !== undefined || task.verified_consensus !== undefined || task.cleanup_pending || task.cleanup_done || task.authorization_sha256 !== undefined || task.expires_at !== undefined || task.legacy_recovery !== undefined)
|
|
582
|
+
throw failure('persistence_unavailable', 'legacy bind context cannot be recovered');
|
|
583
|
+
parseOwnershipMessage(this.message(task, 'bind.request'));
|
|
584
|
+
if (JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, aid)) || !task.local_roles.length || JSON.stringify(Object.keys(task.approvals).sort()) !== JSON.stringify(participants(task).sort()) || task.approvals[task.sub_aid] !== 'accept' || Object.values(task.approvals).some((value) => !['pending', 'accept'].includes(value)))
|
|
585
|
+
throw failure('persistence_unavailable', 'legacy bind approval is damaged');
|
|
586
|
+
for (const [key, prepared] of Object.entries(task.files)) {
|
|
587
|
+
const condition = prepared.condition, published = prepared.published_revision;
|
|
588
|
+
if (key !== 'owner' || !task.local_roles.includes('owner') || task.local_decision !== 'accept' || typeof prepared.raw !== 'string' || prepared.sha256 !== await ownershipSha256(prepared.raw) || !condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision || published !== undefined && (typeof published !== 'string' || !published) || prepared.expires_at !== undefined || prepared.cleaned)
|
|
589
|
+
throw failure('persistence_unavailable', 'legacy bind file cannot be reused');
|
|
590
|
+
const file = parseOwnershipFile(prepared.raw, 'owner', { expectedSubAid: task.sub_aid, expectedOwnerAid: task.owner_aid });
|
|
591
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
592
|
+
throw failure('persistence_unavailable', 'legacy owner declaration differs');
|
|
593
|
+
}
|
|
594
|
+
const validate = (payload, from, to) => {
|
|
595
|
+
if (!payload || ['bind.reject', 'operation.stopped'].includes(payload.action))
|
|
596
|
+
throw failure('persistence_unavailable', 'legacy bind contains terminal or mixed controls');
|
|
597
|
+
const message = parseOwnershipMessage({ ...payload });
|
|
598
|
+
validateOwnershipMessage(message, { authenticated_from_aid: from, local_aid: to, task });
|
|
599
|
+
};
|
|
600
|
+
for (const key of task.received) {
|
|
601
|
+
if (typeof key !== 'string' || key.split('|').length !== 2)
|
|
602
|
+
throw failure('persistence_unavailable', 'legacy received journal is damaged');
|
|
603
|
+
const [from, action] = key.split('|'), payload = this.message(task, action);
|
|
604
|
+
validate(payload, from, aid);
|
|
605
|
+
}
|
|
606
|
+
for (const item of task.inbox) {
|
|
607
|
+
validate(item.payload, item.from_aid, aid);
|
|
608
|
+
if (!task.received.includes(`${item.from_aid}|${item.payload.action}`))
|
|
609
|
+
throw failure('persistence_unavailable', 'legacy inbox is damaged');
|
|
610
|
+
}
|
|
611
|
+
for (const item of task.outbox) {
|
|
612
|
+
validate(item.payload, aid, item.to_aid);
|
|
613
|
+
if (item.key !== `${item.to_aid}|${item.payload.action}` || !/^m-[0-9a-f]{32}$/.test(item.message_id) || item.payload.action !== 'bind.request' && task.local_decision !== 'accept')
|
|
614
|
+
throw failure('persistence_unavailable', 'legacy outbox is damaged');
|
|
615
|
+
}
|
|
616
|
+
for (const key of task.sent) {
|
|
617
|
+
if (typeof key !== 'string' || key.split('|').length !== 2)
|
|
618
|
+
throw failure('persistence_unavailable', 'legacy sent journal is damaged');
|
|
619
|
+
const [to, action] = key.split('|'), payload = this.message(task, action);
|
|
620
|
+
validate(payload, aid, to);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
async recoverLegacyBind(context) {
|
|
624
|
+
const original = context.task;
|
|
625
|
+
await this.validateLegacyBind(original);
|
|
626
|
+
const before = await this.observe(original.sub_aid);
|
|
627
|
+
if (before.lock === 'locked' && before.owner_aid === original.owner_aid && before.relation_version === original.target_relation_version)
|
|
628
|
+
return this.recoverPrior(context);
|
|
629
|
+
if (before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || before.file?.statement)
|
|
630
|
+
throw failure('conflict', 'legacy bind relationship base changed');
|
|
631
|
+
const owner = await this.deps.files.readOwner(original.owner_aid, original.sub_aid), prepared = original.files.owner;
|
|
632
|
+
if (prepared && (!owner.exists || owner.content !== prepared.raw || owner.sha256 !== prepared.sha256) || !prepared && original.local_roles.includes('owner') && owner.exists)
|
|
633
|
+
throw failure('conflict', 'legacy owner original cannot be confirmed');
|
|
634
|
+
const publishedRevision = prepared?.published_revision;
|
|
635
|
+
if (publishedRevision !== undefined && owner.revision !== publishedRevision)
|
|
636
|
+
throw failure(owner.revision ? 'conflict' : 'unavailable', 'legacy published owner revision changed', !owner.revision);
|
|
637
|
+
if (owner.exists) {
|
|
638
|
+
const file = await this.deps.files.verify(owner.content, 'owner', original.sub_aid, original.owner_aid);
|
|
639
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
640
|
+
throw failure('conflict', 'legacy owner declaration changed');
|
|
641
|
+
}
|
|
642
|
+
const serialized = JSON.stringify(original);
|
|
643
|
+
const candidate = prepared ?? (original.local_roles.includes('sub') && owner.exists ? { raw: owner.content, sha256: owner.sha256 } : undefined);
|
|
644
|
+
const originalOwner = { exists: owner.exists, raw: owner.content, sha256: owner.sha256, ...(prepared && owner.revision ? { revision: owner.revision } : {}) };
|
|
645
|
+
const task = { ...original, status: 'pending', step: 'compatibility_checkpoint', retryable: true, files: candidate ? { owner: { raw: candidate.raw, sha256: candidate.sha256, condition: {}, original_owner: originalOwner } } : {}, inbox: [], received: [], outbox: [], sent: [], hints: {}, legacy_recovery: { task: JSON.parse(serialized), revision: context.revision, task_sha256: await ownershipSha256(serialized), checkpoint: { observed_at: before.observed_at, owner_md: originalOwner }, owner_confirmed: false } };
|
|
646
|
+
delete task.code;
|
|
647
|
+
delete task.reason;
|
|
648
|
+
delete task.base_agent_md;
|
|
649
|
+
delete task.base_owner_md;
|
|
650
|
+
await this.captureBase(task, before);
|
|
651
|
+
const afterOwner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid), after = await this.observe(task.sub_aid);
|
|
652
|
+
if (owner.exists !== afterOwner.exists || owner.content !== afterOwner.content || owner.sha256 !== afterOwner.sha256 || before.exists !== after.exists || before.content !== after.content || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid)
|
|
653
|
+
throw failure('conflict', 'legacy compatibility observations changed');
|
|
654
|
+
if (task.local_roles.includes('sub'))
|
|
655
|
+
this.enqueue(task, task.owner_aid, 'bind.request');
|
|
656
|
+
else if (task.local_decision === 'accept')
|
|
657
|
+
this.enqueue(task, task.sub_aid, 'bind.accept');
|
|
658
|
+
context.task = task;
|
|
659
|
+
await this.save(context);
|
|
660
|
+
}
|
|
515
661
|
isTarget(task, ns) {
|
|
516
|
-
return ns.relation_version === task.target_relation_version && (task.operation === 'release' ? ns.lock === 'unlocked' : ns.lock === 'locked' && ns.owner_aid === targetOwner(task));
|
|
662
|
+
return ns.relation_version === (task.operation === 'release' ? task.base_relation_version : task.target_relation_version) && (task.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === targetOwner(task));
|
|
663
|
+
}
|
|
664
|
+
async priorView(original) {
|
|
665
|
+
if (original.base_agent_md != null || original.base_owner_md != null || original.legacy_task || original.legacy_recovery || !original.request_validated || !this.ongoing(original) || Object.values(original.approvals ?? {}).includes('reject'))
|
|
666
|
+
throw failure('persistence_unavailable', 'legacy task context cannot be recovered');
|
|
667
|
+
const task = structuredClone(original);
|
|
668
|
+
for (const [key, prepared] of Object.entries(task.files ?? {})) {
|
|
669
|
+
if (['agent', 'cleanup'].includes(key))
|
|
670
|
+
continue;
|
|
671
|
+
const condition = prepared.condition;
|
|
672
|
+
if (!prepared.original_owner && (!condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision))
|
|
673
|
+
throw failure('persistence_unavailable', 'legacy owner condition is invalid');
|
|
674
|
+
prepared.condition = {};
|
|
675
|
+
prepared.original_owner ??= { exists: false, raw: '', sha256: '' };
|
|
676
|
+
}
|
|
677
|
+
await this.record({ revision: 1, record: task }, true);
|
|
678
|
+
return task;
|
|
679
|
+
}
|
|
680
|
+
async recoverPrior(context) {
|
|
681
|
+
const original = structuredClone(context.task), task = await this.priorView(original);
|
|
682
|
+
const ns = await this.observe(task.sub_aid);
|
|
683
|
+
const base = this.isBase(task, ns), released = task.operation === 'release' && this.isTarget(task, ns);
|
|
684
|
+
const committed = task.operation !== 'release' && this.isTarget(task, ns);
|
|
685
|
+
if (!base && !released && !committed)
|
|
686
|
+
throw failure('conflict', 'legacy relationship no longer matches this task');
|
|
687
|
+
if (!committed)
|
|
688
|
+
await this.captureBase(task, ns);
|
|
689
|
+
for (const [key, prepared] of Object.entries(task.files)) {
|
|
690
|
+
const agent = ['agent', 'cleanup'].includes(key), ownerAid = ['agent', 'owner'].includes(key) ? targetOwner(task) : task.owner_aid;
|
|
691
|
+
await this.deps.files.verify(prepared.raw, agent ? 'agent' : 'owner', task.sub_aid, agent && key === 'cleanup' ? undefined : ownerAid);
|
|
692
|
+
if (agent)
|
|
693
|
+
continue;
|
|
694
|
+
const current = await this.deps.files.readOwner(ownerAid, task.sub_aid), source = original.files[key];
|
|
695
|
+
const revision = source.condition.expected_revision;
|
|
696
|
+
if (revision && (current.content === prepared.raw && source.published_revision !== undefined || !committed && current.content !== prepared.raw)) {
|
|
697
|
+
if (!current.revision)
|
|
698
|
+
throw failure('unavailable', 'legacy owner original revision is unavailable');
|
|
699
|
+
if (current.revision !== (current.content === prepared.raw ? source.published_revision : revision))
|
|
700
|
+
throw failure('conflict', 'legacy owner original revision changed');
|
|
701
|
+
}
|
|
702
|
+
const statement = current.exists ? parseOwnershipFile(current.content, 'owner', { expectedSubAid: task.sub_aid }).statement : undefined;
|
|
703
|
+
if (current.content !== prepared.raw && (source.published_revision !== undefined || current.exists && (key === 'owner' || !this.sameStatement(statement, parseOwnershipFile(prepared.raw, 'owner', { expectedSubAid: task.sub_aid }).statement))) && !committed)
|
|
704
|
+
throw failure('conflict', 'legacy signed owner file was replaced');
|
|
705
|
+
prepared.original_owner = { exists: current.exists, raw: current.content, sha256: current.sha256, ...(revision && current.revision ? { revision: current.revision } : {}) };
|
|
706
|
+
}
|
|
707
|
+
task.legacy_task = { task: original, revision: context.revision, task_sha256: await ownershipSha256(JSON.stringify(original)), checkpoint: { agent_md: task.base_agent_md ?? null, owner_md: task.base_owner_md ?? null } };
|
|
708
|
+
context.task = task;
|
|
709
|
+
if (committed) {
|
|
710
|
+
if (!await this.complete(context, ns))
|
|
711
|
+
throw failure('conflict', 'legacy committed target lacks matching local signed evidence');
|
|
712
|
+
}
|
|
713
|
+
else
|
|
714
|
+
await this.save(context);
|
|
517
715
|
}
|
|
518
716
|
fileTask(context, expectedStatement) {
|
|
519
717
|
const task = { ...context.task, files: context.task.files, expected_agent_statement: expectedStatement,
|
|
@@ -552,7 +750,11 @@ export class OwnershipWorkflow {
|
|
|
552
750
|
}
|
|
553
751
|
}
|
|
554
752
|
const intent = { initiator_aid: aid, operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid };
|
|
555
|
-
|
|
753
|
+
if (operation === 'bind' && requestedId === undefined)
|
|
754
|
+
await this.prepareBind(subAid, intent, guard);
|
|
755
|
+
for (const pending of (await this.records(aid, subAid, undefined, true)).filter((task) => task.sub_aid === subAid && this.ongoing(task))) {
|
|
756
|
+
if (!pending.base_agent_md && pending.request_validated && !this.sameIntent(pending, intent))
|
|
757
|
+
throw failure('conflict', 'another local legacy operation for this sub is pending');
|
|
556
758
|
if (this.sameIntent(pending, intent)) {
|
|
557
759
|
const result = await this.advance(subAid, pending.operation_id, undefined, events, undefined, guard);
|
|
558
760
|
if (result.status !== 'stopped' || result.code !== 'conflict' || !/relation version or target was replaced/.test(result.reason ?? ''))
|
|
@@ -569,11 +771,9 @@ export class OwnershipWorkflow {
|
|
|
569
771
|
}
|
|
570
772
|
const ns = await this.observe(subAid);
|
|
571
773
|
if (operation === 'bind') {
|
|
572
|
-
if (!ns.exists)
|
|
573
|
-
throw failure('unavailable', 'initial agent.md is not published', true);
|
|
574
774
|
if (ns.lock === 'locked')
|
|
575
775
|
throw failure('owner_locked', 'the sub AID has a current owner lock');
|
|
576
|
-
if (ns.file?.frontmatter.type === 'human')
|
|
776
|
+
if ((ns.exists ? ns.file?.frontmatter.type : subAid === aid ? this.deps.currentAidType?.() : undefined) === 'human')
|
|
577
777
|
throw failure('invalid', 'human cannot declare an owner');
|
|
578
778
|
if (ns.file?.statement?.owner_aid === ownerAid)
|
|
579
779
|
throw failure('conflict', 'current owner release requires sub cleanup before binding again');
|
|
@@ -611,6 +811,128 @@ export class OwnershipWorkflow {
|
|
|
611
811
|
async releaseSubaid(subAid) { return await this.start('release', subAid, this.deps.currentAid()); }
|
|
612
812
|
async transferSubaid(subAid, newOwnerAid) { return await this.start('transfer', subAid, this.deps.currentAid(), newOwnerAid); }
|
|
613
813
|
async startRegistrationIntent(ownerAid, operationId, guard) { return await this.start('bind', this.deps.currentAid(), ownerAid, undefined, operationId, guard); }
|
|
814
|
+
async resetTask(operationId, expected) {
|
|
815
|
+
const aid = this.deps.currentAid(), store = this.deps.store;
|
|
816
|
+
if (typeof store.resetOwnershipTask !== 'function')
|
|
817
|
+
throw storageFailure('ownership task reset storage is unavailable');
|
|
818
|
+
let result;
|
|
819
|
+
try {
|
|
820
|
+
result = await store.resetOwnershipTask(aid, operationId, expected);
|
|
821
|
+
}
|
|
822
|
+
catch (error) {
|
|
823
|
+
if (error instanceof StateError)
|
|
824
|
+
throw error;
|
|
825
|
+
throw storageFailure(`ownership task reset failed: ${String(error)}`);
|
|
826
|
+
}
|
|
827
|
+
if (aid !== this.deps.currentAid())
|
|
828
|
+
throw failure('forbidden', 'ownership current identity changed');
|
|
829
|
+
return result;
|
|
830
|
+
}
|
|
831
|
+
async resetOwnershipTask() {
|
|
832
|
+
const aid = this.deps.currentAid(), reset = new Set(), skipped = new Set();
|
|
833
|
+
if (typeof this.deps.store.resetOwnershipTask !== 'function')
|
|
834
|
+
throw storageFailure('ownership task reset storage is unavailable');
|
|
835
|
+
const list = async () => {
|
|
836
|
+
try {
|
|
837
|
+
return await this.storage().listOwnershipTasks(aid, true);
|
|
838
|
+
}
|
|
839
|
+
catch (error) {
|
|
840
|
+
throw storageFailure(`ownership task list failed: ${String(error)}`);
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
for (const original of await list()) {
|
|
844
|
+
let snapshot = original;
|
|
845
|
+
for (;;) {
|
|
846
|
+
if (aid !== this.deps.currentAid())
|
|
847
|
+
throw failure('forbidden', 'ownership current identity changed');
|
|
848
|
+
if (snapshot.record.reset === true && snapshot.revision > 0)
|
|
849
|
+
break;
|
|
850
|
+
const operationId = snapshot.record.operation_id;
|
|
851
|
+
if (typeof operationId !== 'string')
|
|
852
|
+
throw storageFailure('ownership task identity cannot be determined for reset');
|
|
853
|
+
if (!OWNERSHIP_UUID_V4_RE.test(operationId)) {
|
|
854
|
+
skipped.add(operationId);
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
const record = rawTaskRecord(snapshot);
|
|
858
|
+
if (record && (Object.hasOwn(record, 'reset') || Object.hasOwn(record, 'reset_archive'))) {
|
|
859
|
+
skipped.add(operationId);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
let initiator;
|
|
863
|
+
try {
|
|
864
|
+
initiator = ownershipAid(record?.initiator_aid, 'initiator_aid');
|
|
865
|
+
}
|
|
866
|
+
catch { }
|
|
867
|
+
const status = record?.status;
|
|
868
|
+
if (!initiator || typeof status !== 'string' || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(status)) {
|
|
869
|
+
skipped.add(operationId);
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
872
|
+
if (initiator !== aid || status !== 'pending' && status !== 'error')
|
|
873
|
+
break;
|
|
874
|
+
try {
|
|
875
|
+
await this.resetTask(operationId, snapshot);
|
|
876
|
+
reset.add(operationId);
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
if (!error?.ownershipResetConflict)
|
|
881
|
+
throw error;
|
|
882
|
+
const current = (await list()).find((row) => row.record.operation_id === operationId);
|
|
883
|
+
if (!current)
|
|
884
|
+
throw storageFailure('ownership task disappeared during reset');
|
|
885
|
+
snapshot = current;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
return { reset_operation_ids: [...reset].sort(), skipped_operation_ids: [...skipped].sort() };
|
|
890
|
+
}
|
|
891
|
+
async prepareBind(subAid, intent, guard) {
|
|
892
|
+
const aid = this.deps.currentAid();
|
|
893
|
+
let snapshots;
|
|
894
|
+
try {
|
|
895
|
+
snapshots = await this.storage().listOwnershipTasks(aid, true);
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
throw storageFailure(`ownership task list failed: ${String(error)}`);
|
|
899
|
+
}
|
|
900
|
+
for (const snapshot of snapshots) {
|
|
901
|
+
guard?.();
|
|
902
|
+
if (aid !== this.deps.currentAid())
|
|
903
|
+
throw failure('forbidden', 'ownership current identity changed');
|
|
904
|
+
if (snapshot.record.reset === true && snapshot.revision > 0)
|
|
905
|
+
continue;
|
|
906
|
+
if (typeof snapshot.record.sub_aid !== 'string')
|
|
907
|
+
throw storageFailure('ownership task subject cannot be determined for automatic reset');
|
|
908
|
+
if (snapshot.record.sub_aid !== subAid)
|
|
909
|
+
continue;
|
|
910
|
+
const raw = rawTaskRecord(snapshot);
|
|
911
|
+
if (raw && (Object.hasOwn(raw, 'reset') || Object.hasOwn(raw, 'reset_archive')))
|
|
912
|
+
throw storageFailure('ownership reset persistence is damaged');
|
|
913
|
+
let task;
|
|
914
|
+
try {
|
|
915
|
+
task = await this.record(snapshot);
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
if (!error?.ownershipTaskDamage)
|
|
919
|
+
throw error;
|
|
920
|
+
await this.resetTask(String(snapshot.record.operation_id), snapshot);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (!task.base_agent_md && task.request_validated && this.sameIntent(task, intent) && (this.ongoing(task) || task.status === 'stopped' && task.code === 'unsupported_protocol')) {
|
|
924
|
+
try {
|
|
925
|
+
if (task.operation === 'bind' && task.initiator_aid === task.sub_aid && task.base_relation_version === 0 && Object.keys(task.files ?? {}).every((key) => key === 'owner'))
|
|
926
|
+
await this.validateLegacyBind(task);
|
|
927
|
+
else
|
|
928
|
+
await this.priorView(task);
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
await this.resetTask(task.operation_id, snapshot);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
614
936
|
async checkOwnershipConsensus(subAid = this.deps.currentAid()) { return await this.deps.files.checkConsensus(subAid); }
|
|
615
937
|
async decide(operationId, decision, reason) {
|
|
616
938
|
const saved = await this.load(operationId);
|
|
@@ -676,6 +998,8 @@ export class OwnershipWorkflow {
|
|
|
676
998
|
throw error;
|
|
677
999
|
}
|
|
678
1000
|
const snapshot = await this.load(message.operation_id);
|
|
1001
|
+
if (snapshot?.record.reset === true && snapshot.revision > 0)
|
|
1002
|
+
return true;
|
|
679
1003
|
const prior = snapshot ? await this.record(snapshot) : undefined;
|
|
680
1004
|
if (!prior && !['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
|
|
681
1005
|
return true;
|
|
@@ -742,15 +1066,19 @@ export class OwnershipWorkflow {
|
|
|
742
1066
|
guard?.();
|
|
743
1067
|
let snapshots;
|
|
744
1068
|
try {
|
|
745
|
-
snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid());
|
|
1069
|
+
snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid(), true);
|
|
746
1070
|
}
|
|
747
1071
|
catch (error) {
|
|
748
1072
|
throw failure('persistence_unavailable', `ownership task list failed: ${String(error)}`);
|
|
749
1073
|
}
|
|
750
1074
|
for (const snapshot of snapshots) {
|
|
751
1075
|
guard?.();
|
|
1076
|
+
if (snapshot.record.reset === true && snapshot.revision > 0)
|
|
1077
|
+
continue;
|
|
752
1078
|
try {
|
|
753
1079
|
const task = await this.record(snapshot);
|
|
1080
|
+
if (!task.base_agent_md && task.request_validated && task.status === 'completed')
|
|
1081
|
+
continue;
|
|
754
1082
|
if (['rejected', 'stopped'].includes(task.status) && !task.outbox.length && !task.inbox.length || task.status === 'completed' && !task.cleanup_pending && !task.outbox.length && !task.inbox.length && !(['bind', 'transfer'].includes(task.operation) && task.local_roles.includes('sub') && task.local_decision === 'accept' && !task.release_cleanup?.cleaned))
|
|
755
1083
|
continue;
|
|
756
1084
|
await this.execute(task.sub_aid, task.operation_id, undefined, undefined, guard);
|
|
@@ -837,6 +1165,20 @@ export class OwnershipWorkflow {
|
|
|
837
1165
|
}
|
|
838
1166
|
}
|
|
839
1167
|
else {
|
|
1168
|
+
if (action === 'owner.ready' && task.legacy_recovery) {
|
|
1169
|
+
const owner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
1170
|
+
const pinned = task.files.owner;
|
|
1171
|
+
if (pinned && (!owner.exists || owner.content !== pinned.raw || owner.sha256 !== pinned.sha256))
|
|
1172
|
+
throw failure('conflict', 'legacy owner candidate was replaced');
|
|
1173
|
+
if (!owner.exists)
|
|
1174
|
+
throw failure('unavailable', 'legacy owner confirmation is unavailable', true);
|
|
1175
|
+
const file = await this.deps.files.verify(owner.content, 'owner', task.sub_aid, task.owner_aid);
|
|
1176
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== task.target_relation_version)
|
|
1177
|
+
throw failure('conflict', 'legacy owner confirmation differs');
|
|
1178
|
+
if (!pinned)
|
|
1179
|
+
task.files.owner = { raw: owner.content, sha256: owner.sha256, condition: {}, original_owner: { exists: true, raw: owner.content, sha256: owner.sha256 } };
|
|
1180
|
+
task.legacy_recovery.owner_confirmed = true;
|
|
1181
|
+
}
|
|
840
1182
|
if (action === 'transfer.ready' && this.isBase(task, ns)) {
|
|
841
1183
|
const authorization = await this.deps.files.verifyTransfer(this.fileTask(context));
|
|
842
1184
|
task.expires_at = authorization.transfer.expires_at;
|
|
@@ -856,11 +1198,29 @@ export class OwnershipWorkflow {
|
|
|
856
1198
|
async flush(context, ns) {
|
|
857
1199
|
const task = context.task;
|
|
858
1200
|
for (const item of [...task.outbox]) {
|
|
1201
|
+
ns = await this.observe(task.sub_aid);
|
|
859
1202
|
const action = item.payload.action;
|
|
860
1203
|
const notification = ['relation.confirmed', 'sub.cleaned'].includes(action);
|
|
861
1204
|
const terminal = ['bind.reject', 'transfer.reject', 'operation.stopped'].includes(action);
|
|
862
1205
|
const cleaned = action === 'sub.cleaned' && task.status === 'completed' && await this.confirmSubCleaned(task);
|
|
863
|
-
|
|
1206
|
+
let base = this.isBase(task, ns);
|
|
1207
|
+
if (!notification && base && task.request_validated && task.base_agent_md?.statement && task.base_owner_md) {
|
|
1208
|
+
const object = await this.deps.files.readOwner(task.base_agent_md.statement.owner_aid, task.sub_aid), original = task.base_owner_md;
|
|
1209
|
+
const prepared = task.files[task.operation === 'transfer' ? 'transfer' : task.operation === 'release' ? 'release' : 'owner'];
|
|
1210
|
+
base = object.exists === original.exists && object.content === original.raw && object.sha256 === original.sha256 || Boolean(prepared && object.exists && object.content === prepared.raw && object.sha256 === prepared.sha256);
|
|
1211
|
+
if (!base && task.operation === 'transfer' && !task.local_roles.includes('owner') && task.local_decision === 'accept') {
|
|
1212
|
+
try {
|
|
1213
|
+
await this.deps.files.verifyTransfer(this.fileTask(context));
|
|
1214
|
+
base = true;
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
if (ownershipErrorCode(error) !== 'conflict')
|
|
1218
|
+
throw error;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
const target = notification && this.isTarget(task, ns) && (task.operation === 'release' ? this.sameStatement(task.base_agent_md?.statement ?? undefined, ns.file?.statement) : !task.files.agent || this.sameStatement(parseOwnershipFile(task.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement, ns.file?.statement));
|
|
1223
|
+
if (notification ? !target && !cleaned : !base || !terminal && (task.status === 'completed' || task.status === 'rejected' || task.status === 'stopped')) {
|
|
864
1224
|
task.outbox = task.outbox.filter((row) => row.key !== item.key);
|
|
865
1225
|
await this.save(context);
|
|
866
1226
|
continue;
|
|
@@ -889,7 +1249,7 @@ export class OwnershipWorkflow {
|
|
|
889
1249
|
let consensus;
|
|
890
1250
|
if (task.operation === 'release') {
|
|
891
1251
|
consensus = await this.deps.files.checkConsensus(task.sub_aid);
|
|
892
|
-
if ((cleaned ? consensus.status !== 'unbound' || consensus.relation_version !== 0 : consensus.status !== 'released' || consensus.relation_version !== task.
|
|
1252
|
+
if ((cleaned ? consensus.status !== 'unbound' || consensus.relation_version !== 0 : consensus.status !== 'released' || consensus.relation_version !== task.base_relation_version) || consensus.lock !== ns.lock || consensus.owner_aid !== ns.owner_aid || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== await ownershipSha256(ns.content))
|
|
893
1253
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership release could not be freshly confirmed', consensus.status !== 'invalid');
|
|
894
1254
|
if (!cleaned && task.files.release && consensus.owner_md_sha256) {
|
|
895
1255
|
const source = task.files.release;
|
|
@@ -939,7 +1299,7 @@ export class OwnershipWorkflow {
|
|
|
939
1299
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership target materials could not be verified', consensus.status !== 'invalid');
|
|
940
1300
|
if (task.local_roles.includes('sub') && (!task.files.agent || task.files.agent.sha256 !== consensus.agent_md_sha256))
|
|
941
1301
|
throw failure('conflict', 'NS accepted agent.md does not match this task original bytes');
|
|
942
|
-
if (task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') && task.files.owner?.sha256 !== consensus.owner_md_sha256)
|
|
1302
|
+
if ((task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') || task.legacy_recovery && task.local_roles.includes('sub')) && task.files.owner?.sha256 !== consensus.owner_md_sha256)
|
|
943
1303
|
throw failure('conflict', 'Storage owner.md does not match this task original bytes');
|
|
944
1304
|
if (task.local_roles.includes('sub') && task.files.agent.published_etag !== undefined && task.files.agent.published_etag !== consensus.agent_md_etag && task.verified_relation_version === undefined)
|
|
945
1305
|
throw failure('conflict', 'NS accepted agent.md revision changed');
|
|
@@ -975,19 +1335,9 @@ export class OwnershipWorkflow {
|
|
|
975
1335
|
const matches = async (prepared) => Boolean(prepared && prepared.sha256 === evidence.agent_md_sha256 && prepared.sha256 === await ownershipSha256(prepared.raw) && (raw === undefined || prepared.raw === raw) && (prepared.published_etag === undefined || prepared.published_etag === evidence.agent_md_etag));
|
|
976
1336
|
if (await matches(task.files.cleanup))
|
|
977
1337
|
return task.operation_id;
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
history = await this.storage().listOwnershipTasks(this.deps.currentAid());
|
|
981
|
-
}
|
|
982
|
-
catch (error) {
|
|
983
|
-
throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
|
|
984
|
-
}
|
|
985
|
-
for (const snapshot of history) {
|
|
986
|
-
if (!['bind', 'transfer'].includes(String(snapshot.record.operation)))
|
|
987
|
-
continue;
|
|
988
|
-
if (snapshot.record.sub_aid !== task.sub_aid)
|
|
1338
|
+
for (const candidate of await this.records(this.deps.currentAid(), task.sub_aid, ['bind', 'transfer'])) {
|
|
1339
|
+
if (!['bind', 'transfer'].includes(candidate.operation))
|
|
989
1340
|
continue;
|
|
990
|
-
const candidate = await this.record(snapshot);
|
|
991
1341
|
if (task.cleanup_authorization_operation_id && candidate.operation_id !== task.cleanup_authorization_operation_id || candidate.local_decision !== 'accept' || !candidate.local_roles.includes('sub') || candidate.sub_aid !== task.sub_aid || targetOwner(candidate) !== task.owner_aid || candidate.target_relation_version !== task.base_relation_version || !await matches(candidate.release_cleanup?.files.cleanup))
|
|
992
1342
|
continue;
|
|
993
1343
|
const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
|
|
@@ -1008,14 +1358,7 @@ export class OwnershipWorkflow {
|
|
|
1008
1358
|
throw failure('persistence_unavailable', 'ownership cleanup historical authorization is damaged');
|
|
1009
1359
|
return true;
|
|
1010
1360
|
}
|
|
1011
|
-
|
|
1012
|
-
try {
|
|
1013
|
-
history = await this.storage().listOwnershipTasks(context.aid);
|
|
1014
|
-
}
|
|
1015
|
-
catch (error) {
|
|
1016
|
-
throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
|
|
1017
|
-
}
|
|
1018
|
-
const authorized = (await Promise.all(history.filter((row) => row.record.sub_aid === task.sub_aid).map((row) => this.record(row)))).find(matches);
|
|
1361
|
+
const authorized = (await this.records(context.aid, task.sub_aid, ['bind', 'transfer'])).find(matches);
|
|
1019
1362
|
if (!authorized)
|
|
1020
1363
|
return false;
|
|
1021
1364
|
task.cleanup_authorization_operation_id = authorized.operation_id;
|
|
@@ -1147,13 +1490,28 @@ export class OwnershipWorkflow {
|
|
|
1147
1490
|
const accepted = participants(task).every((aid) => task.approvals[aid] === 'accept');
|
|
1148
1491
|
if (task.operation === 'bind') {
|
|
1149
1492
|
if (task.local_roles.includes('owner') && accepted) {
|
|
1150
|
-
|
|
1493
|
+
if (task.legacy_recovery && !task.files.owner) {
|
|
1494
|
+
const owner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid), checkpoint = task.legacy_recovery.checkpoint.owner_md;
|
|
1495
|
+
if (owner.exists !== checkpoint.exists || owner.content !== checkpoint.raw || owner.sha256 !== checkpoint.sha256)
|
|
1496
|
+
throw failure('conflict', 'legacy owner checkpoint was replaced');
|
|
1497
|
+
}
|
|
1498
|
+
const prepared = await this.deps.files.declareSubaid(this.fileTask(context));
|
|
1151
1499
|
this.enqueue(task, task.sub_aid, 'owner.ready');
|
|
1152
|
-
if (task.local_roles.includes('sub'))
|
|
1500
|
+
if (task.local_roles.includes('sub')) {
|
|
1501
|
+
if (task.legacy_recovery) {
|
|
1502
|
+
const owner = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
1503
|
+
if (!owner.exists || owner.content !== prepared.raw || owner.sha256 !== prepared.sha256)
|
|
1504
|
+
throw failure('conflict', 'legacy local owner confirmation differs');
|
|
1505
|
+
const file = await this.deps.files.verify(owner.content, 'owner', task.sub_aid, task.owner_aid);
|
|
1506
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== task.target_relation_version)
|
|
1507
|
+
throw failure('conflict', 'legacy local owner declaration differs');
|
|
1508
|
+
task.legacy_recovery.owner_confirmed = true;
|
|
1509
|
+
}
|
|
1153
1510
|
task.hints['owner.ready'] = true;
|
|
1511
|
+
}
|
|
1154
1512
|
await this.save(context);
|
|
1155
1513
|
}
|
|
1156
|
-
if (task.local_roles.includes('sub') && (task.hints['owner.ready'] || task.approvals[task.owner_aid] === 'accept' || task.files.agent)) {
|
|
1514
|
+
if (task.local_roles.includes('sub') && (task.hints['owner.ready'] || !task.legacy_recovery && task.approvals[task.owner_aid] === 'accept' || task.files.agent) && (!task.legacy_recovery || task.legacy_recovery.owner_confirmed && accepted || task.files.agent)) {
|
|
1157
1515
|
await this.deps.files.declareOwneraid(this.fileTask(context));
|
|
1158
1516
|
}
|
|
1159
1517
|
task.step = accepted ? 'awaiting_confirmation' : 'awaiting_peer_approval';
|