@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +171 -26
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/conversationSniffer.js +25 -8
  8. package/dist/algo/cycleEngine.d.ts +63 -5
  9. package/dist/algo/cycleEngine.js +320 -42
  10. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  11. package/dist/algo/evolutionEvent.d.ts +26 -0
  12. package/dist/algo/evolutionEvent.js +32 -0
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +16 -3
  15. package/dist/algo/geneHealth.d.ts +36 -3
  16. package/dist/algo/geneHealth.js +47 -4
  17. package/dist/algo/geneIntake.d.ts +46 -1
  18. package/dist/algo/geneIntake.js +121 -10
  19. package/dist/algo/geneSelection.d.ts +126 -6
  20. package/dist/algo/geneSelection.js +479 -36
  21. package/dist/algo/index.d.ts +6 -1
  22. package/dist/algo/index.js +6 -1
  23. package/dist/algo/kautoProjection.d.ts +41 -0
  24. package/dist/algo/kautoProjection.js +95 -0
  25. package/dist/algo/kautoValidator.d.ts +68 -0
  26. package/dist/algo/kautoValidator.js +256 -0
  27. package/dist/algo/memoryGraph.d.ts +62 -0
  28. package/dist/algo/memoryGraph.js +86 -0
  29. package/dist/algo/orchestrator.d.ts +17 -1
  30. package/dist/algo/orchestrator.js +30 -4
  31. package/dist/algo/publishEligibility.d.ts +34 -0
  32. package/dist/algo/publishEligibility.js +52 -0
  33. package/dist/algo/solidify.d.ts +11 -2
  34. package/dist/algo/solidify.js +37 -7
  35. package/dist/algo/ucb1.d.ts +53 -0
  36. package/dist/algo/ucb1.js +156 -0
  37. package/dist/assetrepair/hubRejection.d.ts +12 -0
  38. package/dist/assetrepair/hubRejection.js +109 -0
  39. package/dist/assetrepair/index.d.ts +2 -0
  40. package/dist/assetrepair/index.js +2 -0
  41. package/dist/assetrepair/repair.d.ts +33 -0
  42. package/dist/assetrepair/repair.js +155 -0
  43. package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
  44. package/dist/assetstore/assetSidecarRecords.js +384 -0
  45. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  46. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  47. package/dist/assetstore/assetStoreHealth.d.ts +81 -0
  48. package/dist/assetstore/assetStoreHealth.js +319 -0
  49. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  50. package/dist/assetstore/assetStoreLayout.js +6 -0
  51. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  52. package/dist/assetstore/assetStoreStorage.js +336 -0
  53. package/dist/assetstore/assetSyncLedger.d.ts +91 -1
  54. package/dist/assetstore/assetSyncLedger.js +718 -59
  55. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  56. package/dist/assetstore/foreignJsonlSource.js +150 -0
  57. package/dist/assetstore/index.d.ts +5 -0
  58. package/dist/assetstore/index.js +5 -0
  59. package/dist/assetstore/learningHistory.js +3 -3
  60. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  61. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  62. package/dist/assetstore/localJsonl.d.ts +12 -1
  63. package/dist/assetstore/localJsonl.js +251 -42
  64. package/dist/assetstore/provenance.d.ts +93 -4
  65. package/dist/assetstore/provenance.js +368 -84
  66. package/dist/assetstore/provider.d.ts +63 -0
  67. package/dist/assetstore/provider.js +97 -6
  68. package/dist/assetstore/reviewFilter.d.ts +19 -1
  69. package/dist/assetstore/reviewFilter.js +39 -1
  70. package/dist/assetstore/reviewLedger.d.ts +8 -2
  71. package/dist/assetstore/reviewLedger.js +71 -45
  72. package/dist/assetstore/unionReadStore.d.ts +25 -0
  73. package/dist/assetstore/unionReadStore.js +119 -0
  74. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  75. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  76. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  77. package/dist/benchmark/antiGeneRollout.js +4 -3
  78. package/dist/benchmark/index.d.ts +3 -1
  79. package/dist/benchmark/index.js +3 -1
  80. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  81. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  82. package/dist/benchmark/triggerShift.d.ts +62 -0
  83. package/dist/benchmark/triggerShift.js +106 -0
  84. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  85. package/dist/bootstrap/envFingerprint.js +5 -0
  86. package/dist/bootstrap/index.d.ts +3 -1
  87. package/dist/bootstrap/index.js +3 -1
  88. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  89. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  90. package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
  91. package/dist/bootstrap/v1EnvCompat.js +303 -0
  92. package/dist/events/eventArchive.d.ts +2 -0
  93. package/dist/events/eventArchive.js +13 -3
  94. package/dist/events/eventSchema.d.ts +7 -7
  95. package/dist/events/eventStore.d.ts +2 -0
  96. package/dist/events/eventStore.js +5 -1
  97. package/dist/events/ingest.d.ts +2 -1
  98. package/dist/events/ingest.js +15 -0
  99. package/dist/events/paths.d.ts +12 -10
  100. package/dist/events/paths.js +24 -20
  101. package/dist/events/public.d.ts +2 -2
  102. package/dist/events/public.js +2 -2
  103. package/dist/events/reports.d.ts +2 -0
  104. package/dist/events/reports.js +4 -0
  105. package/dist/exec/autoExec.d.ts +68 -4
  106. package/dist/exec/autoExec.js +392 -33
  107. package/dist/exec/autonomousCycle.d.ts +31 -4
  108. package/dist/exec/autonomousCycle.js +71 -13
  109. package/dist/exec/claudeBridge.d.ts +80 -15
  110. package/dist/exec/claudeBridge.js +845 -76
  111. package/dist/exec/executionBinding.d.ts +414 -0
  112. package/dist/exec/executionBinding.js +588 -0
  113. package/dist/exec/index.d.ts +1 -0
  114. package/dist/exec/index.js +1 -0
  115. package/dist/exec/openPrRegistry.d.ts +8 -2
  116. package/dist/exec/openPrRegistry.js +32 -22
  117. package/dist/exec/prompt.js +14 -1
  118. package/dist/exec/proofOfWork.d.ts +1 -1
  119. package/dist/exec/proofOfWork.js +2 -2
  120. package/dist/exec/runnerRegistry.d.ts +153 -36
  121. package/dist/exec/runnerRegistry.js +848 -65
  122. package/dist/exec/selfPr.js +1 -7
  123. package/dist/feedback/envelope.d.ts +61 -0
  124. package/dist/feedback/envelope.js +168 -0
  125. package/dist/feedback/index.d.ts +1 -0
  126. package/dist/feedback/index.js +1 -0
  127. package/dist/hooks/hooks.js +1 -0
  128. package/dist/hub/assetCallLog.d.ts +35 -1
  129. package/dist/hub/assetCallLog.js +124 -1
  130. package/dist/hub/bindings.d.ts +8 -1
  131. package/dist/hub/bindings.js +29 -8
  132. package/dist/hub/capability.d.ts +130 -4
  133. package/dist/hub/conversationDistiller.d.ts +19 -0
  134. package/dist/hub/conversationDistiller.js +115 -37
  135. package/dist/hub/fake.d.ts +3 -2
  136. package/dist/hub/fake.js +2 -1
  137. package/dist/hub/index.d.ts +1 -0
  138. package/dist/hub/index.js +1 -0
  139. package/dist/hub/questionGenerator.d.ts +5 -1
  140. package/dist/hub/questionGenerator.js +8 -6
  141. package/dist/hub/recipeCompose.d.ts +27 -0
  142. package/dist/hub/recipeCompose.js +90 -0
  143. package/dist/hub/sanitize.js +122 -7
  144. package/dist/index.d.ts +6 -1
  145. package/dist/index.js +7 -1
  146. package/dist/issueReporter/index.d.ts +156 -0
  147. package/dist/issueReporter/index.js +1688 -0
  148. package/dist/mailbox/dispatch.d.ts +1 -1
  149. package/dist/mailbox/dispatch.js +22 -6
  150. package/dist/mailbox/envelope.d.ts +7 -1
  151. package/dist/mailbox/envelope.js +9 -2
  152. package/dist/mailbox/ipcServer.d.ts +12 -2
  153. package/dist/mailbox/ipcServer.js +183 -13
  154. package/dist/mailbox/store.d.ts +89 -3
  155. package/dist/mailbox/store.js +895 -41
  156. package/dist/modelCompatibility.d.ts +164 -0
  157. package/dist/modelCompatibility.js +309 -0
  158. package/dist/observers/valueDigestObserver.d.ts +9 -0
  159. package/dist/observers/valueDigestObserver.js +35 -2
  160. package/dist/ops/cleanup.js +1 -1
  161. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  162. package/dist/ops/evolutionGraphProjection.js +315 -0
  163. package/dist/ops/index.d.ts +2 -1
  164. package/dist/ops/index.js +2 -1
  165. package/dist/ops/savingsCore.js +1 -2
  166. package/dist/ops/selfUpdate.d.ts +18 -1
  167. package/dist/ops/selfUpdate.js +88 -23
  168. package/dist/ops/valueOutreach.d.ts +3 -1
  169. package/dist/ops/valueOutreach.js +5 -1
  170. package/dist/personality/schema.d.ts +24 -24
  171. package/dist/schema/evolutionGraph.d.ts +784 -0
  172. package/dist/schema/evolutionGraph.js +187 -0
  173. package/dist/schema/index.d.ts +1 -0
  174. package/dist/schema/index.js +1 -0
  175. package/dist/schema/proofOfWork.d.ts +125 -6
  176. package/dist/schema/proofOfWork.js +102 -4
  177. package/dist/schema/signal.d.ts +3 -3
  178. package/dist/schema/signal.js +1 -1
  179. package/dist/shadow/shadowHub.js +1 -0
  180. package/dist/signals/curriculum.d.ts +55 -0
  181. package/dist/signals/curriculum.js +202 -0
  182. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  183. package/dist/signals/expand.d.ts +15 -1
  184. package/dist/signals/expand.js +169 -1
  185. package/dist/signals/extractor.d.ts +2 -2
  186. package/dist/signals/extractor.js +31 -6
  187. package/dist/signals/index.d.ts +4 -1
  188. package/dist/signals/index.js +4 -1
  189. package/dist/signals/metaSignals.d.ts +4 -0
  190. package/dist/signals/metaSignals.js +42 -0
  191. package/dist/signals/scopeVocabulary.d.ts +75 -0
  192. package/dist/signals/scopeVocabulary.js +91 -0
  193. package/dist/signals/signalGate.js +1 -1
  194. package/dist/signals/taskDomain.d.ts +22 -0
  195. package/dist/signals/taskDomain.js +43 -0
  196. package/dist/strategy/constraintAblation.d.ts +64 -0
  197. package/dist/strategy/constraintAblation.js +2820 -0
  198. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  199. package/dist/strategy/constraintAblationPredicates.js +339 -0
  200. package/dist/strategy/index.d.ts +2 -1
  201. package/dist/strategy/index.js +2 -1
  202. package/dist/trace/index.d.ts +3 -1
  203. package/dist/trace/index.js +3 -1
  204. package/dist/trace/learningTrace.d.ts +216 -0
  205. package/dist/trace/learningTrace.js +298 -0
  206. package/dist/trace/proxyTurns.d.ts +31 -0
  207. package/dist/trace/proxyTurns.js +137 -0
  208. package/dist/trace/trajectory.d.ts +8 -0
  209. package/dist/trace/trajectory.js +14 -2
  210. package/dist/util/fetchPort.d.ts +1 -0
  211. package/dist/util/fetchPort.js +11 -0
  212. package/dist/util/fileLock.d.ts +113 -7
  213. package/dist/util/fileLock.js +1035 -80
  214. package/dist/util/index.d.ts +3 -1
  215. package/dist/util/index.js +2 -1
  216. package/dist/verify/index.d.ts +2 -1
  217. package/dist/verify/index.js +1 -1
  218. package/dist/verify/sandboxRunner.d.ts +30 -0
  219. package/dist/verify/sandboxRunner.js +370 -27
  220. package/dist/verify/sandboxedValidation.d.ts +16 -2
  221. package/dist/verify/sandboxedValidation.js +200 -22
  222. package/dist/verify/validation.d.ts +35 -4
  223. package/dist/verify/validation.js +164 -16
  224. package/dist/wire/geneHints.d.ts +88 -16
  225. package/dist/wire/geneHints.js +124 -15
  226. package/dist/wire/index.d.ts +8 -3
  227. package/dist/wire/index.js +2 -2
  228. package/dist/wire/schemaGate.d.ts +21 -0
  229. package/dist/wire/schemaGate.js +119 -10
  230. package/dist/workflow/dsl.d.ts +24 -3
  231. package/dist/workflow/dsl.js +4 -0
  232. package/dist/workflow/engine.d.ts +5 -1
  233. package/dist/workflow/engine.js +3 -0
  234. package/dist/workflow/index.d.ts +3 -1
  235. package/dist/workflow/index.js +3 -1
  236. package/dist/workflow/runtime.d.ts +110 -0
  237. package/dist/workflow/runtime.js +1298 -0
  238. package/dist/workflow/stateStore.d.ts +172 -0
  239. package/dist/workflow/stateStore.js +1044 -0
  240. package/package.json +12 -4
