@evomap/evolver-core 2.0.0-beta.9 → 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.
- package/dist/algo/candidateAssembly.d.ts +7 -2
- package/dist/algo/candidateAssembly.js +81 -13
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +23 -2
- package/dist/algo/cycleEngine.js +236 -36
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +2 -2
- package/dist/algo/geneHealth.js +5 -4
- package/dist/algo/geneSelection.d.ts +63 -5
- package/dist/algo/geneSelection.js +330 -30
- package/dist/algo/index.d.ts +2 -0
- package/dist/algo/index.js +2 -0
- package/dist/algo/orchestrator.d.ts +8 -1
- package/dist/algo/orchestrator.js +15 -3
- package/dist/algo/publishEligibility.d.ts +34 -0
- package/dist/algo/publishEligibility.js +52 -0
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
- package/dist/assetstore/assetSidecarRecords.js +238 -0
- package/dist/assetstore/assetSidecarRecovery.js +2 -2
- package/dist/assetstore/assetStoreHealth.d.ts +6 -0
- package/dist/assetstore/assetStoreHealth.js +56 -14
- package/dist/assetstore/assetStoreStorage.d.ts +1 -1
- package/dist/assetstore/assetStoreStorage.js +27 -9
- package/dist/assetstore/assetSyncLedger.d.ts +86 -0
- package/dist/assetstore/assetSyncLedger.js +683 -4
- package/dist/assetstore/index.d.ts +1 -0
- package/dist/assetstore/index.js +1 -0
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +4 -2
- package/dist/assetstore/localJsonl.js +89 -9
- package/dist/assetstore/provenance.d.ts +80 -4
- package/dist/assetstore/provenance.js +313 -5
- package/dist/assetstore/provider.d.ts +58 -1
- package/dist/assetstore/provider.js +97 -6
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
- package/dist/bootstrap/v1EnvCompat.js +280 -0
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +1 -0
- package/dist/events/ingest.js +1 -0
- package/dist/events/paths.d.ts +3 -1
- package/dist/events/paths.js +4 -0
- package/dist/events/public.d.ts +2 -2
- package/dist/events/public.js +2 -2
- package/dist/events/reports.d.ts +2 -0
- package/dist/events/reports.js +4 -0
- package/dist/exec/autoExec.d.ts +43 -2
- package/dist/exec/autoExec.js +76 -9
- package/dist/exec/autonomousCycle.d.ts +22 -4
- package/dist/exec/autonomousCycle.js +64 -13
- package/dist/exec/claudeBridge.d.ts +32 -7
- package/dist/exec/claudeBridge.js +281 -29
- package/dist/exec/prompt.js +5 -1
- package/dist/exec/runnerRegistry.d.ts +68 -26
- package/dist/exec/runnerRegistry.js +307 -72
- package/dist/exec/selfPr.js +1 -7
- package/dist/feedback/envelope.d.ts +61 -0
- package/dist/feedback/envelope.js +168 -0
- package/dist/feedback/index.d.ts +1 -0
- package/dist/feedback/index.js +1 -0
- package/dist/hooks/hooks.js +1 -0
- package/dist/hub/assetCallLog.d.ts +35 -1
- package/dist/hub/assetCallLog.js +124 -1
- package/dist/hub/bindings.d.ts +8 -1
- package/dist/hub/bindings.js +17 -6
- package/dist/hub/capability.d.ts +90 -3
- package/dist/hub/fake.d.ts +2 -2
- package/dist/hub/fake.js +1 -1
- package/dist/hub/questionGenerator.d.ts +5 -1
- package/dist/hub/questionGenerator.js +8 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -1
- package/dist/mailbox/dispatch.d.ts +1 -1
- package/dist/mailbox/dispatch.js +22 -6
- package/dist/mailbox/envelope.d.ts +7 -1
- package/dist/mailbox/envelope.js +9 -2
- package/dist/mailbox/ipcServer.d.ts +10 -2
- package/dist/mailbox/ipcServer.js +163 -13
- package/dist/mailbox/store.d.ts +89 -3
- package/dist/mailbox/store.js +895 -41
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/personality/schema.d.ts +16 -16
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/signal.d.ts +3 -3
- package/dist/schema/signal.js +1 -1
- package/dist/signals/curriculum.d.ts +55 -0
- package/dist/signals/curriculum.js +202 -0
- package/dist/signals/cycleHistoryFromEvents.js +17 -8
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +169 -1
- package/dist/signals/extractor.d.ts +2 -2
- package/dist/signals/extractor.js +31 -6
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/metaSignals.d.ts +4 -0
- package/dist/signals/metaSignals.js +42 -0
- package/dist/signals/signalGate.js +1 -1
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/strategy/constraintAblation.js +115 -369
- package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
- package/dist/strategy/constraintAblationPredicates.js +339 -0
- package/dist/trace/index.d.ts +3 -1
- package/dist/trace/index.js +3 -1
- package/dist/trace/learningTrace.d.ts +209 -0
- package/dist/trace/learningTrace.js +276 -0
- package/dist/trace/proxyTurns.d.ts +31 -0
- package/dist/trace/proxyTurns.js +137 -0
- package/dist/verify/sandboxRunner.d.ts +28 -0
- package/dist/verify/sandboxRunner.js +218 -19
- package/dist/verify/sandboxedValidation.d.ts +9 -0
- package/dist/verify/sandboxedValidation.js +113 -13
- package/dist/verify/validation.d.ts +11 -1
- package/dist/verify/validation.js +31 -0
- 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
|
|
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 (
|
|
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
|
-
? {
|
|
35
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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, {
|
|
102
|
-
|
|
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 {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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": "
|
|
29
|
+
"tag": "latest"
|
|
27
30
|
},
|
|
28
31
|
"files": [
|
|
29
32
|
"dist/",
|