@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.
Files changed (167) hide show
  1. package/AGREEMENT.md +6 -6
  2. package/CAPABILITY-MAP.md +6 -0
  3. package/CHANGELOG.md +10 -0
  4. package/SKILL.md +18 -4
  5. package/archguard-runtime-build.mjs +15 -0
  6. package/blueprint-criterion-runner.mjs +110 -0
  7. package/blueprint-criterion-wire.mjs +78 -0
  8. package/calctool-runtime-build.mjs +59 -0
  9. package/calctool-runtime-entry.mjs +92 -0
  10. package/cli.mjs +35 -83
  11. package/local-approved-plan.mjs +74 -15
  12. package/local-archguard-ast.mjs +371 -0
  13. package/local-archguard-checkpoint.mjs +123 -0
  14. package/local-archguard-components.mjs +177 -0
  15. package/local-archguard-contract.mjs +109 -0
  16. package/local-archguard-contracts.mjs +42 -0
  17. package/local-archguard-evaluate.mjs +120 -0
  18. package/local-archguard-language-ast.mjs +165 -0
  19. package/local-archguard-mutation-fence.mjs +28 -0
  20. package/local-archguard-operations.mjs +25 -0
  21. package/local-archguard-scope.mjs +64 -0
  22. package/local-archguard-signing.mjs +52 -0
  23. package/local-archguard-snapshot.mjs +126 -0
  24. package/local-archguard-state.mjs +200 -0
  25. package/local-archguard.mjs +74 -0
  26. package/local-budget-git-attributes.mjs +49 -0
  27. package/local-budget-git-loader.mjs +61 -0
  28. package/local-budget-git-native-attributes.mjs +81 -0
  29. package/local-budget-git-native-darwin.mjs +104 -0
  30. package/local-budget-git-native-linux.mjs +93 -0
  31. package/local-budget-git-native-reads.mjs +88 -0
  32. package/local-budget-git-native-request.mjs +53 -0
  33. package/local-budget-git-native-runtime.mjs +18 -0
  34. package/local-budget-git-native.c +313 -0
  35. package/local-budget-git-native.mjs +109 -0
  36. package/local-budget-git-outcome.mjs +33 -0
  37. package/local-budget-git-scratch-loader.mjs +158 -0
  38. package/local-budget-git-scratch.mjs +112 -0
  39. package/local-budget-git-system-attributes.mjs +44 -0
  40. package/local-budget-git.mjs +92 -0
  41. package/local-budget-grants.mjs +82 -0
  42. package/local-budget-handoff-completion.mjs +70 -0
  43. package/local-budget-handoff-contracts.mjs +53 -0
  44. package/local-budget-handoff.mjs +229 -0
  45. package/local-budget-lock.mjs +55 -0
  46. package/local-budget-reads.mjs +135 -0
  47. package/local-budget-renewal.mjs +86 -34
  48. package/local-budget-source-bindings.mjs +51 -0
  49. package/local-budget-store.mjs +131 -0
  50. package/local-budget-transition.mjs +46 -0
  51. package/local-budget.mjs +185 -0
  52. package/local-calctool-cli.mjs +24 -0
  53. package/local-calctool-contracts.mjs +42 -0
  54. package/local-calctool-runtime.mjs +10 -0
  55. package/local-calctool-storage.mjs +101 -0
  56. package/local-calctool.mjs +103 -0
  57. package/local-chain-bindings.mjs +93 -0
  58. package/local-chain-broker.mjs +61 -0
  59. package/local-chain-contracts.mjs +21 -0
  60. package/local-chain-controller.mjs +321 -0
  61. package/local-chain-human.mjs +48 -0
  62. package/local-chain-lock.mjs +54 -0
  63. package/local-chain-operations.mjs +7 -0
  64. package/local-chain-plan.mjs +232 -0
  65. package/local-chain-release-labels.mjs +12 -0
  66. package/local-chain-results.mjs +75 -0
  67. package/local-cli.mjs +65 -24
  68. package/local-contract-types.mjs +46 -0
  69. package/local-contracts.mjs +126 -0
  70. package/local-credential.mjs +101 -0
  71. package/local-decimal.mjs +85 -0
  72. package/local-error.mjs +23 -0
  73. package/local-evidence-verification.mjs +199 -0
  74. package/local-execution.mjs +115 -304
  75. package/local-final-gate16-bundle.mjs +21 -0
  76. package/local-final-gate16-checks.mjs +88 -0
  77. package/local-final-gate16-contract.mjs +15 -0
  78. package/local-final-gate16-plan.mjs +66 -0
  79. package/local-final-gate16-proof.mjs +75 -0
  80. package/local-final-gate16-release.mjs +17 -0
  81. package/local-final-gate16-state.mjs +70 -0
  82. package/local-final-gate16-worker.mjs +19 -0
  83. package/local-final-gate16.mjs +137 -0
  84. package/local-formula-ast.mjs +106 -0
  85. package/local-formula-engine.mjs +125 -0
  86. package/local-formula-tests.mjs +61 -0
  87. package/local-formula.mjs +25 -125
  88. package/local-integrity.mjs +22 -41
  89. package/local-invocations.mjs +142 -0
  90. package/local-ledger.mjs +15 -9
  91. package/local-merge-archguard-verification.d.mts +44 -0
  92. package/local-merge-archguard-verification.mjs +240 -0
  93. package/local-merge-archguard.mjs +138 -0
  94. package/local-merge-artifacts.mjs +86 -0
  95. package/local-merge-authority.mjs +89 -0
  96. package/local-merge-contracts.mjs +56 -0
  97. package/local-merge-controller.mjs +170 -0
  98. package/local-merge-head.mjs +56 -0
  99. package/local-merge-journal.mjs +77 -0
  100. package/local-merge-mutation.mjs +97 -0
  101. package/local-merge-rollback.mjs +126 -0
  102. package/local-merge-snapshot-rollback.mjs +26 -0
  103. package/local-merge-storage.mjs +103 -0
  104. package/local-merge-targets.mjs +47 -0
  105. package/local-merge-transaction.mjs +133 -0
  106. package/local-mutation-audit.mjs +34 -0
  107. package/local-mutation-authority.mjs +103 -0
  108. package/local-mutation-batch-receipt.mjs +84 -0
  109. package/local-mutation-batch.mjs +143 -0
  110. package/local-mutation-binding.mjs +89 -0
  111. package/local-mutation-files.mjs +270 -0
  112. package/local-mutation-pass-store.mjs +63 -0
  113. package/local-mutation-snapshot.mjs +200 -0
  114. package/local-mutation-wire.mjs +89 -0
  115. package/local-probe.mjs +133 -11
  116. package/local-scope-candidate.mjs +28 -0
  117. package/local-scope-context.mjs +28 -0
  118. package/local-scope-files.mjs +106 -0
  119. package/local-scope-git-attributes.mjs +200 -0
  120. package/local-scope-git-comparison.mjs +8 -0
  121. package/local-scope-git-history.mjs +72 -0
  122. package/local-scope-git-reads.mjs +66 -0
  123. package/local-scope-git.mjs +124 -0
  124. package/local-scope-repository.mjs +249 -0
  125. package/local-scope-signature.mjs +70 -0
  126. package/local-scope-snapshot.mjs +39 -0
  127. package/local-scope-state.mjs +204 -0
  128. package/local-scope-workspaces.mjs +83 -0
  129. package/local-swarm-authority.mjs +143 -0
  130. package/local-swarm-baseline.mjs +32 -0
  131. package/local-swarm-completion.mjs +102 -0
  132. package/local-swarm-contracts.mjs +47 -0
  133. package/local-swarm-mutation.mjs +142 -0
  134. package/local-swarm-priority.mjs +237 -0
  135. package/local-swarm-state.mjs +102 -0
  136. package/local-swarm-storage.mjs +135 -0
  137. package/local-swarm.mjs +179 -0
  138. package/local-task-handoff-completion-wire.mjs +71 -0
  139. package/local-task-handoff-contracts.mjs +66 -0
  140. package/local-task-handoff-fence.mjs +478 -0
  141. package/local-task-handoff-observation.mjs +41 -0
  142. package/local-task-handoff-overlay-wire.mjs +94 -0
  143. package/local-task-handoff-read-authority.mjs +236 -0
  144. package/local-task-handoff-storage.mjs +73 -0
  145. package/local-task-handoff-write-archive.mjs +80 -0
  146. package/local-task-handoff-write-constraints.mjs +66 -0
  147. package/local-task-handoff-write-receipts.mjs +99 -0
  148. package/local-task-handoff-write-wire.mjs +99 -0
  149. package/local-task-routing.mjs +202 -22
  150. package/manifest.json +1 -1
  151. package/package.json +164 -3
  152. package/release-state.json +164 -18
  153. package/release.mjs +242 -4
  154. package/releases.json +12 -0
  155. package/remote-client.mjs +127 -36
  156. package/remote-transport.mjs +80 -0
  157. package/review-client.mjs +38 -0
  158. package/runtime/archguard-runtime-v7.0.44.mjs +314 -0
  159. package/runtime/calctool-runtime-v7.0.43.mjs +1910 -0
  160. package/runtime/git-native-darwin-v1.mjs +5 -0
  161. package/runtime/git-native-linux-v1.mjs +5 -0
  162. package/task-message-cli.mjs +47 -0
  163. package/task-message-entry.mjs +159 -0
  164. package/workflow-local-client.mjs +35 -0
  165. package/workflow-local-contracts.mjs +58 -0
  166. package/workflow-local-request.mjs +53 -0
  167. package/workflow-local-runner.mjs +350 -0