@@ -3,10 +3,119 @@
3
3
  // metachar rejection, node-eval-flag blocking, a scrubbed env, a SIGKILL timeout, and — where unprivileged
4
4
  // namespaces exist — no network + hidden home secrets, so a validation command cannot phone home / exfiltrate to
5
5
  // game the pass/fail. This is the "verifier outside the repo, untrusted caller" half the safety model relies on.
6
- import { existsSync } from 'node:fs';
7
- import { isAbsolute, resolve } from 'node:path';
8
- import { makeSandboxRunner, unshareNetAvailable } from './sandboxRunner.js';
9
- import { runValidation, validationScriptPath } from './validation.js';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs';
8
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
9
+ import { isolationCommand, makeSandboxRunner, sandboxResourceLimitsAvailable, unshareNetAvailable, } from './sandboxRunner.js';
10
+ import { runValidation, tokenizeValidationCommand, validationScriptPath, } from './validation.js';
11
+ let readOnlyIsolationAvailableCache;
12
+ let readOnlyFilesystemIsolationAvailableCache;
13
+ export function readOnlyFilesystemIsolationAvailable() {
14
+ if (process.platform !== 'linux')
15
+ return false;
16
+ if (readOnlyFilesystemIsolationAvailableCache !== undefined)
17
+ return readOnlyFilesystemIsolationAvailableCache;
18
+ const probeRoot = mkdtempSync('/var/tmp/evolver-isolation-probe-');
19
+ const probeHome = mkdtempSync('/var/tmp/evolver-isolation-home-');
20
+ const scratch = join(probeRoot, 'session');
21
+ const probeCwd = join(probeRoot, 'cwd');
22
+ try {
23
+ mkdirSync(scratch);
24
+ mkdirSync(probeCwd);
25
+ const probe = isolationCommand(process.execPath, ['--version'], {
26
+ noNetwork: true,
27
+ hideHomeSecrets: true,
28
+ readOnlyFilesystem: true,
29
+ writableTmpDir: scratch,
30
+ readOnlyRoot: probeCwd,
31
+ cwd: probeCwd,
32
+ });
33
+ readOnlyFilesystemIsolationAvailableCache = spawnSync(probe.cmd, probe.args, {
34
+ cwd: probeCwd,
35
+ env: {
36
+ HOME: process.env.HOME ?? probeHome,
37
+ PATH: process.env.PATH ?? '/usr/bin:/bin',
38
+ },
39
+ input: '\n',
40
+ timeout: 5_000,
41
+ stdio: ['pipe', 'ignore', 'ignore'],
42
+ }).status === 0;
43
+ }
44
+ catch {
45
+ readOnlyFilesystemIsolationAvailableCache = false;
46
+ }
47
+ finally {
48
+ rmSync(probeRoot, { recursive: true, force: true });
49
+ rmSync(probeHome, { recursive: true, force: true });
50
+ }
51
+ return readOnlyFilesystemIsolationAvailableCache;
52
+ }
53
+ export function readOnlyIsolationAvailable() {
54
+ if (process.platform !== 'linux')
55
+ return false;
56
+ if (readOnlyIsolationAvailableCache !== undefined)
57
+ return readOnlyIsolationAvailableCache;
58
+ readOnlyIsolationAvailableCache = sandboxResourceLimitsAvailable() && readOnlyFilesystemIsolationAvailable();
59
+ return readOnlyIsolationAvailableCache;
60
+ }
61
+ function validationReadOnlyRoot(cwd) {
62
+ let current = resolve(cwd);
63
+ while (true) {
64
+ if (existsSync(join(current, '.git')))
65
+ return current;
66
+ const parent = dirname(current);
67
+ if (parent === current)
68
+ return resolve(cwd);
69
+ current = parent;
70
+ }
71
+ }
72
+ function skippedCommand(cmd, script, reason) {
73
+ return { cmd, script, reason };
74
+ }
75
+ /**
76
+ * Validate the script target before it reaches the runner. The runner's mount namespace is read-only, not a chroot,
77
+ * so lexical containment alone is insufficient: a path inside the checkout may resolve through a symlink outside it.
78
+ * Symlink script entries are rejected outright, and the canonical target is checked immediately before execution.
79
+ */
80
+ function classifyValidationScript(cmd, script, validationCwd, readOnlyRoot) {
81
+ if (isAbsolute(script)) {
82
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_outside_root') };
83
+ }
84
+ let candidate;
85
+ try {
86
+ candidate = resolve(validationCwd, script);
87
+ }
88
+ catch {
89
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_unresolvable') };
90
+ }
91
+ if (!pathIsWithin(readOnlyRoot, candidate)) {
92
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_outside_root') };
93
+ }
94
+ let metadata;
95
+ try {
96
+ metadata = lstatSync(candidate);
97
+ }
98
+ catch {
99
+ return { ok: false, skipped: skippedCommand(cmd, script, 'missing_script') };
100
+ }
101
+ if (metadata.isSymbolicLink()) {
102
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_symlink') };
103
+ }
104
+ if (!metadata.isFile()) {
105
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_unresolvable') };
106
+ }
107
+ let canonicalScript;
108
+ try {
109
+ canonicalScript = realpathSync(candidate);
110
+ }
111
+ catch {
112
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_unresolvable') };
113
+ }
114
+ if (!pathIsWithin(readOnlyRoot, canonicalScript)) {
115
+ return { ok: false, skipped: skippedCommand(cmd, script, 'script_outside_root') };
116
+ }
117
+ return { ok: true };
118
+ }
10
119
  /**
11
120
  * Run validation commands in the hardened sandbox. Isolation (no-network + hidden home secrets) is requested only
12
121
  * when unprivileged namespaces are available; elsewhere (Windows/macOS) it degrades to the non-namespace hardening
@@ -15,42 +124,111 @@ import { runValidation, validationScriptPath } from './validation.js';
15
124
  * preserving prior behavior; note this differs from runValidation's own "no commands = not verified" stance.
16
125
  */
