Skip to content

feat: railway code --codex — launch OpenAI Codex in a sandbox with your own sign-in#1006

Closed
codyde wants to merge 11 commits into
masterfrom
feat/code-codex
Closed

feat: railway code --codex — launch OpenAI Codex in a sandbox with your own sign-in#1006
codyde wants to merge 11 commits into
masterfrom
feat/code-codex

Conversation

@codyde

@codyde codyde commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What

New top-level agent launcher: railway code --codex creates (or reuses) a Railway sandbox, injects the user's own Codex sign-in, and drops them into an interactive, authenticated codex session — about 6 seconds end-to-end.

railway code --codex               # sandbox + your local Codex sign-in
railway code --codex --gh          # also inject your GitHub auth (gh auth token)
railway code --codex --new --variable DB_URL=postgres.DATABASE_URL
railway code --codex --idle-timeout 240
railway code --codex -- exec "explain this codebase"

How auth works (and why this shape)

  • Uses the flow OpenAI documents for remote machines/containers: the user's ~/.codex/auth.json is copied into their own sandbox (auth docs, CI/CD guide).
  • Consent-gated: interactive confirm before the file is read (non-interactive requires --yes); the prompt names the GitHub token too when --gh is set.
  • The credential is read client-side and rides ssh stdin into a 0600 file — never argv, never Railway variables, images, or server-side config.
  • --gh reads the live token via gh auth token (correct even with macOS keychain storage), fails fast before a sandbox is spent, and provisions a 0600 token file + GH_TOKEN export + git credential helper — no gh install required in the box.

DX details

  • One connection, one host-key decision per run: provisioning is a single script, and every ssh (provision + interactive launch) multiplexes over one OpenSSH ControlMaster. Relay connections verify against a CLI-owned known-hosts file (~/.railway/known_hosts_relay, accept-new, self-healing) — the user's ~/.ssh/known_hosts is never touched. This works around the relay fleet currently presenting per-instance host keys (7+ observed); the code is commented for revert to strict checking once the relay ships a shared host key / SSH CA.
  • Auto-launch on reconnect: a guarded ~/.profile seed means any interactive railway sandbox ssh into the box drops straight into codex; quitting codex lands in a shell (verified the relay runs bash as a login shell).
  • Sane reuse: repeated runs reuse the active RUNNING/CREATING sandbox instead of minting duplicates; --new forces fresh; disconnect prints reconnect breadcrumbs and the env-scoped sandbox list invocation.
  • Fails with reasons: provisioning captures stderr and distinguishes transient relay blips (retried with backoff) from no-npm / install-failed / host-key states.
  • --variable / --env-file have full sandbox create parity (server-side reference resolution), with a warning when ignored due to reuse.

Validation

  • 10/10 consecutive fresh create→provision→launch→destroy cycles, ~6s each, zero errors (two independent 5-run bursts against production).
  • --gh/--variable verified e2e: variable visible in the box env, token file 0600, api.github.com authenticates, credential helper registered.
  • All 450 existing tests pass; all changes are additive (one internal signature change, both callers updated in-commit).

Notes for reviewers

  • Policy diligence for the auth pattern (OpenAI ToU + Codex docs + precedent review) was done ahead of this PR — the copy flow is vendor-documented; the roadmap alternative is seeding via codex login --device-auth in-sandbox, which this command's UX already points to on abort.
  • The relay host-key inconsistency is a pre-existing infra issue that affects railway ssh/sandbox ssh too; writeup to follow separately.

🤖 Generated with Claude Code

codyde and others added 9 commits July 8, 2026 23:31
…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>
@codyde codyde added the release/minor Author minor release label Jul 9, 2026
codyde and others added 2 commits July 9, 2026 10:25
… 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>
@codyde

codyde commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #1013 — the branch was renamed to feat/code-agents now that it covers both Codex and Claude Code, and the new PR carries the full description. Closing this one.

@codyde codyde closed this Jul 13, 2026
@codyde
codyde deleted the feat/code-codex branch July 13, 2026 18:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/minor Author minor release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant