@agentunion/fastaun 0.5.23 → 0.5.24
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 +33 -0
- package/_packed_docs/CHANGELOG.md +33 -0
- package/_packed_docs/INDEX.md +27 -15
- package/_packed_docs/KITE_DOCS_GUIDE.md +22 -19
- package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +2 -2
- package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +13 -7
- package/_packed_docs/sdk/INDEX.md +21 -6
- package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +49 -2
- 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 +123 -72
- 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 +173 -96
- package/dist/agent-md.d.ts +1 -1
- package/dist/agent-md.js +14 -12
- package/dist/agent-md.js.map +1 -1
- package/dist/aid-store.d.ts +1 -0
- package/dist/aid-store.js +34 -12
- package/dist/aid-store.js.map +1 -1
- package/dist/client.d.ts +9 -10
- package/dist/client.js +115 -94
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/keystore/aid-db.d.ts +1 -8
- package/dist/keystore/aid-db.js +46 -204
- package/dist/keystore/aid-db.js.map +1 -1
- package/dist/keystore/index.d.ts +0 -15
- package/dist/keystore/local-token-store.d.ts +1 -6
- package/dist/keystore/local-token-store.js +0 -15
- package/dist/keystore/local-token-store.js.map +1 -1
- package/dist/ownership-v21.d.ts +2 -2
- package/dist/ownership-v21.js +25 -12
- package/dist/ownership-v21.js.map +1 -1
- package/dist/ownership-workflow.d.ts +30 -59
- package/dist/ownership-workflow.js +260 -1667
- package/dist/ownership-workflow.js.map +1 -1
- package/dist/tools/cross-sdk-agent.d.ts +1 -2
- package/dist/tools/cross-sdk-agent.js +7 -14
- package/dist/tools/cross-sdk-agent.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -3,13 +3,6 @@ import { isScalar, parseDocument } from 'yaml';
|
|
|
3
3
|
import { StateError, ValidationError } from './errors.js';
|
|
4
4
|
import { OWNERSHIP_UUID_V4_RE, ownershipAid, ownershipFailure, ownershipInteger, ownershipSha256, parseOwnershipFile, requireOwnershipApplicable } from './ownership-v21.js';
|
|
5
5
|
export const OWNERSHIP_SDK_MESSAGE_TYPE = 'aun.ownership.sdk.v1';
|
|
6
|
-
class OwnershipTaskDecodeError extends StateError {
|
|
7
|
-
operationId;
|
|
8
|
-
constructor(operationId, reason) {
|
|
9
|
-
super(reason, { stringCode: 'persistence_unavailable' });
|
|
10
|
-
this.operationId = operationId;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
6
|
function participants(task) {
|
|
14
7
|
return [...new Set([task.owner_aid, task.sub_aid, ...(task.new_owner_aid ? [task.new_owner_aid] : [])])];
|
|
15
8
|
}
|
|
@@ -108,8 +101,6 @@ export function parseOwnershipMessage(content, context) {
|
|
|
108
101
|
}
|
|
109
102
|
if (!direction)
|
|
110
103
|
throw ownershipFailure('forbidden', 'ownership action has an invalid authenticated direction');
|
|
111
|
-
if (['owner.ready', 'transfer.ready'].includes(message.action) && task?.local_decision !== 'accept')
|
|
112
|
-
throw ownershipFailure('forbidden', 'ownership local approval is required');
|
|
113
104
|
if (context.ns) {
|
|
114
105
|
const ns = context.ns;
|
|
115
106
|
if (ns.protocol !== '2.1')
|
|
@@ -121,1703 +112,305 @@ export function parseOwnershipMessage(content, context) {
|
|
|
121
112
|
}
|
|
122
113
|
return message;
|
|
123
114
|
}
|
|
124
|
-
function
|
|
125
|
-
const roles = [];
|
|
126
|
-
if (task.owner_aid === local)
|
|
127
|
-
roles.push('owner');
|
|
128
|
-
if (task.sub_aid === local)
|
|
129
|
-
roles.push('sub');
|
|
130
|
-
if (task.new_owner_aid === local)
|
|
131
|
-
roles.push('new_owner');
|
|
132
|
-
return roles;
|
|
133
|
-
}
|
|
134
|
-
export function ownershipTaskResult(task) {
|
|
135
|
-
const result = {
|
|
136
|
-
operation_id: task.operation_id, operation: task.operation, sub_aid: task.sub_aid, owner_aid: task.owner_aid,
|
|
137
|
-
base_relation_version: task.base_relation_version, target_relation_version: task.target_relation_version, status: task.status, step: task.step,
|
|
138
|
-
};
|
|
139
|
-
if (task.operation === 'transfer')
|
|
140
|
-
result.new_owner_aid = task.new_owner_aid;
|
|
141
|
-
for (const field of ['code', 'reason', 'retryable'])
|
|
142
|
-
if (task[field] !== undefined)
|
|
143
|
-
Object.assign(result, { [field]: task[field] });
|
|
144
|
-
return result;
|
|
145
|
-
}
|
|
115
|
+
export function ownershipTaskResult(task) { return { ...task }; }
|
|
146
116
|
export class OwnershipWorkflow {
|
|
147
117
|
deps;
|
|
148
|
-
|
|
118
|
+
requests = new Map();
|
|
149
119
|
constructor(deps) {
|
|
150
120
|
this.deps = deps;
|
|
151
121
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
return this.start('release', ownershipAid(subAid, 'sub_aid'), this.local());
|
|
160
|
-
}
|
|
161
|
-
async transferSubaid(subAid, newOwnerAid) {
|
|
162
|
-
return this.start('transfer', ownershipAid(subAid, 'sub_aid'), this.local(), ownershipAid(newOwnerAid, 'new_owner_aid'));
|
|
163
|
-
}
|
|
164
|
-
async resumeRegistration(ownerAid, operationId) {
|
|
165
|
-
if (typeof operationId !== 'string' || !OWNERSHIP_UUID_V4_RE.test(operationId))
|
|
166
|
-
throw ownershipFailure('invalid', 'registration operation_id is invalid');
|
|
167
|
-
return this.start('bind', this.local(), ownershipAid(ownerAid, 'owner_aid'), undefined, operationId);
|
|
122
|
+
local() { this.deps.assertCurrent?.(); return ownershipAid(this.deps.currentAid(), 'local_aid'); }
|
|
123
|
+
fileTask(record, stage = false) {
|
|
124
|
+
const task = record;
|
|
125
|
+
task.assertActive = () => this.deps.assertCurrent?.();
|
|
126
|
+
task.persist = async () => { this.deps.assertCurrent?.(); };
|
|
127
|
+
task.stage = stage;
|
|
128
|
+
return task;
|
|
168
129
|
}
|
|
169
|
-
|
|
170
|
-
|
|
130
|
+
result(record, status, error) {
|
|
131
|
+
const result = { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid, ...(record.new_owner_aid ? { new_owner_aid: record.new_owner_aid } : {}), base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version, status, step: 'local_actions' };
|
|
132
|
+
if (error)
|
|
133
|
+
Object.assign(result, { code: error instanceof StateError ? error.stringCode : 'unavailable', reason: error instanceof Error ? error.message : String(error), retryable: true });
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
context(record) {
|
|
137
|
+
return { operation_id: record.operation_id, operation: record.operation, sub_aid: record.sub_aid, owner_aid: record.owner_aid, ...(record.new_owner_aid ? { new_owner_aid: record.new_owner_aid } : {}), initiator_aid: record.initiator_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version };
|
|
138
|
+
}
|
|
139
|
+
notify(record, to, action, reason) {
|
|
140
|
+
const { initiator_aid: _initiator, ...context } = this.context(record);
|
|
141
|
+
record.notifications.push({ to, message_id: randomUUID(), payload: { type: OWNERSHIP_SDK_MESSAGE_TYPE, ...context, action, ...(reason ? { reason } : {}) } });
|
|
142
|
+
}
|
|
143
|
+
async capture(record) {
|
|
144
|
+
const agent = await this.deps.files.readAgent(record.sub_aid);
|
|
145
|
+
const parsed = agent.exists ? await this.deps.files.verifyFile(agent.content, 'agent', record.sub_aid) : undefined;
|
|
146
|
+
record.base_agent_md = { exists: agent.exists, raw: agent.content, sha256: ownershipSha256(agent.content), etag: agent.etag, statement: parsed?.statement ?? null, ...(agent.pending || agent.ownership_status === 'pending' ? { pending: true } : {}) };
|
|
147
|
+
if ((agent.pending || agent.ownership_status === 'pending') && parsed?.statement?.owner_aid === targetOwner(record) && parsed.statement.relation_version === record.target_relation_version)
|
|
148
|
+
record.pending_agent_md = { raw: agent.content, sha256: ownershipSha256(agent.content), etag: agent.etag };
|
|
149
|
+
const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
150
|
+
if (owner.exists)
|
|
151
|
+
await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
152
|
+
record.base_owner_md = { exists: owner.exists, raw: owner.content, sha256: owner.exists ? ownershipSha256(owner.content) : '' };
|
|
153
|
+
if (record.operation === 'bind' && owner.exists) {
|
|
154
|
+
const declaration = parseOwnershipFile(owner.content, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
|
|
155
|
+
if (declaration.action === 'declare' && declaration.statement?.owner_aid === record.owner_aid && declaration.statement.relation_version === record.target_relation_version)
|
|
156
|
+
record.owner_proposal = { raw: owner.content, sha256: ownershipSha256(owner.content) };
|
|
157
|
+
}
|
|
171
158
|
}
|
|
172
|
-
async
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
async hasLocalDeclaration(record, role) {
|
|
160
|
+
const current = role === 'agent' ? await this.deps.files.readAgent(record.sub_aid, record.operation === 'transfer') : await this.deps.files.readOwner(targetOwner(record), record.sub_aid);
|
|
161
|
+
if (!current.exists)
|
|
162
|
+
return false;
|
|
163
|
+
const saved = record.files[role];
|
|
164
|
+
if (saved && (current.content !== saved.raw || ownershipSha256(current.content) !== saved.hash))
|
|
165
|
+
return false;
|
|
166
|
+
const file = await this.deps.files.verifyFile(current.content, role, record.sub_aid, role === 'owner' ? targetOwner(record) : undefined), statement = file.statement;
|
|
167
|
+
return (role === 'agent' || file.action === 'declare') && statement?.sub_aid === record.sub_aid && statement.owner_aid === targetOwner(record) && statement.relation_version === record.target_relation_version;
|
|
176
168
|
}
|
|
177
|
-
async
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
let source = this.taskSource(snapshot.operation_id);
|
|
190
|
-
for (let attempt = 0; source; attempt++) {
|
|
191
|
-
let task;
|
|
192
|
-
try {
|
|
193
|
-
const parsed = JSON.parse(source.source);
|
|
194
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
195
|
-
task = parsed;
|
|
196
|
-
}
|
|
197
|
-
catch { }
|
|
198
|
-
if (task && Object.hasOwn(task, 'task_reset')) {
|
|
199
|
-
try {
|
|
200
|
-
this.decode({ operation_id: source.operation_id, revision: source.revision, task });
|
|
169
|
+
async execute(record) {
|
|
170
|
+
const local = this.local();
|
|
171
|
+
try {
|
|
172
|
+
if (record.command === 'cleanup')
|
|
173
|
+
await this.deps.files.cleanupOwnerFile(this.fileTask(record));
|
|
174
|
+
else if (record.command !== 'reject') {
|
|
175
|
+
const task = this.fileTask(record, local === record.sub_aid && record.command === 'request' && record.operation === 'bind' || local === record.sub_aid && record.command === 'accept' && record.operation === 'transfer');
|
|
176
|
+
if (record.operation === 'bind') {
|
|
177
|
+
if (local === record.owner_aid && !await this.hasLocalDeclaration(record, 'owner')) {
|
|
178
|
+
if (local === record.sub_aid)
|
|
179
|
+
task.stage = false;
|
|
180
|
+
await this.deps.files.declareSubaid(task);
|
|
201
181
|
}
|
|
202
|
-
|
|
203
|
-
|
|
182
|
+
if (local === record.sub_aid && !await this.hasLocalDeclaration(record, 'agent'))
|
|
183
|
+
await this.deps.files.declareOwneraid(task);
|
|
184
|
+
}
|
|
185
|
+
else if (record.operation === 'transfer') {
|
|
186
|
+
if (local === record.owner_aid)
|
|
187
|
+
await this.deps.files.transferOwneraid(task);
|
|
188
|
+
if (local === record.new_owner_aid && !await this.hasLocalDeclaration(record, 'owner'))
|
|
189
|
+
await this.deps.files.declareSubaid(task);
|
|
190
|
+
if (local === record.sub_aid && !await this.hasLocalDeclaration(record, 'agent')) {
|
|
191
|
+
task.stage = true;
|
|
192
|
+
await this.deps.files.declareOwneraid(task);
|
|
204
193
|
}
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
let initiator;
|
|
208
|
-
try {
|
|
209
|
-
initiator = ownershipAid(task?.initiator_aid, 'initiator_aid');
|
|
210
|
-
}
|
|
211
|
-
catch { }
|
|
212
|
-
if (!task || !initiator || !['pending', 'completed', 'rejected', 'stopped', 'error'].includes(task.status)) {
|
|
213
|
-
skipped.push(snapshot.operation_id);
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
if (initiator !== local || ['completed', 'rejected', 'stopped'].includes(task.status))
|
|
217
|
-
break;
|
|
218
|
-
let subject;
|
|
219
|
-
try {
|
|
220
|
-
subject = ownershipAid(task.sub_aid, 'sub_aid');
|
|
221
|
-
}
|
|
222
|
-
catch { }
|
|
223
|
-
try {
|
|
224
|
-
const original = source, result = await this.runSerialized(subject ?? source.operation_id, () => { check(); return this.resetSource(original, subject); });
|
|
225
|
-
check();
|
|
226
|
-
if (result.created)
|
|
227
|
-
reset.push(source.operation_id);
|
|
228
|
-
break;
|
|
229
194
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
251
|
-
task = parsed;
|
|
252
|
-
}
|
|
253
|
-
catch { }
|
|
254
|
-
if (Object.hasOwn(task, 'task_reset')) {
|
|
255
|
-
this.decode({ operation_id: operationId, revision: source.revision, task });
|
|
256
|
-
return { operation_id: operationId, reset: true };
|
|
257
|
-
}
|
|
258
|
-
let subject;
|
|
259
|
-
try {
|
|
260
|
-
subject = ownershipAid(task.sub_aid, 'sub_aid');
|
|
195
|
+
else {
|
|
196
|
+
if (local === record.owner_aid)
|
|
197
|
+
await this.deps.files.revokeSubaid(task);
|
|
198
|
+
if (local === record.sub_aid)
|
|
199
|
+
await this.deps.files.revokeOwneraid(task);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const notification of record.notifications) {
|
|
203
|
+
this.deps.assertCurrent?.();
|
|
204
|
+
await this.deps.send(notification.to, notification.payload, notification.message_id);
|
|
205
|
+
this.deps.assertCurrent?.();
|
|
206
|
+
}
|
|
207
|
+
const result = this.result(record, record.command === 'reject' ? 'rejected' : 'completed');
|
|
208
|
+
const verification = await this.deps.files.checkConsensus(record.sub_aid, { owner_aid: targetOwner(record), relation_version: record.target_relation_version });
|
|
209
|
+
this.deps.assertCurrent?.();
|
|
210
|
+
if (verification.status === 'unavailable' || verification.status === 'invalid' || verification.status === 'not_applicable')
|
|
211
|
+
throw ownershipFailure(verification.code ?? verification.status, verification.reason ?? 'ownership verification request failed', true);
|
|
212
|
+
if (verification.status === 'consensus')
|
|
213
|
+
this.deps.emit('ownership.completed', result);
|
|
214
|
+
return result;
|
|
261
215
|
}
|
|
262
|
-
catch {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
216
|
+
catch (error) {
|
|
217
|
+
if (error instanceof StateError && ['conflict', 'owner_locked'].includes(error.stringCode))
|
|
218
|
+
throw error;
|
|
219
|
+
return this.result(record, 'error', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async start(operation, subAid, ownerAid, newOwnerAid) {
|
|
223
|
+
requireOwnershipApplicable(this.deps.currentType?.());
|
|
224
|
+
if (newOwnerAid === ownerAid)
|
|
225
|
+
throw ownershipFailure('forbidden', 'transfer target must differ from the current owner');
|
|
226
|
+
const agent = await this.deps.files.readAgent(subAid), parsed = agent.exists ? await this.deps.files.verifyFile(agent.content, 'agent', subAid) : undefined;
|
|
227
|
+
if (operation === 'bind' && parsed?.frontmatter.type === 'human')
|
|
228
|
+
throw ownershipFailure('owner_locked', 'binding requires a non-human identity');
|
|
229
|
+
if (operation !== 'bind') {
|
|
230
|
+
const consensus = await this.deps.files.checkConsensus(subAid);
|
|
231
|
+
if (consensus.status !== 'consensus' || consensus.owner_aid !== ownerAid)
|
|
232
|
+
throw ownershipFailure('conflict', 'operation requires current owner consensus');
|
|
233
|
+
}
|
|
234
|
+
const baseVersion = operation === 'bind' && agent.lock === 'unlocked' && (agent.pending || agent.ownership_status === 'pending') && parsed?.statement ? parsed.statement.relation_version - 1 : agent.relation_version;
|
|
235
|
+
const record = { operation_id: randomUUID(), operation, sub_aid: subAid, owner_aid: ownerAid, ...(newOwnerAid ? { new_owner_aid: newOwnerAid } : {}), initiator_aid: this.local(), base_relation_version: baseVersion, target_relation_version: operation === 'release' ? baseVersion : baseVersion + 1, command: 'request', files: {}, notifications: [] };
|
|
236
|
+
if (operation === 'bind' && agent.lock === 'locked') {
|
|
237
|
+
if (parsed?.statement?.owner_aid !== ownerAid)
|
|
238
|
+
throw ownershipFailure('owner_locked', 'ownership is locked; release or transfer is required');
|
|
239
|
+
return this.result({ ...record, base_relation_version: agent.relation_version - 1, target_relation_version: agent.relation_version }, 'completed');
|
|
240
|
+
}
|
|
241
|
+
await this.capture(record);
|
|
242
|
+
if (operation === 'bind' && subAid !== ownerAid)
|
|
243
|
+
this.notify(record, this.local() === subAid ? ownerAid : subAid, 'bind.request');
|
|
244
|
+
else if (operation === 'transfer') {
|
|
245
|
+
for (const recipient of new Set([subAid, newOwnerAid]))
|
|
246
|
+
if (recipient !== this.local())
|
|
247
|
+
this.notify(record, recipient, 'transfer.request');
|
|
248
|
+
}
|
|
249
|
+
else if (operation === 'release' && subAid !== ownerAid)
|
|
250
|
+
this.notify(record, subAid, 'relation.confirmed');
|
|
251
|
+
return this.execute(record);
|
|
252
|
+
}
|
|
253
|
+
async bindOwneraid(ownerAid) { return this.start('bind', this.local(), ownershipAid(ownerAid, 'owner_aid')); }
|
|
254
|
+
async bindSubaid(subAid) { return this.start('bind', ownershipAid(subAid, 'sub_aid'), this.local()); }
|
|
255
|
+
async transferSubaid(subAid, newOwnerAid) { return this.start('transfer', ownershipAid(subAid, 'sub_aid'), this.local(), ownershipAid(newOwnerAid, 'new_owner_aid')); }
|
|
256
|
+
async releaseSubaid(subAid) { return this.start('release', ownershipAid(subAid, 'sub_aid'), this.local()); }
|
|
257
|
+
async acceptOwnershipRequest(operationId) { return this.decide(operationId, 'accept'); }
|
|
258
|
+
async rejectOwnershipRequest(operationId, reason = '') { return this.decide(operationId, 'reject', reason); }
|
|
259
|
+
async decide(operationId, decision, reason = '') {
|
|
260
|
+
let request = this.requests.get(operationId);
|
|
261
|
+
if (!request && this.deps.recoverRequest) {
|
|
262
|
+
const recovered = await this.deps.recoverRequest(operationId);
|
|
263
|
+
if (recovered) {
|
|
264
|
+
await this.receive(recovered.from, recovered.payload);
|
|
265
|
+
request = this.requests.get(operationId);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!request)
|
|
269
|
+
throw ownershipFailure('operation_not_found', 'original authenticated ownership request is required');
|
|
270
|
+
const message = request.payload;
|
|
271
|
+
const record = { operation_id: message.operation_id, operation: message.operation, sub_aid: message.sub_aid, owner_aid: message.owner_aid, ...(message.new_owner_aid ? { new_owner_aid: message.new_owner_aid } : {}), base_relation_version: message.base_relation_version, target_relation_version: message.target_relation_version, initiator_aid: request.from, command: decision, files: {}, notifications: [] };
|
|
272
|
+
const consensus = await this.deps.files.checkConsensus(message.sub_aid);
|
|
273
|
+
if (consensus.status === 'unavailable' || consensus.status === 'invalid' || consensus.status === 'not_applicable')
|
|
274
|
+
throw ownershipFailure(consensus.code ?? consensus.status, consensus.reason ?? 'ownership verification request failed', true);
|
|
275
|
+
if (consensus.lock === 'locked') {
|
|
276
|
+
if (consensus.owner_aid === targetOwner(record) && consensus.relation_version === record.target_relation_version) {
|
|
277
|
+
if (decision === 'reject')
|
|
278
|
+
throw ownershipFailure('owner_locked', 'ownership is locked; release or transfer is required');
|
|
279
|
+
return this.result(record, 'completed');
|
|
280
|
+
}
|
|
281
|
+
if (record.operation !== 'transfer' || consensus.owner_aid !== record.owner_aid || consensus.relation_version !== record.base_relation_version)
|
|
282
|
+
throw ownershipFailure('conflict', 'ownership current NS relation does not match this request');
|
|
283
|
+
}
|
|
284
|
+
await this.capture(record);
|
|
285
|
+
this.notify(record, request.from, message.operation === 'bind' ? decision === 'accept' ? 'bind.accept' : 'bind.reject' : decision === 'accept' ? 'transfer.accept' : 'transfer.reject', reason || undefined);
|
|
286
|
+
return this.execute(record);
|
|
268
287
|
}
|
|
269
288
|
async receive(from, content) {
|
|
270
|
-
let
|
|
289
|
+
let value;
|
|
271
290
|
if (typeof content === 'string') {
|
|
272
291
|
try {
|
|
273
|
-
|
|
292
|
+
value = JSON.parse(content);
|
|
274
293
|
}
|
|
275
294
|
catch {
|
|
276
295
|
return false;
|
|
277
296
|
}
|
|
278
297
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (typeof type !== 'string' || !type.startsWith('aun.ownership.'))
|
|
298
|
+
else
|
|
299
|
+
value = content;
|
|
300
|
+
if (!value || typeof value.type !== 'string' || !value.type.startsWith('aun.ownership.'))
|
|
283
301
|
return false;
|
|
284
|
-
if (type !== OWNERSHIP_SDK_MESSAGE_TYPE)
|
|
302
|
+
if (value.type !== OWNERSHIP_SDK_MESSAGE_TYPE)
|
|
285
303
|
return true;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
message
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
297
|
-
throw error;
|
|
304
|
+
const request = value?.action === 'bind.request' || value?.action === 'transfer.request';
|
|
305
|
+
if (request) {
|
|
306
|
+
const message = parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local() });
|
|
307
|
+
const previous = this.requests.get(message.operation_id);
|
|
308
|
+
if (previous && (previous.from !== from || Object.keys(message).some((key) => message[key] !== previous.payload[key])))
|
|
309
|
+
throw ownershipFailure('conflict', 'original ownership request context changed');
|
|
310
|
+
this.requests.set(message.operation_id, { from, payload: structuredClone(message) });
|
|
311
|
+
await this.deps.emit('ownership.request', { ...message, requester_aid: from });
|
|
298
312
|
return true;
|
|
299
313
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
314
|
+
if (!value || value.type !== OWNERSHIP_SDK_MESSAGE_TYPE || !OWNERSHIP_UUID_V4_RE.test(value.operation_id) || !participants(value).includes(from) || !participants(value).includes(this.local()) || from === this.local())
|
|
315
|
+
throw ownershipFailure('forbidden', 'invalid ownership notification sender');
|
|
316
|
+
parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(), task: { ...value, initiator_aid: this.local() } });
|
|
317
|
+
if (value.action === 'bind.reject' || value.action === 'transfer.reject')
|
|
318
|
+
await this.deps.emit('ownership.rejected', { ...value });
|
|
319
|
+
if (value.operation === 'transfer' && value.action === 'transfer.accept' && this.local() === value.owner_aid) {
|
|
320
|
+
const consensus = await this.deps.files.checkConsensus(value.sub_aid);
|
|
321
|
+
if (consensus.status !== 'consensus' || consensus.owner_aid !== value.new_owner_aid || consensus.relation_version !== value.target_relation_version)
|
|
307
322
|
return true;
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
if (message.action === 'sub.cleaned') {
|
|
328
|
-
const began = Date.now();
|
|
329
|
-
const before = await this.readNs(message.sub_aid), after = await this.readNs(message.sub_aid);
|
|
330
|
-
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))
|
|
331
|
-
throw ownershipFailure('unavailable', 'NS changed during sub cleanup verification', true);
|
|
332
|
-
if (!before.exists || before.relation_version !== 0 || before.lock !== 'unlocked' || before.owner_aid || !before.etag)
|
|
333
|
-
return true;
|
|
334
|
-
const file = parseOwnershipFile(before.content, 'agent', { expectedSubAid: message.sub_aid });
|
|
335
|
-
if (Object.hasOwn(file.frontmatter, 'owner') || Object.hasOwn(file.frontmatter, 'ownership'))
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
const key = `${from}|${message.operation_id}|${message.action}`;
|
|
339
|
-
if (!handle.record.received.includes(key) && !handle.record.inbox.some((item) => `${item.from}|${item.payload.operation_id}|${item.payload.action}` === key)) {
|
|
340
|
-
handle.record.inbox.push({ from, payload: message });
|
|
341
|
-
await this.save(handle);
|
|
342
|
-
}
|
|
343
|
-
if (observationError)
|
|
344
|
-
await this.recordError(handle, observationError);
|
|
345
|
-
else
|
|
346
|
-
await this.drive(handle);
|
|
347
|
-
return true;
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
async resume() {
|
|
351
|
-
const failed = [];
|
|
352
|
-
for (const snapshot of this.list(true)) {
|
|
353
|
-
try {
|
|
354
|
-
const record = this.decode(snapshot).record;
|
|
355
|
-
if (this.isReset(record))
|
|
356
|
-
continue;
|
|
357
|
-
if (!record.base_agent_md && record.request_verified && ['completed', 'stopped', 'rejected'].includes(record.status))
|
|
358
|
-
continue;
|
|
359
|
-
await this.runSerialized(record.sub_aid, async () => {
|
|
360
|
-
const handle = this.load(record.operation_id);
|
|
361
|
-
await this.drive(handle);
|
|
362
|
-
if (handle.record.request_verified && handle.record.local_decision === 'pending' && handle.record.status === 'pending' && handle.record.operation !== 'release')
|
|
363
|
-
this.emit(handle.record, 'ownership.request');
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
catch (error) {
|
|
367
|
-
failed.push(`${snapshot.operation_id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
if (failed.length)
|
|
371
|
-
throw ownershipFailure('persistence_unavailable', failed.join('; '));
|
|
372
|
-
}
|
|
373
|
-
local() { return ownershipAid(this.deps.currentAid(), 'local_aid'); }
|
|
374
|
-
requireApplicable(subAid) {
|
|
375
|
-
if (subAid === this.local())
|
|
376
|
-
requireOwnershipApplicable(this.deps.currentType?.());
|
|
377
|
-
}
|
|
378
|
-
store() {
|
|
379
|
-
if (!this.deps.store.loadOwnershipTask || !this.deps.store.listOwnershipTasks || !this.deps.store.compareAndSwapOwnershipTask)
|
|
380
|
-
throw ownershipFailure('persistence_unavailable', 'atomic ownership task storage is unavailable');
|
|
381
|
-
return this.deps.store;
|
|
382
|
-
}
|
|
383
|
-
list(isolateErrors = false) {
|
|
384
|
-
try {
|
|
385
|
-
return this.store().listOwnershipTasks(this.local(), isolateErrors);
|
|
386
|
-
}
|
|
387
|
-
catch (error) {
|
|
388
|
-
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
load(operationId) {
|
|
392
|
-
try {
|
|
393
|
-
const snapshot = this.store().loadOwnershipTask(this.local(), operationId);
|
|
394
|
-
return snapshot ? this.decode(snapshot) : null;
|
|
395
|
-
}
|
|
396
|
-
catch (error) {
|
|
397
|
-
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
taskSource(operationId) {
|
|
401
|
-
try {
|
|
402
|
-
if (!this.deps.store.loadOwnershipTaskSource || !this.deps.store.resetOwnershipTask)
|
|
403
|
-
throw new Error('atomic ownership task reset storage is unavailable');
|
|
404
|
-
return this.deps.store.loadOwnershipTaskSource(this.local(), operationId);
|
|
405
|
-
}
|
|
406
|
-
catch (error) {
|
|
407
|
-
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
async resetSource(source, subject) {
|
|
411
|
-
const task = { protocol: '2.1', operation_id: source.operation_id, local_aid: this.local(), status: 'stopped', step: 'reset',
|
|
412
|
-
...(subject ? { sub_aid: subject } : {}), task_reset: { source: source.source, source_sha256: ownershipSha256(source.source), revision: source.revision, reset_at: Math.trunc(Date.now() / 1000) } };
|
|
413
|
-
try {
|
|
414
|
-
const saved = this.deps.store.resetOwnershipTask(this.local(), source.operation_id, source.revision, source.source, task);
|
|
415
|
-
this.decode(saved);
|
|
416
|
-
if (!this.isReset(saved.task))
|
|
417
|
-
throw new Error('ownership reset marker was not saved');
|
|
418
|
-
const committed = this.taskSource(source.operation_id);
|
|
419
|
-
if (!committed || committed.revision !== saved.revision || JSON.stringify(JSON.parse(committed.source)) !== JSON.stringify(saved.task))
|
|
420
|
-
throw new Error('ownership reset marker readback failed');
|
|
421
|
-
return { operation_id: source.operation_id, reset: true, created: true };
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
if (!(error instanceof Error) || !/snapshot|revision conflict|already reset/.test(error.message))
|
|
425
|
-
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
426
|
-
const winner = this.taskSource(source.operation_id);
|
|
427
|
-
if (winner) {
|
|
428
|
-
let parsed;
|
|
429
|
-
try {
|
|
430
|
-
parsed = JSON.parse(winner.source);
|
|
431
|
-
}
|
|
432
|
-
catch { }
|
|
433
|
-
if (parsed && typeof parsed === 'object' && Object.hasOwn(parsed, 'task_reset')) {
|
|
434
|
-
this.decode({ operation_id: winner.operation_id, revision: winner.revision, task: parsed });
|
|
435
|
-
return { operation_id: source.operation_id, reset: true, created: false };
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
throw ownershipFailure(error instanceof Error && /revision|snapshot|conflict/.test(error.message) ? 'conflict' : 'persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
isReset(record) { return Object.hasOwn(record, 'task_reset'); }
|
|
442
|
-
async resetUnusableTasks(subAid, ownerAid) {
|
|
443
|
-
for (const snapshot of this.list(true)) {
|
|
444
|
-
if (this.isReset(snapshot.task)) {
|
|
445
|
-
this.decode(snapshot);
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (snapshot.task.sub_aid !== subAid)
|
|
449
|
-
continue;
|
|
450
|
-
if (!this.isReset(snapshot.task) && ['completed', 'rejected', 'stopped'].includes(snapshot.task.status))
|
|
451
|
-
continue;
|
|
452
|
-
let record;
|
|
453
|
-
let unusable = false;
|
|
454
|
-
try {
|
|
455
|
-
record = this.decode(snapshot).record;
|
|
456
|
-
}
|
|
457
|
-
catch (error) {
|
|
458
|
-
if (this.isReset(snapshot.task) || !(error instanceof OwnershipTaskDecodeError) || error.operationId !== snapshot.operation_id)
|
|
459
|
-
throw error;
|
|
460
|
-
unusable = true;
|
|
461
|
-
record = snapshot.task;
|
|
462
|
-
}
|
|
463
|
-
if (!unusable && !this.isReset(record) && !record.base_agent_md && record.request_verified && !['completed', 'stopped', 'rejected'].includes(record.status) && record.operation === 'bind' && record.owner_aid === ownerAid && record.initiator_aid === this.local()) {
|
|
464
|
-
try {
|
|
465
|
-
this.legacyTaskView(record);
|
|
466
|
-
}
|
|
467
|
-
catch {
|
|
468
|
-
unusable = true;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (!unusable)
|
|
472
|
-
continue;
|
|
473
|
-
const source = this.taskSource(snapshot.operation_id);
|
|
474
|
-
if (!source || source.revision !== snapshot.revision)
|
|
475
|
-
throw ownershipFailure('conflict', 'ownership task advanced before automatic reset');
|
|
476
|
-
let parsed;
|
|
477
|
-
try {
|
|
478
|
-
parsed = JSON.parse(source.source);
|
|
479
|
-
}
|
|
480
|
-
catch { }
|
|
481
|
-
if (!parsed || typeof parsed !== 'object' || parsed.sub_aid !== subAid)
|
|
482
|
-
throw ownershipFailure('persistence_unavailable', 'ownership task subject cannot be established for automatic reset');
|
|
483
|
-
if (Object.hasOwn(parsed, 'task_reset'))
|
|
484
|
-
throw ownershipFailure('persistence_unavailable', 'ownership reset marker is corrupt');
|
|
485
|
-
if (['completed', 'rejected', 'stopped'].includes(parsed.status))
|
|
486
|
-
continue;
|
|
487
|
-
await this.resetSource(source, subAid);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
decode(snapshot) {
|
|
491
|
-
const record = snapshot.task;
|
|
492
|
-
try {
|
|
493
|
-
if (this.isReset(record)) {
|
|
494
|
-
const task = snapshot.task, reset = task.task_reset;
|
|
495
|
-
if (task.protocol !== '2.1' || task.operation_id !== snapshot.operation_id || !OWNERSHIP_UUID_V4_RE.test(snapshot.operation_id) || task.local_aid !== this.local() || task.status !== 'stopped' || task.step !== 'reset' || Object.keys(task).some((key) => !['protocol', 'operation_id', 'local_aid', 'status', 'step', 'sub_aid', 'task_reset'].includes(key)) || !reset || typeof reset !== 'object' || Array.isArray(reset) || Object.keys(reset).length !== 4 || typeof reset.source !== 'string' || reset.source_sha256 !== ownershipSha256(reset.source) || !Number.isSafeInteger(reset.revision) || Number(reset.revision) < 1 || Number(reset.revision) + 1 !== snapshot.revision || !Number.isSafeInteger(reset.reset_at) || Number(reset.reset_at) <= 0)
|
|
496
|
-
throw new Error('ownership reset marker is corrupt');
|
|
497
|
-
let subject;
|
|
498
|
-
try {
|
|
499
|
-
subject = ownershipAid(JSON.parse(reset.source).sub_aid, 'sub_aid');
|
|
500
|
-
}
|
|
501
|
-
catch { }
|
|
502
|
-
if (task.sub_aid !== subject)
|
|
503
|
-
throw new Error('ownership reset subject changed');
|
|
504
|
-
return { record, revision: snapshot.revision };
|
|
505
|
-
}
|
|
506
|
-
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')
|
|
507
|
-
throw new Error('invalid ownership task');
|
|
508
|
-
if (!record.base_agent_md && record.request_verified && !record.legacy_task)
|
|
509
|
-
return { record, revision: snapshot.revision };
|
|
510
|
-
ownershipAid(record.sub_aid, 'sub_aid');
|
|
511
|
-
ownershipAid(record.owner_aid, 'owner_aid');
|
|
512
|
-
ownershipAid(record.initiator_aid, 'initiator_aid');
|
|
513
|
-
if (record.operation === 'transfer') {
|
|
514
|
-
if (ownershipAid(record.new_owner_aid, 'new_owner_aid') === record.owner_aid)
|
|
515
|
-
throw new Error('invalid transfer task');
|
|
516
|
-
}
|
|
517
|
-
else if (record.new_owner_aid !== undefined)
|
|
518
|
-
throw new Error('unexpected transfer target');
|
|
519
|
-
const baseVersion = ownershipInteger(record.base_relation_version, 'base_relation_version', 0), targetVersion = ownershipInteger(record.target_relation_version, 'target_relation_version');
|
|
520
|
-
if (targetVersion !== baseVersion + 1 && !(record.operation === 'release' && targetVersion === baseVersion) || !participants(record).includes(record.initiator_aid) || record.operation !== 'bind' && record.initiator_aid !== record.owner_aid)
|
|
521
|
-
throw new Error('invalid fixed ownership context');
|
|
522
|
-
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')
|
|
523
|
-
throw new Error('ownership approval or pending state is corrupt');
|
|
524
|
-
if (Object.keys(record.approvals).length !== participants(record).length || record.approvals[record.initiator_aid] !== 'accept' || record.initiator_aid === this.local() && record.local_decision !== 'accept')
|
|
525
|
-
throw new Error('ownership initiator or approval participants are corrupt');
|
|
526
|
-
if (record.legacy_recovery !== undefined) {
|
|
527
|
-
const recovery = record.legacy_recovery, original = recovery.task, checkpoint = recovery.checkpoint;
|
|
528
|
-
if (!original || record.request_verified !== true || recovery.task_sha256 !== ownershipSha256(JSON.stringify(original)) || !Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || !checkpoint || !Number.isSafeInteger(checkpoint.observed_at) || checkpoint.observed_at <= 0 || typeof recovery.owner_confirmed !== 'boolean')
|
|
529
|
-
throw new Error('legacy recovery checkpoint is corrupt');
|
|
530
|
-
this.legacyView(original);
|
|
531
|
-
if (['operation_id', 'operation', 'sub_aid', 'owner_aid', 'initiator_aid', 'base_relation_version', 'target_relation_version'].some((key) => original[key] !== record[key]) || JSON.stringify(checkpoint.agent_md) !== JSON.stringify(record.base_agent_md) || original.local_decision !== 'pending' && original.local_decision !== record.local_decision || participants(record).some((name) => original.approvals[name] !== 'pending' && original.approvals[name] !== record.approvals[name]))
|
|
532
|
-
throw new Error('legacy recovery original context or approval changed');
|
|
533
|
-
const owner = checkpoint.owner_md;
|
|
534
|
-
if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
|
|
535
|
-
throw new Error('legacy recovery owner observation is corrupt');
|
|
536
|
-
if (owner.exists) {
|
|
537
|
-
const file = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
|
|
538
|
-
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
539
|
-
throw new Error('legacy recovery owner observation changed');
|
|
540
|
-
}
|
|
541
|
-
const originalOwner = original.files.owner, pinned = record.files.owner;
|
|
542
|
-
if ((originalOwner || owner.exists) && (!pinned || pinned.raw !== (originalOwner?.raw ?? owner.raw) || pinned.hash !== (originalOwner?.hash ?? owner.sha256) || JSON.stringify(pinned.original_owner) !== JSON.stringify(owner)) || originalOwner && (!owner.exists || originalOwner.raw !== owner.raw || originalOwner.hash !== owner.sha256))
|
|
543
|
-
throw new Error('legacy recovery owner candidate changed');
|
|
544
|
-
if (this.local() === record.owner_aid ? !recovery.owner_confirmed : recovery.owner_confirmed ? !record.received.includes(`${record.owner_aid}|${record.operation_id}|owner.ready`) : Boolean(record.files.agent) || record.status === 'completed')
|
|
545
|
-
throw new Error('legacy recovery owner confirmation is corrupt');
|
|
546
|
-
}
|
|
547
|
-
if (record.legacy_task) {
|
|
548
|
-
const recovery = record.legacy_task, original = recovery.task;
|
|
549
|
-
if (!Number.isSafeInteger(recovery.revision) || recovery.revision < 1 || recovery.revision >= snapshot.revision || recovery.task_sha256 !== ownershipSha256(JSON.stringify(original)))
|
|
550
|
-
throw new Error('legacy task source is corrupt');
|
|
551
|
-
this.priorView(original);
|
|
552
|
-
if (['operation_id', 'operation', 'sub_aid', 'owner_aid', 'new_owner_aid', 'initiator_aid', 'base_relation_version', 'target_relation_version'].some((key) => original[key] !== record[key]) || Object.entries(original.approvals).some(([aid, decision]) => decision !== 'pending' && record.approvals[aid] !== decision) || JSON.stringify(recovery.checkpoint.agent_md) !== JSON.stringify(record.base_agent_md ?? null) || JSON.stringify(recovery.checkpoint.owner_md) !== JSON.stringify(record.base_owner_md ?? null) || !record.base_agent_md && record.status !== 'completed')
|
|
553
|
-
throw new Error('legacy task fixed context changed');
|
|
554
|
-
for (const [key, saved] of Object.entries(original.files))
|
|
555
|
-
if (record.files[key]?.raw !== saved.raw || record.files[key]?.hash !== saved.hash || key === 'agent' && JSON.stringify(record.files[key]?.condition) !== JSON.stringify(saved.condition))
|
|
556
|
-
throw new Error('legacy task signed candidate changed');
|
|
557
|
-
}
|
|
558
|
-
if (record.request_verified && (record.base_agent_md || !record.legacy_task)) {
|
|
559
|
-
const base = record.base_agent_md;
|
|
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
|
-
throw new Error('ownership original agent.md evidence is corrupt');
|
|
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 && !(base.pending && statement?.relation_version === record.target_relation_version && statement.owner_aid === record.owner_aid) || base.pending && record.operation !== 'bind')
|
|
564
|
-
throw new Error('ownership original statement is corrupt');
|
|
565
|
-
const owner = record.base_owner_md;
|
|
566
|
-
if (!owner || typeof owner.exists !== 'boolean' || typeof owner.raw !== 'string' || owner.sha256 !== (owner.exists ? ownershipSha256(owner.raw) : '') || !owner.exists && owner.raw !== '')
|
|
567
|
-
throw new Error('ownership original owner.md evidence is corrupt');
|
|
568
|
-
if (owner.exists) {
|
|
569
|
-
const declaration = parseOwnershipFile(owner.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: statement?.owner_aid ?? record.owner_aid });
|
|
570
|
-
if (!base.pending && declaration.statement?.relation_version !== record.base_relation_version)
|
|
571
|
-
throw new Error('ownership original owner statement is corrupt');
|
|
572
|
-
}
|
|
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
|
-
}
|
|
595
|
-
for (const item of record.inbox)
|
|
596
|
-
parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record });
|
|
597
|
-
for (const key of record.received) {
|
|
598
|
-
if (typeof key !== 'string' || key.split('|').length !== 3)
|
|
599
|
-
throw new Error('ownership received key is corrupt');
|
|
600
|
-
const [from, operationId, action] = key.split('|');
|
|
601
|
-
parseOwnershipMessage({ type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: operationId, operation: record.operation, action,
|
|
602
|
-
sub_aid: record.sub_aid, owner_aid: record.owner_aid, ...(record.operation === 'transfer' ? { new_owner_aid: record.new_owner_aid } : {}),
|
|
603
|
-
base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version }, { authenticated_from_aid: from, local_aid: this.local(), task: record });
|
|
604
|
-
}
|
|
605
|
-
for (const item of record.outbox)
|
|
606
|
-
this.validateOutgoing(record, item);
|
|
607
|
-
if (record.status === 'completed' && (record.verified_relation_version !== record.target_relation_version || record.operation !== 'release' && record.local_decision !== 'accept'))
|
|
608
|
-
throw new Error('ownership completion evidence or local approval is corrupt');
|
|
609
|
-
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'));
|
|
610
|
-
if (transferEvidenceRequired && (!/^[0-9a-f]{64}$/.test(record.authorization_sha256 ?? '') || !Number.isSafeInteger(record.expires_at) || (record.expires_at ?? 0) <= 0))
|
|
611
|
-
throw new Error('ownership transfer authorization evidence is corrupt');
|
|
612
|
-
for (const [key, saved] of Object.entries(record.files)) {
|
|
613
|
-
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))
|
|
614
|
-
throw new Error('ownership saved file is corrupt');
|
|
615
|
-
if (key === 'agent' && !(typeof saved.condition.if_match === 'string' && saved.condition.if_match || saved.condition.if_none_match === '*'))
|
|
616
|
-
throw new Error('ownership original file condition is corrupt');
|
|
617
|
-
const original = saved.original_owner;
|
|
618
|
-
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 !== ''))
|
|
619
|
-
throw new Error('ownership original owner material is corrupt');
|
|
620
|
-
const owner = ['agent', 'owner'].includes(key) ? targetOwner(record) : record.owner_aid;
|
|
621
|
-
const file = parseOwnershipFile(saved.raw, key === 'agent' ? 'agent' : 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: owner });
|
|
622
|
-
const version = ['release', 'transfer', 'cleanup'].includes(key) ? record.base_relation_version : record.target_relation_version;
|
|
623
|
-
if (key === 'agent' && record.operation === 'release' ? Boolean(file.statement) : file.statement?.relation_version !== version)
|
|
624
|
-
throw new Error('ownership saved file relation is corrupt');
|
|
625
|
-
if (key === 'owner' && file.action !== 'declare' || key === 'release' && file.action !== 'release' || key === 'transfer' && (file.action !== 'transfer_owner' || file.transfer?.new_owner_aid !== record.new_owner_aid || file.transfer?.target_relation_version !== record.target_relation_version || file.transfer?.expires_at !== saved.expires_at))
|
|
626
|
-
throw new Error('ownership saved file action is corrupt');
|
|
627
|
-
}
|
|
628
|
-
if (record.release_cleanup !== undefined) {
|
|
629
|
-
const cleanup = record.release_cleanup;
|
|
630
|
-
if (!['bind', 'transfer'].includes(record.operation) || record.status !== 'completed' || !record.local_roles.includes('sub') || record.local_decision !== 'accept' || typeof cleanup.cleaned !== 'boolean' || !cleanup.files || typeof cleanup.files !== 'object' || Array.isArray(cleanup.files))
|
|
631
|
-
throw new Error('ownership historical cleanup context is corrupt');
|
|
632
|
-
for (const [key, saved] of Object.entries(cleanup.files)) {
|
|
633
|
-
if (key !== 'agent' || !saved || saved.hash !== ownershipSha256(saved.raw) || typeof saved.condition?.if_match !== 'string' || !saved.condition.if_match || Object.keys(saved.condition).length !== 1 || parseOwnershipFile(saved.raw, 'agent', { expectedSubAid: record.sub_aid }).statement)
|
|
634
|
-
throw new Error('ownership historical cleanup file is corrupt');
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
if (record.status === 'completed') {
|
|
638
|
-
const evidence = record.completion_evidence;
|
|
639
|
-
const cleaned = record.operation === 'release' && evidence?.status === 'unbound' && evidence.relation_version === 0 && record.cleanup_status === 'cleaned' && Boolean(this.cleanupEvidence(record, evidence));
|
|
640
|
-
if (!evidence || evidence.sub_aid !== record.sub_aid || !cleaned && (evidence.relation_version !== (record.operation === 'release' ? record.base_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))
|
|
641
|
-
throw new Error('ownership target acceptance evidence is corrupt');
|
|
642
|
-
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))
|
|
643
|
-
throw new Error('ownership target file evidence is missing');
|
|
644
|
-
if (record.operation === 'release' && record.local_roles.includes('owner') && !record.files.release)
|
|
645
|
-
throw new Error('ownership release file evidence is missing');
|
|
646
|
-
}
|
|
647
|
-
return { record, revision: snapshot.revision };
|
|
648
|
-
}
|
|
649
|
-
catch (error) {
|
|
650
|
-
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
651
|
-
throw error;
|
|
652
|
-
throw new OwnershipTaskDecodeError(snapshot.operation_id, error instanceof Error ? error.message : String(error));
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
legacyTaskView(record) {
|
|
656
|
-
return record.operation === 'bind' && record.base_relation_version === 0 && record.initiator_aid === record.sub_aid && Object.keys(record.files ?? {}).every((key) => key === 'owner') ? this.legacyView(record) : this.priorView(record);
|
|
657
|
-
}
|
|
658
|
-
legacyView(record) {
|
|
659
|
-
if (Object.hasOwn(record, 'legacy_recovery') || record.protocol !== '2.1' || record.operation !== 'bind' || record.base_relation_version !== 0 || record.target_relation_version !== 1 || record.initiator_aid !== record.sub_aid || record.request_verified !== true || record.status !== 'pending' && !(record.status === 'error' && record.retryable === true) || record.base_agent_md != null || record.base_owner_md != null || !record.files || typeof record.files !== 'object' || Array.isArray(record.files) || Object.keys(record.files).some((key) => key !== 'owner') || record.local_decision === 'reject' || Object.values(record.approvals ?? {}).includes('reject'))
|
|
660
|
-
throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
|
|
661
|
-
const view = JSON.parse(JSON.stringify(record));
|
|
662
|
-
view.request_verified = false;
|
|
663
|
-
view.files = {};
|
|
664
|
-
view.inbox = record.inbox?.map((item) => ({ from: item.from, payload: this.legacyMessage(item.payload) }));
|
|
665
|
-
view.outbox = record.outbox?.map((item) => ({ to: item.to, payload: this.legacyMessage(item.payload) }));
|
|
666
|
-
this.decode({ operation_id: record.operation_id, revision: 1, task: view });
|
|
667
|
-
for (const notice of record.notices) {
|
|
668
|
-
if (typeof notice !== 'string' || notice.split('|').length !== 2)
|
|
669
|
-
throw ownershipFailure('unsupported_protocol', 'legacy ownership notice is corrupt');
|
|
670
|
-
const [to, action] = notice.split('|');
|
|
671
|
-
this.validateOutgoing(view, { to: to, payload: { type: OWNERSHIP_SDK_MESSAGE_TYPE, operation_id: record.operation_id, operation: record.operation, action: action, sub_aid: record.sub_aid, owner_aid: record.owner_aid, base_relation_version: 0, target_relation_version: 1 } });
|
|
672
|
-
}
|
|
673
|
-
const saved = record.files.owner;
|
|
674
|
-
if (saved) {
|
|
675
|
-
if (this.local() !== record.owner_aid || record.local_decision !== 'accept' || typeof saved.raw !== 'string' || saved.hash !== ownershipSha256(saved.raw) || !saved.condition || Object.keys(saved.condition).length !== 1 || typeof saved.condition.expected_revision !== 'string' || !saved.condition.expected_revision || saved.published_revision !== undefined && (typeof saved.published_revision !== 'string' || !saved.published_revision))
|
|
676
|
-
throw ownershipFailure('unsupported_protocol', 'legacy owner candidate cannot be recovered automatically');
|
|
677
|
-
const file = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: record.owner_aid });
|
|
678
|
-
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
679
|
-
throw ownershipFailure('unsupported_protocol', 'legacy owner candidate does not match this task');
|
|
680
|
-
}
|
|
681
|
-
return view;
|
|
682
|
-
}
|
|
683
|
-
legacyMessage(content) {
|
|
684
|
-
if (!content || typeof content !== 'object' || Array.isArray(content))
|
|
685
|
-
throw ownershipFailure('unsupported_protocol', 'legacy ownership message is corrupt');
|
|
686
|
-
return { ...content };
|
|
687
|
-
}
|
|
688
|
-
async upgradeLegacy(handle) {
|
|
689
|
-
if (Boolean(handle.record.base_agent_md) || !handle.record.request_verified || ['completed', 'stopped', 'rejected'].includes(handle.record.status))
|
|
690
|
-
return;
|
|
691
|
-
if (handle.record.operation !== 'bind' || handle.record.base_relation_version !== 0 || handle.record.initiator_aid !== handle.record.sub_aid || Object.keys(handle.record.files ?? {}).some((key) => key !== 'owner'))
|
|
692
|
-
return this.upgradePrior(handle);
|
|
693
|
-
const original = JSON.parse(JSON.stringify(handle.record));
|
|
694
|
-
const record = this.legacyView(original);
|
|
695
|
-
for (const snapshot of this.list(true)) {
|
|
696
|
-
if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
|
|
697
|
-
continue;
|
|
698
|
-
const other = this.decode(snapshot).record;
|
|
699
|
-
if (!other.base_agent_md && other.request_verified && !['completed', 'stopped', 'rejected'].includes(other.status))
|
|
700
|
-
this.legacyTaskView(other);
|
|
701
|
-
if (other.status === 'pending' || other.status === 'error' && other.retryable)
|
|
702
|
-
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
703
|
-
}
|
|
704
|
-
const ns = await this.readNs(record.sub_aid);
|
|
705
|
-
if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
706
|
-
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
707
|
-
if (ns.lock === 'locked' && ns.owner_aid === record.owner_aid && ns.relation_version === record.target_relation_version)
|
|
708
|
-
return this.upgradePrior(handle);
|
|
709
|
-
if (ns.lock !== 'unlocked' || ns.relation_version !== 0 || ns.owner_aid || ns.exists && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement)
|
|
710
|
-
throw ownershipFailure('conflict', 'legacy ownership relation has changed');
|
|
711
|
-
const current = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
712
|
-
if (current.exists) {
|
|
713
|
-
const file = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, record.owner_aid);
|
|
714
|
-
if (file.action !== 'declare' || file.statement?.relation_version !== 1)
|
|
715
|
-
throw ownershipFailure('conflict', 'legacy owner declaration has changed');
|
|
716
|
-
}
|
|
717
|
-
const candidate = original.files.owner;
|
|
718
|
-
if (candidate) {
|
|
719
|
-
await this.deps.files.verifyFile(candidate.raw, 'owner', record.sub_aid, record.owner_aid);
|
|
720
|
-
if (!current.exists || current.content !== candidate.raw || current.sha256 !== candidate.hash)
|
|
721
|
-
throw ownershipFailure('conflict', 'legacy owner candidate was replaced');
|
|
722
|
-
if (candidate.published_revision !== undefined && current.revision !== candidate.published_revision)
|
|
723
|
-
throw ownershipFailure(current.revision ? 'conflict' : 'unavailable', 'legacy published owner revision changed', !current.revision);
|
|
724
|
-
}
|
|
725
|
-
else if (this.local() === record.owner_aid && current.exists)
|
|
726
|
-
throw ownershipFailure('unsupported_protocol', 'legacy owner declaration lacks a local task candidate');
|
|
727
|
-
const owner = { exists: current.exists, raw: current.content, sha256: current.exists ? ownershipSha256(current.content) : '', ...(candidate && current.revision ? { revision: current.revision } : {}) };
|
|
728
|
-
record.status = 'pending';
|
|
729
|
-
record.step = 'approval';
|
|
730
|
-
record.retryable = true;
|
|
731
|
-
record.request_verified = true;
|
|
732
|
-
delete record.code;
|
|
733
|
-
delete record.reason;
|
|
734
|
-
record.inbox = [];
|
|
735
|
-
record.received = [];
|
|
736
|
-
record.outbox = [];
|
|
737
|
-
record.notices = [];
|
|
738
|
-
await this.captureBase(record, ns);
|
|
739
|
-
if (candidate || current.exists && this.local() === record.sub_aid)
|
|
740
|
-
record.files.owner = { raw: candidate?.raw ?? current.content, hash: candidate?.hash ?? current.sha256, condition: {}, original_owner: owner };
|
|
741
|
-
record.legacy_recovery = { task: original, revision: handle.revision, task_sha256: ownershipSha256(JSON.stringify(original)), checkpoint: { observed_at: Math.trunc(Date.now() / 1000), agent_md: record.base_agent_md, owner_md: owner }, owner_confirmed: this.local() === record.owner_aid };
|
|
742
|
-
if (this.local() === record.sub_aid)
|
|
743
|
-
this.enqueue(record, record.owner_aid, 'bind.request');
|
|
744
|
-
else if (record.local_decision === 'accept')
|
|
745
|
-
this.enqueue(record, record.sub_aid, 'bind.accept');
|
|
746
|
-
const after = await this.readNs(record.sub_aid), afterOwner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
747
|
-
if (after.exists !== ns.exists || after.content !== ns.content || after.etag !== ns.etag || after.relation_version !== 0 || after.lock !== 'unlocked' || afterOwner.exists !== current.exists || afterOwner.content !== current.content || afterOwner.sha256 !== current.sha256)
|
|
748
|
-
throw ownershipFailure('conflict', 'legacy recovery observations changed');
|
|
749
|
-
const recovered = { record, revision: handle.revision };
|
|
750
|
-
this.decode({ operation_id: record.operation_id, revision: handle.revision + 1, task: record });
|
|
751
|
-
try {
|
|
752
|
-
await this.save(recovered);
|
|
753
|
-
}
|
|
754
|
-
catch (error) {
|
|
755
|
-
if (error instanceof StateError && error.stringCode === 'persistence_unavailable' && error.message.includes('active ownership task already exists'))
|
|
756
|
-
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
757
|
-
throw error;
|
|
758
|
-
}
|
|
759
|
-
handle.record = record;
|
|
760
|
-
handle.revision = recovered.revision;
|
|
761
|
-
}
|
|
762
|
-
priorView(original) {
|
|
763
|
-
if (original.base_agent_md != null || original.base_owner_md != null || original.legacy_task || original.legacy_recovery || !original.request_verified || !['pending', 'error'].includes(original.status) || original.status === 'error' && !original.retryable || Object.values(original.approvals ?? {}).includes('reject'))
|
|
764
|
-
throw ownershipFailure('unsupported_protocol', 'legacy task context cannot be recovered');
|
|
765
|
-
const record = structuredClone(original);
|
|
766
|
-
record.request_verified = false;
|
|
767
|
-
for (const [key, saved] of Object.entries(record.files ?? {})) {
|
|
768
|
-
const owner = key === 'owner' ? targetOwner(record) : record.owner_aid;
|
|
769
|
-
if (this.local() !== (key === 'agent' ? record.sub_aid : owner))
|
|
770
|
-
throw ownershipFailure('unsupported_protocol', 'legacy file local role is invalid');
|
|
771
|
-
if (key !== 'agent' && key !== 'cleanup') {
|
|
772
|
-
const condition = saved.condition;
|
|
773
|
-
if (!saved.original_owner && (!condition || Object.keys(condition).length !== 1 || typeof condition.expected_revision !== 'string' || !condition.expected_revision))
|
|
774
|
-
throw ownershipFailure('unsupported_protocol', 'legacy owner condition is invalid');
|
|
775
|
-
saved.condition = {};
|
|
776
|
-
saved.original_owner ??= { exists: false, raw: '', sha256: '' };
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
try {
|
|
780
|
-
this.decode({ operation_id: original.operation_id, revision: 1, task: record });
|
|
781
|
-
}
|
|
782
|
-
catch (error) {
|
|
783
|
-
throw ownershipFailure('unsupported_protocol', error instanceof Error ? error.message : String(error));
|
|
784
|
-
}
|
|
785
|
-
return record;
|
|
786
|
-
}
|
|
787
|
-
async upgradePrior(handle) {
|
|
788
|
-
const original = structuredClone(handle.record), record = this.priorView(original);
|
|
789
|
-
for (const snapshot of this.list(true)) {
|
|
790
|
-
if (snapshot.operation_id === record.operation_id || snapshot.task.sub_aid !== record.sub_aid)
|
|
791
|
-
continue;
|
|
792
|
-
const other = this.decode(snapshot).record;
|
|
793
|
-
if (other.status === 'pending' || other.status === 'error' && other.retryable)
|
|
794
|
-
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
795
|
-
}
|
|
796
|
-
const ns = await this.readNs(record.sub_aid);
|
|
797
|
-
if ((ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).frontmatter.type : record.sub_aid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
798
|
-
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
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);
|
|
800
|
-
const released = record.operation === 'release' && ns.lock === 'unlocked' && !ns.owner_aid && ns.relation_version === record.base_relation_version;
|
|
801
|
-
const committed = record.operation !== 'release' && ns.lock === 'locked' && ns.owner_aid === targetOwner(record) && ns.relation_version === record.target_relation_version;
|
|
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)
|
|
804
|
-
throw ownershipFailure('conflict', 'legacy relationship no longer matches this task');
|
|
805
|
-
if (!committed)
|
|
806
|
-
await this.captureBase(record, ns);
|
|
807
|
-
for (const [key, saved] of Object.entries(record.files)) {
|
|
808
|
-
const owner = key === 'owner' || key === 'agent' ? targetOwner(record) : record.owner_aid;
|
|
809
|
-
await this.deps.files.verifyFile(saved.raw, key === 'agent' ? 'agent' : 'owner', record.sub_aid, owner);
|
|
810
|
-
if (key === 'agent')
|
|
811
|
-
continue;
|
|
812
|
-
const current = await this.deps.files.readOwner(owner, record.sub_aid);
|
|
813
|
-
const source = original.files[key];
|
|
814
|
-
const revision = source.condition.expected_revision;
|
|
815
|
-
if (revision && (current.content === saved.raw && source.published_revision !== undefined || !committed && current.content !== saved.raw)) {
|
|
816
|
-
if (!current.revision)
|
|
817
|
-
throw ownershipFailure('unavailable', 'legacy owner original revision is unavailable', true);
|
|
818
|
-
if (current.revision !== (current.content === saved.raw ? source.published_revision : revision))
|
|
819
|
-
throw ownershipFailure('conflict', 'legacy owner original revision changed');
|
|
820
|
-
}
|
|
821
|
-
const currentStatement = current.exists ? parseOwnershipFile(current.content, 'owner', { expectedSubAid: record.sub_aid }).statement : undefined;
|
|
822
|
-
const expected = parseOwnershipFile(saved.raw, 'owner', { expectedSubAid: record.sub_aid }).statement;
|
|
823
|
-
const same = expected && currentStatement && Object.keys(expected).every((field) => expected[field] === currentStatement[field]);
|
|
824
|
-
if (current.content !== saved.raw && (source.published_revision !== undefined || current.exists && (key === 'owner' || !same)) && !committed)
|
|
825
|
-
throw ownershipFailure('conflict', 'legacy signed owner file was replaced');
|
|
826
|
-
saved.original_owner = { exists: current.exists, raw: current.content, sha256: current.exists ? current.sha256 : '', ...(revision && current.revision ? { revision: current.revision } : {}) };
|
|
827
|
-
}
|
|
828
|
-
record.request_verified = true;
|
|
829
|
-
record.legacy_task = { task: original, revision: handle.revision, task_sha256: ownershipSha256(JSON.stringify(original)), checkpoint: { agent_md: record.base_agent_md ?? null, owner_md: record.base_owner_md ?? null } };
|
|
830
|
-
const recovered = { record, revision: handle.revision };
|
|
831
|
-
if (committed) {
|
|
832
|
-
if (!await this.confirm(recovered))
|
|
833
|
-
throw ownershipFailure('conflict', 'legacy committed target lacks matching local signed evidence');
|
|
834
|
-
}
|
|
835
|
-
else
|
|
836
|
-
await this.save(recovered);
|
|
837
|
-
handle.record = record;
|
|
838
|
-
handle.revision = recovered.revision;
|
|
839
|
-
}
|
|
840
|
-
async save(handle) {
|
|
841
|
-
try {
|
|
842
|
-
const snapshot = this.store().compareAndSwapOwnershipTask(this.local(), handle.record.operation_id, handle.revision, handle.record);
|
|
843
|
-
handle.revision = snapshot.revision;
|
|
844
|
-
}
|
|
845
|
-
catch (error) {
|
|
846
|
-
throw ownershipFailure('persistence_unavailable', error instanceof Error ? error.message : String(error));
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
async recordError(handle, error) {
|
|
850
|
-
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
851
|
-
throw error;
|
|
852
|
-
const record = handle.record;
|
|
853
|
-
if (record.status === 'error' && record.retryable)
|
|
854
|
-
record.status = 'pending';
|
|
855
|
-
let code = error instanceof ValidationError ? 'invalid' : error instanceof StateError && error.stringCode ? error.stringCode : 'unavailable';
|
|
856
|
-
if (record.status === 'pending' && ['conflict', 'owner_locked', 'forbidden'].includes(code)) {
|
|
857
|
-
try {
|
|
858
|
-
if (await this.confirm(handle)) {
|
|
859
|
-
await this.flush(handle);
|
|
860
|
-
await this.cleanup(handle);
|
|
861
|
-
return;
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
catch (confirmationError) {
|
|
865
|
-
if (confirmationError instanceof StateError && confirmationError.stringCode === 'persistence_unavailable')
|
|
866
|
-
throw confirmationError;
|
|
867
|
-
error = confirmationError;
|
|
868
|
-
code = error instanceof ValidationError ? 'invalid' : error instanceof StateError && error.stringCode ? error.stringCode : 'unavailable';
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
record.code = code;
|
|
872
|
-
record.reason = error instanceof Error ? error.message : String(error);
|
|
873
|
-
record.retryable = record.status === 'pending' && ['unavailable', 'unsupported_protocol'].includes(code);
|
|
874
|
-
if (record.status === 'pending' && code === 'invalid')
|
|
875
|
-
record.status = 'error';
|
|
876
|
-
else if (record.status === 'pending' && ['conflict', 'owner_locked', 'forbidden'].includes(code))
|
|
877
|
-
record.status = 'stopped';
|
|
878
|
-
await this.save(handle);
|
|
879
|
-
this.emit(record, record.status === 'stopped' ? 'ownership.stopped' : 'ownership.error');
|
|
880
|
-
}
|
|
881
|
-
async runSerialized(subAid, work) {
|
|
882
|
-
const key = `${this.local()}|${subAid}`;
|
|
883
|
-
const before = OwnershipWorkflow.serial.get(key) ?? Promise.resolve();
|
|
884
|
-
const next = before.catch(() => { }).then(work);
|
|
885
|
-
OwnershipWorkflow.serial.set(key, next);
|
|
886
|
-
try {
|
|
887
|
-
return await next;
|
|
888
|
-
}
|
|
889
|
-
finally {
|
|
890
|
-
if (OwnershipWorkflow.serial.get(key) === next)
|
|
891
|
-
OwnershipWorkflow.serial.delete(key);
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
newRecord(context, initiator, decision) {
|
|
895
|
-
const record = {
|
|
896
|
-
protocol: '2.1', operation_id: context.operation_id, operation: context.operation, sub_aid: context.sub_aid, owner_aid: context.owner_aid,
|
|
897
|
-
base_relation_version: context.base_relation_version, target_relation_version: context.target_relation_version, status: 'pending', step: 'approval', retryable: true,
|
|
898
|
-
initiator_aid: initiator, local_roles: localRoles(context, this.local()), local_decision: decision,
|
|
899
|
-
approvals: Object.fromEntries(participants(context).map((name) => [name, name === initiator ? 'accept' : 'pending'])),
|
|
900
|
-
files: {}, inbox: [], received: [], outbox: [], notices: [], request_verified: initiator === this.local(), request_notified: false,
|
|
901
|
-
};
|
|
902
|
-
if (context.operation === 'transfer')
|
|
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;
|
|
906
|
-
record.approvals[this.local()] = decision;
|
|
907
|
-
return record;
|
|
908
|
-
}
|
|
909
|
-
async start(operation, subAid, ownerAid, newOwnerAid, operationId) {
|
|
910
|
-
this.requireApplicable(subAid);
|
|
911
|
-
return this.runSerialized(subAid, async () => {
|
|
912
|
-
if (operation === 'bind' && !operationId)
|
|
913
|
-
await this.resetUnusableTasks(subAid, ownerAid);
|
|
914
|
-
const existing = operationId ? this.load(operationId) : null;
|
|
915
|
-
if (existing) {
|
|
916
|
-
if (this.isReset(existing.record))
|
|
917
|
-
throw ownershipFailure('conflict', 'ownership task was reset');
|
|
918
|
-
await this.upgradeLegacy(existing);
|
|
919
|
-
const record = existing.record;
|
|
920
|
-
if (!record.base_agent_md && record.request_verified)
|
|
921
|
-
throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
|
|
922
|
-
if (record.operation !== operation || record.sub_aid !== subAid || record.owner_aid !== ownerAid || record.new_owner_aid !== newOwnerAid || record.initiator_aid !== this.local())
|
|
923
|
-
throw ownershipFailure('conflict', 'saved registration task context changed');
|
|
924
|
-
await this.drive(existing);
|
|
925
|
-
return ownershipTaskResult(record);
|
|
926
|
-
}
|
|
927
|
-
let ns;
|
|
928
|
-
for (const snapshot of this.list(true)) {
|
|
929
|
-
if (snapshot.task.sub_aid !== subAid)
|
|
930
|
-
continue;
|
|
931
|
-
const active = this.decode(snapshot);
|
|
932
|
-
if (this.isReset(active.record))
|
|
933
|
-
continue;
|
|
934
|
-
if (!active.record.base_agent_md && active.record.request_verified && ['completed', 'stopped', 'rejected'].includes(active.record.status))
|
|
935
|
-
continue;
|
|
936
|
-
if (!active.record.base_agent_md && active.record.request_verified && (active.record.operation !== operation || active.record.owner_aid !== ownerAid || active.record.new_owner_aid !== newOwnerAid || active.record.initiator_aid !== this.local()))
|
|
937
|
-
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
938
|
-
await this.upgradeLegacy(active);
|
|
939
|
-
const record = active.record;
|
|
940
|
-
if (!record.base_agent_md && record.request_verified)
|
|
941
|
-
throw ownershipFailure('unsupported_protocol', 'legacy ownership task cannot be recovered automatically');
|
|
942
|
-
if (record.sub_aid !== subAid || record.status !== 'pending' && !(record.status === 'error' && record.retryable))
|
|
943
|
-
continue;
|
|
944
|
-
const same = record.operation === operation && record.owner_aid === ownerAid && record.new_owner_aid === newOwnerAid && record.initiator_aid === this.local();
|
|
945
|
-
try {
|
|
946
|
-
ns = await this.readNs(subAid);
|
|
947
|
-
}
|
|
948
|
-
catch (error) {
|
|
949
|
-
if (!same)
|
|
950
|
-
throw error;
|
|
951
|
-
await this.recordError(active, error);
|
|
952
|
-
return ownershipTaskResult(record);
|
|
953
|
-
}
|
|
954
|
-
let base = this.isBase(record, ns);
|
|
955
|
-
if (base && record.request_verified) {
|
|
956
|
-
try {
|
|
957
|
-
await this.assertBaseMaterial(active, ns);
|
|
958
|
-
}
|
|
959
|
-
catch (error) {
|
|
960
|
-
if (!(error instanceof StateError) || error.stringCode !== 'conflict')
|
|
961
|
-
throw error;
|
|
962
|
-
base = false;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
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);
|
|
966
|
-
if (!base && !target) {
|
|
967
|
-
await this.recordError(active, ownershipFailure('conflict', 'the operation was superseded by a different relation result'));
|
|
968
|
-
const result = ownershipTaskResult(record);
|
|
969
|
-
if (same && result.status !== 'stopped')
|
|
970
|
-
return result;
|
|
971
|
-
if (result.status !== 'stopped' && result.status !== 'completed')
|
|
972
|
-
throw ownershipFailure(record.code ?? 'unavailable', record.reason ?? 'the original target cannot be confirmed', record.retryable);
|
|
973
|
-
continue;
|
|
974
|
-
}
|
|
975
|
-
if (!same || operationId && operationId !== record.operation_id)
|
|
976
|
-
throw ownershipFailure('conflict', 'a different active ownership intent already exists');
|
|
977
|
-
await this.drive(active);
|
|
978
|
-
return ownershipTaskResult(record);
|
|
979
|
-
}
|
|
980
|
-
if (operation === 'transfer' && newOwnerAid === ownerAid)
|
|
981
|
-
throw ownershipFailure('invalid', 'new owner must differ from original owner');
|
|
982
|
-
ns = await this.readNs(subAid);
|
|
983
|
-
if (operation === 'bind' && ns.lock !== 'unlocked')
|
|
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');
|
|
987
|
-
if (!ns.exists && operation !== 'bind')
|
|
988
|
-
throw ownershipFailure('conflict', 'original agent.md is missing');
|
|
989
|
-
if (operation === 'bind' && (ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).frontmatter.type : subAid === this.local() ? this.deps.currentType?.() : undefined) === 'human')
|
|
990
|
-
throw ownershipFailure('invalid', 'human cannot declare an owner');
|
|
991
|
-
if (operation === 'bind' && ns.exists && !(ns.pending || ns.ownership_status === 'pending') && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: subAid }).statement?.owner_aid === ownerAid)
|
|
992
|
-
throw ownershipFailure('conflict', 'clear the released agent.md statement before binding the same owner');
|
|
993
|
-
if (operation !== 'bind') {
|
|
994
|
-
const consensus = await this.deps.files.checkConsensus(subAid);
|
|
995
|
-
if (consensus.status !== 'consensus' || consensus.owner_aid !== ownerAid || consensus.relation_version !== ns.relation_version)
|
|
996
|
-
throw ownershipFailure(consensus.status === 'invalid' ? 'invalid' : consensus.status === 'unavailable' ? consensus.code ?? 'unavailable' : 'forbidden', 'the current original owner consensus cannot be verified', consensus.status === 'unavailable');
|
|
997
|
-
}
|
|
998
|
-
const targetVersion = ownershipInteger(ns.relation_version + (operation === 'release' ? 0 : 1), 'target_relation_version');
|
|
999
|
-
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');
|
|
1000
|
-
await this.captureBase(record, ns);
|
|
1001
|
-
if (operation !== 'release')
|
|
1002
|
-
for (const to of participants(record))
|
|
1003
|
-
if (to !== this.local())
|
|
1004
|
-
this.enqueue(record, to, operation === 'bind' ? 'bind.request' : 'transfer.request');
|
|
1005
|
-
const handle = { record, revision: null };
|
|
1006
|
-
await this.save(handle);
|
|
1007
|
-
await this.drive(handle);
|
|
1008
|
-
return ownershipTaskResult(record);
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
async decide(operationId, decision, reason) {
|
|
1012
|
-
const initial = this.load(operationId);
|
|
1013
|
-
if (!initial)
|
|
1014
|
-
throw ownershipFailure('operation_not_found', 'no local ownership request matches operation_id');
|
|
1015
|
-
if (this.isReset(initial.record))
|
|
1016
|
-
throw ownershipFailure('conflict', 'ownership task was reset');
|
|
1017
|
-
return this.runSerialized(initial.record.sub_aid, async () => {
|
|
1018
|
-
const handle = this.load(operationId);
|
|
1019
|
-
await this.upgradeLegacy(handle);
|
|
1020
|
-
const record = handle.record;
|
|
1021
|
-
if (!record.base_agent_md && record.request_verified)
|
|
1022
|
-
throw ownershipFailure('unsupported_protocol', 'legacy ownership task requires explicit migration');
|
|
1023
|
-
if (record.operation === 'release' || record.initiator_aid === this.local() || !record.request_verified)
|
|
1024
|
-
throw ownershipFailure('forbidden', 'this task is not a verified local approval request');
|
|
1025
|
-
if (record.local_decision !== 'pending') {
|
|
1026
|
-
if (record.local_decision !== decision)
|
|
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);
|
|
1030
|
-
await this.drive(handle);
|
|
1031
|
-
return ownershipTaskResult(record);
|
|
1032
|
-
}
|
|
1033
|
-
if (record.status === 'error' && record.retryable)
|
|
1034
|
-
record.status = 'pending';
|
|
1035
|
-
if (record.status !== 'pending')
|
|
1036
|
-
throw ownershipFailure('conflict', 'ownership approval stage has ended');
|
|
1037
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1038
|
-
if (!this.isBase(record, ns))
|
|
1039
|
-
throw ownershipFailure('conflict', 'the original relation changed before approval');
|
|
1040
|
-
await this.assertBaseMaterial(handle, ns);
|
|
1041
|
-
record.local_decision = decision;
|
|
1042
|
-
record.approvals[this.local()] = decision;
|
|
1043
|
-
if (decision !== 'accept' || !this.ownerFirst(record))
|
|
1044
|
-
this.enqueue(record, record.initiator_aid, `${record.operation}.${decision === 'accept' ? 'accept' : 'reject'}`, reason);
|
|
1045
|
-
if (decision === 'reject') {
|
|
1046
|
-
record.status = 'rejected';
|
|
1047
|
-
record.step = 'approval';
|
|
1048
|
-
record.retryable = false;
|
|
1049
|
-
if (reason !== undefined)
|
|
1050
|
-
record.reason = reason;
|
|
1051
|
-
}
|
|
1052
|
-
await this.save(handle);
|
|
1053
|
-
this.emit(record, decision === 'reject' ? 'ownership.rejected' : 'ownership.progress');
|
|
1054
|
-
await this.drive(handle);
|
|
1055
|
-
return ownershipTaskResult(record);
|
|
1056
|
-
});
|
|
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;
|
|
323
|
+
const agent = await this.deps.files.readAgent(value.sub_aid), target = await this.deps.files.verifyFile(agent.content, 'agent', value.sub_aid);
|
|
324
|
+
if (target.statement?.owner_aid !== value.new_owner_aid || target.statement.relation_version !== value.target_relation_version)
|
|
325
|
+
throw ownershipFailure('conflict', 'transfer notification does not match current signed relation');
|
|
326
|
+
const original = await this.deps.files.readOwner(value.owner_aid, value.sub_aid);
|
|
327
|
+
if (!original.exists)
|
|
328
|
+
return true;
|
|
329
|
+
const authorization = await this.deps.files.verifyFile(original.content, 'owner', value.sub_aid, value.owner_aid);
|
|
330
|
+
if (authorization.action !== 'transfer_owner' || authorization.statement?.relation_version !== value.base_relation_version || authorization.transfer?.new_owner_aid !== value.new_owner_aid || authorization.transfer?.target_relation_version !== value.target_relation_version)
|
|
331
|
+
throw ownershipFailure('conflict', 'old owner transfer authorization changed');
|
|
332
|
+
const record = { operation_id: value.operation_id, operation: value.operation, sub_aid: value.sub_aid, owner_aid: value.owner_aid, new_owner_aid: value.new_owner_aid, base_relation_version: value.base_relation_version, target_relation_version: value.target_relation_version, initiator_aid: this.local(), command: 'cleanup', files: { transfer: { raw: original.content, hash: ownershipSha256(original.content), condition: {}, original_owner: { exists: true, raw: original.content, sha256: ownershipSha256(original.content) } } }, notifications: [] };
|
|
333
|
+
await this.execute(record);
|
|
334
|
+
}
|
|
335
|
+
if (value.operation === 'release' && value.action === 'relation.confirmed' && from === value.owner_aid && this.local() === value.sub_aid) {
|
|
336
|
+
const current = await this.deps.files.readAgent(value.sub_aid);
|
|
337
|
+
parseOwnershipMessage(content, { authenticated_from_aid: from, local_aid: this.local(), ns: current });
|
|
338
|
+
const record = { operation_id: value.operation_id, operation: value.operation, sub_aid: value.sub_aid, owner_aid: value.owner_aid, base_relation_version: value.base_relation_version, target_relation_version: value.target_relation_version, initiator_aid: from, command: 'accept', files: {}, notifications: [] };
|
|
339
|
+
await this.capture(record);
|
|
340
|
+
await this.execute(record);
|
|
1086
341
|
}
|
|
1087
|
-
record.status = 'pending';
|
|
1088
|
-
record.retryable = true;
|
|
1089
|
-
delete record.code;
|
|
1090
|
-
delete record.reason;
|
|
1091
|
-
await this.save(handle);
|
|
1092
342
|
return true;
|
|
1093
343
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
if (
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
while (handle.record.outbox.length) {
|
|
1128
|
-
const message = handle.record.outbox[0];
|
|
1129
|
-
this.validateOutgoing(handle.record, message);
|
|
1130
|
-
const record = handle.record;
|
|
1131
|
-
if (['rejected', 'stopped', 'error'].includes(record.status) && !['bind.reject', 'transfer.reject', 'operation.stopped'].includes(message.payload.action)) {
|
|
1132
|
-
record.outbox.shift();
|
|
1133
|
-
await this.save(handle);
|
|
1134
|
-
continue;
|
|
1135
|
-
}
|
|
1136
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1137
|
-
const base = this.isBase(record, ns);
|
|
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);
|
|
1139
|
-
const requiresTarget = ['relation.confirmed', 'sub.cleaned'].includes(message.payload.action);
|
|
1140
|
-
if (requiresTarget ? !target : !base && !(record.operation === 'bind' && target && ['owner.ready', 'bind.accept'].includes(message.payload.action))) {
|
|
1141
|
-
record.outbox.shift();
|
|
1142
|
-
await this.save(handle);
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
if (base && record.request_verified)
|
|
1146
|
-
await this.assertBaseMaterial(handle, ns);
|
|
1147
|
-
if (message.payload.action === 'transfer.ready' && record.status !== 'completed') {
|
|
1148
|
-
const expiresAt = record.files.transfer?.expires_at;
|
|
1149
|
-
if (typeof expiresAt !== 'number' || !Number.isSafeInteger(expiresAt) || expiresAt <= 0)
|
|
1150
|
-
throw ownershipFailure('persistence_unavailable', 'persisted transfer authorization expiry is missing or invalid');
|
|
1151
|
-
if (expiresAt <= Math.trunc(Date.now() / 1000))
|
|
1152
|
-
throw ownershipFailure('conflict', 'transfer authorization expired');
|
|
1153
|
-
}
|
|
1154
|
-
if (!message.message_id) {
|
|
1155
|
-
message.message_id = `m-${randomUUID().replace(/-/g, '')}`;
|
|
1156
|
-
await this.save(handle);
|
|
1157
|
-
}
|
|
1158
|
-
this.assertActive(handle);
|
|
1159
|
-
await this.deps.send(message.to, message.payload, message.message_id);
|
|
1160
|
-
handle.record.outbox.shift();
|
|
1161
|
-
await this.save(handle);
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
async readNs(subAid) {
|
|
1165
|
-
const ns = await this.deps.files.readAgent(subAid);
|
|
1166
|
-
const file = ns.exists ? await this.deps.files.verifyFile(ns.content, 'agent', subAid) : undefined;
|
|
1167
|
-
if (ns.relation_version !== (file?.statement?.relation_version ?? 0))
|
|
1168
|
-
throw ownershipFailure('unavailable', 'NS relation metadata does not match agent.md', true);
|
|
1169
|
-
const currentOwner = ns.lock === 'locked' ? file?.statement?.owner_aid : null;
|
|
1170
|
-
if (ns.lock === 'locked' && (!file?.statement || file.statement.relation_version !== ns.relation_version) || ns.owner_aid !== undefined && ns.owner_aid !== currentOwner)
|
|
1171
|
-
throw ownershipFailure('unavailable', 'NS owner metadata and signed agent.md do not agree', true);
|
|
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);
|
|
1194
|
-
}
|
|
1195
|
-
async captureBase(record, ns) {
|
|
1196
|
-
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
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 };
|
|
1201
|
-
if (!statement) {
|
|
1202
|
-
record.base_owner_md = { exists: false, raw: '', sha256: '' };
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
const owner = await this.deps.files.readOwner(statement.owner_aid, record.sub_aid);
|
|
1206
|
-
if ((record.operation === 'transfer' || record.operation === 'release' && ns.lock === 'locked') && !owner.exists)
|
|
1207
|
-
throw ownershipFailure('unavailable', 'original owner.md is unavailable', true);
|
|
1208
|
-
if (owner.exists)
|
|
1209
|
-
await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, statement.owner_aid);
|
|
1210
|
-
record.base_owner_md = { exists: owner.exists, raw: owner.content, sha256: owner.exists ? ownershipSha256(owner.content) : '' };
|
|
1211
|
-
}
|
|
1212
|
-
async assertBaseMaterial(handle, ns) {
|
|
1213
|
-
const record = handle.record;
|
|
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
|
-
}
|
|
1221
|
-
const original = record.base_owner_md;
|
|
1222
|
-
if (!original)
|
|
1223
|
-
throw ownershipFailure('persistence_unavailable', 'original owner.md evidence is missing');
|
|
1224
|
-
const originalOwnerAid = record.base_agent_md.statement?.owner_aid;
|
|
1225
|
-
if (!originalOwnerAid)
|
|
1226
|
-
return;
|
|
1227
|
-
const current = await this.deps.files.readOwner(originalOwnerAid, record.sub_aid);
|
|
1228
|
-
if (current.exists === original.exists && current.content === original.raw && (current.exists ? current.sha256 : '') === original.sha256)
|
|
1229
|
-
return;
|
|
1230
|
-
if (!current.exists)
|
|
1231
|
-
throw ownershipFailure('conflict', 'the original owner.md was replaced');
|
|
1232
|
-
const key = record.operation === 'bind' ? 'owner' : record.operation === 'release' ? 'release' : 'transfer';
|
|
1233
|
-
const saved = record.files[key];
|
|
1234
|
-
if (saved && saved.raw === current.content && saved.hash === current.sha256)
|
|
1235
|
-
return;
|
|
1236
|
-
if (record.operation !== 'bind' && this.local() !== record.owner_aid && record.local_decision === 'accept') {
|
|
1237
|
-
const owner = await this.deps.files.verifyFile(current.content, 'owner', record.sub_aid, originalOwnerAid);
|
|
1238
|
-
const previous = original.exists ? parseOwnershipFile(original.raw, 'owner', { expectedSubAid: record.sub_aid, expectedOwnerAid: originalOwnerAid }) : undefined;
|
|
1239
|
-
const sameStatement = previous?.statement && Object.keys(previous.statement).every((field) => previous.statement[field] === owner.statement?.[field]);
|
|
1240
|
-
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);
|
|
1241
|
-
if (allowed && !saved) {
|
|
1242
|
-
if (record.operation === 'transfer')
|
|
1243
|
-
await this.deps.files.requireTransfer(this.fileTask(handle));
|
|
1244
|
-
record.files[key] = { raw: current.content, hash: current.sha256, condition: {}, original_owner: original, ...(owner.transfer ? { expires_at: owner.transfer.expires_at } : {}) };
|
|
1245
|
-
await this.save(handle);
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
throw ownershipFailure('conflict', 'the original owner statement was replaced');
|
|
1250
|
-
}
|
|
1251
|
-
assertAgentBaseMaterial(record, ns) {
|
|
1252
|
-
if (this.isPending(record, ns))
|
|
1253
|
-
return;
|
|
1254
|
-
const base = record.base_agent_md;
|
|
1255
|
-
if (!base || typeof base.raw !== 'string')
|
|
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');
|
|
1259
|
-
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement ?? null : null;
|
|
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;
|
|
1261
|
-
if (ns.exists !== base.exists || !same)
|
|
1262
|
-
throw ownershipFailure('conflict', 'the original ownership statement was replaced');
|
|
1263
|
-
}
|
|
1264
|
-
async processInbox(handle) {
|
|
1265
|
-
const record = handle.record;
|
|
1266
|
-
while (record.inbox.length) {
|
|
1267
|
-
const item = record.inbox[0];
|
|
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
|
-
}
|
|
1273
|
-
if (record.request_verified && ns.relation_version === record.base_relation_version && !['relation.confirmed', 'sub.cleaned'].includes(item.payload.action))
|
|
1274
|
-
await this.assertBaseMaterial(handle, ns);
|
|
1275
|
-
try {
|
|
1276
|
-
parseOwnershipMessage(item.payload, { authenticated_from_aid: item.from, local_aid: this.local(), task: record, ns });
|
|
1277
|
-
}
|
|
1278
|
-
catch (error) {
|
|
1279
|
-
record.inbox.shift();
|
|
1280
|
-
record.received.push(`${item.from}|${item.payload.operation_id}|${item.payload.action}`);
|
|
1281
|
-
if (record.status === 'pending' && ['bind.request', 'transfer.request'].includes(item.payload.action)) {
|
|
1282
|
-
record.status = 'stopped';
|
|
1283
|
-
record.step = 'conflict';
|
|
1284
|
-
record.retryable = false;
|
|
1285
|
-
}
|
|
1286
|
-
await this.save(handle);
|
|
1287
|
-
this.emit(record, 'ownership.error', { code: error instanceof StateError ? error.stringCode : 'invalid', reason: error instanceof Error ? error.message : String(error), retryable: false });
|
|
1288
|
-
continue;
|
|
1289
|
-
}
|
|
1290
|
-
const action = item.payload.action;
|
|
1291
|
-
if (action === 'owner.ready' && record.legacy_recovery && record.status === 'pending') {
|
|
1292
|
-
const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1293
|
-
if (!owner.exists)
|
|
1294
|
-
throw ownershipFailure(record.files.owner ? 'conflict' : 'unavailable', 'legacy owner confirmation declaration is missing', !record.files.owner);
|
|
1295
|
-
const file = await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
1296
|
-
if (file.action !== 'declare' || file.statement?.relation_version !== record.target_relation_version || record.files.owner && (record.files.owner.raw !== owner.content || record.files.owner.hash !== owner.sha256))
|
|
1297
|
-
throw ownershipFailure('conflict', 'legacy owner confirmation declaration changed');
|
|
1298
|
-
if (!record.files.owner)
|
|
1299
|
-
record.files.owner = { raw: owner.content, hash: owner.sha256, condition: {}, original_owner: { exists: true, raw: owner.content, sha256: owner.sha256 } };
|
|
1300
|
-
record.legacy_recovery.owner_confirmed = true;
|
|
1301
|
-
}
|
|
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
|
-
}
|
|
1314
|
-
if (!record.request_verified)
|
|
1315
|
-
await this.captureBase(record, ns);
|
|
1316
|
-
record.request_verified = true;
|
|
1317
|
-
}
|
|
1318
|
-
if (['bind.accept', 'transfer.accept', 'bind.reject', 'transfer.reject'].includes(action)) {
|
|
1319
|
-
if (record.approvals[item.from] === 'pending' && record.status === 'pending') {
|
|
1320
|
-
const rejected = action.endsWith('.reject');
|
|
1321
|
-
if (rejected) {
|
|
1322
|
-
try {
|
|
1323
|
-
if (await this.confirm(handle)) {
|
|
1324
|
-
record.inbox.shift();
|
|
1325
|
-
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
1326
|
-
await this.save(handle);
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
|
-
const observed = await this.deps.files.checkConsensus(record.sub_aid, { owner_aid: targetOwner(record), relation_version: record.target_relation_version });
|
|
1330
|
-
const targetReached = observed.status === 'consensus' && observed.owner_aid === targetOwner(record) && observed.relation_version === record.target_relation_version;
|
|
1331
|
-
if (targetReached) {
|
|
1332
|
-
record.code = 'unavailable';
|
|
1333
|
-
record.reason = 'ownership target is committed but local evidence is incomplete';
|
|
1334
|
-
record.retryable = true;
|
|
1335
|
-
record.inbox.shift();
|
|
1336
|
-
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
1337
|
-
await this.save(handle);
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
}
|
|
1341
|
-
catch (error) {
|
|
1342
|
-
if (error instanceof StateError && error.stringCode === 'persistence_unavailable')
|
|
1343
|
-
throw error;
|
|
1344
|
-
try {
|
|
1345
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1346
|
-
if (ns.relation_version === record.target_relation_version && ns.lock === 'locked' && ns.owner_aid === targetOwner(record)) {
|
|
1347
|
-
record.code = 'unavailable';
|
|
1348
|
-
record.reason = 'ownership target is committed but local evidence is incomplete';
|
|
1349
|
-
record.retryable = true;
|
|
1350
|
-
record.inbox.shift();
|
|
1351
|
-
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
1352
|
-
await this.save(handle);
|
|
1353
|
-
continue;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
catch (observationError) {
|
|
1357
|
-
if (observationError instanceof StateError && observationError.stringCode === 'persistence_unavailable')
|
|
1358
|
-
throw observationError;
|
|
1359
|
-
}
|
|
1360
|
-
record.code = error instanceof StateError ? error.stringCode : 'unavailable';
|
|
1361
|
-
record.reason = error instanceof Error ? error.message : String(error);
|
|
1362
|
-
record.retryable = true;
|
|
1363
|
-
await this.save(handle);
|
|
1364
|
-
throw error;
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
record.approvals[item.from] = rejected ? 'reject' : 'accept';
|
|
1368
|
-
if (rejected) {
|
|
1369
|
-
record.status = 'rejected';
|
|
1370
|
-
record.step = 'approval';
|
|
1371
|
-
record.retryable = false;
|
|
1372
|
-
if (item.payload.reason !== undefined)
|
|
1373
|
-
record.reason = item.payload.reason;
|
|
1374
|
-
if (record.operation === 'transfer')
|
|
1375
|
-
for (const to of participants(record))
|
|
1376
|
-
if (to !== item.from)
|
|
1377
|
-
this.enqueue(record, to, 'operation.stopped', item.payload.reason);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
if (action === 'operation.stopped' && record.status === 'pending' && !(await this.confirm(handle))) {
|
|
1382
|
-
record.status = 'stopped';
|
|
1383
|
-
record.step = 'stopped';
|
|
1384
|
-
record.retryable = false;
|
|
1385
|
-
}
|
|
1386
|
-
if (action === 'sub.cleaned') {
|
|
1387
|
-
const file = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid });
|
|
1388
|
-
if (!file.statement)
|
|
1389
|
-
record.cleanup_status = 'cleaned';
|
|
1390
|
-
}
|
|
1391
|
-
if (record.operation === 'release' && !record.request_verified) {
|
|
1392
|
-
if (ns.relation_version === 0) {
|
|
1393
|
-
const authorizationId = this.cleanupEvidence(record, { agent_md_etag: ns.etag, agent_md_sha256: ownershipSha256(ns.content), owner_md_sha256: '' }, ns.content);
|
|
1394
|
-
if (!authorizationId)
|
|
1395
|
-
throw ownershipFailure('conflict', 'no local cleanup evidence can verify the release request');
|
|
1396
|
-
const authorization = this.load(authorizationId).record;
|
|
1397
|
-
const saved = authorization.files.agent;
|
|
1398
|
-
const statement = parseOwnershipFile(saved.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1399
|
-
const etag = saved.published_etag ?? authorization.completion_evidence?.agent_md_etag;
|
|
1400
|
-
if (!etag)
|
|
1401
|
-
throw ownershipFailure('persistence_unavailable', 'historical approved agent.md ETag is missing');
|
|
1402
|
-
record.base_agent_md = { exists: true, raw: saved.raw, sha256: saved.hash, etag, statement };
|
|
1403
|
-
const owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1404
|
-
if (owner.exists)
|
|
1405
|
-
await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
1406
|
-
record.base_owner_md = { exists: owner.exists, raw: owner.content, sha256: owner.exists ? ownershipSha256(owner.content) : '' };
|
|
1407
|
-
record.cleanup_authorization_id = authorizationId;
|
|
1408
|
-
if (!(await this.confirm(handle)))
|
|
1409
|
-
throw ownershipFailure('conflict', 'no local cleanup evidence can verify the release request');
|
|
1410
|
-
}
|
|
1411
|
-
else
|
|
1412
|
-
await this.captureBase(record, ns);
|
|
1413
|
-
record.request_verified = true;
|
|
1414
|
-
}
|
|
1415
|
-
record.inbox.shift();
|
|
1416
|
-
record.received.push(`${item.from}|${item.payload.operation_id}|${action}`);
|
|
1417
|
-
const request = record.request_verified && record.local_decision === 'pending' && record.operation !== 'release' && !record.request_notified;
|
|
1418
|
-
if (request)
|
|
1419
|
-
record.request_notified = true;
|
|
1420
|
-
await this.save(handle);
|
|
1421
|
-
if (request)
|
|
1422
|
-
this.emit(record, 'ownership.request');
|
|
1423
|
-
if (record.status === 'rejected')
|
|
1424
|
-
this.emit(record, 'ownership.rejected');
|
|
1425
|
-
if (record.status === 'stopped')
|
|
1426
|
-
this.emit(record, 'ownership.stopped');
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
fileTask(handle) {
|
|
1430
|
-
const record = handle.record;
|
|
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,
|
|
1433
|
-
new_owner_aid: record.new_owner_aid, base_relation_version: record.base_relation_version, target_relation_version: record.target_relation_version,
|
|
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,
|
|
1435
|
-
assertActive: () => this.assertActive(handle),
|
|
1436
|
-
persist: async () => {
|
|
1437
|
-
record.authorization_sha256 = task.authorization_sha256;
|
|
1438
|
-
record.expires_at = task.expires_at;
|
|
1439
|
-
if (record.operation === 'transfer' && task.authorization_sha256 !== undefined && task.expires_at !== undefined && record.step === 'approval')
|
|
1440
|
-
record.step = this.local() === targetOwner(record) ? 'owner' : 'agent';
|
|
1441
|
-
await this.save(handle);
|
|
1442
|
-
} };
|
|
1443
|
-
return task;
|
|
1444
|
-
}
|
|
1445
|
-
assertActive(handle) {
|
|
1446
|
-
const current = this.load(handle.record.operation_id);
|
|
1447
|
-
if (!current || this.isReset(current.record) || current.revision !== handle.revision)
|
|
1448
|
-
throw ownershipFailure('conflict', 'ownership task changed before external action');
|
|
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
|
-
}
|
|
1453
|
-
async drive(handle) {
|
|
1454
|
-
if (this.isReset(handle.record))
|
|
1455
|
-
return;
|
|
1456
|
-
await this.upgradeLegacy(handle);
|
|
1457
|
-
const record = handle.record;
|
|
1458
|
-
if (!record.base_agent_md && record.request_verified)
|
|
1459
|
-
return;
|
|
1460
|
-
if (record.status === 'error' && record.retryable) {
|
|
1461
|
-
record.status = 'pending';
|
|
1462
|
-
await this.save(handle);
|
|
1463
|
-
}
|
|
344
|
+
async checkOwnershipConsensus(subAid = this.local()) { return this.deps.files.checkConsensus(subAid); }
|
|
345
|
+
async bindLocalSubaid(sub) { return this.localSubaid(sub); }
|
|
346
|
+
async transferLocalSubaid(sub, next) { return this.localSubaid(sub, next); }
|
|
347
|
+
async localSubaid(sub, next) {
|
|
348
|
+
const actors = [this, sub, ...(next ? [next] : [])], aids = actors.map((actor) => actor.local()), [ownerAid, subAid, nextAid] = aids;
|
|
349
|
+
const guard = () => { for (const [index, actor] of actors.entries()) {
|
|
350
|
+
actor.deps.assertCurrent?.();
|
|
351
|
+
if (actor.local() !== aids[index] || actor.deps.currentType?.() !== 'agent')
|
|
352
|
+
throw ownershipFailure('conflict', 'local operation source identity changed');
|
|
353
|
+
} };
|
|
354
|
+
if (new Set(aids).size !== aids.length || actors.some((actor) => actor.deps.currentType?.() !== 'agent'))
|
|
355
|
+
throw ownershipFailure('forbidden', 'local ownership requires distinct agent identities');
|
|
356
|
+
guard();
|
|
357
|
+
const operation = next ? 'transfer' : 'bind';
|
|
358
|
+
const consensus = await this.deps.files.checkConsensus(subAid, { owner_aid: nextAid ?? ownerAid, relation_version: 1 });
|
|
359
|
+
guard();
|
|
360
|
+
const agent = await this.deps.files.readAgent(subAid);
|
|
361
|
+
guard();
|
|
362
|
+
const parsed = agent.exists ? await this.deps.files.verifyFile(agent.content, 'agent', subAid) : undefined;
|
|
363
|
+
const committed = consensus.status === 'consensus' && consensus.owner_aid === (nextAid ?? ownerAid) && Boolean(parsed?.statement);
|
|
364
|
+
const pending = agent.lock === 'unlocked' && (agent.pending || agent.ownership_status === 'pending') && parsed?.statement;
|
|
365
|
+
if (!committed && (operation === 'bind' ? !agent.exists || parsed?.frontmatter.type !== 'agent' || agent.lock !== 'unlocked' || Boolean(parsed.statement) && !pending : consensus.status !== 'consensus' || consensus.owner_aid !== ownerAid))
|
|
366
|
+
throw ownershipFailure('conflict', 'local ownership base does not match');
|
|
367
|
+
const baseVersion = committed ? parsed.statement.relation_version - 1 : pending ? parsed.statement.relation_version - 1 : agent.relation_version;
|
|
368
|
+
const record = { operation_id: randomUUID(), operation, sub_aid: subAid, owner_aid: ownerAid, ...(nextAid ? { new_owner_aid: nextAid } : {}), initiator_aid: ownerAid, base_relation_version: baseVersion, target_relation_version: baseVersion + 1, command: 'local', files: {}, notifications: [] };
|
|
369
|
+
await this.capture(record);
|
|
370
|
+
guard();
|
|
371
|
+
const task = () => {
|
|
372
|
+
const value = this.fileTask(record);
|
|
373
|
+
value.assertActive = guard;
|
|
374
|
+
value.persist = async () => { guard(); };
|
|
375
|
+
return value;
|
|
376
|
+
};
|
|
1464
377
|
try {
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
1494
|
-
if (record.status === 'rejected' || record.status === 'stopped' || record.status === 'error')
|
|
1495
|
-
return;
|
|
1496
|
-
if (!record.request_verified)
|
|
1497
|
-
return;
|
|
1498
|
-
if (record.status === 'completed') {
|
|
1499
|
-
const observed = await this.deps.files.checkConsensus(record.sub_aid);
|
|
1500
|
-
await this.resumeSubCleanup(handle, observed);
|
|
1501
|
-
if (await this.confirm(handle, observed))
|
|
1502
|
-
await this.cleanup(handle);
|
|
1503
|
-
await this.flush(handle);
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
if (record.operation === 'release') {
|
|
1507
|
-
if (await this.confirm(handle)) {
|
|
1508
|
-
await this.cleanup(handle);
|
|
1509
|
-
await this.flush(handle);
|
|
1510
|
-
return;
|
|
378
|
+
if (!committed) {
|
|
379
|
+
if (next) {
|
|
380
|
+
await this.deps.files.transferOwneraid(task());
|
|
381
|
+
guard();
|
|
1511
382
|
}
|
|
1512
|
-
if (this.
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
await this.deps.files.revokeSubaid(this.fileTask(handle));
|
|
383
|
+
if (!await (next ?? this).hasLocalDeclaration(record, 'owner')) {
|
|
384
|
+
await (next ?? this).deps.files.declareSubaid(task());
|
|
385
|
+
guard();
|
|
1516
386
|
}
|
|
1517
|
-
if (await
|
|
1518
|
-
await
|
|
1519
|
-
|
|
1520
|
-
return;
|
|
1521
|
-
}
|
|
1522
|
-
if (record.local_decision !== 'accept')
|
|
1523
|
-
return;
|
|
1524
|
-
if (await this.confirm(handle)) {
|
|
1525
|
-
await this.cleanup(handle);
|
|
1526
|
-
await this.flush(handle);
|
|
1527
|
-
return;
|
|
1528
|
-
}
|
|
1529
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1530
|
-
if (!this.isBase(record, ns)) {
|
|
1531
|
-
throw ownershipFailure('conflict', 'the original relation was replaced');
|
|
1532
|
-
}
|
|
1533
|
-
await this.assertBaseMaterial(handle, ns);
|
|
1534
|
-
if (record.expires_at !== undefined && record.expires_at <= Math.trunc(Date.now() / 1000))
|
|
1535
|
-
throw ownershipFailure('conflict', 'transfer authorization expired');
|
|
1536
|
-
const approved = participants(record).every((name) => record.approvals[name] === 'accept');
|
|
1537
|
-
if (record.operation === 'bind' && !approved || record.operation === 'transfer' && this.local() === record.owner_aid && !approved)
|
|
1538
|
-
return;
|
|
1539
|
-
if (record.legacy_recovery && this.local() === record.sub_aid && !record.legacy_recovery.owner_confirmed)
|
|
1540
|
-
return;
|
|
1541
|
-
delete record.code;
|
|
1542
|
-
delete record.reason;
|
|
1543
|
-
record.retryable = true;
|
|
1544
|
-
if (record.operation === 'transfer' && this.local() === record.owner_aid) {
|
|
1545
|
-
record.step = 'transfer';
|
|
1546
|
-
await this.save(handle);
|
|
1547
|
-
await this.deps.files.transferOwneraid(this.fileTask(handle));
|
|
1548
|
-
for (const to of participants(record))
|
|
1549
|
-
this.enqueue(record, to, 'transfer.ready');
|
|
1550
|
-
await this.save(handle);
|
|
1551
|
-
await this.flush(handle);
|
|
1552
|
-
}
|
|
1553
|
-
if (record.operation === 'transfer') {
|
|
1554
|
-
const original = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1555
|
-
if (original.exists && (await this.deps.files.verifyFile(original.content, 'owner', record.sub_aid, record.owner_aid)).action === 'declare') {
|
|
1556
|
-
if (record.authorization_sha256 !== undefined)
|
|
1557
|
-
throw ownershipFailure('conflict', 'transfer authorization was withdrawn');
|
|
1558
|
-
return;
|
|
387
|
+
if (!await sub.hasLocalDeclaration(record, 'agent')) {
|
|
388
|
+
await sub.deps.files.declareOwneraid(task());
|
|
389
|
+
guard();
|
|
1559
390
|
}
|
|
1560
|
-
await this.deps.files.requireTransfer(this.fileTask(handle));
|
|
1561
391
|
}
|
|
1562
|
-
if (
|
|
1563
|
-
record.
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
await this.save(handle);
|
|
1569
|
-
await this.flush(handle);
|
|
392
|
+
else if (next && record.base_owner_md?.exists) {
|
|
393
|
+
const raw = record.base_owner_md.raw, authorization = await this.deps.files.verifyFile(raw, 'owner', subAid, ownerAid);
|
|
394
|
+
guard();
|
|
395
|
+
if (authorization.action !== 'transfer_owner' || authorization.statement?.relation_version !== record.base_relation_version || authorization.transfer?.new_owner_aid !== nextAid || authorization.transfer.target_relation_version !== record.target_relation_version)
|
|
396
|
+
throw ownershipFailure('conflict', 'old owner transfer authorization changed');
|
|
397
|
+
record.files.transfer = { raw, hash: ownershipSha256(raw), condition: {}, original_owner: { exists: true, raw, sha256: ownershipSha256(raw) } };
|
|
1570
398
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
await this.save(handle);
|
|
1579
|
-
await this.deps.files.declareOwneraid(this.fileTask(handle));
|
|
399
|
+
const confirmed = await this.deps.files.checkConsensus(subAid, { owner_aid: nextAid ?? ownerAid, relation_version: record.target_relation_version });
|
|
400
|
+
guard();
|
|
401
|
+
if (confirmed.status !== 'consensus' || confirmed.owner_aid !== (nextAid ?? ownerAid) || confirmed.relation_version !== record.target_relation_version)
|
|
402
|
+
throw ownershipFailure('unavailable', 'local files do not yet confirm ownership', true);
|
|
403
|
+
if (next && record.files.transfer) {
|
|
404
|
+
await this.deps.files.cleanupOwnerFile(task());
|
|
405
|
+
guard();
|
|
1580
406
|
}
|
|
1581
|
-
|
|
1582
|
-
await this.flush(handle);
|
|
1583
|
-
if (completed)
|
|
1584
|
-
await this.cleanup(handle);
|
|
407
|
+
return this.result(record, 'completed');
|
|
1585
408
|
}
|
|
1586
409
|
catch (error) {
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
async confirm(handle, observed) {
|
|
1591
|
-
const record = handle.record;
|
|
1592
|
-
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 });
|
|
1593
|
-
if (result.status === 'invalid' || result.status === 'unavailable')
|
|
1594
|
-
throw ownershipFailure(result.code ?? result.status, result.reason ?? 'ownership consensus cannot be verified', result.retryable);
|
|
1595
|
-
if (record.operation === 'release' && record.status === 'completed' && result.status === 'unbound' && result.relation_version === 0)
|
|
1596
|
-
return true;
|
|
1597
|
-
let cleaned = false;
|
|
1598
|
-
if (record.operation === 'release' && result.status === 'unbound' && result.relation_version === 0) {
|
|
1599
|
-
const began = Date.now();
|
|
1600
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1601
|
-
if (!ns.exists || !ns.etag)
|
|
1602
|
-
throw ownershipFailure('conflict', 'current signed cleanup document is missing');
|
|
1603
|
-
if (ns.etag !== result.agent_md_etag || ownershipSha256(ns.content) !== result.agent_md_sha256)
|
|
1604
|
-
throw ownershipFailure('unavailable', 'agent.md changed during cleanup confirmation', true);
|
|
1605
|
-
if (record.local_roles.includes('owner') && record.files.release) {
|
|
1606
|
-
const source = record.files.release, owner = await this.deps.files.readOwner(record.owner_aid, record.sub_aid);
|
|
1607
|
-
if (owner.exists && owner.content === source.raw && owner.sha256 === source.hash) {
|
|
1608
|
-
await this.deps.files.verifyFile(owner.content, 'owner', record.sub_aid, record.owner_aid);
|
|
1609
|
-
const after = await this.readNs(record.sub_aid);
|
|
1610
|
-
if (after.etag !== ns.etag || after.content !== ns.content || after.relation_version !== 0 || after.lock !== 'unlocked')
|
|
1611
|
-
throw ownershipFailure('unavailable', 'agent.md changed during release confirmation', true);
|
|
1612
|
-
result.owner_md_sha256 = owner.sha256;
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
const authorization = this.cleanupEvidence(record, result, ns.content);
|
|
1616
|
-
if (!authorization)
|
|
1617
|
-
throw ownershipFailure('conflict', 'no local cleanup original can confirm this release task');
|
|
1618
|
-
if (Date.now() - began > 30_000)
|
|
1619
|
-
throw ownershipFailure('unavailable', 'cleanup confirmation verification window elapsed', true);
|
|
1620
|
-
if (authorization !== record.operation_id)
|
|
1621
|
-
record.cleanup_authorization_id = authorization;
|
|
1622
|
-
record.local_decision = 'accept';
|
|
1623
|
-
record.approvals[this.local()] = 'accept';
|
|
1624
|
-
record.cleanup_status = 'cleaned';
|
|
1625
|
-
cleaned = true;
|
|
1626
|
-
}
|
|
1627
|
-
const reached = cleaned || result.relation_version === (record.operation === 'release' ? record.base_relation_version : record.target_relation_version) && (record.operation === 'release' ? result.status === 'released' && result.lock === 'unlocked' : result.status === 'consensus' && result.owner_aid === targetOwner(record));
|
|
1628
|
-
if (!reached)
|
|
1629
|
-
return false;
|
|
1630
|
-
if (record.operation === 'release' && !cleaned) {
|
|
1631
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1632
|
-
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1633
|
-
if (ns.etag !== result.agent_md_etag || ownershipSha256(ns.content) !== result.agent_md_sha256)
|
|
1634
|
-
throw ownershipFailure('unavailable', 'agent.md changed during release confirmation', true);
|
|
1635
|
-
if (statement?.owner_aid !== record.owner_aid || statement.relation_version !== record.base_relation_version)
|
|
1636
|
-
return false;
|
|
1637
|
-
if (record.base_agent_md)
|
|
1638
|
-
this.assertAgentBaseMaterial(record, ns);
|
|
1639
|
-
if (record.files.release && result.owner_md_sha256 && record.files.release.hash !== result.owner_md_sha256)
|
|
1640
|
-
throw ownershipFailure('conflict', 'current release material does not belong to this task');
|
|
1641
|
-
}
|
|
1642
|
-
if (record.operation === 'release' && record.local_roles.includes('owner') && !record.files.release)
|
|
1643
|
-
throw ownershipFailure('conflict', 'local release file evidence is missing');
|
|
1644
|
-
if (record.operation === 'transfer' && record.local_roles.includes('owner') && !record.files.transfer)
|
|
1645
|
-
throw ownershipFailure('conflict', 'local transfer file evidence is missing');
|
|
1646
|
-
if (record.operation === 'release' && this.local() === record.sub_aid && record.local_decision !== 'accept') {
|
|
1647
|
-
const authorization = await this.cleanupAuthorization(record);
|
|
1648
|
-
if (authorization) {
|
|
1649
|
-
record.local_decision = 'accept';
|
|
1650
|
-
record.approvals[this.local()] = 'accept';
|
|
1651
|
-
record.cleanup_authorization_id = authorization.operation_id;
|
|
1652
|
-
await this.save(handle);
|
|
1653
|
-
}
|
|
1654
|
-
const ns = await this.deps.files.readAgent(record.sub_aid);
|
|
1655
|
-
if (!authorization && parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement) {
|
|
1656
|
-
record.step = 'cleanup';
|
|
1657
|
-
if (record.cleanup_status !== 'action_required') {
|
|
1658
|
-
record.cleanup_status = 'action_required';
|
|
1659
|
-
await this.save(handle);
|
|
1660
|
-
this.emit(record, 'ownership.action_required');
|
|
1661
|
-
}
|
|
1662
|
-
return false;
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
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)))
|
|
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');
|
|
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)
|
|
1672
|
-
throw ownershipFailure('conflict', 'published agent.md ETag changed');
|
|
1673
|
-
if (record.status !== 'completed') {
|
|
1674
|
-
record.status = 'completed';
|
|
1675
|
-
record.step = 'confirmed';
|
|
1676
|
-
record.verified_relation_version = record.target_relation_version;
|
|
1677
|
-
record.completion_evidence = { ...result };
|
|
1678
|
-
delete record.code;
|
|
1679
|
-
delete record.reason;
|
|
1680
|
-
record.retryable = false;
|
|
1681
|
-
if (record.operation === 'release' && this.local() === record.owner_aid)
|
|
1682
|
-
this.enqueue(record, record.sub_aid, 'relation.confirmed');
|
|
1683
|
-
else if (record.operation !== 'release' && this.local() === record.sub_aid)
|
|
1684
|
-
for (const to of participants(record))
|
|
1685
|
-
this.enqueue(record, to, 'relation.confirmed');
|
|
1686
|
-
await this.save(handle);
|
|
1687
|
-
this.emit(record, 'ownership.completed');
|
|
1688
|
-
}
|
|
1689
|
-
return true;
|
|
1690
|
-
}
|
|
1691
|
-
cleanupEvidence(record, evidence, raw) {
|
|
1692
|
-
const release = record.files.release;
|
|
1693
|
-
if (record.local_roles.includes('owner') && release && release.hash === evidence.owner_md_sha256)
|
|
1694
|
-
return record.operation_id;
|
|
1695
|
-
if (!record.local_roles.includes('sub'))
|
|
1696
|
-
return undefined;
|
|
1697
|
-
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));
|
|
1698
|
-
if (matches(record.files.agent))
|
|
1699
|
-
return record.operation_id;
|
|
1700
|
-
for (const snapshot of this.list(true)) {
|
|
1701
|
-
if (!['bind', 'transfer'].includes(String(snapshot.task.operation)) || record.cleanup_authorization_id && snapshot.operation_id !== record.cleanup_authorization_id)
|
|
1702
|
-
continue;
|
|
1703
|
-
let candidate;
|
|
1704
|
-
try {
|
|
1705
|
-
candidate = this.decode(snapshot).record;
|
|
1706
|
-
}
|
|
1707
|
-
catch (error) {
|
|
1708
|
-
if (snapshot.task.sub_aid === record.sub_aid)
|
|
1709
|
-
throw error;
|
|
1710
|
-
else
|
|
1711
|
-
continue;
|
|
1712
|
-
}
|
|
1713
|
-
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))
|
|
1714
|
-
continue;
|
|
1715
|
-
const statement = candidate.files.agent && parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1716
|
-
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])))
|
|
1717
|
-
return candidate.operation_id;
|
|
1718
|
-
}
|
|
1719
|
-
return undefined;
|
|
1720
|
-
}
|
|
1721
|
-
async resumeSubCleanup(handle, observed) {
|
|
1722
|
-
const record = handle.record;
|
|
1723
|
-
if (!['bind', 'transfer'].includes(record.operation) || !record.local_roles.includes('sub') || record.local_decision !== 'accept' || record.release_cleanup?.cleaned)
|
|
1724
|
-
return;
|
|
1725
|
-
if (record.release_cleanup && observed.status === 'unbound' && observed.relation_version === 0) {
|
|
1726
|
-
const saved = record.release_cleanup.files.agent;
|
|
1727
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1728
|
-
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)
|
|
1729
|
-
throw ownershipFailure('conflict', 'historical cleanup original does not match current material');
|
|
1730
|
-
saved.published_etag = ns.etag;
|
|
1731
|
-
record.release_cleanup.cleaned = true;
|
|
1732
|
-
await this.save(handle);
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
if (observed.status !== 'released' || observed.relation_version !== record.target_relation_version)
|
|
1736
|
-
return;
|
|
1737
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1738
|
-
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)
|
|
1739
|
-
throw ownershipFailure('unavailable', 'NS changed during historical cleanup observation', true);
|
|
1740
|
-
const statement = parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1741
|
-
const original = record.files.agent && parseOwnershipFile(record.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1742
|
-
if (!statement || !original || Object.keys(original).some((field) => original[field] !== statement[field]))
|
|
1743
|
-
throw ownershipFailure('conflict', 'historical cleanup fields belong to another relation');
|
|
1744
|
-
if (!record.release_cleanup) {
|
|
1745
|
-
record.release_cleanup = { files: {}, cleaned: false };
|
|
1746
|
-
await this.save(handle);
|
|
1747
|
-
}
|
|
1748
|
-
await this.deps.files.revokeOwneraid({ operation: 'release', sub_aid: record.sub_aid, owner_aid: targetOwner(record),
|
|
1749
|
-
base_relation_version: record.target_relation_version, target_relation_version: observed.relation_version,
|
|
1750
|
-
files: record.release_cleanup.files, expected_agent_statement: original, assertActive: () => this.assertActive(handle), persist: () => this.save(handle) });
|
|
1751
|
-
record.release_cleanup.cleaned = true;
|
|
1752
|
-
await this.save(handle);
|
|
1753
|
-
this.emit(record, 'ownership.progress');
|
|
1754
|
-
}
|
|
1755
|
-
async cleanupAuthorization(task) {
|
|
1756
|
-
const ns = await this.readNs(task.sub_aid);
|
|
1757
|
-
const statement = ns.exists ? parseOwnershipFile(ns.content, 'agent', { expectedSubAid: task.sub_aid }).statement : undefined;
|
|
1758
|
-
if (!statement)
|
|
1759
|
-
return undefined;
|
|
1760
|
-
for (const snapshot of this.list(true)) {
|
|
1761
|
-
if (snapshot.task.sub_aid !== task.sub_aid)
|
|
1762
|
-
continue;
|
|
1763
|
-
const candidate = this.decode(snapshot).record;
|
|
1764
|
-
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) {
|
|
1765
|
-
const original = parseOwnershipFile(candidate.files.agent.raw, 'agent', { expectedSubAid: task.sub_aid }).statement;
|
|
1766
|
-
const fixed = task.base_agent_md?.statement;
|
|
1767
|
-
if (original && Object.keys(original).every((field) => original[field] === statement[field]) && (!fixed || Object.keys(fixed).every((field) => fixed[field] === original[field])))
|
|
1768
|
-
return candidate;
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
return undefined;
|
|
1772
|
-
}
|
|
1773
|
-
async cleanup(handle) {
|
|
1774
|
-
const record = handle.record;
|
|
1775
|
-
if (record.operation === 'bind')
|
|
1776
|
-
return;
|
|
1777
|
-
if (record.operation === 'release' && this.local() === record.sub_aid && record.cleanup_status !== 'cleaned') {
|
|
1778
|
-
const ns = await this.readNs(record.sub_aid);
|
|
1779
|
-
if (ns.exists && ns.lock === 'unlocked' && ns.relation_version === 0 && !parseOwnershipFile(ns.content, 'agent', { expectedSubAid: record.sub_aid }).statement) {
|
|
1780
|
-
if (!this.cleanupEvidence(record, { agent_md_sha256: ownershipSha256(ns.content), agent_md_etag: ns.etag, owner_md_sha256: '' }, ns.content))
|
|
1781
|
-
throw ownershipFailure('conflict', 'current cleanup does not match a locally saved original');
|
|
1782
|
-
record.cleanup_status = 'cleaned';
|
|
1783
|
-
await this.save(handle);
|
|
1784
|
-
}
|
|
1785
|
-
else {
|
|
1786
|
-
const authorization = await this.cleanupAuthorization(record);
|
|
1787
|
-
if (!authorization || record.cleanup_authorization_id && authorization.operation_id !== record.cleanup_authorization_id) {
|
|
1788
|
-
if (record.cleanup_status !== 'action_required') {
|
|
1789
|
-
record.cleanup_status = 'action_required';
|
|
1790
|
-
await this.save(handle);
|
|
1791
|
-
this.emit(record, 'ownership.action_required');
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
else {
|
|
1795
|
-
record.cleanup_authorization_id = authorization.operation_id;
|
|
1796
|
-
await this.save(handle);
|
|
1797
|
-
const task = this.fileTask(handle);
|
|
1798
|
-
task.expected_agent_statement = parseOwnershipFile(authorization.files.agent.raw, 'agent', { expectedSubAid: record.sub_aid }).statement;
|
|
1799
|
-
await this.deps.files.revokeOwneraid(task);
|
|
1800
|
-
record.cleanup_status = 'cleaned';
|
|
1801
|
-
this.enqueue(record, record.owner_aid, 'sub.cleaned');
|
|
1802
|
-
await this.save(handle);
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
if (this.local() === record.owner_aid && !record.files.cleanup?.deleted)
|
|
1807
|
-
await this.deps.files.cleanupOwnerFile(this.fileTask(handle));
|
|
1808
|
-
}
|
|
1809
|
-
emit(record, event, extra = {}) {
|
|
1810
|
-
const payload = { ...ownershipTaskResult(record), local_roles: [...record.local_roles], ...extra };
|
|
1811
|
-
if (event === 'ownership.request')
|
|
1812
|
-
payload.requester_aid = record.initiator_aid;
|
|
1813
|
-
if (record.completion_evidence !== undefined)
|
|
1814
|
-
payload.relation_version = record.completion_evidence.relation_version;
|
|
1815
|
-
queueMicrotask(() => { try {
|
|
1816
|
-
const pending = this.deps.emit(event, payload);
|
|
1817
|
-
if (pending)
|
|
1818
|
-
void pending.catch(() => { });
|
|
410
|
+
if (error instanceof StateError && ['conflict', 'owner_locked'].includes(error.stringCode))
|
|
411
|
+
throw error;
|
|
412
|
+
return this.result(record, 'error', error);
|
|
1819
413
|
}
|
|
1820
|
-
catch { } });
|
|
1821
414
|
}
|
|
1822
415
|
}
|
|
1823
416
|
//# sourceMappingURL=ownership-workflow.js.map
|