PR #2064 fixed _exec_long shell injection for DigitalOcean and Sprite
but missed the same bash -c '${cmd}' pattern in Hetzner, GCP, AWS, and
Daytona. Apply the same base64-encoding fix to all four.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Base64-encode the command before embedding it in bash -c to prevent
single-quote breakout in _sprite_exec_long and _digitalocean_exec_long.
Fixes#2063
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove orphaned sh/test/fixtures/ directory. These shell fixture files
(_shared_agent_assertions.sh, hetzner/_env.sh, hetzner/_api_assertions.sh,
digitalocean/_env.sh, digitalocean/_api_assertions.sh) were part of a mock
test harness (mock.sh) that was removed from the repository. The fixture
files reference `assert_api_called` and `MOCK_LOG` variables that are never
defined anywhere, confirming they are unreachable dead code.
Scan results:
- Dead code (sh/test/fixtures/): 5 orphaned fixture files removed
- Dead code (sh/shared, packages/cli/src/): none found
- Stale references to non-existent files: none found
- Python usage (python3 -c / python -c): none found
- Duplicate utilities across cloud modules: loadTokenFromConfig pattern
exists in hetzner/daytona/digitalocean but reads from different cloud-
specific config paths — cannot be consolidated (confirmed intentional)
- Stale comments: none found beyond those already fixed in prior PRs
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: Remove dead code and stale references
Fix stale path comment in sh/shared/key-request.sh that referenced
the wrong location for loadTokenFromConfig (cli/src/ instead of
packages/cli/src/). Also updated wording from "Must match" to "Keep
in sync with" to more accurately describe the relationship.
Scan results (no other issues found):
- Dead code (sh/shared, packages/cli/src): none found
- Stale references to non-existent files: none found
- Python usage (python3 -c / python -c): none found
- Duplicate utilities across cloud modules: none (cloud-specific config
loading functions share the same pattern but read from different paths
and cannot be consolidated)
- Stale comments: one stale path in key-request.sh (fixed)
-- qa/code-quality
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor: Remove dead code and stale references
Remove duplicate `log_step` function from `sh/shared/github-auth.sh`.
`log_step` was identical to `log_info` (same printf format, same output
stream) and had no semantic distinction. All 6 call sites are updated to
use `log_info` directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* fix(sprite): fix all 6 Sprite agent installs for E2E
- Use `npm install -g --prefix` instead of `npm config set prefix` to
avoid creating .npmrc that conflicts with nvm on Sprite VMs
- Fix shell environment setup to only modify .bash_profile (not .bashrc)
so non-interactive bash -c commands retain PATH config
- Add $HOME/.cargo/bin to PATH for zeroclaw (Sprite has no ~/.cargo/env)
- Add $HOME/.local/bin to PATH config for Sprite shell environment
- Add sprite E2E cloud driver with org detection, config corruption fix,
direct command embedding (not $1 positional), and retry logic
- Fix provision.sh to kill full process tree after timeout (prevents
orphaned sprite exec sessions from corrupting config)
- Fix verify.sh zeroclaw check to not rely on ~/.cargo/env existing
Tested: 6/6 Sprite agents pass E2E (claude, codex, openclaw, zeroclaw,
opencode, kilocode). Hermes is not in the Sprite manifest.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: biome format - collapse runSprite call to single line
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Fix stale path comment in sh/shared/key-request.sh that referenced
the wrong location for loadTokenFromConfig (cli/src/ instead of
packages/cli/src/). Also updated wording from "Must match" to "Keep
in sync with" to more accurately describe the relationship.
Scan results (no other issues found):
- Dead code (sh/shared, packages/cli/src): none found
- Stale references to non-existent files: none found
- Python usage (python3 -c / python -c): none found
- Duplicate utilities across cloud modules: none (cloud-specific config
loading functions share the same pattern but read from different paths
and cannot be consolidated)
- Stale comments: one stale path in key-request.sh (fixed)
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Non-interactive SSH sessions don't source .bashrc or .zshrc, so binaries
installed to ~/.local/bin (hermes via uv) or ~/.npm-global/bin (codex,
kilocode via npm) were not found during verification.
Fix all three verify functions and the codex input test to use explicit
PATH with the known install directories, matching the pattern already
used by openclaw and claude.
Verified: AWS 7/7, Hetzner 6/6 implemented, GCP 6/6 implemented.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The teardown was doing a single DELETE without --max-time, so connection
timeouts caused HTTP 000 and the droplet was never deleted. When running
6 agents in batches of 3, batch 1's stale droplet caused batch 2 to fail
with "will exceed your droplet limit."
Fix:
- Add --max-time 30 to prevent curl hangs
- Retry DELETE up to 3 times on failure
- Poll the API after DELETE to confirm the droplet is actually gone (up to 60s)
- Remove -f flag from curl so %{http_code} is always captured
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The openclaw e2e input test was failing for three independent reasons:
1. PATH missing ~/.npm-global/bin — openclaw installs via npm with a
custom prefix, but verify_openclaw and input_test_openclaw didn't
include that directory in PATH
2. Wrong CLI invocation — used `openclaw -p` which doesn't exist.
The correct command is `openclaw agent --message "..." --session-id`
3. Gateway not running — the openclaw gateway (port 18789) can die
between provisioning and verification. Now the input test ensures
the gateway is running before sending the prompt.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Hermes agent was fully implemented in shared/agent-setup.ts (createAgents
includes hermes with install, envVars, and launchCmd) but the convenience
shell script sh/aws/hermes.sh was missing and the matrix showed "missing".
- Add sh/aws/hermes.sh (matching pattern of all other aws agent scripts)
- Update manifest.json: "aws/hermes" -> "implemented"
- Update sh/aws/README.md with Hermes Agent install command
Discovered during QA E2E sweep: E2E suite lists hermes in ALL_AGENTS and
would attempt to provision it; without the matrix entry and script the
agent was silently untracked as a missing implementation gap.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `hermes` to ALL_AGENTS in sh/e2e/lib/common.sh (stale: hermes added to
manifest.json in #2023 but never added to the e2e agent list)
- Add verify_hermes() and input_test_hermes() to sh/e2e/lib/verify.sh and
wire them into verify_agent/run_input_test dispatch tables
- Remove dead log_warn() from sh/shared/github-auth.sh (defined but never called)
- Remove dead get_cloud_env_vars() from sh/shared/key-request.sh (no callers outside file)
- Remove dead invalidate_cloud_key() from sh/shared/key-request.sh (no callers anywhere)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Implements Hermes Agent on Sprite cloud. Hermes is a persistent AI
agent by Nous Research with multi-platform messaging (Telegram,
Discord, Slack, CLI), memory across sessions, tool use, and native
OpenRouter support.
- Add hermes agent entry to manifest.json with env config
- Add matrix entries for all 7 clouds (sprite implemented, rest missing)
- Create sh/sprite/hermes.sh thin bash shim
Closes#1952
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace eval "${cloud_env}" with a while-read loop that only
processes lines matching ^export VAR="VALUE" — arbitrary shell
commands in cloud driver output are silently ignored.
Also removes the now-unused cloud_env local variable since the
while-read loop calls cloud_headless_env directly.
Fixes#2019
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace bash -c "${cmd}" with bash -c '$1' _ "${cmd}" so the
command is passed as a positional argument, not interpolated into
the shell string. Same pattern applied to the timeout wrapper.
Fixes#2018
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: SPAWN_CLI_DIR env var to force local source in e2e and shell scripts
When SPAWN_CLI_DIR is set, the entire toolchain uses local TypeScript
source instead of downloading pre-bundled scripts from GitHub releases:
- e2e.sh: auto-sets SPAWN_CLI_DIR to repo root when running locally
- provision.sh: exports SPAWN_CLI_DIR into the headless subshell
- commands.ts: reads local shell scripts instead of fetching from CDN
- All 36 cloud/agent shell scripts: exec local main.ts when set
This enables e2e tests to validate local changes before they're released.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): add path traversal defense to SPAWN_CLI_DIR script loading
Canonicalize the path via realpathSync and verify it stays inside the
resolved CLI directory before reading. Prevents SPAWN_CLI_DIR from
being used to read arbitrary files via ../ traversal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): harden SPAWN_CLI_DIR path traversal defense
- Validate cloud/agent names don't contain '..', '/' or '\' before
constructing file paths
- Fix root-directory edge case in prefix check by handling trailing
separator correctly
Agent: pr-maintainer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
E2E tests were failing because agent installs didn't complete within
the default 120s timeout, and small VMs ran out of memory during builds.
- INSTALL_WAIT: 120s → 300s (with per-cloud override via cloud_install_wait)
- AWS: nano_3_0 → medium_3_0 (all agents need 4GB for reliable installs)
- DigitalOcean: s-1vcpu-512mb-10gb → s-2vcpu-2gb, cap at 3 parallel
- GCP: e2-medium → e2-standard-2
- Hetzner: cap at 5 parallel (primary IP limit)
- Sprite: 300s install wait (slower exec than SSH)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
When multiple clouds run in parallel, they generate the same app name
(e.g. e2e-claude-TIMESTAMP) and write to the same temp files
(.exit/.stdout/.stderr), causing data corruption.
- Include ACTIVE_CLOUD in make_app_name: e2e-gcp-claude-TIMESTAMP
- Use ${app_name} instead of ${agent} for provision temp files
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Dead code removed:
- `cleanup_stale_apps` function in `sh/e2e/lib/cleanup.sh` — defined but
never called; `e2e.sh` calls `cloud_cleanup_stale` directly instead
- `generateEnvConfig` and `AgentConfig` re-exports from all 7 cloud-specific
`agents.ts` modules (aws, hetzner, gcp, digitalocean, daytona, local,
sprite) — nothing imported these from the cloud modules; they were already
available via `@openrouter/spawn-shared` and `../shared/agents`
All 1435 tests pass, biome lint is clean (0 errors).
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(e2e): multi-cloud test suite with cloud driver pattern
Scale the E2E test suite from AWS-only to all 6 infrastructure clouds
(aws, hetzner, digitalocean, gcp, daytona, sprite) with parallel
execution support.
Architecture:
- Cloud driver pattern: each cloud implements _cloudname_func() functions
- load_cloud_driver() wires cloud-specific functions to generic names
(cloud_exec, cloud_teardown, etc.)
- Shared orchestration stays in one place, cloud details are isolated
New files:
- sh/e2e/e2e.sh — unified entry point with --cloud flag
- sh/e2e/lib/clouds/{aws,hetzner,digitalocean,gcp,daytona,sprite}.sh
Refactored:
- common.sh — removed AWS constants, added load_cloud_driver()
- provision.sh — cloud-agnostic via cloud_headless_env/cloud_provision_verify
- verify.sh — replaced aws_ssh with cloud_exec/cloud_exec_long
- teardown.sh/cleanup.sh — delegate to cloud driver functions
- aws-e2e.sh — thin wrapper: exec e2e.sh --cloud aws
Usage:
e2e.sh --cloud aws # Single cloud
e2e.sh --cloud aws --cloud hetzner # Multiple clouds in parallel
e2e.sh --cloud all --parallel 3 # All clouds, 3 agents parallel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(e2e): prevent subshell EXIT trap inheritance and single-cloud early exit
- Reset EXIT trap in multi-cloud subshells to prevent LOG_DIR deletion
before the main process reads log files
- Use `|| true` for single-cloud run_agents_for_cloud to prevent set -e
from skipping the summary on env validation failure
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: default to parallel agent provisioning in e2e tests
All agents within a cloud now run in parallel by default instead of
sequentially. Use --sequential to restore the old behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: cap sprite parallelism, 4GB for openclaw, remove stderr suppression
- Sprite: add _sprite_max_parallel (cap 2 concurrent agents) to avoid
CLI rate limiting that caused all 6 agents to fail
- AWS: use medium_3_0 (4GB) bundle for openclaw which needs more RAM
- Input tests: remove 2>/dev/null from agent commands so failures
produce visible error output instead of empty responses
- Add cloud_max_parallel to driver interface, respected by e2e.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use bash instead of sh for exec_long across all cloud drivers
Ubuntu's /bin/sh is dash, which doesn't support bash-specific PATH
sourcing from .spawnrc/.cargo/env. This caused codex and zeroclaw
input tests to fail with "command not found" even though verify passed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: codex input test uses positional prompt, not -q flag
codex CLI takes prompt as positional arg: `codex "PROMPT"`.
The -q flag doesn't exist, causing "Usage:" error output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use codex exec -q for non-interactive input test
codex requires `exec` subcommand for non-interactive mode.
Plain `codex PROMPT` expects a TTY (stdin is not a terminal).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: codex exec takes no -q flag, just positional prompt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use cx23 instead of deprecated cx22 for Hetzner e2e tests
Hetzner deprecated server type cx22 (ID 104). The default now uses cx23.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The provision.sh was setting wrong env var names that the TypeScript CLI
does not read:
- AWS_LIGHTSAIL_INSTANCE_NAME → LIGHTSAIL_SERVER_NAME (read by aws.ts:getServerName)
- AWS_REGION → AWS_DEFAULT_REGION (read by aws.ts:authenticate/promptRegion)
- AWS_BUNDLE → LIGHTSAIL_BUNDLE (read by aws.ts:promptBundle)
Without the correct names, each provisioning run created an instance with a
random generated name instead of app_name, causing the post-provision
existence check to fail every time.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove stale Fly.io references from shared shell scripts. Fly.io was
removed as a cloud provider (#1979) but comments referencing its
specific token format ("FlyV1 <macaroon>") and container behavior
remained in key-request.sh and github-auth.sh.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(hetzner): update deprecated cx22/cpx21 server types to cx23/cpx22
Hetzner deprecated the entire cx*2 and cpx*1 server lines on Jan 1, 2026.
New orders fail with "server type is deprecated". Updates to the current
gen3 CX and gen2 CPX lines (cx23, cx33, cx43, cx53, cpx22, cpx32).
Also shows the server type picker by default instead of requiring --custom,
so users can choose their instance size on every deploy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(zeroclaw): append autonomy config instead of overwriting onboard output
zeroclaw onboard generates a complete config with required fields like
default_temperature. Our setup was overwriting that with a partial config
missing required fields, causing a crash loop on startup. Now appends
the security/shell settings instead so onboard's fields are preserved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix biome formatting in agent-setup.ts
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Agent: pr-maintainer
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- Replace local rec() helper in hetzner.ts with shared toRecord() from
@openrouter/spawn-shared, eliminating a duplicate implementation that
already existed in the shared package with equivalent behavior
- Fix stale comment in key-request.sh referencing non-existent qa.sh
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* feat!: remove Fly.io cloud provider support
Drop Fly.io as a supported cloud provider. Sprite (which uses Fly.io
infrastructure internally) is retained.
- Delete packages/cli/src/fly/ module, sh/fly/ scripts, fixtures/fly/
- Remove fly cloud entry and 6 fly matrix entries from manifest.json
- Remove fly imports, destroy cases, and connection handlers from commands.ts
- Remove fly-ssh sentinel from security.ts
- Port E2E test suite from Fly.io to AWS Lightsail (fly-e2e.sh → aws-e2e.sh)
- Update README (7 clouds, 42 combinations), CLAUDE.md, and skill prompts
- Clean up fly references in build config, gitignore, icon sources
- Bump CLI version to 0.11.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: restore Docker image build under sh/docker/
Move openclaw Dockerfile from sh/fly/docker/ to sh/docker/ and rename
workflow from fly-docker.yml to docker.yml with updated paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix extra blank lines in commands.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* refactor: remove dead offerGithubAuth exports from cloud agents.ts files
The per-cloud offerGithubAuth re-exports in each cloud's agents.ts were
never called from outside their own module. The actual GitHub auth
orchestration is handled by shared/orchestrate.ts which calls
offerGithubAuth from shared/agent-setup.ts directly.
Also update stale comment in sh/test/fixtures/_shared_agent_assertions.sh
that referenced mock.sh, a test harness file that no longer exists in
the repository.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: collapse multi-line imports to single-line per biome format
After removing offerGithubAuth exports, the remaining 2-import blocks
should be single-line. Also collapse fly/agents.ts 4-import block and
remove trailing blank line.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Add Environment Variables section to sh/digitalocean/README.md with:
- DO_REGION: list of all 10 available regions with default (nyc3)
- DO_DROPLET_SIZE: list of all 6 available sizes with default (s-2vcpu-4gb)
- --custom flag: interactive region + size picker
Fixes#1968
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#1933. The comments incorrectly implied base64 encoding alone
prevents injection. Safety relies on the combination of base64 output
(no single quotes in alphabet) + single-quote wrapping. Made this
explicit in all 7 affected comments.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Eliminates nested quote eval pattern in favor of direct pipe to sh,
removing potential injection surface in fly_ssh and fly_ssh_long.
Fixes#1927
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace inline env-var prefix pattern (VAR=value command) with explicit
export statements inside the subshell. While the inline prefix is
POSIX-compliant and not a real injection vector, explicit exports are
clearer about intent, eliminate the fragile backslash-continuation chain,
and prevent future copy-paste of the pattern into unsafe contexts.
Fixes#1924
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace single-quote escaping (which only handled ' but not other shell
metacharacters like $(), backticks, ;, ||, &&, |) with base64 encoding.
Base64 output contains only [A-Za-z0-9+/=] characters, completely
eliminating shell metacharacter injection risks regardless of command
content. Compatible with both GNU coreutils (Linux) and BSD (macOS).
Fixes#1912
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- MEDIUM: Validate flyctl auth status before empty FLY_API_TOKEN fallback
in provision.sh (fail fast instead of silent failure)
- LOW: Fix sed -i portability in qa.sh (use sed -i.bak for macOS compat)
- LOW: Increase FLY_API_TOKEN expiry from 2h to 8h in common.sh
- LOW: Add --proto '=https' to all curl -L calls in digitalocean scripts
(6 files) to prevent HTTP downgrade on redirects
Fixes#1913
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: run bun in foreground in DigitalOcean scripts to unfreeze interactive prompts
The _run_with_restart function backgrounded bun with `& + wait` so a SIGTERM
trap could forward the signal. But backgrounding removes bun from the terminal's
foreground process group, which prevents @clack/prompts multiselect from entering
raw mode — arrow keys print as raw escape sequences (^[[A^[[B) and the SSH key
selection prompt freezes.
Fix: run bun in the foreground and detect SIGTERM from exit code 143 (128+15)
instead of using a trap flag + PID tracking. This preserves the restart-on-signal
behavior while giving bun full terminal access for interactive prompts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace @clack/prompts multiselect with /dev/tty picker for SSH keys
When the CLI (parent bun) spawns bash → child bun for cloud scripts,
the parent's event loop keeps fd 0 registered and races with the child's
@clack/prompts for terminal input. This causes the SSH key multiselect
to render but freeze — arrow keys print as raw escape sequences.
Fix: add multiPickToTTY() in picker.ts that opens /dev/tty directly,
bypassing process.stdin entirely. Replace the @clack/prompts multiselect
in ssh-keys.ts with this new function. Also add process.stdin.unref()
to prepareStdinForHandoff() so the parent stops polling fd 0.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* perf: disable SSH compression for interactive sessions
Compression=yes adds per-keystroke CPU overhead that causes
noticeable input lag on normal connections. Only beneficial
on slow/high-latency links.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add live input/output E2E verification for agents
The E2E suite previously only verified static artifacts (binaries, config
files, env vars). An agent with a broken API key or crash-on-launch bug
would pass all checks. This adds an input test phase that sends a real
prompt to each agent and verifies the response contains a marker string.
- Add fly_ssh_long() with configurable timeout for long-running commands
- Add per-agent input test functions (claude -p, codex -q, openclaw -p,
zeroclaw agent -p; opencode/kilocode skip as TUI-only)
- Add run_input_test() dispatcher with SKIP_INPUT_TEST env var support
- Add --skip-input-test CLI flag to fly-e2e.sh
- Chain input test after verify in run_single_agent() pipeline
- Add INPUT_TEST_TIMEOUT constant (default 120s, env-overridable)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: format p.text({ message }) to multi-line for biome
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: correct CLI entry point path in E2E provision script
The path resolution went up 2 levels (../../) from sh/e2e/lib/ which
landed in sh/ instead of the repo root. After the monorepo restructure,
packages/cli/src/index.ts is at the repo root — need 3 levels (../../..).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: format p.text({ message }) to multi-line for biome
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
GCP instance creation was failing with 'Invalid value for field
resource.networkInterfaces[0].subnetwork' when the project VPC uses
custom subnet mode. Add --network and --subnet flags defaulting to
'default', with GCP_NETWORK and GCP_SUBNET env var overrides for
custom VPC setups.
Fixes#1882
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
When bun is freshly installed, rc file patching only takes effect
in new shells. Strengthen the post-install PATH export to include
both hard-coded $HOME/.bun/bin and $HOME/.local/bin alongside
$BUN_INSTALL/bin, so bun and spawn are available in the current
execution context immediately after install.
Fixes#1874
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: remove broken clone_cli() — go straight to pre-built binary
The clone_cli() function (added before the monorepo migration, PR #1853)
only fetches top-level .ts files via the GitHub Contents API. Since the
monorepo reorganised source code into subdirectories (aws/, fly/, hetzner/,
shared/, etc.), clone_cli() silently downloads an incomplete source tree.
bun run build then always fails because cross-directory imports cannot
resolve, and the installer falls through to the pre-built binary anyway.
Every install was burning ~12 unnecessary GitHub API requests (rate-limited
at 60/hr for unauthenticated clients) and several seconds of wasted bun
install + failed build time.
Fix: remove clone_cli() entirely, replace build_and_install() with a
direct binary download. Behaviour is identical for all users (binary path
was already the universal outcome); installs are now faster and cheaper on
the API rate limit.
Agent: code-health
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: update install.sh tests for simplified binary-only installer
Remove tests for clone_cli() and source builds which were removed in
the parent commit. Add tests verifying the direct binary download
approach and asserting that the old clone/build code is gone.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix biome format errors in commands.ts and duplicate-detection test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The linter was running in CI with --warn-only, meaning it never
blocked anything — effectively vaporware. This removes --warn-only
to make it a real gate.
Also adds rules for bash 4.0+ features that were documented in
CLAUDE.md but not enforced:
- MC014: readarray/mapfile (bash 4.0+)
- MC015: coproc (bash 4.0+)
- MC016: &>> redirect (bash 4.0+)
- MC017: relative source paths (breaks curl|bash)
- MC018: wait -n (bash 4.3+)
- MC019: declare -g (bash 4.2+)
Excludes .claude/worktrees/ from scanning (temp copies, not
committed code).
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Agent processes on DigitalOcean droplets were dying silently on SIGTERM
with no logging or recovery. This adds:
- SIGTERM trap handler that logs signal, timestamp, and dashboard URL
- SIGHUP trap handler for terminal disconnection
- Restart loop with exponential backoff (up to 3 attempts) on SIGTERM
- Child process forwarding so bun receives the signal cleanly
Replaces bare `exec bun run` with a managed foreground process that
the shell can monitor and restart.
Fixes#1859
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
All 6 Hetzner agent scripts silently ignored bun installation failures.
Now matches the robust pattern used by every other cloud (aws, fly, gcp,
digitalocean, daytona, local, sprite): prints status, exits on curl failure,
and verifies bun is available after install.
Fixes#1854
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Restructure the repo as a Bun workspace monorepo:
- Move cli/ → packages/cli/
- Create packages/shared/ (@openrouter/spawn-shared) with type-guards and parse utilities
- Add root package.json with workspace configuration
- Update all CLI imports to use @openrouter/spawn-shared
- Deduplicate toRecord/toObjectArray helpers from 4 cloud modules
- Update SPA (slack-bot) to use shared package instead of local toObj()
- Update 48 agent shell scripts for new packages/cli/ path
- Update install.sh, install.ps1, e2e, and test scripts
- Update all GitHub workflows, .gitignore, pre-commit hooks
- Update CLAUDE.md, README.md, and skill prompt references
- Pin all dependency versions (no ^ ranges)
- Bump CLI version 0.9.1 → 0.10.0
All 1908 tests pass. Lint clean. All 8 cloud bundles build.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Adds a Prerequisites section to sh/aws/README.md (updated path after
shell scripts reorganization in #1843) with the Lightsail activation
URL as the first step
- Surfaces the Lightsail activation URL in createInstance error hints
for both CLI and REST paths so users get actionable guidance on failure
- Bumps CLI to 0.9.1
Fixes#1838
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* refactor: move test fixtures to root /fixtures directory
Moves test/fixtures/ → fixtures/ at the repo root for easier
discoverability. Updates all references in CLAUDE.md and QA
prompt files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: move install.ps1 to sh/cli/ and fixtures to root
- Moves cli/install.ps1 → sh/cli/install.ps1 (consistent with install.sh)
- Moves test/fixtures/ → fixtures/ at the repo root
- Updates all references in README, CLAUDE.md, QA prompts, and the ps1 itself
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>
Reorganizes the project so all shell scripts live under a dedicated
/sh directory, enabling the OpenRouter rewrite URL to point at /sh/
instead of the repository root.
Moves:
- cli/install.sh → sh/cli/install.sh
- shared/*.sh → sh/shared/*.sh
- {cloud}/{agent}.sh → sh/{cloud}/{agent}.sh (48 scripts)
- {cloud}/README.md → sh/{cloud}/README.md
- e2e/*.sh → sh/e2e/*.sh
- test/macos-compat.sh → sh/test/macos-compat.sh
- test/fixtures/**/*.sh → sh/test/fixtures/**/*.sh
Updates all references:
- RAW_BASE path construction in commands.ts, update-check.ts
- GitHub auth URL in agent-setup.ts
- Self-referencing URLs in install.sh, github-auth.sh
- CI workflow paths in lint.yml, cli-release.yml
- Test file paths in install-script-validation, manifest-integrity
- Documentation in README.md, cli/README.md, CLAUDE.md
- QA scripts in .claude/skills/
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>