@evomap/evolver-core 2.0.0-beta.8 → 2.0.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 (147) hide show
  1. package/dist/algo/candidateAssembly.d.ts +7 -2
  2. package/dist/algo/candidateAssembly.js +81 -13
  3. package/dist/algo/capabilityCandidates.js +10 -0
  4. package/dist/algo/conversationSniffer.js +20 -7
  5. package/dist/algo/cycleEngine.d.ts +23 -2
  6. package/dist/algo/cycleEngine.js +236 -36
  7. package/dist/algo/exploration.d.ts +7 -0
  8. package/dist/algo/exploration.js +16 -3
  9. package/dist/algo/geneHealth.d.ts +2 -2
  10. package/dist/algo/geneHealth.js +5 -4
  11. package/dist/algo/geneSelection.d.ts +63 -5
  12. package/dist/algo/geneSelection.js +330 -30
  13. package/dist/algo/index.d.ts +2 -0
  14. package/dist/algo/index.js +2 -0
  15. package/dist/algo/orchestrator.d.ts +8 -1
  16. package/dist/algo/orchestrator.js +15 -3
  17. package/dist/algo/publishEligibility.d.ts +34 -0
  18. package/dist/algo/publishEligibility.js +52 -0
  19. package/dist/algo/ucb1.d.ts +53 -0
  20. package/dist/algo/ucb1.js +156 -0
  21. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  22. package/dist/assetstore/assetSidecarRecords.js +238 -0
  23. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  24. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  25. package/dist/assetstore/assetStoreHealth.js +56 -14
  26. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  27. package/dist/assetstore/assetStoreStorage.js +27 -9
  28. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  29. package/dist/assetstore/assetSyncLedger.js +683 -4
  30. package/dist/assetstore/index.d.ts +1 -0
  31. package/dist/assetstore/index.js +1 -0
  32. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  33. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  34. package/dist/assetstore/localJsonl.d.ts +4 -2
  35. package/dist/assetstore/localJsonl.js +91 -11
  36. package/dist/assetstore/provenance.d.ts +80 -4
  37. package/dist/assetstore/provenance.js +313 -5
  38. package/dist/assetstore/provider.d.ts +58 -1
  39. package/dist/assetstore/provider.js +97 -6
  40. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  41. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  42. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  43. package/dist/benchmark/antiGeneRollout.js +4 -3
  44. package/dist/benchmark/index.d.ts +2 -1
  45. package/dist/benchmark/index.js +2 -1
  46. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  47. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  48. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  49. package/dist/bootstrap/envFingerprint.js +5 -0
  50. package/dist/bootstrap/index.d.ts +2 -1
  51. package/dist/bootstrap/index.js +2 -1
  52. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  53. package/dist/bootstrap/v1EnvCompat.js +280 -0
  54. package/dist/events/eventArchive.d.ts +2 -0
  55. package/dist/events/eventArchive.js +13 -3
  56. package/dist/events/eventSchema.d.ts +7 -7
  57. package/dist/events/eventStore.d.ts +2 -0
  58. package/dist/events/eventStore.js +5 -1
  59. package/dist/events/ingest.d.ts +1 -0
  60. package/dist/events/ingest.js +1 -0
  61. package/dist/events/paths.d.ts +3 -1
  62. package/dist/events/paths.js +4 -0
  63. package/dist/events/public.d.ts +2 -2
  64. package/dist/events/public.js +2 -2
  65. package/dist/events/reports.d.ts +2 -0
  66. package/dist/events/reports.js +4 -0
  67. package/dist/exec/autoExec.d.ts +43 -2
  68. package/dist/exec/autoExec.js +76 -9
  69. package/dist/exec/autonomousCycle.d.ts +22 -4
  70. package/dist/exec/autonomousCycle.js +64 -13
  71. package/dist/exec/claudeBridge.d.ts +32 -7
  72. package/dist/exec/claudeBridge.js +281 -29
  73. package/dist/exec/prompt.js +5 -1
  74. package/dist/exec/runnerRegistry.d.ts +68 -26
  75. package/dist/exec/runnerRegistry.js +307 -72
  76. package/dist/exec/selfPr.js +1 -7
  77. package/dist/feedback/envelope.d.ts +61 -0
  78. package/dist/feedback/envelope.js +168 -0
  79. package/dist/feedback/index.d.ts +1 -0
  80. package/dist/feedback/index.js +1 -0
  81. package/dist/hooks/hooks.js +1 -0
  82. package/dist/hub/assetCallLog.d.ts +35 -1
  83. package/dist/hub/assetCallLog.js +124 -1
  84. package/dist/hub/bindings.d.ts +8 -1
  85. package/dist/hub/bindings.js +17 -6
  86. package/dist/hub/capability.d.ts +90 -3
  87. package/dist/hub/fake.d.ts +2 -2
  88. package/dist/hub/fake.js +1 -1
  89. package/dist/hub/questionGenerator.d.ts +5 -1
  90. package/dist/hub/questionGenerator.js +8 -6
  91. package/dist/hub/sanitize.js +3 -2
  92. package/dist/index.d.ts +3 -1
  93. package/dist/index.js +4 -1
  94. package/dist/mailbox/dispatch.d.ts +1 -1
  95. package/dist/mailbox/dispatch.js +22 -6
  96. package/dist/mailbox/envelope.d.ts +7 -1
  97. package/dist/mailbox/envelope.js +9 -2
  98. package/dist/mailbox/ipcServer.d.ts +10 -2
  99. package/dist/mailbox/ipcServer.js +163 -13
  100. package/dist/mailbox/store.d.ts +89 -3
  101. package/dist/mailbox/store.js +895 -41
  102. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  103. package/dist/ops/evolutionGraphProjection.js +315 -0
  104. package/dist/ops/index.d.ts +2 -1
  105. package/dist/ops/index.js +2 -1
  106. package/dist/ops/selfUpdate.d.ts +8 -0
  107. package/dist/ops/selfUpdate.js +24 -8
  108. package/dist/personality/schema.d.ts +16 -16
  109. package/dist/schema/evolutionGraph.d.ts +784 -0
  110. package/dist/schema/evolutionGraph.js +187 -0
  111. package/dist/schema/index.d.ts +1 -0
  112. package/dist/schema/index.js +1 -0
  113. package/dist/schema/signal.d.ts +3 -3
  114. package/dist/schema/signal.js +1 -1
  115. package/dist/signals/curriculum.d.ts +55 -0
  116. package/dist/signals/curriculum.js +202 -0
  117. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  118. package/dist/signals/expand.d.ts +15 -1
  119. package/dist/signals/expand.js +169 -1
  120. package/dist/signals/extractor.d.ts +2 -2
  121. package/dist/signals/extractor.js +31 -6
  122. package/dist/signals/index.d.ts +3 -1
  123. package/dist/signals/index.js +3 -1
  124. package/dist/signals/metaSignals.d.ts +4 -0
  125. package/dist/signals/metaSignals.js +42 -0
  126. package/dist/signals/signalGate.js +1 -1
  127. package/dist/signals/taskDomain.d.ts +22 -0
  128. package/dist/signals/taskDomain.js +43 -0
  129. package/dist/strategy/constraintAblation.d.ts +64 -0
  130. package/dist/strategy/constraintAblation.js +2820 -0
  131. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  132. package/dist/strategy/constraintAblationPredicates.js +339 -0
  133. package/dist/strategy/index.d.ts +2 -1
  134. package/dist/strategy/index.js +2 -1
  135. package/dist/trace/index.d.ts +3 -1
  136. package/dist/trace/index.js +3 -1
  137. package/dist/trace/learningTrace.d.ts +209 -0
  138. package/dist/trace/learningTrace.js +276 -0
  139. package/dist/trace/proxyTurns.d.ts +31 -0
  140. package/dist/trace/proxyTurns.js +137 -0
  141. package/dist/verify/sandboxRunner.d.ts +28 -0
  142. package/dist/verify/sandboxRunner.js +218 -19
  143. package/dist/verify/sandboxedValidation.d.ts +9 -0
  144. package/dist/verify/sandboxedValidation.js +113 -13
  145. package/dist/verify/validation.d.ts +11 -1
  146. package/dist/verify/validation.js +31 -0
  147. package/package.json +5 -2
