Summary
On a standard cgroup-v2 + systemd Linux host, evolver autoexec can never pass validation: every executed cycle ends as status: failed / resolutionStatus: regressed with score: 0.1–0.2, even when the produced diff is correct (proofOfWork.git_diff.files > 0).
The cause is in the validation sandbox, not in the runner or the model.
Environment
- evolver
2.0.37 (npm global, evolver autoexec --solo, runner: "llm")
- Ubuntu 24.04 under WSL2, kernel
6.6.87.2-microsoft-standard-WSL2
- systemd
255 (255.4-1ubuntu8.17), cgroup v2 unified (/sys/fs/cgroup/cgroup.controllers present)
- Node
v24.20.0
- Daemon runs as a systemd user service with
Delegate=yes (recommended practice)
Symptom
Every smoke/real cycle:
{
"taskId": "smoke-valcmd-v6-20260917",
"status": "failed",
"finalStage": "failed",
"reason": "失败 score=0.2 → regressed",
"outcome": { "status": "failed", "score": 0.2 },
"proofOfWork": { "kind": "git_diff", "git_diff": { "files": 1, "lines": 1 } }
}
The diff was applied correctly — validation simply never ran.
Root cause chain
-
@evomap/evolver-cli/dist/requiredSandboxValidation.js:23 — requireIsolation: true is hardcoded:
const result = await runner(safeCommands, cwd, { ...options, requireIsolation: true });
(Same hardcoded value also in evolver-cli/dist/recipe.js:505, evolver-cli/dist/modelCompatibility.js:232, evolver-proxy/dist/bin/evolver-proxy.js:721. We grepped the whole installed tree — there is no env var or config key that turns this off.)
-
@evomap/evolver-core/dist/verify/sandboxedValidation.js:130-133 — when isolation is unavailable, validation is skipped and a hardcoded 0.2 is returned:
const isolationCheck = opts.unshareCheck ?? (opts.requireIsolation ? readOnlyIsolationAvailable : unshareNetAvailable);
const isolated = isolationCheck();
if (opts.requireIsolation && !isolated) {
return { passed: false, ..., score: 0.2, results: [], skipped: [], isolated: false };
}
-
readOnlyIsolationAvailable() is sandboxResourceLimitsAvailable() && readOnlyFilesystemIsolationAvailable().
-
sandboxResourceLimitsAvailable() → createSandboxResourceGroup() (evolver-core/dist/verify/sandboxRunner.js) creates the sandbox cgroup under the process's own cgroup:
const parent = currentCgroupPath(); // = readFileSync('/proc/self/cgroup')
path = join(parent, `evolver-validation-${process.pid}-${Date.now()}-...`);
mkdirSync(path, { mode: 0o700 });
if (!configureSandboxResourceGroup(path)) throw new Error('required cgroup v2 controllers are not delegated');
configureSandboxResourceGroup() requires all seven files to exist: cgroup.procs, cgroup.kill, memory.max, memory.swap.max, memory.oom.group, pids.max, cpu.max. One missing → null → isolation unavailable → hardcoded 0.2.
-
So the sandbox requires cpu/memory/pids to be listed in cgroup.subtree_control of the daemon's own cgroup.
Why that is structurally impossible
The kernel enforces the "No Internal Process Constraint": a non-root cgroup can distribute domain controllers to children only when it has no processes of its own. The daemon process necessarily lives in the cgroup that currentCgroupPath() returns — so that cgroup can never carry those controllers.
Measured on the host above:
# daemon's own cgroup
cgroup.type : domain
procs : 1
subtree_control : '' <-- empty, so the sandbox dir gets no memory.max etc.
# enable controllers while the cgroup is empty (possible)
$ echo "+cpu +memory +pids" > .../cgroup.subtree_control
$ cat .../cgroup.subtree_control
cpu memory pids
$ ls .../probe-child # every required file appears
memory.max memory.swap.max memory.oom.group pids.max cpu.max
# ... but now nothing can live there:
$ echo <pid> > .../cgroup.procs
write error: Device or resource busy <-- both for the original process and for a fresh one
Same result through systemd when the main process is started after delegation is prepared:
systemd: Failed to attach to cgroup /user.slice/.../evolver-cg-probe.service: Device or resource busy
systemd: Main process exited, code=exited, status=219/CGROUP
Host-wide scan — the root cgroup is the only one that has both (it is exempt from the constraint):
procs=221 'cpu memory pids' (root)
procs=0 'cpu memory pids' /user.slice/user-1000.slice/user@1000.service
procs=0 'cpu memory pids' /user.slice/.../app.slice
procs=0 'memory pids' /system.slice
Delegation was verified to work up to the unit level (Delegate=yes, DelegateControllers=cpu cpuset io memory pids, and the chain user.slice → user-1000.slice → user@1000.service → app.slice all distribute cpu memory pids). We also tried DelegateSubgroup=payload (systemd 254+), which leaves the unit cgroup process-free — the controllers enable fine there, but then currentCgroupPath() points at the subgroup, which holds the daemon again: same dead end.
Impact
On any modern Linux with systemd + cgroup v2, evolver autoexec executes nothing beyond "prompt → diff", because no host setup can satisfy step 4. The only configuration that could work is a process running directly in the real root cgroup (exempt), which normally means privileged/root execution outside a delegating manager.
Suggested fixes (any one would unblock this)
- Configurable sandbox parent: allow the sandbox cgroup root to be specified (env/config), and/or allocate it as a sibling under the parent of
currentCgroupPath() — a cgroup that the host manager can keep process-free and delegated.
- Degrade gracefully: don't hard-fail with the sentinel
0.2 when only some controllers/limits are available. Better: run validation anyway (with the limits that exist, or with unshare-only isolation) and report isolated: false in the result instead of scoring it as a regression. A documented escape hatch (e.g. EVOLVER_REQUIRE_ISOLATION=0) would also help — currently none exists.
- Document the requirement: if sandboxed validation genuinely requires the root cgroup, that should be stated explicitly, because no standard systemd-managed deployment can satisfy it.
Happy to re-run any probe or provide full logs — just say which artifact you want.
Summary
On a standard cgroup-v2 + systemd Linux host,
evolver autoexeccan never pass validation: every executed cycle ends asstatus: failed/resolutionStatus: regressedwithscore: 0.1–0.2, even when the produced diff is correct (proofOfWork.git_diff.files > 0).The cause is in the validation sandbox, not in the runner or the model.
Environment
2.0.37(npm global,evolver autoexec --solo,runner: "llm")6.6.87.2-microsoft-standard-WSL2255 (255.4-1ubuntu8.17), cgroup v2 unified (/sys/fs/cgroup/cgroup.controllerspresent)v24.20.0Delegate=yes(recommended practice)Symptom
Every smoke/real cycle:
{ "taskId": "smoke-valcmd-v6-20260917", "status": "failed", "finalStage": "failed", "reason": "失败 score=0.2 → regressed", "outcome": { "status": "failed", "score": 0.2 }, "proofOfWork": { "kind": "git_diff", "git_diff": { "files": 1, "lines": 1 } } }The diff was applied correctly — validation simply never ran.
Root cause chain
@evomap/evolver-cli/dist/requiredSandboxValidation.js:23—requireIsolation: trueis hardcoded:(Same hardcoded value also in
evolver-cli/dist/recipe.js:505,evolver-cli/dist/modelCompatibility.js:232,evolver-proxy/dist/bin/evolver-proxy.js:721. We grepped the whole installed tree — there is no env var or config key that turns this off.)@evomap/evolver-core/dist/verify/sandboxedValidation.js:130-133— when isolation is unavailable, validation is skipped and a hardcoded0.2is returned:readOnlyIsolationAvailable()issandboxResourceLimitsAvailable() && readOnlyFilesystemIsolationAvailable().sandboxResourceLimitsAvailable()→createSandboxResourceGroup()(evolver-core/dist/verify/sandboxRunner.js) creates the sandbox cgroup under the process's own cgroup:configureSandboxResourceGroup()requires all seven files to exist:cgroup.procs,cgroup.kill,memory.max,memory.swap.max,memory.oom.group,pids.max,cpu.max. One missing →null→ isolation unavailable → hardcoded0.2.So the sandbox requires
cpu/memory/pidsto be listed incgroup.subtree_controlof the daemon's own cgroup.Why that is structurally impossible
The kernel enforces the "No Internal Process Constraint": a non-root cgroup can distribute domain controllers to children only when it has no processes of its own. The daemon process necessarily lives in the cgroup that
currentCgroupPath()returns — so that cgroup can never carry those controllers.Measured on the host above:
Same result through systemd when the main process is started after delegation is prepared:
Host-wide scan — the root cgroup is the only one that has both (it is exempt from the constraint):
Delegation was verified to work up to the unit level (
Delegate=yes,DelegateControllers=cpu cpuset io memory pids, and the chainuser.slice → user-1000.slice → user@1000.service → app.sliceall distributecpu memory pids). We also triedDelegateSubgroup=payload(systemd 254+), which leaves the unit cgroup process-free — the controllers enable fine there, but thencurrentCgroupPath()points at the subgroup, which holds the daemon again: same dead end.Impact
On any modern Linux with systemd + cgroup v2,
evolver autoexecexecutes nothing beyond "prompt → diff", because no host setup can satisfy step 4. The only configuration that could work is a process running directly in the real root cgroup (exempt), which normally means privileged/root execution outside a delegating manager.Suggested fixes (any one would unblock this)
currentCgroupPath()— a cgroup that the host manager can keep process-free and delegated.0.2when only some controllers/limits are available. Better: run validation anyway (with the limits that exist, or withunshare-only isolation) and reportisolated: falsein the result instead of scoring it as a regression. A documented escape hatch (e.g.EVOLVER_REQUIRE_ISOLATION=0) would also help — currently none exists.Happy to re-run any probe or provide full logs — just say which artifact you want.