Skip to content

docs: propose Rainier × NoKV workspace continuity - #17

Open
wchwawa wants to merge 1 commit into
tokencanopy:mainfrom
wchwawa:feat/workspace-continuity
Open

docs: propose Rainier × NoKV workspace continuity#17
wchwawa wants to merge 1 commit into
tokencanopy:mainfrom
wchwawa:feat/workspace-continuity

Conversation

@wchwawa

@wchwawa wchwawa commented Aug 31, 2026

Copy link
Copy Markdown

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 /workspace rules, rather than accept host filesystem paths.

Problem

Rainier already preserves /workspace across 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

  • keep Postgres as Rainier's sole control-state authority;
  • let the runtime driver safely export and install workspace data;
  • place a replaceable checkpoint/history provider behind that boundary;
  • prove local cross-runner continuity first;
  • evaluate NoKV asynchronously in shadow mode before any opt-in restore.

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

  • NoKV adds metadata and object-storage operations beside Postgres;
  • the Go integration surface and real driver seam still need design;
  • checkpointing adds I/O, latency, and storage cost;
  • NoKV HA and long-horizon recovery require further qualification;
  • this belongs to an optional post-v0 path, not Rainier's current critical path.

Verification

  • reviewed against 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:

  1. the runtime-driver versus checkpoint-provider boundary;
  2. keeping NoKV optional and shadow-first after v0;
  3. whether the next contribution should be a small local export/install design and E2E before any NoKV adapter.

Signed-off-by: wchwawa <wch19961116@gmail.com>
@wchwawa

wchwawa commented Aug 31, 2026

Copy link
Copy Markdown
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.

@jiashuoz

Copy link
Copy Markdown
Member

Quick summary after comparing this RFC with the existing checkpointing plan and reviewing NoKV source at main@58ecc0f:

  • NoKV has useful immutable-publication, operation-ID-bound retry, reference-tracking, and GC machinery. However, production recovery, HA, and GC behavior still need qualification.
  • A NoKV snapshot is a leased MVCC read version of its path namespace and requires a sealed commit head; it is not a capture of a live runtime volume. A durable Rainier checkpoint would need to publish and seal the intended state, then retain the NoKV commit ID.
  • NoKV deliberately does not preserve several filesystem properties Rainier may need: modes, symlinks, hardlinks, empty directories, xattrs/ACLs, sparse files, and special nodes. Rainier therefore still needs a driver-owned export/install format with an explicit filesystem-fidelity contract.
  • The existing local-first plan remains the safer initial path: driver export/install plus shared round-trip tests, followed in rainier-cloud by a Postgres catalog and immutable archives/manifests in GCS.
  • NoKV is best evaluated initially as a non-authoritative shadow sink consuming that same sealed export. Promotion should require cross-runner restore tests, changed/deleted-state coverage, archive-size and streaming limits, restore-latency targets, and a durable metadata-recovery story beyond the default local Holt store.

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>
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.

2 participants