#631·evolver

[v2] autoexec validation always fails with score 0.2 on cgroup-v2 hosts — sandbox cgroup is derived from the daemon's own (non-distributable) cgroup

Author: Chow36-coderCreated Sep 18, 2026Updated Sep 18, 2026

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.10.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:

json
{
  "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

  1. @evomap/evolver-cli/dist/requiredSandboxValidation.js:23requireIsolation: true is hardcoded:

    javascript
    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.)

  2. @evomap/evolver-core/dist/verify/sandboxedValidation.js:130-133 — when isolation is unavailable, validation is skipped and a hardcoded 0.2 is returned:

    javascript
    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 };
    }
  3. readOnlyIsolationAvailable() is sandboxResourceLimitsAvailable() && readOnlyFilesystemIsolationAvailable().

  4. sandboxResourceLimitsAvailable()createSandboxResourceGroup() (evolver-core/dist/verify/sandboxRunner.js) creates the sandbox cgroup under the process's own cgroup:

    javascript
    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.

  5. 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/[email protected]
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 → [email protected] → 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)

  1. 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.
  2. 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.
  3. 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.