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