17
126
  export async function runSandboxedValidation(cmds, cwd, opts = {}) {
18
- const isolated = (opts.unshareCheck ?? unshareNetAvailable)();
127
+ if (opts.signal?.aborted)
128
+ return { passed: false, cancelled: true, score: 0.2, results: [], skipped: [], isolated: false };
129
+ const isolationCheck = opts.unshareCheck ?? (opts.requireIsolation ? readOnlyIsolationAvailable : unshareNetAvailable);
130
+ const isolated = isolationCheck();
131
+ if (opts.requireIsolation && !isolated) {
132
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped: [], isolated: false };
133
+ }
134
+ let validationCwd = resolve(cwd);
135
+ let readOnlyRoot = opts.readOnlyRoot ? resolve(opts.readOnlyRoot) : validationReadOnlyRoot(validationCwd);
136
+ // Canonicalize the validation root in every mode. Namespace isolation is an additional boundary, not a substitute
137
+ // for refusing an absolute, escaping, or symlinked script path; this also keeps the Windows/macOS path fail closed.
138
+ try {
139
+ validationCwd = realpathSync(validationCwd);
140
+ readOnlyRoot = realpathSync(readOnlyRoot);
141
+ }
142
+ catch {
143
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped: [], isolated };
144
+ }
145
+ if (dirname(readOnlyRoot) === readOnlyRoot || !pathIsWithin(readOnlyRoot, validationCwd)) {
146
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped: [], isolated };
147
+ }
19
148
  // ONLY a truly empty command set is a vacuous pass (nothing to verify). A non-empty set with blank entries is a
