@evomap/evolver-core 2.0.0-beta.14 → 2.0.0-beta.16

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.
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evomap/evolver-core",
3
- "version": "2.0.0-beta.14",
3
+ "version": "2.0.0-beta.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "hub-无关核心: 算法引擎/原材料/mailbox/资产库/workflow",