@entrorove/skill 0.1.23 → 0.1.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.
Files changed (155) hide show
  1. package/AGREEMENT.md +6 -6
  2. package/CHANGELOG.md +5 -0
  3. package/SKILL.md +12 -4
  4. package/archguard-runtime-build.mjs +15 -0
  5. package/blueprint-criterion-runner.mjs +110 -0
  6. package/blueprint-criterion-wire.mjs +78 -0
  7. package/calctool-runtime-build.mjs +59 -0
  8. package/calctool-runtime-entry.mjs +92 -0
  9. package/cli.mjs +17 -82
  10. package/local-approved-plan.mjs +74 -15
  11. package/local-archguard-ast.mjs +371 -0
  12. package/local-archguard-checkpoint.mjs +123 -0
  13. package/local-archguard-components.mjs +177 -0
  14. package/local-archguard-contract.mjs +109 -0
  15. package/local-archguard-contracts.mjs +42 -0
  16. package/local-archguard-evaluate.mjs +120 -0
  17. package/local-archguard-language-ast.mjs +165 -0
  18. package/local-archguard-mutation-fence.mjs +28 -0
  19. package/local-archguard-operations.mjs +25 -0
  20. package/local-archguard-scope.mjs +64 -0
  21. package/local-archguard-signing.mjs +52 -0
  22. package/local-archguard-snapshot.mjs +126 -0
  23. package/local-archguard-state.mjs +200 -0
  24. package/local-archguard.mjs +74 -0
  25. package/local-budget-git-attributes.mjs +49 -0
  26. package/local-budget-git-loader.mjs +61 -0
  27. package/local-budget-git-native-attributes.mjs +81 -0
  28. package/local-budget-git-native-darwin.mjs +104 -0
  29. package/local-budget-git-native-linux.mjs +93 -0
  30. package/local-budget-git-native-reads.mjs +88 -0
  31. package/local-budget-git-native-request.mjs +53 -0
  32. package/local-budget-git-native-runtime.mjs +18 -0
  33. package/local-budget-git-native.c +313 -0
  34. package/local-budget-git-native.mjs +109 -0
  35. package/local-budget-git-outcome.mjs +33 -0
  36. package/local-budget-git-scratch-loader.mjs +158 -0
  37. package/local-budget-git-scratch.mjs +112 -0
  38. package/local-budget-git-system-attributes.mjs +44 -0
  39. package/local-budget-git.mjs +92 -0
  40. package/local-budget-grants.mjs +82 -0
  41. package/local-budget-handoff-completion.mjs +70 -0
  42. package/local-budget-handoff-contracts.mjs +53 -0
  43. package/local-budget-handoff.mjs +229 -0
  44. package/local-budget-lock.mjs +55 -0
  45. package/local-budget-reads.mjs +135 -0
  46. package/local-budget-renewal.mjs +86 -34
  47. package/local-budget-source-bindings.mjs +51 -0
  48. package/local-budget-store.mjs +131 -0
  49. package/local-budget-transition.mjs +46 -0
  50. package/local-budget.mjs +185 -0
  51. package/local-calctool-cli.mjs +22 -0
  52. package/local-calctool-contracts.mjs +42 -0
  53. package/local-calctool-runtime.mjs +10 -0
  54. package/local-calctool-storage.mjs +101 -0
  55. package/local-calctool.mjs +103 -0
  56. package/local-cli.mjs +49 -16
  57. package/local-contract-types.mjs +46 -0
  58. package/local-contracts.mjs +124 -0
  59. package/local-credential.mjs +101 -0
  60. package/local-decimal.mjs +85 -0
  61. package/local-evidence-verification.mjs +199 -0
  62. package/local-execution.mjs +115 -304
  63. package/local-final-gate16-bundle.mjs +21 -0
  64. package/local-final-gate16-checks.mjs +88 -0
  65. package/local-final-gate16-contract.mjs +15 -0
  66. package/local-final-gate16-plan.mjs +66 -0
  67. package/local-final-gate16-proof.mjs +75 -0
  68. package/local-final-gate16-release.mjs +17 -0
  69. package/local-final-gate16-state.mjs +70 -0
  70. package/local-final-gate16-worker.mjs +19 -0
  71. package/local-final-gate16.mjs +137 -0
  72. package/local-formula-ast.mjs +97 -0
  73. package/local-formula-engine.mjs +111 -0
  74. package/local-formula-tests.mjs +61 -0
  75. package/local-formula.mjs +25 -125
  76. package/local-integrity.mjs +22 -41
  77. package/local-invocations.mjs +107 -0
  78. package/local-ledger.mjs +2 -7
  79. package/local-merge-archguard-verification.d.mts +44 -0
  80. package/local-merge-archguard-verification.mjs +240 -0
  81. package/local-merge-archguard.mjs +138 -0
  82. package/local-merge-artifacts.mjs +86 -0
  83. package/local-merge-authority.mjs +89 -0
  84. package/local-merge-contracts.mjs +56 -0
  85. package/local-merge-controller.mjs +170 -0
  86. package/local-merge-head.mjs +56 -0
  87. package/local-merge-journal.mjs +77 -0
  88. package/local-merge-mutation.mjs +97 -0
  89. package/local-merge-rollback.mjs +126 -0
  90. package/local-merge-snapshot-rollback.mjs +26 -0
  91. package/local-merge-storage.mjs +103 -0
  92. package/local-merge-targets.mjs +47 -0
  93. package/local-merge-transaction.mjs +133 -0
  94. package/local-mutation-audit.mjs +34 -0
  95. package/local-mutation-authority.mjs +96 -0
  96. package/local-mutation-batch-receipt.mjs +84 -0
  97. package/local-mutation-batch.mjs +143 -0
  98. package/local-mutation-binding.mjs +86 -0
  99. package/local-mutation-files.mjs +245 -0
  100. package/local-mutation-pass-store.mjs +63 -0
  101. package/local-mutation-snapshot.mjs +200 -0
  102. package/local-mutation-wire.mjs +89 -0
  103. package/local-probe.mjs +133 -11
  104. package/local-scope-candidate.mjs +28 -0
  105. package/local-scope-context.mjs +28 -0
  106. package/local-scope-files.mjs +106 -0
  107. package/local-scope-git-attributes.mjs +200 -0
  108. package/local-scope-git-comparison.mjs +8 -0
  109. package/local-scope-git-history.mjs +72 -0
  110. package/local-scope-git-reads.mjs +66 -0
  111. package/local-scope-git.mjs +124 -0
  112. package/local-scope-repository.mjs +249 -0
  113. package/local-scope-signature.mjs +70 -0
  114. package/local-scope-snapshot.mjs +39 -0
  115. package/local-scope-state.mjs +204 -0
  116. package/local-scope-workspaces.mjs +83 -0
  117. package/local-swarm-authority.mjs +143 -0
  118. package/local-swarm-baseline.mjs +32 -0
  119. package/local-swarm-completion.mjs +102 -0
  120. package/local-swarm-contracts.mjs +47 -0
  121. package/local-swarm-mutation.mjs +142 -0
  122. package/local-swarm-priority.mjs +237 -0
  123. package/local-swarm-state.mjs +102 -0
  124. package/local-swarm-storage.mjs +135 -0
  125. package/local-swarm.mjs +179 -0
  126. package/local-task-handoff-completion-wire.mjs +71 -0
  127. package/local-task-handoff-contracts.mjs +66 -0
  128. package/local-task-handoff-fence.mjs +478 -0
  129. package/local-task-handoff-observation.mjs +38 -0
  130. package/local-task-handoff-overlay-wire.mjs +94 -0
  131. package/local-task-handoff-read-authority.mjs +236 -0
  132. package/local-task-handoff-storage.mjs +73 -0
  133. package/local-task-handoff-write-archive.mjs +80 -0
  134. package/local-task-handoff-write-constraints.mjs +66 -0
  135. package/local-task-handoff-write-receipts.mjs +99 -0
  136. package/local-task-handoff-write-wire.mjs +99 -0
  137. package/local-task-routing.mjs +202 -22
  138. package/manifest.json +1 -1
  139. package/package.json +153 -3
  140. package/release-state.json +153 -17
  141. package/release.mjs +238 -4
  142. package/releases.json +6 -0
  143. package/remote-client.mjs +127 -36
  144. package/remote-transport.mjs +80 -0
  145. package/review-client.mjs +38 -0
  146. package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
  147. package/runtime/calctool-runtime-v7.0.43.mjs +1887 -0
  148. package/runtime/git-native-darwin-v1.mjs +5 -0
  149. package/runtime/git-native-linux-v1.mjs +5 -0
  150. package/task-message-cli.mjs +47 -0
  151. package/task-message-entry.mjs +159 -0
  152. package/workflow-local-client.mjs +35 -0
  153. package/workflow-local-contracts.mjs +58 -0
  154. package/workflow-local-request.mjs +53 -0
  155. package/workflow-local-runner.mjs +350 -0
@@ -1,29 +1,42 @@
1
+ import { mergeVerified as executeVerifiedMerge, rollbackMerge } from './local-merge-controller.mjs';
2
+ import { peerCoordinate, spawnBind, completePeer, verifyCoordinationLease } from './local-swarm.mjs';
3
+ export { peerCoordinate, spawnBind, completePeer, verifyCoordinationLease };
4
+ import { snapshotPlan14, snapshotVerify14, snapshotAuthority14, loadSnapshotReceipt14, readSnapshotCopy14 } from './local-mutation-snapshot.mjs';
5
+ import { mutateGate14, issueMutationPass14, verifyMutationPass14, guardedWrite14 } from './local-mutation-authority.mjs';
6
+ import { writeMutationProjectFile14 } from './local-mutation-files.mjs';
7
+ import { auditMutation14, withMutationAudit14 } from './local-mutation-audit.mjs';
1
8
  import { createHash, createPublicKey, generateKeyPairSync, sign, verify, randomUUID } from 'node:crypto';
2
- import { constants } from 'node:fs';
3
9
  import { promises as fs } from 'node:fs';
4
10
  import os from 'node:os';
5
11
  import path from 'node:path';
6
12
  import { formulaFinalGateFile, formulaFinalGate, evaluateFormula, evaluateFormulaGraph, Decimal } from './local-formula.mjs';
7
- import { LOCAL_LEDGER_OPERATIONS, LOCAL_LEDGER_SCHEMAS, branchCreate, branchList, branchSwitch, diffReport, preflight, ledgerQuery, chainInit, chainResume, chainStatus, chainAnswer, appendLedgerRecord } from './local-ledger.mjs';
8
- import { verifyArchguardPreflight, verifyReceipt, verifySnapshotManifest, verifyTrustedEvidence } from './local-integrity.mjs';
13
+ import { LOCAL_LEDGER_OPERATIONS, branchCreate, branchList, branchSwitch, diffReport, preflight, ledgerQuery, chainInit, chainResume, chainStatus, chainAnswer, appendLedgerRecord } from './local-ledger.mjs';
14
+ import { runHandoffTrustedWrite } from './local-task-handoff-write-receipts.mjs';
15
+ import { snapshotHandoffBinding, verifyArchguardPreflight, verifyReceipt, verifySnapshotManifest, verifyTrustedEvidence } from './local-integrity.mjs';
16
+ import { verifyTestEvidence } from './local-evidence-verification.mjs';
9
17
  import { RUNNER_CAPABILITIES, runApprovedPlan } from './local-approved-plan.mjs';
10
- import { LOCAL_RENEWAL_OPERATIONS, LOCAL_RENEWAL_SCHEMAS, budgetAutoRenewRequest, budgetAutoRenew, budgetAutoRenewStop, consumeBudgetRenewal } from './local-budget-renewal.mjs';
11
- import { LOCAL_PROBE_OPERATIONS, LOCAL_PROBE_SCHEMAS, probe, reassess } from './local-probe.mjs';
12
- import { LOCAL_TASK_ROUTING_OPERATIONS, LOCAL_TASK_ROUTING_SCHEMAS, taskDescribe, messageRoute, messageAccept, taskResume } from './local-task-routing.mjs';
18
+ import { WORKFLOW_LOCAL_CAPABILITIES, runWorkflowLocalStep } from './workflow-local-runner.mjs';
19
+ import { LOCAL_RENEWAL_OPERATIONS, budgetAutoRenewRequest, budgetAutoRenew, budgetAutoRenewStop } from './local-budget-renewal.mjs';
20
+ import { LOCAL_PROBE_OPERATIONS, probe, reassess } from './local-probe.mjs';
21
+ import { LOCAL_TASK_ROUTING_OPERATIONS, taskDescribe, messageRoute, messageAccept, taskResume } from './local-task-routing.mjs';
13
22
  import { withStateLock } from './local-state-lock.mjs';
23
+ import { readBudgetedProjectFile } from './local-budget-reads.mjs';
24
+ import { receiverClaim } from './local-task-handoff-overlay-wire.mjs';
25
+ import { assertTaskHandoffActive, assertTaskHandoffSnapshotBinding, readTaskHandoffSnapshotBinding,
26
+ withTaskHandoffIssueFence, withTaskHandoffWriteFence } from './local-task-handoff-fence.mjs';
27
+ import { LOCAL_OPERATION_SCHEMAS } from './local-contracts.mjs';
28
+ export { LOCAL_OPERATION_SCHEMAS };
14
29
  export const LOCAL_SCHEMA = 'entrorove.local-execution/1.0';
15
30
  export const SNAPSHOT_SCHEMA = 'entrorove.snapshot/1.0';
16
31
  export const RECEIPT_SCHEMA = 'entrorove.snapshot-receipt/1.0';
17
32
  export const PASS_SCHEMA = 'entrorove.mutation-pass/1.0';
18
33
  export const EVIDENCE_SCHEMA = 'entrorove.test-evidence/2.0';
19
34
  export const EVIDENCE_RECEIPT_SCHEMA = 'entrorove.runner-receipt/1.0';
20
- export const BUDGET_SCHEMA = 'entrorove.read-budget/1.0';
21
35
  export const COORDINATION_SCHEMA = 'entrorove.coordination/1.0';
22
- export const READ_BUDGETS = Object.freeze({
23
- lock: Object.freeze({ maxFiles: 3, maxTokenEstimate: 12_000, maxDurationMs: 120_000 }),
24
- probe: Object.freeze({ maxFiles: 10, maxTokenEstimate: 30_000, maxDurationMs: 480_000 }),
25
- swarm: Object.freeze({ maxFiles: 30, maxTokenEstimate: 100_000, maxDurationMs: 3_600_000 }),
26
- });
36
+ import { BUDGET_SCHEMA, READ_BUDGETS, budgetInit, budgetContext, budgetStatus, budgetRead, budgetExtend } from './local-budget.mjs';
37
+ export { BUDGET_SCHEMA, READ_BUDGETS, budgetInit, budgetContext, budgetStatus, budgetRead, budgetExtend };
38
+ export { budgetTransition, LOCAL_BUDGET_TRANSITION_OPERATIONS } from './local-budget-transition.mjs';
39
+ export { budgetHandoff, LOCAL_BUDGET_HANDOFF_OPERATIONS } from './local-budget-handoff.mjs';
27
40
  const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
28
41
  const SHA256 = /^[0-9a-f]{64}$/;
29
42
  const MISSING_SHA256 = sha256('entrorove.missing-file/1.0');
@@ -150,248 +163,89 @@ async function keyPair(home, root) {
150
163
  return { privateKey: pair.privateKey, publicKey: pair.publicKey, publicDer, keyId: sha256(publicDer) };
151
164
  }
152
165
  }
153
- async function fileDigest(file) { return file.exists ? sha256(await fs.readFile(file.target)) : MISSING_SHA256; }
166
+ async function fileDigest(file, input) { return file.exists ? sha256(await readBudgetedProjectFile(input, file.target, { sourcePath: file.path })) : MISSING_SHA256; }
154
167
  export async function snapshotPlan(input) {
155
- const root = await repositoryRoot(input.repositoryRoot);
156
- const flowId = id(input.flowId, 'flowId'); const stepId = id(input.stepId, 'stepId');
157
- const paths = normalizePaths(input.paths);
158
- const snapshotId = id(input.snapshotId ?? `snap-${randomUUID()}`, 'snapshotId');
159
- const directory = path.dirname(statePath(input.home, 'snapshots', root, flowId, 'manifest.json'));
160
- const snapshotRoot = path.join(directory, snapshotId);
161
- await privateDir(snapshotRoot);
162
- const files = [];
163
- for (const relative of paths) {
164
- const source = await projectFile(root, relative, true);
165
- const digest = await fileDigest(source);
166
- const target = path.join(snapshotRoot, relative);
167
- if (source.exists) { await privateDir(path.dirname(target)); await fs.copyFile(source.target, target, constants.COPYFILE_EXCL); }
168
- files.push({ path: relative, exists: source.exists, sha256: digest, mode: source.exists ? source.stat.mode & 0o777 : null });
169
- }
170
- const contractDigest = input.contractDigest ?? null;
171
- if (contractDigest !== null && !SHA256.test(contractDigest)) fail('ENTROROVE_CONTRACT_DIGEST', 'contractDigest must be a lowercase SHA-256 digest');
172
- const manifest = { schemaVersion: SNAPSHOT_SCHEMA, snapshotId, flowId, stepId, repositoryDigest: repoKey(root), paths, files, contractDigest,
173
- createdAt: new Date().toISOString(), snapshotDigest: sha256(canonical({ snapshotId, flowId, stepId, paths, files, contractDigest })) };
174
- const manifestPath = path.join(snapshotRoot, 'manifest.json');
175
- await atomicJson(manifestPath, manifest);
176
- await audit(input.home, root, 'snapshot-plan', { flowId, stepId, snapshotId });
177
- return { schemaVersion: LOCAL_SCHEMA, snapshotId, snapshotRoot, manifestPath, paths, contractDigest, snapshotDigest: manifest.snapshotDigest, forbidGitBranch: true, forbidWorktree: true };
168
+ const result = await snapshotPlan14(input, { handoffBinding: await readTaskHandoffSnapshotBinding(input) });
169
+ await auditMutation14(input, 'snapshot-plan', { snapshotId: result.snapshotId });
170
+ return result;
178
171
  }
