Self-hostable infrastructure for running your coding agents — Claude Code, Codex, Gemini CLI, and friends — in your own cloud, while they feel like local terminal sessions. Sessions survive laptop sleep and network changes; attach shows the agent's real TUI from any machine; your subscriptions and your GitHub identity stay yours.
Active Development & Hosted Cloud Service Coming Soon: Project Rainier is under active development. Alongside self-hosting, a fully managed hosted cloud service is launching soon. We are actively onboarding pilot users, design partners, and collaborators — reach out to josh@tokencanopy.com for early access.
Autonomous coding agents (Claude Code, Codex, Gemini CLI, and others) are transforming software engineering, but running them on personal machines introduces significant friction:
- Laptops sleep, agent runs break: Long-running agent tasks abort when a developer closes their laptop lid, walks away, or changes Wi-Fi networks.
- Remote shouldn't mean headless: Traditional remote containers or batch jobs strip away the rich interactive terminal UI, making real-time steering and feedback difficult.
- Credential sprawl & security risks: Pasting personal GitHub PATs or cloud keys into
.git/configor container environments creates security liabilities; unmonitored container networking risks accidental data exfiltration. - Local resource exhaustion: Running multiple parallel agent sessions pegs CPU/memory, burns battery, and litters local working copies with half-finished branches and scratch files.
Rainier solves this by providing persistent, secure cloud execution with a local-feeling terminal:
- Continuous Sessions: Agents run on cloud sandboxes that survive laptop sleep, network drops, and machine transitions. Reconnecting instantly restores the interactive TUI.
- Zero Credential Exposure: GitHub tokens and secrets are vaulted in the control plane and minted per-git-operation via an in-sandbox helper — never written to disk or container volumes.
- Locked-Down Egress: Built-in network allowlisting restricts external network calls to approved endpoints and registries.
- Parallel Scale: Offload multiple agent sessions simultaneously without bogging down your workstation.
Rainier is currently at v0 (terminal happy path). One controld (Postgres-backed REST + WebSocket API, GitHub identity, least-loaded placement, credential vault) fronts N runnerd VMs that dial it outbound, and the rainier CLI drives the whole fleet. Sessions clone your repositories at boot and push back as you.
- Deployment Guide:
docs/deploy-gce.md - Architecture Specs:
Since the Wave 4 extraction, controld is composed rather than monolithic:
the portable session lifecycle, in-pool placement, runner reconciliation, and
attach/workspace orchestration live once, in
controlapp, behind the frozen public
control contract. internal/controld supplies the
adapters that contract needs — the store as workspace-keyed repositories,
GitHub-login roles as the authorizer, the vault as launch material, the runner
websocket as the transport, dial-back pairing as the attach broker — and keeps
only what is genuinely host-specific: HTTP/WebSocket handling, GitHub login,
secrets, and the sandbox's upward credential request. Rainier Cloud composes
the same controlapp with its own adapters. Three more public packages
carry what a host needs beyond the services: runnerplane
(the runner WebSocket endpoint, generation fencing, dispatch correlation),
attachplane (the terminal attach handshake and
splice), and v0wire (the /v0/ JSON shapes and the
status table), each behind a small host interface that self-hosted
controld implements exactly as a hosted cell does.
make build
# Point the CLI at your controld and log in with your GitHub identity.
# --from-gh borrows the token from the `gh` CLI; --token <t> and
# --client-id <id> (device flow) are the alternatives.
bin/rainier login --from-gh --server http://rainier-1:9090
bin/rainier new --name box1 --image rainier-session:latest # creates, then attaches
bin/rainier ls # id, name, env, state, runner, reachable, age
bin/rainier attach box1 # resumes if needed; Ctrl-] detaches
bin/rainier suspend box1 && bin/rainier resume box1
bin/rainier rm box1A hosted rainier logs in through the browser instead of a GitHub token:
bin/rainier login --cloud https://edge.example.test # prints (and opens) a URL
bin/rainier context list # every server this CLI knows
bin/rainier workspace use ws_example # when the account has severallogin --cloud starts a login attempt, prints the URL you finish signing in
at, polls until the browser half is done, and stores the result as a context
named after the edge host. A context is one server plus the credentials for it;
context use NAME switches, context current says which one is live, and every
other command talks to whichever is current. login --server (the GitHub login
above) writes the default context unless --context NAME names another, so a
self-hosted controld and a hosted edge live side by side in one config.
A hosted context is scoped to one workspace, sent as Rainier-Workspace on
every request: a login that finds exactly one workspace adopts it, and
workspace use <id> chooses when there are several. Hosted access tokens are
short-lived — the CLI refreshes them silently and re-saves the rotated pair, and
only says "log in again" when that is genuinely the remaining step. Set
RAINIER_NO_BROWSER=1 on a headless box to print the URL without opening it.
rainier new attaches immediately by default so you watch the agent boot;
--detach opts out. rainier attach is state-aware: a suspended session is
resumed first, and an established viewer reconnects after transient network or
control-plane interruptions from the last terminal sequence it rendered.
Names are per-owner, so <id|name> takes either.
An environment is the reusable template a session starts from — a base image, a setup script, an egress allowlist, and the team secrets its sessions get as environment variables:
printf %s "$GH_PAT" | bin/rainier secret set GH_TOKEN # write-only; stdin keeps it out of your history
bin/rainier env create dev --image node:22 \
--setup-file ./setup.sh --secret-ref GH_TOKEN \
--egress registry.npmjs.org,github.com
bin/rainier env ls # name, id, image, cached
bin/rainier env create gpu-dev --image node:22 --capability gpu # only lands on a runner that announced gpu
bin/rainier new --name box2 --env dev # ...starts from itThe first session on an environment runs the setup script live — attach and
you watch it — and its image is then snapshot-cached for the next one;
editing the image or the script invalidates that cache without touching any
running session. --image/--egress on new override the environment for
that one session. Setup scripts should install into image-visible paths —
$HOME, or /opt/rainier-env on the stock session image, which is already on
PATH — because that is what the snapshot carries forward, while /workspace
is a per-session volume the snapshot excludes. Details, including the one
hardening flag a first build trades away and why /usr/local is not the
install prefix, in docs/deploy-gce.md §7.
Give an environment a github connector and its sessions arrive with the code already checked out, on a branch of their own, able to push:
bin/rainier login --from-gh --server http://rainier-1:9090 # asks for `repo`
bin/rainier creds # provider, status, scopes
bin/rainier env create app --image node:22 \
--connector-json '{"type":"github","repo":"acme/app"}' \
--init-file ./init.sh # runs after the clone, on every boot
bin/rainier new --env app --name app1 # /workspace/app on branch rainier/app1
bin/rainier diff app1 # per repo: this branch vs the base
bin/rainier push ./notes app1:/workspace/notes # and `pull` the other wayCommits from inside a session are the human's: user.name is your GitHub
login and user.email your GitHub noreply address. The token behind them is
sealed in controld and minted per git operation through an in-sandbox
credential helper — it is never written to .git/config, an environment
variable, or the workspace volume. When it goes stale, one failed operation is
enough: rainier creds reads needs_refresh and both git and the API say
rainier login --refresh github. setup is the cacheable, pre-clone half
(toolchains); init is the per-boot, post-clone half (npm ci and friends),
and it runs on cache hits too. docs/deploy-gce.md §8
has the whole story.
Locally, make e2e brings the whole stack up on your own machine (Postgres in
docker; controld, egressd and a dial-mode runnerd on the host, with runnerd
driving real containers) and drives that same CLI flow end to end, secrets and
environments included — plus, when gh is authenticated with repo and
delete_repo, a real clone/commit/push against a throwaway private repo it
creates and deletes. The dress rehearsal for a real deploy.
| Component | Where | Talks to |
|---|---|---|
rainier |
your laptop | controld (HTTPS/WSS) |
controld |
one VM | Postgres; accepts runner and client connections |
runnerd |
each runner VM | dials controld outbound; drives docker |
egressd |
each runner VM | the session allowlist proxy (egress R4) |
sessiond |
inside each session container | dials its runnerd outbound |
Reachability is outbound-only in one direction (spec rule 3): sessiond → runnerd → controld, and clients talk only to controld. Nothing dials into a runner.
runnerctl and rattach are dev tools, not the product surface: they
drive one runnerd's local HTTP API directly, bypassing the control plane
(no identity, no placement, no durable state). They stay in-tree for
debugging a single box — rainier is the CLI to use.
go test ./... # unit + contract suites (no services needed)
go test ./internal/e2e/ -race # in-process e2e scenes: chaos, environments, git
make e2e # full stack on docker, driven by the real CLI
./scripts/egress-check.sh # egress R4 acceptance (exit 3 = skipped on VM-backed docker)The store contract suite runs against memstore by default and against
Postgres when docker is available; the e2e scenes take
RAINIER_TEST_PG_DSN=postgres://… to run the same scenarios against pgstore.
After make build and rainier login, the developer benchmark drives the real
configured fleet with sequential synthetic scratch sessions:
bin/rainier-latency --rainier bin/rainier --samples 10
bin/rainier-latency --rainier bin/rainier --samples 10 --coldIt performs one unreported warm-up by default, emits JSON Lines observations
and R-7 percentile summaries, and removes every session it creates. Output
contains no session identifiers, terminal contents, user identity, or runner
names. The warmed-fleet targets are p95 under 1.5 s for create-to-usable, 200 ms
for attach by id, 225 ms by name, and 75 ms for terminal interaction RTT.
These exclude image pulls, environment setup, agent CLI boot, burst load, and
geographic network differences; small --samples values make p95 directional.
A managed hosted cloud service of Project Rainier is launching soon. We are actively welcoming pilot users, design partners, and collaborators:
- Engineering teams looking to run persistent, off-laptop agent environments.
- Developers running multi-agent workflows across repositories without local resource drain.
- Organizations needing safe agent sandboxes with vaulted credentials and egress filtering.
To join the pilot program or discuss partnerships, email josh@tokencanopy.com.
