@agentunion/fastaun-browser 0.5.19 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +129 -78
- package/_packed_docs/CHANGELOG.md +129 -78
- package/_packed_docs/INDEX.md +201 -195
- package/_packed_docs/KITE_DOCS_GUIDE.md +93 -90
- package/_packed_docs/agent.md/SCHEMA.md +7 -7
- 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 +1072 -1072
- 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 +443 -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 +658 -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/03-/346/240/270/345/277/203/346/246/202/345/277/265.md +1 -1
- package/_packed_docs/sdk/04-/350/277/236/346/216/245/344/270/216/350/256/244/350/257/201.md +67 -63
- package/_packed_docs/sdk/05-E2EE/345/212/240/345/257/206/351/200/232/344/277/241.md +1 -1
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +35 -8
- package/_packed_docs/sdk/08-/346/234/200/344/275/263/345/256/236/350/267/265.md +3 -3
- package/_packed_docs/sdk/09-group-rpc-manual.md +258 -254
- package/_packed_docs/sdk/09-message-rpc-manual.md +7 -4
- package/_packed_docs/sdk/09-storage-rpc-manual.md +18 -26
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +61 -52
- package/_packed_docs/sdk/INDEX.md +92 -80
- 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 +922 -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 +300 -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 +382 -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 +476 -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 +15 -4
- package/dist/agent-md-schema.d.ts.map +1 -1
- package/dist/agent-md-schema.js +64 -30
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +10 -2
- package/dist/agent-md.d.ts.map +1 -1
- package/dist/agent-md.js +278 -121
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +46 -0
- package/dist/aid-store.d.ts.map +1 -1
- package/dist/aid-store.js +149 -10
- 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 +23 -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 +85 -18
- package/dist/auth.js.map +1 -1
- package/dist/bundle.js +1581 -781
- package/dist/cert-utils.d.ts +49 -1
- package/dist/cert-utils.d.ts.map +1 -1
- package/dist/cert-utils.js +102 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +7 -0
- package/dist/client/delivery.d.ts.map +1 -1
- package/dist/client/delivery.js +243 -28
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts.map +1 -1
- package/dist/client/rpc-pipeline.js +89 -77
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.d.ts.map +1 -1
- package/dist/client/v2-e2ee.js +99 -80
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -17
- package/dist/client.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 +5 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +33 -14
- package/dist/events.js.map +1 -1
- package/dist/facades.js +5 -5
- package/dist/facades.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/org-protocol.d.ts.map +1 -1
- package/dist/org-protocol.js +72 -12
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +25 -8
- package/dist/ownership-v21.d.ts.map +1 -1
- package/dist/ownership-v21.js +142 -73
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +6 -1
- package/dist/ownership-workflow.d.ts.map +1 -1
- package/dist/ownership-workflow.js +221 -58
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +2 -1
- package/dist/tools/cross-sdk-agent.d.ts.map +1 -1
- package/dist/tools/cross-sdk-agent.js +91 -8
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +2 -0
- 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
|
@@ -81,8 +81,8 @@ export function parseOwnershipMessage(content) {
|
|
|
81
81
|
ownershipAid(message.sub_aid, 'sub_aid');
|
|
82
82
|
ownershipAid(message.owner_aid, 'owner_aid');
|
|
83
83
|
ownershipInteger(message.base_relation_version, 'base_relation_version', 0);
|
|
84
|
-
if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + 1)
|
|
85
|
-
throw new ValidationError('ownership target version
|
|
84
|
+
if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + (message.operation === 'release' ? 0 : 1))
|
|
85
|
+
throw new ValidationError('ownership target version does not match operation');
|
|
86
86
|
if (message.operation === 'transfer') {
|
|
87
87
|
if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
|
|
88
88
|
throw new ValidationError('transfer target must differ from current owner');
|
|
@@ -149,7 +149,8 @@ export function validateOwnershipMessage(message, context) {
|
|
|
149
149
|
throw failure('unsupported_protocol', 'NS ownership protocol is unavailable');
|
|
150
150
|
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
151
|
const target = ns.relation_version === message.target_relation_version && (message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === targetOwner(message));
|
|
152
|
-
|
|
152
|
+
const cleaned = message.action === 'sub.cleaned' && Boolean(task) && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.owner_aid;
|
|
153
|
+
if (message.action !== 'operation.stopped' && (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target && !cleaned : !base && !target))
|
|
153
154
|
throw failure('conflict', 'ownership current NS relation does not match this task');
|
|
154
155
|
}
|
|
155
156
|
}
|
|
@@ -215,10 +216,12 @@ export class OwnershipWorkflow {
|
|
|
215
216
|
const task = snapshot.record;
|
|
216
217
|
const localAid = this.deps.currentAid();
|
|
217
218
|
try {
|
|
219
|
+
if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 1)
|
|
220
|
+
throw new Error('invalid persisted task revision');
|
|
218
221
|
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
222
|
throw new Error('invalid task state');
|
|
220
223
|
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)
|
|
224
|
+
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
225
|
throw new Error('invalid local task identity');
|
|
223
226
|
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
227
|
throw new Error('invalid task approvals');
|
|
@@ -226,6 +229,30 @@ export class OwnershipWorkflow {
|
|
|
226
229
|
throw new Error('incomplete task approvals or initiator decision');
|
|
227
230
|
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
231
|
throw new Error('invalid task journals');
|
|
232
|
+
const base = task.base_agent_md;
|
|
233
|
+
if (!base && (task.request_validated || Object.keys(task.files).length || task.local_decision !== 'pending'))
|
|
234
|
+
throw new Error('missing original agent.md context');
|
|
235
|
+
if (base) {
|
|
236
|
+
if (typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || typeof base.etag !== 'string' || base.exists && !base.etag || base.sha256 !== await ownershipSha256(base.raw))
|
|
237
|
+
throw new Error('invalid original agent.md bytes or ETag');
|
|
238
|
+
const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: task.sub_aid }).statement : undefined;
|
|
239
|
+
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))
|
|
240
|
+
throw new Error('invalid original agent.md statement');
|
|
241
|
+
}
|
|
242
|
+
if (task.base_owner_md) {
|
|
243
|
+
const prior = task.base_owner_md;
|
|
244
|
+
if (typeof prior.exists !== 'boolean' || typeof prior.raw !== 'string' || prior.sha256 !== (prior.exists ? await ownershipSha256(prior.raw) : '') || !prior.exists && prior.raw !== '')
|
|
245
|
+
throw new Error('invalid original owner.md context');
|
|
246
|
+
if (base?.statement && !prior.exists && (task.operation === 'transfer' || task.operation === 'release' && task.local_roles.includes('owner')))
|
|
247
|
+
throw new Error('missing original owner.md relation');
|
|
248
|
+
if (prior.exists) {
|
|
249
|
+
const file = parseOwnershipFile(prior.raw, 'owner', { expectedSubAid: task.sub_aid });
|
|
250
|
+
if (!file.statement || file.statement.owner_aid !== base?.statement?.owner_aid || file.statement.relation_version !== base?.statement?.relation_version)
|
|
251
|
+
throw new Error('invalid original owner.md statement');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
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))
|
|
255
|
+
throw new Error('missing original owner.md context');
|
|
229
256
|
if (task.approvals[localAid] !== task.local_decision)
|
|
230
257
|
throw new Error('task decision and approval differ');
|
|
231
258
|
if (task.status === 'completed' && (task.verified_relation_version !== task.target_relation_version || task.operation !== 'release' && task.local_decision !== 'accept'))
|
|
@@ -239,8 +266,11 @@ export class OwnershipWorkflow {
|
|
|
239
266
|
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
267
|
throw new Error('invalid prepared file bytes');
|
|
241
268
|
const condition = prepared.condition;
|
|
242
|
-
if (Object.keys(condition).length !== 1 ||
|
|
269
|
+
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
270
|
throw new Error('invalid prepared file condition');
|
|
271
|
+
const original = prepared.original_owner;
|
|
272
|
+
if (!agentFile && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? await ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
|
|
273
|
+
throw new Error('invalid prepared owner original');
|
|
244
274
|
if (key === 'cleanup' && task.operation !== 'release' || key === 'release' && task.operation !== 'release' || key === 'transfer' && task.operation !== 'transfer' || ['agent', 'owner'].includes(key) && task.operation === 'release')
|
|
245
275
|
throw new Error('invalid prepared file operation');
|
|
246
276
|
const ownerAid = key === 'owner' || key === 'agent' ? targetOwner(task) : task.owner_aid;
|
|
@@ -252,7 +282,7 @@ export class OwnershipWorkflow {
|
|
|
252
282
|
const fileExpiry = prepared.expires_at;
|
|
253
283
|
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
284
|
throw new Error('invalid prepared file authorization');
|
|
255
|
-
if (prepared.
|
|
285
|
+
if (prepared.published_etag !== undefined && (!agentFile || typeof prepared.published_etag !== 'string' || !prepared.published_etag) || prepared.cleaned !== undefined && typeof prepared.cleaned !== 'boolean')
|
|
256
286
|
throw new Error('invalid prepared file result');
|
|
257
287
|
}
|
|
258
288
|
if (task.release_cleanup !== undefined) {
|
|
@@ -266,19 +296,27 @@ export class OwnershipWorkflow {
|
|
|
266
296
|
}
|
|
267
297
|
if (task.status === 'completed') {
|
|
268
298
|
const consensus = task.verified_consensus;
|
|
269
|
-
|
|
299
|
+
const cleaned = task.operation === 'release' && consensus?.status === 'unbound' && consensus.relation_version === 0 && Boolean(await this.cleanupEvidence(task, consensus));
|
|
300
|
+
if (!consensus || consensus.protocol !== '2.1' || consensus.sub_aid !== task.sub_aid || !cleaned && consensus.relation_version !== task.target_relation_version || typeof consensus.agent_md_etag !== 'string' || !consensus.agent_md_etag || !/^[0-9a-f]{64}$/.test(consensus.agent_md_sha256))
|
|
270
301
|
throw new Error('invalid persisted consensus evidence');
|
|
271
302
|
if (task.operation === 'release') {
|
|
272
|
-
if (consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
|
|
303
|
+
if (!cleaned && consensus.status !== 'released' || consensus.lock !== 'unlocked' || consensus.owner_aid !== null || task.local_roles.includes('owner') && !task.files.release)
|
|
273
304
|
throw new Error('invalid persisted release evidence');
|
|
274
305
|
}
|
|
275
306
|
else {
|
|
276
|
-
if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) ||
|
|
307
|
+
if (consensus.status !== 'consensus' || consensus.lock !== 'locked' || consensus.owner_aid !== targetOwner(task) || !/^[0-9a-f]{64}$/.test(consensus.owner_md_sha256))
|
|
277
308
|
throw new Error('invalid persisted binding evidence');
|
|
278
309
|
if (task.local_roles.includes('sub') && task.files.agent?.sha256 !== consensus.agent_md_sha256 || task.local_roles.includes(task.operation === 'transfer' ? 'new_owner' : 'owner') && task.files.owner?.sha256 !== consensus.owner_md_sha256 || task.operation === 'transfer' && task.local_roles.includes('owner') && !task.files.transfer)
|
|
279
310
|
throw new Error('persisted consensus differs from local original files');
|
|
280
311
|
}
|
|
281
312
|
}
|
|
313
|
+
for (const key of task.received) {
|
|
314
|
+
if (typeof key !== 'string' || key.split('|').length !== 2)
|
|
315
|
+
throw new Error('invalid received journal');
|
|
316
|
+
const [fromAid, action] = key.split('|');
|
|
317
|
+
const message = parseOwnershipMessage(this.message(task, action));
|
|
318
|
+
validateOwnershipMessage(message, { authenticated_from_aid: fromAid, local_aid: localAid, task });
|
|
319
|
+
}
|
|
282
320
|
for (const item of task.inbox) {
|
|
283
321
|
parseOwnershipMessage(item.payload);
|
|
284
322
|
validateOwnershipMessage(item.payload, { authenticated_from_aid: item.from_aid, local_aid: localAid, task });
|
|
@@ -287,6 +325,11 @@ export class OwnershipWorkflow {
|
|
|
287
325
|
}
|
|
288
326
|
for (const item of task.outbox) {
|
|
289
327
|
parseOwnershipMessage(item.payload);
|
|
328
|
+
const action = item.payload.action;
|
|
329
|
+
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')
|
|
330
|
+
throw new Error('invalid outbox local approval');
|
|
331
|
+
if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action) && !task.request_validated)
|
|
332
|
+
throw new Error('invalid outbox received request');
|
|
290
333
|
validateOwnershipMessage(item.payload, { authenticated_from_aid: localAid, local_aid: item.to_aid, task });
|
|
291
334
|
if (item.key !== `${item.to_aid}|${item.payload.action}` || typeof item.message_id !== 'string' || !/^m-[0-9a-f]{32}$/.test(item.message_id))
|
|
292
335
|
throw new Error('invalid outbox target');
|
|
@@ -394,7 +437,7 @@ export class OwnershipWorkflow {
|
|
|
394
437
|
if (!saved && !initial)
|
|
395
438
|
throw failure('operation_not_found', 'ownership operation was not found');
|
|
396
439
|
if (!saved && initial) {
|
|
397
|
-
const existing = (await this.records(aid)).find((task) => task.sub_aid === subAid && this.ongoing(task));
|
|
440
|
+
const existing = (await this.records(aid, subAid)).find((task) => task.sub_aid === subAid && this.ongoing(task));
|
|
398
441
|
if (existing) {
|
|
399
442
|
if (!this.sameIntent(existing, initial) || existing.base_relation_version !== initial.base_relation_version || existing.target_relation_version !== initial.target_relation_version)
|
|
400
443
|
throw failure('conflict', 'another local operation for this sub is pending');
|
|
@@ -435,7 +478,7 @@ export class OwnershipWorkflow {
|
|
|
435
478
|
sameIntent(task, intent) {
|
|
436
479
|
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
480
|
}
|
|
438
|
-
async records(aid) {
|
|
481
|
+
async records(aid, subAid) {
|
|
439
482
|
let snapshots;
|
|
440
483
|
try {
|
|
441
484
|
snapshots = await this.storage().listOwnershipTasks(aid);
|
|
@@ -443,32 +486,39 @@ export class OwnershipWorkflow {
|
|
|
443
486
|
catch (error) {
|
|
444
487
|
throw failure('persistence_unavailable', `ownership task list failed: ${String(error)}`);
|
|
445
488
|
}
|
|
446
|
-
return await Promise.all(snapshots.map((snapshot) => this.record(snapshot)));
|
|
489
|
+
return await Promise.all(snapshots.filter((snapshot) => !subAid || typeof snapshot.record.sub_aid !== 'string' || snapshot.record.sub_aid === subAid).map((snapshot) => this.record(snapshot)));
|
|
447
490
|
}
|
|
448
491
|
async observe(subAid) {
|
|
449
492
|
this.requireApplicable(subAid);
|
|
450
493
|
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;
|
|
494
|
+
const file = snapshot.exists ? await this.deps.files.verify(snapshot.content, 'agent', subAid) : null;
|
|
454
495
|
if (file)
|
|
455
496
|
requireOwnershipApplicable(file.frontmatter.type);
|
|
456
|
-
if (
|
|
497
|
+
if ((file?.statement?.relation_version ?? 0) !== snapshot.relation_version)
|
|
457
498
|
throw failure('unavailable', 'NS relation metadata does not match agent.md', true);
|
|
458
499
|
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
500
|
throw failure('unavailable', 'NS lock and current signed agent.md differ', true);
|
|
460
501
|
return { ...snapshot, file, owner_aid: snapshot.lock === 'locked' ? file.statement.owner_aid : null };
|
|
461
502
|
}
|
|
462
503
|
isBase(task, ns) {
|
|
463
|
-
|
|
504
|
+
const base = task.base_agent_md;
|
|
505
|
+
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));
|
|
506
|
+
}
|
|
507
|
+
async captureBase(task, ns) {
|
|
508
|
+
if (!task.base_agent_md)
|
|
509
|
+
task.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: await ownershipSha256(ns.content), etag: ns.etag, statement: ns.file?.statement ?? null };
|
|
510
|
+
if (!task.base_owner_md) {
|
|
511
|
+
const prior = ns.file?.statement ? await this.deps.files.readOwner(ns.file.statement.owner_aid, task.sub_aid) : null;
|
|
512
|
+
task.base_owner_md = prior ? { exists: prior.exists, raw: prior.content, sha256: prior.sha256 } : { exists: false, raw: '', sha256: '' };
|
|
513
|
+
}
|
|
464
514
|
}
|
|
465
515
|
isTarget(task, ns) {
|
|
466
516
|
return ns.relation_version === task.target_relation_version && (task.operation === 'release' ? ns.lock === 'unlocked' : ns.lock === 'locked' && ns.owner_aid === targetOwner(task));
|
|
467
517
|
}
|
|
468
|
-
fileTask(context) {
|
|
469
|
-
const task = { ...context.task, files: context.task.files,
|
|
470
|
-
persist: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; await this.save(context); },
|
|
471
|
-
guard: async () => { context.task.expires_at = task.expires_at; context.task.authorization_sha256 = task.authorization_sha256; await this.save(context); } };
|
|
518
|
+
fileTask(context, expectedStatement) {
|
|
519
|
+
const task = { ...context.task, files: context.task.files, expected_agent_statement: expectedStatement,
|
|
520
|
+
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); },
|
|
521
|
+
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
522
|
return task;
|
|
473
523
|
}
|
|
474
524
|
async start(operation, subAid, ownerAid, newOwnerAid, requestedId, guard) {
|
|
@@ -502,7 +552,7 @@ export class OwnershipWorkflow {
|
|
|
502
552
|
}
|
|
503
553
|
}
|
|
504
554
|
const intent = { initiator_aid: aid, operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid };
|
|
505
|
-
for (const pending of (await this.records(aid)).filter((task) => task.sub_aid === subAid && this.ongoing(task))) {
|
|
555
|
+
for (const pending of (await this.records(aid, subAid)).filter((task) => task.sub_aid === subAid && this.ongoing(task))) {
|
|
506
556
|
if (this.sameIntent(pending, intent)) {
|
|
507
557
|
const result = await this.advance(subAid, pending.operation_id, undefined, events, undefined, guard);
|
|
508
558
|
if (result.status !== 'stopped' || result.code !== 'conflict' || !/relation version or target was replaced/.test(result.reason ?? ''))
|
|
@@ -525,6 +575,8 @@ export class OwnershipWorkflow {
|
|
|
525
575
|
throw failure('owner_locked', 'the sub AID has a current owner lock');
|
|
526
576
|
if (ns.file?.frontmatter.type === 'human')
|
|
527
577
|
throw failure('invalid', 'human cannot declare an owner');
|
|
578
|
+
if (ns.file?.statement?.owner_aid === ownerAid)
|
|
579
|
+
throw failure('conflict', 'current owner release requires sub cleanup before binding again');
|
|
528
580
|
}
|
|
529
581
|
else {
|
|
530
582
|
if (aid !== ownerAid || ns.lock !== 'locked' || ns.owner_aid !== aid)
|
|
@@ -533,10 +585,11 @@ export class OwnershipWorkflow {
|
|
|
533
585
|
if (consensus.status !== 'consensus' || consensus.owner_aid !== aid || consensus.relation_version !== ns.relation_version)
|
|
534
586
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'the current owner relation could not be verified', consensus.status !== 'invalid');
|
|
535
587
|
}
|
|
536
|
-
ownershipInteger(ns.relation_version + 1, 'target_relation_version');
|
|
588
|
+
const targetVersion = ownershipInteger(ns.relation_version + (operation === 'release' ? 0 : 1), 'target_relation_version');
|
|
537
589
|
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:
|
|
590
|
+
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
591
|
const task = this.create(payload, aid, 'accept');
|
|
592
|
+
await this.captureBase(task, ns);
|
|
540
593
|
if (!task.local_roles.length)
|
|
541
594
|
throw failure('forbidden', 'the local AID is not an ownership participant');
|
|
542
595
|
if (aid !== this.deps.currentAid())
|
|
@@ -662,13 +715,28 @@ export class OwnershipWorkflow {
|
|
|
662
715
|
return true;
|
|
663
716
|
}
|
|
664
717
|
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)
|
|
718
|
+
const began = this.now(), before = await this.deps.files.readAgent(task.sub_aid);
|
|
719
|
+
if (!before.exists || before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
|
|
669
720
|
return false;
|
|
670
721
|
const file = parseOwnershipFile(before.content, 'agent', { expectedSubAid: task.sub_aid });
|
|
671
|
-
|
|
722
|
+
if (Object.hasOwn(file.frontmatter, 'owner') || Object.hasOwn(file.frontmatter, 'ownership'))
|
|
723
|
+
return false;
|
|
724
|
+
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)
|
|
725
|
+
return false;
|
|
726
|
+
let ownerHash = '';
|
|
727
|
+
if (task.local_roles.includes('owner') && task.files.release) {
|
|
728
|
+
const original = task.files.release, object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
729
|
+
if (object.exists && object.content === original.raw && object.sha256 === original.sha256) {
|
|
730
|
+
const file = await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
|
|
731
|
+
if (file.action === 'release' && file.statement && ['version', 'owner_aid', 'sub_aid', 'relation_version'].every((key) => file.statement[key] === task.base_agent_md.statement[key]))
|
|
732
|
+
ownerHash = object.sha256;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
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));
|
|
736
|
+
const after = await this.deps.files.readAgent(task.sub_aid);
|
|
737
|
+
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)
|
|
738
|
+
throw failure('unavailable', 'NS changed during sub cleanup verification', true);
|
|
739
|
+
return confirmed;
|
|
672
740
|
}
|
|
673
741
|
async resume(guard) {
|
|
674
742
|
guard?.();
|
|
@@ -681,15 +749,16 @@ export class OwnershipWorkflow {
|
|
|
681
749
|
}
|
|
682
750
|
for (const snapshot of snapshots) {
|
|
683
751
|
guard?.();
|
|
684
|
-
const task = await this.record(snapshot);
|
|
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))
|
|
686
|
-
continue;
|
|
687
752
|
try {
|
|
753
|
+
const task = await this.record(snapshot);
|
|
754
|
+
if (['rejected', 'stopped'].includes(task.status) && !task.outbox.length && !task.inbox.length || task.status === 'completed' && !task.cleanup_pending && !task.outbox.length && !task.inbox.length && !(['bind', 'transfer'].includes(task.operation) && task.local_roles.includes('sub') && task.local_decision === 'accept' && !task.release_cleanup?.cleaned))
|
|
755
|
+
continue;
|
|
688
756
|
await this.execute(task.sub_aid, task.operation_id, undefined, undefined, guard);
|
|
689
757
|
}
|
|
690
758
|
catch (error) {
|
|
759
|
+
guard?.();
|
|
691
760
|
if (ownershipErrorCode(error) !== 'conflict')
|
|
692
|
-
|
|
761
|
+
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
762
|
}
|
|
694
763
|
}
|
|
695
764
|
}
|
|
@@ -703,6 +772,25 @@ export class OwnershipWorkflow {
|
|
|
703
772
|
}
|
|
704
773
|
const entry = task.inbox[0];
|
|
705
774
|
const ns = await this.observe(task.sub_aid);
|
|
775
|
+
if (!task.base_agent_md && !task.request_validated && task.local_decision === 'pending' && !Object.keys(task.files).length) {
|
|
776
|
+
if (this.isBase(task, ns) || task.operation === 'release' && ns.relation_version === task.base_relation_version && ns.file?.statement?.owner_aid === task.owner_aid)
|
|
777
|
+
await this.captureBase(task, ns);
|
|
778
|
+
else if (task.operation === 'release' && ns.relation_version === 0 && !ns.file?.statement && task.local_roles.includes('sub')) {
|
|
779
|
+
for (const candidate of await this.records(context.aid, task.sub_aid)) {
|
|
780
|
+
const original = candidate.files.agent, cleanup = candidate.release_cleanup?.files.cleanup;
|
|
781
|
+
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)
|
|
782
|
+
continue;
|
|
783
|
+
const statement = parseOwnershipFile(original.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
|
|
784
|
+
if (!statement || !cleanup.condition?.if_match)
|
|
785
|
+
continue;
|
|
786
|
+
task.base_agent_md = { exists: true, raw: original.raw, sha256: original.sha256, etag: cleanup.condition.if_match, statement };
|
|
787
|
+
task.cleanup_authorization_operation_id = candidate.operation_id;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (task.base_agent_md)
|
|
792
|
+
await this.save(context);
|
|
793
|
+
}
|
|
706
794
|
try {
|
|
707
795
|
validateOwnershipMessage(entry.payload, { authenticated_from_aid: entry.from_aid, local_aid: context.aid, task, ns });
|
|
708
796
|
}
|
|
@@ -719,6 +807,11 @@ export class OwnershipWorkflow {
|
|
|
719
807
|
}
|
|
720
808
|
else if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action)) {
|
|
721
809
|
const decision = action.endsWith('.accept') ? 'accept' : 'reject';
|
|
810
|
+
if (decision === 'reject' && await this.complete(context, ns)) {
|
|
811
|
+
task.inbox.shift();
|
|
812
|
+
await this.save(context);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
722
815
|
if (!task.approvals[entry.from_aid] || task.approvals[entry.from_aid] === 'pending') {
|
|
723
816
|
task.approvals[entry.from_aid] = decision;
|
|
724
817
|
if (decision === 'reject') {
|
|
@@ -753,7 +846,7 @@ export class OwnershipWorkflow {
|
|
|
753
846
|
}
|
|
754
847
|
else if (action === 'sub.cleaned') {
|
|
755
848
|
const file = ns.file;
|
|
756
|
-
if (ns.relation_version ===
|
|
849
|
+
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
850
|
task.hints[action] = true;
|
|
758
851
|
}
|
|
759
852
|
task.inbox.shift();
|
|
@@ -766,7 +859,8 @@ export class OwnershipWorkflow {
|
|
|
766
859
|
const action = item.payload.action;
|
|
767
860
|
const notification = ['relation.confirmed', 'sub.cleaned'].includes(action);
|
|
768
861
|
const terminal = ['bind.reject', 'transfer.reject', 'operation.stopped'].includes(action);
|
|
769
|
-
|
|
862
|
+
const cleaned = action === 'sub.cleaned' && task.status === 'completed' && await this.confirmSubCleaned(task);
|
|
863
|
+
if (!terminal && (notification ? !this.isTarget(task, ns) && !cleaned : !this.isBase(task, ns) || task.status === 'completed' || task.status === 'rejected' || task.status === 'stopped')) {
|
|
770
864
|
task.outbox = task.outbox.filter((row) => row.key !== item.key);
|
|
771
865
|
await this.save(context);
|
|
772
866
|
continue;
|
|
@@ -789,19 +883,43 @@ export class OwnershipWorkflow {
|
|
|
789
883
|
}
|
|
790
884
|
async complete(context, ns) {
|
|
791
885
|
const task = context.task;
|
|
792
|
-
|
|
886
|
+
const began = Date.now(), cleaned = task.operation === 'release' && ns.exists && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
|
|
887
|
+
if (!this.isTarget(task, ns) && !cleaned)
|
|
793
888
|
return false;
|
|
794
889
|
let consensus;
|
|
795
890
|
if (task.operation === 'release') {
|
|
796
891
|
consensus = await this.deps.files.checkConsensus(task.sub_aid);
|
|
797
|
-
if (consensus.status !== 'released' || consensus.relation_version !== task.target_relation_version || consensus.lock !== ns.lock || consensus.owner_aid !== ns.owner_aid || consensus.agent_md_etag !== ns.etag)
|
|
892
|
+
if ((cleaned ? consensus.status !== 'unbound' || consensus.relation_version !== 0 : consensus.status !== 'released' || consensus.relation_version !== task.target_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
893
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'ownership release could not be freshly confirmed', consensus.status !== 'invalid');
|
|
894
|
+
if (!cleaned && task.files.release && consensus.owner_md_sha256) {
|
|
895
|
+
const source = task.files.release;
|
|
896
|
+
if (source.sha256 !== consensus.owner_md_sha256)
|
|
897
|
+
throw failure('conflict', 'current release material does not belong to this task');
|
|
898
|
+
}
|
|
899
|
+
if (cleaned) {
|
|
900
|
+
const source = task.files.release;
|
|
901
|
+
if (task.local_roles.includes('owner') && source) {
|
|
902
|
+
const object = await this.deps.files.readOwner(task.owner_aid, task.sub_aid);
|
|
903
|
+
if (object.exists && object.content === source.raw && object.sha256 === source.sha256) {
|
|
904
|
+
await this.deps.files.verify(object.content, 'owner', task.sub_aid, task.owner_aid);
|
|
905
|
+
consensus.owner_md_sha256 = object.sha256;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const authorization = await this.cleanupEvidence(task, consensus, ns.content);
|
|
909
|
+
if (!authorization)
|
|
910
|
+
throw failure('conflict', 'no local cleanup original can confirm this release task');
|
|
911
|
+
const after = await this.observe(task.sub_aid);
|
|
912
|
+
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)
|
|
913
|
+
throw failure('unavailable', 'NS changed during cleanup confirmation', true);
|
|
914
|
+
if (authorization !== task.operation_id)
|
|
915
|
+
task.cleanup_authorization_operation_id = authorization;
|
|
916
|
+
}
|
|
799
917
|
const prior = ns.file?.statement;
|
|
800
|
-
if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
|
|
918
|
+
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
919
|
throw failure('conflict', 'ownership released document does not match this generation');
|
|
802
920
|
if (task.local_roles.includes('owner') && !task.files.release)
|
|
803
921
|
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)) {
|
|
922
|
+
if (prior && task.local_roles.includes('sub') && !task.local_roles.includes('owner') && !await this.authorizeCleanup(context, prior)) {
|
|
805
923
|
task.status = 'pending';
|
|
806
924
|
task.step = 'cleanup_action_required';
|
|
807
925
|
task.cleanup_pending = true;
|
|
@@ -825,12 +943,10 @@ export class OwnershipWorkflow {
|
|
|
825
943
|
throw failure('conflict', 'Storage owner.md does not match this task original bytes');
|
|
826
944
|
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
945
|
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
946
|
}
|
|
831
947
|
task.status = 'completed';
|
|
832
948
|
task.step = task.operation === 'release' ? 'released' : 'confirmed';
|
|
833
|
-
task.verified_relation_version =
|
|
949
|
+
task.verified_relation_version = task.target_relation_version;
|
|
834
950
|
task.verified_consensus = { ...consensus, protocol: '2.1' };
|
|
835
951
|
delete task.code;
|
|
836
952
|
delete task.reason;
|
|
@@ -850,9 +966,42 @@ export class OwnershipWorkflow {
|
|
|
850
966
|
await this.save(context);
|
|
851
967
|
return true;
|
|
852
968
|
}
|
|
853
|
-
async
|
|
969
|
+
async cleanupEvidence(task, evidence, raw) {
|
|
970
|
+
const release = task.files.release;
|
|
971
|
+
if (task.local_roles.includes('owner') && release && release.sha256 === evidence.owner_md_sha256 && release.sha256 === await ownershipSha256(release.raw))
|
|
972
|
+
return task.operation_id;
|
|
973
|
+
if (!task.local_roles.includes('sub'))
|
|
974
|
+
return undefined;
|
|
975
|
+
const matches = async (prepared) => Boolean(prepared && prepared.sha256 === evidence.agent_md_sha256 && prepared.sha256 === await ownershipSha256(prepared.raw) && (raw === undefined || prepared.raw === raw) && (prepared.published_etag === undefined || prepared.published_etag === evidence.agent_md_etag));
|
|
976
|
+
if (await matches(task.files.cleanup))
|
|
977
|
+
return task.operation_id;
|
|
978
|
+
let history;
|
|
979
|
+
try {
|
|
980
|
+
history = await this.storage().listOwnershipTasks(this.deps.currentAid());
|
|
981
|
+
}
|
|
982
|
+
catch (error) {
|
|
983
|
+
throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
|
|
984
|
+
}
|
|
985
|
+
for (const snapshot of history) {
|
|
986
|
+
if (!['bind', 'transfer'].includes(String(snapshot.record.operation)))
|
|
987
|
+
continue;
|
|
988
|
+
if (snapshot.record.sub_aid !== task.sub_aid)
|
|
989
|
+
continue;
|
|
990
|
+
const candidate = await this.record(snapshot);
|
|
991
|
+
if (task.cleanup_authorization_operation_id && candidate.operation_id !== task.cleanup_authorization_operation_id || candidate.local_decision !== 'accept' || !candidate.local_roles.includes('sub') || candidate.sub_aid !== task.sub_aid || targetOwner(candidate) !== task.owner_aid || candidate.target_relation_version !== task.base_relation_version || !await matches(candidate.release_cleanup?.files.cleanup))
|
|
992
|
+
continue;
|
|
993
|
+
const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
|
|
994
|
+
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))
|
|
995
|
+
return candidate.operation_id;
|
|
996
|
+
}
|
|
997
|
+
return undefined;
|
|
998
|
+
}
|
|
999
|
+
sameStatement(left, right) {
|
|
1000
|
+
return Boolean(left && right && ['version', 'declaration_id', 'owner_aid', 'sub_aid', 'relation_version', 'declared_at'].every((key) => left[key] === right[key]));
|
|
1001
|
+
}
|
|
1002
|
+
async authorizeCleanup(context, statement) {
|
|
854
1003
|
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;
|
|
1004
|
+
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
1005
|
if (task.cleanup_authorization_operation_id) {
|
|
857
1006
|
const saved = await this.load(task.cleanup_authorization_operation_id);
|
|
858
1007
|
if (!saved || !matches(await this.record(saved)))
|
|
@@ -866,7 +1015,7 @@ export class OwnershipWorkflow {
|
|
|
866
1015
|
catch (error) {
|
|
867
1016
|
throw failure('persistence_unavailable', `ownership cleanup history read failed: ${String(error)}`);
|
|
868
1017
|
}
|
|
869
|
-
const authorized = (await Promise.all(history.map((row) => this.record(row)))).find(matches);
|
|
1018
|
+
const authorized = (await Promise.all(history.filter((row) => row.record.sub_aid === task.sub_aid).map((row) => this.record(row)))).find(matches);
|
|
870
1019
|
if (!authorized)
|
|
871
1020
|
return false;
|
|
872
1021
|
task.cleanup_authorization_operation_id = authorized.operation_id;
|
|
@@ -875,13 +1024,23 @@ export class OwnershipWorkflow {
|
|
|
875
1024
|
}
|
|
876
1025
|
async resumeSubCleanup(context, ns) {
|
|
877
1026
|
const task = context.task;
|
|
878
|
-
if (!['bind', 'transfer'].includes(task.operation) || !task.local_roles.includes('sub') || task.local_decision !== 'accept' || ns.lock !== 'unlocked' ||
|
|
1027
|
+
if (!['bind', 'transfer'].includes(task.operation) || !task.local_roles.includes('sub') || task.local_decision !== 'accept' || ns.lock !== 'unlocked' || task.release_cleanup?.cleaned)
|
|
1028
|
+
return;
|
|
1029
|
+
if (ns.relation_version === 0 && !ns.file?.statement) {
|
|
1030
|
+
const prepared = task.release_cleanup?.files.cleanup;
|
|
1031
|
+
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)
|
|
1032
|
+
throw failure('conflict', 'historical cleanup original file was not confirmed');
|
|
1033
|
+
task.release_cleanup.cleaned = true;
|
|
1034
|
+
await this.save(context);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (ns.relation_version !== task.target_relation_version)
|
|
879
1038
|
return;
|
|
880
1039
|
const consensus = await this.deps.files.checkConsensus(task.sub_aid);
|
|
881
1040
|
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
1041
|
throw failure(consensus.status === 'invalid' ? 'invalid' : 'unavailable', 'NS changed during historical cleanup observation', consensus.status !== 'invalid');
|
|
883
1042
|
const statement = ns.file?.statement;
|
|
884
|
-
if (statement
|
|
1043
|
+
if (!statement || !task.files.agent || !this.sameStatement(statement, parseOwnershipFile(task.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement))
|
|
885
1044
|
throw failure('conflict', 'historical cleanup fields belong to another relation');
|
|
886
1045
|
if (!task.release_cleanup) {
|
|
887
1046
|
task.release_cleanup = { files: {}, cleaned: false };
|
|
@@ -889,7 +1048,7 @@ export class OwnershipWorkflow {
|
|
|
889
1048
|
}
|
|
890
1049
|
if (statement || task.release_cleanup.files.cleanup)
|
|
891
1050
|
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,
|
|
1051
|
+
base_relation_version: task.target_relation_version, target_relation_version: ns.relation_version, files: task.release_cleanup.files, expected_agent_statement: statement,
|
|
893
1052
|
persist: () => this.save(context), guard: () => this.save(context) });
|
|
894
1053
|
task.release_cleanup.cleaned = true;
|
|
895
1054
|
await this.save(context);
|
|
@@ -899,7 +1058,9 @@ export class OwnershipWorkflow {
|
|
|
899
1058
|
const task = context.task;
|
|
900
1059
|
if (!task.cleanup_pending || task.cleanup_done)
|
|
901
1060
|
return;
|
|
902
|
-
|
|
1061
|
+
const releaseOwner = task.operation === 'release' && task.local_roles.includes('owner');
|
|
1062
|
+
const cleaned = task.operation === 'release' && ns.relation_version === 0 && ns.lock === 'unlocked' && !ns.file?.statement;
|
|
1063
|
+
if (!this.isTarget(task, ns) && !cleaned && !releaseOwner) {
|
|
903
1064
|
task.cleanup_pending = false;
|
|
904
1065
|
task.step = 'cleanup_superseded';
|
|
905
1066
|
await this.save(context);
|
|
@@ -907,7 +1068,9 @@ export class OwnershipWorkflow {
|
|
|
907
1068
|
}
|
|
908
1069
|
if (task.operation === 'release' && task.local_roles.includes('sub')) {
|
|
909
1070
|
const prior = ns.file?.statement;
|
|
910
|
-
if (prior && !
|
|
1071
|
+
if (prior && !this.sameStatement(prior, task.base_agent_md?.statement ?? undefined))
|
|
1072
|
+
throw failure('conflict', 'ownership cleanup original relation was replaced');
|
|
1073
|
+
if (prior && !await this.authorizeCleanup(context, prior)) {
|
|
911
1074
|
task.step = 'cleanup_action_required';
|
|
912
1075
|
await this.save(context);
|
|
913
1076
|
this.event(context, 'ownership.action_required');
|
|
@@ -915,18 +1078,18 @@ export class OwnershipWorkflow {
|
|
|
915
1078
|
}
|
|
916
1079
|
if (prior && (prior.owner_aid !== task.owner_aid || prior.relation_version !== task.base_relation_version))
|
|
917
1080
|
throw failure('conflict', 'ownership cleanup current fields differ');
|
|
918
|
-
if (prior)
|
|
919
|
-
await this.deps.files.revokeOwneraid(this.fileTask(context));
|
|
1081
|
+
if (prior || task.files.cleanup)
|
|
1082
|
+
await this.deps.files.revokeOwneraid(this.fileTask(context, prior));
|
|
920
1083
|
this.enqueue(task, task.owner_aid, 'sub.cleaned');
|
|
921
1084
|
}
|
|
922
1085
|
if (task.local_roles.includes('owner') && (task.operation === 'release' || task.operation === 'transfer')) {
|
|
1086
|
+
if (releaseOwner && this.isTarget(task, ns) && ns.file?.statement)
|
|
1087
|
+
return;
|
|
923
1088
|
const prepared = task.files[task.operation === 'release' ? 'release' : 'transfer'];
|
|
924
|
-
if (prepared
|
|
1089
|
+
if (prepared) {
|
|
925
1090
|
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
|
-
}
|
|
1091
|
+
if (object.exists && (object.sha256 !== prepared.sha256 || object.content !== prepared.raw))
|
|
1092
|
+
throw failure('conflict', 'ownership cleanup original owner file has been replaced');
|
|
930
1093
|
}
|
|
931
1094
|
await this.deps.files.cleanupOwnerFile(this.fileTask(context));
|
|
932
1095
|
}
|
|
@@ -962,7 +1125,7 @@ export class OwnershipWorkflow {
|
|
|
962
1125
|
delete task.reason;
|
|
963
1126
|
task.retryable = true;
|
|
964
1127
|
if (await this.complete(context, ns)) {
|
|
965
|
-
if (task.status === 'completed' && task.operation === 'release'
|
|
1128
|
+
if (task.status === 'completed' && task.operation === 'release') {
|
|
966
1129
|
await this.cleanup(context, ns);
|
|
967
1130
|
ns = await this.observe(task.sub_aid);
|
|
968
1131
|
}
|