Skip to content

feat(cli): finish hosted v0 readiness and safe session management - #70

Merged
jiashuoz merged 11 commits into
mainfrom
cli-v0-finish
Sep 9, 2026
Merged

feat(cli): finish hosted v0 readiness and safe session management#70
jiashuoz merged 11 commits into
mainfrom
cli-v0-finish

Conversation

@jiashuoz

@jiashuoz jiashuoz commented Sep 9, 2026

Copy link
Copy Markdown
Member

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.

  • Remove rainier diff; retain advanced commands under help all.
  • Support deterministic session selectors, context-scoped current, versioned JSON, and recovery keys for ambiguous creates.
  • Pin explicit environment selection to the ID used for agent catalog lookup, preserve diagnostic replay by name, redact verbose diagnostics, and report retained capacity accurately after a warm suspension.
  • Add general PR CI, which was missing for changes outside the session-image paths.

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, complete go 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_SERVER or login --cloud. These are not claims of deployed readiness. No release or deployment is included.

jiashuoz and others added 9 commits September 8, 2026 02:16
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
jiashuoz marked this pull request as ready for review September 9, 2026 16:39
@jiashuoz
jiashuoz merged commit 626d8d6 into main Sep 9, 2026
1 check passed
@jiashuoz
jiashuoz deleted the cli-v0-finish branch September 9, 2026 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant