feat(runner): containerized runner — rootless podman panes behind repos.runner #214
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab!214
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/205"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #205
First end-to-end containerized run per the 2026-07-22 container-isolation design, now recorded as ADR-0052: tmux stays host-side and keeps owning liveness/attach/send-keys/capture; a repo set to
runner = containergets its pane command wrapped aspodman run -it --rm(rootless podman + crun) with the full mount inventory. Host mode stays the default and is labeled "unsandboxed — full host access" (break-glass) in the repo-settings UI.What shipped
repos.runnerend to end — migration 0017 (sqlite+postgres), store, reposvc validation, PATCH API, and a new repo-settings Runner section (select + host warning + limit overrides with inherit hints).internal/podmanx— pure pane-argv construction (--userns=keep-id,--network=pasta,--mount type=imageof the agent-tools image at/opt/lab, five binds,-w), deterministiclabrun-container names (recomputable, no stored state), HOME env rewriting, thepodman rm --force --ignorebackstop, and the collect-all preflight (podman ≥ 4, pasta, subuid/subgid, cgroup-v2 memory+pids delegation, dev image + tools refs resolvable) over injectable probes.--memory/--pids-limit/--ulimit nofilefrom seeded global settings rows (8g/4096/16384) with nullable per-repo overrides; the prlimit wrapper is retired for container panes only (tmuxx.WithoutNofileCap); host panes byte-identical to before (dual-runner seam tested).<state>/instances/<runID>/runtimenow holds the run's git cred files, known_hosts (TOFU-seeded from the global file), dialog spool, and--settingsfile, so the container mounts exactly one run's secret surface; wiped with the run (host mode uses the same path — one code path).agent.sock→<state>/agent/agent.sockso the socket directory can be mounted (a socket-file bind pins a dead inode across a server restart); back-compat symlink at the old path keeps pre-upgrade sessions working.--env K=V;LAB_TOKENrides tmux-e+ name-only--env(never in any argv);LAB_URLpinned to the mounted unix socket (a TCP--agent-urlis unreachable through pasta's no-host-route netns by design);PATHpins/opt/lab/binfirst.labrun-containers.labctlby bare PATH lookup, and the container PATH carries/opt/lab/bin(ADR-0051's static labctl). No hook-marker migration.--podman,--container-image(global dev image; empty ⇒ preflight refusal),--container-tools-image provider=ref[,…](@sha256-pinned per ADR-0051).Delegate=yes,XDG_RUNTIME_DIR/linger, passt), definition-of-done §11.Verification
Automated, all green locally:
go test ./...andgo test -tags ui ./...(44 packages, incl. the afk cycle and instance integration suites against real tmux),golangci-lint run,go vet, vitest (930 tests / 64 files),tsc --noEmit, vite build, migration-parity tests. The pane argv, mounts, env split, limits resolution, refusal paths, stop backstops, and the full preflight matrix are unit-pinned (internal/podmanx,internal/instance/container_test.go,internal/reconcile/container_test.go,internal/afk/container_test.go).Not claiming e2e: the lab sandbox has no rootless podman, so the real-podman integration test (
TestContainerPane_integration, gated onLAB_TEST_PODMAN=1) skips here — verified to skip gracefully with distinct messages for each missing prerequisite. The acceptance criteria's full container loop (spawn → chat via tailer → labctl over the socket → push with hook scan → stop cleanup) and the in-container reachability assertions (state dir/master.key/host loopback unreachable; agent.sock + egress reachable) remain maintainer validation on the dev host, per the issue brief. definition-of-done §11 records exactly this split.Notes for the dev-host pass
Delegate=yes,XDG_RUNTIME_DIRfor rootless podman, then set--container-imageand--container-tools-image claude-code=<ref@sha256:…>.image_ref(#207) are untouched follow-ups; the default stayshostuntil preflight is proven.The 2026-07-22 container-isolation tracer bullet (ADR-0052): tmux stays host-side and keeps owning liveness/attach/capture, while a repo set to runner=container gets its pane command wrapped as `podman run -it --rm` with the full mount inventory — worktree + bare rw at host-identical paths, the agent socket directory, the instance HOME at /home/agent, the per-run runtime dir, and the ADR-0051 agent-tools image read-only at /opt/lab — under --userns=keep-id and --network=pasta. - repos.runner ('container'|'host', host default/break-glass, labeled "unsandboxed — full host access") end to end: migration 0017, store, PATCH validation, repo-settings Runner section. - internal/podmanx: pane argv construction, deterministic labrun- container names, HOME env rewrite, rm backstop, and the collect-all startup preflight (podman>=4, pasta, subuid/subgid, cgroup-v2 delegation, images resolvable) behind injectable probes. - Limits --memory/--pids-limit/--ulimit nofile from seeded settings rows (8g/4096/16384) with nullable per-repo overrides; prlimit retired for container panes only (tmuxx WithoutNofileCap). - Per-run runtime dir <state>/instances/<runID>/runtime for git creds, known_hosts (TOFU-seeded from the global file), dialog spool, and the --settings file — the container mounts exactly one run's secret surface; wiped with the run. - agent.sock moved into <state>/agent/ so the socket DIRECTORY can be mounted (a socket-file bind pins a dead inode across restart); back-compat symlink kept at the old path. - Env discipline: nothing inherited; non-secrets as --env K=V, LAB_TOKEN name-only forwarded from tmux -e; LAB_URL pinned to the mounted unix socket; PATH pins /opt/lab/bin first. - Container spawns refused with actionable preflight errors BEFORE the claim; stop/reap/discard kill paths gained the podman rm backstop and startup reconciliation sweeps orphaned labrun- containers. - Pre-push hook unchanged: it already PATH-resolves labctl, and the container PATH carries /opt/lab/bin. - Integration test gated on LAB_TEST_PODMAN=1, skipping gracefully where rootless podman is unavailable; argv/preflight/seam unit-tested throughout. Docs: ADR-0052, CONTEXT.md Runner entry, ops.md container runner section, definition-of-done §11. Closes #205 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>[autoland] verdict: pass
PASS — validated, landable as-is.
Signal relied on: Forgejo Actions CI green —
ci / native(7m53s) andci-nix / flake-check(39m12s) both success. Not re-run. Deep diff review of the security-critical paths (podmanx argv, launch/container/stop, reconcile, afk, agentapi, store, httpapi, web Runner).Conventions: Conventional Commits title, valid
Closes #205. Merges cleanly into main (no conflict).Verified correct:
-e+ name-only--env LAB_TOKEN— never appears as a value in any podman argv (regression-pinned, container_test.go:135-144).podman rm --force --ignorebackstops every kill path + startup orphan sweep.CONCERNS (non-blocking — already scoped as dev-host maintainer validation in the PR & ADR-0052):
--network=pastadefaults with no CI coverage (integration test skips without podman). Confirm on the dev host that a container cannot reach a host127.0.0.1service./proc/self/cgroup; verify the deployed unit delegates to the subtree the pane's container actually lands in, else limits could be silently absent despite green preflight.[autoland] verdict: pass
PASS — validated independently, landable as-is.
Signal relied on: Forgejo Actions CI green —
ci / native(8m33s) andci-nix / flake-check(21m15s) both success (aggregate success, exit 0). Not re-run. Plus a focused read of the security-critical paths: podmanx argv/env/mount/name construction, preflight, the launch dual-runner seam, container wiring, reconcile orphan sweep, migrations, config parsing, and the socket move.Conventions: Conventional Commits title; valid
Closes #205. Merges cleanly into current main (git merge-tree --write-tree, no conflict).Verified correct:
CONCERNS (non-blocking — scoped as dev-host maintainer validation in the PR & ADR-0052):