@@ -5,41 +5,205 @@
5
5
  // module hardens EXECUTION: blocked node eval-flags, shell-metachar rejection, a fresh wiped temp cwd,
6
6
  // a scrubbed env (no secrets leak in), and a SIGKILL timeout. Output is folded + truncated.
7
7
  import { spawn, spawnSync } from 'node:child_process';
8
- import { mkdtempSync, rmSync } from 'node:fs';
8
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
9
9
  import { tmpdir } from 'node:os';
10
- import { join } from 'node:path';
10
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path';
11
11
  import { isNodeExecutable, nodeFlagViolation, SHELL_METACHARS } from './validation.js';
12
12
  const DEFAULT_TIMEOUT_MS = 60_000;
13
13
  const MAX_TIMEOUT_MS = 120_000;
14
14
  const MAX_OUTPUT_CHARS = 4000;
15
+ const CGROUP_ROOT = '/sys/fs/cgroup';
16
+ const TRUSTED_SYSTEM_PATH = '/usr/sbin:/usr/bin:/sbin:/bin';
17
+ const SYSTEM_SH = '/bin/sh';
18
+ const SYSTEM_UNSHARE = ['/usr/bin/unshare', '/bin/unshare'].find((path) => existsSync(path)) ?? '/usr/bin/unshare';
19
+ const RESOURCE_LIMITS = {
20
+ memoryBytes: 1024 * 1024 * 1024,
21
+ processes: 64,
22
+ cpuQuota: '100000 100000',
23
+ scratchBytes: 256 * 1024 * 1024,
24
+ };
15
25
  // Env scrub: only these keys reach the child, so node-secrets/tokens in the parent env cannot leak into validation.
