feat(code): coding agents on cloud agent VMs, and railway ca to drive them - #1013
Merged
Conversation
codyde
force-pushed
the
feat/code-agents
branch
from
August 3, 2026 22:57
99bbcfd to
90e731b
Compare
railway code — launch Codex or Claude Code in a Railway sandbox with your own planrailway code — launch a coding agent on a Railway cloud agent VM
…th your local sign-in New top-level `railway code` agent launcher. `--codex` copies the user's existing ~/.codex/auth.json (the flow OpenAI documents for remote machines and containers) into a fresh or reused sandbox and drops into an interactive codex session over the relay. - consent-gated, client-side read of the credential; it rides ssh stdin into a 0600 file in the sandbox — never argv, Railway variables, or images - seeds COLORTERM + codex folder-trust config (only when absent) - installs @openai/codex in the sandbox when missing (marker-based errors distinguish no-npm / install-failed / relay failure) - reuses the active RUNNING sandbox; --new forces a fresh one; 30m idle cap - ports run_native_ssh_captured (stdin-payload ssh) into ssh::native - create_and_store now returns the sandbox id for reuse by launchers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… real errors Plumbing ssh now captures stderr and retries up to 3x with backoff for transient relay/boot failures. Host-key verification failures fail fast with remediation instead of being retried — that's a security signal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dbox Provisioning seeds a guarded ~/.profile block: plain connects (railway sandbox ssh) run bash as a login shell (verified against a live sandbox), so interactive sessions drop straight into codex. Not exec'd — quitting codex lands in a shell. [ -t 1 ] keeps command/scp sessions out; env guard prevents re-entry; grep guard keeps the seed idempotent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ING boxes - print how to get back in on disconnect (sandbox ssh / code --codex) and the exact env-scoped 'sandbox list' invocation — 'railway sandbox ls' from a differently-linked dir silently shows a different environment - reuse now also matches CREATING sandboxes so a re-run seconds after a launch doesn't mint duplicates - --idle-timeout <minutes> (default 30) controls how long the box survives after disconnect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n script The relay fleet answers with per-instance host keys, so every fresh TCP connection is a new host-key lottery. railway code was making four (seed, inject, install, launch) versus sandbox ssh's one — which is why it failed host-key verification far more often than sandbox ssh against the same sandbox. - provisioning is now ONE script over ONE connection (credential still arrives via stdin, markers still distinguish failure modes) - every ssh in a run shares an OpenSSH ControlMaster (ControlPersist=90s): the provisioning connection is verified once and the interactive launch multiplexes over it — verified the Go relay accepts muxed sessions - run_native_ssh_with_opts / extra_opts on the captured variant let the launcher opt in without changing the shared ssh paths Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rministic Relay connections verify against the CLI's own file (~/.railway/known_hosts_relay, accept-new) and heal it on mismatch — never the user's ~/.ssh/known_hosts. With the relay fleet presenting 7+ per-instance host keys behind one hostname, single-key pinning was both futile (most connections mismatch) and security theater (TOFU re-accept is indistinguishable from MITM). Documented for revert to strict checking once the relay ships a shared host key / SSH CA. Validated: 10/10 consecutive fresh create→provision→launch→destroy cycles, ~6s each (image ships codex preinstalled; install step short-circuits). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h GitHub auth --variable / --env-file ride the same server-side resolution as sandbox create (reference forms like postgres.DATABASE_URL work); a note warns when they're ignored because an existing sandbox is being reused. --gh reads the host token via `gh auth token` (works regardless of where gh stores it — macOS keychain included), fails fast before a sandbox is spent, and provisions the sandbox with a 0600 token file, a GH_TOKEN profile export, and a git credential helper for github.com/gist — no gh install required in the box; gh honors GH_TOKEN natively if present. Token rides ssh stdin over the same multiplexed connection; the consent prompt names it when --gh is set. Verified e2e: fresh sandbox, variable visible in env, token file 0600, api.github.com authenticates, credential helper set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the session The interactive launch exec'd codex, so quitting it tore down the whole ssh session. Now (matching the ~/.profile autostart behavior) codex runs un-exec'd and hands off to a login shell on exit; the exported RAILWAY_CODE_AUTOSTARTED guard keeps that shell's profile from relaunching codex on top of the user. Scripted runs (-- args) keep the exec-and-exit behavior so pipelines don't hang on a trailing shell. Verified live: Ctrl-C out of codex → shell responds → exit closes the session; `-- --version` still exits cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ning The sandbox image doesn't ship distro bubblewrap yet, so codex warns at startup and falls back to its bundled copy (cosmetic — sandboxing works either way). Provisioning now apt-installs it when absent (~8s on fresh boxes); the command -v guard makes this a free no-op once the image ships bwrap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…us updates
- railway code --claude: mints a long-lived token by running `claude
setup-token` invisibly under script(1)'s pty (the browser round-trip
stays interactive; nothing — least of all the token — renders in the
terminal), with CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY env
passthrough and a masked paste prompt as fallbacks. The token rides
ssh stdin into a 0600 env file sourced at launch and on reconnect
- onboarding disable MERGES into the image's pre-baked ~/.claude.json
via jq (node fallback) — the write-when-absent seed never fired
because the image build stamps that file; local ~/.claude/settings.json
is mirrored into the sandbox when present
- credential copy is announced instead of consent-prompted (drops --yes)
- quiet launch output: one-line sandbox create (CreateReport::Quiet),
ensure_ssh_key_quiet drops the key announce for plumbing connections
- terminal-state reset after the agent TUI exits (remote and local)
kills the kitty keyboard protocol leak ("9;5:3u") on unclean exits
- agents refresh synchronously during provisioning: codex npm-installs
only on a real registry version gap (~2s check when current), claude
update no-ops when current — a background update always lost the race
against launch on fresh sandboxes, greeting users with codex's
update-available banner
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`railway code` launches one agent and hands over the terminal. `railway ca`
is the same launcher with somewhere to stand first: a prompt that says what
to build, a tree of every project you can reach, and the sessions running on
each agent — with the session itself in a pane beside them rather than a
screen you have to leave for.
Both commands read the same preferences (~/.railway/agent-prefs.json) and
take the same launch flags, so the split is only about whether you want to
browse. `railway ca` with any flag, or without a terminal, is `railway code`.
The screen:
- A prompt aimed at a default project — chosen in setup, changed with ^t.
New Session puts work on an agent you already have (asking which, when
there is more than one); New Cloud Agent makes one; Manage opens the tree.
- Sessions are pty-backed ssh into the relay's durable sessions, emulated
with vt100 and drawn in the right-hand pane. Several run at once, ⌥f gives
one the whole screen, and shift+enter hands it the real terminal and comes
back to the same session.
- Lifecycle without leaving: sleep, wake, delete behind a confirmation, end
a session, copy its ssh command.
- First run walks through setup in the same card style — where agents live,
which harness, whether to bring your skills, and a theme.
Supporting changes:
- `code::launch` splits into `prepare`/`run_session`/`sleep_agent` behind a
`Progress` trait, so the launch pipeline can report into a TUI rather than
only into spinners.
- Skills sync packs the local skills directory and unpacks it on the VM
add-only: it never takes a name the agent already has, and never ships
Railway's own, which are baked into the image.
- `relay_destination`/`relay_port_args` are exposed because a relay target is
a username rather than a hostname, and the pty session builds its own
invocation.
- Detaching from a durable session leaves the remote agent's mouse tracking
switched on. `clear_mouse_tracking` turns it off on the way out, so pointer
movement stops arriving at the shell prompt as `35;21;32M`.
- `railway logout` drops the cached Claude setup-token.
A wake now ends at "running" rather than at "accepted": the platform reports
the old status until the VM has booted, and clearing the label on the first
answer made a wake that was working look like one that had rolled back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codyde
force-pushed
the
feat/code-agents
branch
from
August 7, 2026 19:54
90e731b to
11643d7
Compare
railway code — launch a coding agent on a Railway cloud agent VMrailway ca to drive them
Seven pty-backed tests assert on a mode-setting escape sequence making it through the pty — the alternate screen, mouse reporting. Windows' ConPTY interprets those for itself rather than passing them along, so the emulator never sees the mode change and the fixture cannot set up. Plain text round-trips fine, which is why the rest run everywhere; these are `#[cfg(unix)]` with the reason recorded at each one. `display_path` also hardcoded `/` after the `~`, which on Windows produced `~/.claude\skills` — half of each convention. Uses the platform separator now, since the string is shown to someone looking at their own filesystem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`railway code` asked its own question. Where an agent goes is the same question
`railway ca` answers, so it now answers it the same way:
1. `-p`/`-e` win outright — the caller said it.
2. The default project from `agent-prefs.json`, which is what "where do
agents go" means. It beats the linked directory: a link is about deploys,
and running `railway code` inside some service's checkout should not put
an agent there.
3. The linked directory, which is a worse answer than a configured default
but a better one than a question.
4. Nothing to go on, and a terminal to ask in: run `railway ca setup`. The
old workspace → project → environment picker answered one launch; setup
answers every launch after it, and asks the same question. Skipping the
project step there falls back to the picker so the launch still happens.
Within that environment the agent is chosen as before — the remembered one,
then your sole existing one, then a fresh one — so a second `railway code` in
the default project lands on the agent already there rather than billing for a
new box.
The precedence is a pure function over (flags, configured, linked) so the order
is checked by tests rather than by reading it. One of those tests pins the
property that keeps this safe: a TUI launch always carries an explicit target,
so neither setup nor any other prompt can be drawn underneath a frame.
Also retargets the multi-agent error at `railway ca`, which can now pick one —
the advice to use the dashboard predates it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codyde
marked this pull request as ready for review
August 7, 2026 20:51
- The saved-default line now names the preferences file it came from
(~/.railway/agent-prefs.json) and how to change it.
- Drop --gh and its VM-side provisioning: the GitHub token ships in the
agent config by default now, so copying the host's token in is
redundant. The remote-side ~/.gh-token sourcing stays so agents
provisioned with --gh keep working.
- Drop the --refresh-auth-only-applies-to-claude note; the flag is
simply a no-op for harnesses that read credentials from disk.
- Replace "Provisioning {harness}" and the skills-already-synced note
with a single "Finalizing Configuration..." step; the skills sync
failure warning stays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* origin/master: feat: shared container-exec primitive + Patroni probe controller (#1048) chore: regenerate schema.json from the Public subgraph introspection (#1047) chore: Release railwayapp version 5.31.1 test: scripted-backboard testkit + first GraphQL-layer tests (#1046) # Conflicts: # src/commands/sandbox.rs # src/gql/schema.json
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.
Coding agents on Railway cloud agent VMs, with your own credentials.
railway cais a TUI to drive them;railway codeis the same launcher without one.Requires railwayapp/mono#34349 and railwayapp/mono#34350 deployed — the cloud agent API is Internal-only until then.
Commands
railway carailway ca setup~/.railway/agent-prefs.jsonrailway ca setup --showrailway ca start [flags]railway code --claude | --codex | --grokrailway code -- <args>railway cawith any launch flag, or with no terminal, behaves asrailway code. Both take the same flags, read the same preferences, and resolve a target the same way.Launch flags
--claude/--codex/--grok— harness; the saved default applies when omitted (RAILWAY_CA_AGENToverrides for one run)--new— force a new VM instead of reusing this environment's agent--rm— destroy the agent on exit;--keep-awake— leave it running instead of sleeping--gh— forward GitHub auth;--refresh-auth— re-deliver credentials to a reused agent--variable K=V/--env-file <path>— environment for the session-p <project>/-e <environment>— target;--name <name>names a newly created agentrailway ca— menu^tfrom the same card (which updates the default, not just this run)⌥tcycles, persisted on exitrailway ca— managevt100, drawn in the right pane; several run at onceenterconnect ·shift+esc/^]release the keyboard ·nnew session ·xend sessionssleep ·wwake ·ddelete (confirmed) ·ccopy ssh command ·rrefresh ·?full key list⌥fmaximize the pane, again to restore ·shift+enterhand over the real terminal and return to the same sessionshift+pgup/pgdnforward to the agent when it owns the mouse, else the emulator's scrollbackshift+dragtakes one back to selectrunning, not ataccepted— the platform reports the old status until the VM boots, so the environment is re-asked every 1.5s until it arrives (3 min cap). Clearing the label on the first answer is what made a working wake look rolled back.RAILWAY_DURABLE_SESSION_NAMEin/proc/*/environand sendsTERM— there is no API for it, and neverpkill -fSkills sync
.git/node_modules/.venv/__pycache__/target,.env*, symlinks, empty skillsrailway codeTarget resolution, shared with
railway ca:-p/-ewin outrightagent-prefs.json— beats the linked directory, because a link is about deploys and shouldn't put an agent in some service's checkoutrailway ca setup, so the answer is remembered rather than asked again next launchWithin that environment: the remembered agent, then your sole existing one, then a fresh one — so a second
railway codelands on the agent already there rather than billing for a new box. A TUI launch always carries an explicit target, which is what keeps any prompt from being drawn underneath a frame.Everything else:
cloud-agent-basebakes every harness andexpress-agent serve --agentsreconciles their config each boot. Removed as dead code: npm/x.ai installers,AGENT-NO-NPM, codex/claude update snippets, codextrust_level, grok[ui] yolo, the~/.claude.jsononboarding merge, bubblewrap install.auth.json; claude uses aclaude setup-tokengrant orANTHROPIC_API_KEY, never the local sign-in's rotating refresh token.mine: trueso it can't adopt a teammate's.~/.claude/settings.jsonmerged, not overwritten — express-agent co-owns that file, so the previous truncating write stripped the harness's Railway MCP tools until next boot.cd— the machine spec setsworkDir(/app) for the workload and every session; forcing$HOMElanded outside the dirs express-agent reconciles trust into.railway logoutclears the cached Claude setup-token.Structure
code::launchsplits intoprepare/run_session/sleep_agentbehind aProgresstrait, so one pipeline reports into either the TUI's loading pane or spinnerstui/app.rsis state and keys only — no rendering, no I/O; every key yields state plus at most oneEffect, which is what makes navigation testable without a terminal or networkrelay_destination/relay_port_argsexposed: a relay target is a username, not a hostname, and the pty session builds its own invocationclear_mouse_trackingafter any interactive relay session — detaching from a durable session leaves the remote agent's mouse tracking on, and every pointer movement then arrives at the shell prompt as35;21;32Mvt100pinned at 0.15 (0.16 conflicts with ratatui's pinnedunicode-width): caps scrollback at one screenful, and hides OSC 8 hyperlinks from link detectionKnown gaps
cloudAgents(workspaceId:)would replace it and enable live status.ca setup— needs a backboard schema change.Verification
781 tests,
cargo fmt --all -- --checkandcargo clippy --all-targets --all-featuresclean. Seven pty tests are#[cfg(unix)]: they need a mode-setting escape sequence to survive the pty, and ConPTY interprets those itself. Driven against prod: launch, reattach, sleep, wake, delete, skills sync, full-screen handoff.