@agentunion/fastaun-browser 0.5.19 → 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 +176 -78
- package/_packed_docs/CHANGELOG.md +176 -78
- package/_packed_docs/INDEX.md +288 -269
- package/_packed_docs/KITE_DOCS_GUIDE.md +101 -95
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- 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/agent.md//350/277/234/347/250/213agent.md/347/274/223/345/255/230/344/270/216etag/351/200/217/344/274/240/346/226/271/346/241/210.md +11 -11
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +372 -331
- package/_packed_docs/protocol/08-AUN-E2EE.md +17 -12
- package/_packed_docs/protocol/10-Group-/345/255/220/345/215/217/350/256/256.md +676 -676
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +8 -12
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/211.md +445 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.0.md +161 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +666 -0
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2113.0.md +233 -0
- package/_packed_docs/protocol/agent.md/SCHEMA.md +1 -1
- 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 +70 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +43 -40
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +179 -19
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +50 -49
- package/_packed_docs/sdk/09-group-rpc-manual.md +261 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +253 -160
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- 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 +105 -92
- package/_packed_docs/sdk/INDEX.md +197 -167
- package/_packed_docs/sdk/Ownership-SDK/345/256/236/347/216/260/346/214/207/345/215/227.md +169 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/227.md +349 -0
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +930 -0
- package/_packed_docs/sdk/Ownership-SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +270 -0
- 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 +363 -0
- 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 +553 -0
- package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +708 -0
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +480 -0
- package/_packed_docs/sdk/Ownership/347/256/241/347/220/206/344/270/223/351/242/230.md +559 -0
- package/_packed_docs/sdk/ProtectedHeaders/347/224/250/346/263/225/344/270/223/351/242/230.md +299 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +173 -0
- package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +211 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/345/256/236/347/216/260/347/247/273/346/244/215/346/214/207/345/215/227.md +331 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273SDK/351/233/206/346/210/220/346/214/207/345/215/227.md +207 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/344/273/243/347/240/201/345/256/241/346/237/245/346/212/245/345/221/212.md +252 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/345/256/236/346/226/275/350/256/241/345/210/222.md +156 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/346/226/271/346/241/210.md +273 -0
- package/_packed_docs//350/267/250/350/256/276/345/244/207/345/267/262/350/257/273/346/234/252/350/257/273/350/257/246/347/273/206/350/256/276/350/256/241.md +211 -0
- package/dist/agent-md-schema.d.ts +16 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +65 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +35 -3
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +384 -150
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +59 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +202 -16
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.d.ts +4 -1
- package/dist/aid.d.ts.map +1 -1
- package/dist/aid.js +24 -42
- package/dist/aid.js.map +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +89 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +6410 -1848
- package/dist/cert-utils.d.ts +50 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +123 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +108 -16
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +1411 -184
- 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 +203 -117
- 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 +1312 -684
- 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 +419 -101
- 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/crypto.d.ts.map +1 -1
- package/dist/crypto.js +8 -6
- package/dist/crypto.js.map +1 -1
- package/dist/events.d.ts +24 -3
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +171 -29
- 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 +368 -12
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +6 -4
- 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 +827 -21
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +43 -9
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +183 -81
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +36 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +610 -89
- 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 +61 -3
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +524 -45
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +4 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +64 -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,8 +89,9 @@ 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
|
-
|
|
85
|
-
|
|
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))
|
|
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)
|
|
88
97
|
throw new ValidationError('transfer target must differ from current owner');
|
|
@@ -148,8 +157,9 @@ 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 =
|
|
152
|
-
|
|
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);
|
|
161
|
+
const cleaned = message.action === 'sub.cleaned' && Boolean(task) && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid;
|
|
162
|
+
if (message.action !== 'operation.stopped' && (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target && !cleaned : !base && !target))
|
|
153
163
|
throw failure('conflict', 'ownership current NS relation does not match this task');
|
|
154
164
|
}
|
|
155
165
|
}
|
|
@@ -199,7 +209,7 @@ export class OwnershipWorkflow {
|
|
|
199
209
|
const store = this.deps.store;
|
|
200
210
|
for (const name of ['loadOwnershipTask', 'listOwnershipTasks', 'compareAndSwapOwnershipTask', 'acquireOwnershipTaskLease', 'renewOwnershipTaskLease', 'releaseOwnershipTaskLease']) {
|
|
201
211
|
if (typeof store[name] !== 'function')
|
|
202
|
-
throw
|
|
212
|
+
throw storageFailure('ownership durable task storage is unavailable');
|
|
203
213
|
}
|
|
204
214
|
return store;
|
|
205
215
|
}
|
|
@@ -208,17 +218,21 @@ export class OwnershipWorkflow {
|
|
|
208
218
|
return await this.storage().loadOwnershipTask(aid, operationId);
|
|
209
219
|
}
|
|
210
220
|
catch (error) {
|
|
211
|
-
throw
|
|
221
|
+
throw storageFailure(`ownership task read failed: ${String(error)}`);
|
|
212
222
|
}
|
|
213
223
|
}
|
|
214
|
-
async record(snapshot) {
|
|
224
|
+
async record(snapshot, legacy = false) {
|
|
215
225
|
const task = snapshot.record;
|
|
226
|
+
if (snapshot.record.reset === true && snapshot.revision > 0)
|
|
227
|
+
throw failure('conflict', 'ownership operation was reset');
|
|
216
228
|
const localAid = this.deps.currentAid();
|
|
217
229
|
try {
|
|
230
|
+
if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1)
|
|
231
|
+
throw new Error('invalid persisted task revision');
|
|
218
232
|
if (task.schema_version !== 1 || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(task.status) || !['pending', 'accept', 'reject'].includes(task.local_decision) || typeof task.step !== 'string')
|
|
219
233
|
throw new Error('invalid task state');
|
|
220
234
|
parseOwnershipMessage({ ...this.message(task, task.operation === 'bind' ? 'bind.request' : task.operation === 'transfer' ? 'transfer.request' : 'relation.confirmed') });
|
|
221
|
-
if (!participants(task).includes(task.initiator_aid) || JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, localAid)) || !task.local_roles.length)
|
|
235
|
+
if (!participants(task).includes(task.initiator_aid) || task.operation !== 'bind' && task.initiator_aid !== task.owner_aid || JSON.stringify(task.local_roles) !== JSON.stringify(ownershipLocalRoles(task, localAid)) || !task.local_roles.length)
|
|
222
236
|
throw new Error('invalid local task identity');
|
|
223
237
|
if (!task.approvals || typeof task.approvals !== 'object' || Array.isArray(task.approvals) || Object.entries(task.approvals).some(([aid, decision]) => !participants(task).includes(aid) || !['pending', 'accept', 'reject'].includes(decision)))
|
|
224
238
|
throw new Error('invalid task approvals');
|
|
@@ -226,6 +240,54 @@ export class OwnershipWorkflow {
|
|
|
226
240
|
throw new Error('incomplete task approvals or initiator decision');
|
|
227
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')
|
|
228
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
|
+
}
|
|
267
|
+
const base = task.base_agent_md;
|
|
268
|
+
if (!base && !legacy && !task.legacy_task && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
|
|
269
|
+
throw new Error('missing original agent.md context');
|
|
270
|
+
if (base) {
|
|
271
|
+
if (typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || typeof base.etag !== 'string' || base.exists && !base.etag || base.sha256 !== await ownershipSha256(base.raw))
|
|
272
|
+
throw new Error('invalid original agent.md bytes or ETag');
|
|
273
|
+
const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: task.sub_aid }).statement : undefined;
|
|
274
|
+
if ((statement?.relation_version ?? 0) !== task.base_relation_version || statement && task.operation !== 'bind' && statement.owner_aid !== task.owner_aid || (statement ? !this.sameStatement(statement, base.statement ?? undefined) : base.statement !== null))
|
|
275
|
+
throw new Error('invalid original agent.md statement');
|
|
276
|
+
}
|
|
277
|
+
if (task.base_owner_md) {
|
|
278
|
+
const prior = task.base_owner_md;
|
|
279
|
+
if (typeof prior.exists !== 'boolean' || typeof prior.raw !== 'string' || prior.sha256 !== (prior.exists ? await ownershipSha256(prior.raw) : '') || !prior.exists && prior.raw !== '')
|
|
280
|
+
throw new Error('invalid original owner.md context');
|
|
281
|
+
if (base?.statement && !prior.exists && (task.operation === 'transfer' || task.operation === 'release' && task.local_roles.includes('owner')))
|
|
282
|
+
throw new Error('missing original owner.md relation');
|
|
283
|
+
if (prior.exists) {
|
|
284
|
+
const file = parseOwnershipFile(prior.raw, 'owner', { expectedSubAid: task.sub_aid });
|
|
285
|
+
if (!file.statement || file.statement.owner_aid !== base?.statement?.owner_aid || file.statement.relation_version !== base?.statement?.relation_version)
|
|
286
|
+
throw new Error('invalid original owner.md statement');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (base?.statement && !task.base_owner_md && !(task.operation === 'release' && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && task.cleanup_authorization_operation_id && !task.files.cleanup))
|
|
290
|
+
throw new Error('missing original owner.md context');
|
|
229
291
|
if (task.approvals[localAid] !== task.local_decision)
|
|
230
292
|
throw new Error('task decision and approval differ');
|
|
231
293
|
if (task.status === 'completed' && (task.verified_relation_version !== task.target_relation_version || task.operation !== 'release' && task.local_decision !== 'accept'))
|
|
@@ -239,20 +301,23 @@ export class OwnershipWorkflow {
|
|
|
239
301
|
if (!['agent', 'owner', 'release', 'transfer', 'cleanup'].includes(key) || !prepared || typeof prepared.raw !== 'string' || !/^[0-9a-f]{64}$/.test(prepared.sha256) || await ownershipSha256(prepared.raw) !== prepared.sha256 || !prepared.condition || typeof prepared.condition !== 'object' || Array.isArray(prepared.condition))
|
|
240
302
|
throw new Error('invalid prepared file bytes');
|
|
241
303
|
const condition = prepared.condition;
|
|
242
|
-
if (Object.keys(condition).length !== 1 ||
|
|
304
|
+
if (agentFile ? Object.keys(condition).length !== 1 || !(typeof condition.if_match === 'string' && condition.if_match || condition.if_none_match === '*') : Object.keys(condition).length !== 0)
|
|
243
305
|
throw new Error('invalid prepared file condition');
|
|
306
|
+
const original = prepared.original_owner;
|
|
307
|
+
if (!agentFile && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? await ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
|
|
308
|
+
throw new Error('invalid prepared owner original');
|
|
244
309
|
if (key === 'cleanup' && task.operation !== 'release' || key === 'release' && task.operation !== 'release' || key === 'transfer' && task.operation !== 'transfer' || ['agent', 'owner'].includes(key) && task.operation === 'release')
|
|
245
310
|
throw new Error('invalid prepared file operation');
|
|
246
311
|
const ownerAid = key === 'owner' || key === 'agent' ? targetOwner(task) : task.owner_aid;
|
|
247
312
|
const file = parseOwnershipFile(prepared.raw, agentFile ? 'agent' : 'owner', { expectedSubAid: task.sub_aid, expectedOwnerAid: key === 'cleanup' ? undefined : ownerAid });
|
|
248
|
-
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')))
|
|
249
314
|
throw new Error('invalid prepared file local role');
|
|
250
315
|
if (key === 'cleanup' ? Boolean(file.statement) : file.statement?.relation_version !== (key === 'owner' || key === 'agent' ? task.target_relation_version : task.base_relation_version))
|
|
251
316
|
throw new Error('invalid prepared file generation');
|
|
252
317
|
const fileExpiry = prepared.expires_at;
|
|
253
318
|
if (!agentFile && file.action !== (key === 'owner' ? 'declare' : key === 'release' ? 'release' : 'transfer_owner') || key === 'transfer' && (typeof fileExpiry !== 'number' || !Number.isSafeInteger(fileExpiry) || fileExpiry <= 0 || fileExpiry !== task.expires_at || file.transfer?.new_owner_aid !== task.new_owner_aid || file.transfer?.target_relation_version !== task.target_relation_version || file.transfer?.expires_at !== fileExpiry) || key !== 'transfer' && fileExpiry !== undefined)
|
|
254
319
|
throw new Error('invalid prepared file authorization');
|
|
255
|
-
if (prepared.
|
|
320
|
+
if (prepared.published_etag !== undefined && (!agentFile || typeof prepared.published_etag !== 'string' || !prepared.published_etag) || prepared.cleaned !== undefined && typeof prepared.cleaned !== 'boolean')
|
|
256
321
|
throw new Error('invalid prepared file result');
|
|
257
322
|
}
|
|
258
323
|
if (task.release_cleanup !== undefined) {
|
|
@@ -266,19 +331,27 @@ export class OwnershipWorkflow {
|
|
|
266
331
|
}
|
|
267
332
|
if (task.status === 'completed') {
|
|
268
333
|
const consensus = task.verified_consensus;
|
|
269
|
-
|
|
334
|
+
const cleaned = task.operation === 'release' && consensus?.status === 'unbound' && consensus.relation_version === 0 && Boolean(await this.cleanupEvidence(task, consensus));
|
|
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))
|
|
270
336
|
throw new Error('invalid persisted consensus evidence');
|
|
271
337
|
if (task.operation === 'release') {
|
|
272
|
-
if (consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
|
|
338
|
+
if (!cleaned && consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
|
|
273
339
|
throw new Error('invalid persisted release evidence');
|
|
274
340
|
}
|
|
275
341
|
else {
|
|
276
|
-
if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) ||
|
|
342
|
+
if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256))
|
|
277
343
|
throw new Error('invalid persisted binding evidence');
|
|
278
|
-
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)
|
|
279
345
|
throw new Error('persisted consensus differs from local original files');
|
|
280
346
|
}
|
|
281
347
|
}
|
|
348
|
+
for (const key of task.received) {
|
|
349
|
+
if (typeof key !== 'string' || key.split('|').length !== 2)
|
|
350
|
+
throw new Error('invalid received journal');
|
|
351
|
+
const [fromAid, action] = key.split('|');
|
|
352
|
+
const message = parseOwnershipMessage(this.message(task, action));
|
|
353
|
+
validateOwnershipMessage(message, { authenticated_from_aid: fromAid, local_aid: localAid, task });
|
|
354
|
+
}
|
|
282
355
|
for (const item of task.inbox) {
|
|
283
356
|
parseOwnershipMessage(item.payload);
|
|
284
357
|
validateOwnershipMessage(item.payload, { authenticated_from_aid: item.from_aid, local_aid: localAid, task });
|
|
@@ -287,6 +360,11 @@ export class OwnershipWorkflow {
|
|
|
287
360
|
}
|
|
288
361
|
for (const item of task.outbox) {
|
|
289
362
|
parseOwnershipMessage(item.payload);
|
|
363
|
+
const action = item.payload.action;
|
|
364
|
+
if (['bind.accept', 'transfer.accept', 'owner.ready', 'transfer.ready'].includes(action) && task.local_decision !== 'accept' || ['bind.reject', 'transfer.reject'].includes(action) && task.local_decision !== 'reject')
|
|
365
|
+
throw new Error('invalid outbox local approval');
|
|
366
|
+
if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action) && !task.request_validated)
|
|
367
|
+
throw new Error('invalid outbox received request');
|
|
290
368
|
validateOwnershipMessage(item.payload, { authenticated_from_aid: localAid, local_aid: item.to_aid, task });
|
|
291
369
|
if (item.key !== `${item.to_aid}|${item.payload.action}` || typeof item.message_id !== 'string' || !/^m-[0-9a-f]{32}$/.test(item.message_id))
|
|
292
370
|
throw new Error('invalid outbox target');
|
|
@@ -294,7 +372,9 @@ export class OwnershipWorkflow {
|
|
|
294
372
|
return task;
|
|
295
373
|
}
|
|
296
374
|
catch (error) {
|
|
297
|
-
|
|
375
|
+
if (error?.ownershipStorageFailure)
|
|
376
|
+
throw error;
|
|
377
|
+
throw Object.assign(failure('persistence_unavailable', `ownership task is damaged: ${String(error)}`), { ownershipTaskDamage: true });
|
|
298
378
|
}
|
|
299
379
|
}
|
|
300
380
|
message(task, action, reason) {
|
|
@@ -323,7 +403,7 @@ export class OwnershipWorkflow {
|
|
|
323
403
|
event(context, event, extra = {}) {
|
|
324
404
|
context.events.push({ event, payload: { ...ownershipTaskResult(context.task), initiator_aid: context.task.initiator_aid, local_roles: context.task.local_roles,
|
|
325
405
|
...(event === 'ownership.request' ? { requester_aid: context.task.initiator_aid } : {}),
|
|
326
|
-
...(context.task.
|
|
406
|
+
...(context.task.verified_consensus === undefined ? {} : { relation_version: context.task.verified_consensus.relation_version }), ...extra } });
|
|
327
407
|
}
|
|
328
408
|
async save(context) {
|
|
329
409
|
context.guard?.();
|
|
@@ -394,22 +474,32 @@ export class OwnershipWorkflow {
|
|
|
394
474
|
if (!saved && !initial)
|
|
395
475
|
throw failure('operation_not_found', 'ownership operation was not found');
|
|
396
476
|
if (!saved && initial) {
|
|
397
|
-
const existing = (await this.records(aid)).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));
|
|
398
478
|
if (existing) {
|
|
399
479
|
if (!this.sameIntent(existing, initial) || existing.base_relation_version !== initial.base_relation_version || existing.target_relation_version !== initial.target_relation_version)
|
|
400
480
|
throw failure('conflict', 'another local operation for this sub is pending');
|
|
401
|
-
|
|
402
|
-
|
|
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: [] };
|
|
403
489
|
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
throw failure('persistence_unavailable', `ownership sub lease release failed: ${String(error)}`);
|
|
406
|
-
}
|
|
407
|
-
return { result: await this.advance(subAid, existing.operation_id, undefined, events, undefined, guard), events: [] };
|
|
408
490
|
}
|
|
409
491
|
}
|
|
410
492
|
context = { aid, task: saved ? await this.record(saved) : initial, revision: saved?.revision ?? null, lease, events: [], guard };
|
|
411
493
|
if (context.task.sub_aid !== subAid)
|
|
412
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
|
+
}
|
|
413
503
|
if (!saved)
|
|
414
504
|
await this.save(context);
|
|
415
505
|
await mutate?.(context);
|
|
@@ -435,40 +525,198 @@ export class OwnershipWorkflow {
|
|
|
435
525
|
sameIntent(task, intent) {
|
|
436
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;
|
|
437
527
|
}
|
|
438
|
-
async records(aid) {
|
|
528
|
+
async records(aid, subAid, operations, strictSub = false) {
|
|
439
529
|
let snapshots;
|
|
440
530
|
try {
|
|
441
|
-
snapshots = await this.storage().listOwnershipTasks(aid);
|
|
531
|
+
snapshots = await this.storage().listOwnershipTasks(aid, true);
|
|
442
532
|
}
|
|
443
533
|
catch (error) {
|
|
444
|
-
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
|
+
}
|
|
445
552
|
}
|
|
446
|
-
return
|
|
553
|
+
return records;
|
|
447
554
|
}
|
|
448
555
|
async observe(subAid) {
|
|
449
556
|
this.requireApplicable(subAid);
|
|
450
557
|
const snapshot = await this.deps.files.readAgent(subAid);
|
|
451
|
-
const file = snapshot.exists ? snapshot.
|
|
452
|
-
? parseOwnershipFile(snapshot.content, 'agent', { expectedSubAid: subAid })
|
|
453
|
-
: await this.deps.files.verify(snapshot.content, 'agent', subAid) : null;
|
|
558
|
+
const file = snapshot.exists ? await this.deps.files.verify(snapshot.content, 'agent', subAid) : null;
|
|
454
559
|
if (file)
|
|
455
560
|
requireOwnershipApplicable(file.frontmatter.type);
|
|
456
|
-
if (
|
|
561
|
+
if ((file?.statement?.relation_version ?? 0) !== snapshot.relation_version)
|
|
457
562
|
throw failure('unavailable', 'NS relation metadata does not match agent.md', true);
|
|
458
563
|
if (snapshot.lock === 'locked' && (!file?.statement || file.statement.relation_version !== snapshot.relation_version || snapshot.owner_aid !== undefined && snapshot.owner_aid !== file.statement.owner_aid))
|
|
459
564
|
throw failure('unavailable', 'NS lock and current signed agent.md differ', true);
|
|
460
565
|
return { ...snapshot, file, owner_aid: snapshot.lock === 'locked' ? file.statement.owner_aid : null };
|
|
461
566
|
}
|
|
462
567
|
isBase(task, ns) {
|
|
463
|
-
|
|
568
|
+
const base = task.base_agent_md;
|
|
569
|
+
return ns.relation_version === task.base_relation_version && (task.operation === 'bind' ? ns.lock === 'unlocked' : ns.lock === 'locked' && ns.owner_aid === task.owner_aid) && (!base || base.exists === ns.exists && (base.statement ? this.sameStatement(base.statement, ns.file?.statement) : !ns.file?.statement && base.raw === ns.content && base.etag === ns.etag));
|
|
570
|
+
}
|
|
571
|
+
async captureBase(task, ns) {
|
|
572
|
+
if (!task.base_agent_md)
|
|
573
|
+
task.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: await ownershipSha256(ns.content), etag: ns.etag, statement: ns.file?.statement ?? null };
|
|
574
|
+
if (!task.base_owner_md) {
|
|
575
|
+
const prior = ns.file?.statement ? await this.deps.files.readOwner(ns.file.statement.owner_aid, task.sub_aid) : null;
|
|
576
|
+
task.base_owner_md = prior ? { exists: prior.exists, raw: prior.content, sha256: prior.sha256 } : { exists: false, raw: '', sha256: '' };
|
|
577
|
+
}
|
|
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);
|
|
464
660
|
}
|
|
465
661
|
isTarget(task, ns) {
|
|
466
|
-
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));
|
|
467
663
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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);
|
|
715
|
+
}
|
|
716
|
+
fileTask(context, expectedStatement) {
|
|
717
|
+
const task = { ...context.task, files: context.task.files, expected_agent_statement: expectedStatement,
|
|
718
|
+
persist: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; context.task.base_agent_md = task.base_agent_md; context.task.base_owner_md = task.base_owner_md; await this.save(context); },
|
|
719
|
+
guard: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; context.task.base_agent_md = task.base_agent_md; context.task.base_owner_md = task.base_owner_md; await this.save(context); } };
|
|
472
720
|
return task;
|
|
473
721
|
}
|
|
474
722
|
async start(operation, subAid, ownerAid, newOwnerAid, requestedId, guard) {
|
|
@@ -502,7 +750,11 @@ export class OwnershipWorkflow {
|
|
|
502
750
|
}
|
|
503
751
|
}
|
|
504
752
|
const intent = { initiator_aid: aid, operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid };
|
|
505
|
-
|
|
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');
|
|
506
758
|
if (this.sameIntent(pending, intent)) {
|
|
507
759
|
const result = await this.advance(subAid, pending.operation_id, undefined, events, undefined, guard);
|
|
508
760
|
if (result.status !== 'stopped' || result.code !== 'conflict' || !/relation version or target was replaced/.test(result.reason ?? ''))
|
|
@@ -519,12 +771,12 @@ export class OwnershipWorkflow {
|
|
|
519
771
|
}
|
|
520
772
|
const ns = await this.observe(subAid);
|
|
521
773
|
if (operation === 'bind') {
|
|
522
|
-
if (!ns.exists)
|
|
523
|
-
throw failure('unavailable', 'initial agent.md is not published', true);
|
|
524
774
|
if (ns.lock === 'locked')
|
|
525
775
|
throw failure('owner_locked', 'the sub AID has a current owner lock');
|
|
526
|
-
if (ns.file?.frontmatter.type === 'human')
|
|
776
|
+
if ((ns.exists ? ns.file?.frontmatter.type : subAid === aid ? this.deps.currentAidType?.() : undefined) === 'human')
|
|
527
777
|
throw failure('invalid', 'human cannot declare an owner');
|
|
778
|
+
if (ns.file?.statement?.owner_aid === ownerAid)
|
|
779
|
+
throw failure('conflict', 'current owner release requires sub cleanup before binding again');
|
|
528
780
|
}
|
|
529
781
|
else {
|
|
530
782
|
if (aid !== ownerAid || ns.lock !== 'locked' || ns.owner_aid !== aid)
|
|
@@ -533,10 +785,11 @@ export class OwnershipWorkflow {
|
|
|
533
785
|
if (consensus.status !== 'consensus' || consensus.owner_aid !== aid || consensus.relation_version !== ns.relation_version)
|
|
534
786
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'the current owner relation could not be verified', consensus.status !== 'invalid');
|
|
535
787
|
}
|
|
536
|
-
ownershipInteger(ns.relation_version + 1, 'target_relation_version');
|
|
788
|
+
const targetVersion = ownershipInteger(ns.relation_version + (operation === 'release' ? 0 : 1), 'target_relation_version');
|
|
537
789
|
const operationId = requestedId ?? crypto.randomUUID();
|
|
538
|
-
const payload = { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation, action: operation === 'bind' ? 'bind.request' : operation === 'transfer' ? 'transfer.request' : 'relation.confirmed', sub_aid: subAid, owner_aid: ownerAid, ...(newOwnerAid ? { new_owner_aid: newOwnerAid } : {}), base_relation_version: ns.relation_version, target_relation_version:
|
|
790
|
+
const payload = { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation, action: operation === 'bind' ? 'bind.request' : operation === 'transfer' ? 'transfer.request' : 'relation.confirmed', sub_aid: subAid, owner_aid: ownerAid, ...(newOwnerAid ? { new_owner_aid: newOwnerAid } : {}), base_relation_version: ns.relation_version, target_relation_version: targetVersion };
|
|
539
791
|
const task = this.create(payload, aid, 'accept');
|
|
792
|
+
await this.captureBase(task, ns);
|
|
540
793
|
if (!task.local_roles.length)
|
|
541
794
|
throw failure('forbidden', 'the local AID is not an ownership participant');
|
|
542
795
|
if (aid !== this.deps.currentAid())
|
|
@@ -558,6 +811,128 @@ export class OwnershipWorkflow {
|
|
|
558
811
|
async releaseSubaid(subAid) { return await this.start('release', subAid, this.deps.currentAid()); }
|
|
559
812
|
async transferSubaid(subAid, newOwnerAid) { return await this.start('transfer', subAid, this.deps.currentAid(), newOwnerAid); }
|
|
560
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
|
+
}
|
|
561
936
|
async checkOwnershipConsensus(subAid = this.deps.currentAid()) { return await this.deps.files.checkConsensus(subAid); }
|
|
562
937
|
async decide(operationId, decision, reason) {
|
|
563
938
|
const saved = await this.load(operationId);
|
|
@@ -623,6 +998,8 @@ export class OwnershipWorkflow {
|
|
|
623
998
|
throw error;
|
|
624
999
|
}
|
|
625
1000
|
const snapshot = await this.load(message.operation_id);
|
|
1001
|
+
if (snapshot?.record.reset === true && snapshot.revision > 0)
|
|
1002
|
+
return true;
|
|
626
1003
|
const prior = snapshot ? await this.record(snapshot) : undefined;
|
|
627
1004
|
if (!prior && !['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
|
|
628
1005
|
return true;
|
|
@@ -662,34 +1039,54 @@ export class OwnershipWorkflow {
|
|
|
662
1039
|
return true;
|
|
663
1040
|
}
|
|
664
1041
|
async confirmSubCleaned(task) {
|
|
665
|
-
const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid)
|
|
666
|
-
if (
|
|
667
|
-
throw failure('unavailable', 'NS changed during sub cleanup verification', true);
|
|
668
|
-
if (!before.exists || before.relation_version !== task.target_relation_version || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
|
|
1042
|
+
const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid);
|
|
1043
|
+
if (!before.exists || before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
|
|
669
1044
|
return false;
|
|
670
1045
|
const file = parseOwnershipFile(before.content, 'agent', { expectedSubAid: task.sub_aid });
|
|
671
|
-
|
|
1046
|
+
if (Object.hasOwn(file.frontmatter, 'owner') || Object.hasOwn(file.frontmatter, 'ownership'))
|
|
1047
|
+
return false;
|
|
1048
|
+
if (!task.base_agent_md?.statement || task.base_agent_md.statement.owner_aid !== task.owner_aid || task.base_agent_md.statement.sub_aid !== task.sub_aid || task.base_agent_md.statement.relation_version !== task.base_relation_version)
|
|
1049
|
+
return false;
|
|
1050
|
+
let ownerHash = '';
|
|
1051
|
+
if (task.local_roles.includes('owner') && task.files.release) {
|
|
1052
|
+
const original = task.files.release, object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
1053
|
+
if (object.exists && object.content === original.raw && object.sha256 === original.sha256) {
|
|
1054
|
+
const file = await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
|
|
1055
|
+
if (file.action === 'release' && file.statement && ['version', 'owner_aid', 'sub_aid', 'relation_version'].every((key) => file.statement[key] === task.base_agent_md.statement[key]))
|
|
1056
|
+
ownerHash = object.sha256;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const confirmed = Boolean(await this.cleanupEvidence(task, { agent_md_etag: before.etag, agent_md_sha256: await ownershipSha256(before.content), owner_md_sha256: ownerHash }, before.content));
|
|
1060
|
+
const after = await this.deps.files.readAgent(task.sub_aid);
|
|
1061
|
+
if (this.now() - began > 30 || before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid || before.content !== after.content)
|
|
1062
|
+
throw failure('unavailable', 'NS changed during sub cleanup verification', true);
|
|
1063
|
+
return confirmed;
|
|
672
1064
|
}
|
|
673
1065
|
async resume(guard) {
|
|
674
1066
|
guard?.();
|
|
675
1067
|
let snapshots;
|
|
676
1068
|
try {
|
|
677
|
-
snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid());
|
|
1069
|
+
snapshots = await this.storage().listOwnershipTasks(this.deps.currentAid(), true);
|
|
678
1070
|
}
|
|
679
1071
|
catch (error) {
|
|
680
1072
|
throw failure('persistence_unavailable', `ownership task list failed: ${String(error)}`);
|
|
681
1073
|
}
|
|
682
1074
|
for (const snapshot of snapshots) {
|
|
683
1075
|
guard?.();
|
|
684
|
-
|
|
685
|
-
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))
|
|
1076
|
+
if (snapshot.record.reset === true && snapshot.revision > 0)
|
|
686
1077
|
continue;
|
|
687
1078
|
try {
|
|
1079
|
+
const task = await this.record(snapshot);
|
|
1080
|
+
if (!task.base_agent_md && task.request_validated && task.status === 'completed')
|
|
1081
|
+
continue;
|
|
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))
|
|
1083
|
+
continue;
|
|
688
1084
|
await this.execute(task.sub_aid, task.operation_id, undefined, undefined, guard);
|
|
689
1085
|
}
|
|
690
1086
|
catch (error) {
|
|
1087
|
+
guard?.();
|
|
691
1088
|
if (ownershipErrorCode(error) !== 'conflict')
|
|
692
|
-
|
|
1089
|
+
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 });
|
|
693
1090
|
}
|
|
694
1091
|
}
|
|
695
1092
|
}
|
|
@@ -703,6 +1100,25 @@ export class OwnershipWorkflow {
|
|
|
703
1100
|
}
|
|
704
1101
|
const entry = task.inbox[0];
|
|
705
1102
|
const ns = await this.observe(task.sub_aid);
|
|
1103
|
+
if (!task.base_agent_md && !task.request_validated && task.local_decision === 'pending' && !Object.keys(task.files).length) {
|
|
1104
|
+
if (this.isBase(task, ns) || task.operation === 'release' && ns.relation_version === task.base_relation_version && ns.file?.statement?.owner_aid === task.owner_aid)
|
|
1105
|
+
await this.captureBase(task, ns);
|
|
1106
|
+
else if (task.operation === 'release' && ns.relation_version === 0 && !ns.file?.statement && task.local_roles.includes('sub')) {
|
|
1107
|
+
for (const candidate of await this.records(context.aid, task.sub_aid)) {
|
|
1108
|
+
const original = candidate.files.agent, cleanup = candidate.release_cleanup?.files.cleanup;
|
|
1109
|
+
if (!['bind', 'transfer'].includes(candidate.operation) || candidate.local_decision !== 'accept' || !candidate.local_roles.includes('sub') || targetOwner(candidate) !== task.owner_aid || candidate.target_relation_version !== task.base_relation_version || !original || !cleanup || cleanup.raw !== ns.content || cleanup.sha256 !== await ownershipSha256(ns.content) || cleanup.published_etag !== undefined && cleanup.published_etag !== ns.etag)
|
|
1110
|
+
continue;
|
|
1111
|
+
const statement = parseOwnershipFile(original.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
|
|
1112
|
+
if (!statement || !cleanup.condition?.if_match)
|
|
1113
|
+
continue;
|
|
1114
|
+
task.base_agent_md = { exists: true, raw: original.raw, sha256: original.sha256, etag: cleanup.condition.if_match, statement };
|
|
1115
|
+
task.cleanup_authorization_operation_id = candidate.operation_id;
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (task.base_agent_md)
|
|
1120
|
+
await this.save(context);
|
|
1121
|
+
}
|
|
706
1122
|
try {
|
|
707
1123
|
validateOwnershipMessage(entry.payload, { authenticated_from_aid: entry.from_aid, local_aid: context.aid, task, ns });
|
|
708
1124
|
}
|
|
@@ -719,6 +1135,11 @@ export class OwnershipWorkflow {
|
|
|
719
1135
|
}
|
|
720
1136
|
else if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action)) {
|
|
721
1137
|
const decision = action.endsWith('.accept') ? 'accept' : 'reject';
|
|
1138
|
+
if (decision === 'reject' && await this.complete(context, ns)) {
|
|
1139
|
+
task.inbox.shift();
|
|
1140
|
+
await this.save(context);
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
722
1143
|
if (!task.approvals[entry.from_aid] || task.approvals[entry.from_aid] === 'pending') {
|
|
723
1144
|
task.approvals[entry.from_aid] = decision;
|
|
724
1145
|
if (decision === 'reject') {
|
|
@@ -744,6 +1165,20 @@ export class OwnershipWorkflow {
|
|
|
744
1165
|
}
|
|
745
1166
|
}
|
|
746
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
|
+
}
|
|
747
1182
|
if (action === 'transfer.ready' && this.isBase(task, ns)) {
|
|
748
1183
|
const authorization = await this.deps.files.verifyTransfer(this.fileTask(context));
|
|
749
1184
|
task.expires_at = authorization.transfer.expires_at;
|
|
@@ -753,7 +1188,7 @@ export class OwnershipWorkflow {
|
|
|
753
1188
|
}
|
|
754
1189
|
else if (action === 'sub.cleaned') {
|
|
755
1190
|
const file = ns.file;
|
|
756
|
-
if (ns.relation_version ===
|
|
1191
|
+
if (ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid && file && !file.statement && !Object.hasOwn(file.frontmatter, 'owner') && !Object.hasOwn(file.frontmatter, 'ownership'))
|
|
757
1192
|
task.hints[action] = true;
|
|
758
1193
|
}
|
|
759
1194
|
task.inbox.shift();
|
|
@@ -763,10 +1198,29 @@ export class OwnershipWorkflow {
|
|
|
763
1198
|
async flush(context, ns) {
|
|
764
1199
|
const task = context.task;
|
|
765
1200
|
for (const item of [...task.outbox]) {
|
|
1201
|
+
ns = await this.observe(task.sub_aid);
|
|
766
1202
|
const action = item.payload.action;
|
|
767
1203
|
const notification = ['relation.confirmed', 'sub.cleaned'].includes(action);
|
|
768
1204
|
const terminal = ['bind.reject', 'transfer.reject', 'operation.stopped'].includes(action);
|
|
769
|
-
|
|
1205
|
+
const cleaned = action === 'sub.cleaned' && task.status === 'completed' && await this.confirmSubCleaned(task);
|
|
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')) {
|
|
770
1224
|
task.outbox = task.outbox.filter((row) => row.key !== item.key);
|
|
771
1225
|
await this.save(context);
|
|
772
1226
|
continue;
|
|
@@ -789,19 +1243,43 @@ export class OwnershipWorkflow {
|
|
|
789
1243
|
}
|
|
790
1244
|
async complete(context, ns) {
|
|
791
1245
|
const task = context.task;
|
|
792
|
-
|
|
1246
|
+
const began = Date.now(), cleaned = task.operation === 'release' && ns.exists && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
|
|
1247
|
+
if (!this.isTarget(task, ns) && !cleaned)
|
|
793
1248
|
return false;
|
|
794
1249
|
let consensus;
|
|
795
1250
|
if (task.operation === 'release') {
|
|
796
1251
|
consensus = await this.deps.files.checkConsensus(task.sub_aid);
|
|
797
|
-
if (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))
|
|
798
1253
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership release could not be freshly confirmed', consensus.status !== 'invalid');
|
|
1254
|
+
if (!cleaned && task.files.release && consensus.owner_md_sha256) {
|
|
1255
|
+
const source = task.files.release;
|
|
1256
|
+
if (source.sha256 !== consensus.owner_md_sha256)
|
|
1257
|
+
throw failure('conflict', 'current release material does not belong to this task');
|
|
1258
|
+
}
|
|
1259
|
+
if (cleaned) {
|
|
1260
|
+
const source = task.files.release;
|
|
1261
|
+
if (task.local_roles.includes('owner') && source) {
|
|
1262
|
+
const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
1263
|
+
if (object.exists && object.content === source.raw && object.sha256 === source.sha256) {
|
|
1264
|
+
await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
|
|
1265
|
+
consensus.owner_md_sha256 = object.sha256;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const authorization = await this.cleanupEvidence(task, consensus, ns.content);
|
|
1269
|
+
if (!authorization)
|
|
1270
|
+
throw failure('conflict', 'no local cleanup original can confirm this release task');
|
|
1271
|
+
const after = await this.observe(task.sub_aid);
|
|
1272
|
+
if (Date.now() - began > 30_000 || after.etag !== ns.etag || after.content !== ns.content || !after.exists || after.relation_version !== 0 || after.lock !== 'unlocked' || after.owner_aid)
|
|
1273
|
+
throw failure('unavailable', 'NS changed during cleanup confirmation', true);
|
|
1274
|
+
if (authorization !== task.operation_id)
|
|
1275
|
+
task.cleanup_authorization_operation_id = authorization;
|
|
1276
|
+
}
|
|
799
1277
|
const prior = ns.file?.statement;
|
|
800
|
-
if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
|
|
1278
|
+
if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version || !this.sameStatement(prior, task.base_agent_md?.statement ?? undefined)))
|
|
801
1279
|
throw failure('conflict', 'ownership released document does not match this generation');
|
|
802
1280
|
if (task.local_roles.includes('owner') && !task.files.release)
|
|
803
1281
|
throw failure('conflict', 'ownership completion lacks this task original owner file');
|
|
804
|
-
if (prior && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && !await this.authorizeCleanup(context)) {
|
|
1282
|
+
if (prior && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && !await this.authorizeCleanup(context, prior)) {
|
|
805
1283
|
task.status = 'pending';
|
|
806
1284
|
task.step = 'cleanup_action_required';
|
|
807
1285
|
task.cleanup_pending = true;
|
|
@@ -821,16 +1299,14 @@ export class OwnershipWorkflow {
|
|
|
821
1299
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership target materials could not be verified', consensus.status !== 'invalid');
|
|
822
1300
|
if (task.local_roles.includes('sub') && (!task.files.agent || task.files.agent.sha256 !== consensus.agent_md_sha256))
|
|
823
1301
|
throw failure('conflict', 'NS accepted agent.md does not match this task original bytes');
|
|
824
|
-
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)
|
|
825
1303
|
throw failure('conflict', 'Storage owner.md does not match this task original bytes');
|
|
826
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)
|
|
827
1305
|
throw failure('conflict', 'NS accepted agent.md revision changed');
|
|
828
|
-
if (task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') && task.files.owner.published_revision !== undefined && task.files.owner.published_revision !== consensus.owner_md_revision && task.verified_relation_version === undefined)
|
|
829
|
-
throw failure('conflict', 'Storage owner.md revision changed');
|
|
830
1306
|
}
|
|
831
1307
|
task.status = 'completed';
|
|
832
1308
|
task.step = task.operation === 'release' ? 'released' : 'confirmed';
|
|
833
|
-
task.verified_relation_version =
|
|
1309
|
+
task.verified_relation_version = task.target_relation_version;
|
|
834
1310
|
task.verified_consensus = { ...consensus, protocol: '2.1' };
|
|
835
1311
|
delete task.code;
|
|
836
1312
|
delete task.reason;
|
|
@@ -850,23 +1326,39 @@ export class OwnershipWorkflow {
|
|
|
850
1326
|
await this.save(context);
|
|
851
1327
|
return true;
|
|
852
1328
|
}
|
|
853
|
-
async
|
|
1329
|
+
async cleanupEvidence(task, evidence, raw) {
|
|
1330
|
+
const release = task.files.release;
|
|
1331
|
+
if (task.local_roles.includes('owner') && release && release.sha256 === evidence.owner_md_sha256 && release.sha256 === await ownershipSha256(release.raw))
|
|
1332
|
+
return task.operation_id;
|
|
1333
|
+
if (!task.local_roles.includes('sub'))
|
|
1334
|
+
return undefined;
|
|
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));
|
|
1336
|
+
if (await matches(task.files.cleanup))
|
|
1337
|
+
return task.operation_id;
|
|
1338
|
+
for (const candidate of await this.records(this.deps.currentAid(), task.sub_aid, ['bind', 'transfer'])) {
|
|
1339
|
+
if (!['bind', 'transfer'].includes(candidate.operation))
|
|
1340
|
+
continue;
|
|
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))
|
|
1342
|
+
continue;
|
|
1343
|
+
const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
|
|
1344
|
+
if (statement?.owner_aid === task.owner_aid && statement.sub_aid === task.sub_aid && statement.relation_version === task.base_relation_version && this.sameStatement(statement, task.base_agent_md?.statement ?? undefined))
|
|
1345
|
+
return candidate.operation_id;
|
|
1346
|
+
}
|
|
1347
|
+
return undefined;
|
|
1348
|
+
}
|
|
1349
|
+
sameStatement(left, right) {
|
|
1350
|
+
return Boolean(left && right && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((key) => left[key] === right[key]));
|
|
1351
|
+
}
|
|
1352
|
+
async authorizeCleanup(context, statement) {
|
|
854
1353
|
const task = context.task;
|
|
855
|
-
const matches = (record) => ['bind', 'transfer'].includes(record.operation) && record.sub_aid === task.sub_aid && record.target_relation_version === task.base_relation_version && record.local_decision === 'accept' && record.local_roles.includes('sub') && targetOwner(record) === task.owner_aid;
|
|
1354
|
+
const matches = (record) => ['bind', 'transfer'].includes(record.operation) && record.sub_aid === task.sub_aid && record.target_relation_version === task.base_relation_version && record.local_decision === 'accept' && record.local_roles.includes('sub') && targetOwner(record) === task.owner_aid && Boolean(record.files.agent && this.sameStatement(parseOwnershipFile(record.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement, statement));
|
|
856
1355
|
if (task.cleanup_authorization_operation_id) {
|
|
857
1356
|
const saved = await this.load(task.cleanup_authorization_operation_id);
|
|
858
1357
|
if (!saved || !matches(await this.record(saved)))
|
|
859
1358
|
throw failure('persistence_unavailable', 'ownership cleanup historical authorization is damaged');
|
|
860
1359
|
return true;
|
|
861
1360
|
}
|
|
862
|
-
|
|
863
|
-
try {
|
|
864
|
-
history = await this.storage().listOwnershipTasks(context.aid);
|
|
865
|
-
}
|
|
866
|
-
catch (error) {
|
|
867
|
-
throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
|
|
868
|
-
}
|
|
869
|
-
const authorized = (await Promise.all(history.map((row) => this.record(row)))).find(matches);
|
|
1361
|
+
const authorized = (await this.records(context.aid, task.sub_aid, ['bind', 'transfer'])).find(matches);
|
|
870
1362
|
if (!authorized)
|
|
871
1363
|
return false;
|
|
872
1364
|
task.cleanup_authorization_operation_id = authorized.operation_id;
|
|
@@ -875,13 +1367,23 @@ export class OwnershipWorkflow {
|
|
|
875
1367
|
}
|
|
876
1368
|
async resumeSubCleanup(context, ns) {
|
|
877
1369
|
const task = context.task;
|
|
878
|
-
if (!['bind', 'transfer'].includes(task.operation) || !task.local_roles.includes('sub') || task.local_decision !== 'accept' || ns.lock !== 'unlocked' ||
|
|
1370
|
+
if (!['bind', 'transfer'].includes(task.operation) || !task.local_roles.includes('sub') || task.local_decision !== 'accept' || ns.lock !== 'unlocked' || task.release_cleanup?.cleaned)
|
|
1371
|
+
return;
|
|
1372
|
+
if (ns.relation_version === 0 && !ns.file?.statement) {
|
|
1373
|
+
const prepared = task.release_cleanup?.files.cleanup;
|
|
1374
|
+
if (!prepared || !prepared.condition?.if_match || prepared.raw !== ns.content || prepared.sha256 !== await ownershipSha256(ns.content) || prepared.published_etag !== undefined && prepared.published_etag !== ns.etag)
|
|
1375
|
+
throw failure('conflict', 'historical cleanup original file was not confirmed');
|
|
1376
|
+
task.release_cleanup.cleaned = true;
|
|
1377
|
+
await this.save(context);
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (ns.relation_version !== task.target_relation_version)
|
|
879
1381
|
return;
|
|
880
1382
|
const consensus = await this.deps.files.checkConsensus(task.sub_aid);
|
|
881
1383
|
if (consensus.status !== 'released' || consensus.relation_version !== ns.relation_version || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== await ownershipSha256(ns.content))
|
|
882
1384
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'NS changed during historical cleanup observation', consensus.status !== 'invalid');
|
|
883
1385
|
const statement = ns.file?.statement;
|
|
884
|
-
if (statement
|
|
1386
|
+
if (!statement || !task.files.agent || !this.sameStatement(statement, parseOwnershipFile(task.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement))
|
|
885
1387
|
throw failure('conflict', 'historical cleanup fields belong to another relation');
|
|
886
1388
|
if (!task.release_cleanup) {
|
|
887
1389
|
task.release_cleanup = { files: {}, cleaned: false };
|
|
@@ -889,7 +1391,7 @@ export class OwnershipWorkflow {
|
|
|
889
1391
|
}
|
|
890
1392
|
if (statement || task.release_cleanup.files.cleanup)
|
|
891
1393
|
await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: task.sub_aid, owner_aid: targetOwner(task),
|
|
892
|
-
base_relation_version: task.target_relation_version, target_relation_version: ns.relation_version, files: task.release_cleanup.files,
|
|
1394
|
+
base_relation_version: task.target_relation_version, target_relation_version: ns.relation_version, files: task.release_cleanup.files, expected_agent_statement: statement,
|
|
893
1395
|
persist: () => this.save(context), guard: () => this.save(context) });
|
|
894
1396
|
task.release_cleanup.cleaned = true;
|
|
895
1397
|
await this.save(context);
|
|
@@ -899,7 +1401,9 @@ export class OwnershipWorkflow {
|
|
|
899
1401
|
const task = context.task;
|
|
900
1402
|
if (!task.cleanup_pending || task.cleanup_done)
|
|
901
1403
|
return;
|
|
902
|
-
|
|
1404
|
+
const releaseOwner = task.operation === 'release' && task.local_roles.includes('owner');
|
|
1405
|
+
const cleaned = task.operation === 'release' && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
|
|
1406
|
+
if (!this.isTarget(task, ns) && !cleaned && !releaseOwner) {
|
|
903
1407
|
task.cleanup_pending = false;
|
|
904
1408
|
task.step = 'cleanup_superseded';
|
|
905
1409
|
await this.save(context);
|
|
@@ -907,7 +1411,9 @@ export class OwnershipWorkflow {
|
|
|
907
1411
|
}
|
|
908
1412
|
if (task.operation === 'release' && task.local_roles.includes('sub')) {
|
|
909
1413
|
const prior = ns.file?.statement;
|
|
910
|
-
if (prior && !
|
|
1414
|
+
if (prior && !this.sameStatement(prior, task.base_agent_md?.statement ?? undefined))
|
|
1415
|
+
throw failure('conflict', 'ownership cleanup original relation was replaced');
|
|
1416
|
+
if (prior && !await this.authorizeCleanup(context, prior)) {
|
|
911
1417
|
task.step = 'cleanup_action_required';
|
|
912
1418
|
await this.save(context);
|
|
913
1419
|
this.event(context, 'ownership.action_required');
|
|
@@ -915,18 +1421,18 @@ export class OwnershipWorkflow {
|
|
|
915
1421
|
}
|
|
916
1422
|
if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
|
|
917
1423
|
throw failure('conflict', 'ownership cleanup current fields differ');
|
|
918
|
-
if (prior)
|
|
919
|
-
await this.deps.files.revokeOwneraid(this.fileTask(context));
|
|
1424
|
+
if (prior || task.files.cleanup)
|
|
1425
|
+
await this.deps.files.revokeOwneraid(this.fileTask(context, prior));
|
|
920
1426
|
this.enqueue(task, task.owner_aid, 'sub.cleaned');
|
|
921
1427
|
}
|
|
922
1428
|
if (task.local_roles.includes('owner') && (task.operation === 'release' || task.operation === 'transfer')) {
|
|
1429
|
+
if (releaseOwner && this.isTarget(task, ns) && ns.file?.statement)
|
|
1430
|
+
return;
|
|
923
1431
|
const prepared = task.files[task.operation === 'release' ? 'release' : 'transfer'];
|
|
924
|
-
if (prepared
|
|
1432
|
+
if (prepared) {
|
|
925
1433
|
const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
926
|
-
if (object.exists && object.sha256
|
|
927
|
-
|
|
928
|
-
await this.save(context);
|
|
929
|
-
}
|
|
1434
|
+
if (object.exists && (object.sha256 !== prepared.sha256 || object.content !== prepared.raw))
|
|
1435
|
+
throw failure('conflict', 'ownership cleanup original owner file has been replaced');
|
|
930
1436
|
}
|
|
931
1437
|
await this.deps.files.cleanupOwnerFile(this.fileTask(context));
|
|
932
1438
|
}
|
|
@@ -962,7 +1468,7 @@ export class OwnershipWorkflow {
|
|
|
962
1468
|
delete task.reason;
|
|
963
1469
|
task.retryable = true;
|
|
964
1470
|
if (await this.complete(context, ns)) {
|
|
965
|
-
if (task.status === 'completed' && task.operation === 'release'
|
|
1471
|
+
if (task.status === 'completed' && task.operation === 'release') {
|
|
966
1472
|
await this.cleanup(context, ns);
|
|
967
1473
|
ns = await this.observe(task.sub_aid);
|
|
968
1474
|
}
|
|
@@ -984,13 +1490,28 @@ export class OwnershipWorkflow {
|
|
|
984
1490
|
const accepted = participants(task).every((aid) => task.approvals[aid] === 'accept');
|
|
985
1491
|
if (task.operation === 'bind') {
|
|
986
1492
|
if (task.local_roles.includes('owner') && accepted) {
|
|
987
|
-
|
|
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));
|
|
988
1499
|
this.enqueue(task, task.sub_aid, 'owner.ready');
|
|
989
|
-
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
|
+
}
|
|
990
1510
|
task.hints['owner.ready'] = true;
|
|
1511
|
+
}
|
|
991
1512
|
await this.save(context);
|
|
992
1513
|
}
|
|
993
|
-
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)) {
|
|
994
1515
|
await this.deps.files.declareOwneraid(this.fileTask(context));
|
|
995
1516
|
}
|
|
996
1517
|
task.step = accepted ? 'awaiting_confirmation' : 'awaiting_peer_approval';
|