20
149
  // malformed plan, NOT a pass: blanks are kept (trimmed to '') so they fall outside the allowlist and fail, rather
21
150
  // than being silently dropped into a pass (Bugbot).
22
151
  if (cmds.length === 0)
23
- return { passed: true, score: 0.95, results: [], skipped: [], isolated };
24
- const runner = makeSandboxRunner({
25
- cwd,
26
- noNetwork: isolated,
27
- hideHomeSecrets: isolated,
28
- ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
29
- ...(opts.unshareCheck ? { unshareCheck: opts.unshareCheck } : {}),
30
- });
152
+ return { passed: true, cancelled: false, score: 0.95, results: [], skipped: [], isolated };
31
153
  const list = cmds.map((c) => String(c ?? '').trim());
32
- // Validation plans may reference repo-relative scripts that do not exist in this checkout; skip only those.
154
+ // Validation plans may reference repo-relative scripts that do not exist in this checkout; skip those, but never
155
+ // execute a script that is absolute, escapes the root, resolves through a symlink, or is not a regular file.
33
156
  const skipped = [];
34
157
  const runnable = [];
35
158
  for (const cmd of list) {
36
159
  const script = validationScriptPath(cmd);
37
- if (script && !isAbsolute(script) && !existsSync(resolve(cwd, script))) {
38
- skipped.push({ cmd, script, reason: 'missing_script' });
39
- console.warn(`[Validation] Skipping validation command (script not in repoRoot): ${script}`);
40
- continue;
160
+ if (script) {
161
+ const classification = classifyValidationScript(cmd, script, validationCwd, readOnlyRoot);
162
+ if (!classification.ok) {
163
+ skipped.push(classification.skipped);
164
+ if (classification.skipped.reason === 'missing_script') {
165
+ console.warn(`[Validation] Skipping validation command (script not in repoRoot): ${script}`);
166
+ }
167
+ else {
168
+ console.warn(`[Validation] Rejecting validation command (${classification.skipped.reason}): ${script}`);
169
+ }
170
+ continue;
171
+ }
41
172
  }
42
173
  runnable.push(cmd);
43
174
  }
44
175
  if (runnable.length === 0)
45
- return { passed: false, score: 0.2, results: [], skipped, isolated };
46
- const allowlist = [...new Set(runnable.map((c) => c.split(/\s+/)[0] ?? '').filter(Boolean))];
47
- const { results, passed } = await runValidation({ commands: runnable.map((cmd) => ({ cmd })), allowlist }, runner);
48
- const complete = skipped.length === 0;
176
+ return { passed: false, cancelled: opts.signal?.aborted === true, score: 0.2, results: [], skipped, isolated };
177
+ const scratch = isolated && opts.requireIsolation
178
+ ? mkdtempSync('/var/tmp/evolver-validation-')
179
+ : undefined;
180
+ const runner = makeSandboxRunner({
181
+ cwd: validationCwd,
182
+ noNetwork: isolated,
183
+ hideHomeSecrets: isolated,
184
+ readOnlyFilesystem: isolated && opts.requireIsolation,
185
+ resourceLimits: isolated && opts.requireIsolation,
186
+ writableTmpDir: scratch,
187
+ readOnlyRoot,
188
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
189
+ ...(opts.resourceGroupFactory ? { resourceGroupFactory: opts.resourceGroupFactory } : {}),
190
+ ...(opts.signal ? { signal: opts.signal } : {}),
191
+ unshareCheck: () => isolated,
192
+ });
193
+ // Re-check each script immediately before its spawn. The initial pass supplies precise skipped reasons, while
194
+ // this second gate closes the degraded-mode window where an earlier validator could replace a later path.
195
+ const guardedRunner = async (cmd, signal) => {
196
+ const script = validationScriptPath(cmd);
197
+ if (script) {
198
+ const classification = classifyValidationScript(cmd, script, validationCwd, readOnlyRoot);
199
+ if (!classification.ok) {
200
+ return {
201
+ exitCode: 126,
202
+ stdout: `[sandbox] rejected: ${classification.skipped.reason}: ${script}`,
203
+ };
204
+ }
205
+ }
206
+ return runner(cmd, signal);
207
+ };
208
+ const allowlist = [...new Set(runnable
209
+ .map((c) => tokenizeValidationCommand(c)?.[0] ?? '')
210
+ .filter(Boolean))];
211
+ let results;
212
+ let passed;
213
+ let cancelled;
214
+ try {
215
+ ({ results, passed, cancelled } = await runValidation({ commands: runnable.map((cmd) => ({ cmd })), allowlist }, guardedRunner, opts.signal));
216
+ }
217
+ finally {
218
+ if (scratch)
219
+ rmSync(scratch, { recursive: true, force: true });
220
+ }
221
+ const complete = skipped.length === 0 && !cancelled;
49
222
  return {
50
223
  passed: passed && complete,
224
+ cancelled,
51
225
  score: passed && complete ? 0.95 : 0.2,
52
226
  results,
53
227
  skipped,
54
228
  isolated,
55
229
  };
230
+ }
231
+ function pathIsWithin(root, target) {
232
+ const rel = relative(root, target);
233
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
56
234
  }
