@agentunion/fastaun 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 +13 -1
- package/dist/agent-md-schema.js +61 -33
- package/dist/agent-md-schema.js.map +1 -1
- package/dist/agent-md.d.ts +8 -1
- package/dist/agent-md.js +243 -119
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +29 -0
- package/dist/aid-store.js +179 -34
- package/dist/aid-store.js.map +1 -1
- package/dist/aid.js +18 -13
- package/dist/aid.js.map +1 -1
- package/dist/auth.js +74 -2
- package/dist/auth.js.map +1 -1
- package/dist/cert-utils.d.ts +43 -1
- package/dist/cert-utils.js +144 -56
- package/dist/cert-utils.js.map +1 -1
- package/dist/client/delivery.d.ts +9 -0
- package/dist/client/delivery.js +179 -11
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/rpc-pipeline.d.ts +1 -0
- package/dist/client/rpc-pipeline.js +125 -82
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.js +95 -89
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.js +48 -19
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +5 -3
- package/dist/events.js +25 -8
- 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 +2 -1
- package/dist/index.js.map +1 -1
- package/dist/keystore/aid-db.d.ts +1 -1
- package/dist/keystore/aid-db.js +11 -2
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +1 -1
- package/dist/keystore/local-token-store.d.ts +1 -1
- package/dist/keystore/local-token-store.js +2 -2
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/org-protocol.js +77 -13
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +26 -8
- package/dist/ownership-v21.js +147 -70
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +5 -0
- package/dist/ownership-workflow.js +366 -56
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +2 -1
- package/dist/tools/cross-sdk-agent.js +109 -6
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +10 -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
|
@@ -36,15 +36,15 @@ export function parseOwnershipMessage(content, context) {
|
|
|
36
36
|
throw new ValidationError('ownership message contains unknown or missing fields');
|
|
37
37
|
if (message.type !== OWNERSHIP_SDK_MESSAGE_TYPE)
|
|
38
38
|
throw ownershipFailure('unsupported_protocol', 'legacy ownership control is unsupported');
|
|
39
|
-
if (!OWNERSHIP_UUID_V4_RE.test(message.operation_id))
|
|
39
|
+
if (typeof message.operation_id !== 'string' || !OWNERSHIP_UUID_V4_RE.test(message.operation_id))
|
|
40
40
|
throw new ValidationError('operation_id must be a UUIDv4');
|
|
41
41
|
if (!['bind', 'release', 'transfer'].includes(message.operation))
|
|
42
42
|
throw new ValidationError('ownership operation is invalid');
|
|
43
43
|
ownershipAid(message.sub_aid, 'sub_aid');
|
|
44
44
|
ownershipAid(message.owner_aid, 'owner_aid');
|
|
45
45
|
ownershipInteger(message.base_relation_version, 'base_relation_version', 0);
|
|
46
|
-
if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + 1)
|
|
47
|
-
throw new ValidationError('ownership target version
|
|
46
|
+
if (ownershipInteger(message.target_relation_version, 'target_relation_version') !== message.base_relation_version + (message.operation === 'release' ? 0 : 1))
|
|
47
|
+
throw new ValidationError('ownership target version does not match the operation');
|
|
48
48
|
if (message.operation === 'transfer') {
|
|
49
49
|
if (ownershipAid(message.new_owner_aid, 'new_owner_aid') === message.owner_aid)
|
|
50
50
|
throw new ValidationError('transfer target must differ from current owner');
|
|
@@ -107,7 +107,7 @@ export function parseOwnershipMessage(content, context) {
|
|
|
107
107
|
if (ns.protocol !== '2.1')
|
|
108
108
|
throw ownershipFailure('unsupported_protocol', 'NS ownership protocol is unavailable');
|
|
109
109
|
const base = ns.relation_version === message.base_relation_version && (message.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === message.owner_aid);
|
|
110
|
-
const target =
|
|
110
|
+
const target = message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === message.target_relation_version || ns.relation_version === 0) : ns.relation_version === message.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(message);
|
|
111
111
|
if (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target : !base && !target)
|
|
112
112
|
throw ownershipFailure('conflict', 'ownership current NS relation does not match this task');
|
|
113
113
|
}
|
|
@@ -154,7 +154,7 @@ export class OwnershipWorkflow {
|
|
|
154
154
|
return this.start('transfer', ownershipAid(subAid, 'sub_aid'), this.local(), ownershipAid(newOwnerAid, 'new_owner_aid'));
|
|
155
155
|
}
|
|
156
156
|
async resumeRegistration(ownerAid, operationId) {
|
|
157
|
-
if (!OWNERSHIP_UUID_V4_RE.test(operationId))
|
|
157
|
+
if (typeof operationId !== 'string' || !OWNERSHIP_UUID_V4_RE.test(operationId))
|
|
158
158
|
throw ownershipFailure('invalid', 'registration operation_id is invalid');
|
|
159
159
|
return this.start('bind', this.local(), ownershipAid(ownerAid, 'owner_aid'), undefined, operationId);
|
|
160
160
|
}
|
|
@@ -220,21 +220,13 @@ export class OwnershipWorkflow {
|
|
|
220
220
|
if (!['bind.request', 'transfer.request'].includes(message.action) && !(message.operation === 'release' && message.action === 'relation.confirmed'))
|
|
221
221
|
return true;
|
|
222
222
|
handle = { record: this.newRecord(message, from, 'pending'), revision: null };
|
|
223
|
-
if (message.operation === 'release') {
|
|
224
|
-
const authorization = this.cleanupAuthorization(handle.record);
|
|
225
|
-
if (authorization) {
|
|
226
|
-
handle.record.local_decision = 'accept';
|
|
227
|
-
handle.record.approvals[this.local()] = 'accept';
|
|
228
|
-
handle.record.cleanup_authorization_id = authorization.operation_id;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
223
|
}
|
|
232
224
|
if (message.action === 'sub.cleaned') {
|
|
233
225
|
const began = Date.now();
|
|
234
226
|
const before = await this.readNs(message.sub_aid), after = await this.readNs(message.sub_aid);
|
|
235
227
|
if (Date.now() - began > 30_000 || before.exists !== after.exists || before.etag !== after.etag || before.relation_version !== after.relation_version || before.lock !== after.lock || before.owner_aid !== after.owner_aid || ownershipSha256(before.content) !== ownershipSha256(after.content))
|
|
236
228
|
throw ownershipFailure('unavailable', 'NS changed during sub cleanup verification', true);
|
|
237
|
-
if (!before.exists || before.relation_version !==
|
|
229
|
+
if (!before.exists || before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
|
|
238
230
|
return true;
|
|
239
231
|
const file = parseOwnershipFile(before.content, 'agent', { expectedSubAid: message.sub_aid });
|
|
240
232
|
if (Object.hasOwn(file.frontmatter, 'owner') || Object.hasOwn(file.frontmatter, 'ownership'))
|
|
@@ -253,15 +245,23 @@ export class OwnershipWorkflow {
|
|
|
253
245
|
});
|
|
254
246
|
}
|
|
255
247
|
async resume() {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
await this.
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
|
|
248
|
+
const failed = [];
|
|
249
|
+
for (const snapshot of this.list(true)) {
|
|
250
|
+
try {
|
|
251
|
+
const record = this.decode(snapshot).record;
|
|
252
|
+
await this.runSerialized(record.sub_aid, async () => {
|
|
253
|
+
const handle = this.load(record.operation_id);
|
|
254
|
+
await this.drive(handle);
|
|
255
|
+
if (handle.record.request_verified && handle.record.local_decision === 'pending' && handle.record.status === 'pending' && handle.record.operation !== 'release')
|
|
256
|
+
this.emit(handle.record, 'ownership.request');
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
failed.push(`${snapshot.operation_id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
261
|
+
}
|
|
264
262
|
}
|
|
263
|
+
if (failed.length)
|
|
264
|
+
throw ownershipFailure('persistence_unavailable', failed.join('; '));
|
|
265
265
|
}
|
|
266
266
|
local() { return ownershipAid(this.deps.currentAid(), 'local_aid'); }
|
|
267
267
|
requireApplicable(subAid) {
|
|
@@ -273,9 +273,9 @@ export class OwnershipWorkflow {
|
|
|
273
273
|
throw ownershipFailure('persistence_unavailable', 'atomic ownership task storage is unavailable');
|
|
274
274
|
return this.deps.store;
|
|
275
275
|
}
|
|
276
|
-
list() {
|
|
276
|
+
list(isolateErrors = false) {
|
|
277
277
|
try {
|
|
278
|
-
return this.store().listOwnershipTasks(this.local());
|
|
278
|
+
return this.store().listOwnershipTasks(this.local(), isolateErrors);
|
|
279
279
|
}
|
|
280
280
|
catch (error) {
|
|
281
281
|
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
@@ -293,7 +293,7 @@ export class OwnershipWorkflow {
|
|
|
293
293
|
decode(snapshot) {
|
|
294
294
|
const record = snapshot.task;
|
|
295
295
|
try {
|
|
296
|
-
if (record.protocol !== '2.1' || !OWNERSHIP_UUID_V4_RE.test(record.operation_id) || record.operation_id !== snapshot.operation_id || !['bind', 'release', 'transfer'].includes(record.operation) || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(record.status) || typeof record.step !== 'string')
|
|
296
|
+
if (record.protocol !== '2.1' || typeof record.operation_id !== 'string' || !OWNERSHIP_UUID_V4_RE.test(record.operation_id) || record.operation_id !== snapshot.operation_id || !['bind', 'release', 'transfer'].includes(record.operation) || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(record.status) || typeof record.step !== 'string')
|
|
297
297
|
throw new Error('invalid ownership task');
|
|
298
298
|
ownershipAid(record.sub_aid, 'sub_aid');
|
|
299
299
|
ownershipAid(record.owner_aid, 'owner_aid');
|
|
@@ -304,22 +304,53 @@ export class OwnershipWorkflow {
|
|
|
304
304
|
}
|
|
305
305
|
else if (record.new_owner_aid !== undefined)
|
|
306
306
|
throw new Error('unexpected transfer target');
|
|
307
|
-
if (ownershipInteger(record.target_relation_version, 'target_relation_version') !== ownershipInteger(record.base_relation_version, 'base_relation_version', 0) + 1 || !participants(record).includes(record.initiator_aid) || record.operation !== 'bind' && record.initiator_aid !== record.owner_aid)
|
|
307
|
+
if (ownershipInteger(record.target_relation_version, 'target_relation_version') !== ownershipInteger(record.base_relation_version, 'base_relation_version', 0) + (record.operation === 'release' ? 0 : 1) || !participants(record).includes(record.initiator_aid) || record.operation !== 'bind' && record.initiator_aid !== record.owner_aid)
|
|
308
308
|
throw new Error('invalid fixed ownership context');
|
|
309
309
|
if (JSON.stringify(record.local_roles) !== JSON.stringify(localRoles(record, this.local())) || !record.local_roles.length || !['pending', 'accept', 'reject'].includes(record.local_decision) || !record.approvals || participants(record).some((aid) => !['pending', 'accept', 'reject'].includes(record.approvals[aid])) || record.approvals[this.local()] !== record.local_decision || !record.files || typeof record.files !== 'object' || Array.isArray(record.files) || !Array.isArray(record.inbox) || !Array.isArray(record.outbox) || !Array.isArray(record.received) || !Array.isArray(record.notices) || typeof record.request_verified !== 'boolean' || typeof record.request_notified !== 'boolean')
|
|
310
310
|
throw new Error('ownership approval or pending state is corrupt');
|
|
311
311
|
if (Object.keys(record.approvals).length !== participants(record).length || record.approvals[record.initiator_aid] !== 'accept' || record.initiator_aid === this.local() && record.local_decision !== 'accept')
|
|
312
312
|
throw new Error('ownership initiator or approval participants are corrupt');
|
|
313
|
+
if (record.request_verified) {
|
|
314
|
+
const base = record.base_agent_md;
|
|
315
|
+
if (!base || base.exists !== true || typeof base.raw !== 'string' || base.sha256 !== ownershipSha256(base.raw) || typeof base.etag !== 'string' || !base.etag)
|
|
316
|
+
throw new Error('ownership original agent.md evidence is corrupt');
|
|
317
|
+
const statement = parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
|
|
318
|
+
if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version)
|
|
319
|
+
throw new Error('ownership original statement is corrupt');
|
|
320
|
+
const owner = record.base_owner_md;
|
|
321
|
+
if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
|
|
322
|
+
throw new Error('ownership original owner.md evidence is corrupt');
|
|
323
|
+
if (owner.exists) {
|
|
324
|
+
const declaration = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: statement?.owner_aid ?? record.owner_aid });
|
|
325
|
+
if (declaration.statement?.relation_version !== record.base_relation_version)
|
|
326
|
+
throw new Error('ownership original owner statement is corrupt');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
for (const item of record.inbox)
|
|
330
|
+
parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record });
|
|
331
|
+
for (const key of record.received) {
|
|
332
|
+
if (typeof key !== 'string' || key.split('|').length !== 3)
|
|
333
|
+
throw new Error('ownership received key is corrupt');
|
|
334
|
+
const [from, operationId, action] = key.split('|');
|
|
335
|
+
parseOwnershipMessage({ type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation: record.operation, action,
|
|
336
|
+
sub_aid: record.sub_aid, owner_aid: record.owner_aid, ...(record.operation === 'transfer' ? { new_owner_aid: record.new_owner_aid } : {}),
|
|
337
|
+
base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version }, { authenticated_from_aid: from, local_aid: this.local(), task: record });
|
|
338
|
+
}
|
|
339
|
+
for (const item of record.outbox)
|
|
340
|
+
this.validateOutgoing(record, item);
|
|
313
341
|
if (record.status === 'completed' && (record.verified_relation_version !== record.target_relation_version || record.operation !== 'release' && record.local_decision !== 'accept'))
|
|
314
342
|
throw new Error('ownership completion evidence or local approval is corrupt');
|
|
315
343
|
const transferEvidenceRequired = record.operation === 'transfer' && (record.authorization_sha256 !== undefined || record.expires_at !== undefined || (record.status === 'completed' || ['owner', 'agent'].includes(record.step) || record.files.agent || record.files.owner) && record.local_roles.some((role) => role === 'sub' || role === 'new_owner'));
|
|
316
344
|
if (transferEvidenceRequired && (!/^[0-9a-f]{64}$/.test(record.authorization_sha256 ?? '') || !Number.isSafeInteger(record.expires_at) || (record.expires_at ?? 0) <= 0))
|
|
317
345
|
throw new Error('ownership transfer authorization evidence is corrupt');
|
|
318
346
|
for (const [key, saved] of Object.entries(record.files)) {
|
|
319
|
-
if (!['agent', 'owner', 'release', 'transfer', 'cleanup'].includes(key) || !saved || typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !saved.condition || typeof saved.condition !== 'object' || Object.keys(saved.condition).length !== 1 || saved.
|
|
347
|
+
if (!['agent', 'owner', 'release', 'transfer', 'cleanup'].includes(key) || !saved || typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !saved.condition || typeof saved.condition !== 'object' || Object.keys(saved.condition).length !== (key === 'agent' ? 1 : 0) || saved.published_etag !== undefined && (key !== 'agent' || typeof saved.published_etag !== 'string' || !saved.published_etag))
|
|
320
348
|
throw new Error('ownership saved file is corrupt');
|
|
321
|
-
if (key === 'agent'
|
|
349
|
+
if (key === 'agent' && !(typeof saved.condition.if_match === 'string' && saved.condition.if_match || saved.condition.if_none_match === '*'))
|
|
322
350
|
throw new Error('ownership original file condition is corrupt');
|
|
351
|
+
const original = saved.original_owner;
|
|
352
|
+
if (key !== 'agent' && key !== 'cleanup' && (!original || typeof original.exists !== 'boolean' || typeof original.raw !== 'string' || original.sha256 !== (original.exists ? ownershipSha256(original.raw) : '') || !original.exists && original.raw !== ''))
|
|
353
|
+
throw new Error('ownership original owner material is corrupt');
|
|
323
354
|
const owner = ['agent', 'owner'].includes(key) ? targetOwner(record) : record.owner_aid;
|
|
324
355
|
const file = parseOwnershipFile(saved.raw, key === 'agent' ? 'agent' : 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: owner });
|
|
325
356
|
const version = ['release', 'transfer', 'cleanup'].includes(key) ? record.base_relation_version : record.target_relation_version;
|
|
@@ -339,7 +370,8 @@ export class OwnershipWorkflow {
|
|
|
339
370
|
}
|
|
340
371
|
if (record.status === 'completed') {
|
|
341
372
|
const evidence = record.completion_evidence;
|
|
342
|
-
|
|
373
|
+
const cleaned = record.operation === 'release' && evidence?.status === 'unbound' && evidence.relation_version === 0 && record.cleanup_status === 'cleaned' && Boolean(this.cleanupEvidence(record, evidence));
|
|
374
|
+
if (!evidence || evidence.sub_aid !== record.sub_aid || !cleaned && (evidence.relation_version !== record.target_relation_version || evidence.status !== (record.operation === 'release' ? 'released' : 'consensus')) || evidence.lock !== (record.operation === 'release' ? 'unlocked' : 'locked') || evidence.owner_aid !== (record.operation === 'release' ? null : targetOwner(record)) || typeof evidence.agent_md_etag !== 'string' || !evidence.agent_md_etag || !/^[0-9a-f]{64}$/.test(evidence.agent_md_sha256) || typeof evidence.owner_md_sha256 !== 'string' || record.operation !== 'release' && !/^[0-9a-f]{64}$/.test(evidence.owner_md_sha256))
|
|
343
375
|
throw new Error('ownership target acceptance evidence is corrupt');
|
|
344
376
|
if (record.operation !== 'release' && (record.local_roles.includes('sub') && record.files.agent?.hash !== evidence.agent_md_sha256 || this.local() === targetOwner(record) && record.files.owner?.hash !== evidence.owner_md_sha256 || record.operation === 'transfer' && record.local_roles.includes('owner') && !record.files.transfer))
|
|
345
377
|
throw new Error('ownership target file evidence is missing');
|
|
@@ -445,8 +477,18 @@ export class OwnershipWorkflow {
|
|
|
445
477
|
await this.recordError(active, error);
|
|
446
478
|
return ownershipTaskResult(record);
|
|
447
479
|
}
|
|
448
|
-
|
|
449
|
-
|
|
480
|
+
let base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
|
|
481
|
+
if (base && record.request_verified) {
|
|
482
|
+
try {
|
|
483
|
+
await this.assertBaseMaterial(active, ns);
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (!(error instanceof StateError) || error.stringCode !== 'conflict')
|
|
487
|
+
throw error;
|
|
488
|
+
base = false;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.target_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
|
|
450
492
|
if (!base && !target) {
|
|
451
493
|
await this.recordError(active, ownershipFailure('conflict', 'the operation was superseded by a different relation result'));
|
|
452
494
|
const result = ownershipTaskResult(record);
|
|
@@ -470,13 +512,16 @@ export class OwnershipWorkflow {
|
|
|
470
512
|
throw ownershipFailure('unavailable', 'initial agent.md is not published', true);
|
|
471
513
|
if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type === 'human')
|
|
472
514
|
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
515
|
+
if (operation === 'bind' && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
|
|
516
|
+
throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
|
|
473
517
|
if (operation !== 'bind') {
|
|
474
518
|
const consensus = await this.deps.files.checkConsensus(subAid);
|
|
475
519
|
if (consensus.status !== 'consensus' || consensus.owner_aid !== ownerAid || consensus.relation_version !== ns.relation_version)
|
|
476
520
|
throw ownershipFailure(consensus.status === 'invalid' ? 'invalid' : consensus.status === 'unavailable' ? consensus.code ?? 'unavailable' : 'forbidden', 'the current original owner consensus cannot be verified', consensus.status === 'unavailable');
|
|
477
521
|
}
|
|
478
|
-
ownershipInteger(ns.relation_version + 1, 'target_relation_version');
|
|
479
|
-
const record = this.newRecord({ operation_id: operationId ?? randomUUID(), operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid, base_relation_version: ns.relation_version, target_relation_version:
|
|
522
|
+
const targetVersion = ownershipInteger(ns.relation_version + (operation === 'release' ? 0 : 1), 'target_relation_version');
|
|
523
|
+
const record = this.newRecord({ operation_id: operationId ?? randomUUID(), operation, sub_aid: subAid, owner_aid: ownerAid, new_owner_aid: newOwnerAid, base_relation_version: ns.relation_version, target_relation_version: targetVersion }, this.local(), 'accept');
|
|
524
|
+
await this.captureBase(record, ns);
|
|
480
525
|
if (operation !== 'release')
|
|
481
526
|
for (const to of participants(record))
|
|
482
527
|
if (to !== this.local())
|
|
@@ -509,6 +554,7 @@ export class OwnershipWorkflow {
|
|
|
509
554
|
const ns = await this.readNs(record.sub_aid);
|
|
510
555
|
if (ns.relation_version !== record.base_relation_version || (record.operation === 'bind' ? ns.lock !== 'unlocked' : ns.lock !== 'locked' || ns.owner_aid !== record.owner_aid))
|
|
511
556
|
throw ownershipFailure('conflict', 'the original relation changed before approval');
|
|
557
|
+
await this.assertBaseMaterial(handle, ns);
|
|
512
558
|
record.local_decision = decision;
|
|
513
559
|
record.approvals[this.local()] = decision;
|
|
514
560
|
this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
|
|
@@ -540,19 +586,42 @@ export class OwnershipWorkflow {
|
|
|
540
586
|
record.outbox.push({ to, payload });
|
|
541
587
|
record.notices.push(key);
|
|
542
588
|
}
|
|
589
|
+
validateOutgoing(record, item) {
|
|
590
|
+
try {
|
|
591
|
+
const message = parseOwnershipMessage(item.payload, { authenticated_from_aid: this.local(), local_aid: item.to, task: { ...record, local_decision: 'accept' } });
|
|
592
|
+
const rejected = ['bind.reject', 'transfer.reject'].includes(message.action);
|
|
593
|
+
if (message.action !== 'operation.stopped' && record.local_decision !== (rejected ? 'reject' : 'accept') || record.approvals[this.local()] !== record.local_decision)
|
|
594
|
+
throw new Error('ownership outgoing action lacks local approval');
|
|
595
|
+
if (['bind.request', 'transfer.request'].includes(message.action) && record.initiator_aid !== this.local())
|
|
596
|
+
throw new Error('ownership outgoing request is not from the initiator');
|
|
597
|
+
if ((message.action === 'transfer.ready' || message.action === 'owner.ready' && record.operation === 'bind') && !participants(record).every((aid) => record.approvals[aid] === 'accept'))
|
|
598
|
+
throw new Error('ownership outgoing action lacks participant approvals');
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
602
|
+
}
|
|
603
|
+
}
|
|
543
604
|
async flush(handle) {
|
|
544
605
|
while (handle.record.outbox.length) {
|
|
545
606
|
const message = handle.record.outbox[0];
|
|
546
|
-
|
|
607
|
+
this.validateOutgoing(handle.record, message);
|
|
547
608
|
const record = handle.record;
|
|
609
|
+
if (['rejected', 'stopped', 'error'].includes(record.status) && !['bind.reject', 'transfer.reject', 'operation.stopped'].includes(message.payload.action)) {
|
|
610
|
+
record.outbox.shift();
|
|
611
|
+
await this.save(handle);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const ns = await this.readNs(record.sub_aid);
|
|
548
615
|
const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
|
|
549
|
-
const target =
|
|
616
|
+
const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.target_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
|
|
550
617
|
const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
|
|
551
618
|
if (requiresTarget ? !target : !base) {
|
|
552
619
|
record.outbox.shift();
|
|
553
620
|
await this.save(handle);
|
|
554
621
|
continue;
|
|
555
622
|
}
|
|
623
|
+
if (base && record.request_verified)
|
|
624
|
+
await this.assertBaseMaterial(handle, ns);
|
|
556
625
|
if (message.payload.action === 'transfer.ready' && record.status !== 'completed') {
|
|
557
626
|
const expiresAt = record.files.transfer?.expires_at;
|
|
558
627
|
if (typeof expiresAt !== 'number' || !Number.isSafeInteger(expiresAt) || expiresAt <= 0)
|
|
@@ -567,19 +636,77 @@ export class OwnershipWorkflow {
|
|
|
567
636
|
}
|
|
568
637
|
async readNs(subAid) {
|
|
569
638
|
const ns = await this.deps.files.readAgent(subAid);
|
|
570
|
-
const file = ns.exists
|
|
571
|
-
if (ns.
|
|
639
|
+
const file = ns.exists ? await this.deps.files.verifyFile(ns.content, 'agent', subAid) : undefined;
|
|
640
|
+
if (ns.relation_version !== (file?.statement?.relation_version ?? 0))
|
|
572
641
|
throw ownershipFailure('unavailable', 'NS relation metadata does not match agent.md', true);
|
|
573
642
|
const currentOwner = ns.lock === 'locked' ? file?.statement?.owner_aid : null;
|
|
574
643
|
if (ns.lock === 'locked' && (!file?.statement || file.statement.relation_version !== ns.relation_version) || ns.owner_aid !== undefined && ns.owner_aid !== currentOwner)
|
|
575
644
|
throw ownershipFailure('unavailable', 'NS owner metadata and signed agent.md do not agree', true);
|
|
576
645
|
return { ...ns, owner_aid: currentOwner };
|
|
577
646
|
}
|
|
647
|
+
async captureBase(record, ns) {
|
|
648
|
+
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
|
|
649
|
+
record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement };
|
|
650
|
+
if (!statement) {
|
|
651
|
+
record.base_owner_md = { exists: false, raw: '', sha256: '' };
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
const owner = await this.deps.files.readOwner(statement.owner_aid, record.sub_aid);
|
|
655
|
+
if ((record.operation === 'transfer' || record.operation === 'release' && ns.lock === 'locked') && !owner.exists)
|
|
656
|
+
throw ownershipFailure('unavailable', 'original owner.md is unavailable', true);
|
|
657
|
+
if (owner.exists)
|
|
658
|
+
await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, statement.owner_aid);
|
|
659
|
+
record.base_owner_md = { exists: owner.exists, raw: owner.content, sha256: owner.exists ? ownershipSha256(owner.content) : '' };
|
|
660
|
+
}
|
|
661
|
+
async assertBaseMaterial(handle, ns) {
|
|
662
|
+
const record = handle.record;
|
|
663
|
+
this.assertAgentBaseMaterial(record, ns);
|
|
664
|
+
const original = record.base_owner_md;
|
|
665
|
+
if (!original)
|
|
666
|
+
throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is missing');
|
|
667
|
+
const originalOwnerAid = record.base_agent_md.statement?.owner_aid;
|
|
668
|
+
if (!originalOwnerAid)
|
|
669
|
+
return;
|
|
670
|
+
const current = await this.deps.files.readOwner(originalOwnerAid, record.sub_aid);
|
|
671
|
+
if (current.exists === original.exists && current.content === original.raw && (current.exists ? current.sha256 : '') === original.sha256)
|
|
672
|
+
return;
|
|
673
|
+
if (!current.exists)
|
|
674
|
+
throw ownershipFailure('conflict', 'the original owner.md was replaced');
|
|
675
|
+
const key = record.operation === 'bind' ? 'owner' : record.operation === 'release' ? 'release' : 'transfer';
|
|
676
|
+
const saved = record.files[key];
|
|
677
|
+
if (saved && saved.raw === current.content && saved.hash === current.sha256)
|
|
678
|
+
return;
|
|
679
|
+
if (record.operation !== 'bind' && this.local() !== record.owner_aid && record.local_decision === 'accept') {
|
|
680
|
+
const owner = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, originalOwnerAid);
|
|
681
|
+
const previous = original.exists ? parseOwnershipFile(original.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: originalOwnerAid }) : undefined;
|
|
682
|
+
const sameStatement = previous?.statement && Object.keys(previous.statement).every((field) => previous.statement[field] === owner.statement?.[field]);
|
|
683
|
+
const allowed = sameStatement && (record.operation === 'release' ? owner.action === 'release' : owner.action === 'transfer_owner' && owner.transfer?.new_owner_aid === record.new_owner_aid && owner.transfer?.target_relation_version === record.target_relation_version);
|
|
684
|
+
if (allowed && !saved) {
|
|
685
|
+
if (record.operation === 'transfer')
|
|
686
|
+
await this.deps.files.requireTransfer(this.fileTask(handle));
|
|
687
|
+
record.files[key] = { raw: current.content, hash: current.sha256, condition: {}, original_owner: original, ...(owner.transfer ? { expires_at: owner.transfer.expires_at } : {}) };
|
|
688
|
+
await this.save(handle);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
throw ownershipFailure('conflict', 'the original owner statement was replaced');
|
|
693
|
+
}
|
|
694
|
+
assertAgentBaseMaterial(record, ns) {
|
|
695
|
+
const base = record.base_agent_md;
|
|
696
|
+
if (!base)
|
|
697
|
+
throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
|
|
698
|
+
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null;
|
|
699
|
+
const same = base.statement ? Boolean(statement && Object.keys(base.statement).every((key) => base.statement[key] === statement[key])) : statement === null && ns.content === base.raw && ownershipSha256(ns.content) === base.sha256;
|
|
700
|
+
if (ns.exists !== base.exists || !same)
|
|
701
|
+
throw ownershipFailure('conflict', 'the original ownership statement was replaced');
|
|
702
|
+
}
|
|
578
703
|
async processInbox(handle) {
|
|
579
704
|
const record = handle.record;
|
|
580
705
|
while (record.inbox.length) {
|
|
581
706
|
const item = record.inbox[0];
|
|
582
707
|
const ns = await this.readNs(record.sub_aid);
|
|
708
|
+
if (record.request_verified && ns.relation_version === record.base_relation_version && !['relation.confirmed', 'sub.cleaned'].includes(item.payload.action))
|
|
709
|
+
await this.assertBaseMaterial(handle, ns);
|
|
583
710
|
try {
|
|
584
711
|
parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record, ns });
|
|
585
712
|
}
|
|
@@ -596,11 +723,60 @@ export class OwnershipWorkflow {
|
|
|
596
723
|
continue;
|
|
597
724
|
}
|
|
598
725
|
const action = item.payload.action;
|
|
599
|
-
if (['bind.request', 'transfer.request'].includes(action))
|
|
726
|
+
if (['bind.request', 'transfer.request'].includes(action)) {
|
|
727
|
+
if (!record.request_verified)
|
|
728
|
+
await this.captureBase(record, ns);
|
|
600
729
|
record.request_verified = true;
|
|
730
|
+
}
|
|
601
731
|
if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action)) {
|
|
602
732
|
if (record.approvals[item.from] === 'pending' && record.status === 'pending') {
|
|
603
733
|
const rejected = action.endsWith('.reject');
|
|
734
|
+
if (rejected) {
|
|
735
|
+
try {
|
|
736
|
+
if (await this.confirm(handle)) {
|
|
737
|
+
record.inbox.shift();
|
|
738
|
+
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
739
|
+
await this.save(handle);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
const observed = await this.deps.files.checkConsensus(record.sub_aid, { owner_aid: targetOwner(record), relation_version: record.target_relation_version });
|
|
743
|
+
const targetReached = observed.status === 'consensus' && observed.owner_aid === targetOwner(record) && observed.relation_version === record.target_relation_version;
|
|
744
|
+
if (targetReached) {
|
|
745
|
+
record.code = 'unavailable';
|
|
746
|
+
record.reason = 'ownership target is committed but local evidence is incomplete';
|
|
747
|
+
record.retryable = true;
|
|
748
|
+
record.inbox.shift();
|
|
749
|
+
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
750
|
+
await this.save(handle);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
756
|
+
throw error;
|
|
757
|
+
try {
|
|
758
|
+
const ns = await this.readNs(record.sub_aid);
|
|
759
|
+
if (ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record)) {
|
|
760
|
+
record.code = 'unavailable';
|
|
761
|
+
record.reason = 'ownership target is committed but local evidence is incomplete';
|
|
762
|
+
record.retryable = true;
|
|
763
|
+
record.inbox.shift();
|
|
764
|
+
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
765
|
+
await this.save(handle);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
catch (observationError) {
|
|
770
|
+
if (observationError instanceof StateError && observationError.stringCode === 'persistence_unavailable')
|
|
771
|
+
throw observationError;
|
|
772
|
+
}
|
|
773
|
+
record.code = error instanceof StateError ? error.stringCode : 'unavailable';
|
|
774
|
+
record.reason = error instanceof Error ? error.message : String(error);
|
|
775
|
+
record.retryable = true;
|
|
776
|
+
await this.save(handle);
|
|
777
|
+
throw error;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
604
780
|
record.approvals[item.from] = rejected ? 'reject' : 'accept';
|
|
605
781
|
if (rejected) {
|
|
606
782
|
record.status = 'rejected';
|
|
@@ -625,8 +801,30 @@ export class OwnershipWorkflow {
|
|
|
625
801
|
if (!file.statement)
|
|
626
802
|
record.cleanup_status = 'cleaned';
|
|
627
803
|
}
|
|
628
|
-
if (record.operation === 'release')
|
|
804
|
+
if (record.operation === 'release' && !record.request_verified) {
|
|
805
|
+
if (ns.relation_version === 0) {
|
|
806
|
+
const authorizationId = this.cleanupEvidence(record, { agent_md_etag: ns.etag, agent_md_sha256: ownershipSha256(ns.content), owner_md_sha256: '' }, ns.content);
|
|
807
|
+
if (!authorizationId)
|
|
808
|
+
throw ownershipFailure('conflict', 'no local cleanup evidence can verify the release request');
|
|
809
|
+
const authorization = this.load(authorizationId).record;
|
|
810
|
+
const saved = authorization.files.agent;
|
|
811
|
+
const statement = parseOwnershipFile(saved.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
812
|
+
const etag = saved.published_etag ?? authorization.completion_evidence?.agent_md_etag;
|
|
813
|
+
if (!etag)
|
|
814
|
+
throw ownershipFailure('persistence_unavailable', 'historical approved agent.md ETag is missing');
|
|
815
|
+
record.base_agent_md = { exists: true, raw: saved.raw, sha256: saved.hash, etag, statement };
|
|
816
|
+
const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
817
|
+
if (owner.exists)
|
|
818
|
+
await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
819
|
+
record.base_owner_md = { exists: owner.exists, raw: owner.content, sha256: owner.exists ? ownershipSha256(owner.content) : '' };
|
|
820
|
+
record.cleanup_authorization_id = authorizationId;
|
|
821
|
+
if (!(await this.confirm(handle)))
|
|
822
|
+
throw ownershipFailure('conflict', 'no local cleanup evidence can verify the release request');
|
|
823
|
+
}
|
|
824
|
+
else
|
|
825
|
+
await this.captureBase(record, ns);
|
|
629
826
|
record.request_verified = true;
|
|
827
|
+
}
|
|
630
828
|
record.inbox.shift();
|
|
631
829
|
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
632
830
|
const request = record.request_verified && record.local_decision === 'pending' && record.operation !== 'release' && !record.request_notified;
|
|
@@ -645,7 +843,7 @@ export class OwnershipWorkflow {
|
|
|
645
843
|
const record = handle.record;
|
|
646
844
|
const task = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid,
|
|
647
845
|
new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
|
|
648
|
-
files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at,
|
|
846
|
+
files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at, base_agent_md: record.base_agent_md, base_owner_md: record.base_owner_md,
|
|
649
847
|
persist: async () => {
|
|
650
848
|
record.authorization_sha256 = task.authorization_sha256;
|
|
651
849
|
record.expires_at = task.expires_at;
|
|
@@ -703,6 +901,7 @@ export class OwnershipWorkflow {
|
|
|
703
901
|
if (ns.relation_version !== record.base_relation_version || record.operation === 'bind' && ns.lock !== 'unlocked' || record.operation === 'transfer' && (ns.lock !== 'locked' || ns.owner_aid !== record.owner_aid)) {
|
|
704
902
|
throw ownershipFailure('conflict', 'the original relation was replaced');
|
|
705
903
|
}
|
|
904
|
+
await this.assertBaseMaterial(handle, ns);
|
|
706
905
|
if (record.expires_at !== undefined && record.expires_at <= Math.trunc(Date.now() / 1000))
|
|
707
906
|
throw ownershipFailure('conflict', 'transfer authorization expired');
|
|
708
907
|
const approved = participants(record).every((name) => record.approvals[name] === 'accept');
|
|
@@ -722,8 +921,11 @@ export class OwnershipWorkflow {
|
|
|
722
921
|
}
|
|
723
922
|
if (record.operation === 'transfer') {
|
|
724
923
|
const original = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
725
|
-
if (original.exists && (await this.deps.files.verifyFile(original.content, 'owner', record.sub_aid, record.owner_aid)).action === 'declare')
|
|
924
|
+
if (original.exists && (await this.deps.files.verifyFile(original.content, 'owner', record.sub_aid, record.owner_aid)).action === 'declare') {
|
|
925
|
+
if (record.authorization_sha256 !== undefined)
|
|
926
|
+
throw ownershipFailure('conflict', 'transfer authorization was withdrawn');
|
|
726
927
|
return;
|
|
928
|
+
}
|
|
727
929
|
await this.deps.files.requireTransfer(this.fileTask(handle));
|
|
728
930
|
}
|
|
729
931
|
if (this.local() === targetOwner(record)) {
|
|
@@ -753,16 +955,67 @@ export class OwnershipWorkflow {
|
|
|
753
955
|
const result = observed ?? await this.deps.files.checkConsensus(record.sub_aid, record.operation === 'release' ? undefined : { owner_aid: targetOwner(record), relation_version: record.target_relation_version });
|
|
754
956
|
if (result.status === 'invalid' || result.status === 'unavailable')
|
|
755
957
|
throw ownershipFailure(result.code ?? result.status, result.reason ?? 'ownership consensus cannot be verified', result.retryable);
|
|
756
|
-
|
|
958
|
+
if (record.operation === 'release' && record.status === 'completed' && result.status === 'unbound' && result.relation_version === 0)
|
|
959
|
+
return true;
|
|
960
|
+
let cleaned = false;
|
|
961
|
+
if (record.operation === 'release' && result.status === 'unbound' && result.relation_version === 0) {
|
|
962
|
+
const began = Date.now();
|
|
963
|
+
const ns = await this.readNs(record.sub_aid);
|
|
964
|
+
if (!ns.exists || !ns.etag)
|
|
965
|
+
throw ownershipFailure('conflict', 'current signed cleanup document is missing');
|
|
966
|
+
if (ns.etag !== result.agent_md_etag || ownershipSha256(ns.content) !== result.agent_md_sha256)
|
|
967
|
+
throw ownershipFailure('unavailable', 'agent.md changed during cleanup confirmation', true);
|
|
968
|
+
if (record.local_roles.includes('owner') && record.files.release) {
|
|
969
|
+
const source = record.files.release, owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
970
|
+
if (owner.exists && owner.content === source.raw && owner.sha256 === source.hash) {
|
|
971
|
+
await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
972
|
+
const after = await this.readNs(record.sub_aid);
|
|
973
|
+
if (after.etag !== ns.etag || after.content !== ns.content || after.relation_version !== 0 || after.lock !== 'unlocked')
|
|
974
|
+
throw ownershipFailure('unavailable', 'agent.md changed during release confirmation', true);
|
|
975
|
+
result.owner_md_sha256 = owner.sha256;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
const authorization = this.cleanupEvidence(record, result, ns.content);
|
|
979
|
+
if (!authorization)
|
|
980
|
+
throw ownershipFailure('conflict', 'no local cleanup original can confirm this release task');
|
|
981
|
+
if (Date.now() - began > 30_000)
|
|
982
|
+
throw ownershipFailure('unavailable', 'cleanup confirmation verification window elapsed', true);
|
|
983
|
+
if (authorization !== record.operation_id)
|
|
984
|
+
record.cleanup_authorization_id = authorization;
|
|
985
|
+
record.local_decision = 'accept';
|
|
986
|
+
record.approvals[this.local()] = 'accept';
|
|
987
|
+
record.cleanup_status = 'cleaned';
|
|
988
|
+
cleaned = true;
|
|
989
|
+
}
|
|
990
|
+
const reached = cleaned || result.relation_version === record.target_relation_version && (record.operation === 'release' ? result.status === 'released' && result.lock === 'unlocked' : result.status === 'consensus' && result.owner_aid === targetOwner(record));
|
|
757
991
|
if (!reached)
|
|
758
992
|
return false;
|
|
993
|
+
if (record.operation === 'release' && !cleaned) {
|
|
994
|
+
const ns = await this.readNs(record.sub_aid);
|
|
995
|
+
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
996
|
+
if (ns.etag !== result.agent_md_etag || ownershipSha256(ns.content) !== result.agent_md_sha256)
|
|
997
|
+
throw ownershipFailure('unavailable', 'agent.md changed during release confirmation', true);
|
|
998
|
+
if (statement?.owner_aid !== record.owner_aid || statement.relation_version !== record.base_relation_version)
|
|
999
|
+
return false;
|
|
1000
|
+
if (record.base_agent_md)
|
|
1001
|
+
this.assertAgentBaseMaterial(record, ns);
|
|
1002
|
+
if (record.files.release && result.owner_md_sha256 && record.files.release.hash !== result.owner_md_sha256)
|
|
1003
|
+
throw ownershipFailure('conflict', 'current release material does not belong to this task');
|
|
1004
|
+
}
|
|
759
1005
|
if (record.operation === 'release' && record.local_roles.includes('owner') && !record.files.release)
|
|
760
1006
|
throw ownershipFailure('conflict', 'local release file evidence is missing');
|
|
761
1007
|
if (record.operation === 'transfer' && record.local_roles.includes('owner') && !record.files.transfer)
|
|
762
1008
|
throw ownershipFailure('conflict', 'local transfer file evidence is missing');
|
|
763
1009
|
if (record.operation === 'release' && this.local() === record.sub_aid && record.local_decision !== 'accept') {
|
|
1010
|
+
const authorization = await this.cleanupAuthorization(record);
|
|
1011
|
+
if (authorization) {
|
|
1012
|
+
record.local_decision = 'accept';
|
|
1013
|
+
record.approvals[this.local()] = 'accept';
|
|
1014
|
+
record.cleanup_authorization_id = authorization.operation_id;
|
|
1015
|
+
await this.save(handle);
|
|
1016
|
+
}
|
|
764
1017
|
const ns = await this.deps.files.readAgent(record.sub_aid);
|
|
765
|
-
if (parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement) {
|
|
1018
|
+
if (!authorization && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement) {
|
|
766
1019
|
record.step = 'cleanup';
|
|
767
1020
|
if (record.cleanup_status !== 'action_required') {
|
|
768
1021
|
record.cleanup_status = 'action_required';
|
|
@@ -774,12 +1027,12 @@ export class OwnershipWorkflow {
|
|
|
774
1027
|
}
|
|
775
1028
|
if (record.operation !== 'release' && (record.local_decision !== 'accept' || this.local() === record.sub_aid && (!record.files.agent || record.files.agent.hash !== result.agent_md_sha256) || this.local() === targetOwner(record) && (!record.files.owner || record.files.owner.hash !== result.owner_md_sha256)))
|
|
776
1029
|
return false;
|
|
777
|
-
if (record.status !== 'completed' && record.operation !== 'release' &&
|
|
778
|
-
throw ownershipFailure('conflict', 'published
|
|
1030
|
+
if (record.status !== 'completed' && record.operation !== 'release' && this.local() === record.sub_aid && record.files.agent?.published_etag !== undefined && record.files.agent.published_etag !== result.agent_md_etag)
|
|
1031
|
+
throw ownershipFailure('conflict', 'published agent.md ETag changed');
|
|
779
1032
|
if (record.status !== 'completed') {
|
|
780
1033
|
record.status = 'completed';
|
|
781
1034
|
record.step = 'confirmed';
|
|
782
|
-
record.verified_relation_version =
|
|
1035
|
+
record.verified_relation_version = record.target_relation_version;
|
|
783
1036
|
record.completion_evidence = { ...result };
|
|
784
1037
|
delete record.code;
|
|
785
1038
|
delete record.reason;
|
|
@@ -794,15 +1047,58 @@ export class OwnershipWorkflow {
|
|
|
794
1047
|
}
|
|
795
1048
|
return true;
|
|
796
1049
|
}
|
|
1050
|
+
cleanupEvidence(record, evidence, raw) {
|
|
1051
|
+
const release = record.files.release;
|
|
1052
|
+
if (record.local_roles.includes('owner') && release && release.hash === evidence.owner_md_sha256)
|
|
1053
|
+
return record.operation_id;
|
|
1054
|
+
if (!record.local_roles.includes('sub'))
|
|
1055
|
+
return undefined;
|
|
1056
|
+
const matches = (saved) => Boolean(saved && saved.hash === evidence.agent_md_sha256 && saved.hash === ownershipSha256(saved.raw) && (saved.published_etag === undefined || saved.published_etag === evidence.agent_md_etag) && (raw === undefined || raw === saved.raw));
|
|
1057
|
+
if (matches(record.files.agent))
|
|
1058
|
+
return record.operation_id;
|
|
1059
|
+
for (const snapshot of this.list(true)) {
|
|
1060
|
+
if (!['bind', 'transfer'].includes(String(snapshot.task.operation)) || record.cleanup_authorization_id && snapshot.operation_id !== record.cleanup_authorization_id)
|
|
1061
|
+
continue;
|
|
1062
|
+
let candidate;
|
|
1063
|
+
try {
|
|
1064
|
+
candidate = this.decode(snapshot).record;
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
if (snapshot.task.sub_aid === record.sub_aid)
|
|
1068
|
+
throw error;
|
|
1069
|
+
else
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
if (candidate.local_decision !== 'accept' || !candidate.local_roles.includes('sub') || candidate.sub_aid !== record.sub_aid || targetOwner(candidate) !== record.owner_aid || candidate.target_relation_version !== record.base_relation_version || !matches(candidate.release_cleanup?.files.agent))
|
|
1073
|
+
continue;
|
|
1074
|
+
const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1075
|
+
if (statement?.owner_aid === record.owner_aid && statement.relation_version === record.base_relation_version && (!record.base_agent_md?.statement || Object.keys(record.base_agent_md.statement).every((field) => record.base_agent_md.statement[field] === statement[field])))
|
|
1076
|
+
return candidate.operation_id;
|
|
1077
|
+
}
|
|
1078
|
+
return undefined;
|
|
1079
|
+
}
|
|
797
1080
|
async resumeSubCleanup(handle, observed) {
|
|
798
1081
|
const record = handle.record;
|
|
799
|
-
if (!['bind', 'transfer'].includes(record.operation) || !record.local_roles.includes('sub') || record.local_decision !== 'accept' ||
|
|
1082
|
+
if (!['bind', 'transfer'].includes(record.operation) || !record.local_roles.includes('sub') || record.local_decision !== 'accept' || record.release_cleanup?.cleaned)
|
|
1083
|
+
return;
|
|
1084
|
+
if (record.release_cleanup && observed.status === 'unbound' && observed.relation_version === 0) {
|
|
1085
|
+
const saved = record.release_cleanup.files.agent;
|
|
1086
|
+
const ns = await this.readNs(record.sub_aid);
|
|
1087
|
+
if (!saved || saved.raw !== ns.content || saved.hash !== observed.agent_md_sha256 || saved.hash !== ownershipSha256(ns.content) || ns.etag !== observed.agent_md_etag || saved.published_etag !== undefined && saved.published_etag !== ns.etag)
|
|
1088
|
+
throw ownershipFailure('conflict', 'historical cleanup original does not match current material');
|
|
1089
|
+
saved.published_etag = ns.etag;
|
|
1090
|
+
record.release_cleanup.cleaned = true;
|
|
1091
|
+
await this.save(handle);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
if (observed.status !== 'released' || observed.relation_version !== record.target_relation_version)
|
|
800
1095
|
return;
|
|
801
1096
|
const ns = await this.readNs(record.sub_aid);
|
|
802
1097
|
if (!ns.exists || ns.lock !== 'unlocked' || ns.relation_version !== observed.relation_version || ns.etag !== observed.agent_md_etag || ownershipSha256(ns.content) !== observed.agent_md_sha256)
|
|
803
1098
|
throw ownershipFailure('unavailable', 'NS changed during historical cleanup observation', true);
|
|
804
1099
|
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
805
|
-
|
|
1100
|
+
const original = record.files.agent && parseOwnershipFile(record.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1101
|
+
if (!statement || !original || Object.keys(original).some((field) => original[field] !== statement[field]))
|
|
806
1102
|
throw ownershipFailure('conflict', 'historical cleanup fields belong to another relation');
|
|
807
1103
|
if (!record.release_cleanup) {
|
|
808
1104
|
record.release_cleanup = { files: {}, cleaned: false };
|
|
@@ -810,16 +1106,26 @@ export class OwnershipWorkflow {
|
|
|
810
1106
|
}
|
|
811
1107
|
await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: record.sub_aid, owner_aid: targetOwner(record),
|
|
812
1108
|
base_relation_version: record.target_relation_version, target_relation_version: observed.relation_version,
|
|
813
|
-
files: record.release_cleanup.files, persist: () => this.save(handle) });
|
|
1109
|
+
files: record.release_cleanup.files, expected_agent_statement: original, persist: () => this.save(handle) });
|
|
814
1110
|
record.release_cleanup.cleaned = true;
|
|
815
1111
|
await this.save(handle);
|
|
816
1112
|
this.emit(record, 'ownership.progress');
|
|
817
1113
|
}
|
|
818
|
-
cleanupAuthorization(task) {
|
|
819
|
-
|
|
1114
|
+
async cleanupAuthorization(task) {
|
|
1115
|
+
const ns = await this.readNs(task.sub_aid);
|
|
1116
|
+
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: task.sub_aid }).statement : undefined;
|
|
1117
|
+
if (!statement)
|
|
1118
|
+
return undefined;
|
|
1119
|
+
for (const snapshot of this.list(true)) {
|
|
1120
|
+
if (snapshot.task.sub_aid !== task.sub_aid)
|
|
1121
|
+
continue;
|
|
820
1122
|
const candidate = this.decode(snapshot).record;
|
|
821
|
-
if (['bind', 'transfer'].includes(candidate.operation) && 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)
|
|
822
|
-
|
|
1123
|
+
if (['bind', 'transfer'].includes(candidate.operation) && 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 && candidate.files.agent) {
|
|
1124
|
+
const original = parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
|
|
1125
|
+
const fixed = task.base_agent_md?.statement;
|
|
1126
|
+
if (original && Object.keys(original).every((field) => original[field] === statement[field]) && (!fixed || Object.keys(fixed).every((field) => fixed[field] === original[field])))
|
|
1127
|
+
return candidate;
|
|
1128
|
+
}
|
|
823
1129
|
}
|
|
824
1130
|
return undefined;
|
|
825
1131
|
}
|
|
@@ -829,12 +1135,14 @@ export class OwnershipWorkflow {
|
|
|
829
1135
|
return;
|
|
830
1136
|
if (record.operation === 'release' && this.local() === record.sub_aid && record.cleanup_status !== 'cleaned') {
|
|
831
1137
|
const ns = await this.readNs(record.sub_aid);
|
|
832
|
-
if (ns.exists && ns.lock === 'unlocked' && ns.relation_version ===
|
|
1138
|
+
if (ns.exists && ns.lock === 'unlocked' && ns.relation_version === 0 && !parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement) {
|
|
1139
|
+
if (!this.cleanupEvidence(record, { agent_md_sha256: ownershipSha256(ns.content), agent_md_etag: ns.etag, owner_md_sha256: '' }, ns.content))
|
|
1140
|
+
throw ownershipFailure('conflict', 'current cleanup does not match a locally saved original');
|
|
833
1141
|
record.cleanup_status = 'cleaned';
|
|
834
1142
|
await this.save(handle);
|
|
835
1143
|
}
|
|
836
1144
|
else {
|
|
837
|
-
const authorization = this.cleanupAuthorization(record);
|
|
1145
|
+
const authorization = await this.cleanupAuthorization(record);
|
|
838
1146
|
if (!authorization || record.cleanup_authorization_id && authorization.operation_id !== record.cleanup_authorization_id) {
|
|
839
1147
|
if (record.cleanup_status !== 'action_required') {
|
|
840
1148
|
record.cleanup_status = 'action_required';
|
|
@@ -845,7 +1153,9 @@ export class OwnershipWorkflow {
|
|
|
845
1153
|
else {
|
|
846
1154
|
record.cleanup_authorization_id = authorization.operation_id;
|
|
847
1155
|
await this.save(handle);
|
|
848
|
-
|
|
1156
|
+
const task = this.fileTask(handle);
|
|
1157
|
+
task.expected_agent_statement = parseOwnershipFile(authorization.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1158
|
+
await this.deps.files.revokeOwneraid(task);
|
|
849
1159
|
record.cleanup_status = 'cleaned';
|
|
850
1160
|
this.enqueue(record, record.owner_aid, 'sub.cleaned');
|
|
851
1161
|
await this.save(handle);
|