# Podman as the default execution home — design

audience: AI coding agents first. slug: `podman-default`
source request: owner 2026-08-16 — podman-first, k3s after ("the low-regret ladder... step one is closer to 'flip the default' than 'build something new'"). The owner's full staging analysis is the rationale; its decisions are pinned below.

## Why (owner analysis, pinned)

- The podman layer EXISTS and is proven: `harness-seat:2` on all three boxes, `deck-podman` sanctioned wrapper, P3 foundation smoke-proven 2026-08-06, box-sessions counts resident podman containers today. This slice PROMOTES an existing layer to default — it builds almost nothing new.
- The immediate win: gates and agents get their own cgroup, killing the shared-slice contention class NOW (containment false-failures, load-dependent flakes, overload triage squeezing tests) without waiting for k3s.
- The k3s-local-testing caveat applies identically: a contained agent/gate runs tests directly inside its container, no offload hop — the doctrine gets debugged on the simpler stack (one `podman exec` away).
- Per-project tooling manifests + split-config rendering get proven against plain containers first.
- k3s is then de-risked to almost pure orchestration (scheduling, placement, restart-on-death).

## What this does NOT fix (owner-named readiness gates, tracked elsewhere)

The transport silent-kill class and fleet provisioning drift (mise versions, manifests) sit BELOW the container layer and travel with us either way. Their fixes gate readiness regardless of rung. (Transport observability landed 2026-08-16 — failures now name themselves; drift is a registered future item.)

## Slices

**P1 — gates run in containers (laptop first).** The land-queue gate and `local-gate` full-mode executions run inside a podman container (the harness-seat image family) instead of the shared build.slice. Seams: `local-gate` gains a container executor (config-named, like the remote executor seam from the gate-offload work — one more executor kind, not a parallel system); the container gets the workspace bind-mounted, the project's toolchain (per-project tooling manifest when it lands; until then the seat image's generic toolchain + the workspace's own node_modules), and NO access to the host's ~/.claude write surfaces. Environment identity: inside these containers the resolver (docs/specs/2026-08-15-environment-identity-design.md) reports seat-container, so the local-testing rule applies — tests run directly, no offload hop. Evidence: one real land-queue ticket whose gate log names the container executor, green, while the host's build.slice shows no gate processes.

**P2 — agent seats default to podman.** New agent dispatches on the boxes (pi_remote_dispatch, factory remote spawns) run inside resident seat containers by default rather than bare on the box. Guard parity holds INSIDE containers (git shim + tool shims on the container PATH — verify, since the shims were proven on the box, not in-container). Evidence: a real dispatched agent shows up in the box-sessions column as a resident container AND a blocked command inside the container is refused with guidance.

**P3 — interim seat scheduler with a NAMED RETIREMENT.** The existing admission-slots machinery points at per-box podman capacity (slots per box = configured container capacity). Owner's explicit failure-mode-to-name: "the transitional layer that never retires." This plan RECORDS: the interim scheduler is replaced by k3s scheduling; its config carries `interim-until: k3s-scheduling`; the k3s adoption plan MUST list removing it as an acceptance item. The emergency-seat contract (docs/specs/2026-08-15-seat-limiter-emergency-design.md) applies to this pool unchanged.

Slice order P1 → P2 → P3; each deploys and proves before the next (delivery doctrine).

## Error handling

Container start failure → the gate/dispatch FAILS VISIBLY naming the container error (image missing, mount denied) — never silent fallback to bare-host execution (silent local fallback is a defect, standing rule). Image absent on a box → same visible failure + the existing build-image.sh path is the named fix. Host-side overload triage capping build.slice no longer touches gates at all — that is the point.

## Testing

P1: gate-in-container fixture (fake ticket, container executor, green + refusal-on-missing-image branches); the containment-invariants suite runs unchanged (host doctrine untouched). P2: in-container shim refusal test per box; box-sessions count increments. P3: slot arithmetic tests incl. emergency-seat debt cases against podman capacity. All: factory pytest green; never weaken a gate.

## Architecture decisions

- Container executor is one more executor kind on the EXISTING local-gate/remote-build seams — a parallel "podman runner" system was rejected (two dispatch paths drift).
- Laptop gates first (P1) because that is where shared-slice contention hurt today; box agents second.
- Interim scheduler reuses admission-slots — building a new scheduler for a layer k3s replaces was rejected (YAGNI + the named retirement risk).
- No change to WHAT gates run — same commands, same fail-closed semantics, different cgroup home.
