use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow, bail};
use clap::Parser;
use colored::Colorize;
use is_terminal::IsTerminal;
use crate::client::{GQLClient, post_graphql};
use crate::commands::cloud_agent::mcp_sync;
use crate::commands::cloud_agent::prefs::{AgentPrefs, DefaultProject};
use crate::commands::cloud_agent::skills_sync;
use crate::commands::sandbox::{resolve_project_and_env, variables_to_input};
use crate::commands::ssh::tel as ssh_tel;
use crate::commands::ssh::{
ensure_ssh_key_quiet, probe_native_ssh, run_native_ssh_captured, run_native_ssh_with_opts,
};
use crate::config::Configs;
use crate::controllers::cloud_agent as ca;
use crate::controllers::project::get_project;
use crate::errors::RailwayError;
use crate::gql::{mutations, queries};
use crate::macros::is_stdout_terminal;
use crate::util::progress::create_shimmer_spinner;
use crate::util::shell::shell_join;
pub(crate) mod bootstrap_setup;
pub(crate) mod client;
mod names;
mod plumbing;
pub(crate) mod saved_config;
pub(crate) fn clear_saved_config() {
if let Some(home) = dirs::home_dir() {
saved_config::clear_in(&home);
}
}
pub(crate) fn save_desktop_configuration(
prepared: &Prepared,
connection: Option<&crate::commands::cloud_agent::opencode::Connection>,
beta: bool,
desktop: &Result<bool>,
) {
let result = saved_config::SavedConfig::from_prepared(prepared)
.map(|saved| match connection {
Some(connection) => saved.with_opencode(connection, beta, desktop),
None => saved,
})
.and_then(|saved| saved.save());
if let Err(error) = result {
eprintln!("Could not save connection details for railway code get-config: {error:#}");
}
}
#[derive(Parser)]
#[clap(args_conflicts_with_subcommands = true)]
pub struct Args {
#[clap(subcommand)]
command: Option<Commands>,
#[clap(flatten)]
launch: LaunchArgs,
}
#[derive(clap::Subcommand)]
enum Commands {
GetConfig(saved_config::Args),
}
pub async fn command(args: Args) -> Result<()> {
if let Some(Commands::GetConfig(args)) = args.command {
return saved_config::command(args);
}
let mut args = args.launch;
if let Some(action) = args.prepare_code_launch()? {
let harness = if args.codex {
client::Harness::Codex
} else if args.opencode2 {
client::Harness::OpenCode2
} else {
client::Harness::OpenCode
};
match action {
ClientAction::Local => {
return client::start(args, harness, client::LaunchMode::LocalClient).await;
}
ClientAction::DesktopOnly => {
return codex_desktop_only(args, Default::default()).await;
}
ClientAction::Connect(selector) => {
return client::connect(args, harness, selector).await;
}
ClientAction::Remote => {
args.agent_args.clear();
args.client_on_agent = true;
client::pin_agent(&mut args).await?;
}
}
}
launch_in_cloud(args).await
}
pub(crate) async fn codex_desktop_only(
mut args: LaunchArgs,
options: crate::commands::cloud_agent::desktop::CodexOptions,
) -> Result<()> {
if args.client_action()? != Some(ClientAction::DesktopOnly) {
bail!("Expected railway code --codex desktop-only");
}
args.agent_args.clear();
client::start(
args,
client::Harness::Codex,
client::LaunchMode::DesktopOnly(options),
)
.await
}
pub(crate) async fn launch_in_cloud(args: LaunchArgs) -> Result<()> {
if args.connection_json {
bail!("--connection-json requires railway code --codex or --opencode2 [connect].");
}
if args.agent_args.first().is_some_and(|a| a == "setup") {
bail!(
"`railway code` passes arguments straight to the agent, so this would run `setup` on the VM.\nDid you mean `railway ca setup`?"
);
}
match args.wants_pane() {
true => crate::commands::cloud_agent::launch_in_pane(args).await,
false => launch(args).await,
}
}
#[derive(Parser, Default, Clone, Debug, PartialEq, Eq)]
#[clap(
group(clap::ArgGroup::new("client_json_harness").args(["codex", "opencode2"]).multiple(true)),
after_help = r#"Examples:
railway ca # launch your configured default
railway ca setup # choose the default agent and skills
railway code --codex # remote Codex server + local terminal client
railway code --codex connect # choose a running Codex server
railway code --codex connect my-box
railway code --codex desktop-only # backend + Desktop configuration, then exit
railway code --codex desktop-only --agent my-box --dir /app
railway code get-config # replay the latest saved connection
railway code get-config my-box # replay a specific agent (name or ID)
railway code get-config my-box --json
railway code --codex remote # run the Codex UI inside Railway CA
railway code --claude # agent VM + your Claude setup-token
railway code --grok # agent VM + your local Grok sign-in
railway code --opencode # remote OpenCode server + local client
railway code --opencode2 # same, with OpenCode2 Beta
railway code --opencode remote # run client and server inside Railway CA
railway code --opencode2 --name my-box
railway code --opencode2 connect
railway code --opencode connect my-box
railway code --railway # Railway's own agent, no sign-in needed
railway code --codex --new # optional: harness launches already create a VM
railway code --codex --variable DB_URL=postgres.DATABASE_URL
railway code --codex --env-file .env
railway code --codex -- exec "explain this codebase"
With no agent flag, the default saved by `railway ca setup` is used
(RAILWAY_CA_AGENT overrides it for one run). With no project or environment
flag, this directory's linked project is used, and your default project when
the directory has no link.
Each `railway code` launch with an explicit harness flag creates a fresh VM by
default, including `remote` and `--` passthrough commands. Use `connect [agent]`
for an existing Codex/OpenCode server, or `railway ca ssh <agent>` for a shell.
`--new` is optional for harness launches; an explicit `--agent` selects an existing
VM for server setup.
`railway code --codex`, `--opencode`, and `--opencode2` prepare a server, print
connection details, and automatically open your local client with command
approvals disabled. Codex updates its server at startup and automatically matches
the local client version and trusts the remote project. All three clients open
inside the Railway CA frame, whose sidebar lists conversation titles and resumes
the selected thread. Missing OpenCode
clients can be installed after confirmation. `connect [agent]` reconnects locally;
`remote` runs the client on the VM over SSH. `--dir` selects the remote directory
(default /app).
`--connection-json` returns credentials as JSON for Codex or OpenCode2.
Codex setup and connect also register and verify its SSH host and save its remote
project in the background. Codex Desktop imports it on its next startup;
setup never launches or activates the app.
`railway code --codex desktop-only` prepares the same backend and Desktop
connection, then exits without prompting for or launching a local terminal client.
`railway ca desktop --codex` is an alias for this setup.
OpenCode launch and connect automatically save the connection in the matching
Desktop edition when its settings file or database is detected. The final output
reports success or a non-fatal configuration failure. You may need to restart
OpenCode Desktop to load the updated configuration.
Sessions running inside `railway ca` open in its manage screen with the
tree collapsed, so it has the whole window and the other agents are one key
away — ⌥f brings the tree back, ⌥n starts another session. `--rm`, a `--`
passthrough, and piped in-VM sessions take the terminal directly instead;
so does `railway ca start`, which never draws the TUI.
Agents persist between runs and stay running when you disconnect, so your
sessions survive to reattach to. `railway ca sleep <agent>` stops the compute
bill; `railway code --rm` destroys it.
Claude auth is minted once (`claude setup-token`), cached locally, and reused —
including the copy already on a reused agent. `--refresh-auth` clears both
caches and re-mints.
Carrying a sign-in from this machine is optional. With no local credential,
sign in on the agent using the harness's login flow.
Note: requires the CLOUD_AGENTS feature to be enabled."#
)]
pub struct LaunchArgs {
#[clap(long)]
codex: bool,
#[clap(long)]
opencode: bool,
#[clap(long)]
opencode2: bool,
#[clap(long, requires = "client_json_harness")]
connection_json: bool,
#[clap(long)]
claude: bool,
#[clap(long)]
grok: bool,
#[clap(long)]
railway: bool,
#[clap(long)]
pub new: bool,
#[clap(long, conflicts_with_all = ["no_bootstrap", "remote_agent", "rm"])]
bootstrap: Option<String>,
#[clap(long, conflicts_with_all = ["remote_agent", "rm"])]
no_bootstrap: bool,
#[clap(long, hide = true)]
keep_awake: bool,
#[clap(long)]
rm: bool,
#[clap(long)]
refresh_auth: bool,
#[clap(long)]
name: Option<String>,
#[clap(long = "variable", value_name = "KEY=VALUE[,KEY=VALUE...]")]
variables: Vec<String>,
#[clap(skip)]
pub(crate) boot_variables: std::collections::BTreeMap<String, String>,
#[clap(long, requires = "new")]
pub(crate) code_endpoint: bool,
#[clap(long, requires = "new", value_parser = ca::parse_code_port)]
code_port: Option<u16>,
#[clap(long = "env-file", value_name = "PATH")]
env_files: Vec<std::path::PathBuf>,
#[clap(long, short)]
pub environment: Option<String>,
#[clap(skip)]
pub(crate) local_name_project: Option<String>,
#[clap(long, short)]
pub project: Option<String>,
agent_args: Vec<String>,
#[clap(long = "dir", value_name = "PATH")]
remote_dir: Option<String>,
#[clap(long = "agent", value_name = "NAME_OR_ID", conflicts_with = "new")]
remote_agent: Option<String>,
#[clap(skip)]
pub initial_prompt: Option<String>,
#[clap(skip)]
pub agent_id: Option<String>,
#[clap(skip)]
pub shell: bool,
#[clap(skip)]
pub app_mode: bool,
#[clap(skip)]
pub(crate) bootstrap_setup: bool,
#[clap(skip)]
pub client_on_agent: bool,
#[clap(skip)]
pub resume_session_id: Option<String>,
}
#[derive(Debug, PartialEq, Eq)]
enum ClientAction {
Local,
DesktopOnly,
Remote,
Connect(Option<String>),
}
impl LaunchArgs {
fn prepare_code_launch(&mut self) -> Result<Option<ClientAction>> {
let action = self.client_action()?;
if matches!(action, Some(ClientAction::Connect(_)))
&& (self.bootstrap.is_some() || self.no_bootstrap)
{
bail!("Bootstrap options create a new VM and cannot be used with connect.");
}
let harness_selected = self.codex
|| self.opencode
|| self.opencode2
|| self.claude
|| self.grok
|| self.railway;
if harness_selected
&& !self.rm
&& self.remote_agent.is_none()
&& self.agent_id.is_none()
&& !matches!(action, Some(ClientAction::Connect(_)))
{
self.new = true;
}
Ok(action)
}
fn client_action(&self) -> Result<Option<ClientAction>> {
let verb = self.agent_args.first().map(String::as_str);
if self.connection_json
&& ((!self.codex && !self.opencode2)
|| self.rm
|| self.app_mode
|| !(matches!(verb, None | Some("connect"))
|| (self.codex && verb == Some("desktop-only"))))
{
bail!(
"--connection-json requires railway code --codex or --opencode2 [connect], or --codex desktop-only."
);
}
let reserved = matches!(verb, Some("remote" | "connect" | "desktop-only"));
let selected = self.codex || self.opencode || self.opencode2;
if !reserved && (!selected || !self.agent_args.is_empty() || self.rm || self.app_mode) {
if self.remote_dir.is_some() || self.remote_agent.is_some() {
bail!("--dir and --agent require a Codex or OpenCode client command.");
}
return Ok(None);
}
if [self.codex, self.opencode, self.opencode2]
.into_iter()
.filter(|selected| *selected)
.count()
!= 1
|| self.claude
|| self.grok
|| self.railway
|| self.shell
{
bail!("Pick exactly one client: --codex, --opencode, or --opencode2.");
}
if self.rm || self.initial_prompt.is_some() {
bail!("Local-client commands cannot be combined with --rm or an initial prompt.");
}
if self
.remote_dir
.as_deref()
.is_some_and(|dir| dir.trim().is_empty())
{
bail!("--dir must name a directory on the agent.");
}
match verb {
None => Ok(Some(ClientAction::Local)),
Some("desktop-only") => {
if !self.codex || self.agent_args.len() != 1 || self.app_mode {
bail!(
"Use railway code --codex desktop-only [--agent NAME_OR_ID] [--dir PATH] [--new]."
);
}
Ok(Some(ClientAction::DesktopOnly))
}
Some("remote") => {
if self.agent_args.len() != 1 || self.remote_dir.is_some() {
bail!(
"Use railway code --codex remote (or --opencode/--opencode2) to open the UI inside the cloud agent; --dir is for local clients."
);
}
Ok(Some(ClientAction::Remote))
}
Some("connect") => {
if self.agent_args.len() > 2
|| self.new
|| self.name.is_some()
|| self.code_endpoint
|| self.code_port.is_some()
|| !self.variables.is_empty()
|| !self.env_files.is_empty()
|| self.refresh_auth
|| self.remote_dir.is_some()
{
bail!(
"connect uses an existing server. Use railway code --codex, --opencode, or --opencode2 to set one up on a fresh VM."
);
}
let positional = self.agent_args.get(1).cloned();
if positional.is_some() && self.remote_agent.is_some() {
bail!("Name the agent after connect or with --agent, not both.");
}
let selector = positional.or_else(|| self.remote_agent.clone());
if selector
.as_deref()
.is_some_and(|value| value.trim().is_empty())
{
bail!("The cloud agent name cannot be empty.");
}
Ok(Some(ClientAction::Connect(selector)))
}
_ => unreachable!("other harness arguments returned above"),
}
}
pub fn is_bare(&self) -> bool {
!self.codex
&& !self.opencode
&& !self.opencode2
&& !self.connection_json
&& !self.claude
&& !self.grok
&& !self.railway
&& !self.new
&& self.bootstrap.is_none()
&& !self.no_bootstrap
&& !self.keep_awake
&& !self.rm
&& !self.refresh_auth
&& self.name.is_none()
&& self.environment.is_none()
&& self.project.is_none()
&& self.initial_prompt.is_none()
&& self.resume_session_id.is_none()
&& self.variables.is_empty()
&& !self.code_endpoint
&& self.code_port.is_none()
&& self.env_files.is_empty()
&& self.agent_args.is_empty()
&& self.remote_dir.is_none()
&& self.remote_agent.is_none()
&& !self.shell
}
pub fn wants_pane(&self) -> bool {
self.pane_shaped() && is_stdout_terminal()
}
fn pane_shaped(&self) -> bool {
!self.rm && self.agent_args.is_empty()
}
pub fn set_harness(&mut self, slug: &str) {
self.claude = slug == "claude";
self.codex = slug == "codex";
self.opencode = slug == "opencode";
self.opencode2 = slug == "opencode2";
self.grok = slug == "grok";
self.railway = slug == "railway";
self.shell = slug == "shell";
}
pub fn for_target(
project_id: String,
environment_id: String,
harness: &str,
force_new: bool,
prompt: Option<String>,
agent_id: Option<String>,
) -> Self {
Self::default().retargeted(
project_id,
environment_id,
harness,
force_new,
prompt,
agent_id,
)
}
pub(crate) fn set_bootstrap_choice(&mut self, name: Option<String>, none: bool) {
self.bootstrap = name;
self.no_bootstrap = none;
}
pub fn retargeted(
mut self,
project_id: String,
environment_id: String,
harness: &str,
force_new: bool,
prompt: Option<String>,
agent_id: Option<String>,
) -> Self {
self.project = Some(project_id);
self.environment = Some(environment_id);
self.new = force_new;
self.initial_prompt = prompt;
self.agent_id = agent_id;
self.set_harness(harness);
self
}
pub fn for_app_mode(
harness: &str,
project: Option<String>,
environment: Option<String>,
) -> Self {
let mut args = Self {
project,
environment,
app_mode: true,
..Self::default()
};
args.set_harness(harness);
args
}
pub(crate) fn for_codex_desktop(
project: Option<String>,
environment: Option<String>,
agent: Option<String>,
directory: String,
new: bool,
) -> Self {
Self {
codex: true,
project,
environment,
remote_agent: agent,
remote_dir: Some(directory),
new,
agent_args: vec!["desktop-only".into()],
..Self::default()
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum Agent {
Codex,
OpenCode,
OpenCode2,
Claude,
Grok,
Railway,
Shell,
}
impl Agent {
fn name(self) -> &'static str {
match self {
Agent::Codex => "codex",
Agent::OpenCode => "opencode",
Agent::OpenCode2 => "opencode2",
Agent::Claude => "claude",
Agent::Grok => "grok",
Agent::Railway => "railway-agent-tui",
Agent::Shell => "bash",
}
}
fn slug(self) -> &'static str {
match self {
Agent::Codex => "codex",
Agent::OpenCode => "opencode",
Agent::OpenCode2 => "opencode2",
Agent::Claude => "claude",
Agent::Grok => "grok",
Agent::Railway => "railway",
Agent::Shell => "shell",
}
}
fn from_slug(slug: &str) -> Option<Self> {
match slug {
"claude" => Some(Agent::Claude),
"codex" => Some(Agent::Codex),
"opencode" => Some(Agent::OpenCode),
"opencode2" => Some(Agent::OpenCode2),
"grok" => Some(Agent::Grok),
"railway" => Some(Agent::Railway),
"shell" => Some(Agent::Shell),
_ => None,
}
}
fn display(self) -> &'static str {
match self {
Agent::Codex => "Codex",
Agent::OpenCode => "OpenCode",
Agent::OpenCode2 => "OpenCode2 [Beta]",
Agent::Claude => "Claude Code",
Agent::Grok => "Grok",
Agent::Railway => "Railway",
Agent::Shell => "a plain shell",
}
}
fn credential_seed(self) -> &'static str {
match self {
Agent::Codex => CODEX_SEED,
Agent::OpenCode => OPENCODE_SEED,
Agent::OpenCode2 => crate::commands::cloud_agent::opencode2::auth::SEED,
Agent::Claude => CLAUDE_SEED,
Agent::Grok => GROK_SEED,
Agent::Railway | Agent::Shell => "",
}
}
fn credential_seed_framed(self, len: usize) -> String {
match self {
Agent::Codex => format!("mkdir -p ~/.codex\nhead -c {len} > ~/.codex/auth.json"),
Agent::OpenCode => format!(
"{OPENCODE_DATA_DIR}\nmkdir -p \"$opencode_data\"\nhead -c {len} > \"$opencode_data/auth.json\"\nchmod 600 \"$opencode_data/auth.json\""
),
Agent::Claude => {
format!("head -c {len} > ~/.claude-code-env\nchmod 600 ~/.claude-code-env")
}
Agent::Grok => format!("mkdir -p ~/.grok\nhead -c {len} > ~/.grok/auth.json"),
Agent::OpenCode2 => crate::commands::cloud_agent::opencode2::auth::seed_framed(len),
Agent::Railway | Agent::Shell => "true".to_string(),
}
}
fn local_signin_path(self, home: &Path, xdg_data_home: Option<&Path>) -> Option<PathBuf> {
match self {
Agent::Codex => Some(home.join(".codex").join("auth.json")),
Agent::Grok => Some(home.join(".grok").join("auth.json")),
Agent::OpenCode => Some(
xdg_data_home
.filter(|p| p.is_absolute())
.map(Path::to_path_buf)
.unwrap_or_else(|| home.join(".local/share"))
.join("opencode/auth.json"),
),
Agent::OpenCode2 | Agent::Claude | Agent::Railway | Agent::Shell => None,
}
}
fn sign_in_on_agent_hint(self) -> &'static str {
match self {
Agent::Codex => "sign in there with `codex login --device-auth`",
Agent::OpenCode => {
"connect a provider in OpenCode Desktop or run `opencode auth login` on the agent"
}
Agent::OpenCode2 => {
"connect a provider in OpenCode Beta or run `opencode2 auth login` on the agent"
}
Agent::Claude => "sign in there with `/login`",
Agent::Grok => "sign in there when it asks",
Agent::Railway => "no sign-in needed — the agent carries its own",
Agent::Shell => "no sign-in needed — nothing starts but a shell",
}
}
}
const CODEX_SEED: &str = r#"mkdir -p ~/.codex
cat > ~/.codex/auth.json"#;
const CLAUDE_SEED: &str = r#"cat > ~/.claude-code-env
chmod 600 ~/.claude-code-env"#;
const CLAUDE_ENV_GUARD: &str =
r#"[ -f "$HOME/.claude-code-env" ] && set -a && . "$HOME/.claude-code-env" && set +a"#;
const GROK_SEED: &str = r#"mkdir -p ~/.grok
cat > ~/.grok/auth.json"#;
const OPENCODE_DATA_DIR: &str = r#"opencode_data="${XDG_DATA_HOME:-$HOME/.local/share}/opencode""#;
const OPENCODE_SEED: &str = r#"opencode_data="${XDG_DATA_HOME:-$HOME/.local/share}/opencode"
mkdir -p "$opencode_data"
cat > "$opencode_data/auth.json"
chmod 600 "$opencode_data/auth.json""#;
const HARNESS_PATH: &str = r#"export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.grok/bin:$HOME/.local/share/mise/shims:$PATH""#;
const COMMON_SEED: &str = r#"grep -q "^COLORTERM=" /etc/environment 2>/dev/null || echo "COLORTERM=truecolor" >> /etc/environment 2>/dev/null || true
if ! grep -q "railway-code agent autostart v4" ~/.profile 2>/dev/null; then
sed -i '/# railway-code agent autostart/,/^fi$/d' ~/.profile 2>/dev/null || true
cat >> ~/.profile <<'PROFEOF'
# railway-code agent autostart v4 (connecting drops into the agent; exit it for a shell)
if [ -z "$RAILWAY_CODE_AUTOSTARTED" ] && [ -t 1 ] && [ ! -f "$HOME/.railway-app-mode" ] && [ -s "$HOME/.railway-code-agent" ]; then
agent="$(cat "$HOME/.railway-code-agent")"
[ -d "$HOME/.grok/bin" ] && export PATH="$HOME/.grok/bin:$PATH"
if command -v "$agent" >/dev/null 2>&1; then
export RAILWAY_CODE_AUTOSTARTED=1
[ -f "$HOME/.gh-token" ] && export GH_TOKEN="$(cat "$HOME/.gh-token")"
[ -f "$HOME/.claude-code-env" ] && set -a && . "$HOME/.claude-code-env" && set +a
"$agent"
printf '\033[<u\033[<u\033[=0;1u\033[?2004l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?1004l\033[?25h'
fi
fi
PROFEOF
fi"#;
const CLAUDE_CREDENTIAL_PROBE: &str =
r#"[ -s ~/.claude-code-env ] && echo CRED-PRESENT || echo CRED-ABSENT"#;
fn provision_script(agent: Agent, write_credential: bool, app_mode: bool) -> String {
let seed = if write_credential {
agent.credential_seed()
} else {
"true"
};
let name = agent.name();
let hash_marker = skills_sync::REMOTE_HASH_MARKER;
let hash_file = skills_sync::REMOTE_HASH_FILE;
let mcp_marker = mcp_sync::REMOTE_HASH_MARKER;
let mcp_file = mcp_sync::REMOTE_HASH_FILE;
let mode_seed = mode_seed(agent, app_mode);
let runtime_seed = if agent == Agent::OpenCode2 {
crate::commands::cloud_agent::opencode2::seed_script()
} else {
"true".to_string()
};
format!(
r#"umask 077
{HARNESS_PATH}
{seed}
{COMMON_SEED}
{runtime_seed}
{mode_seed}
printf '{hash_marker}%s\n' "$(cat "{hash_file}" 2>/dev/null || true)"
printf '{mcp_marker}%s\n' "$(cat "{mcp_file}" 2>/dev/null || true)"
if command -v {name} >/dev/null 2>&1; then echo AGENT-READY; else echo AGENT-MISSING; fi"#
)
}
fn provision_script_with_skills(
agent: Agent,
credential_len: Option<usize>,
app_mode: bool,
skills_hash: &str,
) -> String {
let seed = match credential_len {
Some(len) => agent.credential_seed_framed(len),
None => "true".to_string(),
};
let name = agent.name();
let mode_seed = mode_seed(agent, app_mode);
let runtime_seed = if agent == Agent::OpenCode2 {
crate::commands::cloud_agent::opencode2::seed_script()
} else {
"true".to_string()
};
let sync = skills_sync::sync_body(skills_hash);
format!(
r#"umask 077
{HARNESS_PATH}
{seed}
payload="$HOME/.railway-skills-payload.tgz"
cat > "$payload"
{COMMON_SEED}
{runtime_seed}
{mode_seed}
if command -v {name} >/dev/null 2>&1; then echo AGENT-READY; else echo AGENT-MISSING; fi
{sync}"#
)
}
fn mode_seed(agent: Agent, app_mode: bool) -> String {
if app_mode {
"touch ~/.railway-app-mode\nrm -f ~/.railway-code-agent".to_string()
} else {
let record = match agent {
Agent::Shell => "true".to_string(),
_ => format!("echo {} > ~/.railway-code-agent", agent.name()),
};
format!("rm -f ~/.railway-app-mode\n{record}")
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SessionStyle {
FullTerminal,
Pane,
}
pub(crate) const LOGIN_SHELL_COMMAND: &str = "export RAILWAY_CODE_AUTOSTARTED=1; exec bash -l";
pub(crate) fn harness_env_prefix() -> String {
format!(
"{HARNESS_PATH}; [ -f ~/.gh-token ] && export GH_TOKEN=\"$(cat ~/.gh-token)\"; {CLAUDE_ENV_GUARD}; "
)
}
fn remote_command(
agent: Agent,
env_prefix: &str,
initial_prompt: Option<&str>,
resume_session_id: Option<&str>,
agent_args: &[String],
style: SessionStyle,
) -> String {
if agent == Agent::Shell {
return format!("{env_prefix}{LOGIN_SHELL_COMMAND}");
}
let name = match agent {
Agent::OpenCode2 => "opencode2 --standalone",
Agent::Railway => {
"railway-agent-tui --session \"${RAILWAY_DURABLE_SESSION_NAME:-railway-adhoc-$$}\""
}
_ => agent.name(),
};
let after = match style {
SessionStyle::FullTerminal => "; exec bash -l",
SessionStyle::Pane => "; exit \"$railway_code_status\"",
};
let reset = format!("railway_code_status=$?; {}", terminal_reset_printf());
if matches!(agent, Agent::Claude | Agent::Grok)
&& let Some(id) = resume_session_id.map(str::trim).filter(|id| !id.is_empty())
{
return format!(
"{env_prefix}export RAILWAY_CODE_AUTOSTARTED=1; {name} --resume {}; {reset}{after}",
shell_join(std::slice::from_ref(&id.to_string())),
);
}
match initial_prompt.map(str::trim).filter(|p| !p.is_empty()) {
Some(prompt) if matches!(agent, Agent::OpenCode | Agent::OpenCode2) => format!(
"{env_prefix}export RAILWAY_CODE_AUTOSTARTED=1; {name} --prompt {}; {reset}{after}",
shell_join(std::slice::from_ref(&prompt.to_string())),
),
Some(prompt) => format!(
"{env_prefix}export RAILWAY_CODE_AUTOSTARTED=1; {name} {}; {reset}{after}",
shell_join(std::slice::from_ref(&prompt.to_string())),
),
None if agent_args.is_empty() => {
format!("{env_prefix}export RAILWAY_CODE_AUTOSTARTED=1; {name}; {reset}{after}")
}
None => format!(
"{env_prefix}exec {} {}",
agent.name(),
shell_join(agent_args)
),
}
}
#[derive(Clone)]
struct RelaySsh {
opts: Vec<String>,
known_hosts: std::path::PathBuf,
host_pattern: String,
}
fn mux_socket() -> std::path::PathBuf {
std::env::temp_dir().join(format!(
"railway-cm-{}-{:08x}.sock",
std::process::id(),
rand::random::<u32>()
))
}
fn mux_usable(socket: &std::path::Path) -> bool {
!cfg!(windows) && socket.as_os_str().len() <= 100
}
fn mux_master_opts(socket: &std::path::Path, persist: &str) -> Vec<String> {
if !mux_usable(socket) {
return Vec::new();
}
vec![
"-o".into(),
"ControlMaster=auto".into(),
"-o".into(),
format!("ControlPath={}", socket.display()),
"-o".into(),
format!("ControlPersist={persist}"),
]
}
fn mux_client_opts(socket: &std::path::Path) -> Vec<String> {
if !mux_usable(socket) {
return Vec::new();
}
vec!["-o".into(), format!("ControlPath={}", socket.display())]
}
pub fn relay_known_hosts() -> Result<std::path::PathBuf> {
Ok(relay_ssh()?.known_hosts)
}
fn relay_ssh() -> Result<RelaySsh> {
let home = dirs::home_dir().ok_or_else(|| anyhow!("Unable to get home directory"))?;
let railway_dir = home.join(".railway");
std::fs::create_dir_all(&railway_dir)?;
let known_hosts = railway_dir.join("known_hosts_relay");
let (host, port) = Configs::get_ssh_relay();
let host_pattern = match port {
Some(p) if p != 22 => format!("[{host}]:{p}"),
_ => host.to_string(),
};
Ok(RelaySsh {
opts: vec![
"-o".into(),
format!("UserKnownHostsFile={}", known_hosts.display()),
"-o".into(),
"StrictHostKeyChecking=accept-new".into(),
"-o".into(),
"ServerAliveInterval=30".into(),
"-o".into(),
"ServerAliveCountMax=3".into(),
],
known_hosts,
host_pattern,
})
}
impl RelaySsh {
fn heal_known_hosts(&self) {
let _ = std::process::Command::new("ssh-keygen")
.arg("-R")
.arg(&self.host_pattern)
.arg("-f")
.arg(&self.known_hosts)
.output();
}
}
const TERMINAL_RESET: &str = "\x1b[<u\x1b[<u\x1b[=0;1u\x1b[?2004l\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1004l\x1b[?25h";
fn terminal_reset_printf() -> String {
format!("printf '{}'", TERMINAL_RESET.replace('\x1b', "\\033"))
}
fn local_signin(agent: Agent, home: &Path) -> Result<PendingAuth> {
let xdg_data_home = std::env::var_os("XDG_DATA_HOME").map(PathBuf::from);
if agent == Agent::OpenCode2 {
let database = std::env::var("OPENCODE_DB").ok();
return Ok(
match crate::commands::cloud_agent::opencode2::auth::read(
home,
xdg_data_home.as_deref(),
database.as_deref(),
)? {
Some((line, source)) => PendingAuth::Ready {
line,
source: source.display().to_string(),
},
None => PendingAuth::SignInOnAgent {
note: format!(
"No OpenCode2 provider sign-in to copy from this machine; {}.",
agent.sign_in_on_agent_hint()
),
},
},
);
}
let auth_path = agent.local_signin_path(home, xdg_data_home.as_deref());
read_local_signin(agent, auth_path.as_deref())
}
fn read_local_signin(agent: Agent, auth_path: Option<&Path>) -> Result<PendingAuth> {
let Some(auth_path) = auth_path else {
return Err(anyhow!(
"{} has no local sign-in file to copy",
agent.display()
));
};
let missing = || PendingAuth::SignInOnAgent {
note: format!(
"No {} sign-in on this machine ({}) — starting {} unauthenticated; {}.",
agent.display(),
auth_path.display(),
agent.display(),
agent.sign_in_on_agent_hint()
),
};
if !auth_path.exists() {
return Ok(missing());
}
let bytes = std::fs::read(auth_path)
.with_context(|| format!("Couldn't read {}", auth_path.display()))?;
if bytes.is_empty() {
return Ok(missing());
}
Ok(PendingAuth::Ready {
line: bytes,
source: auth_path.display().to_string(),
})
}
fn claude_token_cache_path() -> Option<std::path::PathBuf> {
Some(claude_token_cache_path_in(&dirs::home_dir()?))
}
fn claude_token_cache_path_in(home: &Path) -> std::path::PathBuf {
home.join(".railway").join("claude-code-token")
}
fn cached_claude_token() -> Option<String> {
let tok = std::fs::read_to_string(claude_token_cache_path()?).ok()?;
let tok = tok.trim().to_string();
(!tok.is_empty() && validate_claude_token(&tok).is_ok()).then_some(tok)
}
fn cache_claude_token(token: &str) {
if let Some(path) = claude_token_cache_path() {
write_token_0600(&path, token);
}
}
fn write_token_0600(path: &std::path::Path, token: &str) {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
opts.mode(0o600);
}
if let Ok(mut f) = opts.open(path) {
use std::io::Write;
let _ = f.write_all(format!("{token}\n").as_bytes());
}
}
fn claude_token_age_days() -> Option<u64> {
let meta = std::fs::metadata(claude_token_cache_path()?).ok()?;
Some(meta.modified().ok()?.elapsed().ok()?.as_secs() / 86_400)
}
fn cached_token_source(age_days: Option<u64>) -> String {
match age_days {
None | Some(0) => "cached setup-token".to_string(),
Some(days @ 1..30) => format!("cached setup-token from {days}d ago"),
Some(days) => {
format!("cached setup-token from {days}d ago — --refresh-auth re-mints")
}
}
}
pub fn clear_claude_token_cache() {
if let Some(home) = dirs::home_dir() {
clear_claude_token_cache_in(&home);
}
}
pub fn clear_claude_token_cache_in(home: &Path) {
let _ = std::fs::remove_file(claude_token_cache_path_in(home));
}
enum PendingAuth {
Ready { line: Vec<u8>, source: String },
MintClaude,
SignInOnAgent { note: String },
None,
}
static CLAUDE_MINT_DECLINED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
fn claude_sign_in_note() -> String {
format!(
"No {} credential to carry from this machine — starting it unauthenticated; {}.",
Agent::Claude.display(),
Agent::Claude.sign_in_on_agent_hint()
)
}
fn claude_sign_in_on_agent() -> PendingAuth {
PendingAuth::SignInOnAgent {
note: claude_sign_in_note(),
}
}
fn claude_credentials_cheap(refresh_auth: bool) -> Result<PendingAuth> {
for var in ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"] {
if let Ok(tok) = std::env::var(var) {
let tok = tok.trim().to_string();
if !tok.is_empty() {
validate_claude_token(&tok)?;
return Ok(PendingAuth::Ready {
line: format!("{var}={tok}\n").into_bytes(),
source: format!("${var}"),
});
}
}
}
if refresh_auth {
clear_claude_token_cache();
} else if let Some(tok) = cached_claude_token() {
return Ok(PendingAuth::Ready {
line: format!("CLAUDE_CODE_OAUTH_TOKEN={tok}\n").into_bytes(),
source: cached_token_source(claude_token_age_days()),
});
}
if CLAUDE_MINT_DECLINED.load(std::sync::atomic::Ordering::Relaxed) {
return Ok(claude_sign_in_on_agent());
}
if which::which("claude").is_err() {
return Ok(claude_sign_in_on_agent());
}
Ok(PendingAuth::MintClaude)
}
fn mint_claude_credentials() -> Result<Option<(Vec<u8>, String)>> {
use colored::Colorize;
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
eprintln!(
"{}",
"No Claude credential found — set CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`) or ANTHROPIC_API_KEY to carry one from this machine."
.yellow()
);
CLAUDE_MINT_DECLINED.store(true, std::sync::atomic::Ordering::Relaxed);
return Ok(None);
}
if !CLAUDE_AUTO_MINT_FAILED.load(std::sync::atomic::Ordering::Relaxed) {
let spinner = create_shimmer_spinner(
"Minting a Claude token — approve the browser prompt if one appears",
);
match mint_claude_credential_headless() {
Ok(tok) => {
spinner.finish_and_clear();
return Ok(Some((
format!("CLAUDE_CODE_OAUTH_TOKEN={tok}\n").into_bytes(),
"claude setup-token".to_string(),
)));
}
Err(e) => {
spinner.finish_and_clear();
eprintln!(
"{}",
format!(
"Couldn't mint a token automatically ({e}) — run `claude setup-token` in another terminal instead."
)
.yellow()
)
}
}
}
let tok = crate::util::prompt::prompt_secret(
"Run `claude setup-token` on this machine, then paste the token (enter to skip and sign in on the agent)",
)?;
let tok = tok.trim().to_string();
if tok.is_empty() {
CLAUDE_MINT_DECLINED.store(true, std::sync::atomic::Ordering::Relaxed);
return Ok(None);
}
validate_claude_token(&tok)?;
cache_claude_token(&tok);
Ok(Some((
format!("CLAUDE_CODE_OAUTH_TOKEN={tok}\n").into_bytes(),
"claude setup-token".to_string(),
)))
}
const SETUP_TOKEN_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120);
static CLAUDE_AUTO_MINT_FAILED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub fn mint_claude_credential_headless() -> Result<String> {
let minted = run_claude_setup_token().and_then(|tok| {
validate_claude_token(&tok)?;
Ok(tok)
});
match minted {
Ok(tok) => {
cache_claude_token(&tok);
Ok(tok)
}
Err(e) => {
CLAUDE_AUTO_MINT_FAILED.store(true, std::sync::atomic::Ordering::Relaxed);
Err(e)
}
}
}
fn run_claude_setup_token() -> Result<String> {
let capture = std::env::temp_dir().join(format!(
"railway-setup-token-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
{
let f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&capture)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
f.set_permissions(std::fs::Permissions::from_mode(0o600))?;
}
}
#[cfg(not(unix))]
{
let _ = std::fs::remove_file(&capture);
bail!("automatic token capture needs a unix pty");
}
#[cfg(unix)]
{
use std::process::Stdio;
let inner = "stty cols 500 rows 50 2>/dev/null; exec claude setup-token";
#[cfg(target_os = "macos")]
let mut cmd = {
let mut c = std::process::Command::new("script");
c.arg("-q").arg(&capture).args(["/bin/sh", "-c", inner]);
c
};
#[cfg(not(target_os = "macos"))]
let mut cmd = {
let mut c = std::process::Command::new("script");
c.args(["-q", "-e", "-c", inner]).arg(&capture);
c
};
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
let mut child = cmd
.spawn()
.map_err(|e| anyhow!("couldn't spawn script(1): {e}"))?;
let deadline = std::time::Instant::now() + SETUP_TOKEN_TIMEOUT;
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break Some(status),
Ok(None) if std::time::Instant::now() >= deadline => {
let _ = child.kill();
let _ = child.wait();
break None;
}
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(200)),
Err(e) => {
let _ = child.kill();
let _ = child.wait();
let _ = std::fs::remove_file(&capture);
return Err(anyhow!("couldn't wait on script(1): {e}"));
}
}
};
let recorded = std::fs::read_to_string(&capture);
let _ = std::fs::remove_file(&capture);
match extract_claude_token(&recorded.unwrap_or_default()) {
Some(tok) => Ok(tok),
None => match status {
None => Err(anyhow!(
"the browser sign-in didn't complete within {}s",
SETUP_TOKEN_TIMEOUT.as_secs()
)),
Some(s) if !s.success() => Err(anyhow!(
"`claude setup-token` exited without minting a token — is claude installed locally?"
)),
Some(_) => Err(anyhow!("`claude setup-token` finished without a token")),
},
}
}
}
fn extract_claude_token(raw: &str) -> Option<String> {
let text = strip_ansi(raw);
let is_tok = |c: char| c.is_ascii_alphanumeric() || c == '-' || c == '_';
let lines: Vec<&str> = text.split(['\r', '\n']).collect();
let mut best: Option<String> = None;
for (i, line) in lines.iter().enumerate() {
let Some(pos) = line.find("sk-ant-oat01-") else {
continue;
};
let mut tok: String = line[pos..].chars().take_while(|&c| is_tok(c)).collect();
let mut at_eol = pos + tok.len() >= line.trim_end().len();
let mut j = i + 1;
while at_eol {
while j < lines.len() && lines[j].is_empty() {
j += 1;
}
let Some(cont) = lines.get(j) else { break };
let cont = cont.strip_prefix(' ').unwrap_or(cont);
let cont = cont.trim_end();
if !cont.is_empty() && cont.chars().all(is_tok) {
tok.push_str(cont);
j += 1;
at_eol = true;
} else {
break;
}
}
if best.as_ref().is_none_or(|b| tok.len() > b.len()) {
best = Some(tok);
}
}
best.filter(|t| t.len() >= 60)
}
fn strip_ansi(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut chars = raw.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\x1b' => match chars.next() {
Some('[') => {
for n in chars.by_ref() {
if ('\x40'..='\x7e').contains(&n) {
break;
}
}
}
Some(']') | Some('P') | Some('X') | Some('^') | Some('_') => {
while let Some(n) = chars.next() {
if n == '\x07' {
break;
}
if n == '\x1b' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
Some('(') | Some(')') => {
chars.next();
}
_ => {}
},
'\x0e' | '\x0f' => {}
_ => out.push(c),
}
}
out
}
fn validate_claude_token(tok: &str) -> Result<()> {
if tok
.chars()
.any(|c| c.is_whitespace() || c.is_control() || "'\"\\$`;#".contains(c))
{
bail!(
"That doesn't look like a Claude token (it contains whitespace or shell-special characters)."
);
}
Ok(())
}
fn ssh_plumbing(
target: &str,
command: &str,
identity: Option<&std::path::Path>,
stdin_payload: Option<&[u8]>,
relay: &RelaySsh,
mux_socket: Option<&std::path::Path>,
) -> Result<Vec<u8>> {
const BACKOFF_SECS: [u64; 5] = [2, 3, 5, 5, 5];
let attempts = BACKOFF_SECS.len() + 1;
let (command, payload) = plumbing::script_input(command, stdin_payload);
let mut last: (i32, String) = (1, String::new());
for attempt in 1..=attempts {
let (code, out, err) =
run_native_ssh_captured(target, &command, identity, Some(&payload), &relay.opts)?;
if code == 0 {
return Ok(out);
}
let stderr_text = String::from_utf8_lossy(&err).trim().to_string();
let stdout_text = String::from_utf8_lossy(&out).trim().to_string();
let reason = if stderr_text.is_empty() {
stdout_text
} else if stdout_text.is_empty() {
stderr_text.clone()
} else {
format!("{stderr_text}\n{stdout_text}")
};
let hostkey_mismatch = stderr_text.contains("Host key verification failed")
|| stderr_text.contains("REMOTE HOST IDENTIFICATION HAS CHANGED");
if hostkey_mismatch {
relay.heal_known_hosts();
}
if let Some(socket) = mux_socket {
let _ = std::fs::remove_file(socket);
}
last = (code, reason);
if attempt < attempts {
let wait = if hostkey_mismatch {
0
} else {
BACKOFF_SECS[attempt - 1]
};
if wait > 0 {
std::thread::sleep(std::time::Duration::from_secs(wait));
}
}
}
let (code, reason) = last;
if reason.is_empty() {
bail!(
"SSH to the agent failed after {attempts} attempts (exit {code}), with no message from the relay."
)
}
bail!("SSH to the agent failed after {attempts} attempts (exit {code}):\n{reason}")
}
const READY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(180);
pub(crate) struct RelayAccess {
pub identity: Option<std::path::PathBuf>,
pub relay_opts: Vec<String>,
}
pub(crate) async fn relay_access() -> Result<RelayAccess> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let identity = ensure_ssh_key_quiet(&client, &configs).await?;
let relay = relay_ssh()?;
Ok(RelayAccess {
identity,
relay_opts: relay.opts,
})
}
pub(crate) async fn wait_until_connectable(
client: &reqwest::Client,
backboard: &str,
environment_id: &str,
id: &str,
access: &RelayAccess,
initial_delay: std::time::Duration,
) -> Result<(ca::Agent, Option<std::path::PathBuf>)> {
use ca::Status as S;
let wait_started = std::time::Instant::now();
let diagnostics = ssh_tel::timing_diagnostics();
let ssh_target = format!("agent:{environment_id}:{id}");
let deadline = std::time::Instant::now() + READY_TIMEOUT;
if !initial_delay.is_zero() {
tokio::time::sleep(initial_delay).await;
}
let mut round = 0u32;
let mut last_fetch: Option<std::time::Instant> = None;
let mut last_agent: Option<ca::Agent> = None;
loop {
round += 1;
let round_started = std::time::Instant::now();
let socket = mux_socket();
let target = ssh_target.clone();
let identity = access.identity.clone();
let mut opts = access.relay_opts.clone();
opts.extend(mux_master_opts(&socket, "3s"));
let probe = tokio::task::spawn_blocking(move || {
probe_native_ssh(&target, identity.as_deref(), &opts)
});
let fetch_due = last_fetch.is_none_or(|at| at.elapsed().as_millis() >= 700);
if fetch_due {
let agent = ca::get(client, backboard, environment_id, id)
.await?
.ok_or_else(|| anyhow!("Agent {id} disappeared while starting."))?;
last_fetch = Some(std::time::Instant::now());
match agent.status {
S::Running | S::Starting | S::Sleeping => {}
S::Crashed => bail!(
"Agent {} crashed while starting. `railway code --new` for a fresh one.",
agent.name
),
S::Failed => bail!(
"Agent {} failed to start. `railway code --new` for a fresh one.",
agent.name
),
S::Deleting => bail!("Agent {} is being deleted.", agent.name),
S::Unknown(ref s) => bail!("Agent {} is in an unknown state ({s}).", agent.name),
}
if agent.status == S::Running {
ssh_tel::record_stage("wait_connectable", wait_started.elapsed(), true);
return Ok((agent, None));
}
last_agent = Some(agent);
}
let routed = probe.await?.unwrap_or(false);
if diagnostics {
eprintln!(
"[wait_connectable] round {round}: status={:?} round={}ms routed={routed} fetched={fetch_due}",
last_agent.as_ref().map(|a| &a.status),
round_started.elapsed().as_millis()
);
}
if routed {
ssh_tel::record_stage("wait_connectable", wait_started.elapsed(), true);
let agent = last_agent
.ok_or_else(|| anyhow!("Agent {id} was never observed while starting."))?;
return Ok((agent, Some(socket)));
}
release_probe_master(&socket, &ssh_target);
if std::time::Instant::now() >= deadline {
bail!(
"Agent {id} did not become connectable within {}s (last state: {:?}).",
READY_TIMEOUT.as_secs(),
last_agent.map(|a| a.status)
);
}
let cadence = if wait_started.elapsed() < std::time::Duration::from_secs(4) {
std::time::Duration::from_millis(250)
} else {
std::time::Duration::from_millis(750)
};
if let Some(rest) = cadence.checked_sub(round_started.elapsed()) {
tokio::time::sleep(rest).await;
}
}
}
fn release_probe_master(socket: &std::path::Path, target: &str) {
if !mux_usable(socket) {
return;
}
let _ = std::process::Command::new("ssh")
.arg("-O")
.arg("exit")
.arg("-o")
.arg(format!("ControlPath={}", socket.display()))
.arg(target)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
}
async fn ready_existing_agent(
client: &reqwest::Client,
backboard: &str,
environment_id: &str,
agent: ca::Agent,
progress: &dyn Progress,
access: &RelayAccess,
) -> Result<(ca::Agent, Option<std::path::PathBuf>)> {
use ca::Status as S;
match agent.status {
S::Running => {
progress.note(&format!(
"Using agent {} (--new for a fresh one)",
agent.name
));
Ok((agent, None))
}
S::Sleeping | S::Starting => {
progress.step(&format!("Waking agent {}", agent.name));
let mut probe_delay = std::time::Duration::ZERO;
if agent.status == S::Sleeping {
let wake_started = std::time::Instant::now();
let wake = post_graphql::<mutations::CloudAgentWake, _>(
client,
backboard,
mutations::cloud_agent_wake::Variables {
id: agent.id.clone(),
},
)
.await;
ssh_tel::record_stage("wake_mutation", wake_started.elapsed(), wake.is_ok());
if let Err(e) = wake {
return Err(e.into());
}
probe_delay = std::time::Duration::from_millis(350);
}
let (running, probe_master) = wait_until_connectable(
client,
backboard,
environment_id,
&agent.id,
access,
probe_delay,
)
.await?;
progress.note(&format!(
"Woke agent {} — your work is on its disk",
running.name
));
Ok((running, probe_master))
}
S::Crashed | S::Failed | S::Deleting | S::Unknown(_) => bail!(
"Agent {} is reported as {} and cannot be connected to. Check `railway ca list` and retry, or use `railway code --new` to create a separate agent.",
agent.name,
agent.status.label()
),
}
}
async fn sole_owned_agent_id(
client: &reqwest::Client,
backboard: &str,
environment_id: &str,
) -> Result<Option<String>> {
let agents = ca::list_in_environment(client, backboard, environment_id, true).await?;
match agents.as_slice() {
[] => Ok(None),
[only] => Ok(Some(only.id.clone())),
many => bail!(
"You have {} cloud agents in this environment and no local record of which one `railway code` should use:\n{}\nPick one with `railway ca`, or `railway code --new` to add another.",
many.len(),
many.iter()
.map(|a| format!(" {} ({})", a.name, a.id))
.collect::<Vec<_>>()
.join("\n")
),
}
}
pub(crate) async fn resolve_target(
configs: &mut Configs,
client: &reqwest::Client,
args: &LaunchArgs,
prefs: &mut AgentPrefs,
home: &Path,
) -> Result<names::Target> {
let linked = if args.project.is_none() && args.environment.is_none() {
configs
.get_linked_project()
.await
.ok()
.and_then(|l| l.environment.clone().map(|env| (l.project, env)))
} else {
None
};
let linked = match linked {
Some((project_id, environment_id)) => {
let lookup = get_project(client, configs, project_id.clone()).await;
match stale_link_reason(&lookup, &environment_id) {
Some(reason) => {
eprintln!(
"{}",
format!(
"This directory is linked to a {reason} — ignoring the link (`railway link` to fix it)."
)
.yellow()
);
if let Some(default) = prefs.default_project.as_ref() {
eprintln!(
"{}",
format!(
"Using your default cloud agents project instead: {} ({}).",
default.project_name, default.environment_name
)
.dimmed()
);
}
None
}
None => Some((project_id, environment_id)),
}
}
None => None,
};
let mut target = match choose_target(args, prefs.default_project.as_ref(), linked) {
TargetSource::Flags => {
if let (Some(project), Some(environment)) = (&args.project, &args.environment)
&& is_uuid(project)
&& is_uuid(environment)
{
names::Target::new((project.clone(), environment.clone()), false)
} else {
names::Target::new(
resolve_project_and_env(
configs,
client,
args.project.clone(),
args.environment.clone(),
)
.await?,
false,
)
}
}
TargetSource::Configured(project_id, environment_id) => {
names::Target::new((project_id, environment_id), true)
}
TargetSource::Linked(project_id, environment_id) => {
names::Target::new((project_id, environment_id), false)
}
TargetSource::Setup => {
println!(
"{}",
"No default project for cloud agents yet — let's set one up.".dimmed()
);
crate::commands::cloud_agent::setup::command(Default::default()).await?;
*prefs = AgentPrefs::load_in(home).unwrap_or_default();
match prefs.default_project.clone() {
Some(default) => {
names::Target::new((default.project_id, default.environment_id), true)
}
None => names::Target::new(
resolve_project_and_env(configs, client, None, None).await?,
false,
),
}
}
TargetSource::Ask => names::Target::new(
resolve_project_and_env(configs, client, None, None).await?,
false,
),
};
target.use_local_name |= args.local_name_project.as_deref() == Some(target.project_id.as_str());
Ok(target)
}
fn combined_provision_payload(
credential: Option<&[u8]>,
tarball: &[u8],
) -> (Vec<u8>, Option<usize>) {
let len = credential.map(<[u8]>::len);
let mut payload = Vec::with_capacity(len.unwrap_or(0) + tarball.len());
if let Some(credential) = credential {
payload.extend_from_slice(credential);
}
payload.extend_from_slice(tarball);
(payload, len)
}
fn is_uuid(value: &str) -> bool {
let bytes = value.as_bytes();
if bytes.len() != 36 {
return false;
}
bytes.iter().enumerate().all(|(i, b)| match i {
8 | 13 | 18 | 23 => *b == b'-',
_ => b.is_ascii_hexdigit(),
})
}
#[derive(Debug, PartialEq, Eq)]
enum TargetSource {
Flags,
Configured(String, String),
Linked(String, String),
Setup,
Ask,
}
fn choose_target(
args: &LaunchArgs,
configured: Option<&DefaultProject>,
linked: Option<(String, String)>,
) -> TargetSource {
if args.project.is_some() || args.environment.is_some() {
return TargetSource::Flags;
}
if let Some((project_id, environment_id)) = linked {
return TargetSource::Linked(project_id, environment_id);
}
if let Some(default) = configured {
return TargetSource::Configured(
default.project_id.clone(),
default.environment_id.clone(),
);
}
match is_stdout_terminal() {
true => TargetSource::Setup,
false => TargetSource::Ask,
}
}
fn stale_link_reason(
lookup: &std::result::Result<queries::RailwayProject, RailwayError>,
environment_id: &str,
) -> Option<&'static str> {
match lookup {
Err(RailwayError::ProjectNotFound) => Some("project that no longer exists"),
Err(_) => None,
Ok(project) if project.deleted_at.is_some() => Some("project that was deleted"),
Ok(project) => {
let live = project
.environments
.edges
.iter()
.any(|edge| edge.node.id == environment_id && edge.node.deleted_at.is_none());
(!live).then_some("environment that no longer exists")
}
}
}
async fn resolve_agent(
configs: &mut Configs,
client: &reqwest::Client,
args: &LaunchArgs,
target: &names::Target,
harness: Agent,
progress: &dyn Progress,
access: &RelayAccess,
) -> Result<(ca::Agent, bool, Option<std::path::PathBuf>)> {
let environment_id = target.environment_id.as_str();
let backboard = configs.get_backboard();
if args.agent_id.is_some() && (args.bootstrap.is_some() || args.no_bootstrap) {
bail!(
"Bootstrap options create a new VM and cannot be used when connecting to an existing agent."
);
}
let candidate = match (
&args.agent_id,
args.new || args.bootstrap.is_some() || args.no_bootstrap,
) {
(Some(id), _) => Some(id.clone()),
(None, true) => None,
(None, false) => match configs.get_code_agent(environment_id) {
Some(id) => Some(id),
None => sole_owned_agent_id(client, &backboard, environment_id).await?,
},
};
if let Some(id) = candidate {
let agent = ca::get(client, &backboard, environment_id, &id)
.await?
.ok_or_else(|| anyhow!(
"Agent {id} is unavailable in this environment. Check `railway ca list`, or use `railway code --new` to create a separate agent."
))?;
let (ready, probe_master) =
ready_existing_agent(client, &backboard, environment_id, agent, progress, access)
.await?;
warn_ignored_variables(args, progress);
if !args.bootstrap_setup {
configs.set_code_agent(environment_id, &ready.id);
configs.write()?;
}
return Ok((ready, false, probe_master));
}
if args.connection_json && args.agent_id.is_some() {
bail!(
"The selected cloud agent is unavailable. Refresh the agent list before reconnecting."
);
}
let bootstrap = crate::controllers::agent_bootstrap::resolve_for_create(
configs,
client,
&backboard,
environment_id,
args.bootstrap.as_deref(),
args.no_bootstrap,
)
.await?;
let variables = create_variables(args)?;
let name = names::for_launch(client, configs, args, harness, target).await?;
if let Some(b) = &bootstrap {
progress.step(&format!("Restoring bootstrap '{}'", b.name));
} else {
progress.step("Creating a cloud agent");
}
let create_started = std::time::Instant::now();
let create = post_graphql::<mutations::CloudAgentCreate, _>(
client,
crate::controllers::agent_bootstrap::internal_url(&backboard),
mutations::cloud_agent_create::Variables {
input: mutations::cloud_agent_create::CloudAgentCreateInput {
environment_id: environment_id.to_owned(),
name,
variables,
code_endpoint: create_code_endpoint(args),
cloud_agent_checkpoint_id: None,
agent_bootstrap_id: bootstrap.map(|b| b.id),
},
},
)
.await;
ssh_tel::record_stage("create_mutation", create_started.elapsed(), create.is_ok());
let created = match create {
Ok(res) => res.cloud_agent_create,
Err(e) => return Err(e.into()),
};
configs.set_code_agent(environment_id, &created.id);
configs.write()?;
match wait_until_connectable(
client,
&backboard,
environment_id,
&created.id,
access,
std::time::Duration::from_millis(350),
)
.await
{
Ok((running, probe_master)) => {
progress.note(&format!("Created agent {}", running.name));
Ok((running, true, probe_master))
}
Err(e) => {
progress.finish();
Err(e)
}
}
}
fn create_code_endpoint(
args: &LaunchArgs,
) -> Option<mutations::cloud_agent_create::CloudAgentCodeEndpointInput> {
(args.code_endpoint || args.code_port.is_some()).then(|| {
mutations::cloud_agent_create::CloudAgentCodeEndpointInput {
port: args.code_port.map(i64::from),
}
})
}
fn create_variables(args: &LaunchArgs) -> Result<Option<serde_json::Value>> {
let mut variables = variables_to_input(&args.env_files, &args.variables)?.unwrap_or_default();
variables.extend(args.boot_variables.clone());
Ok(ca::with_default_variables(Some(serde_json::to_value(
variables,
)?)))
}
fn warn_ignored_variables(args: &LaunchArgs, progress: &dyn Progress) {
use colored::Colorize;
if !args.variables.is_empty() || !args.env_files.is_empty() {
progress.note(
&"Note: --variable/--env-file only apply when an agent is created — reusing this environment's. Add --new to create with these variables."
.yellow()
.to_string(),
);
}
}
async fn destroy_agent(
configs: &mut Configs,
client: &reqwest::Client,
environment_id: &str,
) -> Result<()> {
use colored::Colorize;
eprintln!(
"{}",
"Note: `railway ca delete` supersedes --rm — it names any agent and confirms first."
.dimmed()
);
let backboard = configs.get_backboard();
let Some(id) = configs.get_code_agent(environment_id) else {
println!("No agent recorded for this environment.");
return Ok(());
};
let name = ca::get(client, &backboard, environment_id, &id)
.await?
.map(|a| a.name);
configs.remove_code_agent(environment_id);
configs.write()?;
match name {
Some(name) => {
post_graphql::<mutations::CloudAgentDelete, _>(
client,
&backboard,
mutations::cloud_agent_delete::Variables { id },
)
.await?;
println!("✓ Deleted agent {name}");
}
None => println!("Agent {id} is already gone."),
}
Ok(())
}
pub fn default_harness() -> Result<&'static str> {
let home = dirs::home_dir().ok_or_else(|| anyhow!("Unable to get home directory"))?;
let mut prefs = AgentPrefs::load_in(&home).unwrap_or_default();
Ok(resolve_agent_choice(&LaunchArgs::default(), &mut prefs, &home)?.slug())
}
const AGENT_ENV_VAR: &str = "RAILWAY_CA_AGENT";
fn resolve_agent_choice(args: &LaunchArgs, prefs: &mut AgentPrefs, home: &Path) -> Result<Agent> {
let flagged: Vec<Agent> = [
(args.codex, Agent::Codex),
(args.opencode, Agent::OpenCode),
(args.opencode2, Agent::OpenCode2),
(args.claude, Agent::Claude),
(args.grok, Agent::Grok),
(args.railway, Agent::Railway),
(args.shell, Agent::Shell),
]
.into_iter()
.filter_map(|(set, agent)| set.then_some(agent))
.collect();
match flagged.as_slice() {
[agent] => return Ok(*agent),
[] => {}
_ => bail!(
"Pick one agent: --codex, --claude, --opencode, --opencode2, --grok, or --railway."
),
}
if let Ok(slug) = std::env::var(AGENT_ENV_VAR) {
let slug = slug.trim().to_lowercase();
if !slug.is_empty() {
let agent = Agent::from_slug(&slug).ok_or_else(|| {
anyhow!(
"{AGENT_ENV_VAR}={slug} is not a known agent (claude, codex, opencode, opencode2, grok, railway, or shell)."
)
})?;
return Ok(agent);
}
}
if let Some(agent) = prefs.agent.as_deref().and_then(Agent::from_slug) {
eprintln!(
"{}",
format!(
"Launching {} — your default configuration in {}. You can change this by running `railway ca setup`.",
agent.display(),
AgentPrefs::path_in(home).display()
)
.dimmed()
);
return Ok(agent);
}
if !is_stdout_terminal() {
bail!(
"No default agent configured. Run `railway ca setup`, pass a flag \
(`railway ca --claude`), or set {AGENT_ENV_VAR}=claude."
);
}
let slug = crate::commands::cloud_agent::setup::prompt_agent(home, None)?;
let agent = Agent::from_slug(&slug).ok_or_else(|| anyhow!("Unknown agent selected: {slug}"))?;
prefs.agent = Some(slug);
match prefs.save_in(home) {
Ok(()) => eprintln!(
"{}",
"Saved as your default (`railway ca setup` to change).".dimmed()
),
Err(err) => eprintln!("{}", format!("Couldn't save your choice: {err}").yellow()),
}
Ok(agent)
}
pub trait Progress: Send + Sync {
fn step(&self, text: &str);
fn note(&self, text: &str);
fn finish(&self);
}
#[derive(Default)]
pub struct CliProgress {
spinner: std::sync::Mutex<Option<indicatif::ProgressBar>>,
}
impl Progress for CliProgress {
fn step(&self, text: &str) {
let mut slot = self.spinner.lock().unwrap_or_else(|e| e.into_inner());
if let Some(previous) = slot.take() {
previous.finish_and_clear();
}
*slot = Some(create_shimmer_spinner(text));
}
fn note(&self, text: &str) {
let slot = self.spinner.lock().unwrap_or_else(|e| e.into_inner());
match slot.as_ref() {
Some(spinner) => spinner.suspend(|| eprintln!("{text}")),
None => eprintln!("{text}"),
}
}
fn finish(&self) {
let mut slot = self.spinner.lock().unwrap_or_else(|e| e.into_inner());
if let Some(spinner) = slot.take() {
spinner.finish_and_clear();
}
}
}
pub struct Prepared {
pub remote_cmd: String,
pub ssh_target: String,
pub identity: Option<std::path::PathBuf>,
pub relay_opts: Vec<String>,
pub agent_id: String,
pub agent_name: String,
pub environment_id: String,
pub harness: &'static str,
pub created: bool,
}
pub struct ResolvedLaunch {
pub project_id: String,
pub environment_id: String,
pub(crate) local_name_project: Option<String>,
pub harness: &'static str,
}
pub async fn resolve_launch(
args: &LaunchArgs,
configs: &mut Configs,
client: &reqwest::Client,
) -> Result<ResolvedLaunch> {
let home = dirs::home_dir().ok_or_else(|| anyhow!("Unable to get home directory"))?;
let mut prefs = AgentPrefs::load_in(&home).unwrap_or_default();
let target = resolve_target(configs, client, args, &mut prefs, &home).await?;
let harness = resolve_agent_choice(args, &mut prefs, &home)?.slug();
Ok(ResolvedLaunch {
local_name_project: target.local_name_project(),
project_id: target.project_id,
environment_id: target.environment_id,
harness,
})
}
pub async fn launch(args: LaunchArgs) -> Result<()> {
use colored::Colorize;
if args.connection_json {
bail!("--connection-json requires railway code --codex or --opencode2 [connect].");
}
if args.rm {
let mut configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let (_project_id, environment_id) =
resolve_project_and_env(&mut configs, &client, args.project, args.environment).await?;
return destroy_agent(&mut configs, &client, &environment_id).await;
}
eprintln!(
"{}",
"Warning: Railway cloud agents are experimental and APIs may change or break during testing."
.yellow()
);
let progress = CliProgress::default();
let ensure_fut = async {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
crate::commands::cloud_agent::access::ensure_enabled(&client, &configs).await
};
let prepare_fut = prepare(&args, &progress, SessionStyle::FullTerminal);
tokio::pin!(ensure_fut);
tokio::pin!(prepare_fut);
let prepared = tokio::select! {
enabled = &mut ensure_fut => {
enabled?;
prepare_fut.await?
}
prepared = &mut prepare_fut => {
ensure_fut.await?;
prepared?
}
};
progress.finish();
println!("Launching {}…", prepared.harness);
let exit_code = run_session(&prepared)?;
ssh_tel::drain_detached(std::time::Duration::from_secs(2)).await;
if std::io::stdout().is_terminal() {
use std::io::Write;
let mut out = std::io::stdout();
let _ = out.write_all(TERMINAL_RESET.as_bytes());
let _ = out.flush();
}
println!(
"\nDisconnected — agent {} is still running. `railway ca sleep {}` stops the compute bill.",
prepared.agent_name.cyan(),
prepared.agent_name
);
if prepared.created {
println!("Agents persist between runs — this one is yours until you --rm it.");
}
println!("Get back in:");
if prepared.harness == "shell" {
println!(
" railway ca ssh {} -- bash # wakes it and opens a plain shell",
prepared.agent_name
);
} else {
println!(
" railway code --{} # wakes it and drops back into {}",
prepared.harness, prepared.harness
);
}
println!(
" railway ca ssh {} # same, by name — and reattaches your session",
prepared.agent_name
);
println!(
" railway ca ssh {} -- bash # plain shell",
prepared.agent_name
);
println!("Destroy it:");
println!(" railway ca delete {}", prepared.agent_name);
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
pub fn run_session(prepared: &Prepared) -> Result<i32> {
let cmd = vec![prepared.remote_cmd.clone()];
let code = run_native_ssh_with_opts(
&prepared.ssh_target,
Some(&cmd),
prepared.identity.as_deref(),
None,
&prepared.relay_opts,
);
crate::commands::ssh::native::clear_mouse_tracking();
code
}
pub async fn prepare(
args: &LaunchArgs,
progress: &dyn Progress,
style: SessionStyle,
) -> Result<Prepared> {
let start = std::time::Instant::now();
let home = dirs::home_dir().ok_or_else(|| anyhow!("Unable to get home directory"))?;
let mut prefs = AgentPrefs::load_in(&home).unwrap_or_default();
let agent = match resolve_agent_choice(args, &mut prefs, &home) {
Ok(agent) => agent,
Err(err) => {
crate::commands::cloud_agent::telemetry::track_launch_outcome(
"unresolved",
None,
start.elapsed(),
Some(&format!("{err:#}")),
)
.await;
return Err(err);
}
};
let result = prepare_inner(args, progress, agent, prefs, &home, style).await;
if !args.app_mode
&& let Ok(prepared) = &result
&& let Err(error) =
saved_config::SavedConfig::from_prepared(prepared).and_then(|saved| saved.save())
{
progress.note(&format!(
"Could not save connection details for railway code get-config: {error:#}"
));
}
ssh_tel::flush_stages("cloud_agent_launch");
match &result {
Ok(prepared) => crate::commands::cloud_agent::telemetry::track_launch_outcome_detached(
agent.slug(),
Some(prepared.created),
start.elapsed(),
),
Err(e) => {
crate::commands::cloud_agent::telemetry::track_launch_outcome(
agent.slug(),
None,
start.elapsed(),
Some(&format!("{e:#}")),
)
.await
}
}
result
}
async fn prepare_inner(
args: &LaunchArgs,
progress: &dyn Progress,
agent: Agent,
mut prefs: AgentPrefs,
home: &Path,
style: SessionStyle,
) -> Result<Prepared> {
let pending = match agent {
Agent::Codex | Agent::Grok | Agent::OpenCode | Agent::OpenCode2 => {
ssh_tel::timed_for("cloud_agent_launch", "credential", async {
local_signin(agent, home)
})
.await?
}
Agent::Claude => {
ssh_tel::timed_for("cloud_agent_launch", "credential", async {
claude_credentials_cheap(args.refresh_auth)
})
.await?
}
Agent::Railway | Agent::Shell => PendingAuth::None,
};
if args.bootstrap_setup && matches!(pending, PendingAuth::MintClaude) {
bail!(
"Claude sign-in was not cached. Sign in with `railway code --claude`, then retry bootstrap setup."
);
}
match pending {
PendingAuth::Ready { ref source, .. } => progress.note(&format!(
"Using your {} credential ({source}) on the agent",
agent.display()
)),
PendingAuth::SignInOnAgent { ref note } => progress.note(note),
PendingAuth::None if agent == Agent::Shell => {
progress.note("No coding agent — opening a plain shell on the VM")
}
PendingAuth::None => progress.note("Using the agent's own integrated Railway credentials"),
PendingAuth::MintClaude => {}
}
let packed_skills = ssh_tel::timed_for("cloud_agent_launch", "skills_pack", async {
skills_sync::pack(&prefs, home, &|note| progress.note(note))
})
.await?;
if let Some(packed) = &packed_skills {
progress.note(&format!(
"Including {} of your skills ({})",
packed.names.len(),
packed.source_dir.display()
));
}
let packed_mcp = match std::env::current_dir().map(|cwd| mcp_sync::pack(&prefs, &cwd)) {
Ok(Ok(packed)) => packed,
Ok(Err(err)) => {
if args.bootstrap_setup {
return Err(err.context("Could not copy project MCP configuration"));
}
progress.note(&format!("Skipping MCP import: {err:#}"));
None
}
Err(_) => None,
};
if let Some(packed) = &packed_mcp {
progress.note(&format!(
"Including {} MCP servers from the project ({})",
packed.names.len(),
packed.source_path.display()
));
}
let mut configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let (target_res, identity) = tokio::join!(
ssh_tel::timed_for(
"cloud_agent_launch",
"resolve_target",
resolve_target(&mut configs, &client, args, &mut prefs, home),
),
ssh_tel::timed_for("cloud_agent_launch", "ssh_key", async {
let key_configs = Configs::new()?;
crate::commands::ssh::native::ensure_ssh_key_noninteractive(&client, &key_configs).await
}),
);
let launch_target = target_res?;
let environment_id = launch_target.environment_id.clone();
let identity = match identity {
Ok(identity) => identity,
Err(err) if args.bootstrap_setup => return Err(err),
Err(_) => {
let key_configs = Configs::new()?;
ssh_tel::timed_for(
"cloud_agent_launch",
"ssh_key_interactive",
ensure_ssh_key_quiet(&client, &key_configs),
)
.await?
}
};
let relay = ssh_tel::timed_for("cloud_agent_launch", "relay", async { relay_ssh() }).await?;
let access = RelayAccess {
identity: identity.clone(),
relay_opts: relay.opts.clone(),
};
let (cloud_agent, created, probe_master) = ssh_tel::timed_for(
"cloud_agent_launch",
"resolve_agent",
resolve_agent(
&mut configs,
&client,
args,
&launch_target,
agent,
progress,
&access,
),
)
.await?;
if !args.bootstrap_setup {
configs.set_code_agent(&environment_id, &cloud_agent.id);
configs.write()?;
}
let target = format!("agent:{environment_id}:{}", cloud_agent.id);
let auth = match pending {
PendingAuth::Ready { line, source } => Some((line, source)),
PendingAuth::SignInOnAgent { .. } | PendingAuth::None => None,
PendingAuth::MintClaude => {
let needs_probe = !created && !args.refresh_auth;
let inherit = if needs_probe {
let probe = ssh_tel::timed_for("cloud_agent_launch", "claude_probe", async {
ssh_plumbing(
&target,
CLAUDE_CREDENTIAL_PROBE,
identity.as_deref(),
None,
&relay,
None,
)
})
.await?;
String::from_utf8_lossy(&probe).contains("CRED-PRESENT")
} else {
false
};
if inherit {
progress.note(
"Reusing the Claude credential already on this agent (--refresh-auth to replace it)",
);
None
} else {
match ssh_tel::timed_for("cloud_agent_launch", "claude_mint", async {
mint_claude_credentials()
})
.await?
{
Some((line, source)) => {
progress.note(&format!(
"Using your Claude Code credential ({source}) on the agent"
));
Some((line, source))
}
None => {
progress.note(&claude_sign_in_note());
None
}
}
}
}
};
progress.step("Finalizing Configuration...");
let (master_socket, mux_provision) = match probe_master {
Some(socket) => {
let client_opts = mux_client_opts(&socket);
(socket, client_opts)
}
None => {
let socket = mux_socket();
let master_opts = mux_master_opts(&socket, "30s");
(socket, master_opts)
}
};
let mux_client = mux_client_opts(&master_socket);
let provision_relay = {
let mut r = relay.clone();
r.opts.extend(mux_provision);
r
};
let provision = async {
let target = target.clone();
let identity = identity.clone();
let relay = provision_relay.clone();
let master_socket = master_socket.clone();
let app_mode = args.app_mode;
let skills_note = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let notes = skills_note.clone();
let result = tokio::task::spawn_blocking(move || -> Result<()> {
let push = |line: String| {
if let Ok(mut n) = notes.lock() {
n.push(line);
}
};
let skills_note = |out: &str| {
let reason = if out.contains("SKILLS-NO-TAR") {
"the agent has no `tar`"
} else if out.contains("SKILLS-EXTRACT-FAILED") {
"the transfer did not unpack"
} else {
"the sync did not report success"
};
format!(
"Couldn't sync your skills onto the agent ({reason}); continuing without them."
)
};
let mcp_note = |out: &str| {
let reason = if out.contains("MCP-NO-JQ") {
"the agent has no `jq`"
} else if out.contains("MCP-BAD-JSON") {
"the payload did not survive the trip"
} else if out.contains("MCP-MERGE-FAILED") {
"a harness config would not merge"
} else {
"the sync did not report success"
};
format!(
"Couldn't sync the project's MCP servers ({reason}); continuing without them."
)
};
let sync_mcp = |packed: &mcp_sync::PackedMcp| -> Result<()> {
let out = ssh_plumbing(
&target,
&mcp_sync::provision_script(&packed.hash),
identity.as_deref(),
Some(&packed.payload),
&relay,
Some(&master_socket),
)?;
let out = String::from_utf8_lossy(&out);
if !out.contains("MCP-OK") {
push(mcp_note(&out));
}
Ok(())
};
let check_ready = |out: &str| -> Result<()> {
if out.contains("AGENT-READY") {
Ok(())
} else if out.contains("AGENT-MISSING") {
bail!(
"`{}` was not found on the agent (PATH: ~/.local/bin, ~/.opencode/bin, ~/.grok/bin, mise shims). The harness could not be prepared; report this with the agent id.",
agent.name()
)
} else {
bail!(
"Provisioning produced no status marker — the connection likely dropped mid-script."
)
}
};
if created && let Some(packed) = &packed_skills {
let (payload, credential_len) = combined_provision_payload(
auth.as_ref().map(|(line, _)| line.as_slice()),
&packed.tarball,
);
let out = ssh_plumbing(
&target,
&provision_script_with_skills(agent, credential_len, app_mode, &packed.hash),
identity.as_deref(),
Some(&payload),
&relay,
Some(&master_socket),
)?;
let out = String::from_utf8_lossy(&out);
check_ready(&out)?;
if !out.contains("SKILLS-OK") {
push(skills_note(&out));
}
if let Some(packed) = &packed_mcp {
sync_mcp(packed)?;
}
return Ok(());
}
let out = ssh_plumbing(
&target,
&provision_script(agent, auth.is_some(), app_mode),
identity.as_deref(),
auth.as_ref().map(|(line, _)| line.as_slice()),
&relay,
Some(&master_socket),
)?;
let out = String::from_utf8_lossy(&out);
check_ready(&out)?;
if let Some(packed) = packed_skills {
if skills_sync::parse_remote_hash(&out).as_deref() != Some(packed.hash.as_str()) {
let out = ssh_plumbing(
&target,
&skills_sync::provision_script(&packed.hash),
identity.as_deref(),
Some(&packed.tarball),
&relay,
Some(&master_socket),
)?;
let out = String::from_utf8_lossy(&out);
if !out.contains("SKILLS-OK") {
push(skills_note(&out));
}
}
}
if let Some(packed) = &packed_mcp
&& mcp_sync::parse_remote_hash(&out).as_deref() != Some(packed.hash.as_str())
{
sync_mcp(packed)?;
}
Ok(())
})
.await
.map_err(anyhow::Error::from)
.and_then(|r| r);
let notes = skills_note.lock().unwrap_or_else(|e| e.into_inner());
for line in notes.iter() {
progress.note(line);
}
if args.bootstrap_setup && !notes.is_empty() {
bail!(
"Bootstrap configuration sync was incomplete: {}",
notes.join("; ")
);
}
result
};
ssh_tel::timed_for("cloud_agent_launch", "provision", provision).await?;
let env_prefix = harness_env_prefix();
let remote_cmd = remote_command(
agent,
&env_prefix,
args.initial_prompt.as_deref(),
args.resume_session_id.as_deref(),
&args.agent_args,
style,
);
Ok(Prepared {
remote_cmd,
ssh_target: target,
identity,
relay_opts: {
let mut opts = relay.opts;
opts.extend(mux_client);
opts
},
agent_id: cloud_agent.id,
agent_name: cloud_agent.name,
environment_id,
harness: agent.slug(),
created,
})
}
pub struct ConnectInfo {
pub ssh_target: String,
pub identity: Option<std::path::PathBuf>,
pub relay_opts: Vec<String>,
}
pub async fn connect_info(environment_id: &str, agent_id: &str) -> Result<ConnectInfo> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
let identity = ensure_ssh_key_quiet(&client, &configs).await?;
let relay = relay_ssh()?;
Ok(ConnectInfo {
ssh_target: format!("agent:{environment_id}:{agent_id}"),
identity,
relay_opts: relay.opts,
})
}
const FLUSH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
pub async fn flush_disk(environment_id: &str, agent_id: &str) {
let flush = async {
let info = connect_info(environment_id, agent_id).await.ok()?;
let mut opts = info.relay_opts;
opts.push("-o".to_string());
opts.push(format!("ConnectTimeout={}", FLUSH_TIMEOUT.as_secs()));
tokio::task::spawn_blocking(move || {
run_native_ssh_captured(
&info.ssh_target,
"sync",
info.identity.as_deref(),
None,
&opts,
)
})
.await
.ok()
};
let _ = tokio::time::timeout(FLUSH_TIMEOUT, flush).await;
}
pub async fn kill_session(environment_id: &str, agent_id: &str, session_name: &str) -> Result<()> {
let info = connect_info(environment_id, agent_id).await?;
let script = kill_script(session_name);
let relay = relay_ssh()?;
let out = tokio::task::spawn_blocking(move || {
ssh_plumbing(
&info.ssh_target,
&script,
info.identity.as_deref(),
None,
&relay,
None,
)
})
.await??;
let out = String::from_utf8_lossy(&out);
match out.split("KILLED:").nth(1).and_then(|n| {
n.trim()
.lines()
.next()
.and_then(|n| n.trim().parse::<u32>().ok())
}) {
Some(0) => bail!("nothing was running under that session"),
Some(_) => Ok(()),
None => bail!("the agent did not confirm the session ended"),
}
}
fn kill_script(session_name: &str) -> String {
format!(
r#"killed=0
for p in /proc/[0-9]*; do
pid="${{p#/proc/}}"
[ "$pid" = "$$" ] && continue
if grep -qzxF 'RAILWAY_DURABLE_SESSION_NAME={session_name}' "$p/environ" 2>/dev/null; then
kill -TERM "$pid" 2>/dev/null && killed=$((killed+1))
fi
done
echo "KILLED:$killed""#
)
}
pub fn claude_needs_local_mint() -> bool {
!matches!(
claude_credentials_cheap(false),
Ok(PendingAuth::Ready { .. } | PendingAuth::SignInOnAgent { .. })
)
}
pub fn ensure_claude_credential_cached(harness: &str) -> Result<()> {
if harness != "claude" {
return Ok(());
}
if let PendingAuth::MintClaude = claude_credentials_cheap(false)? {
match mint_claude_credentials()? {
Some((_line, source)) => {
eprintln!("Using your Claude Code credential ({source}) on the agent")
}
None => eprintln!("{}", claude_sign_in_note()),
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testkit::MockBackboard;
use clap::Parser;
use serde_json::json;
#[test]
fn bootstrap_flags_require_a_new_vm_and_survive_tui_retargeting() {
let args = LaunchArgs::try_parse_from(["code", "--codex", "--bootstrap", "dev"]).unwrap();
assert!(!args.is_bare());
let args = args.retargeted("project".into(), "env".into(), "codex", true, None, None);
assert_eq!(args.bootstrap.as_deref(), Some("dev"));
let mut connect = LaunchArgs::try_parse_from([
"code",
"--codex",
"--bootstrap",
"dev",
"connect",
"existing",
])
.unwrap();
assert!(
connect
.prepare_code_launch()
.unwrap_err()
.to_string()
.contains("cannot be used with connect")
);
for flags in [
vec!["code", "--bootstrap", "dev", "--no-bootstrap"],
vec![
"code",
"--codex",
"--agent",
"existing",
"--bootstrap",
"dev",
],
vec!["code", "--codex", "--agent", "existing", "--no-bootstrap"],
] {
assert!(LaunchArgs::try_parse_from(flags).is_err());
}
}
#[tokio::test]
async fn bootstrap_launch_passes_selected_bootstrap_to_create() {
for harness in [
Agent::Codex,
Agent::OpenCode,
Agent::OpenCode2,
Agent::Claude,
Agent::Grok,
Agent::Railway,
Agent::Shell,
] {
for (override_name, clean, expected) in [
(None, false, Some("default")),
(Some("dev"), false, Some("dev")),
(None, true, None),
] {
let server = MockBackboard::spawn();
let dir = tempfile::tempdir().unwrap();
let mut configs = server.configs(&dir);
let row = |name| {
json!({"id": name, "name": name, "environmentId": "env", "status": "READY",
"failureReason": null, "updatedAt": "2026-09-11T00:00:00Z",
"activeVersion": {"sourceCloudAgentId": "codex-source", "checkpoint": {"id": "disk-checkpoint"}}})
};
configs
.set_agent_bootstrap_default("env", "default", false)
.await
.unwrap();
server.stub(
"AgentBootstraps",
json!({"agentBootstraps": [row("default"), row("dev")]}),
);
server.stub_graphql_error("CloudAgentCreate", "creation reached");
let args = LaunchArgs {
new: true,
name: Some("fresh".into()),
bootstrap: override_name.map(str::to_owned),
no_bootstrap: clean,
..Default::default()
}
.retargeted(
"project".into(),
"env".into(),
harness.slug(),
true,
None,
None,
);
let error = resolve_agent(
&mut configs,
&reqwest::Client::new(),
&args,
&names::Target::new(("project".into(), "env".into()), false),
harness,
&CliProgress::default(),
&RelayAccess {
identity: None,
relay_opts: vec![],
},
)
.await
.unwrap_err();
assert!(error.to_string().contains("creation reached"), "{error}");
let request = &server.variables_for("CloudAgentCreate")[0]["input"];
assert_eq!(request["agentBootstrapId"].as_str(), expected);
assert_eq!(request["environmentId"], "env");
if clean {
assert!(server.variables_for("AgentBootstraps").is_empty());
}
}
}
}
#[tokio::test]
async fn bootstrap_setup_does_not_remember_its_temporary_vm() {
let server = MockBackboard::spawn();
let dir = tempfile::tempdir().unwrap();
let mut configs = server.configs(&dir);
configs.set_code_agent("env", "existing");
configs.write().unwrap();
server.stub("CloudAgent", json!({"cloudAgent": {"id": "setup", "name": "setup", "status": "RUNNING", "projectId": "project", "environmentId": "env", "createdAt": "2026-09-11T00:00:00Z"}}));
let args = LaunchArgs {
agent_id: Some("setup".into()),
bootstrap_setup: true,
app_mode: true,
..Default::default()
};
let (agent, created, _) = resolve_agent(
&mut configs,
&reqwest::Client::new(),
&args,
&names::Target::new(("project".into(), "env".into()), false),
Agent::Railway,
&CliProgress::default(),
&RelayAccess {
identity: None,
relay_opts: vec![],
},
)
.await
.unwrap();
assert_eq!(agent.id, "setup");
assert!(!created);
configs.reload().unwrap();
assert_eq!(configs.get_code_agent("env").as_deref(), Some("existing"));
}
#[tokio::test]
async fn an_unusable_selected_vm_is_never_replaced() {
for selection in ["explicit", "remembered", "sole"] {
for status in [
"FAILED",
"CRASHED",
"DELETING",
"FUTURE_STATE",
"missing",
"lookup_error",
] {
let server = MockBackboard::spawn();
let dir = tempfile::tempdir().unwrap();
let mut configs = server.configs(&dir);
let mut args = LaunchArgs::default();
if selection == "explicit" {
args.agent_id = Some("existing".into());
} else if selection == "remembered" {
configs.set_code_agent("env", "existing");
}
configs.write().unwrap();
let saved = std::fs::read(dir.path().join("config.json")).unwrap();
let node = json!({
"id": "existing", "name": "my-agent", "status": status,
"projectId": "project", "environmentId": "env",
"createdAt": "2026-09-10T00:00:00Z"
});
server.stub("CloudAgents", json!({"cloudAgents": [node.clone()]}));
match status {
"missing" => server.stub("CloudAgent", json!({"cloudAgent": null})),
"lookup_error" => {
server.stub_graphql_error("CloudAgent", "temporarily unavailable")
}
_ => server.stub("CloudAgent", json!({"cloudAgent": node})),
}
let error = resolve_agent(
&mut configs,
&reqwest::Client::new(),
&args,
&names::Target::new(("project".into(), "env".into()), false),
Agent::Claude,
&CliProgress::default(),
&RelayAccess {
identity: None,
relay_opts: vec![],
},
)
.await
.unwrap_err();
assert!(
!error.to_string().contains("no scripted response"),
"{selection}/{status}: {error}"
);
let operations: Vec<_> = server
.requests()
.iter()
.map(|r| r["operationName"].as_str().unwrap().to_owned())
.collect();
let expected = if selection == "sole" {
vec!["CloudAgents", "CloudAgent"]
} else {
vec!["CloudAgent"]
};
assert_eq!(operations, expected, "{selection}/{status}");
assert_eq!(
std::fs::read(dir.path().join("config.json")).unwrap(),
saved
);
if selection == "remembered" {
assert_eq!(configs.get_code_agent("env").as_deref(), Some("existing"));
}
}
}
}
#[tokio::test]
async fn creation_is_reached_only_for_new_or_an_empty_inventory() {
for new in [false, true] {
let server = MockBackboard::spawn();
let dir = tempfile::tempdir().unwrap();
let mut configs = server.configs(&dir);
if new {
configs.set_code_agent("env", "existing");
}
server.stub("CloudAgents", json!({"cloudAgents": []}));
server.stub_graphql_error("CloudAgentCreate", "creation reached");
let args = LaunchArgs {
new,
name: Some("fresh-agent".into()),
..Default::default()
};
let error = resolve_agent(
&mut configs,
&reqwest::Client::new(),
&args,
&names::Target::new(("project".into(), "env".into()), false),
Agent::Claude,
&CliProgress::default(),
&RelayAccess {
identity: None,
relay_opts: vec![],
},
)
.await
.unwrap_err();
assert!(error.to_string().contains("creation reached"), "{error}");
assert_eq!(server.variables_for("CloudAgentCreate").len(), 1);
assert!(server.variables_for("CloudAgent").is_empty());
assert_eq!(server.variables_for("CloudAgents").len(), usize::from(!new));
}
}
#[test]
fn an_ordinary_launch_opens_in_the_pane() {
for argv in [
vec!["code"],
vec!["code", "--claude"],
vec!["code", "--new", "--name", "api"],
vec!["code", "-p", "proj_1", "-e", "env_prod"],
vec!["code", "--variable", "K=V", "--refresh-auth"],
] {
let args = LaunchArgs::parse_from(&argv);
assert!(args.pane_shaped(), "{argv:?} should open in the pane");
}
}
#[test]
fn destroying_and_exec_take_the_terminal_instead() {
assert!(!LaunchArgs::parse_from(["code", "--rm"]).pane_shaped());
assert!(
!LaunchArgs::parse_from(["code", "--codex", "--", "exec", "explain this"])
.pane_shaped()
);
}
#[test]
fn retargeting_overrides_what_the_tui_decides() {
let args = LaunchArgs::parse_from(["code", "--codex", "-p", "old_p", "-e", "old_e"])
.retargeted(
"new_p".into(),
"new_e".into(),
"claude",
true,
Some("fix the tests".into()),
Some("ca_1".into()),
);
assert_eq!(args.project.as_deref(), Some("new_p"));
assert_eq!(args.environment.as_deref(), Some("new_e"));
assert_eq!(args.agent_id.as_deref(), Some("ca_1"));
assert_eq!(args.initial_prompt.as_deref(), Some("fix the tests"));
assert!(args.new);
assert!(args.claude, "the harness the TUI chose");
assert!(!args.codex, "and only that one");
}
#[test]
fn retargeting_carries_the_flags_the_tui_cannot_ask_for() {
let args = LaunchArgs::parse_from([
"code",
"--new",
"--name",
"api",
"--variable",
"DB=postgres.DATABASE_URL",
"--env-file",
".env",
"--refresh-auth",
])
.retargeted("p".into(), "e".into(), "claude", true, None, None);
assert_eq!(args.name.as_deref(), Some("api"));
assert_eq!(args.variables, ["DB=postgres.DATABASE_URL"]);
assert_eq!(args.env_files, [std::path::PathBuf::from(".env")]);
assert!(args.refresh_auth);
}
fn note_of(pending: PendingAuth) -> String {
match pending {
PendingAuth::SignInOnAgent { note } => note,
PendingAuth::Ready { source, .. } => panic!("expected a fallback, got {source}"),
PendingAuth::MintClaude => panic!("expected a fallback, got a mint"),
PendingAuth::None => panic!("expected a fallback, got a harness needing no credential"),
}
}
#[test]
fn a_missing_local_signin_falls_back_to_signing_in_on_the_agent() {
let home = tempfile::tempdir().unwrap();
let note = note_of(local_signin(Agent::Codex, home.path()).unwrap());
assert!(note.contains("codex login --device-auth"), "{note}");
let note = note_of(local_signin(Agent::Grok, home.path()).unwrap());
assert!(note.contains("Grok"), "{note}");
}
#[test]
fn an_empty_local_signin_falls_back_too() {
let home = tempfile::tempdir().unwrap();
std::fs::create_dir_all(home.path().join(".codex")).unwrap();
std::fs::write(home.path().join(".codex").join("auth.json"), "").unwrap();
note_of(local_signin(Agent::Codex, home.path()).unwrap());
}
#[test]
fn a_local_signin_is_carried_verbatim() {
let home = tempfile::tempdir().unwrap();
let auth = home.path().join(".grok").join("auth.json");
std::fs::create_dir_all(auth.parent().unwrap()).unwrap();
std::fs::write(&auth, r#"{"k":1}"#).unwrap();
match local_signin(Agent::Grok, home.path()).unwrap() {
PendingAuth::Ready { line, source } => {
assert_eq!(line, br#"{"k":1}"#);
assert_eq!(source, auth.display().to_string());
}
_ => panic!("expected the local sign-in to be carried"),
}
}
#[test]
fn opencode_auth_uses_xdg_data_and_preserves_the_provider_map() {
let home = tempfile::tempdir().unwrap();
let xdg = home.path().join("custom data");
assert_eq!(
Agent::OpenCode
.local_signin_path(home.path(), None)
.unwrap(),
home.path().join(".local/share/opencode/auth.json")
);
let path = Agent::OpenCode
.local_signin_path(home.path(), Some(&xdg))
.unwrap();
assert_eq!(path, xdg.join("opencode/auth.json"));
assert!(matches!(
read_local_signin(Agent::OpenCode, Some(&path)).unwrap(),
PendingAuth::SignInOnAgent { .. }
));
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let auth = br#"{"openai":{"type":"api","key":"test-only"},"example":{"type":"oauth","access":"test-access","refresh":"test-refresh","expires":123}}"#;
std::fs::write(&path, auth).unwrap();
let PendingAuth::Ready { line, .. } =
read_local_signin(Agent::OpenCode, Some(&path)).unwrap()
else {
panic!("expected provider credentials")
};
assert_eq!(line, auth);
}
#[cfg(unix)]
#[test]
fn opencode_credential_seed_frames_stdin_and_protects_existing_file() {
use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::process::{Command, Stdio};
for framed in [false, true] {
let data = tempfile::tempdir().unwrap();
let auth_path = data.path().join("opencode/auth.json");
std::fs::create_dir_all(auth_path.parent().unwrap()).unwrap();
std::fs::write(&auth_path, "old auth").unwrap();
std::fs::set_permissions(&auth_path, std::fs::Permissions::from_mode(0o644)).unwrap();
let auth = br#"{"provider":{"type":"api","key":"test"}}"#;
let script = if framed {
format!(
"{}\ncat",
Agent::OpenCode.credential_seed_framed(auth.len())
)
} else {
Agent::OpenCode.credential_seed().to_string()
};
let mut child = Command::new("sh")
.args(["-c", &script])
.env("XDG_DATA_HOME", data.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
stdin.write_all(auth).unwrap();
if framed {
stdin.write_all(b"skills payload").unwrap();
}
drop(stdin);
let output = child.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(std::fs::read(&auth_path).unwrap(), auth);
assert_eq!(
std::fs::metadata(&auth_path).unwrap().permissions().mode() & 0o777,
0o600
);
if framed && cfg!(target_os = "linux") {
assert_eq!(output.stdout, b"skills payload");
}
}
}
#[test]
fn code_harness_launches_create_new_agents_by_default() {
for flag in [
"--codex",
"--opencode",
"--opencode2",
"--claude",
"--grok",
"--railway",
] {
for extra in [
vec![],
vec!["--new"],
vec![
"--name",
"my-box",
"--variable",
"K=V",
"--env-file",
".env",
],
vec!["--", "exec", "explain this codebase"],
] {
let argv = [vec!["code", flag], extra].concat();
let mut args = LaunchArgs::try_parse_from(&argv).unwrap();
args.prepare_code_launch().unwrap();
assert!(args.new, "{argv:?} must create a fresh VM");
}
}
for argv in [
vec!["code", "--codex", "remote"],
vec!["code", "--opencode", "remote"],
vec!["code", "--opencode2", "remote"],
vec!["code", "--codex", "desktop-only"],
vec!["code", "--codex", "--connection-json"],
vec!["code", "--opencode2", "--connection-json"],
vec!["code", "--codex", "desktop-only", "--connection-json"],
] {
let mut args = LaunchArgs::try_parse_from(&argv).unwrap();
assert!(args.prepare_code_launch().unwrap().is_some());
assert!(args.new, "{argv:?} must create a fresh VM");
}
}
#[test]
fn code_connect_and_explicit_targets_do_not_request_new_agents() {
for flag in ["--codex", "--opencode", "--opencode2"] {
for (extra, selector) in [
(vec!["connect"], None),
(vec!["connect", "my-box"], Some("my-box".to_string())),
(
vec!["connect", "--agent", "my-box"],
Some("my-box".to_string()),
),
] {
let mut args =
LaunchArgs::try_parse_from([vec!["code", flag], extra].concat()).unwrap();
assert_eq!(
args.prepare_code_launch().unwrap(),
Some(ClientAction::Connect(selector))
);
assert!(!args.new, "connect must never create a VM");
}
for extra in [vec![], vec!["remote"]] {
let mut args = LaunchArgs::try_parse_from(
[vec!["code", flag, "--agent", "my-box"], extra].concat(),
)
.unwrap();
args.prepare_code_launch().unwrap();
assert!(!args.new);
assert_eq!(args.remote_agent.as_deref(), Some("my-box"));
}
let mut args = LaunchArgs::try_parse_from(["code", flag, "connect", "--new"]).unwrap();
assert!(args.prepare_code_launch().is_err());
}
let mut args =
LaunchArgs::try_parse_from(["code", "--codex", "desktop-only", "--agent", "my-box"])
.unwrap();
assert_eq!(
args.prepare_code_launch().unwrap(),
Some(ClientAction::DesktopOnly)
);
assert!(!args.new);
}
#[test]
fn code_creation_default_is_scoped_to_harness_launches() {
for argv in [
vec!["code"],
vec!["code", "--rm"],
vec!["code", "--codex", "--rm"],
vec!["code", "--claude", "--rm"],
] {
let mut args = LaunchArgs::try_parse_from(&argv).unwrap();
assert_eq!(args.prepare_code_launch().unwrap(), None);
assert!(!args.new, "{argv:?} must retain its existing behavior");
}
let mut args = LaunchArgs::try_parse_from(["code", "--new"]).unwrap();
args.prepare_code_launch().unwrap();
assert!(args.new);
let args = LaunchArgs::try_parse_from(["ca", "--codex"]).unwrap();
assert!(!args.new);
let args = LaunchArgs::for_target(
"project".into(),
"environment".into(),
"codex",
false,
None,
Some("existing-agent".into()),
);
assert!(!args.new);
assert_eq!(args.agent_id.as_deref(), Some("existing-agent"));
}
#[test]
fn connection_json_only_accepts_server_connection_actions() {
for flag in ["--codex", "--opencode2"] {
for extra in [vec![], vec!["connect", "box"], vec!["--new"]] {
let args = LaunchArgs::try_parse_from(
[vec!["code", flag, "--connection-json"], extra].concat(),
)
.unwrap();
assert!(args.connection_json);
assert!(args.client_action().unwrap().is_some());
}
for extra in [vec!["remote"], vec!["--rm"], vec!["--", "run", "hello"]] {
let args = LaunchArgs::try_parse_from(
[vec!["code", flag, "--connection-json"], extra].concat(),
)
.unwrap();
assert!(args.client_action().is_err());
}
}
assert!(LaunchArgs::try_parse_from(["code", "--connection-json"]).is_err());
}
#[test]
fn codex_desktop_only_accepts_backend_setup_flags_and_json() {
for argv in [
vec!["code", "--codex", "desktop-only"],
vec![
"code",
"--codex",
"desktop-only",
"--agent",
"box",
"--dir",
"/app/project",
],
vec![
"code",
"--codex",
"--new",
"desktop-only",
"--name",
"desktop-box",
"--variable",
"A=B",
"--env-file",
".env",
],
vec![
"code",
"--codex",
"--connection-json",
"desktop-only",
"-p",
"project",
"-e",
"env",
],
] {
let args = LaunchArgs::try_parse_from(argv).unwrap();
assert_eq!(
args.client_action().unwrap(),
Some(ClientAction::DesktopOnly)
);
assert!(!args.pane_shaped());
}
}
#[test]
fn desktop_only_rejects_other_harnesses_and_session_arguments() {
for argv in [
vec!["code", "desktop-only"],
vec!["code", "--opencode", "desktop-only"],
vec!["code", "--opencode2", "desktop-only"],
vec!["code", "--grok", "desktop-only"],
vec!["code", "--claude", "desktop-only"],
vec!["code", "--codex", "--claude", "desktop-only"],
vec!["code", "--codex", "desktop-only", "box"],
vec!["code", "--codex", "desktop-only", "connect"],
vec!["code", "--codex", "desktop-only", "--rm"],
vec!["code", "--codex", "desktop-only", "--dir", " "],
] {
let args = LaunchArgs::try_parse_from(argv).unwrap();
assert!(args.client_action().is_err(), "{args:?}");
}
let mut args = LaunchArgs::try_parse_from(["code", "--codex", "desktop-only"]).unwrap();
args.initial_prompt = Some("run tests".into());
assert!(args.client_action().is_err());
}
#[test]
fn client_actions_separate_local_clients_from_cloud_terminal_sessions() {
for flag in ["--codex", "--opencode", "--opencode2"] {
let local =
LaunchArgs::try_parse_from(["code", flag, "--new", "--dir", "/app/project"])
.unwrap();
assert_eq!(local.client_action().unwrap(), Some(ClientAction::Local));
for argv in [
vec!["code", flag, "remote", "--new"],
vec!["code", flag, "--new", "remote"],
] {
let mut remote = LaunchArgs::try_parse_from(argv).unwrap();
assert_eq!(remote.client_action().unwrap(), Some(ClientAction::Remote));
remote.agent_args.clear();
assert!(remote.pane_shaped());
}
for (extra, expected) in [
(vec!["connect"], None),
(vec!["connect", "my-box"], Some("my-box".to_string())),
(
vec!["connect", "--agent", "my-box"],
Some("my-box".to_string()),
),
] {
let args =
LaunchArgs::try_parse_from([vec!["code", flag], extra].concat()).unwrap();
assert_eq!(
args.client_action().unwrap(),
Some(ClientAction::Connect(expected))
);
}
}
}
#[test]
fn opencode_rejects_ambiguous_or_destructive_client_actions() {
for argv in [
vec!["code", "--codex", "--opencode", "connect"],
vec!["code", "--codex", "connect", "--new"],
vec!["code", "--codex", "remote", "--dir", "/app"],
vec!["code", "remote"],
vec!["code", "--opencode", "--opencode2"],
vec!["code", "--opencode", "remote", "extra"],
vec!["code", "--opencode", "remote", "--rm"],
vec!["code", "--opencode", "remote", "--dir", "/app"],
vec!["code", "--opencode", "connect", "--new"],
vec!["code", "--opencode", "connect", "box", "--agent", "other"],
vec!["code", "--opencode", "connect", "box", "extra"],
vec!["code", "--opencode", "connect", "--variable", "A=B"],
vec!["code", "--opencode", "--dir", " "],
] {
assert!(
LaunchArgs::try_parse_from(argv)
.unwrap()
.client_action()
.is_err()
);
}
assert!(
LaunchArgs::try_parse_from(["code", "--opencode", "--new", "--agent", "box"]).is_err()
);
assert_eq!(
LaunchArgs::try_parse_from(["code", "--opencode", "--rm"])
.unwrap()
.client_action()
.unwrap(),
None
);
}
#[test]
fn code_endpoint_creation_is_typed_and_ports_are_validated() {
let mut args = LaunchArgs::try_parse_from(["code", "--opencode2", "--new"]).unwrap();
assert!(create_code_endpoint(&args).is_none());
args.code_endpoint = true;
assert!(create_code_endpoint(&args).unwrap().port.is_none());
args.code_port = Some(5000);
assert_eq!(create_code_endpoint(&args).unwrap().port, Some(5000));
args.boot_variables
.insert("OPENCODE_SERVER_USERNAME".into(), "opencode".into());
let variables = create_variables(&args).unwrap().unwrap();
assert!(variables.get("RAILWAY_CODE_PORT").is_none());
assert_eq!(variables["OPENCODE_SERVER_USERNAME"], "opencode");
assert_eq!(variables["SHELL"], "/bin/bash");
for port in ["0", "1023", "8080", "8790", "65536", "nope"] {
assert!(LaunchArgs::try_parse_from(["code", "--new", "--code-port", port]).is_err());
}
assert!(LaunchArgs::try_parse_from(["code", "--new", "--code-port", "5000"]).is_ok());
assert!(LaunchArgs::try_parse_from(["code", "--code-port", "5000"]).is_err());
}
#[test]
fn explicit_harness_passthrough_keeps_flags_owned_by_the_harness() {
let args = LaunchArgs::try_parse_from([
"code",
"--opencode2",
"--",
"run",
"--server",
"https://server.example",
"--dir",
"/project",
])
.unwrap();
assert_eq!(
args.agent_args,
[
"run",
"--server",
"https://server.example",
"--dir",
"/project"
]
);
assert_eq!(args.client_action().unwrap(), None);
assert_eq!(args.remote_dir, None);
}
#[test]
fn opencode2_launch_seeds_the_shim_and_preserves_prompt_arguments() {
let args = LaunchArgs::for_app_mode("opencode2", None, None);
assert_eq!(
resolve_agent_choice(&args, &mut AgentPrefs::default(), Path::new("/tmp")).unwrap(),
Agent::OpenCode2
);
let script = provision_script(Agent::OpenCode2, false, true);
assert!(script.contains("~/.local/bin/opencode2"));
assert!(script.contains("command -v opencode2"));
assert!(!provision_script(Agent::OpenCode, false, true).contains("opencode-beta/releases"));
let command = remote_command(
Agent::OpenCode2,
"",
Some("explain this project"),
None,
&[],
SessionStyle::Pane,
);
assert!(command.contains("opencode2 --standalone --prompt 'explain this project'"));
}
#[test]
fn opencode_desktop_provisioning_overrides_a_saved_default() {
let home = tempfile::tempdir().unwrap();
let args = LaunchArgs::for_app_mode("opencode", None, None);
let mut prefs = AgentPrefs {
agent: Some("claude".into()),
..Default::default()
};
assert_eq!(
resolve_agent_choice(&args, &mut prefs, home.path()).unwrap(),
Agent::OpenCode
);
assert!(!args.is_bare());
let script = provision_script(Agent::OpenCode, false, args.app_mode);
assert!(script.contains("touch ~/.railway-app-mode"));
assert!(script.contains("command -v opencode"));
assert!(!script.contains("$opencode_data/auth.json"));
let command = remote_command(
Agent::OpenCode,
"",
Some("explain this project"),
None,
&[],
SessionStyle::Pane,
);
assert!(
command.contains("opencode --prompt 'explain this project'"),
"{command}"
);
}
#[test]
fn an_unauthenticated_launch_still_provisions_the_agent() {
for agent in [Agent::Codex, Agent::Claude, Agent::Grok, Agent::OpenCode] {
let script = provision_script(agent, false, false);
assert!(!script.contains("cat > ~/"), "{script}");
assert!(script.contains("railway-code agent autostart"));
assert!(script.contains("AGENT-READY"));
assert!(script.contains(&format!("echo {} > ~/.railway-code-agent", agent.name())));
}
}
#[test]
fn the_claude_fallback_says_how_to_sign_in_on_the_agent() {
let note = claude_sign_in_note();
assert!(note.contains("Claude Code"), "{note}");
assert!(note.contains("/login"), "{note}");
}
#[test]
fn provision_script_delivers_credentials_only() {
let codex = provision_script(Agent::Codex, true, false);
assert!(codex.contains("cat > ~/.codex/auth.json"));
assert!(codex.contains("echo codex > ~/.railway-code-agent"));
let claude = provision_script(Agent::Claude, true, false);
assert!(claude.contains("cat > ~/.claude-code-env"));
assert!(claude.contains("echo claude > ~/.railway-code-agent"));
let grok = provision_script(Agent::Grok, true, false);
assert!(grok.contains("cat > ~/.grok/auth.json"));
assert!(grok.contains("echo grok > ~/.railway-code-agent"));
for script in [&codex, &claude, &grok] {
assert!(script.contains("railway-code agent autostart"));
assert!(script.contains(". \"$HOME/.claude-code-env\""));
assert!(script.contains("AGENT-READY"));
assert!(script.contains("AGENT-MISSING"));
assert!(script.contains("$HOME/.local/bin"));
assert!(script.contains("$HOME/.grok/bin"));
assert!(!script.contains("cd \"$HOME\""));
assert!(!script.contains("cd ~"));
assert!(!script.contains("npm install"));
assert!(!script.contains("install.sh"));
assert!(!script.contains("apt-get"));
assert!(!script.contains("hasCompletedOnboarding"));
assert!(!script.contains("trust_level"));
assert!(!script.contains("yolo"));
assert!(!script.contains("config.toml"));
}
}
#[test]
fn the_carried_claude_token_is_always_sourced() {
for guard in [CLAUDE_ENV_GUARD, COMMON_SEED] {
assert!(
!guard.contains("credentials.json"),
"login must not outrank the carried token: {guard}"
);
assert!(guard.contains(".claude-code-env"), "{guard}");
}
assert!(COMMON_SEED.contains("railway-code agent autostart v4"));
assert!(COMMON_SEED.contains("sed -i '/# railway-code agent autostart/,/^fi$/d'"));
}
#[test]
fn app_mode_and_session_mode_disagree_about_the_autostart() {
let app = provision_script(Agent::Claude, false, true);
assert!(app.contains("touch ~/.railway-app-mode"));
assert!(app.contains("rm -f ~/.railway-code-agent"));
assert!(!app.contains("> ~/.railway-code-agent"));
let session = provision_script(Agent::Claude, false, false);
assert!(session.contains("rm -f ~/.railway-app-mode"));
assert!(session.contains("echo claude > ~/.railway-code-agent"));
assert!(!session.contains("touch ~/.railway-app-mode"));
assert!(COMMON_SEED.contains(r#"[ ! -f "$HOME/.railway-app-mode" ]"#));
}
#[test]
fn the_cached_token_source_carries_its_age() {
assert_eq!(cached_token_source(None), "cached setup-token");
assert_eq!(cached_token_source(Some(0)), "cached setup-token");
assert_eq!(
cached_token_source(Some(12)),
"cached setup-token from 12d ago"
);
assert_eq!(
cached_token_source(Some(92)),
"cached setup-token from 92d ago — --refresh-auth re-mints"
);
}
#[test]
fn provision_script_omits_the_seed_when_reusing_a_credential() {
let claude = provision_script(Agent::Claude, false, false);
assert!(!claude.contains("cat > ~/.claude-code-env"));
assert!(claude.contains("$HOME/.local/bin"));
assert!(claude.contains("railway-code agent autostart"));
assert!(claude.contains("echo claude > ~/.railway-code-agent"));
assert!(claude.contains("AGENT-READY"));
for (agent, seed) in [
(Agent::Codex, "cat > ~/.codex/auth.json"),
(Agent::Grok, "cat > ~/.grok/auth.json"),
] {
assert!(provision_script(agent, true, false).contains(seed));
assert!(!provision_script(agent, false, false).contains(seed));
}
}
#[test]
fn railway_needs_no_credential_seed() {
let script = provision_script(Agent::Railway, false, false);
for other_seed in [
"cat > ~/.claude-code-env",
"cat > ~/.codex/auth.json",
"cat > ~/.grok/auth.json",
] {
assert!(!script.contains(other_seed), "{script}");
}
assert!(script.contains("echo railway-agent-tui > ~/.railway-code-agent"));
assert!(script.contains("if command -v railway-agent-tui"));
assert!(script.contains("railway-code agent autostart"));
assert!(script.contains("AGENT-READY"));
}
#[test]
fn combined_provision_frames_the_credential() {
let script = provision_script_with_skills(Agent::Claude, Some(42), false, "abc123");
assert!(
script.contains("head -c 42 > ~/.claude-code-env"),
"{script}"
);
assert!(!script.contains("cat > ~/.claude-code-env"), "{script}");
assert!(script.contains(r#"cat > "$payload""#), "{script}");
assert!(script.contains("'abc123'"), "{script}");
}
#[test]
fn combined_provision_reports_ready_before_skills() {
let script = provision_script_with_skills(Agent::Claude, Some(10), false, "h");
let ready = script.find("AGENT-READY").expect("ready marker");
let sync = script.find("command -v tar").expect("sync block");
assert!(ready < sync, "{script}");
let cred = script.find("head -c 10").expect("framed credential");
let drain = script.find(r#"cat > "$payload""#).expect("payload drain");
assert!(cred < drain, "{script}");
}
#[test]
fn combined_provision_without_credential_reads_only_the_tarball() {
let script = provision_script_with_skills(Agent::Claude, None, false, "h");
assert!(!script.contains("head -c"), "{script}");
assert!(script.contains(r#"cat > "$payload""#), "{script}");
assert!(script.contains("AGENT-READY"), "{script}");
}
#[test]
fn combined_payload_framing_splits_back_exactly() {
let credential = b"CLAUDE_CODE_OAUTH_TOKEN=tok-123\n";
let tarball = [0x1f, 0x8b, 0x08, 0x00, 0x42];
let (payload, len) = combined_provision_payload(Some(credential), &tarball);
let len = len.expect("credential present");
assert_eq!(&payload[..len], credential);
assert_eq!(&payload[len..], &tarball);
let script = provision_script_with_skills(Agent::Claude, Some(len), false, "h");
assert!(script.contains(&format!("head -c {len} ")), "{script}");
let (payload, len) = combined_provision_payload(None, &tarball);
assert!(len.is_none());
assert_eq!(payload, tarball);
}
#[test]
fn uuid_shapes() {
assert!(is_uuid("ddcba2f8-a773-4929-bfbd-52450cdf0356"));
assert!(is_uuid("DDCBA2F8-A773-4929-BFBD-52450CDF0356"));
assert!(!is_uuid("production"));
assert!(!is_uuid("ddcba2f8-a773-4929-bfbd-52450cdf035")); assert!(!is_uuid("ddcba2f8-a773-4929-bfbd-52450cdf035g")); assert!(!is_uuid("ddcba2f8_a773_4929_bfbd_52450cdf0356")); }
#[test]
fn mux_gating() {
let short = std::path::Path::new("/tmp/railway-cm-1-abc.sock");
if cfg!(windows) {
assert!(mux_master_opts(short, "10s").is_empty());
assert!(mux_client_opts(short).is_empty());
} else {
assert!(!mux_master_opts(short, "10s").is_empty());
assert!(!mux_client_opts(short).is_empty());
let long = std::path::PathBuf::from(format!("/{}/cm.sock", "x".repeat(120)));
assert!(mux_master_opts(&long, "10s").is_empty());
assert!(mux_client_opts(&long).is_empty());
}
}
#[test]
fn a_shell_launch_starts_no_harness_and_retargets_nothing() {
for (prompt, args) in [
(None, vec![]),
(Some("fix the tests"), vec![]),
(None, vec!["exec".to_string(), "explain this".to_string()]),
] {
let cmd = remote_command(
Agent::Shell,
"P; ",
prompt,
None,
&args,
SessionStyle::FullTerminal,
);
assert_eq!(cmd, "P; export RAILWAY_CODE_AUTOSTARTED=1; exec bash -l");
}
let script = provision_script(Agent::Shell, false, false);
assert!(
!script.contains("~/.railway-code-agent"),
"a shell launch must not retarget reconnects: {script}"
);
for seed in [
"cat > ~/.claude-code-env",
"cat > ~/.codex/auth.json",
"cat > ~/.grok/auth.json",
] {
assert!(!script.contains(seed), "{script}");
}
assert!(script.contains("railway-code agent autostart"));
assert!(script.contains("if command -v bash"), "{script}");
assert!(script.contains("AGENT-READY"));
}
#[test]
fn no_provision_step_writes_harness_config() {
for agent in [
Agent::Claude,
Agent::Codex,
Agent::Grok,
Agent::OpenCode,
Agent::OpenCode2,
Agent::Railway,
Agent::Shell,
] {
for write_credential in [true, false] {
let script = provision_script(agent, write_credential, false);
assert!(!script.contains(".claude/settings.json"), "{script}");
assert!(!script.contains(".claude.json"), "{script}");
assert!(!script.contains("apiKeyHelper"), "{script}");
}
}
assert!(!CLAUDE_SEED.contains("hasCompletedOnboarding"));
}
#[test]
fn provision_script_reports_the_agents_skills_hash() {
let script = provision_script(Agent::Claude, true, false);
assert!(script.contains(skills_sync::REMOTE_HASH_MARKER));
assert!(script.contains(skills_sync::REMOTE_HASH_FILE));
assert!(script.contains(mcp_sync::REMOTE_HASH_MARKER));
assert!(script.contains(mcp_sync::REMOTE_HASH_FILE));
assert!(script.contains("2>/dev/null || true"));
}
#[cfg(unix)]
#[test]
fn every_generated_provision_script_parses_as_shell() {
use std::io::Write;
use std::process::{Command, Stdio};
let mut scripts: Vec<(String, String)> = Vec::new();
for agent in [
Agent::Codex,
Agent::Claude,
Agent::Grok,
Agent::OpenCode,
Agent::OpenCode2,
Agent::Railway,
Agent::Shell,
] {
for write_credential in [true, false] {
for app_mode in [true, false] {
scripts.push((
format!("provision {agent:?} cred={write_credential} app={app_mode}"),
provision_script(agent, write_credential, app_mode),
));
}
}
scripts.push((
format!("provision+skills {agent:?}"),
provision_script_with_skills(agent, Some(42), false, "deadbeef"),
));
}
scripts.push((
"skills follow-up".into(),
skills_sync::provision_script("deadbeef"),
));
scripts.push((
"mcp follow-up".into(),
mcp_sync::provision_script("deadbeef"),
));
for (label, script) in scripts {
let mut child = Command::new("sh")
.arg("-n")
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child
.stdin
.take()
.unwrap()
.write_all(script.as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"`{label}` is not valid shell:\n{}\n--- script ---\n{script}",
String::from_utf8_lossy(&out.stderr)
);
}
}
#[test]
fn remote_command_shapes() {
use SessionStyle::FullTerminal;
let seeded = remote_command(
Agent::Claude,
"P; ",
Some("fix the tests"),
None,
&[],
FullTerminal,
);
assert!(seeded.contains("claude 'fix the tests';"), "{seeded}");
assert!(seeded.ends_with("exec bash -l"));
assert!(!seeded.contains("exec claude"));
let interactive = remote_command(Agent::Claude, "P; ", None, None, &[], FullTerminal);
assert!(interactive.contains("claude;"), "{interactive}");
assert!(interactive.ends_with("exec bash -l"));
let scripted = remote_command(
Agent::Codex,
"P; ",
None,
None,
&["exec".into(), "explain this".into()],
FullTerminal,
);
assert!(
scripted.contains("exec codex exec 'explain this'"),
"{scripted}"
);
assert!(!scripted.contains("bash -l"));
let blank = remote_command(Agent::Grok, "P; ", Some(" "), None, &[], FullTerminal);
assert_eq!(
blank,
remote_command(Agent::Grok, "P; ", None, None, &[], FullTerminal)
);
let railway = remote_command(
Agent::Railway,
"P; ",
Some("fix the tests"),
None,
&[],
FullTerminal,
);
assert!(
railway.contains(
"railway-agent-tui --session \"${RAILWAY_DURABLE_SESSION_NAME:-railway-adhoc-$$}\" 'fix the tests';"
),
"{railway}"
);
let railway_bare = remote_command(Agent::Railway, "P; ", None, None, &[], FullTerminal);
assert!(
railway_bare.contains(
"railway-agent-tui --session \"${RAILWAY_DURABLE_SESSION_NAME:-railway-adhoc-$$}\";"
),
"{railway_bare}"
);
let railway_exec = remote_command(
Agent::Railway,
"P; ",
None,
None,
&["--continue".into()],
FullTerminal,
);
assert!(
railway_exec.contains("exec railway-agent-tui --continue"),
"{railway_exec}"
);
assert!(!railway_exec.contains("--session"), "{railway_exec}");
}
#[test]
fn remote_command_resume_shapes() {
use SessionStyle::FullTerminal;
let resumed = remote_command(
Agent::Claude,
"P; ",
None,
Some("abc-123"),
&[],
FullTerminal,
);
assert!(resumed.contains("claude --resume abc-123;"), "{resumed}");
assert!(resumed.ends_with("exec bash -l"));
let hostile = remote_command(
Agent::Claude,
"P; ",
None,
Some("a'; rm -rf /'"),
&[],
FullTerminal,
);
assert!(
hostile.contains(r"claude --resume 'a'\''; rm -rf /'\''';"),
"{hostile}"
);
let codex = remote_command(
Agent::Codex,
"P; ",
None,
Some("abc-123"),
&[],
FullTerminal,
);
assert!(!codex.contains("--resume"), "{codex}");
let blank = remote_command(Agent::Claude, "P; ", None, Some(" "), &[], FullTerminal);
assert_eq!(
blank,
remote_command(Agent::Claude, "P; ", None, None, &[], FullTerminal)
);
}
#[test]
fn a_pane_session_ends_with_the_harness() {
for prompt in [None, Some("fix the tests")] {
let pane = remote_command(Agent::Claude, "P; ", prompt, None, &[], SessionStyle::Pane);
assert!(!pane.contains("bash -l"), "{pane}");
assert!(pane.contains("\\033[?25h'"), "{pane}");
assert!(pane.ends_with("exit \"$railway_code_status\""), "{pane}");
}
}
#[cfg(unix)]
#[test]
fn a_pane_preserves_the_harness_exit_status_after_resetting_the_terminal() {
for agent in [Agent::Railway, Agent::Claude, Agent::OpenCode] {
for prompt in [None, Some("fix the tests")] {
for status in [0, 1, 42, 127] {
let prefix = format!(
"{}() {{ echo harness-output; return {status}; }}; ",
agent.name()
);
let command =
remote_command(agent, &prefix, prompt, None, &[], SessionStyle::Pane);
let output = std::process::Command::new("bash")
.args(["--noprofile", "--norc", "-c", &command])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(status), "{command}");
assert!(output.stdout.starts_with(b"harness-output\n"));
assert!(output.stdout.ends_with(TERMINAL_RESET.as_bytes()));
}
}
}
}
#[test]
fn a_prompt_cannot_break_out_of_its_quoting() {
let nasty = remote_command(
Agent::Claude,
"P; ",
Some("'; rm -rf / #"),
None,
&[],
SessionStyle::FullTerminal,
);
assert!(!nasty.contains("; rm -rf / #;"), "{nasty}");
assert!(
nasty.contains(r"'\''"),
"expected shell-escaped quoting: {nasty}"
);
}
fn default_project() -> DefaultProject {
DefaultProject {
project_id: "proj_default".into(),
project_name: "Cloud Agents".into(),
environment_id: "env_default".into(),
environment_name: "production".into(),
}
}
#[test]
fn the_linked_directory_beats_the_configured_default() {
let linked = || Some(("proj_linked".to_string(), "env_linked".to_string()));
let args = LaunchArgs {
project: Some("proj_flag".into()),
..Default::default()
};
assert_eq!(
choose_target(&args, Some(&default_project()), linked()),
TargetSource::Flags
);
let args = LaunchArgs {
environment: Some("env_flag".into()),
..Default::default()
};
assert_eq!(
choose_target(&args, Some(&default_project()), linked()),
TargetSource::Flags
);
assert_eq!(
choose_target(&LaunchArgs::default(), Some(&default_project()), linked()),
TargetSource::Linked("proj_linked".into(), "env_linked".into())
);
assert_eq!(
choose_target(&LaunchArgs::default(), Some(&default_project()), None),
TargetSource::Configured("proj_default".into(), "env_default".into())
);
}
fn project_lookup(deleted: bool, envs: &[(&str, bool)]) -> queries::RailwayProject {
let stamp = "2026-01-01T00:00:00Z";
serde_json::from_value(serde_json::json!({
"id": "proj_linked",
"name": "linked",
"workspaceId": "ws",
"deletedAt": deleted.then_some(stamp),
"workspace": { "name": "ws" },
"buckets": { "edges": [] },
"environments": { "edges": envs.iter().map(|(id, dead)| serde_json::json!({
"node": {
"id": id,
"name": id,
"canAccess": true,
"deletedAt": dead.then_some(stamp),
"unmergedChangesCount": 0,
}
})).collect::<Vec<_>>() },
"services": { "edges": [] },
}))
.expect("a RailwayProject deserializes from the fields the query selects")
}
#[test]
fn only_a_definitely_dead_link_is_demoted() {
assert_eq!(
stale_link_reason(&Err(RailwayError::ProjectNotFound), "env_linked"),
Some("project that no longer exists")
);
assert_eq!(
stale_link_reason(
&Ok(project_lookup(true, &[("env_linked", false)])),
"env_linked"
),
Some("project that was deleted")
);
assert_eq!(
stale_link_reason(
&Ok(project_lookup(false, &[("env_other", false)])),
"env_linked"
),
Some("environment that no longer exists")
);
assert_eq!(
stale_link_reason(
&Ok(project_lookup(false, &[("env_linked", true)])),
"env_linked"
),
Some("environment that no longer exists")
);
assert_eq!(
stale_link_reason(
&Ok(project_lookup(false, &[("env_linked", false)])),
"env_linked"
),
None
);
assert_eq!(
stale_link_reason(
&Err(RailwayError::GraphQLError("connection reset".into())),
"env_linked"
),
None
);
}
#[test]
fn nothing_to_go_on_runs_setup_when_there_is_a_terminal() {
let want = match is_stdout_terminal() {
true => TargetSource::Setup,
false => TargetSource::Ask,
};
assert_eq!(choose_target(&LaunchArgs::default(), None, None), want);
}
#[test]
fn a_tui_launch_always_targets_by_flag() {
let args = LaunchArgs::for_target(
"proj_1".into(),
"env_prod".into(),
"claude",
false,
None,
None,
);
assert!(args.project.is_some() && args.environment.is_some());
assert_eq!(
choose_target(&args, None, None),
TargetSource::Flags,
"a TUI launch must never reach a prompt"
);
}
#[test]
fn launch_args_bareness_and_targeting() {
assert!(LaunchArgs::default().is_bare());
let mut flagged = LaunchArgs::default();
flagged.set_harness("codex");
assert!(
!flagged.is_bare(),
"a harness flag is not a bare invocation"
);
let mut railway = LaunchArgs::default();
railway.set_harness("railway");
assert!(!railway.is_bare());
let mut shell = LaunchArgs::default();
shell.set_harness("shell");
assert!(shell.shell, "the shell choice must survive the mapping");
assert!(!shell.is_bare());
shell.set_harness("claude");
assert!(!shell.shell, "picking a harness clears it");
let targeted = LaunchArgs::for_target(
"proj_1".into(),
"env_1".into(),
"grok",
true,
Some("do the thing".into()),
None,
);
assert!(!targeted.is_bare());
assert_eq!(targeted.project.as_deref(), Some("proj_1"));
assert_eq!(targeted.environment.as_deref(), Some("env_1"));
assert!(targeted.new);
assert_eq!(targeted.initial_prompt.as_deref(), Some("do the thing"));
let pinned = LaunchArgs::for_target(
"proj_1".into(),
"env_1".into(),
"claude",
false,
None,
Some("ca_7".into()),
);
assert_eq!(pinned.agent_id.as_deref(), Some("ca_7"));
assert!(!pinned.new, "pinning an agent must not also create one");
assert_eq!(
[targeted.claude, targeted.codex, targeted.grok]
.iter()
.filter(|x| **x)
.count(),
1
);
}
#[test]
fn the_kill_script_is_plain_text() {
let script = kill_script("claude-3s9r89");
let bad: Vec<char> = script
.chars()
.filter(|c| c.is_control() && *c != '\n')
.collect();
assert!(bad.is_empty(), "control characters in the script: {bad:?}");
assert!(!script.contains('\0'));
assert!(script.contains("claude-3s9r89"));
}
#[test]
fn the_kill_script_matches_on_the_environment() {
let script = kill_script("claude-3s9r89");
assert!(script.contains("RAILWAY_DURABLE_SESSION_NAME=claude-3s9r89"));
assert!(script.contains("/environ"));
assert!(
!script.contains("pkill"),
"pkill -f would take the whole box"
);
assert!(
script.contains("kill -TERM"),
"TERM lets a harness save first"
);
assert!(
script.contains("KILLED:"),
"the caller counts what it ended"
);
}
#[test]
fn agent_slugs_round_trip() {
for agent in [Agent::Claude, Agent::Codex, Agent::Grok] {
assert_eq!(agent.slug(), agent.name());
assert_eq!(Agent::from_slug(agent.name()), Some(agent));
}
assert_eq!(Agent::from_slug("railway"), Some(Agent::Railway));
assert_eq!(Agent::Railway.slug(), "railway");
assert_eq!(Agent::Railway.name(), "railway-agent-tui");
assert_eq!(Agent::from_slug("shell"), Some(Agent::Shell));
assert_eq!(Agent::Shell.slug(), "shell");
assert_eq!(Agent::Shell.name(), "bash");
assert!(Agent::from_slug("droid").is_none());
assert!(Agent::from_slug("").is_none());
}
#[cfg(unix)]
#[test]
fn cached_token_is_created_0600_regardless_of_umask() {
use std::os::unix::fs::PermissionsExt;
let dir = std::env::temp_dir().join(format!("railway-tok-{}", std::process::id()));
let path = dir.join("nested").join("claude-code-token");
let _ = std::fs::remove_dir_all(&dir);
write_token_0600(&path, "sk-ant-oat01-abc");
let mode = std::fs::metadata(&path)
.expect("written")
.permissions()
.mode()
& 0o777;
assert_eq!(mode, 0o600, "cached token was {mode:o}, not 0600");
assert_eq!(
std::fs::read_to_string(&path).unwrap().trim(),
"sk-ant-oat01-abc"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn claude_token_validation_rejects_shell_specials() {
assert!(validate_claude_token("sk-ant-oat01-abc_DEF-123").is_ok());
for bad in ["has space", "quote'", "semi;colon", "dollar$var", "tick`"] {
assert!(validate_claude_token(bad).is_err(), "accepted: {bad}");
}
}
const FAKE_A: &str =
"sk-ant-oat01-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const FAKE_B: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
fn fake_recording() -> String {
format!(
"\x1b[?25l\x1b[<u\x1b[>1u\x1b[38;2;78;186;101m Long-lived authentication token created successfully!\r\x1b[1B\x1b[39m\x1b[K\r\x1b[1B Your OAuth token (valid for 1 year):\x1b[K\r\x1b[1B\x1b[K\r\x1b[1B \x1b[38;2;255;193;7m{FAKE_A}\r\x1b[1B\x1b[39m \x1b[38;2;255;193;7m{FAKE_B}\r\x1b[1C\x1b[2B\x1b[38;2;153;153;153mStore this token securely. You won't be able to see it again.\r\x1b[1C\x1b[1B\x1b[39m\x1b[K\r\r\n\x1b]8;id=1;https://example.com\x07link\x1b]8;;\x07\r\n"
)
}
#[test]
fn extracts_wrapped_token_from_recording() {
let tok = extract_claude_token(&fake_recording()).expect("token");
assert_eq!(tok, format!("{FAKE_A}{FAKE_B}"));
}
#[test]
fn extracts_unwrapped_token() {
let raw = format!("junk\r\n \x1b[33m{FAKE_A}\x1b[39m\r\nStore this token securely.\r\n");
assert_eq!(extract_claude_token(&raw).as_deref(), Some(FAKE_A));
}
#[test]
fn rejects_fragments_and_noise() {
assert_eq!(extract_claude_token("sk-ant-oat01-tooshort\r\n"), None);
assert_eq!(
extract_claude_token("\x1b]8;;https://claude.com/oauth?x=1\x07sign in\x1b]8;;\x07\r\n"),
None
);
}
#[test]
fn terminal_reset_printf_has_no_raw_escapes() {
let printf = terminal_reset_printf();
assert!(!printf.contains('\x1b'));
assert!(printf.starts_with("printf '") && printf.ends_with('\''));
}
}