docs: propose Rainier × NoKV workspace continuity - #17
Open
wchwawa wants to merge 1 commit into
Open
Conversation
Signed-off-by: wchwawa <wch19961116@gmail.com>
Author
|
@jiashuoz, could you please review this high-level direction and share your opinion? In particular, I would value your guidance on the driver/provider boundary and whether a local cross-runner E2E should be the next contribution before any NoKV shadow adapter. |
Member
|
Quick summary after comparing this RFC with the existing checkpointing plan and reviewing NoKV source at
Overall: NoKV looks promising as an immutable checkpoint substrate, but it should not yet replace the provider-neutral checkpoint boundary or become a runtime dependency. The two approaches can be complementary if NoKV sits behind Rainier's export/install contract. |
jiashuoz
added a commit
that referenced
this pull request
Sep 5, 2026
) A coding agent inside a session cannot be logged in today: its home directory is on the read-only rootfs, and nothing carries a login from one session to the next. This plan gives each (creator, workspace) a writable agent home on the runner at /rainier/agents, points Claude Code and Codex at their subdirectories through the variables they already honor, and has sessiond keep the provider's allowlisted credential files equal to the control plane's sealed copy — fetched at boot, put on every change, emptied on a downward revoke — over the session RPC that already carries the git credential. Custody is one set per (user, provider), projected into every workspace the person is in with membership re-checked at each delivery; logout (everywhere) and withdrawal (leaving one workspace) are kept apart. Everything provider-specific is one row in controlapp; sessiond, the driver, the stores, and the RPC never spell a provider name. Three CLI verbs and /v0/agents on top. Tag v0.0.3. Task 0 is four probes on the fleet VM — where Claude Code writes its onboarding file under CLAUDE_CONFIG_DIR, the paste-a-code login inside a container, the egress hosts, two processes sharing one directory — and the plan carries the fallback if the first comes out the other way. The live-fleet phase uses a synthetic `test` provider so the e2e never touches a real account; the one real login is the operator's and leaves no transcript. Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
jiashuoz
added a commit
that referenced
this pull request
Sep 5, 2026
* feat(controlapp): the agent home on every create — the wire field, the provider table, the variables A coding agent needs a writable place for its configuration and its credential, and a session has none: $HOME is on the read-only rootfs. This gives every create for a person a HomeMount — one volume per (creator, workspace), named by an opaque hash so `docker volume ls` on a runner discloses no account id — and points each agent at its own subdirectory through the variable it already honors (CLAUDE_CONFIG_DIR, CODEX_HOME). The sandbox learns which directories and which files are the credential set from RAINIER_AGENTS_B64, a manifest of paths and names that carries no provider knowledge and no credential, and the providers' hosts join the egress allowlist at dispatch the way the git hosts already do. Everything provider-specific is one table in controlapp/agents.go — claude, codex, and a synthetic `test` row a host enables only for the e2e — and the rows are the probe results of 2026-09-05. The three session-RPC method names that keep the set equal to custody are declared once in protocol/runner, additive at version 1: a Spec without a home marshals to exactly the bytes it did before. A session with no creator gets none of this, because a home belongs to a person and a variable pointing at an unmounted directory would be a claim the create cannot keep. Task 1 of docs/superpowers/plans/2026-09-04-agent-credential-home.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * feat(driver): mount the agent home — one prepared volume per (creator, workspace), never torn down with a session The runner's create now carries Spec.Home, and the docker driver mounts it: the named volume is created if absent, prepared once by the same CAP_CHOWN-only job the workspace volume gets — the mkdir seed is as load bearing here as there, or docker would re-initialize the empty volume from the image's root-owned mount point on every boot — and mounted at its path after the workspace, before the runtime wiring. Destroy, RemoveWorkspace, and DestroyContainer never name it: it belongs to the (creator, workspace), and other sessions of the same person may be mounted on it at that moment. The volume-preparation path is shared between the two mounts rather than copied, so there is one place for the chown to be forgotten. A half-specified home is refused by Create before any side effect, naming the field, rather than becoming a daemon-side syntax error; a nil one mounts nothing, which is the honest state for a session with no creator or a create from an older control plane. runnerd copies the wire's HomeMount into the driver's, field for field, exactly as it copies the repo list. dockerRun is a package variable now, so a test can assert what did NOT run: a volume prepared once and not twice, a teardown that never names the home. The docker-backed snapshot test proves a file under the home's path is absent from a committed image. Task 2 of docs/superpowers/plans/2026-09-04-agent-credential-home.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * feat(sessiond): keep the agent homes equal to custody — fetch at boot, put on change, empty on revoke The manifest a create injects (RAINIER_AGENTS_B64: directories and file names, never a provider, never a credential) is what sessiond acts on. An "agents" stage sits between clone and init; it is a wait, not the work, because the fetch is a session RPC and the relay that carries it exists only once the session runs. sessiond fetches on its own goroutine while the earlier stages run, writes each file 0600 by temp-and-rename, marks the stage done, and then, every two seconds, stats the allowlisted files and puts the set upward when their digest moved — a background token refresh included — with 2 s to 30 s backoff on failure and one final put on shutdown. A downward revoke removes the files and forgets the baseline. The only outcome that fails a session is a runner that mounts no home at all, with the sentence that names the fix; a refusal or a timeout from custody is a boot note and the agent starts anyway, asking for a login, which is the truthful state. Hygiene is the invariant: reads use O_NOFOLLOW, the set is capped at 64 KiB and a symlink or an oversize set is a note rather than a put, the bytes are never kept — a digest is compared, not the credential — and a test proves that no three-byte run of the fixture reaches a log line or a note, which is what forced the file's whole log vocabulary away from words like "sent". Task 3 of docs/superpowers/plans/2026-09-04-agent-credential-home.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * feat(controlapp): custody of a person's agent logins — the service, the port, the contract suite, the self-hosted vault, the RPC answers The credential set a coding agent writes inside its home is now something the control plane holds: one sealed row per (person, agent), fetched by every session that person starts and replaced by every put their sessions make. The service in controlapp owns the authorization for both hosts. A fetch or a put is answered only for a session with a creator, for a provider the table names, and for a creator the host's authorizer still lets attach to their own session in that workspace — which is "is a current member" without a new action in the frozen contract — and a put is bounded: bare names on the provider's allowlist, sixty-four kilobytes in all. A fetch is filtered against the current allowlist on the way out, so no stored row can become a write outside it. Logout revokes custody and sends the downward revoke to the person's live sessions; withdrawal sends it in one workspace and leaves custody alone. Refusals are fixed sentences wrapping control's sentinels, and a host relays only those. The contract suite runs over every store. Self-hosted controld seals the set under RAINIER_SECRETS_KEY with (user, provider, version) bound as additional authenticated data — seal.go grows an AAD pair, and Seal/Open become that pair with nil, so every value sealed before still opens — and the vault assigns the version under a compare-and-set, so two concurrent puts cannot land on one version and a row's ciphertext and its version can never disagree. Migration 0010 adds the table beside credentials, cascading with its operator; srpc.go answers the two upward methods beside the mint, in the shapes sessiond reads, with a pre-decode bound on the request. The Postgres suite ran against a real database: the head-migration expectation moves to 10. Task 4 of docs/superpowers/plans/2026-09-04-agent-credential-home.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * feat(cli): rainier agent login | ls | logout, and /v0/agents The three verbs a person uses. `agent login <provider> --env NAME` is not new machinery: it creates a session running the provider's own login command with no repositories, attaches exactly as `new` does, and when the person exits or detaches it removes the session and reads /v0/agents to say whether custody's version moved — "logged in as of …" or "login did not complete: the agent wrote no credential". `agent ls` lists every provider this build knows, logged in or not, with the workspaces the login reaches; `agent logout` says what it is about to do to every workspace and to a running agent, asks, and destroys the set. GET /v0/agents renders the caller's own logins from custody's statuses, which carry a provider, a version, and a timestamp and have nowhere to put a credential; every row keeps the same key set, with `since` null on a row that has nothing to date, as the wire's rule requires. DELETE /v0/agents/{provider} is a logout: 204 whether or not a set existed, 404 for a provider this build has no row for, and its message is custody's own sentence rather than an error's text. Self-hosted controld has no runtime membership change, so no withdrawal hook exists here and the registration says why; a hosted cell calls Withdraw from its own membership path. The CLI honors RAINIER_E2E_TEST_AGENT so the fleet e2e can log the synthetic provider in. Task 5 of docs/superpowers/plans/2026-09-04-agent-credential-home.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * docs+e2e: the agent credential home — the synthetic provider on the e2e controld, the live phase, the documents The fleet e2e gains a phase that proves the whole credential path with no real account: the e2e's controld offers the synthetic "test" provider (RAINIER_E2E_TEST_AGENT, read once at startup and never a flag), `rainier agent login test` logs it in through a FIFO-driven attach, custody reaches v1, the home volume is REMOVED, a second session boots and holds the file — which can then only have come down the RPC — at mode 0600, a rewrite reaches custody as v2 within the sync interval, the environment's snapshot carries nothing under the mount, `agent logout` empties custody and the live session, and no log holds the credential. The relay's stage vocabulary names the "agents" stage. README gains "Agent login"; the deploy guide gains §9, the credential home as an operator sees it, including the two things the first live run taught: the proxy URL's placeholder password and the claude.ai connectors' retry cost. The plan records what execution decided beyond its text: the stage waits on a marker, boot notes are not surfaced yet, custody assigns the version under a compare-and-set, membership is ActionAttach on the creator's own session, an incomplete login exits 0, and the single-runner proof by volume removal. Task 6 of docs/superpowers/plans/2026-09-04-agent-credential-home.md, minus the tag, which follows the live run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * e2e: the agent phase brings its own environment and never blocks on the login FIFO The environments phase removes its environment on the way out, so the agent phase creates one of its own — with a setup script, so it caches a snapshot the phase can inspect — and opens the login FIFO read-write so a CLI that refused the request and exited cannot leave the writer blocked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * fix(agents): put the agent's last write the moment it exits, and let `agent login` wait for custody before removing the session The first live run found the race the design did not: a login command that writes its file and exits at once — the synthetic provider, and exactly the shape of a device-code login — ended the attach, the CLI removed the session in the same second, and the container was gone before the two-second sync tick could put. sessiond now flushes every home synchronously as the child exits, which is the same event the CLI acts on; and `agent login` reads custody before it removes the session, waiting up to ten seconds for the version to move and stopping the moment it does, with one more look after the removal for the shutdown put. Both are tested; the CLI's request order moves the status read ahead of the removal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * cli: agent ls shows the version custody is at The plan's table has a VERSION column and the fleet e2e reads it to tell a refresh from a login; the first live run showed the column had been left out. A row with no set reads 0. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * e2e: the agent phase's probes match only what the shell prints, never the typed line attach_probe's rule: the pattern must not appear in the command text, or the PTY's echo of the typed line satisfies the wait before the command has run. Every probe in the agent phase broke it; each marker is now split in the typed text and whole only in the output. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C * fix(driver): a snapshot boots the environment's command, not the command of the session that built it `docker commit` records the container's own Cmd into the image. The session that builds an environment's cache is not always a shell — `rainier agent login` runs the agent's login command and exits, and it is often the first session of its environment — so every later session from the cache booted the login command instead of the shell. The driver now reads the base image's command back from docker at commit time and pins it with `--change CMD`, beside the environment keys it already strips. Found by the agent-credential phase of the fleet e2e; a pre-existing bug for any first session with a custom command. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KyUMzcATwGtnL9bfwMkr1C --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.
TL;DR
Propose a high-level, post-v0 collaboration boundary for portable workspace continuity. Rainier and Postgres retain lifecycle authority; the runtime driver owns workspace export/install; NoKV remains an optional checkpoint and provenance provider evaluated shadow-first.
This PR is intentionally RFC-only. An exploratory local prototype was excluded after review against current main showed that the first implementation must integrate through Rainier's opaque driver and existing
/workspacerules, rather than accept host filesystem paths.Problem
Rainier already preserves
/workspaceacross container crash and cold park on one runner. Its v0 design still accepts losing unpushed work when that runner VM is lost, while the v1 roadmap calls for object-storage checkpoints and cross-runner recovery.Consequences
Without an explicit boundary, Rainier would need to assemble and qualify workspace identity, recovery, history, provenance, retention, and cleanup across Postgres, object storage, and runtime-specific volumes. Coupling that work directly to NoKV would instead introduce a second authority and a premature v0 dependency.
Example
A runner disappears with uncommitted code, generated files, and multi-repository work in
/workspace. Rainier recreates those files on another runner, then lets the agent continue through its native resume mechanism. Process memory, PTY, sockets, and GPU state are explicitly outside this proposal.Solution
Gain
NoKV can reduce the consistency machinery Rainier would otherwise build around versioned workspace history, retries, recovery, provenance, references, and garbage collection. The claim is not that any storage primitive is unique; the harder-to-replace value is the integrated agent-workspace lifecycle.
Tradeoffs
Verification
main@c44d65322f832deb3c70e093957b9c3ebcb14769;git diff --check;go test -race -count=1 ./...;go vet ./...;go build ./....No real cross-runner restore or production NoKV integration is claimed.
Decision requested
@jiashuoz, would you please review the direction and share your opinion, especially on: