Repository navigation
codeaf devbox: an always-on computer on your own Railway account - #2081
Merged
Merged
Conversation
…te key A dev box is created by `codeaf devbox up`, and the laptop that creates it generates the box's remote host key and admission book itself, so the box can be reached with `codeaf chat --at <name>` without a second approval and without widening the identity grant (one code still never gives both). DevboxRemote produces those files with the same readers serve uses, and returns the pinned machine record. The machine book now keeps an optional alias beside the key-derived relay name; Reach.Open resolves the alias to the pinned record, and `codeaf devices` prints the alias when there is one. A status probe must be able to give up on a box that is not serving yet: a cancelled or deadlined context now closes the stream during the handshake, and the raw relay upgrade honours the caller's deadline instead of always waiting out its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A computer that only runs `codeaf serve` never received the identity's saved provider keys, preferences or shared memories, because only the terminal surface ran the home-state loop; every new `--at` chat on such a computer stopped on a missing key. A dev box only serves, so it needs them. serve keeps that state in step only when started with CODEAF_SERVE_SYNC=1, which the dev box's boot script sets. UNSET, SERVE BEHAVES EXACTLY AS IT ALWAYS HAS: runServe gains one deferred call that starts nothing. When asked, it runs syncsetup.RunHomeState (one round at start and one per sync interval, without starting a cluster member; quiet, unpaired, unavailable and removed homes end it silently, other failures are said once each) and a presence worker on syncsetup's gated clients that keeps the device record fresh for `devbox status` and stops when the identity, device or relay binding changes. Both start through guard.Go and are joined before serve returns. The engine cached its first profile load forever, including a failure, so an engine started before the keys arrived kept refusing after they had. It now caches only a successful load; success paths are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`codeaf devbox up <name>` writes a readable Terraform folder the person owns (default <codeaf home>/devbox/<name>/), shows the plan, and on one confirmation creates one private project, one service and one persistent volume on their Railway account with their RAILWAY_TOKEN. Without a token it says in one sentence where to create one and creates nothing. Railway's provider triggers several deployments within seconds and a redeploy can reuse a snapshot without the variables, which killed a box mid-pair on the first real run. So up applies in three guarded steps: create the box with no secrets and let Railway settle; send a one-time pairing code plus the box's seeded remote key and admission book, where the plan may change only the variable collection; and, once the pinned remote key answers, remove those one-time values from Railway and from the state, deleting the state backup that still held them. The remote key is generated once per up and kept across up to three codes, and a reachability probe runs alongside the join so a lost acknowledgement is not mistaken for a failure. "is up" is said only after a successful connection. Once the box answers, up also sends this computer's saved keys once, so the first `--at` chat from a laptop that was a fleet of one has them. The box boots from boot.sh on a stock image. Its first install uses this computer's own release when they share a channel, because the installer's GitHub API lookup is rate limited per address and Railway's egress is shared; later boots refresh to the channel's newest build and keep the earlier one on failure. It waits idle until it has a code, joins once (a marker on the volume keeps redeploys from pairing again), pins its sync relay to the laptop's (each release channel has its own), seeds its remote files atomically and independently, and serves with CODEAF_SERVE_SYNC=1. `status` probes the box through its pinned key and reports up or down, region, project and last seen. `down` destroys every resource, then revokes the joined device by the id saved at up, and forgets the alias. Failure receipts name the exact command, with --dir for a non-default folder. Nothing changes for anyone who never runs devbox. The AWS module is `terraform validate`d only; it has never been applied and the command does not offer it. The manual's new page answers "can I run codeaf on my own server" and "set up a dev box". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plumbing The registry law reads every CODEAF_ name spelled in Go and wants each one to be a settings row or operator plumbing. The three CODEAF_DEVBOX_ setup values are service variables for the box's boot script that codeaf only looks for in Terraform state to remove them, and CODEAF_SERVE_SYNC is one serve process's posture set by that script; none is a preference a person keeps. Co-Authored-By: Claude Opus 5.5 <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.
codeaf devbox up <name>gives you an always-on computer on your own Railway account. The box is paired into your identity, listed in/devicesand Settings → Devices, and reachable withcodeaf chat --at <name>.codeaf devbox statusandcodeaf devbox downinspect it and remove it. The Terraform is written to a folder you own (default<codeaf home>/devbox/<name>/), so you can read every resource it creates.Nothing changes for anyone who never runs
devbox.codeaf servestarts its new home-state and presence workers only whenCODEAF_SERVE_SYNC=1is set, and only the box's boot script sets it. OtherwiserunServegains one deferred call that starts nothing. The pairing changes only take effect for a machine record that has an alias, which onlydevbox upwrites. Probes can now be cancelled mid-handshake. The details are under "Touches pairing and serve" below.What changed
codeaf devbox up | status | down(internal/devbox,cmd/codeaf/devbox.go). It creates one private project, one service and one persistent volume through the Railway Terraform provider. The box boots from a stock image, installs the released codeaf for its channel the way the public installer does, joins with a one-time pairing code and serves.Up runs three guarded applies. On the first real run, Railway's provider triggered four deployments within seconds. A redeploy reused a snapshot without the variables, and it killed the deployment holding the code while it was pairing. So:
railway_variable_collection.box.The remote key is generated once per
upand reused across up to three codes. A reachability probe runs during the join, so a lost acknowledgement is not mistaken for a failure. Up prints "is up" only after a successful connection.The first chat works right away. Once the box answers,
uppushes this computer's saved keys once (one bounded home-state round). Without that, a laptop that had been a fleet of one has never sent its keys anywhere, and the first--atchat stopped on a missing key.One relay per channel (One relay per release channel, deployed by CI when relay code changes #2079). Joining a box doesn't save a relay when the laptop uses its own build's default. So the box's boot script saves the laptop's relay before the first join, the same way a pairing that named one would.
GitHub's API limit. On a real run, a box's first install failed for seven minutes with "GitHub's API rate limit for this address is used up", because Railway's egress addresses are shared. The first install now uses this computer's own release tag (
VERSION=<tag>, no API call) when both share a channel. Later boots refresh to the channel's newest build and keep the earlier one if that fails.The identity grant is unchanged. One code still never grants both chats and remote access:
internal/pair/grant.goandjoin.goare untouched. Remote access comes from a host key and admission book that the laptop generates and Terraform seeds on the box, so only the creating laptop is admitted.downdestroys every resource first, then revokes the joined device by the id saved atup, so a renamed box or a reused name is handled. Failure receipts name the exact command, with--dirwhen the folder is not the default.Manual:
internal/manual/chat/a-dev-box-on-your-own-railway.mdanswers "can I run codeaf on my own server" and "set up a dev box". It covers who pays (you pay Railway), what the box holds (the whole identity, including saved provider keys, like any paired computer), and the AWS state.Touches pairing and serve: what changes for everyone else
codeaf serve: unchanged unlessCODEAF_SERVE_SYNC=1is set.TestServeKeepsStateOnlyWhenADevBoxAskspins this: only the value1opts in.pair.Book: an optionalaliasfield, written only bydevbox up. Lookup and dedupe change only for records that have one.codeaf devicesprints the alias for those records.pair.Reach.Openandrelayraw upgrade: a cancelled or deadlined context now closes the stream mid-handshake, and the deadline is the earlier of the caller's and the built-in one. Callers that never cancel see no difference.AWS is unproven
internal/devbox/terraform/awshas passedterraform validateand nothing more. It has never been applied, because no AWS credentials were available, andcodeaf devboxdoes not offer it. The manual says the same.Validation contract, proven by hand on real Railway
Everything was run against a real Railway account from throwaway homes, on binaries built from this branch, including dev builds that use the dev relay. Test projects were named
codeaf-l2-test-<n>, and every one was destroyed.upon a fresh name: created exactly one project, one service and one volume. The fastest run took 73 s to "is up", including the secret clear. The box was listed as joined incodeaf devices, TUI/devicesand Settings → Devices.chat --at <name>: ran a turn on the box. Every model key on the test machine was expired, so the repository's scripted model stub (test/remote/stub) ran on the box. Itsbash pwdtool call answered/data/home/work, the box's own work folder. From a brand-new, never-paired laptop on a dev build,devbox upfollowed directly bychat --atworked with nothing opened in between, becauseuppushed the keys.down: destroyed all 3 resources on four joined boxes. Each device shows as removed, and the Railway projects listing shows nocodeaf-l2-*project.status:<name>: up; region …; project codeaf-<name>; last seen …, through a pinned-key probe.internal/manual/chat_test.goreach the new page, and the packed corpus answers the same questions (internal/manual/devbox_test.go).After step 3 of
up, Railway's variables andterraform.tfstatewere checked: neither heldCODEAF_DEVBOX_CODE,CODEAF_DEVBOX_REMOTE_KEYorCODEAF_DEVBOX_REMOTE_DEVICES, and no state backup was left. On a dev-build laptop, the box saved the laptop's dev relay as its sync relay and served through it.Tests
The new tests use a fake Terraform executable, a fake Railway GraphQL server and the in-process relay fixtures. No test reaches the network. They cover:
boot.sh: idle without a code, a failed pair, a failed install, atomic and independent seeding, the relay pin, the pinned first install and its unpinned retry;up → down → up → downthrough the real removal wiring;Three tests outside this lane fail on
origin/devtoo, under the same conditions:TestHostedTakeBackReachesAcquireWithoutSharedUploadsOrReleaseDelay(a 1 s timing ceiling) andTestScopesCountsEveryRelayRequest/TestPublicTakeRestoresEveryWorkspaceSetRoot(take path). They are not caused by this change.Mac check (stability)
Recheck after the head moved to 7fc2ae8 (one commit registering the new environment names as plumbing):
🤖 Generated with Claude Code