@agentunion/fastaun 0.5.21 → 0.5.22
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 +58 -1
- package/_packed_docs/CHANGELOG.md +58 -1
- package/_packed_docs/INDEX.md +465 -453
- package/_packed_docs/KITE_DOCS_GUIDE.md +168 -164
- package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +307 -67
- package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +41 -15
- package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +5 -5
- package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +669 -666
- package/_packed_docs/sdk/0.5.22-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +212 -0
- package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +199 -0
- package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +16 -0
- package/_packed_docs/sdk/09-group-rpc-manual.md +10 -0
- package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
- package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +3 -1
- package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +2 -0
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +138 -134
- package/_packed_docs/sdk/INDEX.md +255 -239
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +939 -930
- 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 +312 -363
- 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 +322 -553
- 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 +4 -4
- package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +484 -480
- 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 +190 -97
- 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 +158 -127
- package/dist/agent-md.d.ts +1 -1
- package/dist/agent-md.js +11 -4
- package/dist/agent-md.js.map +1 -1
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +18 -2
- package/dist/auth.js.map +1 -1
- package/dist/client/delivery.d.ts +28 -0
- package/dist/client/delivery.js +399 -44
- package/dist/client/delivery.js.map +1 -1
- package/dist/client/group-state.js +3 -0
- package/dist/client/group-state.js.map +1 -1
- package/dist/client/rpc-pipeline.js +28 -2
- package/dist/client/rpc-pipeline.js.map +1 -1
- package/dist/client/v2-e2ee.js +109 -39
- package/dist/client/v2-e2ee.js.map +1 -1
- package/dist/client.js +38 -10
- package/dist/client.js.map +1 -1
- package/dist/group-fs.d.ts +1 -0
- package/dist/group-fs.js +37 -19
- package/dist/group-fs.js.map +1 -1
- package/dist/org-protocol.d.ts +1 -0
- package/dist/org-protocol.js +54 -7
- package/dist/org-protocol.js.map +1 -1
- package/dist/ownership-v21.d.ts +21 -3
- package/dist/ownership-v21.js +135 -37
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +7 -2
- package/dist/ownership-workflow.js +173 -17
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/storage/lowlevel.d.ts +6 -0
- package/dist/storage/lowlevel.js +24 -7
- package/dist/storage/lowlevel.js.map +1 -1
- package/dist/storage/read-cache.d.ts +70 -0
- package/dist/storage/read-cache.js +625 -0
- package/dist/storage/read-cache.js.map +1 -0
- package/dist/storage/vfs.d.ts +2 -0
- package/dist/storage/vfs.js +50 -15
- package/dist/storage/vfs.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +21 -1
- package/dist/tools/cross-sdk-agent.js +301 -19
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/transport.js +6 -2
- 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
|
@@ -114,7 +114,7 @@ export function parseOwnershipMessage(content, context) {
|
|
|
114
114
|
const ns = context.ns;
|
|
115
115
|
if (ns.protocol !== '2.1')
|
|
116
116
|
throw ownershipFailure('unsupported_protocol', 'NS ownership protocol is unavailable');
|
|
117
|
-
const base = ns.relation_version === message.base_relation_version && (message.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === message.owner_aid);
|
|
117
|
+
const base = ns.relation_version === message.base_relation_version && (message.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === message.owner_aid) || message.operation === 'bind' && ns.relation_version === message.target_relation_version && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending');
|
|
118
118
|
const target = message.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === message.base_relation_version || ns.relation_version === 0) : ns.relation_version === message.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(message);
|
|
119
119
|
if (['relation.confirmed', 'sub.cleaned'].includes(message.action) ? !target : !base && !target)
|
|
120
120
|
throw ownershipFailure('conflict', 'ownership current NS relation does not match this task');
|
|
@@ -560,17 +560,38 @@ export class OwnershipWorkflow {
|
|
|
560
560
|
if (!base || typeof base.exists !== 'boolean' || typeof base.raw !== 'string' || base.sha256 !== ownershipSha256(base.raw) || typeof base.etag !== 'string' || (base.exists ? !base.etag : base.raw !== '' || base.etag !== '' || record.operation !== 'bind' || record.base_relation_version !== 0))
|
|
561
561
|
throw new Error('ownership original agent.md evidence is corrupt');
|
|
562
562
|
const statement = base.exists ? parseOwnershipFile(base.raw, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
563
|
-
if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version)
|
|
563
|
+
if (JSON.stringify(statement) !== JSON.stringify(base.statement) || (statement?.relation_version ?? 0) !== record.base_relation_version && !(base.pending && statement?.relation_version === record.target_relation_version && statement.owner_aid === record.owner_aid) || base.pending && record.operation !== 'bind')
|
|
564
564
|
throw new Error('ownership original statement is corrupt');
|
|
565
565
|
const owner = record.base_owner_md;
|
|
566
566
|
if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
|
|
567
567
|
throw new Error('ownership original owner.md evidence is corrupt');
|
|
568
568
|
if (owner.exists) {
|
|
569
569
|
const declaration = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: statement?.owner_aid ?? record.owner_aid });
|
|
570
|
-
if (declaration.statement?.relation_version !== record.base_relation_version)
|
|
570
|
+
if (!base.pending && declaration.statement?.relation_version !== record.base_relation_version)
|
|
571
571
|
throw new Error('ownership original owner statement is corrupt');
|
|
572
572
|
}
|
|
573
573
|
}
|
|
574
|
+
if (record.pending_agent_md) {
|
|
575
|
+
const pending = record.pending_agent_md;
|
|
576
|
+
const statement = parseOwnershipFile(pending.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
577
|
+
if (record.operation !== 'bind' || pending.sha256 !== ownershipSha256(pending.raw) || !pending.etag || statement?.owner_aid !== record.owner_aid || statement.relation_version !== record.target_relation_version)
|
|
578
|
+
throw new Error('ownership pending original is corrupt');
|
|
579
|
+
}
|
|
580
|
+
if (record.owner_proposal) {
|
|
581
|
+
const proposal = record.owner_proposal, file = parseOwnershipFile(proposal.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
|
|
582
|
+
if (!this.ownerFirst(record) || proposal.sha256 !== ownershipSha256(proposal.raw) || file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version)
|
|
583
|
+
throw new Error('ownership owner proposal is corrupt');
|
|
584
|
+
}
|
|
585
|
+
if (record.owner_first !== undefined && (record.owner_first !== true || record.operation !== 'bind' || record.initiator_aid !== record.owner_aid || record.sub_aid === record.owner_aid) || record.owner_first && record.request_verified && this.local() === record.sub_aid && !record.owner_proposal)
|
|
586
|
+
throw new Error('ownership owner-first context is corrupt');
|
|
587
|
+
if (record.rejection_cleanup) {
|
|
588
|
+
const cleanup = record.rejection_cleanup;
|
|
589
|
+
if (record.operation !== 'bind' || record.status !== 'rejected' || !record.local_roles.includes('sub') || !record.files.agent || typeof cleanup.cleaned !== 'boolean' || !cleanup.files || Object.keys(cleanup.files).some((key) => key !== 'agent'))
|
|
590
|
+
throw new Error('ownership rejection cleanup is corrupt');
|
|
591
|
+
const file = cleanup.files.agent;
|
|
592
|
+
if (file && (file.hash !== ownershipSha256(file.raw) || file.condition.if_match !== record.files.agent.published_etag || Object.keys(file.condition).length !== 1 || parseOwnershipFile(file.raw, 'agent', { expectedSubAid: record.sub_aid }).statement))
|
|
593
|
+
throw new Error('ownership rejection cleanup original is corrupt');
|
|
594
|
+
}
|
|
574
595
|
for (const item of record.inbox)
|
|
575
596
|
parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record });
|
|
576
597
|
for (const key of record.received) {
|
|
@@ -778,7 +799,8 @@ export class OwnershipWorkflow {
|
|
|
778
799
|
const base = ns.relation_version === record.base_relation_version && (record.operation === 'bind' ? ns.lock === 'unlocked' && !ns.owner_aid : ns.lock === 'locked' && ns.owner_aid === record.owner_aid);
|
|
779
800
|
const released = record.operation === 'release' && ns.lock === 'unlocked' && !ns.owner_aid && ns.relation_version === record.base_relation_version;
|
|
780
801
|
const committed = record.operation !== 'release' && ns.lock === 'locked' && ns.owner_aid === targetOwner(record) && ns.relation_version === record.target_relation_version;
|
|
781
|
-
|
|
802
|
+
const pending = record.operation === 'bind' && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.target_relation_version && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement?.owner_aid === record.owner_aid;
|
|
803
|
+
if (!base && !released && !committed && !pending)
|
|
782
804
|
throw ownershipFailure('conflict', 'legacy relationship no longer matches this task');
|
|
783
805
|
if (!committed)
|
|
784
806
|
await this.captureBase(record, ns);
|
|
@@ -879,6 +901,8 @@ export class OwnershipWorkflow {
|
|
|
879
901
|
};
|
|
880
902
|
if (context.operation === 'transfer')
|
|
881
903
|
record.new_owner_aid = context.new_owner_aid;
|
|
904
|
+
if (context.operation === 'bind' && initiator === context.owner_aid && context.owner_aid !== context.sub_aid)
|
|
905
|
+
record.owner_first = true;
|
|
882
906
|
record.approvals[this.local()] = decision;
|
|
883
907
|
return record;
|
|
884
908
|
}
|
|
@@ -927,7 +951,7 @@ export class OwnershipWorkflow {
|
|
|
927
951
|
await this.recordError(active, error);
|
|
928
952
|
return ownershipTaskResult(record);
|
|
929
953
|
}
|
|
930
|
-
let base =
|
|
954
|
+
let base = this.isBase(record, ns);
|
|
931
955
|
if (base && record.request_verified) {
|
|
932
956
|
try {
|
|
933
957
|
await this.assertBaseMaterial(active, ns);
|
|
@@ -958,11 +982,13 @@ export class OwnershipWorkflow {
|
|
|
958
982
|
ns = await this.readNs(subAid);
|
|
959
983
|
if (operation === 'bind' && ns.lock !== 'unlocked')
|
|
960
984
|
throw ownershipFailure('owner_locked', 'the current owner relation is locked');
|
|
985
|
+
if (operation === 'bind' && subAid !== this.local() && (ns.pending || ns.ownership_status === 'pending'))
|
|
986
|
+
throw ownershipFailure('conflict', 'pending ownership must resume its original operation');
|
|
961
987
|
if (!ns.exists && operation !== 'bind')
|
|
962
988
|
throw ownershipFailure('conflict', 'original agent.md is missing');
|
|
963
989
|
if (operation === 'bind' && (ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type : subAid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
964
990
|
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
965
|
-
if (operation === 'bind' && ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
|
|
991
|
+
if (operation === 'bind' && ns.exists && !(ns.pending || ns.ownership_status === 'pending') && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
|
|
966
992
|
throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
|
|
967
993
|
if (operation !== 'bind') {
|
|
968
994
|
const consensus = await this.deps.files.checkConsensus(subAid);
|
|
@@ -999,6 +1025,8 @@ export class OwnershipWorkflow {
|
|
|
999
1025
|
if (record.local_decision !== 'pending') {
|
|
1000
1026
|
if (record.local_decision !== decision)
|
|
1001
1027
|
throw ownershipFailure('conflict', 'the persisted local approval cannot be changed');
|
|
1028
|
+
if (decision === 'accept' && record.status === 'stopped' && record.operation === 'transfer' && this.local() === record.sub_aid && record.step === 'agent' && record.code === 'conflict' && record.reason === 'conflict: target owner declaration does not match this task' && !await this.recoverTargetDeclaration(handle))
|
|
1029
|
+
return ownershipTaskResult(record);
|
|
1002
1030
|
await this.drive(handle);
|
|
1003
1031
|
return ownershipTaskResult(record);
|
|
1004
1032
|
}
|
|
@@ -1007,12 +1035,13 @@ export class OwnershipWorkflow {
|
|
|
1007
1035
|
if (record.status !== 'pending')
|
|
1008
1036
|
throw ownershipFailure('conflict', 'ownership approval stage has ended');
|
|
1009
1037
|
const ns = await this.readNs(record.sub_aid);
|
|
1010
|
-
if (
|
|
1038
|
+
if (!this.isBase(record, ns))
|
|
1011
1039
|
throw ownershipFailure('conflict', 'the original relation changed before approval');
|
|
1012
1040
|
await this.assertBaseMaterial(handle, ns);
|
|
1013
1041
|
record.local_decision = decision;
|
|
1014
1042
|
record.approvals[this.local()] = decision;
|
|
1015
|
-
|
|
1043
|
+
if (decision !== 'accept' || !this.ownerFirst(record))
|
|
1044
|
+
this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
|
|
1016
1045
|
if (decision === 'reject') {
|
|
1017
1046
|
record.status = 'rejected';
|
|
1018
1047
|
record.step = 'approval';
|
|
@@ -1026,6 +1055,42 @@ export class OwnershipWorkflow {
|
|
|
1026
1055
|
return ownershipTaskResult(record);
|
|
1027
1056
|
});
|
|
1028
1057
|
}
|
|
1058
|
+
async recoverTargetDeclaration(handle) {
|
|
1059
|
+
const record = handle.record, local = this.local(), base = record.base_agent_md;
|
|
1060
|
+
if (!base || record.new_owner_aid === local || record.files.agent || record.files.owner || record.owner_proposal || record.legacy_task || record.legacy_recovery || record.approvals[local] !== 'accept' || record.authorization_sha256 === undefined || record.expires_at === undefined)
|
|
1061
|
+
return false;
|
|
1062
|
+
const began = Date.now();
|
|
1063
|
+
const sameBase = (ns) => this.isBase(record, ns) && ns.exists === base.exists && ns.content === base.raw && ownershipSha256(ns.content) === base.sha256 && ns.etag === base.etag;
|
|
1064
|
+
try {
|
|
1065
|
+
if (!sameBase(await this.readNs(record.sub_aid)))
|
|
1066
|
+
return false;
|
|
1067
|
+
await this.deps.files.requireTransfer(this.fileTask(handle));
|
|
1068
|
+
const target = await this.deps.files.readOwner(targetOwner(record), record.sub_aid);
|
|
1069
|
+
if (!target.exists)
|
|
1070
|
+
return false;
|
|
1071
|
+
const declaration = await this.deps.files.verifyFile(target.content, 'owner', record.sub_aid, targetOwner(record));
|
|
1072
|
+
if (declaration.action !== 'declare' || declaration.statement.relation_version !== record.target_relation_version)
|
|
1073
|
+
return false;
|
|
1074
|
+
if (!sameBase(await this.readNs(record.sub_aid)))
|
|
1075
|
+
return false;
|
|
1076
|
+
await this.deps.files.requireTransfer(this.fileTask(handle));
|
|
1077
|
+
const after = await this.deps.files.readOwner(targetOwner(record), record.sub_aid);
|
|
1078
|
+
if (!after.exists || after.content !== target.content || after.sha256 !== target.sha256 || after.version !== target.version || after.revision !== target.revision || Date.now() - began > 30_000 || this.local() !== local)
|
|
1079
|
+
return false;
|
|
1080
|
+
this.assertActive(handle);
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
1084
|
+
throw error;
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
record.status = 'pending';
|
|
1088
|
+
record.retryable = true;
|
|
1089
|
+
delete record.code;
|
|
1090
|
+
delete record.reason;
|
|
1091
|
+
await this.save(handle);
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1029
1094
|
enqueue(record, to, action, reason) {
|
|
1030
1095
|
if (to === this.local())
|
|
1031
1096
|
return;
|
|
@@ -1038,11 +1103,13 @@ export class OwnershipWorkflow {
|
|
|
1038
1103
|
payload.new_owner_aid = record.new_owner_aid;
|
|
1039
1104
|
if (reason !== undefined)
|
|
1040
1105
|
payload.reason = reason;
|
|
1041
|
-
record.outbox.push({ to, payload });
|
|
1106
|
+
record.outbox.push({ to, payload, message_id: `m-${randomUUID().replace(/-/g, '')}` });
|
|
1042
1107
|
record.notices.push(key);
|
|
1043
1108
|
}
|
|
1044
1109
|
validateOutgoing(record, item) {
|
|
1045
1110
|
try {
|
|
1111
|
+
if (item.message_id !== undefined && (typeof item.message_id !== 'string' || !/^m-[0-9a-f]{32}$/.test(item.message_id)))
|
|
1112
|
+
throw new Error('ownership outgoing message_id is corrupt');
|
|
1046
1113
|
const message = parseOwnershipMessage(item.payload, { authenticated_from_aid: this.local(), local_aid: item.to, task: { ...record, local_decision: 'accept' } });
|
|
1047
1114
|
const rejected = ['bind.reject', 'transfer.reject'].includes(message.action);
|
|
1048
1115
|
if (message.action !== 'operation.stopped' && record.local_decision !== (rejected ? 'reject' : 'accept') || record.approvals[this.local()] !== record.local_decision)
|
|
@@ -1067,10 +1134,10 @@ export class OwnershipWorkflow {
|
|
|
1067
1134
|
continue;
|
|
1068
1135
|
}
|
|
1069
1136
|
const ns = await this.readNs(record.sub_aid);
|
|
1070
|
-
const base =
|
|
1137
|
+
const base = this.isBase(record, ns);
|
|
1071
1138
|
const target = record.operation === 'release' ? ns.lock === 'unlocked' && !ns.owner_aid && (ns.relation_version === record.base_relation_version || ns.relation_version === 0) : ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record);
|
|
1072
1139
|
const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
|
|
1073
|
-
if (requiresTarget ? !target : !base) {
|
|
1140
|
+
if (requiresTarget ? !target : !base && !(record.operation === 'bind' && target && ['owner.ready', 'bind.accept'].includes(message.payload.action))) {
|
|
1074
1141
|
record.outbox.shift();
|
|
1075
1142
|
await this.save(handle);
|
|
1076
1143
|
continue;
|
|
@@ -1084,8 +1151,12 @@ export class OwnershipWorkflow {
|
|
|
1084
1151
|
if (expiresAt <= Math.trunc(Date.now() / 1000))
|
|
1085
1152
|
throw ownershipFailure('conflict', 'transfer authorization expired');
|
|
1086
1153
|
}
|
|
1154
|
+
if (!message.message_id) {
|
|
1155
|
+
message.message_id = `m-${randomUUID().replace(/-/g, '')}`;
|
|
1156
|
+
await this.save(handle);
|
|
1157
|
+
}
|
|
1087
1158
|
this.assertActive(handle);
|
|
1088
|
-
await this.deps.send(message.to, message.payload);
|
|
1159
|
+
await this.deps.send(message.to, message.payload, message.message_id);
|
|
1089
1160
|
handle.record.outbox.shift();
|
|
1090
1161
|
await this.save(handle);
|
|
1091
1162
|
}
|
|
@@ -1098,11 +1169,35 @@ export class OwnershipWorkflow {
|
|
|
1098
1169
|
const currentOwner = ns.lock === 'locked' ? file?.statement?.owner_aid : null;
|
|
1099
1170
|
if (ns.lock === 'locked' && (!file?.statement || file.statement.relation_version !== ns.relation_version) || ns.owner_aid !== undefined && ns.owner_aid !== currentOwner)
|
|
1100
1171
|
throw ownershipFailure('unavailable', 'NS owner metadata and signed agent.md do not agree', true);
|
|
1101
|
-
|
|
1172
|
+
if (!file?.statement)
|
|
1173
|
+
return { ...ns, owner_aid: currentOwner };
|
|
1174
|
+
const consensus = await this.deps.files.checkConsensus(subAid);
|
|
1175
|
+
if (consensus.status === 'invalid' || consensus.status === 'unavailable' || consensus.status === 'not_applicable')
|
|
1176
|
+
throw ownershipFailure(consensus.code ?? consensus.status, consensus.reason ?? 'ownership materials cannot be verified', consensus.retryable);
|
|
1177
|
+
if (consensus.relation_version !== ns.relation_version || consensus.agent_md_etag !== ns.etag || consensus.agent_md_sha256 !== ownershipSha256(ns.content))
|
|
1178
|
+
throw ownershipFailure('unavailable', 'agent.md changed during ownership verification', true);
|
|
1179
|
+
return { ...ns, lock: consensus.lock, owner_aid: consensus.owner_aid };
|
|
1180
|
+
}
|
|
1181
|
+
isPending(record, ns) {
|
|
1182
|
+
if (record.operation !== 'bind' || ns.lock !== 'unlocked' || !(ns.pending || ns.ownership_status === 'pending') || ns.relation_version !== record.target_relation_version)
|
|
1183
|
+
return false;
|
|
1184
|
+
const saved = record.files.agent;
|
|
1185
|
+
const candidate = saved ? { raw: saved.raw, sha256: saved.hash, etag: saved.published_etag } : record.pending_agent_md ?? (record.base_agent_md?.pending ? record.base_agent_md : undefined);
|
|
1186
|
+
return Boolean(candidate && ns.content === candidate.raw && ownershipSha256(ns.content) === candidate.sha256 && (!candidate.etag || ns.etag === candidate.etag));
|
|
1187
|
+
}
|
|
1188
|
+
isPendingBase(record, ns) {
|
|
1189
|
+
const base = record.base_agent_md;
|
|
1190
|
+
return record.operation === 'bind' && Boolean(base?.pending) && ns.lock === 'unlocked' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.base_relation_version && ns.content === base.raw && ns.etag === base.etag && ownershipSha256(ns.content) === base.sha256;
|
|
1191
|
+
}
|
|
1192
|
+
isBase(record, ns) {
|
|
1193
|
+
return this.isPending(record, ns) || this.isPendingBase(record, ns) || 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);
|
|
1102
1194
|
}
|
|
1103
1195
|
async captureBase(record, ns) {
|
|
1104
1196
|
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
1105
|
-
record.
|
|
1197
|
+
const pending = record.operation === 'bind' && (ns.pending || ns.ownership_status === 'pending') && ns.relation_version === record.target_relation_version && statement?.owner_aid === record.owner_aid;
|
|
1198
|
+
record.base_agent_md = { exists: ns.exists, raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag, statement, ...(record.operation === 'bind' && (ns.pending || ns.ownership_status === 'pending') ? { pending: true } : {}) };
|
|
1199
|
+
if (pending)
|
|
1200
|
+
record.pending_agent_md = { raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag };
|
|
1106
1201
|
if (!statement) {
|
|
1107
1202
|
record.base_owner_md = { exists: false, raw: '', sha256: '' };
|
|
1108
1203
|
return;
|
|
@@ -1117,6 +1212,12 @@ export class OwnershipWorkflow {
|
|
|
1117
1212
|
async assertBaseMaterial(handle, ns) {
|
|
1118
1213
|
const record = handle.record;
|
|
1119
1214
|
this.assertAgentBaseMaterial(record, ns);
|
|
1215
|
+
if (record.owner_proposal) {
|
|
1216
|
+
const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1217
|
+
if (!current.exists || current.content !== record.owner_proposal.raw || current.sha256 !== record.owner_proposal.sha256)
|
|
1218
|
+
throw ownershipFailure('conflict', 'owner binding proposal changed');
|
|
1219
|
+
await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
|
|
1220
|
+
}
|
|
1120
1221
|
const original = record.base_owner_md;
|
|
1121
1222
|
if (!original)
|
|
1122
1223
|
throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is missing');
|
|
@@ -1148,9 +1249,13 @@ export class OwnershipWorkflow {
|
|
|
1148
1249
|
throw ownershipFailure('conflict', 'the original owner statement was replaced');
|
|
1149
1250
|
}
|
|
1150
1251
|
assertAgentBaseMaterial(record, ns) {
|
|
1252
|
+
if (this.isPending(record, ns))
|
|
1253
|
+
return;
|
|
1151
1254
|
const base = record.base_agent_md;
|
|
1152
1255
|
if (!base || typeof base.raw !== 'string')
|
|
1153
1256
|
throw ownershipFailure('persistence_unavailable', 'original agent.md evidence is missing');
|
|
1257
|
+
if (base.pending && (ns.content !== base.raw || ns.etag !== base.etag))
|
|
1258
|
+
throw ownershipFailure('conflict', 'the pending ownership original was replaced');
|
|
1154
1259
|
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
1155
1260
|
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;
|
|
1156
1261
|
if (ns.exists !== base.exists || !same)
|
|
@@ -1161,6 +1266,10 @@ export class OwnershipWorkflow {
|
|
|
1161
1266
|
while (record.inbox.length) {
|
|
1162
1267
|
const item = record.inbox[0];
|
|
1163
1268
|
const ns = await this.readNs(record.sub_aid);
|
|
1269
|
+
if (record.operation === 'bind' && record.request_verified && !record.pending_agent_md && item.payload.action === 'bind.accept' && item.from === record.sub_aid && (ns.pending || ns.ownership_status === 'pending') && ns.lock === 'unlocked' && ns.relation_version === record.target_relation_version && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement?.owner_aid === record.owner_aid) {
|
|
1270
|
+
record.pending_agent_md = { raw: ns.content, sha256: ownershipSha256(ns.content), etag: ns.etag };
|
|
1271
|
+
await this.save(handle);
|
|
1272
|
+
}
|
|
1164
1273
|
if (record.request_verified && ns.relation_version === record.base_relation_version && !['relation.confirmed', 'sub.cleaned'].includes(item.payload.action))
|
|
1165
1274
|
await this.assertBaseMaterial(handle, ns);
|
|
1166
1275
|
try {
|
|
@@ -1191,6 +1300,17 @@ export class OwnershipWorkflow {
|
|
|
1191
1300
|
record.legacy_recovery.owner_confirmed = true;
|
|
1192
1301
|
}
|
|
1193
1302
|
if (['bind.request', 'transfer.request'].includes(action)) {
|
|
1303
|
+
if (this.ownerFirst(record)) {
|
|
1304
|
+
if (ns.lock !== 'unlocked' || ns.pending || ns.ownership_status === 'pending')
|
|
1305
|
+
throw ownershipFailure('conflict', 'owner binding requires the original unlocked relation');
|
|
1306
|
+
const proposal = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1307
|
+
if (!proposal.exists)
|
|
1308
|
+
throw ownershipFailure('unavailable', 'owner binding proposal is missing', true);
|
|
1309
|
+
const file = await this.deps.files.verifyFile(proposal.content, 'owner', record.sub_aid, record.owner_aid);
|
|
1310
|
+
if (file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version || record.owner_proposal && (record.owner_proposal.raw !== proposal.content || record.owner_proposal.sha256 !== proposal.sha256))
|
|
1311
|
+
throw ownershipFailure('conflict', 'owner binding proposal does not match this operation');
|
|
1312
|
+
record.owner_proposal = { raw: proposal.content, sha256: proposal.sha256 };
|
|
1313
|
+
}
|
|
1194
1314
|
if (!record.request_verified)
|
|
1195
1315
|
await this.captureBase(record, ns);
|
|
1196
1316
|
record.request_verified = true;
|
|
@@ -1309,8 +1429,9 @@ export class OwnershipWorkflow {
|
|
|
1309
1429
|
fileTask(handle) {
|
|
1310
1430
|
const record = handle.record;
|
|
1311
1431
|
const task = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid,
|
|
1432
|
+
stage: record.operation === 'bind' && record.initiator_aid === record.sub_aid && !record.legacy_recovery && !record.legacy_task,
|
|
1312
1433
|
new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
|
|
1313
|
-
files: record.files, authorization_sha256: record.authorization_sha256, expires_at: record.expires_at, base_agent_md: record.base_agent_md, base_owner_md: record.base_owner_md,
|
|
1434
|
+
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, pending_agent_md: record.pending_agent_md, owner_proposal: record.owner_proposal,
|
|
1314
1435
|
assertActive: () => this.assertActive(handle),
|
|
1315
1436
|
persist: async () => {
|
|
1316
1437
|
record.authorization_sha256 = task.authorization_sha256;
|
|
@@ -1326,6 +1447,9 @@ export class OwnershipWorkflow {
|
|
|
1326
1447
|
if (!current || this.isReset(current.record) || current.revision !== handle.revision)
|
|
1327
1448
|
throw ownershipFailure('conflict', 'ownership task changed before external action');
|
|
1328
1449
|
}
|
|
1450
|
+
ownerFirst(record) {
|
|
1451
|
+
return record.operation === 'bind' && record.initiator_aid === record.owner_aid && record.owner_aid !== record.sub_aid && Boolean(record.owner_first || record.owner_proposal) && !record.legacy_recovery && !record.legacy_task;
|
|
1452
|
+
}
|
|
1329
1453
|
async drive(handle) {
|
|
1330
1454
|
if (this.isReset(handle.record))
|
|
1331
1455
|
return;
|
|
@@ -1339,7 +1463,34 @@ export class OwnershipWorkflow {
|
|
|
1339
1463
|
}
|
|
1340
1464
|
try {
|
|
1341
1465
|
await this.processInbox(handle);
|
|
1466
|
+
if (this.ownerFirst(record) && record.status === 'pending' && record.request_verified && this.local() === record.owner_aid) {
|
|
1467
|
+
const ns = await this.readNs(record.sub_aid);
|
|
1468
|
+
if (this.isBase(record, ns)) {
|
|
1469
|
+
await this.assertBaseMaterial(handle, ns);
|
|
1470
|
+
record.step = 'owner';
|
|
1471
|
+
await this.save(handle);
|
|
1472
|
+
await this.deps.files.declareSubaid(this.fileTask(handle));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
if (record.operation === 'bind' && (record.initiator_aid === record.sub_aid || this.ownerFirst(record)) && record.status === 'pending' && record.request_verified && record.local_decision === 'accept' && this.local() === record.sub_aid && !record.legacy_recovery && !record.legacy_task) {
|
|
1476
|
+
record.step = 'agent';
|
|
1477
|
+
await this.save(handle);
|
|
1478
|
+
await this.deps.files.declareOwneraid(this.fileTask(handle));
|
|
1479
|
+
}
|
|
1342
1480
|
await this.flush(handle);
|
|
1481
|
+
if (this.ownerFirst(record) && record.status === 'rejected' && this.local() === record.owner_aid && record.files.owner)
|
|
1482
|
+
await this.deps.files.cancelOwnerProposal(this.fileTask(handle));
|
|
1483
|
+
if (record.operation === 'bind' && record.status === 'rejected' && this.local() === record.sub_aid && record.files.agent && !record.rejection_cleanup?.cleaned) {
|
|
1484
|
+
if (!record.rejection_cleanup) {
|
|
1485
|
+
record.rejection_cleanup = { files: {}, cleaned: false };
|
|
1486
|
+
await this.save(handle);
|
|
1487
|
+
}
|
|
1488
|
+
const task = this.fileTask(handle);
|
|
1489
|
+
task.files = record.rejection_cleanup.files;
|
|
1490
|
+
await this.deps.files.cancelPendingBind(task, record.files.agent);
|
|
1491
|
+
record.rejection_cleanup.cleaned = true;
|
|
1492
|
+
await this.save(handle);
|
|
1493
|
+
}
|
|
1343
1494
|
if (record.status === 'rejected' || record.status === 'stopped' || record.status === 'error')
|
|
1344
1495
|
return;
|
|
1345
1496
|
if (!record.request_verified)
|
|
@@ -1376,7 +1527,7 @@ export class OwnershipWorkflow {
|
|
|
1376
1527
|
return;
|
|
1377
1528
|
}
|
|
1378
1529
|
const ns = await this.readNs(record.sub_aid);
|
|
1379
|
-
if (
|
|
1530
|
+
if (!this.isBase(record, ns)) {
|
|
1380
1531
|
throw ownershipFailure('conflict', 'the original relation was replaced');
|
|
1381
1532
|
}
|
|
1382
1533
|
await this.assertBaseMaterial(handle, ns);
|
|
@@ -1412,7 +1563,8 @@ export class OwnershipWorkflow {
|
|
|
1412
1563
|
record.step = 'owner';
|
|
1413
1564
|
await this.save(handle);
|
|
1414
1565
|
await this.deps.files.declareSubaid(this.fileTask(handle));
|
|
1415
|
-
this.
|
|
1566
|
+
if (!this.ownerFirst(record))
|
|
1567
|
+
this.enqueue(record, record.sub_aid, 'owner.ready');
|
|
1416
1568
|
await this.save(handle);
|
|
1417
1569
|
await this.flush(handle);
|
|
1418
1570
|
}
|
|
@@ -1512,6 +1664,10 @@ export class OwnershipWorkflow {
|
|
|
1512
1664
|
}
|
|
1513
1665
|
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)))
|
|
1514
1666
|
return false;
|
|
1667
|
+
if (record.owner_proposal && record.owner_proposal.sha256 !== result.owner_md_sha256)
|
|
1668
|
+
throw ownershipFailure('conflict', 'owner binding proposal changed before acceptance');
|
|
1669
|
+
if (record.operation === 'bind' && record.pending_agent_md && (record.pending_agent_md.sha256 !== result.agent_md_sha256 || record.pending_agent_md.etag !== result.agent_md_etag))
|
|
1670
|
+
throw ownershipFailure('conflict', 'pending ownership original changed before acceptance');
|
|
1515
1671
|
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)
|
|
1516
1672
|
throw ownershipFailure('conflict', 'published agent.md ETag changed');
|
|
1517
1673
|
if (record.status !== 'completed') {
|