@@ -0,0 +1,63 @@
1
+ import path from 'node:path';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { withStateLock } from './local-state-lock.mjs';
4
+ import { atomicMutationJson14, mutationFail14, mutationRoot14, mutationStateRoot14, mutationTarget14,
5
+ privateMutationDirectory14, readMutationJson14 } from './local-mutation-files.mjs';
6
+ import { mutationPass14 } from './local-mutation-wire.mjs';
7
+
8
+ const SCHEMA = 'entrorove.mutation-consumption/1.0';
9
+ export async function readStoredMutationPass14(input) {
10
+ const root = await mutationRoot14(input.repositoryRoot), stateRoot = await mutationStateRoot14(input);
11
+ if (typeof input.gatePassPath !== 'string' || !path.isAbsolute(input.gatePassPath)) {
12
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_REQUIRED', 'Guarded writing requires its stored original cloud pass');
13
+ }
14
+ const pass = mutationPass14(await readMutationJson14(input.gatePassPath));
15
+ const expected = path.join(stateRoot, 'gates', sha256(root).slice(0, 32), input.flowId, `${pass.passId}.json`);
16
+ if (input.gatePassPath !== expected) mutationFail14('ENTROROVE_MUTATION_PASS_LOCATION', 'Mutation pass is outside its original repository and flow storage');
17
+ return pass;
18
+ }
19
+ export async function mutationPassLocations14(input, binding, pass) {
20
+ const stateRoot = await mutationStateRoot14(input);
21
+ const gates = path.join(stateRoot, 'gates', pass.repositoryDigest, pass.flowId);
22
+ const anchor = process.env.ENTROROVE_TASK_HANDOFF_STATE_DIRECTORY;
23
+ const consumption = path.join(anchor, 'mutations', pass.repositoryDigest, pass.flowId);
24
+ await privateMutationDirectory14(gates, { privateFrom: stateRoot });
25
+ await privateMutationDirectory14(consumption, { privateFrom: anchor });
26
+ return { stateRoot, anchor, gates, gatePassPath: path.join(gates, `${pass.passId}.json`),
27
+ consumptionPath: path.join(consumption, `${pass.passId}.json`), executionHome: binding.home };
28
+ }
29
+ function initial(pass, locations) {
30
+ return { schemaVersion: SCHEMA, passId: pass.passId, passDigest: sha256(canonical(pass)), flowId: pass.flowId,
31
+ stepId: pass.stepId, executionAttemptNonce: pass.executionAttemptNonce, executionHome: locations.executionHome,
32
+ paths: [...pass.paths], writes: {} };
33
+ }
34
+ export async function readMutationConsumption14(pass, locations) {
35
+ const ledger = await readMutationJson14(locations.consumptionPath), expected = initial(pass, locations);
36
+ if (Object.keys(ledger).sort().join('\0') !== Object.keys(expected).sort().join('\0')
37
+ || Object.keys(expected).filter(field => field !== 'writes').some(field => canonical(ledger[field]) !== canonical(expected[field]))
38
+ || !ledger.writes || typeof ledger.writes !== 'object' || Array.isArray(ledger.writes)
39
+ || Object.entries(ledger.writes).some(([target, value]) => !pass.paths.includes(target)
40
+ || !value || !['pending', 'written', 'failed'].includes(value.status) || !/^[0-9a-f]{64}$/.test(value.contentDigest)
41
+ || !Number.isFinite(Date.parse(value.at)))) mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_INVALID', 'Mutation consumption history lost its original pass binding');
42
+ return ledger;
43
+ }
44
+ export async function installMutationPass14(input, binding, pass) {
45
+ const locations = await mutationPassLocations14(input, binding, pass);
46
+ return withStateLock(locations.consumptionPath, async () => {
47
+ const previous = await mutationTarget14(locations.gates, path.basename(locations.gatePassPath), true);
48
+ if (previous.exists && canonical(await readMutationJson14(previous.target)) !== canonical(pass)) {
49
+ mutationFail14('ENTROROVE_MUTATION_PASS_COLLISION', 'Stored mutation pass differs from its original cloud authority');
50
+ }
51
+ try { await readMutationConsumption14(pass, locations); }
52
+ catch (error) {
53
+ if (error.code !== 'ENOENT') throw error;
54
+ if (previous.exists) mutationFail14('ENTROROVE_MUTATION_CONSUMPTION_MISSING', 'Existing pass lost its consumption history; cloud reconciliation is required');
55
+ await atomicMutationJson14(locations.consumptionPath, initial(pass, locations), { privateFrom: locations.anchor });
56
+ }
57
+ if (!previous.exists) await atomicMutationJson14(locations.gatePassPath, pass, { privateFrom: locations.stateRoot });
58
+ return locations;
59
+ });
60
+ }
61
+ export async function saveMutationConsumption14(locations, ledger) {
62
+ return atomicMutationJson14(locations.consumptionPath, ledger, { privateFrom: locations.anchor, replace: true });
63
+ }
@@ -0,0 +1,200 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { canonical, sha256, snapshotHandoffBinding, verifySnapshotManifest } from './local-integrity.mjs';
5
+ import { budgetBindingForRead } from './local-budget-reads.mjs';
6
+ import { atomicMutationJson14, cleanupMutationDirectory14, mutationFail14, mutationId14, mutationPaths14,
7
+ mutationPath14, mutationRead14, mutationRoot14, mutationStateRoot14, mutationTarget14, privateMutationDirectory14,
8
+ readMutationJson14, writeMutationFile14 } from './local-mutation-files.mjs';
9
+
10
+ const LOCAL_SCHEMA = 'entrorove.local-execution/1.0';
11
+ const SNAPSHOT_SCHEMA = 'entrorove.snapshot/1.0';
12
+ const RECEIPT_SCHEMA = 'entrorove.snapshot-receipt/1.0';
13
+ const COPY_LAYOUT = 'files/1.0';
14
+ const SHA256 = /^[0-9a-f]{64}$/;
15
+ const MISSING_SHA256 = sha256('entrorove.missing-file/1.0');
16
+ const repoKey = root => sha256(root).slice(0, 32);
17
+ function layoutBinding(value) {
18
+ if (!Object.hasOwn(value, 'copyLayout')) return {};
19
+ if (value.copyLayout !== COPY_LAYOUT) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'snapshot copy layout is invalid');
20
+ return { copyLayout: value.copyLayout };
21
+ }
22
+ function handoffBinding(value) {
23
+ try { return snapshotHandoffBinding(value); }
24
+ catch { mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'snapshot handoff epoch and baseline receipt binding are invalid'); }
25
+ }
26
+ function receiptCore(receipt) {
27
+ return { schemaVersion: RECEIPT_SCHEMA, snapshotId: receipt.snapshotId, flowId: receipt.flowId, stepId: receipt.stepId,
28
+ repositoryDigest: receipt.repositoryDigest, snapshotDigest: receipt.snapshotDigest, contractDigest: receipt.contractDigest ?? null,
29
+ paths: receipt.paths, fileManifestDigest: receipt.fileManifestDigest, ...handoffBinding(receipt), ...layoutBinding(receipt) };
30
+ }
31
+ export function loadSnapshotReceipt14(receipt) {
32
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) || receipt.schemaVersion !== RECEIPT_SCHEMA
33
+ || !SHA256.test(String(receipt.snapshotDigest)) || !SHA256.test(String(receipt.fileManifestDigest))
34
+ || !SHA256.test(String(receipt.receiptDigest)) || !/^[0-9a-f]{32}$/.test(String(receipt.repositoryDigest))
35
+ || receipt.contractDigest !== null && receipt.contractDigest !== undefined && !SHA256.test(String(receipt.contractDigest))) {
36
+ mutationFail14('ENTROROVE_RECEIPT_INVALID', 'snapshot receipt fields are invalid');
37
+ }
38
+ for (const field of ['snapshotId', 'flowId', 'stepId']) mutationId14(receipt[field], field);
39
+ const paths = mutationPaths14(receipt.paths);
40
+ if (canonical(paths) !== canonical(receipt.paths)) mutationFail14('ENTROROVE_RECEIPT_INVALID', 'snapshot receipt paths must be sorted');
41
+ const core = receiptCore(receipt);
42
+ if (receipt.receiptDigest !== sha256(canonical(core))) mutationFail14('ENTROROVE_RECEIPT_INVALID', 'snapshot receipt digest is invalid');
43
+ return { ...core, paths: [...paths], receiptDigest: receipt.receiptDigest };
44
+ }
45
+ async function location(input, root, binding = input) {
46
+ for (const field of ['flowId', 'stepId', 'snapshotId']) mutationId14(binding[field], field);
47
+ const stateRoot = await mutationStateRoot14(input);
48
+ const snapshotRoot = path.join(stateRoot, 'snapshots', repoKey(root), binding.flowId, binding.snapshotId);
49
+ return { stateRoot, snapshotRoot, manifestPath: path.join(snapshotRoot, 'manifest.json') };
50
+ }
51
+ function checkManifest(manifest, root, binding) {
52
+ const integrity = verifySnapshotManifest(manifest, { snapshotId: binding.snapshotId, flowId: binding.flowId,
53
+ stepId: binding.stepId, repositoryDigest: repoKey(root) });
54
+ if (!integrity.verified) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', integrity.reason ?? 'snapshot manifest is invalid');
55
+ if (canonical(mutationPaths14(manifest.paths)) !== canonical(manifest.paths)) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'snapshot paths are invalid');
56
+ layoutBinding(manifest);
57
+ for (const file of manifest.files) {
58
+ if (file.exists ? !Number.isInteger(file.mode) || file.mode < 0 || file.mode > 0o777
59
+ : file.mode !== null || file.sha256 !== MISSING_SHA256) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'snapshot file existence or mode binding is invalid');
60
+ }
61
+ return manifest;
62
+ }
63
+ function copyRoot(snapshotRoot, manifest) {
64
+ return manifest.copyLayout === COPY_LAYOUT ? path.join(snapshotRoot, 'files') : snapshotRoot;
65
+ }
66
+ export function snapshotCopyPath14(manifest, snapshotRoot, relative) {
67
+ layoutBinding(manifest); mutationPath14(relative);
68
+ if (!Array.isArray(manifest.paths) || !manifest.paths.includes(relative)) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'copy path is outside its snapshot path set');
69
+ return path.join(copyRoot(snapshotRoot, manifest), relative);
70
+ }
71
+ async function readCopy(input, snapshotRoot, root, relative) {
72
+ if (root === snapshotRoot || !await budgetBindingForRead(input)) return mutationRead14(input, root, relative);
73
+ // The unchanged budget reader recognizes its original snapshot path shape. A
74
+ // private, transient hardlink preserves that shape and charges the exact copy
75
+ // bytes under the original source path, including manifest.json user files.
76
+ const before = await mutationTarget14(root, relative);
77
+ const stateRoot = await mutationStateRoot14(input);
78
+ const repository = await mutationRoot14(input.repositoryRoot);
79
+ mutationId14(input.flowId, 'flowId');
80
+ const proxyRoot = path.join(stateRoot, 'snapshots', repoKey(repository), input.flowId, `read-${randomUUID()}`);
81
+ await privateMutationDirectory14(path.dirname(proxyRoot), { privateFrom: stateRoot });
82
+ const owned = await privateMutationDirectory14(proxyRoot, { exclusive: true, privateFrom: stateRoot });
83
+ let read;
84
+ try {
85
+ const proxy = path.join(proxyRoot, relative);
86
+ await privateMutationDirectory14(path.dirname(proxy), { privateFrom: stateRoot });
87
+ await fs.link(before.target, proxy);
88
+ read = await mutationRead14(input, proxyRoot, relative);
89
+ } finally { await cleanupMutationDirectory14(owned); }
90
+ const after = await mutationTarget14(root, relative);
91
+ if (before.stat.dev !== after.stat.dev || before.stat.ino !== after.stat.ino || read.stat.dev !== after.stat.dev
92
+ || read.stat.ino !== after.stat.ino || before.stat.size !== after.stat.size || before.stat.mtimeMs !== after.stat.mtimeMs) {
93
+ mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'snapshot copy changed during its budgeted read');
94
+ }
95
+ return { ...read, target: after.target, stat: after.stat, mode: after.stat.mode & 0o777 };
96
+ }
97
+ export async function readSnapshotCopy14(input, snapshotRoot, manifest, relative) {
98
+ const expected = manifest.files?.find(file => file.path === relative);
99
+ const target = snapshotCopyPath14(manifest, snapshotRoot, relative);
100
+ if (!expected || typeof expected.exists !== 'boolean') mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'snapshot copy file entry is invalid');
101
+ const root = copyRoot(snapshotRoot, manifest); await mutationRoot14(root);
102
+ const copy = await mutationTarget14(root, relative, true);
103
+ if (copy.exists !== expected.exists) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', `${relative} snapshot copy existence is invalid`);
104
+ if (!expected.exists) {
105
+ if (expected.sha256 !== MISSING_SHA256 || expected.mode !== null) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'missing snapshot copy binding is invalid');
106
+ return { target, path: relative, exists: false, content: null, sha256: MISSING_SHA256, mode: null, stat: null };
107
+ }
108
+ if (!Number.isInteger(expected.mode) || expected.mode < 0 || expected.mode > 0o777) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'snapshot source mode is invalid');
109
+ const read = await readCopy(input, snapshotRoot, root, relative);
110
+ if (read.sha256 !== expected.sha256 || read.stat.nlink !== 1) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', `${relative} snapshot copy is invalid`);
111
+ if (manifest.copyLayout && process.platform !== 'win32' && read.mode !== 0o600) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'snapshot copies must have private file permissions');
112
+ return { ...read, exists: true, sourceMode: expected.mode };
113
+ }
114
+ export async function preserveSnapshotCopies14(input, snapshotRoot, manifest, destination) {
115
+ const stateRoot = await mutationStateRoot14(input);
116
+ await privateMutationDirectory14(copyRoot(destination, manifest), { privateFrom: stateRoot });
117
+ for (const expected of manifest.files) {
118
+ const observed = await readSnapshotCopy14(input, snapshotRoot, manifest, expected.path);
119
+ if (expected.exists) await writeMutationFile14(snapshotCopyPath14(manifest, destination, expected.path),
120
+ observed.content, { privateFrom: stateRoot });
121
+ }
122
+ }
123
+ async function copyFacts(input, snapshotRoot, manifest) {
124
+ const files = [];
125
+ for (const expected of manifest.files) {
126
+ await readSnapshotCopy14(input, snapshotRoot, manifest, expected.path);
127
+ files.push({ path: expected.path, sourceHash: expected.sha256, snapshotHash: expected.sha256 });
128
+ }
129
+ return files;
130
+ }
131
+ export async function verifySnapshotSources14(input, root, manifest, { paths = manifest.paths } = {}) {
132
+ const selected = mutationPaths14(paths);
133
+ if (selected.some(relative => !manifest.paths.includes(relative))) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'source verification is outside the snapshot path set');
134
+ for (const expected of manifest.files.filter(file => selected.includes(file.path))) {
135
+ const source = await mutationTarget14(root, expected.path, true);
136
+ if (source.exists !== expected.exists) mutationFail14('ENTROROVE_SNAPSHOT_CHANGED', `${expected.path} changed after snapshot`);
137
+ if (expected.exists) {
138
+ const read = await mutationRead14(input, root, expected.path);
139
+ if (read.sha256 !== expected.sha256 || read.mode !== expected.mode) mutationFail14('ENTROROVE_SNAPSHOT_CHANGED', `${expected.path} changed after snapshot`);
140
+ } else if (expected.sha256 !== MISSING_SHA256) mutationFail14('ENTROROVE_SNAPSHOT_CORRUPT', 'missing source hash is invalid');
141
+ }
142
+ }
143
+ async function copySource(input, root, snapshotRoot, relative, stateRoot) {
144
+ const source = await mutationTarget14(root, relative, true);
145
+ if (!source.exists) return { path: relative, exists: false, sha256: MISSING_SHA256, mode: null };
146
+ const read = await mutationRead14(input, root, relative);
147
+ await writeMutationFile14(path.join(snapshotRoot, 'files', relative), read.content, { privateFrom: stateRoot });
148
+ return { path: relative, exists: true, sha256: read.sha256, mode: read.mode };
149
+ }
150
+ export async function snapshotPlan14(input, { handoffBinding: binding = {} } = {}) {
151
+ const root = await mutationRoot14(input.repositoryRoot);
152
+ const flowId = mutationId14(input.flowId, 'flowId'); const stepId = mutationId14(input.stepId, 'stepId');
153
+ const paths = mutationPaths14(input.paths); const handoff = handoffBinding(binding);
154
+ const snapshotId = mutationId14(input.snapshotId ?? `snap-${randomUUID()}`, 'snapshotId');
155
+ const contractDigest = input.contractDigest ?? null;
156
+ if (contractDigest !== null && !SHA256.test(contractDigest)) mutationFail14('ENTROROVE_CONTRACT_DIGEST', 'contractDigest must be a lowercase SHA-256 digest');
157
+ const loc = await location(input, root, { flowId, stepId, snapshotId });
158
+ await privateMutationDirectory14(path.dirname(loc.snapshotRoot), { privateFrom: loc.stateRoot });
159
+ const owned = await privateMutationDirectory14(loc.snapshotRoot, { exclusive: true, privateFrom: loc.stateRoot });
160
+ try {
161
+ await privateMutationDirectory14(path.join(loc.snapshotRoot, 'files'), { privateFrom: loc.stateRoot });
162
+ const files = [];
163
+ for (const relative of paths) files.push(await copySource(input, root, loc.snapshotRoot, relative, loc.stateRoot));
164
+ const core = { snapshotId, flowId, stepId, paths, files, contractDigest, ...handoff };
165
+ const manifest = { schemaVersion: SNAPSHOT_SCHEMA, ...core, repositoryDigest: repoKey(root), copyLayout: COPY_LAYOUT,
166
+ createdAt: new Date().toISOString(), snapshotDigest: sha256(canonical(core)) };
167
+ await atomicMutationJson14(loc.manifestPath, manifest, { privateFrom: loc.stateRoot });
168
+ await copyFacts(input, loc.snapshotRoot, manifest);
169
+ await verifySnapshotSources14(input, root, manifest);
170
+ return { schemaVersion: LOCAL_SCHEMA, snapshotId, snapshotRoot: loc.snapshotRoot, manifestPath: loc.manifestPath,
171
+ paths, contractDigest, snapshotDigest: manifest.snapshotDigest, copyLayout: COPY_LAYOUT, forbidGitBranch: true, forbidWorktree: true };
172
+ } catch (error) {
173
+ try { await cleanupMutationDirectory14(owned); } catch { /* Preserve the primary error and refuse unsafe cleanup. */ }
174
+ throw error;
175
+ }
176
+ }
177
+ export async function snapshotVerify14(input) {
178
+ const root = await mutationRoot14(input.repositoryRoot); const loc = await location(input, root);
179
+ const manifest = checkManifest(await readMutationJson14(loc.manifestPath), root, input);
180
+ const files = await copyFacts(input, loc.snapshotRoot, manifest);
181
+ await verifySnapshotSources14(input, root, manifest);
182
+ const core = receiptCore({ ...manifest, fileManifestDigest: sha256(canonical(files)) });
183
+ const receipt = { ...core, receiptDigest: sha256(canonical(core)) };
184
+ return { schemaVersion: LOCAL_SCHEMA, verified: true, receipt };
185
+ }
186
+ export async function snapshotAuthority14(input, root, value) {
187
+ const actualRoot = await mutationRoot14(input.repositoryRoot);
188
+ if (actualRoot !== root) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'snapshot authority root differs from its repository binding');
189
+ const receipt = loadSnapshotReceipt14(value);
190
+ if (receipt.flowId !== input.flowId || receipt.stepId !== input.stepId || receipt.repositoryDigest !== repoKey(root)
191
+ || input.snapshotId !== undefined && input.snapshotId !== receipt.snapshotId) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'snapshot receipt scope is invalid');
192
+ const loc = await location(input, root, receipt);
193
+ const manifest = checkManifest(await readMutationJson14(loc.manifestPath), root, receipt);
194
+ if (manifest.snapshotDigest !== receipt.snapshotDigest || canonical(manifest.paths) !== canonical(receipt.paths)
195
+ || manifest.contractDigest !== receipt.contractDigest || canonical(handoffBinding(manifest)) !== canonical(handoffBinding(receipt))
196
+ || canonical(layoutBinding(manifest)) !== canonical(layoutBinding(receipt))) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'snapshot receipt does not match its manifest or layout');
197
+ const files = await copyFacts(input, loc.snapshotRoot, manifest);
198
+ if (receipt.fileManifestDigest !== sha256(canonical(files))) mutationFail14('ENTROROVE_SNAPSHOT_BINDING', 'snapshot receipt file manifest digest is invalid');
199
+ return { manifest, manifestPath: loc.manifestPath, snapshotRoot: loc.snapshotRoot, copyRoot: copyRoot(loc.snapshotRoot, manifest) };
200
+ }
@@ -0,0 +1,89 @@
1
+ import { createPublicKey, verify } from 'node:crypto';
2
+ import { canonical, sha256 } from './local-integrity.mjs';
3
+ import { canonicalHandoffSignature } from './local-task-handoff-overlay-wire.mjs';
4
+ import { mutationFail14, mutationPaths14 } from './local-mutation-files.mjs';
5
+
6
+ export const MUTATION_IDENTITY_FIELDS14 = ['flowId', 'taskId', 'ownerId', 'taskDigest', 'chainDigest', 'chainRevision',
7
+ 'acceptanceReceiptId', 'acceptanceDigest', 'routeDigest', 'receiver', 'receiverDigest', 'stepId', 'leaseId', 'leaseDigest',
8
+ 'generation', 'planId', 'planDigest', 'executionPlanSha256', 'executionAuthorityId', 'executionAuthorityDigest',
9
+ 'executionAttemptNonce', 'epoch', 'runnerId', 'runnerKeyId', 'repositoryId', 'repositoryDigest', 'scopeBindingDigest',
10
+ 'scopeArtifactDigest', 'scopeContractDigest', 'paths', 'newFilePaths', 'issuedAt', 'expiresAt'];
11
+ const CHALLENGE_FIELDS = ['schemaVersion', 'challengeId', 'snapshotId', 'nonce', ...MUTATION_IDENTITY_FIELDS14];
12
+ const PASS_FIELDS = ['schemaVersion', 'passId', 'challengeId', 'challengeDigest', 'challengeNonce', 'snapshotId', 'nonce',
13
+ ...MUTATION_IDENTITY_FIELDS14, 'handoffEpoch', 'snapshotDigest', 'snapshotReceiptDigest', 'snapshotReceipt',
14
+ 'snapshotManifestDigest', 'attestationDigest', 'coordinationRequired'];
15
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
16
+ const SHA = /^[0-9a-f]{64}$/;
17
+ function exact(value, fields) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value)
19
+ || Object.keys(value).sort().join('\0') !== [...fields].sort().join('\0')) {
20
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority has invalid fields');
21
+ }
22
+ }
23
+ export function mutationLifetime14(value, { live = true } = {}) {
24
+ const start = Date.parse(value.issuedAt), end = Date.parse(value.expiresAt);
25
+ if (!Number.isFinite(start) || !Number.isFinite(end) || new Date(start).toISOString() !== value.issuedAt
26
+ || new Date(end).toISOString() !== value.expiresAt || start > Date.now() || end <= start || end - start > 90_000
27
+ || live && end <= Date.now()) mutationFail14('ENTROROVE_MUTATION_AUTHORITY_EXPIRED', 'Mutation authority is expired or has invalid issuance');
28
+ }
29
+ function pinnedAuthority() {
30
+ try {
31
+ const encoded = process.env.ENTROROVE_WORKFLOW_PLAN_PUBLIC_KEY;
32
+ if (!encoded) throw new Error();
33
+ const der = Buffer.from(encoded, 'base64'), key = createPublicKey({ key: der, type: 'spki', format: 'der' });
34
+ if (key.asymmetricKeyType !== 'ed25519' || key.export({ type: 'spki', format: 'der' }).toString('base64') !== encoded) throw new Error();
35
+ return { key, keyId: sha256(der) };
36
+ } catch { mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Pinned cloud mutation authority is unavailable or invalid'); }
37
+ }
38
+ function identity(payload) {
39
+ const ids = ['flowId', 'taskId', 'acceptanceReceiptId', 'stepId', 'leaseId', 'planId', 'executionAuthorityId',
40
+ 'executionAttemptNonce', 'runnerId', 'repositoryId', 'snapshotId', 'nonce', 'challengeId'];
41
+ const digests = ['taskDigest', 'chainDigest', 'acceptanceDigest', 'routeDigest', 'receiverDigest', 'leaseDigest',
42
+ 'planDigest', 'executionPlanSha256', 'executionAuthorityDigest', 'runnerKeyId', 'scopeBindingDigest',
43
+ 'scopeArtifactDigest', 'scopeContractDigest'];
44
+ if (ids.some(field => typeof payload[field] !== 'string' || !ID.test(payload[field]))
45
+ || digests.some(field => typeof payload[field] !== 'string' || !SHA.test(payload[field]))
46
+ || !/^[0-9a-f]{32}$/.test(payload.repositoryDigest) || !/^[0-9a-f-]{36}$/.test(payload.ownerId)
47
+ || !Number.isSafeInteger(payload.epoch) || payload.epoch < 0
48
+ || ['generation', 'chainRevision'].some(field => !Number.isSafeInteger(payload[field]) || payload[field] < 1)) {
49
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority identity is invalid');
50
+ }
51
+ const paths = mutationPaths14(payload.paths);
52
+ if (canonical(paths) !== canonical(payload.paths) || !Array.isArray(payload.newFilePaths)
53
+ || payload.newFilePaths.some(value => !paths.includes(value)) || new Set(payload.newFilePaths).size !== payload.newFilePaths.length
54
+ || canonical([...payload.newFilePaths].sort()) !== canonical(payload.newFilePaths)) {
55
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority path set is invalid');
56
+ }
57
+ exact(payload.receiver, ['taskId', 'ownerId', 'projectId', 'hostId', 'threadId', 'descriptionDigest', 'taskDigest']);
58
+ if (payload.receiver.taskId !== payload.taskId || payload.receiver.ownerId !== payload.ownerId
59
+ || payload.receiver.taskDigest !== payload.taskDigest || !SHA.test(payload.receiver.descriptionDigest)
60
+ || sha256(canonical(payload.receiver)) !== payload.receiverDigest
61
+ || ['projectId', 'hostId', 'threadId'].some(field => !ID.test(String(payload.receiver[field])))) {
62
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority lost its original cloud receiver');
63
+ }
64
+ }
65
+ function authority(value, fields, schema, options) {
66
+ exact(value, [...fields, 'authorityReceipt']);
67
+ const { authorityReceipt: receipt, ...payload } = value;
68
+ exact(receipt, ['schemaVersion', 'keyId', 'payloadDigest', 'signature']);
69
+ const bytes = Buffer.from(canonical(payload)), key = pinnedAuthority();
70
+ let signature; try { signature = canonicalHandoffSignature(receipt.signature); }
71
+ catch { mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Cloud mutation signature encoding is invalid'); }
72
+ if (payload.schemaVersion !== schema || bytes.length > 1024 * 1024
73
+ || receipt.schemaVersion !== 'entrorove.workflow-authority-receipt/1.0' || receipt.keyId !== key.keyId
74
+ || receipt.payloadDigest !== sha256(bytes) || !verify(null, bytes, key.key, signature)) {
75
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation authority requires a valid pinned cloud signature');
76
+ }
77
+ identity(payload); mutationLifetime14(payload, options); return value;
78
+ }
79
+ export function mutationChallenge14(value, options) {
80
+ return authority(value, CHALLENGE_FIELDS, 'entrorove.mutation-snapshot-challenge/1.0', options);
81
+ }
82
+ export function mutationPass14(value, options) {
83
+ const pass = authority(value, PASS_FIELDS, 'entrorove.mutation-pass/1.0', options);
84
+ if (!ID.test(pass.passId) || !ID.test(pass.challengeNonce) || pass.handoffEpoch !== pass.epoch || pass.coordinationRequired !== false
85
+ || ['challengeDigest', 'snapshotDigest', 'snapshotReceiptDigest', 'snapshotManifestDigest', 'attestationDigest'].some(field => !SHA.test(pass[field]))) {
86
+ mutationFail14('ENTROROVE_MUTATION_AUTHORITY_INVALID', 'Mutation pass has invalid snapshot or original epoch bindings');
87
+ }
88
+ return pass;
89
+ }
package/local-probe.mjs CHANGED
@@ -1,13 +1,135 @@
1
- import { promises as fs } from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
1
+ import { operationSchemasFor } from './local-contracts.mjs';
2
+ import { observedGoalRisk, observedScopeRisk, scopeFail, scopeRoot, scopeDigest } from './local-scope-files.mjs';
3
+ import { collectScopeFacts } from './local-scope-repository.mjs';
4
+ import { initializeScope, retainedScope, scopeBinding, upgradeScope } from './local-scope-state.mjs';
5
+ import { sha256 } from './local-integrity.mjs';
6
+ import { attestScopeFacts } from './local-scope-signature.mjs';
7
+ import { resolveScopePaths } from './local-scope-context.mjs';
8
+ import { readScopeSnapshot, verifyFreshScopeSnapshot } from './local-scope-snapshot.mjs';
9
+ import { rejectScopePrivileges, verifiedScopeCandidate } from './local-scope-candidate.mjs';
10
+ import { budgetBindingForRead } from './local-budget-reads.mjs';
11
+ import { budgetLocation, loadBudget } from './local-budget-store.mjs';
4
12
 
5
- const ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6
- function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
7
- function safe(value) { if (typeof value !== 'string' || !value || value !== value.normalize('NFC') || path.isAbsolute(value) || value.includes('\\') || /[\u0000-\u001f\u007f]/.test(value) || value.split('/').some(part => !part || part === '.' || part === '..')) fail('ENTROROVE_PATH_UNSAFE', 'probe path is unsafe'); return value; }
8
- async function root(value) { if (typeof value !== 'string' || !path.isAbsolute(value)) fail('ENTROROVE_ROOT_INVALID', 'repositoryRoot must be absolute'); const stat = await fs.lstat(value); if (!stat.isDirectory() || stat.isSymbolicLink()) fail('ENTROROVE_ROOT_INVALID', 'repositoryRoot must be a real directory'); return fs.realpath(value); }
9
- async function filesUnder(rootPath, relative, result = []) { const target = path.resolve(rootPath, ...safe(relative).split('/')); const stat = await fs.lstat(target); if (stat.isSymbolicLink()) fail('ENTROROVE_FILE_INVALID', 'probe refuses symlinks'); if (stat.isFile()) { result.push({ path: relative, bytes: stat.size }); return result; } if (!stat.isDirectory()) fail('ENTROROVE_FILE_INVALID', 'probe path is not a file or directory'); for (const name of await fs.readdir(target)) if (!['.git', 'node_modules', '.entrorove', '.aimlock', '.archguard'].includes(name)) await filesUnder(rootPath, `${relative}/${name}`, result); return result; }
10
- export async function probe(input) { const rootPath = await root(input.repositoryRoot); const requested = input.paths ?? input.targetPaths ?? ['.']; const paths = requested.length === 1 && requested[0] === '.' ? (await fs.readdir(rootPath)).filter(name => !['.git', 'node_modules', '.entrorove'].includes(name)).map(name => name) : requested.map(safe); const files = []; for (const relative of paths) await filesUnder(rootPath, relative, files); const fileCount = files.length; const bytes = files.reduce((total, file) => total + file.bytes, 0); const lines = Math.ceil(bytes / 4); const mode = fileCount <= 1 && lines <= 500 ? 'lock' : fileCount <= 3 && lines <= 500 ? 'probe' : 'swarm'; return { schemaVersion: 'entrorove.local-execution/1.0', status: 'verified', repositoryRoot: rootPath, facts: { fileCount, bytes, tokenEstimate: lines, files: files.sort((left, right) => left.path.localeCompare(right.path)) }, classification: { mode, reason: mode === 'swarm' ? 'more than three files or over the 500-line budget' : mode === 'probe' ? 'bounded multi-file read' : 'single-file read' }, forbidGitBranch: true, forbidWorktree: true }; }
11
- export async function reassess(input) { const result = await probe(input); const order = ['lock', 'probe', 'swarm']; const requested = input.currentMode ?? result.classification.mode; if (!ID.test(requested) || !order.includes(requested)) fail('ENTROROVE_MODE_INVALID', 'currentMode must be lock, probe, or swarm'); const currentIndex = order.indexOf(requested); const actualIndex = order.indexOf(result.classification.mode); const nextMode = actualIndex > currentIndex ? order[Math.min(currentIndex + 1, order.length - 1)] : requested; return { ...result, operation: 'reassess', previousMode: requested, mode: nextMode, upgraded: nextMode !== requested, preservedSnapshotId: input.snapshotId ?? null }; }
13
+ const types = new Set(['implementation', 'documentation', 'calculation', 'merge', 'review', 'decision', 'diagnosis']);
14
+ const canonicalPaths = files => files.map(file => file.path).join('\0');
15
+ const scopeModes = ['lock', 'probe', 'swarm'];
16
+ export function classifyScope(facts, task) {
17
+ if (task !== undefined && (!task || typeof task !== 'object' || Array.isArray(task)
18
+ || !types.has(task.workType) || !['low', 'medium', 'high'].includes(task.difficulty)
19
+ || !['low', 'medium', 'high'].includes(task.risk) || typeof task.goal !== 'string' || !task.goal.trim())) scopeFail('ENTROROVE_TASK_INVALID', 'scope task requires goal, workType, difficulty, and risk');
20
+ const crossModule = facts.crossModule || task?.crossModule === true; const parallel = task?.parallel === true || task?.needParallel === true;
21
+ const overBudget = facts.estimatedChangedLines > 500 || facts.git.binary;
22
+ const risks = ['low', 'medium', 'high'];
23
+ const risk = risks[Math.max(risks.indexOf(task?.risk ?? 'low'), risks.indexOf(facts.risk ?? 'low'),
24
+ risks.indexOf(observedScopeRisk(facts.files)), risks.indexOf(observedGoalRisk(task?.goal ?? '')))];
25
+ const diagnosis = task?.workType === 'diagnosis';
26
+ const explicit = task?.explicitAimlockRequested === true || task?.explicitEntroroveRequested === true
27
+ || (Array.isArray(task?.userSpecifiedSkills) && task.userSpecifiedSkills.some(skill => typeof skill === 'string' && ['aimlock', 'entrorove'].includes(skill.toLowerCase())));
28
+ const mode = facts.fileCount > 3 || overBudget || crossModule || parallel ? 'swarm' : facts.fileCount > 1 || diagnosis ? 'probe' : 'lock';
29
+ const bypass = task !== undefined && !diagnosis && task.difficulty === 'low' && !overBudget && !crossModule && risk !== 'high'
30
+ && !parallel && !explicit && task.unclear !== true && task.architectureContract !== true && facts.importGraph.complete;
31
+ return { decision: bypass ? 'bypass' : 'active', mode, risk, crossModule, estimatedChangedLines: facts.estimatedChangedLines,
32
+ reason: bypass ? 'observed bounded low-risk task' : mode === 'swarm' ? 'observed scope, changed lines, package dependencies, or requested parallel work require Swarm'
33
+ : mode === 'probe' ? 'observed scope contains at most three files through 500 lines' : 'observed scope contains one file through 500 lines',
34
+ readOnly: ['diagnosis', 'review', 'calculation', 'decision'].includes(task?.workType), writeAllowed: false, workerCount: 0 };
35
+ }
36
+ async function observe(input, retained, fresh = false) {
37
+ const repositoryRoot = await scopeRoot(input.repositoryRoot);
38
+ const requested = await resolveScopePaths(input, repositoryRoot);
39
+ if (requested === undefined) scopeFail('ENTROROVE_SCOPE_EMPTY', 'explicit target paths are required');
40
+ const original = (!retained || fresh) && input.snapshotId != null ? await readScopeSnapshot(input, repositoryRoot) : null;
41
+ const facts = await collectScopeFacts(repositoryRoot, requested, input.baselineRef, fresh ? original.known : retained?.history.at(-1).facts.files ?? original?.known, input);
42
+ const classification = classifyScope(facts, input.task ?? retained?.originalTask ?? undefined);
43
+ return { schemaVersion: 'entrorove.local-execution/1.0', status: 'reported', repositoryRoot,
44
+ repositoryDigest: sha256(repositoryRoot).slice(0, 32), facts, factsDigest: scopeDigest(facts), classification,
45
+ forbidGitBranch: true, forbidWorktree: true, trusted: false };
46
+ }
47
+ async function handoffReadBudget(input) {
48
+ const binding = await budgetBindingForRead(input);
49
+ if (!binding) return false;
50
+ const loaded = await loadBudget(await budgetLocation(binding));
51
+ return loaded.state.enforced && loaded.state.grant?.purpose === 'handoff-read';
52
+ }
53
+ async function probeHandoffCandidate(input, root, candidate) {
54
+ scopeBinding(input);
55
+ const native = await import('./local-task-handoff-read-authority.mjs');
56
+ const readHome = await native.assertTaskHandoffReadHome({ ...input, taskId: candidate.receiverTaskId, ownerId: candidate.ownerId });
57
+ if (readHome.epoch !== candidate.receiverEpoch - 1 || readHome.mutationAllowed !== false) {
58
+ scopeFail('ENTROROVE_SCOPE_CANDIDATE_BINDING', 'candidate observation requires its actual current read-only receiver home');
59
+ }
60
+ const result = await observe(input, null, true);
61
+ result.classification.decision = 'active';
62
+ result.facts.snapshotEvidence = await verifyFreshScopeSnapshot(input, root, result.facts);
63
+ result.factsDigest = scopeDigest(result.facts);
64
+ await verifiedScopeCandidate(input, root, result.facts.snapshotEvidence.manifest);
65
+ return attestScopeFacts(input, { ...result, scopePurpose: 'handoff-candidate',
66
+ requiredMode: result.classification.mode, snapshotDigest: result.facts.snapshotEvidence.snapshotDigest,
67
+ preservedSnapshotDigest: null, preservedSnapshotId: null, currentSnapshotFresh: true,
68
+ receiverOrigin: { flowId: candidate.flowId, taskId: candidate.receiverTaskId,
69
+ scopeDigest: candidate.receiverOriginalScopeDigest, checkpointDigest: candidate.receiverCheckpointDigest,
70
+ acceptanceReceiptId: candidate.receiverAcceptanceReceiptId, sourceCopiesVerified: false } });
71
+ }
72
+ export async function probe(input) {
73
+ rejectScopePrivileges(input);
74
+ const root = await scopeRoot(input.repositoryRoot);
75
+ const candidate = await verifiedScopeCandidate(input, root);
76
+ if (candidate && await handoffReadBudget(input)) return probeHandoffCandidate(input, root, candidate);
77
+ const hasBinding = ['flowId', 'stepId', 'snapshotId'].some(field => input[field] !== undefined);
78
+ const retained = hasBinding ? await retainedScope(input, root, true) : null;
79
+ const fresh = !!candidate || (!!retained && retained.stepId !== input.stepId);
80
+ if (fresh && (!retained || typeof input.signerConfigPath !== 'string' || !input.signerConfigPath || typeof input.challenge !== 'string' || !input.challenge)) {
81
+ scopeFail('ENTROROVE_SCOPE_ATTESTATION_BINDING', 'a new-step observation requires its preserved original scope, configured signer and new challenge binding');
82
+ }
83
+ const result = await observe(input, retained, fresh);
84
+ if (fresh) result.classification.decision = 'active';
85
+ if (fresh && !candidate && canonicalPaths(result.facts.files) !== canonicalPaths(retained.history.at(-1).facts.files)) {
86
+ scopeFail('ENTROROVE_SCOPE_CHANGED', 'fresh original-scope observation cannot replace its original scope paths; reassess growth explicitly');
87
+ }
88
+ if (result.classification.decision === 'bypass') return result;
89
+ if (['flowId', 'stepId', 'snapshotId'].some(field => input[field] !== undefined)) {
90
+ scopeBinding(input);
91
+ result.facts.snapshotEvidence = await verifyFreshScopeSnapshot(input, result.repositoryRoot, result.facts);
92
+ result.factsDigest = scopeDigest(result.facts);
93
+ if (fresh) {
94
+ await verifiedScopeCandidate(input, result.repositoryRoot, result.facts.snapshotEvidence.manifest);
95
+ return attestScopeFacts(input, { ...result, scopePurpose: candidate ? 'handoff-candidate' : 'fresh-observation',
96
+ revision: retained.revision,
97
+ classification: { ...result.classification, mode: candidate ? result.classification.mode : retained.mode }, requiredMode: result.classification.mode,
98
+ originalBinding: { flowId: retained.flowId, stepId: retained.stepId, snapshotId: retained.originalSnapshot.snapshotId },
99
+ snapshotDigest: result.facts.snapshotEvidence.snapshotDigest, preservedSnapshotDigest: retained.originalSnapshot.digest,
100
+ preservedSnapshotId: retained.originalSnapshot.snapshotId, currentSnapshotFresh: true });
101
+ }
102
+ const saved = await initializeScope(input, result.repositoryRoot, result.facts, result.classification);
103
+ return attestScopeFacts(input, { ...result, revision: saved.state.revision, statePath: saved.statePath,
104
+ classification: { ...result.classification, mode: saved.state.mode }, requiredMode: result.classification.mode,
105
+ snapshotDigest: result.facts.snapshotEvidence.snapshotDigest, preservedSnapshotDigest: saved.state.originalSnapshot.digest,
106
+ preservedSnapshotId: saved.state.originalSnapshot.snapshotId, currentSnapshotFresh: true });
107
+ }
108
+ return attestScopeFacts(input, result);
109
+ }
110
+ function observationReassessment(input, result) {
111
+ const previousMode = input.currentMode === undefined ? result.classification.mode : input.currentMode;
112
+ const current = scopeModes.indexOf(previousMode), required = scopeModes.indexOf(result.classification.mode);
113
+ const mode = required > current ? scopeModes[Math.min(current + 1, scopeModes.length - 1)] : previousMode;
114
+ return { ...result, operation: 'reassess', previousMode, mode, requiredMode: result.classification.mode,
115
+ classification: { ...result.classification, mode }, upgraded: mode !== previousMode,
116
+ preservedSnapshotId: null, currentSnapshotFresh: false };
117
+ }
118
+ export async function reassess(input) {
119
+ rejectScopePrivileges(input);
120
+ if (Object.hasOwn(input, 'handoffCandidate')) scopeFail('ENTROROVE_SCOPE_INPUT_INVALID', 'temporary handoff candidates cannot reassess or replace the original scope');
121
+ if (input.currentMode !== undefined && !scopeModes.includes(input.currentMode)) scopeFail('ENTROROVE_MODE_INVALID', 'currentMode must be lock, probe, or swarm');
122
+ const bound = ['flowId', 'stepId', 'expectedRevision', 'taskDigest', 'ownerId', 'chainDigest', 'repositoryId', 'challenge']
123
+ .some(field => input[field] !== undefined) || input.snapshotId != null || input.signerConfigPath != null;
124
+ if (!bound) return observationReassessment(input, await observe(input, null));
125
+ scopeBinding(input); const retained = await retainedScope(input, await scopeRoot(input.repositoryRoot));
126
+ const result = await observe(input, retained);
127
+ const saved = await upgradeScope(input, result.repositoryRoot, result.facts, result.classification);
128
+ return { ...result, operation: 'reassess', previousMode: saved.previousMode, mode: saved.mode, requiredMode: saved.requiredMode,
129
+ classification: { ...result.classification, mode: saved.mode, decision: 'active' }, upgraded: saved.upgraded,
130
+ revision: saved.state.revision, statePath: saved.statePath, snapshotDigest: saved.state.originalSnapshot.digest,
131
+ preservedSnapshotId: saved.state.originalSnapshot.snapshotId, preservedFactsDigest: saved.state.originalFactsDigest,
132
+ historyDigest: saved.state.history.at(-1).recordDigest, currentSnapshotFresh: false };
133
+ }
12
134
  export const LOCAL_PROBE_OPERATIONS = Object.freeze({ probe, reassess });
13
- export const LOCAL_PROBE_SCHEMAS = Object.freeze({ probe: { type: 'object', additionalProperties: false, required: ['repositoryRoot'] }, reassess: { type: 'object', additionalProperties: false, required: ['repositoryRoot', 'currentMode'] } });
135
+ export const LOCAL_PROBE_SCHEMAS = operationSchemasFor(LOCAL_PROBE_OPERATIONS);
@@ -0,0 +1,28 @@
1
+ import { canonical } from './local-integrity.mjs';
2
+ import { scopeFail } from './local-scope-files.mjs';
3
+ import { readTaskHandoffSnapshotBinding } from './local-task-handoff-fence.mjs';
4
+ import { HANDOFF_CANDIDATE_FIELDS, overlayAuthority } from './local-task-handoff-overlay-wire.mjs';
5
+
6
+ export function rejectScopePrivileges(input) {
7
+ if (['approved', 'verified', 'accepted', 'trusted', 'writeAllowed', 'pass', 'nonce'].some(field => Object.hasOwn(input, field))) {
8
+ scopeFail('ENTROROVE_SCOPE_INPUT_INVALID', 'scope observations reject caller approval, execution rights and receipt nonce assertions');
9
+ }
10
+ }
11
+ export async function verifiedScopeCandidate(input, root, snapshot) {
12
+ if (!Object.hasOwn(input, 'handoffCandidate')) return null;
13
+ const candidate = overlayAuthority(input.handoffCandidate, HANDOFF_CANDIDATE_FIELDS, 'entrorove.task-handoff-scope-challenge/1.0');
14
+ const fields = ['flowId', 'stepId', 'ownerId', 'taskDigest', 'chainDigest', 'repositoryId'];
15
+ if (fields.some(field => input[field] !== candidate[field]) || input.challenge !== candidate.challengeId
16
+ || typeof input.signerConfigPath !== 'string' || !input.signerConfigPath
17
+ || canonical(input.paths) !== canonical(candidate.paths)) scopeFail('ENTROROVE_SCOPE_CANDIDATE_BINDING', 'temporary scope must bind its exact original task, flow, repository, signer and real cloud challenge');
18
+ const binding = await readTaskHandoffSnapshotBinding({ ...input, repositoryRoot: root });
19
+ if (binding.handoffEpoch !== candidate.receiverEpoch || binding.handoffBaselineReceiptDigest !== candidate.baselineReceiptDigest) {
20
+ scopeFail('ENTROROVE_SCOPE_CANDIDATE_BINDING', 'temporary scope requires its registered external runner and current cloud receiver epoch');
21
+ }
22
+ if (snapshot && (snapshot.handoffEpoch !== binding.handoffEpoch || snapshot.handoffBaselineReceiptDigest !== binding.handoffBaselineReceiptDigest
23
+ || canonical(snapshot.paths) !== canonical(candidate.paths) || Date.parse(snapshot.createdAt) < Date.parse(candidate.issuedAt)
24
+ || !Number.isFinite(Date.parse(snapshot.createdAt)) || Date.parse(snapshot.createdAt) > Date.now())) {
25
+ scopeFail('ENTROROVE_SCOPE_CANDIDATE_BINDING', 'temporary scope requires actual fresh candidate copies bound to the exact cloud epoch and release baseline');
26
+ }
27
+ return candidate;
28
+ }
@@ -0,0 +1,28 @@
1
+ import path from 'node:path';
2
+ import { sha256 } from './local-integrity.mjs';
3
+ import { scopeFail, scopePath, scopeRead } from './local-scope-files.mjs';
4
+
5
+ export async function resolveScopePaths(input, root) {
6
+ const literal = Object.hasOwn(input, 'paths') ? input.paths : input.targetPaths;
7
+ if (input.targetSymbols === undefined) return literal === undefined
8
+ && !['flowId', 'stepId'].some(field => input[field] !== undefined) && input.snapshotId == null ? ['.'] : literal;
9
+ if (!Array.isArray(input.targetSymbols) || !input.targetSymbols.length || input.targetSymbols.some(symbol => typeof symbol !== 'string' || !symbol.trim())) scopeFail('ENTROROVE_CONTEXT_SYMBOL_INVALID', 'targetSymbols must contain exact non-empty symbols');
10
+ if (typeof input.contextMapPath !== 'string') scopeFail('ENTROROVE_CONTEXT_MAP_REQUIRED', 'exact symbols require a fresh ContextBase map');
11
+ let map;
12
+ try { map = JSON.parse((await scopeRead(root, scopePath(input.contextMapPath), input)).content.toString('utf8')); }
13
+ catch (error) { if (error.code) throw error; scopeFail('ENTROROVE_CONTEXT_MAP_INVALID', 'ContextBase map is invalid JSON'); }
14
+ if (map.schemaVersion !== 'entrorove.context-map/1.0' || map.repositoryDigest !== sha256(root).slice(0, 32) || !Array.isArray(map.entries)) scopeFail('ENTROROVE_CONTEXT_MAP_INVALID', 'ContextBase map is not bound to this repository');
15
+ const resolved = [];
16
+ for (const symbol of input.targetSymbols) {
17
+ const candidates = map.entries.filter(entry => entry?.symbol === symbol);
18
+ if (!candidates.length) scopeFail('ENTROROVE_CONTEXT_SYMBOL_MISSING', 'the exact symbol is absent from the map');
19
+ if (candidates.length !== 1) scopeFail('ENTROROVE_CONTEXT_SYMBOL_AMBIGUOUS', 'the exact symbol has more than one map entry');
20
+ const entry = candidates[0]; const relative = scopePath(entry.path);
21
+ const file = await scopeRead(root, relative, input);
22
+ if (file.sha256 !== entry.fileSha256) scopeFail('ENTROROVE_CONTEXT_MAP_STALE', 'the exact symbol map no longer matches the source hash');
23
+ resolved.push(relative);
24
+ }
25
+ const paths = [...new Set([...(literal ?? []), ...resolved])];
26
+ if (paths.some(relative => path.isAbsolute(relative))) scopeFail('ENTROROVE_PATH_UNSAFE', 'ContextBase map must contain repository-relative paths');
27
+ return paths;
28
+ }