feat(cli): finish hosted v0 readiness and safe session management - #70
Merged
Conversation
The CLI had grown twenty-odd commands, and `rainier --help` listed all of them: transfers, snapshots, secrets, environments, contexts, workspaces and the self-hosted credential vault sat beside the four things a hosted developer actually does. A new developer could not read the product off one screen, which is the only test of a command surface that matters. The public surface is now sign in, check readiness, authenticate a coding agent, and create and manage sessions — 23 lines of default help. Everything else still dispatches, under `rainier help all`, labeled Advanced. A session has three independent facts and this CLI keeps them apart: the sandbox's lifecycle (`state`), the process inside it (`child_exit_code`), and whether the control plane can reach it (`reachable`). They fail separately, so they are three columns. A session whose agent exited is still Running — it holds its filesystem and it is still attachable — and a runner that dropped its link makes a session Unavailable, not Failed. Action eligibility comes from the raw API's own transition rules and never from the display word: `SuspendSession` accepts `running` and nothing else, so Stop is not offered for a queued session merely because queued and creating share the label Starting. docs/cli-v0-contract.md is the authority for what each command promises and which Cloud APIs each promise depends on. Rainier Cloud 81d1ad3 shipped the compute enrollment and `workspace_not_ready`, so `status` reads the workspace's own compute state — both `status` and `health`, because ready capacity nobody can reach is not ready. Two contracts are still outstanding and both are isolated in cmd/rainier/readiness.go: a bearer-reachable onboarding destination, and a per-environment agent launch catalog. Neither is guessed at. New: `logout`, `status`, `info`, `stop`, `delete`, the `current` selector, and `--json` on everything a script reads — carrying the canonical API facts verbatim beside the derived ones. `diff` is removed completely: git inside the session is the source of truth. `doctor`, `suspend`, `rm` and `agent ls` remain as hidden aliases for the commands that replaced them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q3GPB683VghXL2XA4XWK7Y
A session could not run an integration test. There is no way to get a database after the fact — `sudo` is not installed, the rootfs is read-only, and no package archive is on the egress allowlist — so the client AND the server halves belong in the image, which is the one thing a Dedicated runner is allowed to pull. Nothing starts. `create_main_cluster = false` keeps postgresql-common from running initdb into a directory the read-only rootfs could never use, and a policy-rc.d refuses every maintainer-script service start for the duration of the install. There is no service in the entrypoint and no port bound until a developer runs one of the documented commands. PGDG, and what the pin covers. Debian bookworm ships PostgreSQL 15 and the cell is a 17 with store tests written against it; no 17 for bookworm avoids PGDG directly or transitively. A second apt archive is a real trust decision, so the signing key's full 40-hex fingerprint is an ARG a reviewer can read — and the primary-key COUNT is asserted first and separately, because `gpg --dearmor` converts every key in the file and `signed-by=` then trusts the whole keyring. Versions resolve against a moving archive exactly as Debian's do; rainier-apt-sources.txt records both archives. libpq5 and libpq-dev are named explicitly with their major floored at the server's: apt is as free to resolve the 15-to-17 upgrade by removing the -dev package as by upgrading it, and PGDG ships one libpq for every major it carries, so an archive that has released an 18 correctly hands this image an 18.x libpq beside the 17 server. Durable state goes on the workspace volume, because that is the only writable path surviving a suspend. Sockets and pidfiles deliberately do not: protocol/workspace.TarGz refuses a socket rather than skipping it, so one under /workspace fails `rainier push`/`pull` of any tree containing it and an unclean exit leaves it there to keep failing. They live on the per-container tmpfs, which is where per-container state belongs. `rainier-pg` and `rainier-redis` are root-owned helpers beside sessiond. They install nothing and need no privilege, and the raw tools stay on PATH; they exist because three things here are not the defaults. PostgreSQL's compiled-in socket directory is on the read-only rootfs. This image generates no locales, so an initdb inheriting an unset LANG builds an SQL_ASCII database that mangles the first non-ASCII row a test inserts. And `pg_ctl -w stop` polls kill(pid, 0), which cannot tell a shut-down postmaster from an unreaped zombie — while postmaster.pid outlives the container it was written in, so it is never taken as proof a server is running and a pid that may since have been reused is never signalled. TMPDIR is still not set, and a contract test now says so. /tmp is a per-container noexec tmpfs and that is where agent scratch belongs — not in a checkpoint, an archive or `rainier pull`. Exactly one build temp moves off it, GOTMPDIR, because `go test` executes what it builds. Setting TMPDIR globally would move Claude Code's and Codex's scratch onto the volume that leaves the runner and quietly change the sandbox each believes it has. Verification. Ten new tests run both helpers against stub binaries with no docker, covering the initdb flags, the refusals, the stop paths (stale pidfile, a server still rejecting connections, a server that never stops), the deadline, the DSN, and the durable/runtime split. They are named TestSessionImage* so the qualification workflow's -run selector actually runs them. The container smoke checks that a fresh session has nothing listening and no cluster in the image, then initdbs a cluster, starts it, commits one transaction and rolls another back, reconnects over the DSN the helper prints, asserts the listener is loopback and that no socket appears under /workspace while both servers run, stops and restarts with the data intact, round-trips SQLite from shell and Python, and starts, PINGs and stops Redis — as uid 1000, read-only rootfs, no network at all. The smoke also reports now. A failing check becomes a workflow annotation and the sizes become notices, because a job log is not always reachable from where the fix has to be made, and because size is a rollout gate rather than a curiosity. The services layer measures 14 packages and 244,512 KiB installed — the build diffs its own package set to get that, so it counts the Debian-sourced dependencies too. libllvm19 and libz3-4 are 149 MiB of it, for the query JIT postgresql-17 hard-depends on; removing them means building PostgreSQL from source, which docs/session-image.md argues against.
jiashuoz
marked this pull request as ready for review
September 9, 2026 16:39
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The CLI now provides a compact hosted workflow for login, readiness, agent authentication, and persistent session management. Session lifecycle, process exit, and runner reachability remain separate; missing readiness data never implies the workspace is usable.
rainier diff; retain advanced commands underhelp all.current, versioned JSON, and recovery keys for ambiguous creates.This includes and supersedes PR #65. It preserves its history and reconciles the complete CLI change onto main after #72.
Validation:
make verify, CLI/client race tests, completego test -race -p 1 ./..., and independent plus adversarial review passed. A real local Docker fleet run passed login, session lifecycle/attachment, environment setup/cache, and credential restore/revocation. GitHub publishing was intentionally skipped; egress enforcement still requires native Linux. The initial concurrent full race run hit an intermittent placement-test timeout; three isolated repeats and the complete serialized run passed. Detailed evidence:docs/cli-v0-validation.md.Release dependencies: Cloud must still provide the bearer-reachable web destination and per-environment agent launch catalog for the complete hosted shortcut flow. Until then, the CLI reports the missing catalog and supports explicit commands. Release packaging must also configure the hosted server; source builds support
RAINIER_SERVERorlogin --cloud. These are not claims of deployed readiness. No release or deployment is included.