@entrorove/skill 0.1.23 → 0.1.25
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/AGREEMENT.md +6 -6
- package/CAPABILITY-MAP.md +6 -0
- package/CHANGELOG.md +10 -0
- package/SKILL.md +18 -4
- package/archguard-runtime-build.mjs +15 -0
- package/blueprint-criterion-runner.mjs +110 -0
- package/blueprint-criterion-wire.mjs +78 -0
- package/calctool-runtime-build.mjs +59 -0
- package/calctool-runtime-entry.mjs +92 -0
- package/cli.mjs +35 -83
- package/local-approved-plan.mjs +74 -15
- package/local-archguard-ast.mjs +371 -0
- package/local-archguard-checkpoint.mjs +123 -0
- package/local-archguard-components.mjs +177 -0
- package/local-archguard-contract.mjs +109 -0
- package/local-archguard-contracts.mjs +42 -0
- package/local-archguard-evaluate.mjs +120 -0
- package/local-archguard-language-ast.mjs +165 -0
- package/local-archguard-mutation-fence.mjs +28 -0
- package/local-archguard-operations.mjs +25 -0
- package/local-archguard-scope.mjs +64 -0
- package/local-archguard-signing.mjs +52 -0
- package/local-archguard-snapshot.mjs +126 -0
- package/local-archguard-state.mjs +200 -0
- package/local-archguard.mjs +74 -0
- package/local-budget-git-attributes.mjs +49 -0
- package/local-budget-git-loader.mjs +61 -0
- package/local-budget-git-native-attributes.mjs +81 -0
- package/local-budget-git-native-darwin.mjs +104 -0
- package/local-budget-git-native-linux.mjs +93 -0
- package/local-budget-git-native-reads.mjs +88 -0
- package/local-budget-git-native-request.mjs +53 -0
- package/local-budget-git-native-runtime.mjs +18 -0
- package/local-budget-git-native.c +313 -0
- package/local-budget-git-native.mjs +109 -0
- package/local-budget-git-outcome.mjs +33 -0
- package/local-budget-git-scratch-loader.mjs +158 -0
- package/local-budget-git-scratch.mjs +112 -0
- package/local-budget-git-system-attributes.mjs +44 -0
- package/local-budget-git.mjs +92 -0
- package/local-budget-grants.mjs +82 -0
- package/local-budget-handoff-completion.mjs +70 -0
- package/local-budget-handoff-contracts.mjs +53 -0
- package/local-budget-handoff.mjs +229 -0
- package/local-budget-lock.mjs +55 -0
- package/local-budget-reads.mjs +135 -0
- package/local-budget-renewal.mjs +86 -34
- package/local-budget-source-bindings.mjs +51 -0
- package/local-budget-store.mjs +131 -0
- package/local-budget-transition.mjs +46 -0
- package/local-budget.mjs +185 -0
- package/local-calctool-cli.mjs +24 -0
- package/local-calctool-contracts.mjs +42 -0
- package/local-calctool-runtime.mjs +10 -0
- package/local-calctool-storage.mjs +101 -0
- package/local-calctool.mjs +103 -0
- package/local-chain-bindings.mjs +93 -0
- package/local-chain-broker.mjs +61 -0
- package/local-chain-contracts.mjs +21 -0
- package/local-chain-controller.mjs +321 -0
- package/local-chain-human.mjs +48 -0
- package/local-chain-lock.mjs +54 -0
- package/local-chain-operations.mjs +7 -0
- package/local-chain-plan.mjs +232 -0
- package/local-chain-release-labels.mjs +12 -0
- package/local-chain-results.mjs +75 -0
- package/local-cli.mjs +65 -24
- package/local-contract-types.mjs +46 -0
- package/local-contracts.mjs +126 -0
- package/local-credential.mjs +101 -0
- package/local-decimal.mjs +85 -0
- package/local-error.mjs +23 -0
- package/local-evidence-verification.mjs +199 -0
- package/local-execution.mjs +115 -304
- package/local-final-gate16-bundle.mjs +21 -0
- package/local-final-gate16-checks.mjs +88 -0
- package/local-final-gate16-contract.mjs +15 -0
- package/local-final-gate16-plan.mjs +66 -0
- package/local-final-gate16-proof.mjs +75 -0
- package/local-final-gate16-release.mjs +17 -0
- package/local-final-gate16-state.mjs +70 -0
- package/local-final-gate16-worker.mjs +19 -0
- package/local-final-gate16.mjs +137 -0
- package/local-formula-ast.mjs +106 -0
- package/local-formula-engine.mjs +125 -0
- package/local-formula-tests.mjs +61 -0
- package/local-formula.mjs +25 -125
- package/local-integrity.mjs +22 -41
- package/local-invocations.mjs +142 -0
- package/local-ledger.mjs +15 -9
- package/local-merge-archguard-verification.d.mts +44 -0
- package/local-merge-archguard-verification.mjs +240 -0
- package/local-merge-archguard.mjs +138 -0
- package/local-merge-artifacts.mjs +86 -0
- package/local-merge-authority.mjs +89 -0
- package/local-merge-contracts.mjs +56 -0
- package/local-merge-controller.mjs +170 -0
- package/local-merge-head.mjs +56 -0
- package/local-merge-journal.mjs +77 -0
- package/local-merge-mutation.mjs +97 -0
- package/local-merge-rollback.mjs +126 -0
- package/local-merge-snapshot-rollback.mjs +26 -0
- package/local-merge-storage.mjs +103 -0
- package/local-merge-targets.mjs +47 -0
- package/local-merge-transaction.mjs +133 -0
- package/local-mutation-audit.mjs +34 -0
- package/local-mutation-authority.mjs +103 -0
- package/local-mutation-batch-receipt.mjs +84 -0
- package/local-mutation-batch.mjs +143 -0
- package/local-mutation-binding.mjs +89 -0
- package/local-mutation-files.mjs +270 -0
- package/local-mutation-pass-store.mjs +63 -0
- package/local-mutation-snapshot.mjs +200 -0
- package/local-mutation-wire.mjs +89 -0
- package/local-probe.mjs +133 -11
- package/local-scope-candidate.mjs +28 -0
- package/local-scope-context.mjs +28 -0
- package/local-scope-files.mjs +106 -0
- package/local-scope-git-attributes.mjs +200 -0
- package/local-scope-git-comparison.mjs +8 -0
- package/local-scope-git-history.mjs +72 -0
- package/local-scope-git-reads.mjs +66 -0
- package/local-scope-git.mjs +124 -0
- package/local-scope-repository.mjs +249 -0
- package/local-scope-signature.mjs +70 -0
- package/local-scope-snapshot.mjs +39 -0
- package/local-scope-state.mjs +204 -0
- package/local-scope-workspaces.mjs +83 -0
- package/local-swarm-authority.mjs +143 -0
- package/local-swarm-baseline.mjs +32 -0
- package/local-swarm-completion.mjs +102 -0
- package/local-swarm-contracts.mjs +47 -0
- package/local-swarm-mutation.mjs +142 -0
- package/local-swarm-priority.mjs +237 -0
- package/local-swarm-state.mjs +102 -0
- package/local-swarm-storage.mjs +135 -0
- package/local-swarm.mjs +179 -0
- package/local-task-handoff-completion-wire.mjs +71 -0
- package/local-task-handoff-contracts.mjs +66 -0
- package/local-task-handoff-fence.mjs +478 -0
- package/local-task-handoff-observation.mjs +41 -0
- package/local-task-handoff-overlay-wire.mjs +94 -0
- package/local-task-handoff-read-authority.mjs +236 -0
- package/local-task-handoff-storage.mjs +73 -0
- package/local-task-handoff-write-archive.mjs +80 -0
- package/local-task-handoff-write-constraints.mjs +66 -0
- package/local-task-handoff-write-receipts.mjs +99 -0
- package/local-task-handoff-write-wire.mjs +99 -0
- package/local-task-routing.mjs +202 -22
- package/manifest.json +1 -1
- package/package.json +164 -3
- package/release-state.json +164 -18
- package/release.mjs +242 -4
- package/releases.json +12 -0
- package/remote-client.mjs +127 -36
- package/remote-transport.mjs +80 -0
- package/review-client.mjs +38 -0
- package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
- package/runtime/calctool-runtime-v7.0.43.mjs +1910 -0
- package/runtime/git-native-darwin-v1.mjs +5 -0
- package/runtime/git-native-linux-v1.mjs +5 -0
- package/task-message-cli.mjs +47 -0
- package/task-message-entry.mjs +159 -0
- package/workflow-local-client.mjs +35 -0
- package/workflow-local-contracts.mjs +58 -0
- package/workflow-local-request.mjs +53 -0
- package/workflow-local-runner.mjs +350 -0
package/local-swarm.mjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
4
|
+
import { readSwarmCloudAuthority, verifyStoredSwarmGrant } from './local-swarm-authority.mjs';
|
|
5
|
+
import { appendSwarmWorker, reportSwarmWorker, validateSwarmState } from './local-swarm-state.mjs';
|
|
6
|
+
import { readSwarmOwnership, readSwarmJson, swarmOwnershipRecord, swarmStorage, validateSwarmOwnership, writeSwarmJson } from './local-swarm-storage.mjs';
|
|
7
|
+
import { withStateLock } from './local-state-lock.mjs';
|
|
8
|
+
import { withTaskHandoffIssueFence, withTaskHandoffReadFence } from './local-task-handoff-fence.mjs';
|
|
9
|
+
import { captureSwarmCompletion, captureSwarmOwnershipCompletions, revalidateSwarmCompletion } from './local-swarm-completion.mjs';
|
|
10
|
+
import { claimSwarmWriteIntent, closeSwarmWriteIntent, readSwarmCheckpointRequests,
|
|
11
|
+
registerSwarmWriteIntent, validateSwarmWriteIntents } from './local-swarm-priority.mjs';
|
|
12
|
+
|
|
13
|
+
function fail(code, message, details = {}) { throw Object.assign(new Error(message), { code, ...details }); }
|
|
14
|
+
function freshState(authority) {
|
|
15
|
+
return { schemaVersion: 'entrorove.coordination/1.0', flowId: authority.plan.flowId,
|
|
16
|
+
repositoryDigest: authority.repositoryDigest, maxWorkers: 0, workers: [], leases: [], events: [], delegationGrants: [] };
|
|
17
|
+
}
|
|
18
|
+
function registration(state, authority, input) {
|
|
19
|
+
const grants = state.delegationGrants;
|
|
20
|
+
if (!Array.isArray(grants) || grants.length > 50) fail('ENTROROVE_SWARM_STATE_INVALID', 'registered delegation grants are missing or exceed the worker bound');
|
|
21
|
+
const ids = new Set();
|
|
22
|
+
for (const value of grants) {
|
|
23
|
+
const grant = verifyStoredSwarmGrant(value, true);
|
|
24
|
+
if (grant.flowId !== authority.plan.flowId || grant.localRepositoryDigest !== authority.repositoryDigest || ids.has(grant.workerId)) {
|
|
25
|
+
fail('ENTROROVE_SWARM_STATE_INVALID', 'registered grant changed its worker or original flow binding');
|
|
26
|
+
}
|
|
27
|
+
ids.add(grant.workerId);
|
|
28
|
+
}
|
|
29
|
+
const existing = grants.find(value => value.workerId === authority.grant.workerId);
|
|
30
|
+
if (existing && canonical(existing) !== canonical(input.delegationGrant)) fail('ENTROROVE_SWARM_WORKER_DUPLICATE', 'workerId is frozen to its original exact grant');
|
|
31
|
+
const updated = existing ? grants : [...grants, input.delegationGrant];
|
|
32
|
+
const authorized = updated.filter(value => Date.parse(value.expiresAt) > Date.now()).length;
|
|
33
|
+
if (authorized > 50 || (input.workerCount !== undefined && (!Number.isSafeInteger(input.workerCount)
|
|
34
|
+
|| input.workerCount < 0 || input.workerCount > 50))) fail('ENTROROVE_SWARM_WORKER_LIMIT', 'caller workerCount must be a bounded conservative limit');
|
|
35
|
+
const retainedActive = state.workers.filter(value => value.status === 'active').length;
|
|
36
|
+
return { ...state, maxWorkers: Math.max(retainedActive, Math.min(authorized, input.workerCount ?? 50)), delegationGrants: updated };
|
|
37
|
+
}
|
|
38
|
+
function validateStoredWorkers(state, authority) {
|
|
39
|
+
validateSwarmState(state, { flowId: authority.plan.flowId, repositoryDigest: authority.repositoryDigest });
|
|
40
|
+
for (const worker of state.workers) {
|
|
41
|
+
const grant = verifyStoredSwarmGrant(worker.delegationGrant, true);
|
|
42
|
+
if (worker.grantDigest !== sha256(canonical(worker.delegationGrant)) || grant.flowId !== state.flowId
|
|
43
|
+
|| grant.localRepositoryDigest !== state.repositoryDigest || grant.workerId !== worker.workerId || grant.leaseId !== worker.leaseId
|
|
44
|
+
|| grant.priority !== worker.priority || canonical(grant.paths) !== canonical(worker.paths) || grant.expiresAt !== worker.expiresAt) {
|
|
45
|
+
fail('ENTROROVE_SWARM_STATE_INVALID', 'worker metadata differs from its actual signed delegation grant');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return state;
|
|
49
|
+
}
|
|
50
|
+
function workerRecord(authority, input) {
|
|
51
|
+
return { workerId: authority.grant.workerId, leaseId: authority.lease.leaseId, flowId: authority.plan.flowId,
|
|
52
|
+
handoffEpoch: authority.epoch, paths: authority.paths, priority: authority.grant.priority, status: 'active',
|
|
53
|
+
createdAt: new Date().toISOString(), expiresAt: authority.grant.expiresAt,
|
|
54
|
+
grantDigest: sha256(canonical(input.delegationGrant)), delegationGrant: input.delegationGrant };
|
|
55
|
+
}
|
|
56
|
+
function exactWorker(state, authority, input) {
|
|
57
|
+
const worker = state.workers.find(value => value.workerId === authority.grant.workerId && value.leaseId === authority.lease.leaseId);
|
|
58
|
+
if (!worker || (input.leaseId !== undefined && input.leaseId !== worker.leaseId)
|
|
59
|
+
|| worker.grantDigest !== sha256(canonical(input.delegationGrant)) || worker.handoffEpoch !== authority.epoch) {
|
|
60
|
+
fail('ENTROROVE_SWARM_LEASE_INVALID', 'caller does not hold the original exact worker/cloud lease');
|
|
61
|
+
}
|
|
62
|
+
return worker;
|
|
63
|
+
}
|
|
64
|
+
async function completionIdle(authority, input, fence, proof) {
|
|
65
|
+
if (fence.activities.some(value => value.leaseId === authority.lease.leaseId)) fail('ENTROROVE_SWARM_EXECUTION_ACTIVE', 'actual in-flight execution must reach its original completed checkpoint');
|
|
66
|
+
for (const target of authority.paths) {
|
|
67
|
+
const lock = path.join(authority.home, '.entrorove', 'write-locks', authority.repositoryDigest, `${sha256(target)}.lock`);
|
|
68
|
+
try { await fs.lstat(lock); fail('ENTROROVE_SWARM_EXECUTION_ACTIVE', 'an actual target write lock prevents peer completion'); }
|
|
69
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
70
|
+
}
|
|
71
|
+
const result = await revalidateSwarmCompletion(proof, authority, input);
|
|
72
|
+
if (!result || !['reported', 'failed'].includes(result.status)) fail('ENTROROVE_SWARM_EXECUTION_UNKNOWN', 'actual original workflow result is not completed');
|
|
73
|
+
if (!result.receipt || !Number.isInteger(result.process?.exitCode)) fail('ENTROROVE_SWARM_EXECUTION_UNKNOWN', 'interrupted unsigned outcome cannot release the original signed reservation');
|
|
74
|
+
const started = Date.parse(result.process?.startedAt), finished = Date.parse(result.process?.finishedAt);
|
|
75
|
+
if (!Number.isFinite(started) || !Number.isFinite(finished) || started < Date.parse(authority.lease.issuedAt)
|
|
76
|
+
|| started < Date.parse(authority.grant.issuedAt) || started >= Date.parse(authority.grant.expiresAt)
|
|
77
|
+
|| finished < started || finished > Date.parse(authority.lease.expiresAt)) fail('ENTROROVE_SWARM_EXECUTION_UNKNOWN', 'completed process started outside its actual grant or exceeded the original cloud lease');
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
async function observeCoordination(input) {
|
|
81
|
+
const original = await readSwarmCloudAuthority(input, 'verify'), storage = await swarmStorage(original, 'read');
|
|
82
|
+
return withStateLock(storage.ownershipFile, () => withStateLock(storage.stateFile, async () => {
|
|
83
|
+
const authority = await readSwarmCloudAuthority(input, 'verify');
|
|
84
|
+
const ownership = await validateSwarmOwnership(await readSwarmOwnership(storage, authority, 'read'), authority);
|
|
85
|
+
const state = validateStoredWorkers(await readSwarmJson(storage.stateFile, null), authority);
|
|
86
|
+
const worker = exactWorker(state, authority, input);
|
|
87
|
+
const reservation = ownership.records.find(value => value.flowId === authority.plan.flowId
|
|
88
|
+
&& value.workerId === worker.workerId && value.leaseId === worker.leaseId);
|
|
89
|
+
if (worker.status !== 'active' || !reservation || reservation.grantDigest !== worker.grantDigest) {
|
|
90
|
+
fail('ENTROROVE_SWARM_LEASE_INVALID', 'actual worker lease is inactive or unreserved');
|
|
91
|
+
}
|
|
92
|
+
const checkpointRequests = await readSwarmCheckpointRequests(ownership, authority, input);
|
|
93
|
+
await readSwarmCloudAuthority(input, 'verify');
|
|
94
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', verified: true, workerId: worker.workerId,
|
|
95
|
+
leaseId: worker.leaseId, paths: worker.paths, expiresAt: worker.expiresAt,
|
|
96
|
+
grantDigest: worker.grantDigest, executionStatus: 'running', checkpointRequests };
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
async function operate(rawInput, operation) {
|
|
100
|
+
let input; try { input = structuredClone(rawInput); } catch { fail('ENTROROVE_SWARM_AUTHORITY_INVALID', 'peer input must be finite JSON data'); }
|
|
101
|
+
if (operation === 'verify') return observeCoordination(input);
|
|
102
|
+
const purpose = operation === 'complete' ? 'report' : 'issue';
|
|
103
|
+
const original = await readSwarmCloudAuthority(input, purpose);
|
|
104
|
+
const storage = await swarmStorage(original);
|
|
105
|
+
const beforeOwnership = await readSwarmJson(storage.ownershipFile, null);
|
|
106
|
+
const completionProofs = await captureSwarmOwnershipCompletions(beforeOwnership, original.root);
|
|
107
|
+
const completionProof = purpose === 'report' ? await captureSwarmCompletion(input) : null;
|
|
108
|
+
const fenceInput = { repositoryRoot: original.root, home: original.home, flowId: original.plan.flowId,
|
|
109
|
+
epoch: original.epoch, leaseId: original.lease.leaseId, paths: original.paths };
|
|
110
|
+
const hold = purpose === 'issue' ? withTaskHandoffIssueFence : withTaskHandoffReadFence;
|
|
111
|
+
return hold(fenceInput, async fence => {
|
|
112
|
+
const authority = await readSwarmCloudAuthority(input, purpose);
|
|
113
|
+
if (fence.epoch !== authority.epoch) fail('ENTROROVE_SWARM_LEASE_INVALID', 'worker grant epoch changed while acquiring its original fence');
|
|
114
|
+
return withStateLock(storage.ownershipFile, () => withStateLock(storage.stateFile, async () => {
|
|
115
|
+
await readSwarmCloudAuthority(input, purpose);
|
|
116
|
+
const checkedOwnership = await validateSwarmOwnership(await readSwarmOwnership(storage, authority), authority, completionProofs);
|
|
117
|
+
const ownership = await validateSwarmWriteIntents(checkedOwnership, authority, completionProofs);
|
|
118
|
+
const state = validateStoredWorkers(await readSwarmJson(storage.stateFile, freshState(authority)), authority);
|
|
119
|
+
if (operation === 'coordinate') {
|
|
120
|
+
if (input.priority !== undefined && input.priority !== authority.grant.priority) fail('ENTROROVE_SWARM_PRIORITY_INVALID', 'caller priority cannot replace the signed assignment priority');
|
|
121
|
+
const updated = registration(state, authority, input);
|
|
122
|
+
const queued = await registerSwarmWriteIntent(ownership, authority, input, completionProofs);
|
|
123
|
+
await writeSwarmJson(storage.ownershipFile, queued.index);
|
|
124
|
+
await writeSwarmJson(storage.stateFile, { ...updated, events: [...updated.events, { event: 'peer-coordinate', workerId: authority.grant.workerId, at: new Date().toISOString() }] });
|
|
125
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', maxWorkers: updated.maxWorkers,
|
|
126
|
+
workerId: authority.grant.workerId, priority: authority.grant.priority,
|
|
127
|
+
grantDigest: sha256(canonical(input.delegationGrant)), intentId: queued.intent.intentId, intentSequence: queued.intent.sequence };
|
|
128
|
+
}
|
|
129
|
+
if (operation === 'spawn') {
|
|
130
|
+
if (input.parentWorkerId !== undefined && input.parentWorkerId !== null) fail('ENTROROVE_RECURSION_FORBIDDEN', 'a derived worker cannot spawn another worker');
|
|
131
|
+
if (input.priority !== undefined && input.priority !== authority.grant.priority) fail('ENTROROVE_SWARM_PRIORITY_INVALID', 'caller priority cannot replace the signed assignment priority');
|
|
132
|
+
if (!state.delegationGrants?.some(value => canonical(value) === canonical(input.delegationGrant))) fail('ENTROROVE_SWARM_CLOUD_AUTHORITY_REQUIRED', 'the exact cloud assignment must be coordinated first');
|
|
133
|
+
if (state.workers.some(value => value.workerId === authority.grant.workerId)) fail('ENTROROVE_SWARM_WORKER_DUPLICATE', 'workerId is already bound and cannot be reused');
|
|
134
|
+
if (state.leases.some(value => value.leaseId === authority.lease.leaseId)) fail('ENTROROVE_SWARM_LEASE_INVALID', 'leaseId is already bound');
|
|
135
|
+
const decision = await claimSwarmWriteIntent(ownership, authority, input, completionProofs);
|
|
136
|
+
await writeSwarmJson(storage.ownershipFile, decision.index);
|
|
137
|
+
if (!decision.mayAcquire) {
|
|
138
|
+
const active = decision.blockedBy.some(value => value.kind === 'active');
|
|
139
|
+
const unknown = decision.blockedBy.some(value => value.reconcileRequired);
|
|
140
|
+
fail(unknown ? 'ENTROROVE_SWARM_RECONCILE_REQUIRED' : active ? 'ENTROROVE_SWARM_PATH_CONFLICT' : 'ENTROROVE_SWARM_PRIORITY_WAIT',
|
|
141
|
+
unknown ? 'original execution outcome is unknown; reconcile before releasing its paths' : 'the original signed worker must wait for its overlapping path turn',
|
|
142
|
+
{ status: 'waiting', intentId: decision.intentId, readyPaths: decision.readyPaths, blockedPaths: decision.blockedPaths,
|
|
143
|
+
blockedBy: decision.blockedBy, safeCheckpointRequested: decision.safeCheckpointRequested, freshAimlockSnapshotRequired: true });
|
|
144
|
+
}
|
|
145
|
+
const record = swarmOwnershipRecord(authority, input);
|
|
146
|
+
const updated = appendSwarmWorker(state, workerRecord(authority, input));
|
|
147
|
+
const reserved = await validateSwarmWriteIntents({ ...decision.index, records: [...decision.index.records, record] }, authority, completionProofs);
|
|
148
|
+
// Reserve first: an interrupted publication can only retain an extra
|
|
149
|
+
// conservative reservation, never publish an unreserved active worker.
|
|
150
|
+
await writeSwarmJson(storage.ownershipFile, reserved);
|
|
151
|
+
await writeSwarmJson(storage.stateFile, updated);
|
|
152
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', status: 'running', workerId: authority.grant.workerId,
|
|
153
|
+
leaseId: authority.lease.leaseId, paths: authority.paths, expiresAt: authority.grant.expiresAt,
|
|
154
|
+
delegationGrant: input.delegationGrant, grantDigest: record.grantDigest };
|
|
155
|
+
}
|
|
156
|
+
const worker = exactWorker(state, authority, input);
|
|
157
|
+
const reservation = ownership.records.find(value => value.flowId === authority.plan.flowId && value.workerId === worker.workerId && value.leaseId === worker.leaseId);
|
|
158
|
+
const actual = await completionIdle(authority, input, fence, completionProof);
|
|
159
|
+
const outcome = actual.status === 'reported' ? 'completed' : 'failed';
|
|
160
|
+
if (input.status !== undefined && input.status !== outcome) fail('ENTROROVE_SWARM_REPORT_INVALID', 'caller report conflicts with its actual signed process result');
|
|
161
|
+
const closed = await closeSwarmWriteIntent(ownership, authority, input, completionProof, completionProofs);
|
|
162
|
+
if (worker.status !== 'active') {
|
|
163
|
+
if (worker.outcome !== outcome || !['reported', 'failed', 'blocked'].includes(worker.status)) fail('ENTROROVE_SWARM_REPORT_INVALID', 'completion replay changed its original reported outcome');
|
|
164
|
+
await writeSwarmJson(storage.ownershipFile, { ...closed, records: closed.records.filter(value => value !== reservation) });
|
|
165
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', status: worker.status, trafficLight: worker.status === 'reported' ? 'yellow' : 'red', workerId: worker.workerId, leaseId: worker.leaseId };
|
|
166
|
+
}
|
|
167
|
+
const reported = reportSwarmWorker(state, { workerId: worker.workerId, leaseId: worker.leaseId, outcome }, true);
|
|
168
|
+
// Close the worker first; a crash before index cleanup cannot leave a
|
|
169
|
+
// live old worker after the same paths have been reassigned.
|
|
170
|
+
await writeSwarmJson(storage.stateFile, reported.state);
|
|
171
|
+
await writeSwarmJson(storage.ownershipFile, { ...closed, records: closed.records.filter(value => value !== reservation) });
|
|
172
|
+
return { schemaVersion: 'entrorove.local-execution/1.0', ...reported.result };
|
|
173
|
+
}));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export async function peerCoordinate(input) { return operate(input, 'coordinate'); }
|
|
177
|
+
export async function spawnBind(input) { return operate(input, 'spawn'); }
|
|
178
|
+
export async function completePeer(input) { return operate(input, 'complete'); }
|
|
179
|
+
export async function verifyCoordinationLease(input) { return operate(input, 'verify'); }
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createPublicKey, verify } from 'node:crypto';
|
|
2
|
+
import { canonical, sha256 } from './local-integrity.mjs';
|
|
3
|
+
import { canonicalHandoffSignature, overlayAuthority, receiverClaim } from './local-task-handoff-overlay-wire.mjs';
|
|
4
|
+
import { verifyWriteReceipt } from './local-task-handoff-write-wire.mjs';
|
|
5
|
+
|
|
6
|
+
const ORIGINAL = ['challengeId', 'handoffId', 'messageRequestId', 'taskId', 'flowId', 'ownerId', 'taskDigest', 'chainDigest',
|
|
7
|
+
'checkpointReceiptId', 'checkpointDigest', 'originalScopeDigest', 'localRepositoryDigest', 'epoch', 'previousOwnerEpoch',
|
|
8
|
+
'paths', 'passId', 'passDigest', 'leaseId', 'leaseDigest', 'generation', 'scopeContractDigest',
|
|
9
|
+
'sourceReleaseReceiptDigest', 'claimReceiptDigest'];
|
|
10
|
+
export const HANDOFF_COMPLETION_FIELDS = ['schemaVersion', ...ORIGINAL, 'runnerKeyId', 'issuedAt', 'expiresAt'];
|
|
11
|
+
const RECEIPT_FIELDS = ['schemaVersion', 'receiptId', 'purpose', 'status', ...ORIGINAL, 'challengeDigest', 'files',
|
|
12
|
+
'scopeFingerprintDigest', 'workflowStateDigest', 'revokedPassIds', 'revokedLeaseIds', 'keyId', 'nonce', 'issuedAt', 'expiresAt', 'signature'];
|
|
13
|
+
const WRITE_FIELDS = ['architectureAuthorityDigest', 'writeCheckpointReceipts', 'writeCheckpointReceiptsDigest'];
|
|
14
|
+
function fail(message) { const error = new Error(message); error.code = 'ENTROROVE_HANDOFF_COMPLETION'; throw error; }
|
|
15
|
+
export function completionChallenge(value, claim, ctx, live = true) {
|
|
16
|
+
const payload = overlayAuthority(value, HANDOFF_COMPLETION_FIELDS, 'entrorove.task-handoff-completion-challenge/1.0', live);
|
|
17
|
+
const { pass, lease, challenge } = receiverClaim(claim, ctx, false);
|
|
18
|
+
if (['handoffId', 'messageRequestId', 'ownerId', 'flowId', 'taskDigest', 'chainDigest', 'runnerKeyId', 'localRepositoryDigest', 'scopeContractDigest'].some(key => payload[key] !== pass[key])
|
|
19
|
+
|| payload.taskId !== pass.receiverTaskId || payload.originalScopeDigest !== pass.receiverOriginalScopeDigest
|
|
20
|
+
|| payload.epoch !== pass.receiverEpoch || payload.previousOwnerEpoch !== pass.epoch || payload.passId !== pass.passId
|
|
21
|
+
|| payload.passDigest !== sha256(canonical(claim.pass)) || payload.leaseId !== lease.leaseId
|
|
22
|
+
|| payload.leaseDigest !== sha256(canonical(claim.lease)) || payload.generation !== lease.generation
|
|
23
|
+
|| payload.sourceReleaseReceiptDigest !== challenge.baselineReceiptDigest
|
|
24
|
+
|| canonical(payload.paths) !== canonical(pass.paths)) fail('completion challenge is outside its actual receiver takeover, scope or grant binding');
|
|
25
|
+
return payload;
|
|
26
|
+
}
|
|
27
|
+
export function completionReceipt(bundle, claim, ctx, live = false) {
|
|
28
|
+
const challenge = completionChallenge(bundle?.challenge, claim, ctx, live), receipt = bundle?.receipt;
|
|
29
|
+
const present = WRITE_FIELDS.filter(field => Object.hasOwn(receipt ?? {}, field));
|
|
30
|
+
if (present.length !== 3 && (present.length || live)) fail('live completion requires the full original trusted write receipt batch; legacy evidence is history only');
|
|
31
|
+
if (!receipt || Object.keys(receipt).sort().join('\0') !== [...RECEIPT_FIELDS, ...present].sort().join('\0')) fail('completion idle receipt fields are invalid');
|
|
32
|
+
if (present.length) {
|
|
33
|
+
const authority = claim.pass.architectureAuthority;
|
|
34
|
+
if (receipt.architectureAuthorityDigest !== (authority ? sha256(canonical(authority)) : null)
|
|
35
|
+
|| !Array.isArray(receipt.writeCheckpointReceipts) || receipt.writeCheckpointReceipts.length > 4096
|
|
36
|
+
|| Buffer.byteLength(canonical(receipt.writeCheckpointReceipts)) > 768_000
|
|
37
|
+
|| receipt.writeCheckpointReceiptsDigest !== sha256(canonical(receipt.writeCheckpointReceipts))) fail('completion changed its original authority or complete write receipt batch');
|
|
38
|
+
for (const item of receipt.writeCheckpointReceipts) verifyWriteReceipt(item, claim, ctx);
|
|
39
|
+
}
|
|
40
|
+
if (receipt.schemaVersion !== 'entrorove.task-handoff-completion-idle-receipt/1.0' || receipt.purpose !== 'completion' || receipt.status !== 'paused'
|
|
41
|
+
|| receipt.challengeDigest !== sha256(canonical(challenge)) || ORIGINAL.some(key => canonical(receipt[key]) !== canonical(challenge[key]))
|
|
42
|
+
|| !Array.isArray(receipt.files) || !receipt.files.length || receipt.files.length > 512
|
|
43
|
+
|| canonical(receipt.files.map(file => file.path)) !== canonical(challenge.paths) || receipt.scopeFingerprintDigest !== sha256(canonical(receipt.files))
|
|
44
|
+
|| !Array.isArray(receipt.revokedPassIds) || !receipt.revokedPassIds.includes(challenge.passId)
|
|
45
|
+
|| !Array.isArray(receipt.revokedLeaseIds) || !receipt.revokedLeaseIds.includes(challenge.leaseId)) fail('completion receipt lost its actual fingerprints and revoked takeover grants');
|
|
46
|
+
for (const file of receipt.files) {
|
|
47
|
+
const fields = ['path', 'sha256', 'bytes', 'lines', ...(Object.hasOwn(file, 'exists') ? ['exists'] : [])];
|
|
48
|
+
if (Object.keys(file).sort().join('\0') !== fields.sort().join('\0')
|
|
49
|
+
|| !/^[0-9a-f]{64}$/.test(file.sha256) || !Number.isSafeInteger(file.bytes) || file.bytes < 0
|
|
50
|
+
|| !Number.isSafeInteger(file.lines) || file.lines < 0 || (Object.hasOwn(file, 'exists')
|
|
51
|
+
&& (file.exists !== false || file.sha256 !== sha256('') || file.bytes !== 0 || file.lines !== 0))) fail('completion fingerprint fields are invalid');
|
|
52
|
+
}
|
|
53
|
+
const issued = Date.parse(receipt.issuedAt), expires = Date.parse(receipt.expiresAt);
|
|
54
|
+
if (!Number.isFinite(issued) || !Number.isFinite(expires) || issued < Date.parse(challenge.issuedAt) || issued > Date.now()
|
|
55
|
+
|| expires <= issued || expires > Date.parse(challenge.expiresAt) || (live && expires <= Date.now())) fail('completion idle receipt lifetime is invalid');
|
|
56
|
+
let key, der; const encoded = process.env.ENTROROVE_TASK_HANDOFF_RUNNER_PUBLIC_KEY;
|
|
57
|
+
try { der = Buffer.from(encoded ?? '', 'base64'); key = createPublicKey({ key: der, format: 'der', type: 'spki' }); }
|
|
58
|
+
catch { fail('completion trusted runner identity is unavailable'); }
|
|
59
|
+
const { signature, ...core } = receipt;
|
|
60
|
+
if (key.asymmetricKeyType !== 'ed25519' || key.export({ format: 'der', type: 'spki' }).toString('base64') !== encoded
|
|
61
|
+
|| receipt.keyId !== sha256(der) || receipt.keyId !== challenge.runnerKeyId
|
|
62
|
+
|| !verify(null, Buffer.from(canonical(core)), key, canonicalHandoffSignature(signature))) fail('completion idle receipt requires its trusted receiver signature');
|
|
63
|
+
return receipt;
|
|
64
|
+
}
|
|
65
|
+
export function completionReceiptCore(challenge, data, keyId, ttlMs) {
|
|
66
|
+
const now = Date.now(), original = Object.fromEntries(ORIGINAL.map(field => [field, challenge[field]]));
|
|
67
|
+
return { schemaVersion: 'entrorove.task-handoff-completion-idle-receipt/1.0', receiptId: data.receiptId,
|
|
68
|
+
purpose: 'completion', status: 'paused', ...original, challengeDigest: sha256(canonical(challenge)), ...data,
|
|
69
|
+
keyId, nonce: data.nonce, issuedAt: new Date(now).toISOString(),
|
|
70
|
+
expiresAt: new Date(Math.min(now + ttlMs, Date.parse(challenge.expiresAt))).toISOString() };
|
|
71
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { object, ledgerIdentifier, sha256, integer, paths, nullable, relativePath } from './local-contract-types.mjs';
|
|
2
|
+
import { archguardOperationSchemas } from './local-archguard-evaluate.mjs';
|
|
3
|
+
export const HANDOFF_PASS_FIELDS = ['schemaVersion', 'passId', 'handoffId', 'messageRequestId', 'ownerId', 'previousOwnerTaskId',
|
|
4
|
+
'previousOwnerFlowId', 'receiverTaskId', 'flowId', 'taskDigest', 'chainDigest', 'runnerKeyId', 'localRepositoryDigest', 'epoch',
|
|
5
|
+
'receiverEpoch', 'previousOwnerScopeDigest', 'receiverOriginalScopeDigest', 'scopeContractDigest', 'architectureContractDigest', 'nativeOutputDigest',
|
|
6
|
+
'scopeSnapshotDigest', 'snapshotId', 'scopeFingerprintDigest', 'paths', 'receiptId', 'issuedAt', 'expiresAt', 'previousOwnerScopeContract', 'scopeContract', 'architectureAuthority'];
|
|
7
|
+
export const HANDOFF_LEASE_FIELDS = ['schemaVersion', 'purpose', 'leaseId', 'handoffId', 'messageRequestId', 'ownerId', 'taskId', 'flowId',
|
|
8
|
+
'stepId', 'taskDigest', 'chainDigest', 'challengeId', 'runnerKeyId', 'localRepositoryDigest', 'epoch', 'receiverEpoch', 'paths', 'generation',
|
|
9
|
+
'mutationAllowed', 'passId', 'passDigest', 'claimReceiptId', 'issuedAt', 'expiresAt'];
|
|
10
|
+
export const HANDOFF_CANDIDATE_FIELDS = ['schemaVersion', 'challengeId', 'handoffId', 'messageRequestId', 'flowId', 'stepId', 'ownerId',
|
|
11
|
+
'taskDigest', 'chainDigest', 'runnerId', 'runnerKeyId', 'repositoryId', 'localRepositoryDigest', 'previousOwnerTaskId', 'previousOwnerFlowId',
|
|
12
|
+
'previousOwnerTaskDigest', 'previousOwnerChainDigest', 'previousOwnerAcceptanceReceiptId', 'receiverTaskId', 'receiverAcceptanceReceiptId',
|
|
13
|
+
'sourceCheckpointDigest', 'receiverCheckpointDigest', 'originalScopeDigest', 'receiverOriginalScopeDigest', 'paths', 'epoch',
|
|
14
|
+
'receiverEpoch', 'baselineReceiptDigest', 'issuedAt', 'expiresAt', 'nonce'];
|
|
15
|
+
|
|
16
|
+
const native = await archguardOperationSchemas();
|
|
17
|
+
const exact = properties => object(properties, Object.keys(properties));
|
|
18
|
+
const string = { type: 'string' }, boolean = { type: 'boolean' };
|
|
19
|
+
const timestamp = { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$' };
|
|
20
|
+
const uuid = { type: 'string', pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' };
|
|
21
|
+
const scopePaths = { type: 'array', items: relativePath, maxItems: 512 };
|
|
22
|
+
export const HANDOFF_SCOPE_CONTRACT_SCHEMA = exact({ schemaVersion: { const: 'aimlock.scope-contract/1.0' },
|
|
23
|
+
allowedPaths: { ...scopePaths, minItems: 1 }, forbiddenPaths: scopePaths,
|
|
24
|
+
maxChangedLines: integer(1, 1_000_000), allowNewFiles: boolean, allowDeleteFiles: boolean });
|
|
25
|
+
const reason = { type: 'string', maxLength: 1024, pattern: '\\S' };
|
|
26
|
+
const intent = { oneOf: [exact({ action: { const: 'update' }, expectedDigest: sha256, reason,
|
|
27
|
+
patch: { ...native['contract-update'].input.properties.patch, minProperties: 1 } }),
|
|
28
|
+
exact({ action: { const: 'resume' }, expectedDigest: sha256, reason })] };
|
|
29
|
+
const interaction = exact({ schemaVersion: { const: 'confirm.interaction/1.0' }, requestId: ledgerIdentifier,
|
|
30
|
+
type: { const: 'confirm' }, risk: { const: 'high' }, question: string, default: { type: 'null' }, timeout: { type: 'null' },
|
|
31
|
+
rememberable: { const: false }, memoryKey: { const: '' }, riskDescription: string, timeoutAction: { const: 'wait' },
|
|
32
|
+
options: { type: 'array', minItems: 2, maxItems: 2, additionalItems: false, items: [
|
|
33
|
+
exact({ id: { const: 'approve' }, label: { const: 'Approve' } }), exact({ id: { const: 'reject' }, label: { const: 'Reject' } })] },
|
|
34
|
+
callback: exact({ operation: { enum: ['contract-update', 'resume'] }, payload: intent }) });
|
|
35
|
+
const decision = exact({ interactionId: ledgerIdentifier, interaction, interactionDigest: sha256, intent, ledgerDigest: sha256,
|
|
36
|
+
status: { enum: ['pending', 'answered'] }, actorId: nullable(uuid), answer: nullable({ enum: ['approve', 'reject'] }),
|
|
37
|
+
answerRequestId: nullable(ledgerIdentifier), batchable: { const: false }, consumed: boolean, createdAt: timestamp,
|
|
38
|
+
answeredAt: nullable(timestamp) });
|
|
39
|
+
export const HANDOFF_ARCHITECTURE_AUTHORITY_SCHEMA = exact({ flowId: ledgerIdentifier, ownerId: uuid, taskDigest: sha256,
|
|
40
|
+
chainDigest: sha256, scopeArtifactDigest: sha256, contract: native['contract-get'].input.properties.contract,
|
|
41
|
+
contractDigest: sha256, revision: integer(1), ledger: native['drift-status'].input.properties.ledger,
|
|
42
|
+
ledgerDigest: sha256, driftEpochStart: integer(0), driftStatus: { enum: ['green', 'yellow', 'red'] },
|
|
43
|
+
interrupted: boolean, decisions: { type: 'array', items: decision }, sequence: integer(1), headDigest: sha256 });
|
|
44
|
+
const authorityReceipt = object({ schemaVersion: { const: 'entrorove.workflow-authority-receipt/1.0' },
|
|
45
|
+
keyId: sha256, payloadDigest: sha256, signature: { type: 'string', pattern: '^[A-Za-z0-9_-]{86}$' } },
|
|
46
|
+
['schemaVersion', 'keyId', 'payloadDigest', 'signature']);
|
|
47
|
+
function field(name) {
|
|
48
|
+
if (['previousOwnerScopeContract', 'scopeContract'].includes(name)) return HANDOFF_SCOPE_CONTRACT_SCHEMA;
|
|
49
|
+
if (name === 'architectureAuthority') return nullable(HANDOFF_ARCHITECTURE_AUTHORITY_SCHEMA);
|
|
50
|
+
if (name === 'localRepositoryDigest') return { type: 'string', pattern: '^[0-9a-f]{32}$' };
|
|
51
|
+
if (name === 'architectureContractDigest') return nullable(sha256);
|
|
52
|
+
if (name.endsWith('Digest') || name === 'runnerKeyId') return sha256;
|
|
53
|
+
if (name === 'paths') return { ...paths, maxItems: 512 };
|
|
54
|
+
if (['epoch', 'receiverEpoch', 'previousEpoch', 'generation'].includes(name)) return integer(0, 2147483647);
|
|
55
|
+
if (name === 'mutationAllowed') return { const: true };
|
|
56
|
+
if (name === 'purpose') return { const: 'takeover-write' };
|
|
57
|
+
if (['issuedAt', 'expiresAt'].includes(name)) return { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$' };
|
|
58
|
+
return ledgerIdentifier;
|
|
59
|
+
}
|
|
60
|
+
function signed(fields, schemaVersion) {
|
|
61
|
+
const properties = { ...Object.fromEntries(fields.map(name => [name, field(name)])), schemaVersion: { const: schemaVersion }, authorityReceipt };
|
|
62
|
+
return object(properties, Object.keys(properties));
|
|
63
|
+
}
|
|
64
|
+
export const HANDOFF_WRITE_PASS_SCHEMA = signed(HANDOFF_PASS_FIELDS, 'entrorove.task-handoff-write-pass/1.0');
|
|
65
|
+
export const HANDOFF_PROVISIONAL_LEASE_SCHEMA = signed(HANDOFF_LEASE_FIELDS, 'entrorove.task-handoff-provisional-lease/1.0');
|
|
66
|
+
export const HANDOFF_SNAPSHOT_CANDIDATE_SCHEMA = signed(HANDOFF_CANDIDATE_FIELDS, 'entrorove.task-handoff-scope-challenge/1.0');
|