From 65a2efd5badcb1eb03b94cd44d90a73ed9b1337c Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:48:49 -0700 Subject: [PATCH] fix: gcp use root SSH user instead of whoami (#2503) The `resolveUsername()` function called `whoami` and validated against a regex that rejected dots in usernames (e.g. `adrian.hale`), causing "Invalid username" errors. All other clouds use a static SSH user (root for Hetzner/DO, ubuntu for AWS). Switch GCP to use `root` consistently: - Replace dynamic `whoami` lookup with static `GCP_SSH_USER = "root"` - Simplify cloud-init startup script (already runs as root) - Fix bun symlink path to use /root instead of /home/${username} - Remove unused `username` field from GcpState Closes #2502 Co-authored-by: Claude Co-authored-by: Claude Opus 4.6 (1M context) Co-authored-by: L <6723574+louisgv@users.noreply.github.com> --- packages/cli/package.json | 2 +- packages/cli/src/gcp/gcp.ts | 41 +++++++++---------------------------- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 12a134cf..dbd3ae00 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.16.11", + "version": "0.16.12", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/gcp/gcp.ts b/packages/cli/src/gcp/gcp.ts index ece8c64b..dd7015c6 100644 --- a/packages/cli/src/gcp/gcp.ts +++ b/packages/cli/src/gcp/gcp.ts @@ -146,7 +146,6 @@ interface GcpState { zone: string; instanceName: string; serverIp: string; - username: string; } const _state: GcpState = { @@ -154,7 +153,6 @@ const _state: GcpState = { zone: "", instanceName: "", serverIp: "", - username: "", }; /** Return SSH connection info for tunnel support. */ @@ -645,29 +643,10 @@ async function ensureSshKey(): Promise { // ─── Username ─────────────────────────────────────────────────────────────── +const GCP_SSH_USER = "root"; + function resolveUsername(): string { - if (_state.username) { - return _state.username; - } - const result = Bun.spawnSync( - [ - "whoami", - ], - { - stdio: [ - "ignore", - "pipe", - "ignore", - ], - }, - ); - const username = new TextDecoder().decode(result.stdout).trim(); - if (!/^[a-zA-Z0-9_-]+$/.test(username)) { - logError("Invalid username detected"); - throw new Error("Invalid username"); - } - _state.username = username; - return username; + return GCP_SSH_USER; } // ─── Server Name ──────────────────────────────────────────────────────────── @@ -682,7 +661,7 @@ export async function promptSpawnName(): Promise { // ─── Cloud Init Startup Script ────────────────────────────────────────────── -function getStartupScript(username: string, tier: CloudInitTier = "full"): string { +function getStartupScript(tier: CloudInitTier = "full"): string { const packages = getPackagesForTier(tier); const lines = [ "#!/bin/bash", @@ -694,15 +673,15 @@ function getStartupScript(username: string, tier: CloudInitTier = "full"): strin lines.push( "# Install Node.js 22 via n (run as root so it installs to /usr/local/bin/)", `${NODE_INSTALL_CMD} || true`, - "# Install Claude Code as the login user", - `su - "${username}" -c 'curl --proto "=https" -fsSL https://claude.ai/install.sh | bash' || true`, + "# Install Claude Code", + 'curl --proto "=https" -fsSL https://claude.ai/install.sh | bash || true', ); } if (needsBun(tier)) { lines.push( - "# Install Bun as the login user", - `su - "${username}" -c 'curl --proto "=https" -fsSL https://bun.sh/install | bash' || true`, - `ln -sf /home/${username}/.bun/bin/bun /usr/local/bin/bun 2>/dev/null || true`, + "# Install Bun", + 'curl --proto "=https" -fsSL https://bun.sh/install | bash || true', + "ln -sf /root/.bun/bin/bun /usr/local/bin/bun 2>/dev/null || true", ); } lines.push( @@ -734,7 +713,7 @@ export async function createInstance( // Write startup script to a temp file (random suffix prevents collisions and predictable paths) const tmpFile = `/tmp/spawn_startup_${Date.now()}_${Math.random().toString(36).slice(2)}.sh`; - writeFileSync(tmpFile, getStartupScript(username, tier), { + writeFileSync(tmpFile, getStartupScript(tier), { mode: 0o600, });