@@ -19,15 +19,35 @@ export interface RunOutput {
19
19
  exitCode: number;
20
20
  stdout: string;
21
21
  }
22
- export type CommandRunner = (cmd: string) => Promise<RunOutput> | RunOutput;
22
+ export type CommandRunner = (cmd: string, signal?: AbortSignal) => Promise<RunOutput> | RunOutput;
23
23
  export declare const SHELL_METACHARS: RegExp;
24
+ /**
25
+ * Parse one validation command without invoking a shell. Quotes group whitespace and backslash only escapes
26
+ * whitespace, quotes, or another backslash; no variable expansion or command substitution is performed.
27
+ * A null result means the command has an unterminated quote and must be rejected.
28
+ */
29
+ export declare function tokenizeValidationCommand(command: string): string[] | null;
24
30
  export declare const BLOCKED_NODE_FLAGS: Set<string>;
25
31
  export declare function normalizeExecutableName(executable: string): string;
26
32
  export declare function isNodeExecutable(executable: string): boolean;
27
33
  export declare function nodeFlagViolation(executable: string, args: readonly string[]): string | null;
34
+ export type NodeValidationInvocation = {
35
+ kind: 'script';
36
+ script: string;
37
+ } | {
38
+ kind: 'safe_no_script';
39
+ } | {
40
+ kind: 'invalid';
41
+ option?: string;
42
+ };
43
+ /**
44
+ * 将 Node 命令限制为明确的脚本位置,防止带参数的选项吞掉下一个 token 后绕过脚本路径检查。
45
+ * 验证只需要普通脚本、语法检查,以及兼容旧 Gene 的 version/help 身份检查;其他前置选项默认拒绝。
46
+ */
47
+ export declare function classifyNodeValidationInvocation(executable: string, args: readonly string[]): NodeValidationInvocation;
28
48
  /** 命令首 token(可执行名)是否在项目声明的 allowlist 内. */
