carlos / platform Public

plan: zdd flag readiness round 3 (draft)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 1a8a80372291167848b856f0656e1916862ab335 parent 34a4328
1 file changed, +725 −0
  • docs/superpowers/plans/2026-10-13-zero-downtime-flag-readiness-3.md +725 −0
diff --git a/docs/superpowers/plans/2026-10-13-zero-downtime-flag-readiness-3.md b/docs/superpowers/plans/2026-10-13-zero-downtime-flag-readiness-3.md
new file mode 100644
index 000000000..da55125c4
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-13-zero-downtime-flag-readiness-3.md
@@ -0,0 +1,725 @@
+# Zero-downtime flag readiness, round 3: placement under the hardened edge unit
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make armed generation starts work on the shipped, hardened `carlos-edge.service` by replacing clone3 `CLONE_INTO_CGROUP` placement with an exec-shim, make arming prove placement before it arms, and fix the three smaller defects the second dev-box check found.
+
+**Architecture:** Every `cgroup.Manager` start (flat, generation, worker, legacy sidecar, seed) starts the carlos binary itself as `/proc/self/exe __cgexec`. The shim receives the tenant's exec triple over a pipe, writes its own PID into the target cgroup's `cgroup.procs` through an already-validated directory fd, checks `/proc/self/cgroup` and the directory's identity, then `execve`s the tenant. The parent learns the outcome over a close-on-exec status pipe before `Start` returns, so a shim that exits before exec is a placement failure (`cgroup.ErrLimitNotApplied` for generations) and the tenant never runs outside its cgroup. Arming does one real shim placement into a scratch cgroup and declines with a reason code if it fails.
+
+**Tech Stack:** Go 1.26, `golang.org/x/sys/unix`, cgroup v2, systemd 257 (transient user units for kernel tests).
+
+**Spec:** /home/paulca/amadan.net/carlos/platform-design-zero-downtime-deploys/docs/superpowers/specs/2026-10-07-zero-downtime-deploys-design.md (rev 17), with the approved rev-18 amendment on main: `docs/superpowers/specs/2026-10-12-zero-downtime-deploys-rev18-amendment.md`. §7.3 (containment) is the section this plan serves; the spec does not name a placement mechanism, so no spec text changes.
+
+**Inputs:** amadan discussion carlos/platform #272, latest reply ("Dev-box checks, round 2", 2026-10-13); command log `/home/paulca/.cache/tmp/claude-1001/zdd-dev-checks2.log`; roll notes `docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md`.
+
+**Base:** branch `zdd-flag-readiness-3` from origin/main `34a4328f`.
+
+## Global Constraints
+
+- Paul's ruling (2026-10-13): do NOT relax `RestrictNamespaces=yes` or any other line of `internal/host/units/carlos-edge.service`. That file is not edited by this plan.
+- A generation's tenant code never runs outside its cgroup: no PID-move, no start-then-move, no retry outside the cgroup on the required (generation) path.
+- The shim never runs tenant code, never reads tenant-controlled environment before placement, and passes argv, env, cwd, fds 0..2+len(ExtraFiles), and the `SysProcAttr` the caller set to the tenant exactly as a direct `exec.Cmd.Start` would.
+- The PID `cmd.Process.Pid` the activator tracks is the shim's PID, which is the tenant's after `execve`; `cmd.Wait` reports the tenant's exit.
+- The strict path (`PrepareStrict`, `PrepareStrictParent`, `(*Prepared).Start`, `internal/cgroup/strict*.go`) keeps its clone3 placement unchanged.
+- No new member-facing strings unless unavoidable; any that are added carry draft text and a `TODO-COPY` marker and go into `copy-review/strings.json`, and the controller stops and reports that path.
+- Commits use the identity `claude <claude~paul@keymail.dev>`: `git -c user.name=claude -c user.email=claude~paul@keymail.dev commit ...`. Never push; the controller pushes.
+- Sandboxed Go: `GOCACHE=$TMPDIR/gocache` on every go command. Kernel and socket tests run unsandboxed with `TMPDIR=/tmp/k`. Never set GOCACHE/GOMODCACHE/GOTMPDIR under `/tmp`.
+- Kernel tests on DOCS-T1 run in a transient delegated user unit, for example:
+ `systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k -E CARLOS_SHIM_KERNEL=1 <test binary> -test.run '<regex>' -test.v -test.count=1`
+ (build the binary first with `go test -c -o $TMPDIR/cgroup.test ./internal/cgroup`, and copy it to `/tmp/k` if a unit's sandbox hides `$TMPDIR`). User delegation on DOCS-T1 offers `cpu memory pids`, not `io`.
+- Do not run anything on real boxes. Never run `git worktree prune`. Never push to main or run `amadan branch merge`.
+- Before committing: `gofmt -l` clean on touched files, `go vet` on touched packages, and `GOOS=darwin go build ./... && GOOS=windows go build ./...` when a touched file is not linux-only (linux-only code needs `//go:build linux` and a `!linux` stub).
+- Heavy test loops run under `nice -n 19`.
+- Integrity: reports paste real command output only.
+
+## Rulings recorded by this plan
+
+Each is ledgered by the controller as `Ruling: <what> — <why> — <cost if wrong>`.
+
+1. **(Paul, 2026-10-13)** The exec-shim replaces clone3 placement; no hardening on carlos-edge is relaxed. — RestrictNamespaces makes systemd's seccomp filter answer clone3 with ENOSYS, so CLONE_INTO_CGROUP is impossible in the agent. — Cost if wrong: one extra exec of the carlos binary per spawn.
+2. Flat starts (the non-required path, flag off included) use the shim too. On a shim *placement* failure, a flat start falls back to today's start-then-move by PID; a generation start never does. — The flat path's contract is that accounting never breaks a spawn, and every flat start under the shipped unit already uses start-then-move today. — Cost if wrong: a flag-off tenant can still run for a moment outside its cgroup when the shim fails, as every flag-off start does today.
+3. The clone3 attempt and its latch (`Manager.fallback`) leave the Manager start paths. The strict claims path keeps clone3. — Paul: keep the strict-claims path's existing placement. — Cost if wrong: with host claims on under the shipped unit, strict launches answer `ErrStrictUnsupported` (refused, never uncapped) until that path gets the shim. Claims are off everywhere and claims-on boxes decline arming; the roll notes record it as a claims-on-swaps precondition.
+4. The shim is `/proc/self/exe __cgexec`, used only when `cgroup.RunShimIfRequested()` has run in this process. — `/proc/self/exe` is the running build even after a roll replaces the file on disk; the hook guard stops a test binary without the hook from re-running its own tests as a "shim". — Cost if wrong: a binary that forgets the hook declines arming and its flat starts use start-then-move: loud, never unsafe.
+5. The tenant's path, argv and env travel over a pipe in a NUL-delimited encoding, never in the shim's argv or environment, and the shim runs with an empty environment. — Secrets stay out of `/proc/<pid>/cmdline`; no tenant variable (`GODEBUG`, `LD_*`, `GOMAXPROCS`) shapes the shim before placement; JSON would rewrite invalid UTF-8. — Cost if wrong: one pipe per spawn.
+6. A placement in flight counts as a member of its target cgroup and every ancestor up to the host directory: `Cleanup.Populated` answers true, `Cleanup.Members` answers `ErrPlacementInFlight`, `Cleanup.KillAndEmpty` waits for it to resolve (bounded by its ctx), and the legacy-parent guard counts one aimed at the host directory itself. — With clone3 the child appeared in its cgroup atomically; the shim spends a few milliseconds in the agent's cgroup first, and an emptiness proof must not miss it. — Cost if wrong: a stop that lands during a spawn's few milliseconds waits for it or answers unproven (the lease is kept), never a false empty.
+7. A new decline code `cgroup-placement-failed` reuses the approved declined phrase (`unavailableDeclined`), so it adds no copy. — Same meaning as the other declined codes for a member. — Cost if wrong: a console older than the box shows the generic unknown-code phrase for it until consoles roll (they roll after boxes).
+8. The arm-time placement check is box-wide: a failure declines the whole box, nested rows included. — One flag, one decision, as for `host-claims-on`. — Cost if wrong: a box whose nested rows could swap declines until flat placement works.
+9. The shim's status deadline is 10 s (`shimDeadline`). On expiry the shim is SIGKILLed and reaped, and the start is a placement failure. — A wake's budget is 30 s; a Go binary that cannot start in 10 s is a box condition. — Cost if wrong: a box that loaded refuses generation starts.
+10. `reset-failed` after a generation stop is best-effort: a failure is logged and the stop still answers 204. — The stop itself succeeded; the failed-list entry is cosmetic. — Cost if wrong: a failed generation unit can linger in `systemctl --failed`.
+11. The roll notes' probe app moves into the module as `cmd/zddprobe`, with a test that launches it with the platform's argv. — A heredoc cannot be tested. — Cost if wrong: one more `cmd/` directory.
+12. The 3000-seed simulation sweep runs only if the branch diff touches `internal/rollout` or the simulation's files; the shim, the arming check and the stop path are outside the modelled scheduler. — Cost if wrong: an unswept scheduler change; the final review checks the diff's paths.
+
+## Review Focus
+
+1. **Exact pass-through.** A tenant env with invalid UTF-8, an empty string, `=` inside a value, a 1 MiB value, and an argv with an empty element, a newline and spaces reach the tenant byte-for-byte, the same as a direct start (Task 1, `TestShimKernelPassesArgvEnvExactly`).
+2. **Inherited descriptors.** A `build()` that sets `ExtraFiles` (two pipes) gets them at fds 3 and 4 in the tenant, and the tenant has no other fd above 2: none of the shim's spec, directory or status fds, and none the agent opened without close-on-exec (Task 1, `TestShimKernelFDsExact`).
+3. **The agent's binary replaced on disk.** After a roll replaces `/opt/carlos/carlos`, the running agent's shim still execs (Task 1, `TestShimKernelSurvivesDeletedExecutable`).
+4. **A stop during a spawn.** `KillAndEmpty` or `Populated` on a host whose generation is being placed never reports empty while the shim is in flight (Task 2, `TestCleanupCountsPlacementInFlight`).
+5. **A missing tenant binary.** The start returns the command's own `fork/exec ... no such file or directory` error, not `ErrLimitNotApplied`, nothing ran, the shim is reaped, and an empty generation directory is removed (Task 1 and Task 2, `TestShimExecFailureIsTheCommands`, `TestGenerationMissingBinaryIsNotABoxCondition`).
+
+---
+
+## File structure
+
+| File | Task | Responsibility |
+|---|---|---|
+| `internal/cgroup/shim.go` | 1 | Verbs, the hook (`RunShimIfRequested`, `shimAvailable`), the spec encoding, `placementError`, `ErrPlacementInFlight`. All platforms. |
+| `internal/cgroup/shim_linux.go` | 1 | `runShim`, `runProbe`, `startViaShim` (parent side). |
+| `internal/cgroup/shim_other.go` | 1 | `!linux` stubs: the shim verbs exit 127, `startViaShim` answers a placement error. |
+| `internal/cgroup/main_test.go` | 1 | `TestMain` calling `RunShimIfRequested()`. |
+| `internal/cgroup/shim_test.go` | 1 | Encoding round-trips (all platforms). |
+| `internal/cgroup/shim_linux_test.go` | 1 | Ungated parent/shim protocol tests that need no delegation. |
+| `internal/cgroup/shim_kernel_linux_test.go` | 1 | Gated (`CARLOS_SHIM_KERNEL=1`) real-cgroup shim tests. |
+| `main.go` | 1 | `cgroup.RunShimIfRequested()` as the first statement of `main()`. |
+| `internal/cgroup/inflight.go` | 1 | The in-flight placement registry on `Manager`. |
+| `internal/cgroup/cgroup.go`, `cgroup_linux.go`, `cgroup_other.go`, `gen.go`, `cleanup_linux.go` | 2 | `startIn` on the shim; latch removed; Cleanup and the parent guard count placements in flight. |
+| `internal/cgroup/verify_linux.go`, `verify_other.go` | 3 | `Manager.VerifyPlacement`. |
+| `internal/host/rollout.go`, `internal/rolloutwords/words.go`, `internal/status/snapshot.go` | 3 | The decline code and its wording. |
+| `internal/cgroup/edge_hardening_kernel_linux_test.go` | 4 | The transient-unit test under the edge unit's sandbox lines. |
+| `cmd/zddprobe/main.go`, `cmd/zddprobe/main_test.go` | 5 | The roll notes' probe app, tested with the platform's argv. |
+| `internal/cgroup/strict_kernel_linux_test.go` | 6 | `logf` on the test's second managers. |
+| `internal/instancectl/gen.go` (+ its test) | 6 | `reset-failed` after a generation stop. |
+| `docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md` | 5, 7 | The probe section; the round-3 notes and the dev re-check list. |
+
+---
+
+### Task 1: The exec-shim (opus)
+
+**Files:**
+- Create: `internal/cgroup/shim.go`, `internal/cgroup/shim_linux.go`, `internal/cgroup/shim_other.go`, `internal/cgroup/inflight.go`, `internal/cgroup/main_test.go`, `internal/cgroup/shim_test.go`, `internal/cgroup/shim_linux_test.go`, `internal/cgroup/inflight_linux_test.go`, `internal/cgroup/shim_kernel_linux_test.go`
+- Modify: `main.go:20` (first statement of `main()`); `internal/cgroup/cgroup.go:173-184` (the registry's fields on `Manager`)
+
+**Interfaces:**
+- Produces (used by Tasks 2 to 4):
+ - `const ShimVerb = "__cgexec"`, `const ProbeVerb = "__cgprobe"`
+ - `func RunShimIfRequested()` — first statement of `main()` and of `TestMain`. Marks the process as able to serve the shim; if `os.Args[1]` is `ShimVerb` or `ProbeVerb` it runs that and calls `os.Exit`, never returning.
+ - `func shimAvailable() bool` — the hook ran in this process and the platform is linux.
+ - `type placementError struct{ err error }` with `Error()` and `Unwrap()`; `func isPlacementError(err error) bool`.
+ - `var ErrPlacementInFlight = errors.New("cgroup: a process is being placed in this cgroup")` (defined here, used by Task 2).
+ - `func (m *Manager) startViaShim(dir string, cmd *exec.Cmd, before func() error) (refused bool, err error)` — linux. On nil error the tenant is running in `dir` and `cmd.Process` is it. `refused` means `before` said no and nothing started. Any other error: nothing ran and any shim process is reaped; a `*placementError` means the process could not be placed, anything else is the command's own error (as `exec.Cmd.Start` would have returned it). It registers the placement in flight itself, after `before` agrees.
+ - The in-flight registry: `beginPlacement`, `placementsUnder`, `placementsAt`, `waitPlacementsUnder` (signatures below).
+ - `var shimDeadline = 10 * time.Second`
+ - `var shimExe = "/proc/self/exe"`
+- Consumes: `ParseSelfCgroup` (`internal/cgroup/cgroup.go:155`).
+
+**The protocol.**
+
+Parent, in `startViaShim`:
+1. If `!shimAvailable()`, return `&placementError{errors.New("the placement shim is not available in this process")}`. If `cmd.Err != nil` (a failed `LookPath` in `build()`), return `cmd.Err`.
+2. Open `dir` with `unix.Open(dir, O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC, 0)`. `unix.Fstatfs` it; anything but `unix.CGROUP2_SUPER_MAGIC` is a placement error. Compute `expected`: `dir` must start with `/sys/fs/cgroup/`; `expected` is `dir` with the `/sys/fs/cgroup` prefix cut (so it starts with `/`). Anything else is a placement error.
+3. Capture the tenant before rewriting anything: `path := cmd.Path`; `argv := cmd.Args`, or `[]string{cmd.Path}` when empty (exec.Cmd's own rule); `env := cmd.Environ()` (exactly what `Start` would pass, `PWD` included); `keep := 3 + len(cmd.ExtraFiles)`.
+4. `os.Pipe()` twice: spec (shim reads) and status (shim writes). Rewrite `cmd`: `Path = shimExe`, `Args = []string{"carlos-cgexec", ShimVerb, itoa(keep), itoa(keep+1), itoa(keep+2)}`, `Env = []string{}`, `ExtraFiles = append(original ExtraFiles, specR, dirFile, statusW)`. Leave `Dir`, `Stdin`, `Stdout`, `Stderr`, `SysProcAttr`, `Cancel`, `WaitDelay` untouched.
+5. Ask `before` (when set); a refusal returns `true, before's error` with nothing started. Only then `done := m.beginPlacement(dir)` (the in-flight registry, below), and `startCmd(cmd)` (`internal/cgroup/cgroup.go:332`). The registry is taken after `before` and never across it: a stop that waits on the registry may hold a lock `before` needs. Close the parent's `specR`, `dirFile` and `statusW` whatever happened. A failed `startCmd` (the carlos binary itself did not start) is a placement error; `done()` and return it.
+6. Write the encoded spec to `specW` with a write deadline at `shimDeadline`, then close it. Read `statusR` to EOF with a read deadline at `shimDeadline` (pipes from `os.Pipe` support deadlines).
+7. Decide from what was read: a line `E <errno>` is the command's own failure: `&os.PathError{Op: "fork/exec", Path: path, Err: syscall.Errno(n)}`. A line `P <reason>` is a placement error with that reason. `R` with nothing after it is success. Nothing at all (EOF without `R`) is a placement error ("the shim exited before exec"). A deadline is a placement error ("no answer from the shim within 10s").
+8. On any failure: `cmd.Process.Kill()` then `cmd.Wait()` (ignore its error), so no shim outlives the call, then `done()`. On success `done()` and return `false, nil`: the process is in `dir` by then, so a proof that reads `dir` sees it.
+
+**The in-flight registry** (`inflight.go`, used by Task 2's proofs): a mutex-guarded `map[string]int` of target directories (cleaned paths), plus a `chan struct{}` that is closed and replaced whenever a count drops, so waiters wake.
+
+```go
+func (m *Manager) beginPlacement(dir string) (done func()) // done is idempotent
+func (m *Manager) placementsUnder(dir string) int // targets equal to dir or below dir+"/"
+func (m *Manager) placementsAt(dir string) int // targets equal to dir
+func (m *Manager) waitPlacementsUnder(ctx context.Context, dir string) error // nil at zero, ctx.Err() if ctx ends first
+```
+
+A nil `*Manager` answers zero and nil, and its `beginPlacement` returns a no-op.
+
+Shim, in `runShim(args []string) int` (args are the three fd numbers):
+1. Parse the three fds; they must be `keep`, `keep+1`, `keep+2` for some `keep >= 3`. Set `FD_CLOEXEC` on the status fd first, so every later path closes it at exec.
+2. Read the spec fd to EOF, at most 16 MiB, and close it. Decode it.
+3. Mark every fd `>= keep` close-on-exec: `unix.CloseRange(uint(keep), math.MaxUint32, unix.CLOSE_RANGE_CLOEXEC)`; if that answers `ENOSYS`, `EPERM` or `EINVAL`, walk `/proc/self/fd` and set `FD_CLOEXEC` on each fd `>= keep`. This covers the spec, directory and status fds and anything the agent leaked without close-on-exec.
+4. `unix.Fstatfs(dirFD)` must be `CGROUP2_SUPER_MAGIC`.
+5. `unix.Openat(dirFD, "cgroup.procs", O_WRONLY|O_NOFOLLOW|O_CLOEXEC, 0)`; write `strconv.Itoa(os.Getpid())`; close. This moves the whole process (every Go runtime thread) while it is still carlos code.
+6. Read `/proc/self/cgroup`; `ParseSelfCgroup`; `"/"+rel` must equal the spec's `Expected`.
+7. `unix.Fstat(dirFD)` and `unix.Lstat("/sys/fs/cgroup"+Expected)` must agree on `Dev` and `Ino`, so the cgroup the kernel reports is the directory the parent validated.
+8. Any failure in 1 to 7: write `P <step>: <error>\n` to the status fd and return 125. Tenant code has not run.
+9. Write `R\n`, then `syscall.Exec(spec.Path, spec.Argv, spec.Env)`. `syscall.Exec` restores the original `RLIMIT_NOFILE` (Go source `syscall/exec_unix.go:286`). If it returns, write `E <errno>\n` and return 126.
+
+The shim writes nothing to fds 0 to 2.
+
+`runProbe() int` (for Task 3): read `/proc/self/cgroup`, print `"/"+rel` and a newline to stdout, return 0; return 1 when there is no v2 line.
+
+**Spec encoding** (`shim.go`): fields each terminated by `\x00`, in order: the magic `carlos-cgexec-1`, `Path`, `Expected`, `KeepFDs` (decimal), `TestHold` (`0` or `1`), `len(Argv)` (decimal), each argv element, `len(Env)` (decimal), each env entry. Encoding refuses any string that contains `\x00` (execve could not carry it anyway). Decoding refuses a wrong magic, a short or trailing buffer, or a count above 1<<20.
+
+```go
+type shimSpec struct {
+ Path string
+ Argv []string
+ Env []string
+ Expected string // "/..." as /proc/self/cgroup shows it after "0::"
+ KeepFDs int // fds 0..KeepFDs-1 are the tenant's
+ TestHold bool // test binaries only: the shim waits for SIGUSR1 before step 4
+}
+
+func encodeShimSpec(s shimSpec) ([]byte, error)
+func decodeShimSpec(b []byte) (shimSpec, error)
+```
+
+`TestHold` is honoured only when `testing.Testing()` is true in the shim process (the package already imports `testing`, `cgroup.go:30`), so in the carlos binary it is inert. When honoured, the shim sleeps before step 4 until it is signalled, with default signal dispositions (no `signal.Notify`): a test's SIGTERM or SIGKILL then kills it before placement, which is the case under test. The parent sets the field from `var shimTestHold bool`, which only test files flip.
+
+- [ ] **Step 1: Write the failing tests**
+
+`internal/cgroup/main_test.go`:
+
+```go
+package cgroup
+
+import (
+ "os"
+ "testing"
+)
+
+func TestMain(m *testing.M) {
+ RunShimIfRequested()
+ os.Exit(m.Run())
+}
+```
+
+`internal/cgroup/shim_test.go` (all platforms):
+
+```go
+func TestShimSpecRoundTripsExactBytes(t *testing.T) {
+ in := shimSpec{
+ Path: "/run/carlos/exe/app",
+ Argv: []string{"app", "", "two words", "line\nbreak", "--db", "/data/x.db"},
+ Env: []string{"A=1", "EMPTY=", "EQ=a=b", "BAD=\xff\xfe", "BIG=" + strings.Repeat("x", 1<<20)},
+ Expected: "/system.slice/carlos-edge.service/carlos-exec@a.example/g3",
+ KeepFDs: 5,
+ }
+ b, err := encodeShimSpec(in)
+ if err != nil {
+ t.Fatal(err)
+ }
+ out, err := decodeShimSpec(b)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(in, out) {
+ t.Fatalf("round trip changed the spec")
+ }
+}
+
+func TestShimSpecRefusesNUL(t *testing.T) {
+ if _, err := encodeShimSpec(shimSpec{Path: "/x", Argv: []string{"a\x00b"}, Expected: "/g", KeepFDs: 3}); err == nil {
+ t.Fatal("a NUL inside argv was encoded")
+ }
+}
+
+func TestShimSpecRefusesTruncatedAndTrailing(t *testing.T) {
+ b, _ := encodeShimSpec(shimSpec{Path: "/x", Argv: []string{"x"}, Env: []string{"A=1"}, Expected: "/g", KeepFDs: 3})
+ for _, bad := range [][]byte{b[:len(b)-1], append(append([]byte{}, b...), 'z', 0), []byte("carlos-cgexec-0\x00")} {
+ if _, err := decodeShimSpec(bad); err == nil {
+ t.Fatalf("decoded a bad spec %q", bad)
+ }
+ }
+}
+```
+
+`internal/cgroup/shim_linux_test.go` (ungated; a temp directory is not cgroupfs, so these exercise the failure paths):
+
+```go
+// TestShimRefusesANonCgroupDirectory: the parent's own check (step 2)
+// refuses before anything starts, as a placement error.
+func TestShimRefusesANonCgroupDirectory(t *testing.T) {
+ m := &Manager{root: t.TempDir(), logf: t.Logf}
+ marker := filepath.Join(t.TempDir(), "ran")
+ cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
+ refused, err := m.startViaShim(m.root, cmd, nil)
+ if refused || !isPlacementError(err) {
+ t.Fatalf("refused=%v err=%v, want a placement error", refused, err)
+ }
+ if cmd.Process != nil {
+ t.Fatal("a process was started")
+ }
+ if _, err := os.Stat(marker); err == nil {
+ t.Fatal("tenant code ran")
+ }
+}
+
+// TestShimUnavailableWithoutTheHook: a process whose main never ran the
+// hook never starts a shim.
+func TestShimUnavailableWithoutTheHook(t *testing.T) { ... flip the hooked flag off for the test, restore in Cleanup; startViaShim answers a placement error and cmd.Process stays nil ... }
+
+// TestShimPlacementFailureIsReported runs a real shim (this test binary,
+// through TestMain's hook) against a temp directory. Split startViaShim
+// into its checked prologue (steps 1 to 3) and a core,
+// func (m *Manager) startShimCore(dir string, dirFile *os.File, expected string, cmd *exec.Cmd, before func() error) (bool, error)
+// and call the core with a temp directory's fd, which the prologue would
+// have refused. The shim's own fstatfs check (shim step 4) answers "P".
+func TestShimPlacementFailureIsReported(t *testing.T) {
+ m := &Manager{root: t.TempDir(), logf: t.Logf}
+ f, err := os.Open(m.root)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ marker := filepath.Join(t.TempDir(), "ran")
+ cmd := exec.Command("/bin/sh", "-c", "touch "+marker)
+ _, err = m.startShimCore(m.root, f, "/nowhere", cmd, nil)
+ if !isPlacementError(err) {
+ t.Fatalf("err=%v, want a placement error", err)
+ }
+ if cmd.ProcessState == nil {
+ t.Fatal("the shim was not reaped")
+ }
+ if _, err := os.Stat(marker); err == nil {
+ t.Fatal("tenant code ran")
+ }
+ if m.placementsUnder(m.root) != 0 {
+ t.Fatal("the placement is still registered")
+ }
+}
+
+// TestShimBeforeRefusalRegistersNothing: before says no, nothing starts and
+// nothing is registered.
+func TestShimBeforeRefusalRegistersNothing(t *testing.T) { ... }
+
+// TestPlacementsUnderCountsTheSubtree and TestWaitPlacementsUnderWakes
+// cover the registry (inflight_linux_test.go):
+func TestPlacementsUnderCountsTheSubtree(t *testing.T) {
+ m := &Manager{root: "/r", logf: t.Logf}
+ done := m.beginPlacement("/r/carlos-exec@a.example/g2")
+ if m.placementsUnder("/r/carlos-exec@a.example") != 1 || m.placementsAt("/r/carlos-exec@a.example") != 0 {
+ t.Fatal("a child's placement is under its parent, not at it")
+ }
+ if m.placementsUnder("/r/carlos-exec@a.exampleX") != 0 {
+ t.Fatal("a prefix without a separator matched")
+ }
+ done()
+ done() // idempotent
+ if m.placementsUnder("/r/carlos-exec@a.example") != 0 {
+ t.Fatal("done did not drop the count")
+ }
+}
+
+func TestWaitPlacementsUnderWakes(t *testing.T) { ... one in flight; a waiter with a 50 ms ctx returns context.DeadlineExceeded; a second waiter returns nil within 1 s of done() ... }
+```
+
+The exec-failure case needs a placement that succeeds, so it lives in the kernel file below. This file also covers the status parser:
+
+```go
+func TestShimStatusParsing(t *testing.T) {
+ for _, c := range []struct {
+ in string
+ placement bool
+ execErrno syscall.Errno
+ ok bool
+ }{
+ {"R\n", false, 0, true},
+ {"", true, 0, false},
+ {"P procs: permission denied\n", true, 0, false},
+ {"R\nE 2\n", false, syscall.ENOENT, false},
+ {"R\nE 13\n", false, syscall.EACCES, false},
+ {"garbage", true, 0, false},
+ } {
+ err := parseShimStatus("/bin/app", []byte(c.in))
+ ... assert ok => err == nil; placement => isPlacementError(err); execErrno => errors.As *os.PathError with Op "fork/exec", Path "/bin/app", Err == execErrno ...
+ }
+}
+```
+
+`internal/cgroup/shim_kernel_linux_test.go` (gated: `CARLOS_SHIM_KERNEL=1` inside a delegated unit; reuse `genKernelManager`'s setup at `gen_kernel_linux_test.go:27`, and skip without the variable). Each test places into `m.root/<name>` made with `os.Mkdir` and a `TierDefault` limit written through `m.applyLimit(..., true)`:
+
+- `TestShimKernelPlacesBeforeExec`: tenant `/bin/sh -c 'cat /proc/self/cgroup; echo $$'`. After `startViaShim` returns: `/proc/<pid>/cgroup` names the target; the tenant's printed cgroup is the target; the tenant's printed `$$` equals `cmd.Process.Pid` (the leader PID is the shim's, now the tenant's); `cmd.Wait()` returns nil.
+- `TestShimKernelExitCodePassesThrough`: tenant `/bin/sh -c 'exit 7'`; `cmd.Wait()` gives an `*exec.ExitError` with `ExitCode() == 7`.
+- `TestShimKernelPassesArgvEnvExactly`: the test binary as tenant in a child mode (`CARLOS_SHIM_CHILD=dump`, env carrying Review Focus 1's values) writes `os.Args` and `os.Environ()` as NUL-joined bytes to stdout; compare against the same command started directly with `cmd.Start()`; also compare its `os.Getwd()` with `cmd.Dir`.
+- `TestShimKernelFDsExact`: `ExtraFiles` of two pipes; the agent side also opens one file with `syscall.Open` without `O_CLOEXEC` before the start (closed in Cleanup). The child mode lists `/proc/self/fd` (excluding the fd its own listing opens): exactly `0 1 2 3 4`. Also `SigBlk`, `SigIgn` from `/proc/self/status` equal a direct start's.
+- `TestShimExecFailureIsTheCommands`: tenant path `/nonexistent/app` (set `cmd.Path` directly so `LookPath` does not fail first): error is `*os.PathError` with `Err == syscall.ENOENT`, `!isPlacementError(err)`, `cmd.ProcessState != nil`.
+- `TestShimKernelPlacementFailureNeverRunsTenant`: target a cgroup the shim cannot write (`chmod 0444` its `cgroup.procs`, restore in Cleanup): placement error, marker file absent, reaped.
+- `TestShimKernelSignalBeforeExec`: `shimTestHold = true`; a `startCmd` seam wrapper (see `cgroup.go:332`) starts the shim and then sends SIGTERM to it; the result is a placement error ("exited before exec"), the marker is absent, `cmd.ProcessState` is set. Then the same with SIGKILL.
+- `TestShimKernelDeadline`: `shimTestHold = true`, `shimDeadline = 300ms` (restored in Cleanup), no signal: a placement error naming the deadline, the shim reaped, marker absent.
+- `TestShimKernelSurvivesDeletedExecutable`: copy `os.Args[0]` to `t.TempDir()/copy.test`, run the copy as a child test (`-test.run '^TestShimKernelDeletedChild$'`, env `CARLOS_SHIM_KERNEL=1 CARLOS_SHIM_DELETED_CHILD=1`); the child deletes its own executable, then places `/bin/true` through `startViaShim` and must succeed.
+- `TestShimKernelOverhead` (informational, never fails on time): 20 shim starts of `/bin/true` against 20 direct starts; `t.Logf` the medians.
+
+- [ ] **Step 2: Run the tests to see them fail**
+
+Run: `cd <worktree> && GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -run 'Shim' -count=1`
+Expected: build failure (`encodeShimSpec`, `startViaShim`, `RunShimIfRequested` undefined).
+
+- [ ] **Step 3: Implement**
+
+`shim.go`:
+
+```go
+package cgroup
+
+import (
+ "errors"
+ "os"
+ "sync/atomic"
+)
+
+// ShimVerb is the carlos binary's hidden placement verb (Paul's ruling,
+// #272, 2026-10-13). carlos-edge.service's RestrictNamespaces=yes makes
+// systemd's seccomp filter answer clone3 with ENOSYS, so no child can be
+// created inside its cgroup. Instead the child starts as this binary,
+// writes its own PID into the target cgroup through a directory fd the
+// parent validated, checks where it landed, and only then execs the
+// tenant: the tenant's code never runs outside its limits.
+const ShimVerb = "__cgexec"
+
+// ProbeVerb prints the process's own cgroup and exits: the tenant the
+// arm-time placement check (VerifyPlacement) execs through the shim.
+const ProbeVerb = "__cgprobe"
+
+var shimHooked atomic.Bool
+
+// RunShimIfRequested must be the first statement of main(), and of TestMain
+// in a test binary that places processes. It marks this process as able to
+// serve the shim (shimAvailable), and when this process IS a shim or a
+// probe it runs it and exits; it returns only when it is neither.
+func RunShimIfRequested() {
+ shimHooked.Store(true)
+ if len(os.Args) < 2 {
+ return
+ }
+ switch os.Args[1] {
+ case ShimVerb:
+ os.Exit(runShim(os.Args[2:]))
+ case ProbeVerb:
+ os.Exit(runProbe())
+ }
+}
+
+// placementError is a start whose process could not be placed in its
+// cgroup before exec. Nothing of the tenant ran.
+type placementError struct{ err error }
+
+func (e *placementError) Error() string { return "placement before exec: " + e.err.Error() }
+func (e *placementError) Unwrap() error { return e.err }
+
+func isPlacementError(err error) bool {
+ var p *placementError
+ return errors.As(err, &p)
+}
+
+// ErrPlacementInFlight is a cleanup asked for members while a process is
+// being placed in its cgroup (Task 2).
+var ErrPlacementInFlight = errors.New("cgroup: a process is being placed in this cgroup")
+```
+
+`shimAvailable()` is `shimHooked.Load()` in `shim_linux.go` and `false` in `shim_other.go`. `shim_other.go`'s `runShim`/`runProbe` return 127, and its `startViaShim` returns `false, &placementError{errors.New("placement before exec needs linux")}`.
+
+`shim_linux.go` implements the parent and shim protocol above. Factor the status decision into `parseShimStatus(path string, b []byte) error` so `TestShimStatusParsing` covers it. Keep every comment to what the code does and why.
+
+`main.go`, first statement of `main()`:
+
+```go
+func main() {
+ // The placement shim (internal/cgroup, #272) must run before anything
+ // else touches the environment or the logger: a shim process carries
+ // none of the tenant's settings and exits from inside this call.
+ cgroup.RunShimIfRequested()
+ rootaws.ScrubEnvironment()
+```
+
+(Import `github.com/carlosframework/platform/internal/cgroup` if `main.go` does not already.)
+
+- [ ] **Step 4: Run the tests**
+
+Run (sandboxed): `GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -count=1`
+Expected: PASS; the kernel tests SKIP.
+
+Run (unsandboxed, kernel):
+```bash
+GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/cgroup.test ./internal/cgroup && \
+systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k -E CARLOS_SHIM_KERNEL=1 -E CARLOS_GEN_KERNEL=1 \
+ /tmp/k/cgroup.test -test.run 'ShimKernel|ShimExecFailure' -test.v -test.count=1
+```
+Expected: every `ShimKernel` test PASS, with the overhead medians in the log. Paste the real output into the report. Then `rm /tmp/k/cgroup.test`.
+
+Also: `GOOS=darwin GOCACHE=$TMPDIR/gocache go build ./... && GOOS=windows GOCACHE=$TMPDIR/gocache go build ./...` and `go vet ./internal/cgroup .`
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add internal/cgroup/shim*.go internal/cgroup/main_test.go main.go
+git -c user.name=claude -c user.email=claude~paul@keymail.dev commit -m "cgroup: the placement exec-shim (#272)"
+```
+
+---
+
+### Task 2: Every Manager start places through the shim (opus)
+
+**Files:**
+- Modify: `internal/cgroup/cgroup.go:170-184` (Manager: drop `fallback`), `cgroup.go:330-549` (`startCmd`, `StartGated` doc, `FakePlacementForTest`, `startIn`), `internal/cgroup/cgroup_linux.go` (drop what only the clone3 start used; keep `setInto` and `shouldLatch`, which `strict_linux.go:344-350` uses), `internal/cgroup/cgroup_other.go`, `internal/cgroup/gen.go:171-235` (`ensureParent`), `internal/cgroup/cleanup_linux.go:54-353` (Cleanup), the tests that pinned the clone3 start and its latch (`internal/cgroup/cgroup_linux_test.go`, `gen_test.go`, `cgroup_test.go`), and comments naming the clone3 start in `internal/activator/{swap.go:205-207, backend_exec.go:2132, backend_exec.go:2920, genbackend.go:400, genbackend.go:495, workerbuild.go:102}` and `internal/cgroup/recreate_linux.go:19,118-122`.
+
+**Interfaces:**
+- Consumes (Task 1): `startViaShim`, `shimAvailable`, `isPlacementError`, `ErrPlacementInFlight`, and the registry (`beginPlacement`, `placementsUnder`, `placementsAt`, `waitPlacementsUnder`).
+- Produces:
+ - `var placeStart = func(m *Manager, dir string, cmd *exec.Cmd, before func() error) (bool, error) { return m.startViaShim(dir, cmd, before) }` — the seam `FakePlacementForTest` swaps.
+ - The start functions' signatures do not change.
+
+**Behaviour.**
+
+`startIn(host, dir, limit, required, build, before)`:
+- Required (generation): if `!shimAvailable()`, log once and return `ErrLimitNotApplied` before the directory is made or `build()` runs (as the latch refusal does today, `cgroup.go:453-458`). Make the directory and apply the limit as today. `refused, err := placeStart(m, dir, build(), before)` (the shim registers the placement itself). Refused: remove the directory if empty, return `before`'s error. Success: `biasOOM`, return the cmd. A placement error: remove the directory if empty, log `cgroup: <host>: placement before exec failed (<err>) — generation start refused`, return `fmt.Errorf("%w: %s: %w", ErrLimitNotApplied, host, err)`. Any other error is the command's: remove the directory if empty, return it unwrapped.
+- Flat: make the directory (failure: start plain, as today `cgroup.go:465-466`) and apply the limit best-effort as today. If `!shimAvailable()`, go straight to `startAndMoveIn` (today's fallback). Otherwise place through the shim. Success: `biasOOM`, return. Refused: as today. A placement error: log `cgroup: <host>: placement before exec failed (<err>) — using the PID-move fallback for this spawn` and `startAndMoveIn` with a fresh `build()` behind `before` (Ruling 2). Any other error is the command's: remove the directory if empty and return it, with no retry.
+- `Manager.fallback`, the read-once comment and the unreachable required-path guard at `cgroup.go:538-546` go; keep a single explicit statement that the required path never reaches `startAndMoveIn`.
+
+`FakePlacementForTest`: keep its name, signature, panic outside tests and no-`t.Parallel` note; it now swaps `placeStart` for one that runs `startGated(cmd, before)` with no shim (as if the kernel had placed it) and restores it in Cleanup. Update its doc.
+
+`startAndMoveIn` (the flat fallback, `cgroup.go:625`) registers its target too, from after `before` agrees until the PID write has returned: its child runs in the agent's cgroup for that window, and a proof must not miss it either. Give `startGated` an optional register step so both paths take the registry at the same point.
+
+Cleanup (`cleanup_linux.go`): `openCleanupAt` records the manager and the directory path it pinned (`filepath.Join(m.root, ExecName(host))`, plus `/child` for a child cleanup) in `cleanupState`. `Populated` answers `true, nil` while `m.placementsUnder(path) > 0`, before reading `cgroup.events`. `Members` answers `nil, ErrPlacementInFlight` while one is in flight. `KillAndEmpty` first calls `m.waitPlacementsUnder(ctx, path)`, returning its error (unproven) if ctx ends first, then does what it does today.
+
+`ensureParent` (`gen.go:198`): after `parentProcs`, add `m.placementsAt(dir)` to `n`, so a flat start being placed into the host directory itself counts as a direct process and the generation start is refused with `ErrLegacyParentPopulated`.
+
+Check every caller of `Cleanup.Members` (`grep -rn "\.Members()" internal`) treats an error as unproven (keeps the lease, records nothing as empty). If one does not, fix that caller in this task and name it in the report.
+
+- [ ] **Step 1: Write the failing tests**
+
+`TestCleanupCountsPlacementInFlight` (in `cleanup_linux_test.go`, over the existing fake-root helper at `cleanup_linux_test.go:28`, which builds `&Manager{root: root}` — give it `logf: t.Logf`): with a placement begun on `<host dir>/g1`, a host Cleanup's `Populated()` is `true` even though the fake `cgroup.events` says `populated 0`; `Members()` is `ErrPlacementInFlight`; `KillAndEmpty` with a 50 ms ctx returns an error and writes nothing to `cgroup.kill`; after `done()`, `Populated()` reads the file again.
+
+`TestEnsureParentCountsAPlacementAtTheParent` (in `gen_test.go`): a placement begun on the host directory itself makes `StartChildGated` answer `ErrLegacyParentPopulated` and build nothing.
+
+`TestGenerationMissingBinaryIsNotABoxCondition` (in `gen_test.go`, with a `placeStart` stub returning `&os.PathError{Op: "fork/exec", Path: "/x", Err: syscall.ENOENT}`): `StartChildGated` returns that error, `errors.Is(err, ErrLimitNotApplied)` is false, and the child directory is gone.
+
+`TestGenerationPlacementFailureIsLimitNotApplied` (stub returns `&placementError{...}`): `errors.Is(err, ErrLimitNotApplied)`, child directory gone, `build` called once.
+
+`TestGenerationRefusedWithoutTheShim` (flip the hook flag off): `ErrLimitNotApplied`, `build` never called, no child directory made.
+
+`TestFlatPlacementFailureFallsBackToPIDMove` (stub returns a placement error): `StartGated` succeeds through `startAndMoveIn` (the fake `cgroup.procs` holds the child's PID), `build` called twice, `before` asked twice.
+
+`TestFlatCommandFailureIsNotRetried` (stub returns a `fork/exec` ENOENT): returned as is, `build` called once.
+
+Replace the clone3 latch tests in `cgroup_linux_test.go` (those asserting `UseCgroupFD`, `shouldLatch` latching, or `fallback`) with the tests above; keep any test of `shouldLatch` that the strict path still relies on.
+
+- [ ] **Step 2: Run them to see them fail**
+
+Run: `GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -count=1`
+Expected: FAIL / build errors on the new names.
+
+- [ ] **Step 3: Implement** the behaviour above.
+
+- [ ] **Step 4: Run the tests**
+
+```bash
+GOCACHE=$TMPDIR/gocache go test ./internal/cgroup -count=1
+GOCACHE=$TMPDIR/gocache go vet ./internal/cgroup ./internal/activator
+GOOS=darwin GOCACHE=$TMPDIR/gocache go build ./... && GOOS=windows GOCACHE=$TMPDIR/gocache go build ./...
+```
+Then unsandboxed, the activator package and the other cgroup users (sockets need `/tmp/k`):
+```bash
+mkdir -p /tmp/k && TMPDIR=/tmp/k GOCACHE=$HOME/.cache/go-build nice -n 19 go test ./internal/activator ./internal/host ./internal/stopevidence ./internal/instancectl -count=1
+```
+Then the generation kernel tests in a delegated user unit, which now place through the shim:
+```bash
+GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/cgroup.test ./internal/cgroup && \
+systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k -E CARLOS_GEN_KERNEL=1 -E CARLOS_SHIM_KERNEL=1 \
+ /tmp/k/cgroup.test -test.run 'GenKernel|GenOOMKernel|ShimKernel|GenerationStartRefusedBesideADirectParentMember|ClaimContainment' -test.v -test.count=1; rm -f /tmp/k/cgroup.test
+```
+Expected: PASS (paste the real output). A test that asserted clone3-specific kernel behaviour (the `cgroup.kill` respawn kill was clone3's) is updated to assert the shim's behaviour on the same scenario, and the report says what changed and why.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git -c user.name=claude -c user.email=claude~paul@keymail.dev commit -am "cgroup: every Manager start places through the shim; placements in flight count as members (#272)"
+```
+(`git add` the new files first.)
+
+---
+
+### Task 3: Arming verifies placement (opus)
+
+**Files:**
+- Create: `internal/cgroup/verify_linux.go`, `internal/cgroup/verify_other.go`, `internal/cgroup/verify_linux_test.go`
+- Modify: `internal/host/rollout.go:22-44` (the code), `rollout.go:150-166` (`swapArming`), `internal/host/rollout_test.go`, `internal/rolloutwords/words.go:84-112` (+ its test), `internal/status/snapshot.go:300-304` (comment)
+
+**Interfaces:**
+- Consumes: Task 1's `startViaShim` (which registers the placement in flight), `ProbeVerb`, `shimAvailable`, `shimExe`.
+- Produces:
+ - `func (m *Manager) VerifyPlacement(ctx context.Context) error` (linux); the `!linux` stub returns an error.
+ - `const placementCheckName = "carlos-placement-check"`
+ - In `internal/host`: `rolloutPlacementFailed = "cgroup-placement-failed"` and `var placementCheck = func(m *cgroup.Manager) error` (a 15 s ctx around `VerifyPlacement`), a seam host tests replace.
+
+**Behaviour.** `VerifyPlacement`:
+1. Nil manager: `ErrNoDelegation`. `!shimAvailable()`: an error saying the shim is not available.
+2. `dir := filepath.Join(m.root, placementCheckName)`. `os.Remove(dir)` (a leftover from an earlier run is empty: the unit's restart killed its members); then `os.Mkdir(dir, 0o755)`; `defer` a removal that waits for the probe to be reaped first.
+3. `m.applyLimit("placement check", dir, TierDefault, true)`; an error is returned.
+4. `cmd := exec.Command(shimExe, ProbeVerb)` with `Env = []string{}` and `Stdout` a buffer. `m.startViaShim(dir, cmd, nil)`. Any error is returned (a placement error or not: either means the check failed).
+5. Wait for the probe with `ctx` (kill and reap on `ctx.Done()`). It must exit 0 and print exactly `expected` (the same string `startViaShim` computed: `dir` without `/sys/fs/cgroup`).
+6. `memory.max` in `dir` must read back `TierDefault.MemoryMax`.
+7. Remove `dir`; a removal failure is logged, not returned.
+
+`swapArming`: after `swapDelegation` passes (`rollout.go:160`), call `placementCheck(eb.Cgroups)`. On error: `log.Printf("agent: REFUSING %s=overlap (%s): a test placement into a scratch cgroup failed (%v), so generations could not be started inside their limits; this box stays on today's stop-then-wake paths", rollout.SwapEnv, rolloutPlacementFailed, err)`, `declineRollout(rolloutPlacementFailed)`, return false. A box that declines this way honours failure records exactly as the other declined codes do (`honourDeclinedFailures`); check that path keys on "declined", not on a list of codes, and if it keys on a list, add the code there.
+
+`rolloutwords`: `"cgroup-placement-failed": unavailableDeclined` in `unavailablePhrases`, and the code in `UnavailableDeclines`'s case list. `status/snapshot.go`: name the code in the comment that lists the declined codes. Grep for every other list of declined codes (`grep -rn '"host-claims-on"' --include=*.go .`) and add the new code to each, with a test where one exists.
+
+- [ ] **Step 1: Write the failing tests**
+
+`internal/host/rollout_test.go`:
+
+```go
+func TestSwapArmingDeclinesWhenPlacementFails(t *testing.T) {
+ // build eb as TestSwapArming's armed case does (rollout_test.go:526)
+ orig := placementCheck
+ placementCheck = func(*cgroup.Manager) error { return errors.New("probe exited before exec") }
+ t.Cleanup(func() { placementCheck = orig })
+ if swapArming(overlapEnv(pub), eb) {
+ t.Fatal("armed with a failed placement check")
+ }
+ if rolloutUnarmed() != rolloutPlacementFailed {
+ t.Fatalf("declined as %q", rolloutUnarmed())
+ }
+}
+```
+
+and make the existing armed-case tests (`rollout_test.go:526`, `:568` and any other test that expects `swapArming` true) stub `placementCheck` to nil-returning, in a helper they share. A flag-off `swapArming` must not call `placementCheck` (assert with a stub that fails the test).
+
+`internal/rolloutwords`: `Unavailable("cgroup-placement-failed")` equals the declined phrase and `UnavailableDeclines` is true for it.
+
+`internal/cgroup/verify_linux_test.go`: ungated: a manager over a temp root answers an error (the temp directory is no cgroup) and leaves no `carlos-placement-check` behind. Gated (`CARLOS_SHIM_KERNEL=1`, `genKernelManager`): `VerifyPlacement` returns nil and the directory is gone afterwards; with `shimExe` pointed at `/bin/true` (restored in Cleanup) it returns an error and the directory is gone.
+
+- [ ] **Step 2: Run to see them fail.** `GOCACHE=$TMPDIR/gocache go test ./internal/host ./internal/rolloutwords ./internal/cgroup -run 'Placement|Unavailable|SwapArming' -count=1` — build errors.
+
+- [ ] **Step 3: Implement.**
+
+- [ ] **Step 4: Run.** The same packages sandboxed (host tests may need unsandboxed `TMPDIR=/tmp/k` for sockets: use that if a socket error appears), plus the gated verify tests in a delegated user unit as in Task 1. Paste real output.
+
+- [ ] **Step 5: Commit** — `"host: arming verifies a real shim placement; decline cgroup-placement-failed (#272)"`.
+
+---
+
+### Task 4: The shim under the edge unit's hardening (opus)
+
+**Files:**
+- Create: `internal/cgroup/edge_hardening_kernel_linux_test.go`
+
+**Interfaces:**
+- Consumes: Task 1 (`startViaShim`), Task 2 (`StartChildGated` on the shim), Task 3 (`VerifyPlacement`), `genKernelManager` (`gen_kernel_linux_test.go:27`).
+- Produces: `TestEdgeHardeningPlacement` (the driver) and `TestEdgeHardeningInner` (runs inside the unit).
+
+**Behaviour.** The driver, gated by `CARLOS_EDGE_HARDENING_KERNEL=1` and run outside any unit:
+1. Read `../host/units/carlos-edge.service` (go test runs in the package directory). Collect every `Key=value` line in `[Service]`.
+2. Sort each key into one of two explicit tables in the test: **applied** (passed as `-p Key=value` to `systemd-run --user`) and **excluded** (with a one-line reason each). A `[Service]` key in neither table fails the test, so a new hardening line forces a decision here.
+3. The applied table must contain at least: `RestrictNamespaces`, `SystemCallFilter`, `SystemCallErrorNumber`, `SystemCallArchitectures`, `NoNewPrivileges`, `MemoryDenyWriteExecute`, `LockPersonality`, `RestrictRealtime`, `RestrictAddressFamilies`, `ProtectProc`, `ProtectClock`, `ProtectHostname`, `ProtectKernelTunables`, `ProtectKernelModules`, `ProtectKernelLogs`, `PrivateDevices`, `ProtectSystem`, `TasksMax`. The test asserts each is present in the unit file and applied. If one of these cannot be applied in a user unit on DOCS-T1, move it to excluded with the real `systemd-run` error as its reason, and say so in the report: Task 7 carries it into the dev re-check.
+4. Expected exclusions, each with its reason: `ExecStart`, `ExecReload`, `Restart`, `RestartSec`, `EnvironmentFile` (process control, not sandbox); `User`, `Group`, `AmbientCapabilities`, `CapabilityBoundingSet` (a user manager cannot change identity or capabilities); `ReadWritePaths` (names box paths absent here); `Slice`, `OOMPolicy`, `Delegate` (the test passes its own `Delegate=yes`); `ProtectHome`, `PrivateTmp` only if the test binary cannot be made visible inside them (prefer keeping them and making the binary visible with `-p BindReadOnlyPaths=<binary>`; say which in the report).
+5. Run `systemd-run --user --wait --pipe --collect -p Delegate=yes <applied...> -E CARLOS_EDGE_HARDENING_INNER=1 -E CARLOS_SHIM_KERNEL=1 -E CARLOS_GEN_KERNEL=1 <binary> -test.run '^TestEdgeHardeningInner$' -test.v -test.count=1` and fail unless the inner run passes; stream its output to the test log.
+
+The inner test, gated by `CARLOS_EDGE_HARDENING_INNER=1`:
+1. **Positive control, the filter is on:** `unix.Syscall(unix.SYS_CLONE3, 0, 0, 0)` answers `ENOSYS`. And a plain `exec.Cmd` with `SysProcAttr{UseCgroupFD: true, CgroupFD: <a scratch cgroup fd>}` fails to start with `ENOSYS`. If either succeeds the test fails: the unit is not applying `RestrictNamespaces`, and nothing else here would prove anything.
+2. **Placement succeeds:** `StartChildGated("hardening.example", GenChild(1), lim, lim, build, nil)` with `lim := Limit{MemoryMax: 64 << 20}` and a tenant that prints its cgroup then waits on stdin: the cgroup is `.../carlos-exec@hardening.example/g1`, and the PID the tenant prints is `cmd.Process.Pid`.
+3. **The limit applies:** the existing `hog` child mode (`gen_kernel_linux_test.go:65`) started the same way into `g2` is OOM-killed: `memory.events` in `g2` shows `oom_kill` of at least 1, the process dies by SIGKILL, and `g1`'s tenant is still alive.
+4. **Arming would arm:** `m.VerifyPlacement(ctx)` returns nil.
+
+- [ ] **Step 1: Write the test** (both functions, the two tables).
+- [ ] **Step 2: Run it against a branch where it must fail**, to prove it can: temporarily make `placeStart` use the old clone3 start (a local, uncommitted edit, `setInto` + `cmd.Start()`); the inner step 2 must fail with `ENOSYS`/`ErrLimitNotApplied`. Paste that output, then revert the edit (`git diff` must show only the new test file).
+- [ ] **Step 3: Run it for real**:
+```bash
+GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/cgroup.test ./internal/cgroup && \
+cd internal/cgroup && CARLOS_EDGE_HARDENING_KERNEL=1 TMPDIR=/tmp/k /tmp/k/cgroup.test -test.run '^TestEdgeHardeningPlacement$' -test.v -test.count=1; rm -f /tmp/k/cgroup.test
+```
+(unsandboxed). Expected: PASS, with the applied and excluded tables printed. Paste the real output.
+- [ ] **Step 4: Commit** — `"cgroup: kernel test, shim placement under carlos-edge's sandbox lines (#272)"`.
+
+---
+
+### Task 5: The probe app accepts the platform's argv (sonnet)
+
+**Files:**
+- Create: `cmd/zddprobe/main.go`, `cmd/zddprobe/main_test.go`
+- Modify: `docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md:841-895` (section "1. Build a probe app")
+
+**Interfaces:** none shared.
+
+**Behaviour.** The platform runs an exec-backed instance as `<binary> --socket <sock> --db <db>` (`internal/activator/backend_exec.go:2147`, `:2335`, `internal/activator/claim_snapshot.go:221`), and `carlos deploy`'s vet boots it as `<binary> -socket <sock>` (`internal/vet/vet.go:135`). The probe accepts both: `--socket` (required) and `--db` (accepted and ignored: the probe keeps no data). Everything else is the roll notes' program unchanged: serve HTTP on the unix socket, every request sleeps 50 ms then answers `ok <version>\n`, and SIGTERM or SIGINT drains with a 15 s shutdown. `version` is set by `-ldflags "-X main.version=..."`. Put the server in `func run(args []string, stop <-chan os.Signal) error` so `main` is `if err := run(os.Args[1:], sigs); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }`.
+
+- [ ] **Step 1: Write the failing test** — `main_test.go` launches the test binary itself as the probe, the way the platform launches it:
+
+```go
+func TestMain(m *testing.M) {
+ if os.Getenv("ZDDPROBE_AS_MAIN") == "1" {
+ main()
+ os.Exit(0)
+ }
+ os.Exit(m.Run())
+}
+
+// launch starts this test binary as the probe with argv, as the platform
+// starts an instance: argv only, no shell.
+func launch(t *testing.T, argv ...string) *exec.Cmd {
+ cmd := exec.Command(os.Args[0], argv...)
+ cmd.Env = append(os.Environ(), "ZDDPROBE_AS_MAIN=1")
+ cmd.Stderr = os.Stderr
+ if err := cmd.Start(); err != nil {
+ t.Fatal(err)
+ }
+ t.Cleanup(func() { _ = cmd.Process.Kill(); _, _ = cmd.Process.Wait() })
+ return cmd
+}
+
+func TestProbeServesWithThePlatformsRunArgv(t *testing.T) {
+ dir := t.TempDir()
+ sock, db := filepath.Join(dir, "app.sock"), filepath.Join(dir, "app.db")
+ cmd := launch(t, "--socket", sock, "--db", db) // backend_exec.go:2335
+ body := getOverUnix(t, sock, "/healthz") // retries until the socket answers, 10 s cap
+ if !strings.HasPrefix(body, "ok ") {
+ t.Fatalf("body %q", body)
+ }
+ // SIGTERM drains: an in-flight request still completes, then exit 0.
+ ...
+ if err := cmd.Wait(); err != nil {
+ t.Fatalf("exit after SIGTERM: %v", err)
+ }
+}
+
+func TestProbeServesWithVetsArgv(t *testing.T) { ... launch(t, "-socket", sock) (vet.go:135) answers ok ... }
+```
+
+`getOverUnix` is a small helper with an `http.Client` whose transport dials the unix socket. The drain check: start a request, send SIGTERM 10 ms later, and assert the request answers 200 and `cmd.Wait()` returns nil.
+
+- [ ] **Step 2: Run to see it fail:** `TMPDIR=/tmp/k GOCACHE=$TMPDIR/gocache go test ./cmd/zddprobe -count=1` (socket paths: if the sandbox refuses the socket, rerun unsandboxed with `TMPDIR=/tmp/k`). Expected: build failure.
+- [ ] **Step 3: Implement `main.go`.** Then replace the roll notes' heredoc with: build from a platform checkout at the release commit, `for v in zdd-1 zdd-2 zdd-3 zdd-4; do CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-X main.version=$v" -o "$TMPDIR/zdd-probe/probe-$v" ./cmd/zddprobe; done`, and a sentence that the probe takes the platform's `--socket` and `--db` (the round-2 dev check found the old one exited at once on `--db`). Keep the rest of "The probe" as it is, and fix its `cd`/path lines to match.
+- [ ] **Step 4: Run** the test (paste output), `go vet ./cmd/zddprobe`, gofmt.
+- [ ] **Step 5: Commit** — `"cmd/zddprobe: the roll notes' probe, taking the platform's --socket and --db (#272)"`.
+
+---
+
+### Task 6: TestStrictKernel's managers, and reset-failed after a generation stop (sonnet)
+
+**Files:**
+- Modify: `internal/cgroup/strict_kernel_linux_test.go:160`, `:190`
+- Modify: `internal/instancectl/gen.go:224-238` (`stopGen`) and its test file (find it with `grep -rln "stopGen\|StopGen" internal/instancectl/*_test.go`)
+
+**Behaviour.**
+
+(a) `TestStrictKernel` full mode builds second managers with no `logf` (`&Manager{root: m.root}` at `:160` and `:190`); production always sets one, and `freshenOne` (`recreate_linux.go:156`) logs through it, so the test panics. Give both `logf: t.Logf`. No production change.
+
+(b) After `stopGen`'s stop or kill succeeds, and before `s.removeGenFiles(unit)`, run `s.Systemctl(ctx, "reset-failed", unit)`. An answer containing `not loaded` is ignored; any other error is logged with `s.logf("instance-control: reset-failed %s: %v", unit, err)` and the stop still answers 204 (Ruling 10). `reset-failed` is already one of the control service's verbs (`server.go:641`).
+
+- [ ] **Step 1: Write the failing test** for (b): with the recording fake `Systemctl` the instancectl tests already use, a `/stop?gen=N` (and `&kill=1`) records `reset-failed <unit>` after the stop or kill; with `reset-failed` answering an error, the response is still 204 and a log line was written; a stop that fails records no `reset-failed`.
+- [ ] **Step 2: Run to see it fail:** `GOCACHE=$TMPDIR/gocache go test ./internal/instancectl -run StopGen -count=1` (unsandboxed with `TMPDIR=/tmp/k` if sockets are refused).
+- [ ] **Step 3: Implement (a) and (b).**
+- [ ] **Step 4: Run** the instancectl package, then TestStrictKernel's full mode for real, in a delegated user unit with no RestrictNamespaces (the strict path keeps clone3, Ruling 3):
+```bash
+GOCACHE=$TMPDIR/gocache go test -c -o /tmp/k/cgroup.test ./internal/cgroup && mkdir -p /tmp/k/strictproof && \
+systemd-run --user --wait --pipe --collect -p Delegate=yes -E TMPDIR=/tmp/k -E CARLOS_STRICT_KERNEL=full -E CARLOS_STRICT_PROOF_DIR=/tmp/k/strictproof \
+ /tmp/k/cgroup.test -test.run '^TestStrictKernel$' -test.v -test.count=1; rm -rf /tmp/k/cgroup.test /tmp/k/strictproof
+```
+Expected: all five subtests PASS (round 2 saw this pass with the same fix in scratch). Paste real output. If the mode needs other variables, read `strict_kernel_linux_test.go:30-60` and set them; say which in the report.
+- [ ] **Step 5: Commit** — `"instancectl: reset-failed a stopped generation unit; strict kernel test managers log (#272)"`.
+
+---
+
+### Task 7: Roll notes (sonnet)
+
+**Files:**
+- Modify: `docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md`
+
+**Behaviour.** Add a section `### Roll notes from zdd-flag-readiness-3` after `### Roll notes from zdd-flag-readiness` (`:336`), in the file's plain style (full stops, no em dashes), covering:
+
+1. The four round-2 blockers and where each is closed (Tasks 1 to 6 by commit; the controller passes the commit list).
+2. The shim, in two sentences, and Paul's ruling that the edge unit's hardening stays.
+3. **Behaviour change on roll, flag off:** every exec child now starts through the shim. What the agent log shows on a healthy box (no `placement before exec failed` lines, no PID-move lines), and what a failing shim looks like (`placement before exec failed (...) — using the PID-move fallback for this spawn`): the box still serves, as today.
+4. The new decline code `cgroup-placement-failed`, its log line, and that a console older than the box shows the unknown-code phrase until consoles roll.
+5. Host claims: the strict path keeps clone3, so under the shipped unit a claims-on box's strict launches answer `ErrStrictUnsupported`; that is now a precondition of claims-on swaps.
+6. **Dev re-check for Paul** (replacing round 2's blocked steps): roll; flag-off wakes answer 200 with no fallback lines; set `CARLOS_SWAP=overlap` and restart; the log shows `overlap swaps armed` and no `REFUSING`; the probe (built from `cmd/zddprobe`) under load through zdd-1 → zdd-3 with 0 failed; the hold control; a generation unit stopped by the control service is not in `systemctl --failed`; and any edge-unit line Task 4 had to exclude (the controller passes the list), checked on the real unit.
+7. The DOCS-T1 kernel-test recipe (`systemd-run --user ... -p Delegate=yes`) and the Task 4 command.
+
+Update the "Still blocks flag-on" lists that name the four round-2 blockers so they point to this section.
+
+- [ ] **Step 1: Write the section and the list edits.**
+- [ ] **Step 2: Check names against the code:** every identifier, log line and flag the section quotes is grepped in the tree; paste the grep output in the report.
+- [ ] **Step 3: Commit** — `"roll notes: zdd-flag-readiness-3 (#272)"`.