Containerized provider login: the login pane and every provider-CLI invocation become podman run against the rw-mounted master store — the agent-tools image is a container-mode host's only CLI distribution¶
Provider login (claude auth login --claudeai, codex login --device-auth) ran as a host tmux pane executing the CLI from the host PATH — and the pane was only the visible half of the host-CLI dependency. The adapters also shell out to host CLIs for auth status (which gates every spawn and detects login completion), logout, the ADR-0055 credential-refresh poke, and codex's boot-time model-catalog probe. So a container-mode host — whose every session already runs its CLI from the agent-tools mount (ADR-0051/0052) — still needed every provider CLI installed just to log in and stay logged in. Issue #206 removes that: on a host with container wiring, the login pane command becomes podman run and every non-interactive CLI call becomes an ephemeral podman run --rm, both against the bind-mounted master credential store, so the agent-tools image is the only CLI distribution a container-mode host needs.
The pins, decided:
-
Trigger — config-driven, no fallback. Login (and the non-interactive CLI surface below) runs containerized whenever container wiring is configured: podman, the startup preflight, and a tools image for the provider. A red or unfinished preflight makes
LoginStartrefuse with the actionable preflight error — httpapi maps it to a 400 carrying the text verbatim via a newprovider.ErrLoginUnavailablesentinel — mirroring the run-spawn refusals. Host-mode login survives only on servers with no container config at all; there is never a silent fallback from container to host, because a fallback would hide the misconfiguration behind a login that works for the wrong reason. -
Image — the global default dev image, and only that. Login is repo-less, machine-level state, so the per-repo dev-image resolution (ADR-0053) never applies: the login container runs the global
--container-imagedefault, a missing global default is a refusal naming the knob, and the per-attemptimage exists-then-pull (EnsureImage) applies exactly as for runs. -
Seam — a decorated SessionRunner; the adapters' login flows are byte-for-byte untouched. The wiring layer (
cmd/lab/main.go) hands each adapter a SessionRunner that, for login sessions only (tmuxx.IsLoginSession— ADR-0034's predicate), wraps the provider's CLI argv into thepodman runpane command; every non-login session passes through untouched. The per-provider inputs come from one small declaration on the provider contract —MasterStoreSpec{EnvVar, HostDir, HomeSubdir}(claude:CLAUDE_CONFIG_DIR/ the override-aware host dir /.claude) — so podman knowledge stays out of the provider packages and the scrape/paste/poll/teardown logic stays exactly as it was. The decorator's Stop also runs thepodman rm --force --ignorebackstop (a login CLI may ignore SIGHUP, ADR-0052's lesson) and wipes the scratch home. -
Mounts and env. The master store rw, its mount source resolved through the provider's override-aware resolver —
$CLAUDE_CONFIG_DIR/$CODEX_HOMEhonored, which makes the operator override officially supported in container mode (the #211 criterion) — mounted nested under the container-side home (/home/agent/.claude,/home/agent/.codex) with the config-dir var pinned explicitly inside the container, so the CLI can never derive some other path. HOME is a per-attempt host scratch dir under<state>/logins/, bind-mounted at/home/agent(ownership just works under--userns=keep-id); teardown wipes it. The tools image mounts at/opt/labwith the PATH prepend, as in runs. No secrets in env;TERMforwards by name. -
The non-interactive CLI surface rides the same machinery. The adapters' exec sites — claude's
auth status --json,auth logout, and the-p --model haikurefresh poke; codex'slogin status,logout, thecodex execrefresh probe, and thedebug modelscatalog probe — move behind an injectable CLIRunner seam. The container implementation runs each invocation as an ephemeral non-ttypodman run --rmagainst the same mounted master store with the same pinned env; the host implementation preserves today's exec behavior byte-for-byte. This is forced, not optional: the spawn gate runsAuthStatusbefore every spawn, login-completion detection polls it, and the ADR-0055 refresh loop pokes the CLI — a CLI-less host would break all three if only the login pane were containerized. The container CLIRunner waits (ctx-bounded) for the preflight verdict instead of failing boot-time races, and podman-level failures (exit 125/126/127) are reported as could-not-run, never as a CLI verdict — a missing image must never read as "logged out". -
Limits and gates — nothing new. The global container settings rows (
container_memory/container_pids/container_nofile; no repo override, login being repo-less) cap the login container, and the startup preflight verdict is the gate. No new knobs, no new flags. -
Lifecycle — deterministic names, idempotent re-join, sweepable strays. The login container's name is
podmanx.ContainerNameover the login session name — the existinglabrun-namespace — so the startup orphan sweep accounts for a live login's container by construction and reaps leaked ones with no stored state. A pending login's container survives a server restart (tmux pane liveness underKillMode=process, exactly as for runs) andLoginStartre-joins it idempotently. Ephemeral CLI containers get uniquelabrun--prefixed names, so a wedged one is reaped by the next boot sweep.
Status¶
Accepted. Settled via issue #206 (2026-07-25 grilled triage; the CLIRunner surface is the completion the acceptance criteria force — the CLI-less-host criterion is unreachable with only the pane containerized). Builds on ADR-0034 (per-provider login sessions — the session names and IsLoginSession predicate are reused unchanged), ADR-0051 (the agent-tools images — now the only CLI distribution for a container-mode host), ADR-0052 (the runner split — tmux stays host-side and the pane command becomes podman run; this closes that ADR's named containerized-login follow-up), and ADR-0055 (the master store, refresh poke, and completion-detection surfaces are exactly what this containerizes — the seam methods are untouched, only where their subprocesses execute moves). Deliberately out of scope: retiring host-mode login (break-glass, still working on container-less servers); the master store format and the OAuth capture mechanism; a dedicated login image; login-specific resource limits; and real-OAuth verification pre-merge — the maintainer runs the manual matrix (claude login, codex device login, restart-mid-login resume, a subsequent container run authenticating, a CLI-less host) after merge.
Considered options¶
-
Keep the host CLI as a fallback when container wiring is present. Rejected: a silent fallback hides misconfiguration — a login that quietly runs the host CLI on a container host "works" while proving nothing about the containerized path, and the first genuinely CLI-less host discovers the gap in production. Config-driven with no fallback means a red preflight is a loud, actionable refusal — the same honesty contract run spawns already carry.
-
Containerize only the login pane. Rejected: the pane is the smallest part of the CLI surface. The spawn gate (
AuthStatusbefore every spawn), login-completion detection (a poll of the same call), and the ADR-0055 refresh loop all exec the CLI — with only the pane containerized, a CLI-less host would refuse every spawn, never detect a completed login, and let the master store's tokens expire unrefreshed. The CLIRunner seam is what makes the CLI-less criterion true rather than aspirational. -
A dedicated login image. Rejected: the global default dev image plus the agent-tools mount already is the CLI distribution — a second image would be one more publish pipeline, one more preflight ref, and one more version-skew surface, all to run a CLI the tools image already carries.
-
Per-repo image resolution for login. Rejected: login is repo-less, machine-level state — no repo's dev image can meaningfully claim it, and picking one arbitrarily would make login behavior depend on which repo the operator happened to configure first.
-
Login-specific resource limits. Rejected: the global
container_memory/container_pids/container_nofilerows bound a login pane at least as well as they bound a full agent session; a second set of knobs would be configuration surface with no scenario demanding it.
Consequences¶
-
Every auth-status check on a container-wired host is now a podman container spin. Sub-second in practice, and the 30s status cache bounds the cadence — but the spawn gate, the completion poll, and the refresh loop all pay it. The cost is accepted as the price of the CLI-less host.
-
codex's boot-time catalog probe can race the preflight verdict. The adapter probes
debug modelsat construction; on a container host the CLIRunner waits ctx-bounded for the preflight verdict, and when it does not land in time codex serves the compiled-in fallback catalog with one warning — ADR-0043's designed degradation, graceful by construction. -
NixOS operators can drop the provider CLIs from the unit PATH on container-mode hosts. Every
agentPackagesvalue may be set tonull— the tools image is the CLI distribution. The module defaults are unchanged by this slice: dropping the CLIs is an operator choice, not a new default. -
Hosts using the
hostrunner still need the CLIs. Host-runner repos resolve the provider CLI from the host PATH exactly as before, and host-mode login remains the break-glass on container-less servers — this ADR removes the requirement, not the option. -
docs/ops.md carries the operator contract: the CLI-less-host statement, the override-aware mount source, the
<state>/logins/scratch homes, and the preflight text surfacing in the login UI.