From 8ee54d01a848d47e2b7023dfceb6cf5ace53646e Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 19 Feb 2026 05:36:24 -0800 Subject: [PATCH] fix: harden agent reliability + security across all clouds (#1468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: add spawn delete command to README Co-Authored-By: Claude Opus 4.6 * fix: harden openclaw across all clouds — validation, reliability, performance Fixes multiple issues causing openclaw to break on most clouds: Bugs fixed: - Double-prefixed model ID (openrouter/openrouter/auto) in config generation - AWS gateway starting without env vars (missing .zshrc source) - DigitalOcean sourcing .spawnrc instead of .zshrc for gateway - Destructive rm -rf ~/.openclaw on re-runs (now mkdir -p) Validation added: - API key checked against OpenRouter /auth/key endpoint with re-prompt on failure - Model ID verified against OpenRouter model list with re-prompt loop - openrouter/auto and openrouter/free bypass model check Reliability improvements: - Standardized gateway launch with * fix: skip sudo in gh install when running as root (Fly.io containers) Co-Authored-By: Claude Opus 4.6 * fix: address PR review — skip validation in tests, quote escaped cmd, escape model_id - verify_openrouter_key and verify_openrouter_model skip network calls when SPAWN_SKIP_API_VALIDATION, BUN_ENV=test, or NODE_ENV=test is set - install_agent timeout wrapper now quotes the escaped command for defense in depth - model_id in openclaw JSON now uses json_escape() for consistency Co-Authored-By: Claude Opus 4.6 * fix: remove double-escaping in install_agent that broke shell operators install_agent() was wrapping commands with printf '%q' + bash -c before passing them to the run callback. But run callbacks (run_server, run_sprite, ssh_run_server) already handle escaping for remote transport. The double- escaping turned && || > | into literal characters, causing 'source' to treat the entire command as a single filename. Co-Authored-By: Claude Opus 4.6 * fix: use local github-auth.sh instead of curling from main When running from a local checkout, base64-encode the local github-auth.sh and send it inline to the remote machine. This ensures fixes (like the sudo skip for root) take effect immediately without waiting for a merge to main. Co-Authored-By: Claude Opus 4.6 * fix: handle github-auth errors gracefully instead of terminating GitHub CLI setup is optional — failures should not abort the spawn session. Guard both run_callback calls in offer_github_auth with || log_warn so the script continues even if gh install fails. Co-Authored-By: Claude Opus 4.6 * fix: use GOOGLE_GEMINI_BASE_URL to route Gemini CLI through OpenRouter Gemini CLI ignores OPENAI_BASE_URL — it uses GEMINI_API_KEY to talk directly to Google's API. The OpenRouter key is not a valid Google API key, so all requests fail with "API key not valid". Use GOOGLE_GEMINI_BASE_URL to redirect Gemini CLI to OpenRouter's endpoint. Fixes all 9 cloud gemini scripts + manifest.json. Co-Authored-By: Claude Opus 4.6 * fix: guard optional spawn_agent hooks so failures don't kill the session With set -eo pipefail, any unguarded failure terminates the script. Several optional operations in spawn_agent were unguarded: - agent_configure: config file uploads (agent works with defaults) - agent_save_connection: convenience JSON for spawn list - agent_pre_launch: gateway daemons, startup hooks - agent_pre_provision: pre-provision prompts - .spawnrc shell hooks: hooking env vars into .bashrc/.zshrc These now log warnings and continue instead of aborting. Critical steps (cloud_authenticate, agent_install, cloud_provision) still exit on failure. Co-Authored-By: Claude Opus 4.6 * fix: audit and fix env vars, escaping, and error handling across all agents Audit findings from 3 parallel agents, fixes applied: **Env vars (4 agents fixed across 9 clouds each = 36 scripts):** - Amazon Q: remove fake OPENAI_* vars (Q uses AWS auth, can't use OpenRouter) - Cline: replace OPENAI_* env vars with `cline auth -p openrouter` command - Open Interpreter: drop OPENAI_* vars, use only OPENROUTER_API_KEY (native support via --model flag) - NanoClaw: add ANTHROPIC_BASE_URL to .env file (was missing, requests went to Anthropic directly) **Escaping:** - execute_agent_non_interactive: replace printf '%q' with single-quote wrapping to avoid double-escaping on Fly.io **Manifest updated** for amazonq, cline, interpreter entries. Co-Authored-By: Claude Opus 4.6 * fix: use setsid to detach openclaw gateway daemon from SSH sessions The gateway daemon launch (`nohup openclaw gateway ... & disown`) hangs on all clouds because SSH/exec channels wait for child FDs to close. setsid creates a new session, fully detaching the daemon so the channel can close immediately. Falls back to nohup where setsid is unavailable. Consolidates the daemon launch into a shared start_openclaw_gateway() function used by all 9 cloud scripts. Co-Authored-By: Claude Opus 4.6 * fix: configure npm global prefix for non-root clouds (AWS, GCP, OVH) AWS Lightsail, GCP, and OVH SSH as non-root users (ubuntu/login user), so `npm install -g` fails with EACCES on /usr/local/lib/node_modules/. Fix: configure npm prefix to ~/.npm-global during cloud-init/setup and add ~/.npm-global/bin to the SSH PATH prefix so agent install commands find globally-installed npm binaries without sudo. Co-Authored-By: Claude Opus 4.6 * fix: remove broken OpenRouter routing from Gemini CLI scripts Gemini CLI uses Google's native API format (/v1beta/models/:streamGenerateContent), not the OpenAI-compatible format (/v1/chat/completions). No base URL override can bridge this — the request formats are fundamentally incompatible. Same situation as Amazon Q (uses vendor-specific auth/API). Removed GEMINI_API_KEY and GOOGLE_GEMINI_BASE_URL from all 9 scripts + manifest. Co-Authored-By: Claude Opus 4.6 * fix: auto-install AWS CLI and gcloud SDK when missing Instead of printing manual install instructions and exiting, both CLIs now auto-install: - AWS: downloads official .pkg (macOS) or .zip (Linux) installer - GCP: uses brew cask on macOS, Google's tarball installer on Linux Falls back to manual instructions if auto-install fails. Co-Authored-By: Claude Opus 4.6 * fix: nanoclaw — install Docker on Linux, fix hardcoded /root/ path Two issues broke NanoClaw on all clouds: 1. .env upload hardcoded /root/nanoclaw/.env — fails on non-root clouds (AWS=ubuntu, GCP=user, OVH=ubuntu). Now uses upload_config_file with $HOME which expands on the remote side. 2. NanoClaw requires a container runtime. On Linux it uses Docker, but Docker was never installed. Added Docker install via get.docker.com to all cloud scripts (with sudo where SSH user is non-root). Co-Authored-By: Claude Opus 4.6 * fix: address security review findings from PR #1463 - Reject symlinked github-auth.sh before base64-encoding (falls back to remote URL) - Hide API key from process list using curl -K - instead of -H in verify_openrouter_key Co-Authored-By: Claude Opus 4.6 * fix: quote OPENROUTER_API_KEY in cline auth to prevent command injection Unquoted variable in `cline auth -p openrouter -k ${OPENROUTER_API_KEY}` allows shell metacharacters in the key to execute arbitrary commands on the remote server. Wrapping in escaped double quotes prevents expansion. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- aws/amazonq.sh | 5 +-- aws/cline.sh | 8 ++-- aws/interpreter.sh | 4 +- aws/lib/common.sh | 45 +++++++++++++++++----- aws/nanoclaw.sh | 12 +++--- aws/openclaw.sh | 2 +- daytona/amazonq.sh | 5 +-- daytona/cline.sh | 9 +++-- daytona/interpreter.sh | 4 +- daytona/nanoclaw.sh | 12 +++--- daytona/openclaw.sh | 2 +- digitalocean/amazonq.sh | 5 +-- digitalocean/cline.sh | 8 ++-- digitalocean/interpreter.sh | 4 +- digitalocean/nanoclaw.sh | 12 +++--- digitalocean/openclaw.sh | 2 +- fly/amazonq.sh | 5 +-- fly/cline.sh | 9 +++-- fly/interpreter.sh | 4 +- fly/nanoclaw.sh | 12 +++--- fly/openclaw.sh | 2 +- gcp/amazonq.sh | 5 +-- gcp/cline.sh | 8 ++-- gcp/interpreter.sh | 4 +- gcp/lib/common.sh | 76 +++++++++++++++++++++---------------- gcp/nanoclaw.sh | 12 +++--- gcp/openclaw.sh | 2 +- hetzner/amazonq.sh | 6 +-- hetzner/cline.sh | 8 ++-- hetzner/interpreter.sh | 4 +- hetzner/nanoclaw.sh | 12 +++--- hetzner/openclaw.sh | 2 +- local/amazonq.sh | 5 +-- local/cline.sh | 9 +++-- local/interpreter.sh | 4 +- local/nanoclaw.sh | 10 ++--- local/openclaw.sh | 2 +- ovh/amazonq.sh | 5 +-- ovh/cline.sh | 8 ++-- ovh/interpreter.sh | 4 +- ovh/lib/common.sh | 7 +++- ovh/nanoclaw.sh | 12 +++--- ovh/openclaw.sh | 2 +- shared/common.sh | 51 +++++++++++++++++++------ sprite/amazonq.sh | 5 +-- sprite/cline.sh | 9 +++-- sprite/interpreter.sh | 4 +- sprite/nanoclaw.sh | 12 +++--- sprite/openclaw.sh | 2 +- 49 files changed, 253 insertions(+), 208 deletions(-) diff --git a/aws/amazonq.sh b/aws/amazonq.sh index 0f77e0d4..8128c7a9 100755 --- a/aws/amazonq.sh +++ b/aws/amazonq.sh @@ -14,11 +14,10 @@ log_info "Amazon Q on AWS Lightsail" echo "" agent_install() { install_agent "Amazon Q CLI" "curl -fsSL https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q-cli-install.sh | bash" cloud_run; } +# Amazon Q uses AWS Builder ID auth — cannot route through OpenRouter. agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" } agent_launch_cmd() { echo 'source ~/.zshrc && q chat'; } diff --git a/aws/cline.sh b/aws/cline.sh index 4f31dc1d..4a78f9ff 100755 --- a/aws/cline.sh +++ b/aws/cline.sh @@ -16,9 +16,11 @@ echo "" agent_install() { install_agent "Cline" "npm install -g cline" cloud_run; } agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" +} +agent_configure() { + log_step "Authenticating Cline with OpenRouter..." + cloud_run "source ~/.zshrc && cline auth -p openrouter -k \"${OPENROUTER_API_KEY}\"" } agent_launch_cmd() { echo 'source ~/.zshrc && cline'; } diff --git a/aws/interpreter.sh b/aws/interpreter.sh index 0fcf89f1..d2a103da 100755 --- a/aws/interpreter.sh +++ b/aws/interpreter.sh @@ -21,9 +21,7 @@ agent_install() { } agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" } agent_launch_cmd() { printf 'export PATH="$HOME/.local/bin:$PATH"; source ~/.zshrc && interpreter --model %s --api_key %s' "${MODEL_ID}" "${OPENROUTER_API_KEY}"; } diff --git a/aws/lib/common.sh b/aws/lib/common.sh index 4d3e4a12..ab079408 100644 --- a/aws/lib/common.sh +++ b/aws/lib/common.sh @@ -31,13 +31,38 @@ INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-5} # Delay between ins ensure_aws_cli() { if ! command -v aws &>/dev/null; then - _log_diagnostic \ - "AWS CLI is required but not installed" \ - "aws command not found in PATH" \ - --- \ - "Install the AWS CLI: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" \ - "Or on macOS: brew install awscli" - return 1 + log_step "Installing AWS CLI..." + if [[ "$(uname)" == "Darwin" ]]; then + # macOS: download the .pkg installer + local _aws_tmp + _aws_tmp=$(mktemp -d) + curl -fsSL "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "${_aws_tmp}/AWSCLIV2.pkg" \ + && sudo installer -pkg "${_aws_tmp}/AWSCLIV2.pkg" -target / \ + && rm -rf "${_aws_tmp}" \ + && log_info "AWS CLI installed" \ + || { + rm -rf "${_aws_tmp}" + log_error "Auto-install failed. Install manually:" + log_error " brew install awscli" + log_error " or: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" + return 1 + } + else + # Linux: download the zip installer + local _aws_tmp + _aws_tmp=$(mktemp -d) + curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "${_aws_tmp}/awscliv2.zip" \ + && unzip -q "${_aws_tmp}/awscliv2.zip" -d "${_aws_tmp}" \ + && sudo "${_aws_tmp}/aws/install" \ + && rm -rf "${_aws_tmp}" \ + && log_info "AWS CLI installed" \ + || { + rm -rf "${_aws_tmp}" + log_error "Auto-install failed. Install manually:" + log_error " https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" + return 1 + } + fi fi # Verify credentials are configured if ! aws sts get-caller-identity &>/dev/null; then @@ -120,9 +145,11 @@ npm install -g n && n 22 && ln -sf /usr/local/bin/node /usr/bin/node && ln -sf / su - ubuntu -c 'curl -fsSL https://bun.sh/install | bash' # Install Claude Code su - ubuntu -c 'curl -fsSL https://claude.ai/install.sh | bash' +# Configure npm global prefix so ubuntu can npm install -g without sudo +su - ubuntu -c 'mkdir -p ~/.npm-global/bin && npm config set prefix ~/.npm-global' # Configure PATH -echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.bashrc -echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.zshrc +echo 'export PATH="${HOME}/.npm-global/bin:${HOME}/.claude/local/bin:${HOME}/.local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.bashrc +echo 'export PATH="${HOME}/.npm-global/bin:${HOME}/.claude/local/bin:${HOME}/.local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.zshrc chown ubuntu:ubuntu /home/ubuntu/.bashrc /home/ubuntu/.zshrc touch /home/ubuntu/.cloud-init-complete chown ubuntu:ubuntu /home/ubuntu/.cloud-init-complete diff --git a/aws/nanoclaw.sh b/aws/nanoclaw.sh index 443cd8ce..b2875a68 100755 --- a/aws/nanoclaw.sh +++ b/aws/nanoclaw.sh @@ -14,6 +14,8 @@ log_info "NanoClaw on AWS Lightsail" echo "" agent_install() { + log_step "Installing Docker (required by NanoClaw on Linux)..." + cloud_run "command -v docker >/dev/null || (curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker \$(whoami))" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -27,13 +29,9 @@ agent_env_vars() { "ANTHROPIC_BASE_URL=https://openrouter.ai/api" } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - trap 'rm -f "${dotenv_temp}"' EXIT - chmod 600 "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { echo 'cd ~/nanoclaw && source ~/.zshrc && npm run dev'; } diff --git a/aws/openclaw.sh b/aws/openclaw.sh index 61ea285d..1d34def2 100755 --- a/aws/openclaw.sh +++ b/aws/openclaw.sh @@ -25,7 +25,7 @@ agent_env_vars() { } agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 /dev/null || (curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker \$(whoami))" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -28,13 +30,9 @@ agent_env_vars() { } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - chmod 600 "${dotenv_temp}" - track_temp_file "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { diff --git a/daytona/openclaw.sh b/daytona/openclaw.sh index 316dae52..dc3db85b 100644 --- a/daytona/openclaw.sh +++ b/daytona/openclaw.sh @@ -31,7 +31,7 @@ agent_configure() { } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 /dev/null || (curl -fsSL https://get.docker.com | sh)" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -27,13 +29,9 @@ agent_env_vars() { "ANTHROPIC_BASE_URL=https://openrouter.ai/api" } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - trap 'rm -f "${dotenv_temp}"' EXIT - chmod 600 "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { echo 'cd ~/nanoclaw && source ~/.zshrc && npm run dev'; } diff --git a/digitalocean/openclaw.sh b/digitalocean/openclaw.sh index 6667be54..a558f245 100755 --- a/digitalocean/openclaw.sh +++ b/digitalocean/openclaw.sh @@ -25,7 +25,7 @@ agent_env_vars() { } agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 /dev/null || (curl -fsSL https://get.docker.com | sh)" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -28,13 +30,9 @@ agent_env_vars() { } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - chmod 600 "${dotenv_temp}" - track_temp_file "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { diff --git a/fly/openclaw.sh b/fly/openclaw.sh index 69d047f6..6ed605a4 100644 --- a/fly/openclaw.sh +++ b/fly/openclaw.sh @@ -36,7 +36,7 @@ agent_configure() { } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 /dev/null; then return 0; fi + + log_step "Installing Google Cloud SDK..." + if [[ "$(uname)" == "Darwin" ]] && command -v brew &>/dev/null; then + brew install --cask google-cloud-sdk \ + && log_info "Google Cloud SDK installed via Homebrew" \ + || { + log_error "Auto-install failed. Install manually: brew install --cask google-cloud-sdk" + return 1 + } + # Homebrew cask puts gcloud in a non-standard location — source it + local _gcloud_path + for _gcloud_path in \ + "$(brew --prefix)/share/google-cloud-sdk/path.bash.inc" \ + "$(brew --prefix)/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/path.bash.inc"; do + if [[ -f "${_gcloud_path}" ]]; then + source "${_gcloud_path}" + break + fi + done + else + # Linux / macOS without brew: use Google's installer + local _gcp_tmp + _gcp_tmp=$(mktemp -d) + curl -fsSL "https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-cli-linux-x86_64.tar.gz" \ + -o "${_gcp_tmp}/gcloud.tar.gz" \ + && tar -xzf "${_gcp_tmp}/gcloud.tar.gz" -C "${HOME}" \ + && "${HOME}/google-cloud-sdk/install.sh" --quiet --path-update true \ + && export PATH="${HOME}/google-cloud-sdk/bin:${PATH}" \ + && rm -rf "${_gcp_tmp}" \ + && log_info "Google Cloud SDK installed" \ + || { + rm -rf "${_gcp_tmp}" + log_error "Auto-install failed. Install manually:" + log_error " https://cloud.google.com/sdk/docs/install" + return 1 + } + fi + if ! command -v gcloud &>/dev/null; then - log_error "Google Cloud SDK (gcloud) is required but not installed" - log_error "" - log_error "Possible causes:" - log_error " - gcloud CLI has not been installed on this machine" - log_error "" - log_error "How to fix:" - log_error " 1. Install gcloud CLI for your platform:" - log_error "" - log_error " ${CYAN}macOS (Homebrew)${NC}" - log_error " brew install google-cloud-sdk" - log_error "" - log_error " ${CYAN}Ubuntu/Debian${NC}" - log_error " curl https://sdk.cloud.google.com | bash" - log_error " exec -l \$SHELL # Restart shell" - log_error "" - log_error " ${CYAN}Fedora/RHEL${NC}" - log_error " sudo tee -a /etc/yum.repos.d/google-cloud-sdk.repo << EOM" - log_error " [google-cloud-cli]" - log_error " name=Google Cloud CLI" - log_error " baseurl=https://packages.cloud.google.com/yum/repos/cloud-sdk-el9-x86_64" - log_error " enabled=1" - log_error " gpgcheck=1" - log_error " repo_gpgcheck=0" - log_error " gpgkey=https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg" - log_error " EOM" - log_error " sudo dnf install google-cloud-cli" - log_error "" - log_error " 2. Full installation guide: ${CYAN}https://cloud.google.com/sdk/docs/install${NC}" - log_error "" - log_error " 3. After installation, authenticate:" - log_error " gcloud auth login" - log_error " gcloud config set project YOUR_PROJECT_ID" + log_error "gcloud not found after install. You may need to restart your shell." return 1 fi } @@ -249,8 +257,10 @@ if [[ ! "$GCP_USERNAME" =~ ^[a-zA-Z0-9_-]+$ ]]; then fi su - "$GCP_USERNAME" -c 'curl -fsSL https://bun.sh/install | bash' || true su - "$GCP_USERNAME" -c 'curl -fsSL https://claude.ai/install.sh | bash' || true +# Configure npm global prefix so non-root user can npm install -g without sudo +su - "$GCP_USERNAME" -c 'mkdir -p ~/.npm-global/bin && npm config set prefix ~/.npm-global' # Configure PATH for all users -echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.local/bin:${HOME}/.bun/bin:${PATH}"' >> /etc/profile.d/spawn.sh +echo 'export PATH="${HOME}/.npm-global/bin:${HOME}/.claude/local/bin:${HOME}/.local/bin:${HOME}/.bun/bin:${PATH}"' >> /etc/profile.d/spawn.sh chmod +x /etc/profile.d/spawn.sh touch /tmp/.cloud-init-complete CLOUD_INIT_EOF diff --git a/gcp/nanoclaw.sh b/gcp/nanoclaw.sh index 4994298a..913ac37a 100755 --- a/gcp/nanoclaw.sh +++ b/gcp/nanoclaw.sh @@ -14,6 +14,8 @@ log_info "NanoClaw on GCP Compute Engine" echo "" agent_install() { + log_step "Installing Docker (required by NanoClaw on Linux)..." + cloud_run "command -v docker >/dev/null || (curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker \$(whoami))" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -27,13 +29,9 @@ agent_env_vars() { "ANTHROPIC_BASE_URL=https://openrouter.ai/api" } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - trap 'rm -f "${dotenv_temp}"' EXIT - chmod 600 "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { echo 'cd ~/nanoclaw && source ~/.zshrc && npm run dev'; } diff --git a/gcp/openclaw.sh b/gcp/openclaw.sh index 01e47b7f..2a1a11f0 100755 --- a/gcp/openclaw.sh +++ b/gcp/openclaw.sh @@ -25,7 +25,7 @@ agent_env_vars() { } agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 /dev/null || (curl -fsSL https://get.docker.com | sh)" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -27,13 +29,9 @@ agent_env_vars() { "ANTHROPIC_BASE_URL=https://openrouter.ai/api" } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - trap 'rm -f "${dotenv_temp}"' EXIT - chmod 600 "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { echo 'cd ~/nanoclaw && source ~/.zshrc && npm run dev'; } diff --git a/hetzner/openclaw.sh b/hetzner/openclaw.sh index c582def7..67dc22d5 100755 --- a/hetzner/openclaw.sh +++ b/hetzner/openclaw.sh @@ -25,7 +25,7 @@ agent_env_vars() { } agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 /dev/null; cline auth -p openrouter -k \"${OPENROUTER_API_KEY}\"" } agent_launch_cmd() { diff --git a/local/interpreter.sh b/local/interpreter.sh index d8d59163..1b585e8e 100644 --- a/local/interpreter.sh +++ b/local/interpreter.sh @@ -22,9 +22,7 @@ agent_install() { agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" } agent_launch_cmd() { diff --git a/local/nanoclaw.sh b/local/nanoclaw.sh index 386c2c97..a004bf71 100644 --- a/local/nanoclaw.sh +++ b/local/nanoclaw.sh @@ -28,13 +28,9 @@ agent_env_vars() { } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - chmod 600 "${dotenv_temp}" - track_temp_file "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "${HOME}/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { diff --git a/local/openclaw.sh b/local/openclaw.sh index e450bfd7..85b74e3b 100644 --- a/local/openclaw.sh +++ b/local/openclaw.sh @@ -31,7 +31,7 @@ agent_configure() { } agent_pre_launch() { - cloud_run "source ~/.zshrc 2>/dev/null; nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" + start_openclaw_gateway cloud_run wait_for_openclaw_gateway cloud_run } diff --git a/ovh/amazonq.sh b/ovh/amazonq.sh index 19d7c6dd..27731aaa 100755 --- a/ovh/amazonq.sh +++ b/ovh/amazonq.sh @@ -14,11 +14,10 @@ log_info "Amazon Q on OVHcloud" echo "" agent_install() { install_agent "Amazon Q CLI" "curl -fsSL https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q-cli-install.sh | bash" cloud_run; } +# Amazon Q uses AWS Builder ID auth — cannot route through OpenRouter. agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" } agent_launch_cmd() { echo 'source ~/.zshrc && q chat'; } diff --git a/ovh/cline.sh b/ovh/cline.sh index 1572aae0..5c3d828a 100755 --- a/ovh/cline.sh +++ b/ovh/cline.sh @@ -16,9 +16,11 @@ echo "" agent_install() { install_agent "Cline" "npm install -g cline" cloud_run; } agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" +} +agent_configure() { + log_step "Authenticating Cline with OpenRouter..." + cloud_run "source ~/.zshrc && cline auth -p openrouter -k \"${OPENROUTER_API_KEY}\"" } agent_launch_cmd() { echo 'source ~/.zshrc && cline'; } diff --git a/ovh/interpreter.sh b/ovh/interpreter.sh index 5cc0f158..fba1b3aa 100755 --- a/ovh/interpreter.sh +++ b/ovh/interpreter.sh @@ -21,9 +21,7 @@ agent_install() { } agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" } agent_launch_cmd() { printf 'export PATH="$HOME/.local/bin:$PATH"; source ~/.zshrc && interpreter --model %s --api_key %s' "${MODEL_ID}" "${OPENROUTER_API_KEY}"; } diff --git a/ovh/lib/common.sh b/ovh/lib/common.sh index 4d40558f..75da1160 100644 --- a/ovh/lib/common.sh +++ b/ovh/lib/common.sh @@ -383,9 +383,12 @@ install_base_deps() { # Install Claude Code run_ovh "$ip" "curl -fsSL https://claude.ai/install.sh | bash" + # Configure npm global prefix so non-root user can npm install -g without sudo + run_ovh "$ip" "mkdir -p ~/.npm-global/bin && npm config set prefix ~/.npm-global" + # Configure PATH - run_ovh "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.bashrc" - run_ovh "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.zshrc" + run_ovh "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.npm-global/bin:\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.bashrc" + run_ovh "$ip" "printf '%s\n' 'export PATH=\"\${HOME}/.npm-global/bin:\${HOME}/.local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.zshrc" log_info "Base dependencies installed" } diff --git a/ovh/nanoclaw.sh b/ovh/nanoclaw.sh index de11e5a6..565a68bf 100755 --- a/ovh/nanoclaw.sh +++ b/ovh/nanoclaw.sh @@ -14,6 +14,8 @@ log_info "NanoClaw on OVHcloud" echo "" agent_install() { + log_step "Installing Docker (required by NanoClaw on Linux)..." + cloud_run "command -v docker >/dev/null || (curl -fsSL https://get.docker.com | sudo sh && sudo usermod -aG docker \$(whoami))" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -27,13 +29,9 @@ agent_env_vars() { "ANTHROPIC_BASE_URL=https://openrouter.ai/api" } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - trap 'rm -f "${dotenv_temp}"' EXIT - chmod 600 "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { echo 'cd ~/nanoclaw && source ~/.zshrc && npm run dev'; } diff --git a/ovh/openclaw.sh b/ovh/openclaw.sh index d794014d..9d9b7006 100755 --- a/ovh/openclaw.sh +++ b/ovh/openclaw.sh @@ -25,7 +25,7 @@ agent_env_vars() { } agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 /dev/null; then return 0; fi # skip if no curl local response http_code - response=$(curl -s --connect-timeout 5 --max-time 10 -w "\n%{http_code}" \ - -H "Authorization: Bearer ${api_key}" \ + # Pass auth header via stdin (-K -) so the API key isn't visible in ps output + response=$(printf 'header = "Authorization: Bearer %s"\n' "${api_key}" | \ + curl -s --connect-timeout 5 --max-time 10 -w "\n%{http_code}" \ + -K - \ "https://openrouter.ai/api/v1/auth/key" 2>/dev/null) || return 0 # network error = skip http_code=$(printf '%s' "${response}" | tail -1) @@ -1342,7 +1344,7 @@ offer_github_auth() { # a merge to main. Base64-encode it for safe inline transport. local gh_cmd local _local_gh="${SCRIPT_DIR:-}/../../shared/github-auth.sh" - if [[ -n "${SCRIPT_DIR:-}" && -f "${_local_gh}" ]]; then + if [[ -n "${SCRIPT_DIR:-}" && -f "${_local_gh}" && ! -L "${_local_gh}" ]]; then local _gh_b64 _gh_b64=$(base64 < "${_local_gh}" | tr -d '\n') gh_cmd="printf '%s' '${_gh_b64}' | base64 -d | bash" @@ -1720,7 +1722,15 @@ spawn_agent() { server_name=$(get_server_name) cloud_provision "${server_name}" - # 6. Wait for readiness + # 4. Get API key while server provisions (overlaps with cloud-init) + get_or_prompt_api_key + + # 5. Model selection while server provisions (if agent needs it) + if [[ -n "${AGENT_MODEL_PROMPT:-}" ]]; then + MODEL_ID=$(get_model_id_interactive "${AGENT_MODEL_DEFAULT:-openrouter/auto}" "${agent_name}") || exit 1 + fi + + # 6. Wait for readiness (may already be done after OAuth) cloud_wait_ready # 7. Install agent @@ -2239,18 +2249,20 @@ execute_agent_non_interactive() { log_step "Executing ${agent_name} with prompt in non-interactive mode..." - # Escape the prompt for safe shell execution - # We use printf %q which properly escapes special characters for bash - local escaped_prompt - escaped_prompt=$(printf '%q' "${prompt}") + # Do NOT use printf '%q' here — the run callback (run_server, sprite exec, + # ssh) already handles escaping for remote transport. Double-escaping breaks + # prompts containing quotes, spaces, or special characters on Fly.io. + # Single-quote the prompt to protect it from shell expansion. + local safe_prompt + safe_prompt="'$(printf '%s' "${prompt}" | sed "s/'/'\\\\''/g")'" # Build the command based on exec callback type if [[ "${exec_callback}" == *"sprite"* ]]; then # Sprite execution (no -tty flag for non-interactive) - sprite exec -s "${sprite_name}" -- zsh -c "source ~/.zshrc && ${agent_name} ${agent_flags} ${escaped_prompt}" + sprite exec -s "${sprite_name}" -- zsh -c "source ~/.zshrc && ${agent_name} ${agent_flags} ${safe_prompt}" else # Generic SSH execution - ${exec_callback} "${sprite_name}" "source ~/.zshrc && ${agent_name} ${agent_flags} ${escaped_prompt}" + ${exec_callback} "${sprite_name}" "source ~/.zshrc && ${agent_name} ${agent_flags} ${safe_prompt}" fi } @@ -2371,7 +2383,8 @@ ssh_run_server() { local ip="${1}" local cmd="${2}" # Single-quoted so $HOME/$PATH expand on the remote side, not locally. - local path_prefix='export PATH="$HOME/.local/bin:$HOME/.bun/bin:$PATH"' + # .npm-global/bin: user-writable npm prefix (AWS Lightsail runs as ubuntu, not root) + local path_prefix='export PATH="$HOME/.npm-global/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH"' if [[ -n "${SPAWN_DEBUG:-}" ]]; then cmd="set -x; ${cmd}" fi @@ -3190,6 +3203,22 @@ setup_openclaw_config() { upload_config_file "${upload_callback}" "${run_callback}" "${openclaw_json}" "\$HOME/.openclaw/openclaw.json" } +# Start OpenClaw gateway as a fully detached daemon +# Usage: start_openclaw_gateway RUN_CALLBACK +# +# Arguments: +# RUN_CALLBACK - Function to run commands: func(command) +# +# SSH/exec channels hang if a backgrounded daemon inherits the session's file +# descriptors. setsid creates a new session, fully detaching the gateway so +# the channel can close. Falls back to nohup where setsid is unavailable +# (e.g. macOS local — no SSH, so the hang doesn't apply). +start_openclaw_gateway() { + local run_callback="${1}" + log_step "Starting OpenClaw gateway daemon..." + ${run_callback} "source ~/.zshrc 2>/dev/null; if command -v setsid >/dev/null 2>&1; then setsid openclaw gateway > /tmp/openclaw-gateway.log 2>&1 < /dev/null & else nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 < /dev/null & fi" +} + # Wait for OpenClaw gateway to be ready # Usage: wait_for_openclaw_gateway RUN_CALLBACK # diff --git a/sprite/amazonq.sh b/sprite/amazonq.sh index b2773a63..1450d2d0 100755 --- a/sprite/amazonq.sh +++ b/sprite/amazonq.sh @@ -16,11 +16,10 @@ agent_install() { install_agent "Amazon Q CLI" "curl -fsSL https://desktop-release.q.us-east-1.amazonaws.com/latest/amazon-q-cli-install.sh | bash" cloud_run } +# Amazon Q uses AWS Builder ID auth — cannot route through OpenRouter. agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" } agent_launch_cmd() { diff --git a/sprite/cline.sh b/sprite/cline.sh index 28260bc6..4dd505ed 100755 --- a/sprite/cline.sh +++ b/sprite/cline.sh @@ -18,9 +18,12 @@ agent_install() { agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" +} + +agent_configure() { + log_step "Authenticating Cline with OpenRouter..." + cloud_run "source ~/.zshrc && cline auth -p openrouter -k \"${OPENROUTER_API_KEY}\"" } agent_launch_cmd() { diff --git a/sprite/interpreter.sh b/sprite/interpreter.sh index db67abf0..505b71a4 100755 --- a/sprite/interpreter.sh +++ b/sprite/interpreter.sh @@ -22,9 +22,7 @@ agent_install() { agent_env_vars() { generate_env_config \ - "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_API_KEY=${OPENROUTER_API_KEY}" \ - "OPENAI_BASE_URL=https://openrouter.ai/api/v1" + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" } agent_launch_cmd() { diff --git a/sprite/nanoclaw.sh b/sprite/nanoclaw.sh index b5732c72..b1e73fc9 100644 --- a/sprite/nanoclaw.sh +++ b/sprite/nanoclaw.sh @@ -13,6 +13,8 @@ log_info "NanoClaw on Sprite" echo "" agent_install() { + log_step "Installing Docker (required by NanoClaw on Linux)..." + cloud_run "command -v docker >/dev/null || (curl -fsSL https://get.docker.com | sh)" log_step "Installing tsx..." cloud_run "source ~/.bashrc && bun install -g tsx" log_step "Cloning and building nanoclaw..." @@ -28,13 +30,9 @@ agent_env_vars() { } agent_configure() { - log_step "Configuring nanoclaw..." - local dotenv_temp - dotenv_temp=$(mktemp) - chmod 600 "${dotenv_temp}" - track_temp_file "${dotenv_temp}" - printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${dotenv_temp}" - cloud_upload "${dotenv_temp}" "/root/nanoclaw/.env" + local dotenv_content + dotenv_content=$(printf 'ANTHROPIC_API_KEY=%s\nANTHROPIC_BASE_URL=https://openrouter.ai/api\n' "${OPENROUTER_API_KEY}") + upload_config_file cloud_upload cloud_run "${dotenv_content}" "\$HOME/nanoclaw/.env" } agent_launch_cmd() { diff --git a/sprite/openclaw.sh b/sprite/openclaw.sh index 54f21301..9638e440 100755 --- a/sprite/openclaw.sh +++ b/sprite/openclaw.sh @@ -31,7 +31,7 @@ agent_configure() { } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1