Skip to content

feat(sandbox): fork, templates, and --variable/--env-file on create#933

Merged
codyde merged 7 commits into
masterfrom
feat/sandbox-fork-variables
Jun 4, 2026
Merged

feat(sandbox): fork, templates, and --variable/--env-file on create#933
codyde merged 7 commits into
masterfrom
feat/sandbox-fork-variables

Conversation

@codyde
Copy link
Copy Markdown
Collaborator

@codyde codyde commented Jun 4, 2026

What shipped

Expands the railway sandbox namespace (#925) with forking, templates, and variable seeding, riding backend support that already landed in mono (sandboxCreate.sourceSandboxId railwayapp/mono#30565, sandboxCreate.variables railwayapp/mono#30635, sandboxTemplateBuild).

railway sandbox fork [ID]

  • Forks a running sandbox's root volume into a new sandbox; defaults to the active sandbox when no ID is given (positional or --id, mirroring destroy)
  • The fork becomes the active ref, so a bare railway sandbox ssh / exec immediately targets it

railway sandbox template build (aliases: create, new)

  • Builds a template from repeatable -c '<shell command>' instructions via sandboxTemplateBuild; --wait polls the status query until READY/FAILED
  • Templates are content-addressed server-side (id = sha256 of the recipe, ~7-day snapshot cache) — re-running an identical build is an instant cache hit ("ready (cached)")
  • template status <id-or-name> and template list round out the surface
  • railway sandbox create --template <name-or-id>: sandboxCreate needs the full recipe (not just the hash), so the CLI stores built recipes locally in config.json with an optional --name handle and resends the instructions on create

--private-network on create and fork

  • Maps to sandboxCreate.networkIsolation: PRIVATE; default stays ISOLATED (field omitted)
  • First production caller of the PRIVATE path — verified live: privnet DNS resolves and a psql query against postgres.railway.internal succeeds from both a created and a forked sandbox (--variable DB=Postgres.DATABASE_URL + --private-network is the combo that makes resolved internal URLs actually usable)

--variable KEY=VALUE on create and fork

  • Repeatable and comma-separable: --variable FOO=bar,BAZ=qux. A comma only splits when every segment carries its own =, so values containing commas (ALLOWED=a.com,b.com) stay intact
  • Values may use Railway references, resolved server-side at create time:
    • bare: DB=Postgres.DATABASE_URL (auto-wrapped to ${{Postgres.DATABASE_URL}})
    • pre-wrapped: DB=${{Postgres.DATABASE_URL}} (passes through verbatim)
    • embedded: URL=http://${{svc.HOST}}:8080
  • Auto-wrap is strict (name.UPPER_SNAKE) so plain values like 1.5 or example.com are never mangled; the unambiguous shared. namespace accepts any case (shared.char)

--env-file PATH on create and fork

  • Repeatable; dotenv-style parsing (comments, blank lines, export prefix, single/double-quoted values, trailing # comments on unquoted values)
  • References inside files resolve the same way; --variable flags override file entries on key collision

fix(ssh): env-aware relay host (dev-mode Permission denied bug)

  • The SSH relay was hardcoded to ssh.railway.com while backboard URLs follow RAILWAY_ENV — dev-mode CLIs registered keys against the develop backboard but dialed the prod relay → Permission denied (publickey) (reported by Pierre in raildev)
  • Now mirrors backboard's controllers/ssh mapping: dev → ssh.railway-develop.com -p 2222; staging falls through to prod exactly like backboard's IS_DEV-only branch. Covers native ssh, tmux, the generated ~/.ssh/config block, and volume SFTP
  • Prod is byte-identical(ssh.railway.com, None) yields the same target string, no extra args, same config block (existing tests pass unmodified); prod smoke-tested (create → ssh → destroy)

Telemetry

  • Command telemetry walks nested subcommand levels: sandbox template build now reports sub_command: "template:build" (previously just "template"). Heads-up for dashboard owners: other nested commands get richer values too (keys:add, files:pull) — exact-match filters on the old single-level names should become prefix matches
  • sandbox ssh emits the same stage-tagged failure events as railway ssh (tel.rs generalized): ssh_resolve_target / ssh_key_setup / ssh_session / ssh_exit_nonzero under command: "sandbox"
  • (Server-side utilization was already covered: sandbox mutations are tagged source: cli via user-agent, with fork/template metric tags and template cache hit/miss analytics)

Misc

  • Experimental warning printed on every railway sandbox invocation (stderr only — --json stdout stays clean): "Railway sandboxes are experimental and APIs may change or break during testing."
  • src/gql/schema.json refreshed from prod introspection (own commit; large diff is accumulated drift, only the sandbox types are load-bearing here)

Testing

  • 16 unit tests covering the comma heuristic, auto-wrap edge cases (1.5, example.com, a.b.C, pre-wrapped, embedded, shared. lowercase), dotenv parsing, and file/flag precedence
  • Full live E2E against production (raildesk project):
    • create with all variable forms at once (env-file + repeated + comma + bare/wrapped/embedded refs + shared lowercase), exec exit-code/stderr/--id semantics, ssh command mode
    • fork volume inheritance verified via marker file, fork-with-variables, positional fork, ref movement after fork
    • template: build with --wait (npm install + marker file), instant cache hit on rebuild, status/list, create --template verified to boot with the baked filesystem (pnpm 9.15.9 + marker present), unknown-template error hint
    • error paths (bad pair, missing file, bogus fork source) and all three destroy forms incl. ref clearing

🤖 Generated with Claude Code

codyde and others added 3 commits June 4, 2026 01:33
Picks up SandboxCreateInput.sourceSandboxId / networkIsolation / variables
plus accumulated drift in unrelated types since the last refresh.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `railway sandbox fork [ID]` forks a sandbox (default: the active one)
  via sandboxCreate.sourceSandboxId; the new sandbox becomes the active
  ref so subsequent `sandbox ssh`/`exec` target the fork
- `--variable KEY=VALUE` on create and fork: repeatable and
  comma-separable (a comma only splits when every segment carries its
  own `=`, so values containing commas stay intact)
- Bare Railway references auto-wrap: `DB=Postgres.DATABASE_URL` →
  `${{Postgres.DATABASE_URL}}`, resolved server-side at create time.
  Strict pattern (UPPER_SNAKE var segment) so `1.5`/`example.com`
  never wrap; the unambiguous `shared.` namespace allows any case
  (`shared.char`). Pre-wrapped/embedded ${{...}} values pass through
  verbatim
- `--env-file PATH` (repeatable) loads dotenv-style files (comments,
  export prefix, quoted values, trailing comments); `--variable` flags
  override file entries on key collision
- Experimental warning on all `railway sandbox` commands (stderr, so
  --json stdout stays clean)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `railway sandbox template build` (aliases: create, new) builds a
  template from repeatable `-c` shell instructions via
  sandboxTemplateBuild; `--wait` polls until READY/FAILED. Templates
  are content-addressed server-side (id = recipe hash, ~7d snapshot
  cache), so re-running an identical build is an instant cache hit
- `railway sandbox template status <id-or-name>` and `template list`
- `railway sandbox create --template <name-or-id>`: sandboxCreate
  needs the full recipe (not just the hash), so built recipes are
  stored locally in config.json (StoredSandboxTemplate) with an
  optional `--name` handle; create resends the stored instructions
- Composes with --variable/--env-file/--idle-timeout-minutes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codyde codyde changed the title feat(sandbox): railway sandbox fork + --variable/--env-file on create and fork feat(sandbox): fork, templates, and --variable/--env-file on create Jun 4, 2026
Maps to sandboxCreate.networkIsolation: PRIVATE (mono #30438/#30618).
Default stays ISOLATED (field omitted). First production caller of the
PRIVATE path — verified live: privnet DNS resolves and a psql query
against postgres.railway.internal succeeds from both a created and a
forked sandbox, while default sandboxes remain isolated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codyde codyde force-pushed the feat/sandbox-fork-variables branch from f6cf64a to 2553547 Compare June 4, 2026 14:54
…ng prod

The SSH relay was a compile-time constant (ssh.railway.com) while
backboard URLs follow RAILWAY_ENV — so a dev-mode CLI registered keys
and created sandboxes against the develop backboard but dialed the
production relay, which rejected the (correctly registered) key with
'Permission denied (publickey)'.

Mirror backboard's controllers/ssh mapping: dev → ssh.railway-develop.com
-p 2222; staging falls through to the prod relay exactly like backboard's
IS_DEV-only branch (probes show ssh.railway-staging.com does not serve
the SSH relay). Applies to native ssh, tmux sessions, the generated
~/.ssh/config block (Port line when non-default), and volume SFTP.

Production behavior is byte-identical: (ssh.railway.com, None) produces
the same target string, zero extra args, and the same config block —
existing config-block tests pass unmodified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codyde codyde added release/minor Author minor release release/major Author major release and removed release/minor Author minor release labels Jun 4, 2026
codyde and others added 2 commits June 4, 2026 08:23
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Command telemetry now walks nested subcommand levels (joined with ':'),
  so `sandbox template build` reports sub_command="template:build"
  instead of just "template". Note: this also enriches other nested
  commands (e.g. "keys:add" instead of "keys" for `ssh keys add`,
  "files:pull" for `volume files pull`) — dashboards doing exact
  matches on the old single-level values will want a prefix match.
- `sandbox ssh` now emits the same stage-tagged failure events as
  `railway ssh` (tel.rs generalized with *_for variants taking a
  command namespace): ssh_resolve_target / ssh_key_setup / ssh_session /
  ssh_exit_nonzero under command="sandbox".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codyde codyde merged commit 91a6213 into master Jun 4, 2026
6 checks passed
@codyde codyde deleted the feat/sandbox-fork-variables branch June 4, 2026 15:37
codyde added a commit that referenced this pull request Jun 4, 2026
…-browser-login

* origin/master: (28 commits)
  chore: Release railwayapp version 5.0.0
  feat(sandbox): fork, templates, and `--variable`/`--env-file` on create (#933)
  chore: Release railwayapp version 4.68.0
  feat(volume): include modified time in files JSON (#931)
  chore: Release railwayapp version 4.67.0
  Add service source connection support (#934)
  chore: Release railwayapp version 4.66.2
  Make GraphQL HTTP timeout configurable via RAILWAY_HTTP_TIMEOUT (#932)
  chore: Release railwayapp version 4.66.1
  feat(volume): show status and scheduled deletion date in volume list (#928)
  SSH Command: Handle Identity Files (#926)
  chore: Release railwayapp version 4.66.0
  feat(sandbox): `railway sandbox` commands (create/list/ssh/exec/destroy) (#925)
  chore: Release railwayapp version 4.65.0
  SSH Agent Support, `russh` edition. (#915)
  chore: Release railwayapp version 4.64.0
  chore: Release railwayapp version 4.63.0
  Rephrase agent advisory and gate by CLI version (#919)
  Forward --remote to setup agent in cli.new installer (#918)
  chore: Release railwayapp version 4.62.0
  ...

# Conflicts:
#	src/consts.rs
#	src/util/mod.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release/major Author major release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant