# Slice containment coverage audit

audience: AI coding agents. Scope: every launch path on this workstation that can start
agent work, and whether the resulting tasks land inside a capped, killable slice.

A cgroup is inherited: a child stays in its parent's slice unless something creates a new
cgroup. So only three things can put agent work outside `agent.slice`:

1. a **new** cgroup created without a slice — `systemd-run --scope` does NOT inherit the
   caller's slice, it lands in `app.slice`;
2. a **unit** that declares no `Slice=`;
3. a **root ancestor** that was already outside the slice when it started.

Everything else is placement inherited from the parent and needs no fix.

## Verdicts

| id | path | verdict |
|---|---|---|
| A1 | `modules/harness/v2/child.js` `childSlice()` | **FIXED** — returned `undefined` unless `HARNESS_CHILD_SLICE` was set (only `run-remote.mjs` sets it), so every local runplan dispatch created a scope in `app.slice`. Now defaults to `agent.slice`. |
| A2 | `modules/harness/src/daemon.js` `buildScopedSpawnSpec` | **FIXED** — transient scope carried no `--slice`; now `--slice=agent.slice`. |
| A3 | `confine.sh` tier-2 `rlimit_floor` | **By design** — degraded fallback when `systemd-run` is unavailable. It creates no cgroup, so it inherits the caller's slice; it loses the per-scope caps, not the slice. |
| A4 | `cpu-guard.sh` DEFAULT-PASS (`:269`) and the other `exec "$@"` branches | **By design, not an escape** — each `exec` replaces the process in place, creating no cgroup. `AGENT_BUILD_SCOPE_ACTIVE` (`:255`) and `CPU_GUARD_ACTIVE` (`:41`) mean an ancestor scope already contains it; the build.slice check (`:46`) means it is already capped. The BLACKLIST default-pass declines to *re-scope* non-heavy work; that work stays in whatever slice its parent occupies. |
| A5 | `SHIM_REENTRY=1` → `exec "$real"` | **By design** — `shim_guard_enter` sets it only when an ancestor already applied the shim for that command name, i.e. confinement already happened upstream. Re-applying would nest a scope inside a scope, which systemd refuses. |
| A6 | `git-hook-confine.sh` fail-open | **Pre-existing, left** — a git hook that cannot be confined still has to run or the commit breaks. Its own 16G `MemoryMax`/`MemoryHigh` was removed (see #45). |
| A7 | systemd `--user` units with no `Slice=` | **Partly fixed, rest decided.** `golive-multideal.service` and `golive-zync.service` run 45-minute build/deploy jobs — now `Slice=build.slice`, `TasksMax=512`. The other 22 units are guards, notifiers, sweepers and small daemons (`agent-guard`, `pids-guard`, `agent-reaper`, `notif-recorder`, `headless-guard`, `claude-transcript-backup`, `buildbox-parity`, `buildbox-watch`, `cliproxyapi`, `tmpjail-gc`, `agent-session-sweep`, …). None launches a build or an agent session locally. They stay out of `agent.slice` deliberately: a guard that lives in the slice it polices dies with the cgroup it was supposed to kill. `app.slice` is outside `KILLABLE_SLICES`, which is the property they need. |
| A8 | `v2/bin/ensure-control-api.sh` transient `v2-control-api` unit | **By design** — a shared observability daemon, long-lived and explicitly required to survive a run's cleanup. Same reasoning as A7's guards: it must not be inside the killable set. |
| A9 | `bin/runplan:50` `exec node runplan.js` | **Not an escape** — `exec`, no new cgroup; inherits the invoking agent's slice. |
| B1 | `systemctl --user show-environment` PATH lacks `~/.claude/bin` | **Not the containment mechanism.** `~/.config/environment.d/10-claude-bin.conf` exists but the running manager predates it, so `ExecStart=` payloads resolve unshimmed binaries. Relying on a PATH shim to place a unit's work was always the weaker mechanism; the unit's own `Slice=` is authoritative and is what A7 uses. No fix attempted here — restarting the user manager to reload `environment.d` would tear down every live agent session. |
| B2 | absolute-path invocation (`/home/user/.local/bin/claude`) | **By design, and the correct behavior.** It bypasses the shim, so the process stays where its launcher was. All five measured escapees were owner-launched interactive sessions, which belong in `human.slice` and must never be killable. Agent-launched children no longer depend on PATH at all: A1/A2 name the slice explicitly. |
| — | `_tmpjail-shim.sh:153-160` byte-identical duplicate of `:144-151` | **Pre-existing dead code, left** — not created by this change; reported, not deleted. |

## Not covered

- A process the owner starts by hand outside any shim is deliberately uncontained beyond
  `human.slice`. That is the point of `human.slice`.
- Remote work on a buildbox is placed by that host's own slice layout; this audit covers
  the workstation.
