Skip to content

feat(code): railway code — launch a coding agent on a Railway cloud agent VM - #1013

Draft
codyde wants to merge 13 commits into
masterfrom
feat/code-agents
Draft

feat(code): railway code — launch a coding agent on a Railway cloud agent VM#1013
codyde wants to merge 13 commits into
masterfrom
feat/code-agents

Conversation

@codyde

@codyde codyde commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

railway code --codex / --claude / --grok launches a coding agent on a Railway cloud agent VM using your own credentials.

Requires railwayapp/mono#34349 and railwayapp/mono#34350 deployed — the cloud agent API is Internal-only until then.

Why the VM does most of it

cloud-agent-base bakes every harness, and express-agent serve --agents reconciles their config on every boot (MCP servers, hooks, onboarding/trust flags, autonomy posture). So this installs nothing and configures nothing — doing either would fight the reconciler for the same files.

Deleted as dead code: the npm and x.ai installers, AGENT-NO-NPM handling, the codex/claude update snippets, codex trust_level, grok [ui] yolo, the ~/.claude.json onboarding merge, and the bubblewrap install. What's left is credential delivery and the launch.

Credentials

Read client-side, announced, and delivered over ssh stdin into a 0600 file — deliberately not create-time variables, so they never reach the VM spec, an image, an argv, or server-side config. A reused agent is refreshed the same way a fresh one is.

  • codex / grok — copies the local auth.json
  • claude — a claude setup-token grant (or ANTHROPIC_API_KEY), never the local sign-in's rotating refresh token

Lifecycle

Agents are durable and have no idle timeout, so disconnecting sleeps the agent and the next run wakes it, with the disk intact. --keep-awake leaves it running; --rm destroys it.

Reuse is keyed per environment, not a single global slot. When the local pointer is missing (second machine, wiped config) it adopts your sole existing agent rather than silently minting a duplicate that bills forever — scoped with mine: true, without which it could adopt a teammate's box.

Two fixes worth review

  • ~/.claude/settings.json is merged, not overwritten. On an agent VM that file is co-owned — express-agent writes the railway and playwright MCP servers and its hook entries there — so the previous truncating write would have stripped the harness's Railway tools until the next boot. Merged with hooks/mcpServers left to their owner.
  • No cd. The machine spec sets workDir for the workload and every in-VM session (/app); forcing $HOME overrode a platform default and landed outside the dirs express-agent reconciles trust into.

Verification

cargo check/fmt clean, 550/550 tests. schema.json regenerated from the mono branch.

codyde and others added 13 commits August 3, 2026 15:44
…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>
@codyde
codyde force-pushed the feat/code-agents branch from 99bbcfd to 90e731b Compare August 3, 2026 22:57
@codyde codyde changed the title feat: railway code — launch Codex or Claude Code in a Railway sandbox with your own plan feat(code): railway code — launch a coding agent on a Railway cloud agent VM Aug 3, 2026
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.

1 participant