179
172
  export async function snapshotVerify(input) {
180
- const root = await repositoryRoot(input.repositoryRoot); const flowId = id(input.flowId, 'flowId'); const stepId = id(input.stepId, 'stepId');
181
- const snapshotId = id(input.snapshotId, 'snapshotId');
182
- const manifestPath = path.join(path.dirname(statePath(input.home, 'snapshots', root, flowId, 'manifest.json')), snapshotId, 'manifest.json');
183
- const manifest = await readJson(manifestPath);
184
- const integrity = verifySnapshotManifest(manifest, { snapshotId, flowId, stepId, repositoryDigest: repoKey(root) });
185
- if (!integrity.verified) fail('ENTROROVE_SNAPSHOT_CORRUPT', integrity.reason ?? 'snapshot manifest is invalid');
186
- const files = [];
187
- for (const expected of manifest.files) {
188
- const source = await projectFile(root, expected.path, true);
189
- const actual = await fileDigest(source);
190
- if (actual !== expected.sha256) fail('ENTROROVE_SNAPSHOT_CHANGED', `${expected.path} changed after snapshot`);
191
- const snapshotTarget = path.join(path.dirname(manifestPath), expected.path);
192
- const snapshotDigest = expected.exists ? sha256(await fs.readFile(snapshotTarget)) : MISSING_SHA256;
193
- if (snapshotDigest !== expected.sha256) fail('ENTROROVE_SNAPSHOT_CORRUPT', `${expected.path} snapshot copy is invalid`);
194
- files.push({ path: expected.path, sourceHash: actual, snapshotHash: expected.sha256 });
195
- }
196
- const core = { schemaVersion: RECEIPT_SCHEMA, snapshotId, flowId, stepId, repositoryDigest: repoKey(root), snapshotDigest: manifest.snapshotDigest, contractDigest: manifest.contractDigest ?? null, paths: manifest.paths, fileManifestDigest: sha256(canonical(files)) };
197
- const receipt = { ...core, receiptDigest: sha256(canonical(core)) };
198
- await audit(input.home, root, 'snapshot-verify', { flowId, stepId, snapshotId, receiptDigest: receipt.receiptDigest });
199
- return { schemaVersion: LOCAL_SCHEMA, verified: true, receipt };
200
- }
201
- async function loadReceipt(input) {
202
- const receipt = input.receipt;
203
- if (!receipt || receipt.schemaVersion !== RECEIPT_SCHEMA || !SHA256.test(receipt.snapshotDigest) || !SHA256.test(receipt.receiptDigest)
204
- || !Array.isArray(receipt.paths) || receipt.receiptDigest !== sha256(canonical({ schemaVersion: receipt.schemaVersion, snapshotId: receipt.snapshotId,
205
- flowId: receipt.flowId, stepId: receipt.stepId, repositoryDigest: receipt.repositoryDigest, snapshotDigest: receipt.snapshotDigest,
206
- contractDigest: receipt.contractDigest ?? null, paths: receipt.paths, fileManifestDigest: receipt.fileManifestDigest }))) fail('ENTROROVE_RECEIPT_INVALID', 'snapshot receipt is invalid');
207
- return receipt;
208
- }
209
- async function snapshotAuthority(input, root, receipt) {
210
- const manifestPath = path.join(path.dirname(statePath(input.home, 'snapshots', root, receipt.flowId, 'manifest.json')), receipt.snapshotId, 'manifest.json'); const manifest = await readJson(manifestPath);
211
- const check = verifySnapshotManifest(manifest, { snapshotId: receipt.snapshotId, flowId: receipt.flowId, stepId: receipt.stepId, repositoryDigest: repoKey(root) });
212
- if (!check.verified || manifest.snapshotDigest !== receipt.snapshotDigest || canonical(manifest.paths) !== canonical(receipt.paths) || (manifest.contractDigest ?? null) !== (receipt.contractDigest ?? null)) fail('ENTROROVE_SNAPSHOT_BINDING', check.reason ?? 'snapshot receipt does not match its manifest');
213
- return { manifest, manifestPath };
214
- }
215
- export async function mutateGate(input) {
216
- if (input.accepted !== true) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', allowed: false, code: 'ENTROROVE_MUTATE_NOT_ACCEPTED' };
217
- id(input.flowId, 'flowId'); id(input.stepId, 'stepId');
218
- const receipt = await loadReceipt(input); const root = await repositoryRoot(input.repositoryRoot);
219
- if (receipt.flowId !== input.flowId || receipt.stepId !== input.stepId || receipt.repositoryDigest !== repoKey(root)) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', allowed: false, code: 'ENTROROVE_RECEIPT_SCOPE' };
220
- await snapshotAuthority(input, root, receipt);
221
- if (input.paths && canonical(normalizePaths(input.paths)) !== canonical(normalizePaths(receipt.paths))) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', allowed: false, code: 'ENTROROVE_RECEIPT_PATHS' };
222
- if (input.coordinationRequired === true) {
223
- try { await verifyCoordinationLease({ ...input, paths: receipt.paths }); } catch (error) { return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', allowed: false, code: error.code ?? 'ENTROROVE_LEASE_INVALID' }; }
173
+ const result = await snapshotVerify14(input);
174
+ await auditMutation14(input, 'snapshot-verify', { snapshotId: input.snapshotId, receiptDigest: result.receipt.receiptDigest });
175
+ return result;
176
+ }
177
+ async function loadReceipt(input) { return loadSnapshotReceipt14(input.receipt); }
178
+ async function snapshotAuthority(input, root, receipt) { return snapshotAuthority14(input, root, receipt); }
179
+ export async function mutateGate(input) { return mutateGate14(input); }
180
+ export async function issueMutationPass(input) { return issueMutationPass14(input); }
181
+ export async function verifyMutationPass(input) { return verifyMutationPass14(input); }
182
+ async function writeSnapshotFile(input, root, targetPath, baseline) {
183
+ if (!baseline || input.expectedSha256 && input.expectedSha256 !== baseline.sha256) fail('ENTROROVE_WRITE_BASELINE', 'target does not match its exact expected snapshot baseline');
184
+ const content = typeof input.content === 'string' ? input.content : JSON.stringify(input.content);
185
+ if (typeof content !== 'string') fail('ENTROROVE_CONTENT_INVALID', 'content must be text or JSON');
186
+ const written = await writeMutationProjectFile14(input, root, targetPath, content, baseline);
187
+ const record = { flowId: input.flowId, stepId: input.stepId, targetPath, sha256: written.sha256, ...(input.handoffId ? { handoffId: input.handoffId } : {}) };
188
+ await auditMutation14(input, 'guarded-write', record);
189
+ await appendLedgerRecord({ home: input.home, repositoryRoot: root }, 'guarded-write', record);
190
+ return { schemaVersion: LOCAL_SCHEMA, status: 'verified', written: true, targetPath, sha256: written.sha256 };
191
+ }
192
+ async function guardedHandoffWrite(input) {
193
+ const required = ['repositoryRoot', 'flowId', 'stepId', 'targetPath', 'content', 'handoffId', 'epoch', 'handoffPass', 'provisionalLease'];
194
+ if (required.some(key => !Object.hasOwn(input, key)) || Object.keys(input).some(key => ![...required, 'home', 'expectedSha256'].includes(key))) {
195
+ fail('ENTROROVE_HANDOFF_WRITE_INPUT', 'takeover write requires only its exact original cloud pass, lease and target fields');
224
196
  }
225
- return { schemaVersion: LOCAL_SCHEMA, status: 'verified', allowed: true, snapshotId: receipt.snapshotId, receiptDigest: receipt.receiptDigest, paths: receipt.paths, coordinationRequired: input.coordinationRequired === true };
226
- }
227
- export async function issueMutationPass(input) {
228
- const gate = await mutateGate(input); if (!gate.allowed) return gate;
229
- const root = await repositoryRoot(input.repositoryRoot); const authority = await keyPair(input.home, root); const now = Date.now();
230
- const ttlSeconds = input.ttlSeconds ?? 300;
231
- if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > 300) fail('ENTROROVE_PASS_TTL_INVALID', 'ttlSeconds must be 1..300');
232
- const pass = { schemaVersion: PASS_SCHEMA, passId: `pass-${randomUUID()}`, flowId: input.flowId, stepId: input.stepId,
233
- repositoryDigest: repoKey(root), snapshotId: gate.snapshotId, snapshotReceiptDigest: gate.receiptDigest, snapshotReceipt: input.receipt, paths: gate.paths, coordinationRequired: gate.coordinationRequired,
234
- issuedAt: new Date(now).toISOString(), expiresAt: new Date(now + ttlSeconds * 1000).toISOString(), keyId: authority.keyId, nonce: randomUUID() };
235
- const signature = sign(null, Buffer.from(canonical(pass)), authority.privateKey).toString('base64url');
236
- const signedPass = { ...pass, signature }; const file = statePath(input.home, 'gates', root, input.flowId, `${pass.passId}.json`);
237
- await atomicJson(file, signedPass); await audit(input.home, root, 'mutation-pass-issued', { flowId: input.flowId, stepId: input.stepId, passId: pass.passId });
238
- return { schemaVersion: LOCAL_SCHEMA, gatePassPath: file, pass: signedPass };
239
- }
240
- async function verifyPass(input) {
197
+ const root = await repositoryRoot(input.repositoryRoot), target = safePath(input.targetPath, 'targetPath');
241
198
  id(input.flowId, 'flowId'); id(input.stepId, 'stepId');
242
- const root = await repositoryRoot(input.repositoryRoot); const gateDirectory = path.dirname(statePath(input.home, 'gates', root, input.flowId, 'pass.json'));
243
- const gatePath = path.resolve(input.gatePassPath ?? ''); inside(gateDirectory, gatePath, 'gatePassPath');
244
- const pass = await readJson(gatePath); const authority = await keyPair(input.home, root);
245
- const target = safePath(input.targetPath, 'targetPath'); const paths = Array.isArray(pass.paths) ? pass.paths : [];
246
- const valid = pass.schemaVersion === PASS_SCHEMA && pass.repositoryDigest === repoKey(root) && pass.flowId === input.flowId && pass.stepId === input.stepId
247
- && paths.includes(target) && pass.keyId === authority.keyId && typeof pass.signature === 'string'
248
- && verify(null, Buffer.from(canonical(Object.fromEntries(Object.entries(pass).filter(([key]) => key !== 'signature')))), authority.publicKey, Buffer.from(pass.signature, 'base64url'))
249
- && Date.parse(pass.issuedAt) <= Date.now() && Date.parse(pass.expiresAt) > Date.now();
250
- if (!valid) fail('ENTROROVE_PASS_INVALID', 'mutation pass is forged, expired, or outside its scope');
251
- if (!pass.snapshotReceipt || pass.snapshotReceipt.receiptDigest !== pass.snapshotReceiptDigest) fail('ENTROROVE_PASS_INVALID', 'mutation pass has no bound snapshot receipt');
252
- const receipt = await loadReceipt({ receipt: pass.snapshotReceipt });
253
- await snapshotAuthority(input, root, receipt);
254
- if (receipt.flowId !== pass.flowId || receipt.stepId !== pass.stepId || receipt.repositoryDigest !== pass.repositoryDigest || receipt.snapshotId !== pass.snapshotId) fail('ENTROROVE_PASS_INVALID', 'mutation pass snapshot scope is invalid');
255
- if (pass.coordinationRequired === true) await verifyCoordinationLease({ ...input, paths: [target] });
256
- return { root, pass, target };
199
+ const lock = path.join(homeRoot(input.home), 'write-locks', repoKey(root), `${sha256(target)}.lock`);
200
+ return withStateLock(lock, () => withTaskHandoffWriteFence({ ...input, repositoryRoot: root,
201
+ passId: input.handoffPass?.passId, leaseId: input.provisionalLease?.leaseId }, async (state, capability) => {
202
+ if (!state.receiverClaim || state.receiverRestoration) fail('ENTROROVE_HANDOFF_WRITE_REVOKED', 'takeover scope is absent or already restored');
203
+ const supplied = { pass: input.handoffPass, lease: input.provisionalLease, challenge: state.receiverClaim.challenge };
204
+ const { pass, lease, challenge } = receiverClaim(supplied, { flowId: input.flowId, repo: repoKey(root) });
205
+ if (canonical(supplied) !== canonical(state.receiverClaim) || input.handoffId !== pass.handoffId
206
+ || input.epoch !== pass.receiverEpoch || input.stepId !== lease.stepId || !pass.paths.includes(target)) {
207
+ fail('ENTROROVE_HANDOFF_WRITE_BINDING', 'takeover write changed its actual receiver, signed grant or exact path scope');
208
+ }
209
+ const manifestPath = path.join(path.dirname(statePath(input.home, 'snapshots', root, input.flowId, 'manifest.json')), pass.snapshotId, 'manifest.json');
210
+ const manifest = await readJson(manifestPath);
211
+ const integrity = verifySnapshotManifest(manifest, { snapshotId: pass.snapshotId, flowId: input.flowId, stepId: lease.stepId, repositoryDigest: repoKey(root) });
212
+ if (!integrity.verified || manifest.snapshotDigest !== pass.scopeSnapshotDigest || canonical(manifest.paths) !== canonical(pass.paths)
213
+ || manifest.handoffEpoch !== pass.receiverEpoch || manifest.handoffBaselineReceiptDigest !== challenge.baselineReceiptDigest) {
214
+ fail('ENTROROVE_HANDOFF_WRITE_SNAPSHOT', 'takeover write requires its original fresh snapshot and actual release baseline');
215
+ }
216
+ const baseline = manifest.files.find(file => file.path === target);
217
+ if (!baseline || (baseline.exists && sha256((await readSnapshotCopy14(input, path.dirname(manifestPath), manifest, target)).content) !== baseline.sha256)) {
218
+ fail('ENTROROVE_HANDOFF_WRITE_SNAPSHOT', 'takeover snapshot copy changed before the actual physical write');
219
+ }
220
+ return runHandoffTrustedWrite({ input: { ...input, targetPath: target }, ...capability, manifest,
221
+ snapshotDirectory: path.dirname(manifestPath),
222
+ performActualWrite: ({ input: actualInput, baseline: current }) => writeSnapshotFile(actualInput, root, target, current) });
223
+ }));
257
224
  }
258
- export async function verifyMutationPass(input) { const verified = await verifyPass(input); return { schemaVersion: LOCAL_SCHEMA, verified: true, pass: verified.pass, targetPath: verified.target }; }
259
225
  export async function guardedWrite(input) {
260
- const verified = await verifyPass(input);
261
- const lockFile = path.join(homeRoot(input.home), 'write-locks', repoKey(verified.root), `${sha256(verified.target)}.lock`);
262
- return withStateLock(lockFile, async () => {
263
- const target = await projectFile(verified.root, verified.target, true);
264
- const { manifest } = await snapshotAuthority(input, verified.root, verified.pass.snapshotReceipt); const baseline = manifest.files.find(item => item.path === verified.target);
265
- if (!baseline) fail('ENTROROVE_WRITE_SCOPE', 'target is absent from the snapshot manifest');
266
- if (await fileDigest(target) !== baseline.sha256) fail('ENTROROVE_WRITE_BASELINE', 'target changed after the snapshot');
267
- const expected = input.expectedSha256 ?? null;
268
- if (expected && (!SHA256.test(expected) || await fileDigest(target) !== expected)) fail('ENTROROVE_WRITE_BASELINE', 'target does not match the expected snapshot hash');
269
- const content = typeof input.content === 'string' ? input.content : JSON.stringify(input.content);
270
- if (typeof content !== 'string') fail('ENTROROVE_CONTENT_INVALID', 'content must be text or JSON');
271
- await privateDir(path.dirname(target.target)); const temporary = path.join(path.dirname(target.target), `.${path.basename(target.target)}.${randomUUID()}.tmp`);
272
- await fs.writeFile(temporary, content, { flag: 'wx', mode: target.exists ? target.stat.mode & 0o777 : 0o600 }); await fs.rename(temporary, target.target);
273
- await audit(input.home, verified.root, 'guarded-write', { flowId: input.flowId, stepId: input.stepId, targetPath: verified.target });
274
- await appendLedgerRecord({ home: input.home, repositoryRoot: verified.root }, 'guarded-write', { flowId: input.flowId, stepId: input.stepId, targetPath: verified.target, sha256: sha256(content) });
275
- return { schemaVersion: LOCAL_SCHEMA, status: 'verified', written: true, targetPath: verified.target, sha256: sha256(content) };
276
- });
277
- }
278
- function enforced(context) {
279
- if (!context || typeof context !== 'object' || Array.isArray(context)) fail('ENTROROVE_CONTEXT_REQUIRED', 'executionContext is required');
280
- if (Object.keys(context).length === 1 && context.executionIsolation === 'local') return false;
281
- if (Object.keys(context).length !== 2 || !['direct', 'sandbox'].includes(context.executionIsolation) || typeof context.cloudSandboxEnabled !== 'boolean') fail('ENTROROVE_CONTEXT_INVALID', 'executionContext is invalid');
282
- if (context.executionIsolation === 'sandbox' && context.cloudSandboxEnabled !== true) fail('ENTROROVE_CONTEXT_INVALID', 'sandbox requires cloudSandboxEnabled=true');
283
- return context.executionIsolation === 'sandbox';
284
- }
285
- async function budgetFile(input) { const root = await repositoryRoot(input.repositoryRoot); const flowId = id(input.flowId, 'flowId'); return { root, file: statePath(input.home, 'budgets', root, flowId, 'budget.json') }; }
286
- function budgetView(state, now = Date.now()) {
287
- const elapsedMs = state.enforced ? state.elapsedMs + now - Date.parse(state.startedAt) : state.elapsedMs;
288
- return { ...state, elapsedMs, remainingFiles: state.enforced ? Math.max(0, state.maxFiles - state.files.length) : null,
289
- remainingTokenEstimate: state.enforced && state.maxTokenEstimate !== null ? Math.max(0, state.maxTokenEstimate - state.tokenEstimate) : null,
290
- remainingDurationMs: state.enforced ? Math.max(0, state.maxDurationMs - elapsedMs) : null,
291
- enforcement: state.enforced ? 'cloud-sandbox' : 'continuous' };
292
- }
293
- export async function budgetInit(input) {
294
- const mode = input.mode; if (!READ_BUDGETS[mode]) fail('ENTROROVE_MODE_INVALID', 'mode must be lock, probe, or swarm');
295
- const { root, file } = await budgetFile(input); const limits = READ_BUDGETS[mode]; const isEnforced = enforced(input.executionContext);
296
- return withStateLock(file, async () => {
297
- try { const existing = await readJson(file); if (existing.schemaVersion !== BUDGET_SCHEMA || existing.flowId !== input.flowId || existing.repositoryDigest !== repoKey(root) || existing.mode !== mode || canonical(existing.executionContext) !== canonical(input.executionContext)) fail('ENTROROVE_BUDGET_REINITIALIZE', 'budget is already initialized with a different scope'); return budgetView(existing); } catch (error) { if (error.code !== 'ENOENT') throw error; }
298
- const state = { schemaVersion: BUDGET_SCHEMA, flowId: input.flowId, repositoryDigest: repoKey(root), mode, executionContext: input.executionContext,
299
- enforced: isEnforced, startedAt: new Date().toISOString(), elapsedMs: 0, ...limits, files: [], readCalls: 0, tokenEstimate: 0, extensions: [] };
300
- await atomicJson(file, state); await audit(input.home, root, 'budget-init', { flowId: input.flowId, mode }); return budgetView(state);
301
- });
302
- }
303
- export async function budgetContext(input) {
304
- const { root, file } = await budgetFile(input);
305
- return withStateLock(file, async () => {
306
- const state = await readJson(file); const isEnforced = enforced(input.executionContext); if (state.enforced && !isEnforced) fail('ENTROROVE_BUDGET_DOWNGRADE', 'an enforced sandbox budget cannot be downgraded');
307
- const updated = { ...state, executionContext: input.executionContext, enforced: isEnforced }; await atomicJson(file, updated); return budgetView(updated);
308
- });
309
- }
310
- export async function budgetStatus(input) { const { file } = await budgetFile(input); return budgetView(await readJson(file)); }
311
- export async function budgetRead(input) {
312
- const { root, file } = await budgetFile(input);
313
- return withStateLock(file, async () => {
314
- const relative = safePath(input.path); const originalState = await readJson(file); const state = await consumeBudgetRenewal({ file, state: originalState }); const before = budgetView(state);
315
- const source = await projectFile(root, relative); const bytes = source.stat.size; const tokens = Math.ceil(bytes / 4); const fresh = !state.files.includes(relative);
316
- if (state.enforced && fresh && before.remainingFiles <= 0) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read file budget exhausted');
317
- if (state.enforced && before.remainingTokenEstimate !== null && tokens > before.remainingTokenEstimate) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read token budget exhausted');
318
- if (state.enforced && before.remainingDurationMs <= 0) fail('ENTROROVE_BUDGET_EXHAUSTED', 'read duration budget exhausted');
319
- const updated = { ...state, files: fresh ? [...state.files, relative] : state.files, readCalls: state.readCalls + 1, tokenEstimate: state.tokenEstimate + tokens };
320
- await atomicJson(file, updated); await audit(input.home, root, 'budget-read', { flowId: input.flowId, path: relative, tokens });
321
- return { schemaVersion: LOCAL_SCHEMA, path: relative, content: await fs.readFile(source.target, 'utf8'), budget: budgetView(updated) };
322
- });
323
- }
324
- export async function budgetExtend(input) {
325
- const { root, file } = await budgetFile(input);
326
- return withStateLock(file, async () => {
327
- const state = await readJson(file); if (!state.enforced) fail('ENTROROVE_BUDGET_NOT_APPLICABLE', 'budget extension applies only to a cloud sandbox');
328
- const confirmation = input.confirmation; const additions = input.additions;
329
- if (confirmation?.answer !== 'approve' || typeof confirmation.requestId !== 'string') fail('ENTROROVE_CONFIRMATION_REQUIRED', 'explicit budget confirmation is required');
330
- if (!additions || !Number.isSafeInteger(additions.files) || !Number.isSafeInteger(additions.tokenEstimate) || !Number.isSafeInteger(additions.durationMs)
331
- || additions.files < 0 || additions.tokenEstimate < 0 || additions.durationMs < 0 || additions.files + additions.tokenEstimate + additions.durationMs === 0) fail('ENTROROVE_EXTENSION_INVALID', 'budget additions must be positive integers');
332
- if (state.extensions.some(item => item.requestId === confirmation.requestId)) fail('ENTROROVE_CONFIRMATION_REPLAYED', 'budget confirmation was already applied');
333
- const updated = { ...state, maxFiles: state.maxFiles + additions.files, maxTokenEstimate: state.maxTokenEstimate === null ? additions.tokenEstimate : state.maxTokenEstimate + additions.tokenEstimate,
334
- maxDurationMs: state.maxDurationMs + additions.durationMs, extensions: [...state.extensions, { requestId: confirmation.requestId, additions, at: new Date().toISOString() }] };
335
- await atomicJson(file, updated); await audit(input.home, root, 'budget-extend', { flowId: input.flowId, requestId: confirmation.requestId }); return budgetView(updated);
336
- });
337
- }
338
- async function coordinationState(input, create = false) {
339
- const root = await repositoryRoot(input.repositoryRoot); const flowId = id(input.flowId, 'flowId'); const file = statePath(input.home, 'coordination', root, flowId, 'state.json');
340
- if (create) { try { return { root, file, state: await readJson(file) }; } catch (error) { if (error.code !== 'ENOENT') throw error; const state = { schemaVersion: COORDINATION_SCHEMA, flowId, repositoryDigest: repoKey(root), workers: [], leases: [], events: [] }; await atomicJson(file, state); return { root, file, state }; } }
341
- return { root, file, state: await readJson(file) };
342
- }
343
- async function coordinateUnlocked(input) {
344
- const workerCount = input.workerCount ?? 0; if (!Number.isSafeInteger(workerCount) || workerCount < 0 || workerCount > 50) fail('ENTROROVE_WORKER_LIMIT', 'workerCount must be between 0 and 50');
345
- if (input.priority !== undefined && !['urgent', 'normal', 'dependency', 'background'].includes(input.priority)) fail('ENTROROVE_PRIORITY_INVALID', 'priority is invalid');
346
- const authority = await coordinationState(input, true); const updated = { ...authority.state, maxWorkers: workerCount, priority: input.priority ?? 'normal', events: [...authority.state.events, { event: 'coordinate', at: new Date().toISOString() }] };
347
- await atomicJson(authority.file, updated); await audit(input.home, authority.root, 'peer-coordinate', { flowId: input.flowId, workerCount }); return { schemaVersion: LOCAL_SCHEMA, status: 'verified', maxWorkers: workerCount, priority: updated.priority };
348
- }
349
- export async function peerCoordinate(input) {
350
- const root = await repositoryRoot(input.repositoryRoot); const flowId = id(input.flowId, 'flowId');
351
- return withStateLock(statePath(input.home, 'coordination', root, flowId, 'state.json'), () => coordinateUnlocked(input));
352
- }
353
- async function spawnUnlocked(input) {
354
- const paths = normalizePaths(input.paths); const priority = input.priority ?? 'normal'; if (!['urgent', 'normal', 'dependency', 'background'].includes(priority)) fail('ENTROROVE_PRIORITY_INVALID', 'priority is invalid');
355
- const authority = await coordinationState(input, true); const active = authority.state.workers.filter(worker => worker.status === 'active'); if (active.length >= (authority.state.maxWorkers ?? 0)) fail('ENTROROVE_WORKER_LIMIT', 'coordination worker budget is exhausted');
356
- if (input.parentWorkerId) fail('ENTROROVE_RECURSION_FORBIDDEN', 'derived workers cannot recursively spawn workers');
357
- const workerId = id(input.workerId ?? `worker-${randomUUID()}`, 'workerId'); const leaseId = `lease-${randomUUID()}`; const expiresAt = new Date(Date.now() + 300_000).toISOString();
358
- const worker = { workerId, leaseId, flowId: input.flowId, paths, priority, status: 'active', expiresAt, createdAt: new Date().toISOString() };
359
- await atomicJson(authority.file, { ...authority.state, workers: [...authority.state.workers, worker], leases: [...authority.state.leases, worker], events: [...authority.state.events, { event: 'spawn-bind', workerId, at: new Date().toISOString() }] });
360
- return { schemaVersion: LOCAL_SCHEMA, status: 'verified', workerId, leaseId, paths, expiresAt };
361
- }
362
- export async function spawnBind(input) {
363
- const root = await repositoryRoot(input.repositoryRoot); const flowId = id(input.flowId, 'flowId');
364
- return withStateLock(statePath(input.home, 'coordination', root, flowId, 'state.json'), () => spawnUnlocked(input));
365
- }
366
- async function completeUnlocked(input) {
367
- const authority = await coordinationState(input); const worker = authority.state.workers.find(item => item.workerId === input.workerId && item.leaseId === input.leaseId && item.status === 'active'); if (!worker) fail('ENTROROVE_LEASE_INVALID', 'coordination lease is invalid');
368
- if (Date.parse(worker.expiresAt) <= Date.now()) fail('ENTROROVE_LEASE_EXPIRED', 'coordination lease has expired');
369
- const status = input.status ?? 'completed'; if (!['completed', 'failed', 'blocked'].includes(status)) fail('ENTROROVE_STATUS_INVALID', 'peer status is invalid');
370
- const updatedWorker = { ...worker, status, completedAt: new Date().toISOString() }; const state = { ...authority.state, workers: authority.state.workers.map(item => item.workerId === worker.workerId ? updatedWorker : item), leases: authority.state.leases.map(item => item.workerId === worker.workerId ? updatedWorker : item) };
371
- await atomicJson(authority.file, state); return { schemaVersion: LOCAL_SCHEMA, status: 'verified', workerId: worker.workerId, state: status };
372
- }
373
- export async function completePeer(input) {
374
- const root = await repositoryRoot(input.repositoryRoot); const flowId = id(input.flowId, 'flowId');
375
- return withStateLock(statePath(input.home, 'coordination', root, flowId, 'state.json'), () => completeUnlocked(input));
376
- }
226
+ if (Object.hasOwn(input, 'handoffPass') || Object.hasOwn(input, 'provisionalLease')) {
227
+ return withMutationAudit14(input, 'guarded-write', () => guardedHandoffWrite(input));
228
+ }
229
+ return guardedWrite14(input);
230
+ }
231
+
232
+
233
+
234
+
235
+
236
+
237
+
377
238
  async function verifyEvidenceValue(evidence, input, requireBinding = false) {
378
- const fields = ['schemaVersion', 'evidenceId', 'kind', 'runner', 'command', 'exitCode', 'durationMs', 'summary', 'artifactSha256', 'subject', 'subjectDigest', 'receipt', 'flowId', 'stepId'];
379
- if (!evidence || Object.keys(evidence).some(field => !fields.includes(field)) || evidence.schemaVersion !== EVIDENCE_SCHEMA || typeof evidence.evidenceId !== 'string' || !evidence.evidenceId.trim() || evidence.evidenceId.length > 200 || !['test', 'build', 'lint', 'security', 'benchmark'].includes(evidence.kind)
380
- || !['local', 'trusted-runner', 'sandbox'].includes(evidence.runner) || typeof evidence.command !== 'string' || evidence.command.trim().length === 0 || evidence.command.length > 2_000 || !Number.isInteger(evidence.exitCode) || evidence.exitCode < -128 || evidence.exitCode > 255
381
- || !Number.isInteger(evidence.durationMs) || evidence.durationMs < 0 || evidence.durationMs > 86_400_000 || typeof evidence.summary !== 'string' || evidence.summary.trim().length === 0 || evidence.summary.length > 4_000 || !SHA256.test(evidence.artifactSha256)) return { verified: false, reason: 'evidence schema invalid' };
382
- if (evidence.runner === 'local') return { verified: false, reason: 'local evidence is incomplete without a trusted receipt' };
383
- const receipt = evidence.receipt; if (!receipt || ![EVIDENCE_RECEIPT_SCHEMA, 'validator.execution-receipt/1.0'].includes(receipt.schemaVersion)) return { verified: false, reason: 'signed evidence receipt is required' };
384
- const binding = { flowId: input.flowId, stepId: input.stepId, snapshotDigest: input.snapshotDigest, contractDigest: input.contractDigest };
385
- if (requireBinding) return verifyTrustedEvidence(evidence, binding);
386
- const subject = Object.fromEntries(Object.entries(evidence).filter(([key]) => key !== 'receipt'));
387
- try { if (receipt.schemaVersion === 'validator.execution-receipt/1.0') { if (typeof input.receiptPublicKey !== 'string' || receipt.subjectDigest !== evidence.subjectDigest || receipt.result?.runner !== 'trusted-runner' || receipt.result.exitCode !== evidence.exitCode || receipt.result.durationMs !== evidence.durationMs || receipt.result.summary !== evidence.summary) throw new Error('Trusted execution receipt is not bound'); const unsigned = Object.fromEntries(Object.entries(receipt).filter(([key]) => key !== 'signature')); verifyReceipt(receipt, unsigned, { schemaVersion: 'validator.execution-receipt/1.0', publicKey: input.receiptPublicKey }); } else verifyReceipt(receipt, subject); return { verified: true, evidence }; } catch (error) { return { verified: false, reason: error instanceof Error ? error.message : String(error) }; }
388
- }
389
- export async function verifyCoordinationLease(input) {
390
- const authority = await coordinationState(input); const worker = authority.state.workers.find(item => item.workerId === input.workerId && item.leaseId === input.leaseId && item.status === 'active');
391
- if (!worker || Date.parse(worker.expiresAt) <= Date.now()) fail('ENTROROVE_LEASE_INVALID', 'coordination lease is invalid or expired');
392
- const paths = normalizePaths(input.paths ?? []); if (paths.some(target => !worker.paths.some(scope => target === scope || target.startsWith(`${scope}/`)))) fail('ENTROROVE_LEASE_SCOPE', 'coordination lease does not cover target paths');
393
- return { schemaVersion: LOCAL_SCHEMA, verified: true, workerId: worker.workerId, leaseId: worker.leaseId, paths: worker.paths, expiresAt: worker.expiresAt };
239
+ const publicKey = process.env.ENTROROVE_VALIDATOR_RECEIPT_PUBLIC_KEY?.trim();
240
+ if (publicKey && input.receiptPublicKey !== undefined && input.receiptPublicKey !== publicKey) {
241
+ return { verified: false, authentic: false, successful: false, status: 'blocked', evidenceState: 'blocked',
242
+ code: 'ENTROROVE_EVIDENCE_KEY', reason: 'Supplied receipt key does not match independent verifier configuration' };
243
+ }
244
+ const binding = { flowId: input.flowId, stepId: input.stepId, snapshotDigest: input.snapshotDigest,
245
+ contractDigest: input.contractDigest, receiptPublicKey: input.receiptPublicKey };
246
+ return requireBinding ? verifyTrustedEvidence(evidence, binding) : verifyTestEvidence(evidence, { publicKey, binding });
394
247
  }