29
49
  export declare function isAllowed(cmd: string, allowlist: readonly string[]): boolean;
30
- /** Extract the local script targeted by `node <script> ...`, or null for non-script node commands. */
50
+ /** Extract the unambiguous script targeted by `node <script> ...`, or null for non-script/unsupported commands. */
31
51
  export declare function validationScriptPath(cmd: string): string | null;
32
52
  /** stdout 摘要 = 首行 + 末行(去 canary.js 隐形约定: 不找魔法字符串, 只留可读摘要). */
33
53
  export declare function summarizeStdout(stdout: string): string;
@@ -35,7 +55,18 @@ export declare function summarizeStdout(stdout: string): string;
35
55
  * 跑校验(M4A-4). 判价值 = **exit code + stdout 摘要**, 不依赖 canary.js 魔法字符串(批注#22).
36
56
  * 不在 allowlist 的命令 auto-deny(不执行, allowed=false), 守无人值守安全.
37
57
  */
38
- export declare function runValidation(plan: ValidationPlan, run: CommandRunner): Promise<{
58
+ export declare function runValidation(plan: ValidationPlan, run: CommandRunner, signal?: AbortSignal): Promise<{
39
59
  results: ValidationResult[];
40
60
  passed: boolean;
41
- }>;
61
+ cancelled: boolean;
62
+ }>;
63
+ /**
64
+ * 检查验证命令是否安全可作为 Gene.validation 条目使用(忠实移植 v1 policyCheck.isValidationCommandAllowed)。
65
+ * Gene 验证命令只允许 `node …` 形式,不允许非 node 命令(如 pnpm/npm/echo 等)。
66
+ * 安全条件:
67
+ * 1. 必须以 `node ` 开头
68
+ * 2. 不包含命令替换(` 或 $()
69
+ * 3. 不包含 shell 元字符(引号外)
70
+ * 4. 不包含被阻止的 node 标志(-e/--eval/--print 等)
71
+ */
72
+ export declare function isValidationCommandAllowed(cmd: string): boolean;
@@ -1,6 +1,70 @@
1
1
  // With shell:false these are passed literally (harmless), but their presence means an injection attempt
2
2
  // or a command that wrongly assumes a shell (e.g. `a && b`) — reject loudly so it is split, not silently mis-run.
3
3
  export const SHELL_METACHARS = /[;&|`$<>\n\r]/;
4
+ /**
5
+ * Parse one validation command without invoking a shell. Quotes group whitespace and backslash only escapes
6
+ * whitespace, quotes, or another backslash; no variable expansion or command substitution is performed.
7
+ * A null result means the command has an unterminated quote and must be rejected.
8
+ */
9
+ export function tokenizeValidationCommand(command) {
10
+ const value = String(command ?? '');
11
+ const tokens = [];
12
+ let current = '';
13
+ let tokenStarted = false;
14
+ let quote = null;
15
+ let escaped = false;
16
+ for (let index = 0; index < value.length; index += 1) {
17
+ const ch = value[index];
18
+ if (escaped) {
19
+ current += ch;
20
+ tokenStarted = true;
21
+ escaped = false;
22
+ continue;
23
+ }
24
+ if (ch === '\\' && quote !== "'") {
25
+ const next = value[index + 1];
26
+ if (next !== undefined && (/\s/.test(next) || next === '"' || next === "'" || next === '\\')) {
27
+ escaped = true;
28
+ tokenStarted = true;
29
+ continue;
30
+ }
31
+ }
32
+ if (quote !== null) {
33
+ if (ch === quote) {
34
+ quote = null;
35
+ }
36
+ else {
37
+ current += ch;
38
+ }
39
+ tokenStarted = true;
40
+ continue;
41
+ }
42
+ if (ch === '"' || ch === "'") {
43
+ quote = ch;
44
+ tokenStarted = true;
45
+ continue;
46
+ }
47
+ if (/\s/.test(ch)) {
48
+ if (tokenStarted) {
49
+ tokens.push(current);
50
+ current = '';
51
+ tokenStarted = false;
52
+ }
53
+ continue;
54
+ }
55
+ current += ch;
56
+ tokenStarted = true;
57
+ }
58
+ if (quote !== null)
59
+ return null;
60
+ if (escaped) {
61
+ current += '\\';
62
+ tokenStarted = true;
63
+ }
64
+ if (tokenStarted)
65
+ tokens.push(current);
66
+ return tokens;
67
+ }
4
68
  // Node flags that turn `node` into an arbitrary-code evaluator or preload hook. A legit validation command is
5
69
  // `node <script> [args]`; these flags must be denied by the sandbox and must never be hidden by missing-script skip.
6
70
  export const BLOCKED_NODE_FLAGS = new Set([
@@ -12,6 +76,10 @@ export const BLOCKED_NODE_FLAGS = new Set([
12
76
  '--inspect', '--inspect-brk', '--inspect-port', '--inspect-publish-uid',
13
77
  // Watch: keeps the process alive past its work → hangs validation until the sandbox SIGKILLs it (DoS).
14
78
  '--watch', '--watch-path', '--watch-preserve-output',
79
+ // Package/test runners execute code selected from package metadata or the checkout, not one declared script.
80
+ '--run', '--test',
81
+ // Input-type changes stdin into an evaluator; validation must always name a script explicitly.
82
+ '--input-type', '-',
15
83
  // Module-resolution override: can redirect imports/requires to attacker-chosen code paths.
16
84
  '--conditions', '-C',
17
85
  ]);
@@ -28,32 +96,72 @@ export function nodeFlagViolation(executable, args) {
28
96
  if (!isNodeExecutable(executable))
29
97
  return null;
30
98
  for (const a of args) {
99
+ // Node stops interpreting its own flags at the first script (or `--` delimiter). Anything after that is
100
+ // an argv value owned by the script, even when it happens to match a blocked Node option name.
101
+ if (a === '--' || !a.startsWith('-'))
102
+ return null;
31
103
  const flag = a.split('=')[0] ?? a;
32
104
  if (BLOCKED_NODE_FLAGS.has(flag))
33
105
  return flag;
34
106
  }
35
107
  return null;
36
108
  }
109
+ const SAFE_NODE_SCRIPT_FLAGS = new Set(['-c', '--check']);
110
+ const SAFE_NODE_NO_SCRIPT_FLAGS = new Set(['-v', '--version', '-h', '--help']);
111
+ /**
112
+ * 将 Node 命令限制为明确的脚本位置,防止带参数的选项吞掉下一个 token 后绕过脚本路径检查。
113
+ * 验证只需要普通脚本、语法检查,以及兼容旧 Gene 的 version/help 身份检查;其他前置选项默认拒绝。
114
+ */
115
+ export function classifyNodeValidationInvocation(executable, args) {
116
+ if (!isNodeExecutable(executable))
117
+ return { kind: 'invalid' };
118
+ const blocked = nodeFlagViolation(executable, args);
119
+ if (blocked)
120
+ return { kind: 'invalid', option: blocked };
121
+ if (args.length === 1 && SAFE_NODE_NO_SCRIPT_FLAGS.has(args[0])) {
122
+ return { kind: 'safe_no_script' };
123
+ }
124
+ let index = 0;
125
+ let afterEndOfOptions = false;
126
+ while (index < args.length) {
127
+ const token = args[index];
128
+ if (token === '--') {
129
+ afterEndOfOptions = true;
130
+ index += 1;
131
+ break;
132
+ }
133
+ if (SAFE_NODE_SCRIPT_FLAGS.has(token)) {
134
+ index += 1;
135
+ continue;
136
+ }
137
+ if (token.startsWith('-')) {
138
+ return { kind: 'invalid', option: token.split('=')[0] ?? token };
139
+ }
140
+ break;
141
+ }
142
+ const script = args[index];
143
+ if (!script || (!afterEndOfOptions && script.startsWith('-')))
144
+ return { kind: 'invalid' };
145
+ return { kind: 'script', script };
146
+ }
37
147
  /** 命令首 token(可执行名)是否在项目声明的 allowlist 内. */
38
148
  export function isAllowed(cmd, allowlist) {
39
- const head = cmd.trim().split(/\s+/)[0] ?? '';
149
+ const tokens = tokenizeValidationCommand(cmd);
150
+ const head = tokens?.[0] ?? '';
40
151
  return allowlist.includes(head);
41
152
  }
42
- /** Extract the local script targeted by `node <script> ...`, or null for non-script node commands. */
153
+ /** Extract the unambiguous script targeted by `node <script> ...`, or null for non-script/unsupported commands. */
43
154
  export function validationScriptPath(cmd) {
44
- const tokens = String(cmd ?? '').trim().split(/\s+/);
155
+ const tokens = tokenizeValidationCommand(cmd);
156
+ if (!tokens || tokens.length < 2)
157
+ return null;
45
158
  const executable = tokens[0] ?? '';
46
- if (tokens.length < 2 || !isNodeExecutable(executable))
159
+ if (!isNodeExecutable(executable))
47
160
  return null;
48
- if (SHELL_METACHARS.test(cmd) || nodeFlagViolation(executable, tokens.slice(1)))
161
+ if (SHELL_METACHARS.test(cmd))
49
162
  return null;
50
- for (let i = 1; i < tokens.length; i += 1) {
51
- const token = tokens[i];
52
- if (token.startsWith('-'))
53
- continue;
54
- return /\.(?:c|m)?js$/.test(token) ? token : null;
55
- }
56
- return null;
163
+ const invocation = classifyNodeValidationInvocation(executable, tokens.slice(1));
164
+ return invocation.kind === 'script' ? invocation.script : null;
57
165
  }
58
166
  /** stdout 摘要 = 首行 + 末行(去 canary.js 隐形约定: 不找魔法字符串, 只留可读摘要). */
59
167
  export function summarizeStdout(stdout) {
@@ -68,18 +176,58 @@ export function summarizeStdout(stdout) {
68
176
  * 跑校验(M4A-4). 判价值 = **exit code + stdout 摘要**, 不依赖 canary.js 魔法字符串(批注#22).
69
177
  * 不在 allowlist 的命令 auto-deny(不执行, allowed=false), 守无人值守安全.
70
178
  */
71
- export async function runValidation(plan, run) {
179
+ export async function runValidation(plan, run, signal) {
72
180
  const results = [];
181
+ let cancelled = signal?.aborted === true;
73
182
  for (const c of plan.commands) {
183
+ if (signal?.aborted) {
184
+ cancelled = true;
185
+ break;
186
+ }
74
187
  const label = c.label ?? c.cmd;
75
188
  if (!isAllowed(c.cmd, plan.allowlist)) {
76
189
  results.push({ label, cmd: c.cmd, allowed: false, exitCode: null, stdoutSummary: '', passed: false });
77
190
  continue;
78
191
  }
79
- const out = await run(c.cmd);
192
+ const out = await run(c.cmd, signal);
80
193
  results.push({ label, cmd: c.cmd, allowed: true, exitCode: out.exitCode, stdoutSummary: summarizeStdout(out.stdout), passed: out.exitCode === 0 });
194
+ // runner 可能在返回结果后才观察到取消;此时不能让完整的 exit-0 列表伪装成通过。
195
+ if (signal?.aborted) {
196
+ cancelled = true;
197
+ break;
198
+ }
81
199
  }
82
200
  // 全部 allowed 的命令都 exit 0 才算通过; 有命令被 deny 视为未通过(校验不完整)
83
- const passed = results.length > 0 && results.every((r) => r.allowed && r.passed);
84
- return { results, passed };
201
+ const passed = !cancelled && results.length > 0 && results.every((r) => r.allowed && r.passed);
202
+ return { results, passed, cancelled };
203
+ }
204
+ /**
205
+ * 检查验证命令是否安全可作为 Gene.validation 条目使用(忠实移植 v1 policyCheck.isValidationCommandAllowed)。
206
+ * Gene 验证命令只允许 `node …` 形式,不允许非 node 命令(如 pnpm/npm/echo 等)。
207
+ * 安全条件:
208
+ * 1. 必须以 `node ` 开头
209
+ * 2. 不包含命令替换(` 或 $()
210
+ * 3. 不包含 shell 元字符(引号外)
211
+ * 4. 不包含被阻止的 node 标志(-e/--eval/--print 等)
212
+ */
213
+ export function isValidationCommandAllowed(cmd) {
214
+ const c = String(cmd || '').trim();
215
+ if (!c)
216
+ return false;
217
+ // 不允许命令替换
218
+ if (/`|\$\(/.test(c))
219
+ return false;
220
+ // 移除引号内的内容后检查 shell 元字符
221
+ const stripped = c.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
222
+ if (SHELL_METACHARS.test(stripped))
223
+ return false;
224
+ // 检查被阻止或会让脚本位置产生歧义的 node 标志
225
+ const tokens = tokenizeValidationCommand(c);
226
+ if (!tokens)
227
+ return false;
228
+ const executable = tokens[0] ?? '';
229
+ if (executable !== 'node')
230
+ return false;
231
+ const args = tokens.slice(1);
232
+ return classifyNodeValidationInvocation(executable, args).kind !== 'invalid';
85
233
  }