16
26
  const DEFAULT_ENV_ALLOW = ['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'SYSTEMROOT'];
17
27
  /** Credential dirs hidden under $HOME when hideHomeSecrets is set (dirs only — never where binaries live). */
18
28
  const SECRET_HOME_DIRS = ['.evomap', '.ssh', '.aws', '.gnupg', '.docker', '.kube'];
19
29
  // Fixed setup run inside the mount ns: tmpfs over each existing secret dir, then exec the command. The command
20
30
  // is passed as POSITIONAL args (`exec "$@"`), never interpolated into the script — no shell-injection vector.
21
- const HIDE_SECRETS_SCRIPT = `for d in ${SECRET_HOME_DIRS.map((d) => `"$HOME/${d}"`).join(' ')}; do [ -d "$d" ] && mount -t tmpfs none "$d" 2>/dev/null; done; exec "$@"`;
31
+ const HIDE_SECRETS_SETUP = `PATH=${TRUSTED_SYSTEM_PATH}; export PATH; for d in ${SECRET_HOME_DIRS.map((d) => `"$HOME/${d}"`).join(' ')}; do if [ -d "$d" ]; then mount -t tmpfs none "$d" 2>/dev/null || exit 126; fi; done`;
32
+ const READ_ONLY_FILESYSTEM_SETUP = [
33
+ `PATH=${TRUSTED_SYSTEM_PATH}; export PATH`,
34
+ 'session_tmp="$1"; shift',
35
+ 'validation_root="$1"; shift',
36
+ 'validation_cwd="$1"; shift',
37
+ 'host_bin="$1"; shift',
38
+ '[ -n "$HOME" ] && [ "$HOME" != / ] && [ -d "$HOME" ] || exit 126',
39
+ 'case "$validation_cwd/" in "$validation_root/"*) ;; *) exit 126 ;; esac',
40
+ 'case "$HOME/" in "$validation_root/"*) exit 126 ;; esac',
41
+ `mount -t tmpfs -o size=${RESOURCE_LIMITS.scratchBytes},nr_inodes=65536 none "$session_tmp" 2>/dev/null || exit 126`,
42
+ 'mkdir -p "$session_tmp/source" "$session_tmp/home" || exit 126',
43
+ 'mkdir -p "$session_tmp/dev" || exit 126',
44
+ ': > "$session_tmp/node" || exit 126',
45
+ 'mount --bind "$validation_root" "$session_tmp/source" 2>/dev/null || exit 126',
46
+ 'mount --bind "$host_bin" "$session_tmp/node" 2>/dev/null || exit 126',
47
+ 'for device in null zero random urandom; do : > "$session_tmp/dev/$device" || exit 126; mount --bind "/dev/$device" "$session_tmp/dev/$device" 2>/dev/null || exit 126; done',
48
+ 'mount -t tmpfs none "$HOME" 2>/dev/null || exit 126',
49
+ '[ ! -d /run ] || mount -t tmpfs none /run 2>/dev/null || exit 126',
50
+ 'mount -t tmpfs none /tmp 2>/dev/null || exit 126',
51
+ 'mkdir -p "$validation_root" || exit 126',
52
+ 'mount --bind "$session_tmp/source" "$validation_root" 2>/dev/null || exit 126',
53
+ 'mounts="$(findmnt -rn -o TARGET -R /)" || exit 126',
54
+ '[ -n "$mounts" ] || exit 126',
55
+ 'printf "%s\\n" "$mounts" | while IFS= read -r target; do mount -o remount,bind,ro "$target" 2>/dev/null || exit 126; done || exit 126',
56
+ 'mount -o remount,bind,rw,exec "$session_tmp" 2>/dev/null || exit 126',
57
+ 'mount -t tmpfs -o mode=755,size=65536,nr_inodes=64 none /dev 2>/dev/null || exit 126',
58
+ 'for device in null zero random urandom; do : > "/dev/$device" || exit 126; mount --bind "$session_tmp/dev/$device" "/dev/$device" 2>/dev/null || exit 126; done',
59
+ 'mount -o remount,ro /dev 2>/dev/null || exit 126',
60
+ 'mount --rbind "$session_tmp" /tmp 2>/dev/null || exit 126',
61
+ 'cd "$validation_cwd" || exit 126',
62
+ 'export HOME=/tmp/home TMPDIR=/tmp TMP=/tmp TEMP=/tmp',
63
+ ].join('; ');
64
+ const DROP_NAMESPACE_PRIVILEGES = [
65
+ 'supervisor_pid=$$',
66
+ 'exec 3<&0',
67
+ 'setpriv --no-new-privs --securebits=+noroot,+noroot_locked --bounding-set=-all --inh-caps=-all --ambient-caps=-all -- /tmp/node "$@" </dev/null 3<&- & workload_pid=$!',
68
+ // The pipe is owned by the parent Node process. Kernel EOF is therefore a reliable parent-death signal,
69
+ // including SIGKILL, while unshare --kill-child propagates supervisor death through the PID namespace.
70
+ '(IFS= read -r _ <&3 || kill -KILL "$supervisor_pid") & watchdog_pid=$!',
71
+ 'exec 3<&-',
72
+ 'wait "$workload_pid"; status=$?',
73
+ 'kill "$watchdog_pid" 2>/dev/null || true',
74
+ 'wait "$watchdog_pid" 2>/dev/null || true',
75
+ 'exit "$status"',
76
+ ].join('; ');
77
+ function pathIsWithin(root, target) {
78
+ const rel = relative(root, target);
79
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
80
+ }
81
+ function currentCgroupPath() {
82
+ const entry = readFileSync('/proc/self/cgroup', 'utf8')
83
+ .split(/\r?\n/)
84
+ .find((line) => line.startsWith('0::'));
85
+ if (!entry)
86
+ return null;
87
+ const relativePath = entry.slice(3).replace(/^\/+/, '');
88
+ const path = resolve(CGROUP_ROOT, relativePath);
89
+ return pathIsWithin(CGROUP_ROOT, path) ? path : null;
90
+ }
91
+ /** Configure an already-created cgroup. Kept separate so limit values are testable without resource exhaustion. */
92
+ export function configureSandboxResourceGroup(path) {
93
+ try {
94
+ const required = ['cgroup.procs', 'cgroup.kill', 'memory.max', 'memory.swap.max', 'memory.oom.group', 'pids.max', 'cpu.max'];
95
+ if (!required.every((name) => existsSync(join(path, name))))
96
+ return false;
97
+ writeFileSync(join(path, 'memory.max'), String(RESOURCE_LIMITS.memoryBytes));
98
+ writeFileSync(join(path, 'memory.swap.max'), '0');
99
+ writeFileSync(join(path, 'memory.oom.group'), '1');
100
+ writeFileSync(join(path, 'pids.max'), String(RESOURCE_LIMITS.processes));
101
+ writeFileSync(join(path, 'cpu.max'), RESOURCE_LIMITS.cpuQuota);
102
+ return true;
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
108
+ /** Allocate a delegated cgroup v2 for one validation command. Returns null unless every limit is enforceable. */
109
+ export function createSandboxResourceGroup() {
110
+ if (process.platform !== 'linux' || !existsSync(join(CGROUP_ROOT, 'cgroup.controllers')))
111
+ return null;
112
+ let path = null;
113
+ try {
114
+ const parent = currentCgroupPath();
115
+ if (!parent)
116
+ return null;
117
+ path = join(parent, `evolver-validation-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
118
+ mkdirSync(path, { mode: 0o700 });
119
+ if (!configureSandboxResourceGroup(path))
120
+ throw new Error('required cgroup v2 controllers are not delegated');
121
+ let cleaned = false;
122
+ return {
123
+ procsFile: join(path, 'cgroup.procs'),
124
+ cleanup: () => {
125
+ if (cleaned)
126
+ return;
127
+ cleaned = true;
128
+ try {
129
+ writeFileSync(join(path, 'cgroup.kill'), '1');
130
+ }
131
+ catch { /* already empty or removed */ }
132
+ try {
133
+ rmdirSync(path);
134
+ }
135
+ catch { /* kernel may release the empty group asynchronously */ }
136
+ },
137
+ };
138
+ }
139
+ catch {
140
+ if (path) {
141
+ try {
142
+ writeFileSync(join(path, 'cgroup.kill'), '1');
143
+ }
144
+ catch { /* best effort */ }
145
+ try {
146
+ rmdirSync(path);
147
+ }
148
+ catch { /* best effort */ }
149
+ }
150
+ return null;
151
+ }
152
+ }
153
+ /** Join the cgroup before exec, so attacker-controlled code never runs outside the aggregate resource budget. */
154
+ export function resourceLimitedCommand(cmd, args, procsFile) {
155
+ const setup = 'printf "%s" "$$" > "$1" || exit 126; shift; exec "$@"';
156
+ return { cmd: SYSTEM_SH, args: ['-c', setup, 'sh', procsFile, cmd, ...args] };
157
+ }
158
+ export function sandboxResourceLimitsAvailable() {
159
+ const group = createSandboxResourceGroup();
160
+ if (!group)
161
+ return false;
162
+ group.cleanup();
163
+ return true;
164
+ }
22
165
  /** Wrap an (executable,args) in unprivileged namespaces per the requested isolation (pure — testable). */
23
166
  export function isolationCommand(bin, args, opts) {
167
+ if (opts.readOnlyFilesystem && !opts.writableTmpDir) {
168
+ throw new Error('read-only filesystem isolation requires writableTmpDir');
169
+ }
170
+ if (opts.readOnlyFilesystem && (!opts.readOnlyRoot || !opts.cwd)) {
171
+ throw new Error('read-only filesystem isolation requires readOnlyRoot and cwd');
172
+ }
24
173
  const flags = ['-r'];
25
- if (opts.hideHomeSecrets)
174
+ if (opts.hideHomeSecrets || opts.readOnlyFilesystem)
26
175
  flags.push('-m');
27
176
  if (opts.noNetwork)
28
177
  flags.push('-n');
29
- if (!opts.noNetwork && !opts.hideHomeSecrets)
178
+ if (opts.readOnlyFilesystem)
179
+ flags.push('-p', '-i', '-u');
180
+ if (!opts.noNetwork && !opts.hideHomeSecrets && !opts.readOnlyFilesystem)
30
181
  return { cmd: bin, args: [...args] };
31
- const base = [...flags, '--fork', '--kill-child', '--'];
182
+ const base = [...flags, '--fork', '--kill-child', ...(opts.readOnlyFilesystem ? ['--mount-proc'] : []), '--'];
183
+ const setup = [
184
+ ...(opts.hideHomeSecrets && !opts.readOnlyFilesystem ? [HIDE_SECRETS_SETUP] : []),
185
+ ...(opts.readOnlyFilesystem ? [READ_ONLY_FILESYSTEM_SETUP] : []),
186
+ opts.readOnlyFilesystem ? DROP_NAMESPACE_PRIVILEGES : 'exec "$@"',
187
+ ].join('; ');
32
188
  // Mount setup needs a launcher (sh runs the FIXED script then exec "$@"); net-only needs no shell.
33
- return opts.hideHomeSecrets
34
- ? { cmd: 'unshare', args: [...base, 'sh', '-c', HIDE_SECRETS_SCRIPT, 'sh', bin, ...args] }
35
- : { cmd: 'unshare', args: [...base, bin, ...args] };
189
+ return opts.hideHomeSecrets || opts.readOnlyFilesystem
190
+ ? {
191
+ cmd: SYSTEM_UNSHARE,
192
+ args: [...base, SYSTEM_SH, '-c', setup, 'sh', ...(opts.readOnlyFilesystem
193
+ ? [opts.writableTmpDir, opts.readOnlyRoot, opts.cwd]
194
+ : []), bin, ...args],
195
+ }
196
+ : { cmd: SYSTEM_UNSHARE, args: [...base, bin, ...args] };
36
197
  }
37
198
  let unshareCache;
38
199
  /** Whether unprivileged user+mount+net namespaces (`unshare -r -m -n`) work here (cached) — covers both isolation modes. */
39
200
  export function unshareNetAvailable() {
40
201
  if (unshareCache === undefined) {
41
202
  try {
42
- unshareCache = spawnSync('unshare', ['-r', '-m', '-n', 'true'], { timeout: 5000 }).status === 0;
203
+ unshareCache = spawnSync(SYSTEM_UNSHARE, ['-r', '-m', '-n', 'true'], {
204
+ env: { PATH: TRUSTED_SYSTEM_PATH },
205
+ timeout: 5000,
206
+ }).status === 0;
43
207
  }
44
208
  catch {
45
209
  unshareCache = false;
@@ -78,28 +242,63 @@ export function makeSandboxRunner(opts = {}) {
78
242
  if (badFlag)
79
243
  return deny(`rejected: blocked node flag ${badFlag}`);
80
244
  // Isolation (#26): fail-safe — if we can't actually create the namespaces, refuse rather than run un-isolated.
81
- if ((opts.noNetwork || opts.hideHomeSecrets) && !(opts.unshareCheck ?? unshareNetAvailable)()) {
82
- return deny('rejected: isolation (noNetwork/hideHomeSecrets) requested but unprivileged namespaces (unshare -r -m -n) are unavailable here');
245
+ if ((opts.noNetwork || opts.hideHomeSecrets || opts.readOnlyFilesystem) && !(opts.unshareCheck ?? unshareNetAvailable)()) {
246
+ return deny('rejected: requested namespace isolation is unavailable');
83
247
  }
248
+ const resourceGroup = opts.resourceLimits
249
+ ? (opts.resourceGroupFactory ?? createSandboxResourceGroup)()
250
+ : null;
251
+ if (opts.resourceLimits && !resourceGroup)
252
+ return deny('rejected: cgroup v2 resource limits are unavailable');
84
253
  const ownTemp = !opts.cwd;
85
254
  let cwd;
86
255
  try {
87
256
  cwd = opts.cwd ?? mkdtempSync(join(tmpdir(), 'evo-sbx-'));
88
257
  }
89
258
  catch (e) {
259
+ resourceGroup?.cleanup();
90
260
  return deny(`temp dir failed: ${e instanceof Error ? e.message : 'err'}`, 1);
91
261
  }
92
- const cleanup = () => { if (ownTemp) {
93
- try {
94
- rmSync(cwd, { recursive: true, force: true });
262
+ let cleaned = false;
263
+ const cleanup = () => {
264
+ if (cleaned)
265
+ return;
266
+ cleaned = true;
267
+ resourceGroup?.cleanup();
268
+ if (ownTemp) {
269
+ try {
270
+ rmSync(cwd, { recursive: true, force: true });
271
+ }
272
+ catch { /* best effort */ }
95
273
  }
96
- catch { /* best effort */ }
97
- } };
274
+ };
98
275
  // Resolve 'node' to the running binary so spawn(shell:false) works on Windows too (where bare 'node' would ENOENT).
99
276
  const bin = isNodeExecutable(executable) ? process.execPath : executable;
100
277
  try {
101
- const { cmd: spawnCmd, args: spawnArgs } = isolationCommand(bin, args, { noNetwork: opts.noNetwork, hideHomeSecrets: opts.hideHomeSecrets });
102
- const child = spawn(spawnCmd, spawnArgs, { shell: false, cwd, env: scrubEnv(envAllow), timeout, killSignal: 'SIGKILL', stdio: ['ignore', 'pipe', 'pipe'] });
278
+ const { cmd: spawnCmd, args: spawnArgs } = isolationCommand(bin, args, {
279
+ noNetwork: opts.noNetwork,
280
+ hideHomeSecrets: opts.hideHomeSecrets,
281
+ readOnlyFilesystem: opts.readOnlyFilesystem,
282
+ writableTmpDir: opts.writableTmpDir,
283
+ readOnlyRoot: opts.readOnlyRoot ?? cwd,
284
+ cwd,
285
+ });
286
+ const env = scrubEnv(envAllow);
287
+ if (opts.noNetwork || opts.hideHomeSecrets || opts.readOnlyFilesystem || opts.resourceLimits) {
288
+ env['PATH'] = TRUSTED_SYSTEM_PATH;
289
+ }
290
+ if (opts.readOnlyFilesystem) {
291
+ Object.assign(env, { TMPDIR: '/tmp', TMP: '/tmp', TEMP: '/tmp' });
292
+ }
293
+ const limited = resourceGroup ? resourceLimitedCommand(spawnCmd, spawnArgs, resourceGroup.procsFile) : { cmd: spawnCmd, args: spawnArgs };
294
+ const child = spawn(limited.cmd, limited.args, {
295
+ shell: false,
296
+ cwd,
297
+ env,
298
+ timeout,
299
+ killSignal: 'SIGKILL',
300
+ stdio: [opts.readOnlyFilesystem ? 'pipe' : 'ignore', 'pipe', 'pipe'],
301
+ });
103
302
  let out = '';
104
303
  const cap = (d) => { if (out.length < MAX_OUTPUT_CHARS)
105
304
  out += String(d); };
@@ -1,3 +1,4 @@
1
+ import { type SandboxResourceGroup } from './sandboxRunner.js';
1
2
  import { type ValidationResult } from './validation.js';
2
3
  export interface SandboxedValidationSkippedCommand {
3
4
  cmd: string;
@@ -19,11 +20,19 @@ export interface SandboxedValidationResult {
19
20
  */
20
21
  isolated: boolean;
21
22
  }
23
+ export declare function readOnlyFilesystemIsolationAvailable(): boolean;
24
+ export declare function readOnlyIsolationAvailable(): boolean;
22
25
  export interface SandboxedValidationOptions {
23
26
  /** Per-command timeout (ms), forwarded to the sandbox runner. */
24
27
  timeoutMs?: number;
25
28
  /** Test seam: override the unprivileged-namespace availability probe. */
26
29
  unshareCheck?: () => boolean;
30
+ /** Refuse before spawning when network/home namespace isolation is unavailable. */
31
+ requireIsolation?: boolean;
32
+ /** Checkout root to preserve read-only at its original absolute path. */
33
+ readOnlyRoot?: string;
34
+ /** Injected cgroup allocator (test seam). */
35
+ resourceGroupFactory?: () => SandboxResourceGroup | null;
27
36
  }
28
37
  /**
29
38
  * Run validation commands in the hardened sandbox. Isolation (no-network + hidden home secrets) is requested only
@@ -3,10 +3,72 @@
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';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { existsSync, 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';
9
10
  import { runValidation, 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
+ }
10
72
  /**
11
73
  * Run validation commands in the hardened sandbox. Isolation (no-network + hidden home secrets) is requested only
12
74
  * when unprivileged namespaces are available; elsewhere (Windows/macOS) it degrades to the non-namespace hardening
@@ -15,26 +77,37 @@ import { runValidation, validationScriptPath } from './validation.js';
15
77
  * preserving prior behavior; note this differs from runValidation's own "no commands = not verified" stance.
16
78
  */
17
79
  export async function runSandboxedValidation(cmds, cwd, opts = {}) {
18
- const isolated = (opts.unshareCheck ?? unshareNetAvailable)();
80
+ const isolationCheck = opts.unshareCheck ?? (opts.requireIsolation ? readOnlyIsolationAvailable : unshareNetAvailable);
81
+ const isolated = isolationCheck();
82
+ if (opts.requireIsolation && !isolated) {
83
+ return { passed: false, score: 0.2, results: [], skipped: [], isolated: false };
84
+ }
85
+ let validationCwd = resolve(cwd);
86
+ let readOnlyRoot = opts.readOnlyRoot ? resolve(opts.readOnlyRoot) : validationReadOnlyRoot(validationCwd);
87
+ if (opts.requireIsolation && isolated) {
88
+ try {
89
+ validationCwd = realpathSync(validationCwd);
90
+ readOnlyRoot = realpathSync(readOnlyRoot);
91
+ }
92
+ catch {
93
+ return { passed: false, score: 0.2, results: [], skipped: [], isolated };
94
+ }
95
+ if (dirname(readOnlyRoot) === readOnlyRoot || !pathIsWithin(readOnlyRoot, validationCwd)) {
96
+ return { passed: false, score: 0.2, results: [], skipped: [], isolated };
97
+ }
98
+ }
19
99
  // ONLY a truly empty command set is a vacuous pass (nothing to verify). A non-empty set with blank entries is a
20
100
  // malformed plan, NOT a pass: blanks are kept (trimmed to '') so they fall outside the allowlist and fail, rather
21
101
  // than being silently dropped into a pass (Bugbot).
22
102
  if (cmds.length === 0)
23
103
  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
- });
31
104
  const list = cmds.map((c) => String(c ?? '').trim());
32
105
  // Validation plans may reference repo-relative scripts that do not exist in this checkout; skip only those.
33
106
  const skipped = [];
34
107
  const runnable = [];
35
108
  for (const cmd of list) {
36
109
  const script = validationScriptPath(cmd);
37
- if (script && !isAbsolute(script) && !existsSync(resolve(cwd, script))) {
110
+ if (script && !isAbsolute(script) && !existsSync(resolve(validationCwd, script))) {
38
111
  skipped.push({ cmd, script, reason: 'missing_script' });
39
112
  console.warn(`[Validation] Skipping validation command (script not in repoRoot): ${script}`);
40
113
  continue;
@@ -43,8 +116,31 @@ export async function runSandboxedValidation(cmds, cwd, opts = {}) {
43
116
  }
44
117
  if (runnable.length === 0)
45
118
  return { passed: false, score: 0.2, results: [], skipped, isolated };
119
+ const scratch = isolated && opts.requireIsolation
120
+ ? mkdtempSync('/var/tmp/evolver-validation-')
121
+ : undefined;
122
+ const runner = makeSandboxRunner({
123
+ cwd: validationCwd,
124
+ noNetwork: isolated,
125
+ hideHomeSecrets: isolated,
126
+ readOnlyFilesystem: isolated && opts.requireIsolation,
127
+ resourceLimits: isolated && opts.requireIsolation,
128
+ writableTmpDir: scratch,
129
+ readOnlyRoot,
130
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
131
+ ...(opts.resourceGroupFactory ? { resourceGroupFactory: opts.resourceGroupFactory } : {}),
132
+ unshareCheck: () => isolated,
133
+ });
46
134
  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);
135
+ let results;
136
+ let passed;
137
+ try {
138
+ ({ results, passed } = await runValidation({ commands: runnable.map((cmd) => ({ cmd })), allowlist }, runner));
139
+ }
140
+ finally {
141
+ if (scratch)
142
+ rmSync(scratch, { recursive: true, force: true });
143
+ }
48
144
  const complete = skipped.length === 0;
49
145
  return {
50
146
  passed: passed && complete,
@@ -53,4 +149,8 @@ export async function runSandboxedValidation(cmds, cwd, opts = {}) {
53
149
  skipped,
54
150
  isolated,
55
151
  };
152
+ }
153
+ function pathIsWithin(root, target) {
154
+ const rel = relative(root, target);
155
+ return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
56
156
  }
@@ -38,4 +38,14 @@ export declare function summarizeStdout(stdout: string): string;
38
38
  export declare function runValidation(plan: ValidationPlan, run: CommandRunner): Promise<{
39
39
  results: ValidationResult[];
40
40
  passed: boolean;
41
- }>;
41
+ }>;
42
+ /**
43
+ * 检查验证命令是否安全可作为 Gene.validation 条目使用(忠实移植 v1 policyCheck.isValidationCommandAllowed)。
44
+ * Gene 验证命令只允许 `node …` 形式,不允许非 node 命令(如 pnpm/npm/echo 等)。
45
+ * 安全条件:
46
+ * 1. 必须以 `node ` 开头
47
+ * 2. 不包含命令替换(` 或 $()
48
+ * 3. 不包含 shell 元字符(引号外)
49
+ * 4. 不包含被阻止的 node 标志(-e/--eval/--print 等)
50
+ */
51
+ export declare function isValidationCommandAllowed(cmd: string): boolean;
@@ -82,4 +82,35 @@ export async function runValidation(plan, run) {
82
82
  // 全部 allowed 的命令都 exit 0 才算通过; 有命令被 deny 视为未通过(校验不完整)
83
83
  const passed = results.length > 0 && results.every((r) => r.allowed && r.passed);
84
84
  return { results, passed };
85
+ }
86
+ /**
87
+ * 检查验证命令是否安全可作为 Gene.validation 条目使用(忠实移植 v1 policyCheck.isValidationCommandAllowed)。
88
+ * Gene 验证命令只允许 `node …` 形式,不允许非 node 命令(如 pnpm/npm/echo 等)。
89
+ * 安全条件:
90
+ * 1. 必须以 `node ` 开头
91
+ * 2. 不包含命令替换(` 或 $()
92
+ * 3. 不包含 shell 元字符(引号外)
93
+ * 4. 不包含被阻止的 node 标志(-e/--eval/--print 等)
94
+ */
95
+ export function isValidationCommandAllowed(cmd) {
96
+ const c = String(cmd || '').trim();
97
+ if (!c)
98
+ return false;
99
+ // 只允许 node 命令作为验证命令
100
+ if (!c.startsWith('node '))
101
+ return false;
102
+ // 不允许命令替换
103
+ if (/`|\$\(/.test(c))
104
+ return false;
105
+ // 移除引号内的内容后检查 shell 元字符
106
+ const stripped = c.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
107
+ if (SHELL_METACHARS.test(stripped))
108
+ return false;
109
+ // 检查被阻止的 node 标志
110
+ const tokens = c.split(/\s+/);
111
+ const executable = tokens[0] ?? '';
112
+ const args = tokens.slice(1);
113
+ if (nodeFlagViolation(executable, args))
114
+ return false;
115
+ return true;
85
116
  }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@evomap/evolver-core",
3
- "version": "2.0.0-beta.8",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": "^22.13.0 || >=23.4.0"
8
+ },
6
9
  "description": "hub-无关核心: 算法引擎/原材料/mailbox/资产库/workflow",
7
10
  "main": "./dist/index.js",
8
11
  "types": "./dist/index.d.ts",
@@ -23,7 +26,7 @@
23
26
  },
24
27
  "publishConfig": {
25
28
  "access": "public",
26
- "tag": "v2-beta"
29
+ "tag": "latest"
27
30
  },
28
31
  "files": [
29
32
  "dist/",