248
+
395
249
  export async function signEvidence(input) {
396
250
  const root = await repositoryRoot(input.repositoryRoot); const authority = await keyPair(input.home, root); const evidence = { ...input.evidence, schemaVersion: EVIDENCE_SCHEMA, runner: input.evidence?.runner ?? 'local' };
397
251
  if (typeof evidence.evidenceId !== 'string' || !evidence.evidenceId.trim() || evidence.evidenceId.length > 200) fail('ENTROROVE_EVIDENCE_ID', 'evidenceId must be a non-empty string up to 200 characters');
@@ -402,69 +256,26 @@ export async function signEvidence(input) {
402
256
  return { ...evidence, receipt };
403
257
  }
404
258
  export async function verifyEvidence(input) { return verifyEvidenceValue(input.evidence, input); }
405
- export async function mergeVerified(input) {
406
- const root = await repositoryRoot(input.repositoryRoot); id(input.flowId, 'flowId'); id(input.stepId, 'stepId'); if (!input.sourceRef || !input.targetRef) fail('ENTROROVE_MERGE_REFS_REQUIRED', 'sourceRef and targetRef are required');
407
- if (!input.snapshotReceipt) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', code: 'ENTROROVE_SNAPSHOT_REQUIRED' };
408
- const snapshotReceipt = await loadReceipt({ receipt: input.snapshotReceipt });
409
- if (snapshotReceipt.flowId !== input.flowId || snapshotReceipt.stepId !== input.stepId || snapshotReceipt.repositoryDigest !== repoKey(root)) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', code: 'ENTROROVE_SNAPSHOT_SCOPE' };
410
- let manifest;
411
- try { ({ manifest } = await snapshotAuthority(input, root, snapshotReceipt)); } catch (error) { return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', code: error.code ?? 'ENTROROVE_SNAPSHOT_BINDING' }; }
412
- const binding = { flowId: input.flowId, stepId: input.stepId, snapshotDigest: snapshotReceipt.snapshotDigest, contractDigest: snapshotReceipt.contractDigest, receiptPublicKey: input.receiptPublicKey };
413
- const preflight = verifyArchguardPreflight(input.preflight, binding);
414
- if (!preflight.verified) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', code: 'ENTROROVE_PREFLIGHT_BLOCKED', reason: preflight.reason };
415
- if (!SHA256.test(binding.contractDigest ?? '')) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', code: 'ENTROROVE_CONTRACT_DIGEST' };
416
- if (!Array.isArray(input.evidence) || input.evidence.length === 0) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', code: 'ENTROROVE_EVIDENCE_REQUIRED' };
417
- const checks = await Promise.all(input.evidence.map(evidence => verifyEvidenceValue(evidence, binding, true))); if (checks.some(check => !check.verified) || input.evidence.some(evidence => evidence.exitCode !== 0)) return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', code: 'ENTROROVE_EVIDENCE_UNVERIFIED', checks };
418
- let written = 0; for (const change of input.changes ?? []) { await guardedWrite({ ...change, repositoryRoot: root, home: input.home, flowId: input.flowId, stepId: input.stepId }); written += 1; }
419
- await audit(input.home, root, 'merge-verified', { flowId: input.flowId, stepId: input.stepId, sourceRef: input.sourceRef, targetRef: input.targetRef });
420
- await appendLedgerRecord({ home: input.home, repositoryRoot: root }, 'merge-verified', { flowId: input.flowId, stepId: input.stepId, sourceRef: input.sourceRef, targetRef: input.targetRef, snapshotDigest: binding.snapshotDigest, contractDigest: binding.contractDigest });
421
- return { schemaVersion: LOCAL_SCHEMA, status: 'verified', merged: true, changes: written, sourceRef: input.sourceRef, targetRef: input.targetRef, preflightKeyId: preflight.keyId };
422
- }
423
- export async function rollback(input) {
424
- const root = await repositoryRoot(input.repositoryRoot); const flowId = id(input.flowId, 'flowId'); const snapshotId = id(input.snapshotId, 'snapshotId'); const manifestPath = path.join(path.dirname(statePath(input.home, 'snapshots', root, flowId, 'manifest.json')), snapshotId, 'manifest.json'); const manifest = await readJson(manifestPath);
425
- const integrity = verifySnapshotManifest(manifest, { snapshotId, flowId, stepId: input.stepId ?? manifest.stepId, repositoryDigest: repoKey(root) });
426
- if (!integrity.verified) fail('ENTROROVE_SNAPSHOT_CORRUPT', integrity.reason ?? 'snapshot manifest is invalid');
427
- for (const entry of manifest.files) { const target = await projectFile(root, entry.path, true); const copy = path.join(path.dirname(manifestPath), entry.path); if (entry.exists) { if (sha256(await fs.readFile(copy)) !== entry.sha256) fail('ENTROROVE_SNAPSHOT_CORRUPT', `${entry.path} snapshot copy is invalid`); await privateDir(path.dirname(target.target)); await fs.copyFile(copy, target.target); } else if (target.exists) await fs.unlink(target.target); }
428
- await audit(input.home, root, 'rollback', { flowId, stepId: input.stepId ?? null, snapshotId }); return { schemaVersion: LOCAL_SCHEMA, status: 'verified', rolledBack: true, snapshotId };
429
- }
259
+ export async function mergeVerified(input) { return executeVerifiedMerge(input); }
260
+ export async function rollback(input) { return rollbackMerge(input); }
430
261
  export async function finalGate(input) {
431
- const checks = [input.audit, input.test, input.operations]; const passed = checks.every(check => check === true || ['passed', 'green', 'complete'].includes(check?.status));
432
- const evidence = Array.isArray(input.evidence) ? input.evidence : [];
433
- const bound = input.preflight !== undefined || input.snapshotDigest !== undefined || input.contractDigest !== undefined;
434
- const binding = { flowId: input.flowId, stepId: input.stepId, snapshotDigest: input.snapshotDigest, contractDigest: input.contractDigest, receiptPublicKey: input.receiptPublicKey };
435
- const preflight = bound ? verifyArchguardPreflight(input.preflight, binding) : { verified: true };
436
- const validation = evidence.length ? await Promise.all(evidence.map(item => verifyEvidenceValue(item, bound ? binding : input, bound))) : [];
437
- const trusted = evidence.length > 0 && preflight.verified && validation.every(item => item.verified) && evidence.every(evidenceItem => evidenceItem.exitCode === 0);
438
- const complete = passed && trusted;
262
+ if (input.gateId) {
263
+ const { runFinalGate16 } = await import('./local-final-gate16.mjs');
264
+ return runFinalGate16(input);
265
+ }
439
266
  if (input.repositoryRoot) {
440
267
  const root = await repositoryRoot(input.repositoryRoot);
441
- await appendLedgerRecord({ home: input.home, repositoryRoot: root }, 'final-gate', { flowId: input.flowId ?? null, stepId: input.stepId ?? null, status: complete ? 'complete' : 'blocked', evidenceCount: evidence.length });
268
+ await appendLedgerRecord({ home: input.home, repositoryRoot: root }, 'final-gate',
269
+ { flowId: input.flowId ?? null, stepId: input.stepId ?? null, status: 'blocked',
270
+ code: 'ENTROROVE_FINAL_GATE_ACTUAL_CHECKS_REQUIRED' });
442
271
  }
443
- return { schemaVersion: LOCAL_SCHEMA, status: complete ? 'complete' : 'blocked', authority: 'trusted-local-runner', checks: { audit: Boolean(passed && checks[0]), test: Boolean(passed && checks[1]), operations: Boolean(passed && checks[2]) }, evidenceCount: evidence.length, guarantee: complete ? 'local-completion-gate-green' : 'local-completion-gate-blocked', preflight: preflight.verified ? 'verified' : 'blocked' };
272
+ return { schemaVersion: LOCAL_SCHEMA, status: 'blocked', authority: 'local-evidence',
273
+ code: 'ENTROROVE_FINAL_GATE_ACTUAL_CHECKS_REQUIRED',
274
+ checks: { audit: false, test: false, operations: false },
275
+ guarantee: 'Original engine, frozen role plans and actual three-party checks are required' };
444
276
  }
445
- export const LOCAL_OPERATION_SCHEMAS = Object.fromEntries([
446
- ['capabilities', []], ['snapshot-plan', ['repositoryRoot', 'flowId', 'stepId', 'paths']], ['snapshot-verify', ['repositoryRoot', 'flowId', 'stepId', 'snapshotId']],
447
- ['mutate-gate', ['repositoryRoot', 'flowId', 'stepId', 'accepted', 'receipt']], ['gate-issue', ['repositoryRoot', 'flowId', 'stepId', 'accepted', 'receipt']],
448
- ['gate-verify', ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath']], ['guarded-write', ['repositoryRoot', 'flowId', 'stepId', 'gatePassPath', 'targetPath', 'content']],
449
- ['budget-init', ['repositoryRoot', 'flowId', 'mode', 'executionContext']], ['budget-context', ['repositoryRoot', 'flowId', 'executionContext']], ['budget-read', ['repositoryRoot', 'flowId', 'path']], ['budget-status', ['repositoryRoot', 'flowId']], ['budget-extend', ['repositoryRoot', 'flowId', 'confirmation', 'additions']],
450
- ['peer-coordinate', ['repositoryRoot', 'flowId']], ['coordinate', ['repositoryRoot', 'flowId']], ['spawn-bind', ['repositoryRoot', 'flowId', 'paths']], ['peer-complete', ['repositoryRoot', 'flowId', 'workerId', 'leaseId']], ['complete', ['repositoryRoot', 'flowId', 'workerId', 'leaseId']], ['lease-verify', ['repositoryRoot', 'flowId', 'workerId', 'leaseId', 'paths']],
451
- ['evidence-sign', ['repositoryRoot', 'evidence']], ['evidence-verify', ['repositoryRoot', 'evidence']], ['merge-verified', ['repositoryRoot', 'flowId', 'stepId', 'sourceRef', 'targetRef', 'snapshotReceipt', 'evidence', 'preflight']], ['rollback', ['repositoryRoot', 'flowId', 'snapshotId']], ['final-gate', ['repositoryRoot', 'audit', 'test', 'operations', 'evidence']],
452
- ['formula-evaluate', ['formula']], ['formula-graph', ['formulas']], ['formula-final-gate', ['engine']], ['calculation-final-gate', ['engine']],
453
- ['run-approved-plan', ['repositoryRoot', 'planPath', 'approvalPath', 'signerConfigPath']],
454
- ].map(([operation, required]) => [operation, { type: 'object', additionalProperties: false, required }]));
455
- Object.assign(LOCAL_OPERATION_SCHEMAS, LOCAL_LEDGER_SCHEMAS);
456
- Object.assign(LOCAL_OPERATION_SCHEMAS, LOCAL_RENEWAL_SCHEMAS, LOCAL_PROBE_SCHEMAS);
457
- Object.assign(LOCAL_OPERATION_SCHEMAS, LOCAL_TASK_ROUTING_SCHEMAS);
458
- Object.freeze(LOCAL_OPERATION_SCHEMAS);
459
- export const LOCAL_CAPABILITIES = Object.freeze({ schemaVersion: LOCAL_SCHEMA, operations: Object.freeze([
460
- 'capabilities', 'snapshot-plan', 'snapshot-verify', 'mutate-gate', 'gate-issue', 'gate-verify', 'guarded-write',
461
- 'budget-init', 'budget-context', 'budget-read', 'budget-status', 'budget-extend', 'peer-coordinate', 'spawn-bind',
462
- 'peer-complete', 'complete', 'lease-verify', 'evidence-sign', 'evidence-verify', 'merge-verified', 'rollback', 'final-gate',
463
- 'formula-evaluate', 'formula-graph', 'formula-final-gate', 'calculation-final-gate', 'run-approved-plan',
464
- ...Object.keys(LOCAL_LEDGER_OPERATIONS),
465
- ...Object.keys(LOCAL_RENEWAL_OPERATIONS), ...Object.keys(LOCAL_PROBE_OPERATIONS),
466
- ...Object.keys(LOCAL_TASK_ROUTING_OPERATIONS),
467
- ]), operationSchemas: LOCAL_OPERATION_SCHEMAS, stateDirectory: '~/.entrorove', forbidGitBranch: true, forbidWorktree: true, evidenceSchema: EVIDENCE_SCHEMA, budgets: READ_BUDGETS, validatorRunner: RUNNER_CAPABILITIES });
277
+
278
+ export const LOCAL_CAPABILITIES = Object.freeze({ schemaVersion: LOCAL_SCHEMA, operations: Object.freeze(Object.keys(LOCAL_OPERATION_SCHEMAS)), operationSchemas: LOCAL_OPERATION_SCHEMAS, stateDirectory: '~/.entrorove', forbidGitBranch: true, forbidWorktree: true, evidenceSchema: EVIDENCE_SCHEMA, budgets: READ_BUDGETS, validatorRunner: RUNNER_CAPABILITIES, workflowRunner: WORKFLOW_LOCAL_CAPABILITIES });
468
279
  export function capabilities() { return LOCAL_CAPABILITIES; }
469
280
  export const guardedWriteFile = guardedWrite;
470
281
  export const verifyMutationPassFile = verifyMutationPass;
@@ -474,7 +285,7 @@ export const readFileWithinBudget = budgetRead;
474
285
  export const readBudgetStatus = budgetStatus;
475
286
  export const extendReadBudget = budgetExtend;
476
287
  export const runLocalFinalGate = finalGate;
477
- export { Decimal, evaluateFormula, evaluateFormulaGraph, formulaFinalGate, formulaFinalGateFile, runApprovedPlan };
288
+ export { Decimal, evaluateFormula, evaluateFormulaGraph, formulaFinalGate, formulaFinalGateFile, runApprovedPlan, runWorkflowLocalStep };
478
289
  export { LOCAL_LEDGER_OPERATIONS };
479
290
  export { LOCAL_RENEWAL_OPERATIONS, LOCAL_PROBE_OPERATIONS };
480
291
  export { branchCreate, branchList, branchSwitch, diffReport, preflight, ledgerQuery, appendLedgerRecord, chainInit, chainResume, chainStatus, chainAnswer };
@@ -0,0 +1,21 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { FINAL_GATE16_BUNDLE_PREFIX } from './local-final-gate16-contract.mjs';
3
+ import { fail16, sha16 } from './local-final-gate16-checks.mjs';
4
+
5
+ export const FINAL_GATE16_CHECKER_PINS = Object.freeze({
6
+ 'local-final-gate16-worker.mjs': '344d64b6c1c5552b96960e0d8514a876a3dbfa9762ab9eb749038e8351727121',
7
+ 'local-final-gate16-checks.mjs': '1dd30e9666d458d8b12ca6e4975f6f0ac8450c3ea9cbecc7401ae037f66b589a',
8
+ 'local-decimal.mjs': '93580b0cbdac2e881942658c1cd10394b432219f8657afaaaf4cb1c35c91d644',
9
+ 'local-formula-ast.mjs': 'e7475176409f146f46eb8565be8df4108c88f12b46c9b600915834f9843089d7',
10
+ 'local-formula-engine.mjs': '3b6ed599575b17d3da738e0c798804264540cbff87c58459e2ef3fa6072cd003',
11
+ 'local-formula-tests.mjs': 'd6d2809572c46955e6b556023ed6a12362c8acd0ad0d5be3b1b2dd64439215c0',
12
+ });
13
+ export async function finalGate16Bundle() {
14
+ const files = [];
15
+ for (const [name, sha256] of Object.entries(FINAL_GATE16_CHECKER_PINS)) {
16
+ const bytes = await readFile(new URL(name, import.meta.url));
17
+ if (sha16(bytes) !== sha256) fail16('FINAL_GATE16_CHECKER_PIN', 'The installed checker closure differs from its reviewed source');
18
+ files.push({ path: FINAL_GATE16_BUNDLE_PREFIX + name, sha256, bytes });
19
+ }
20
+ return files.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
21
+ }