Simplify the cloud matrix by removing Daytona. All Daytona-specific code,
scripts, tests, and configuration have been removed. Daytona has been moved
to "Previously Considered" in the Cloud Provider Wishlist (#1183) and can
be revived on community demand.
Closes#2260
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: tarball workflow failures (root ownership, swapfile, hermes TTY)
- Use sudo mv + chown for tarball in release step (root-owned from capture)
- Skip swapfile creation if /swapfile already exists (GitHub Actions runners)
- Tolerate hermes setup wizard failure when /dev/tty unavailable in CI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: capture claude symlink target in tarball + fix verify PATH
The claude installer creates a symlink at ~/.local/bin/claude pointing
to ~/.local/share/claude/versions/X.Y.Z. The capture script was missing
~/.local/share/claude/, causing a broken symlink in the tarball.
Also add ~/.npm-global/bin to the verify PATH check for claude (npm
fallback install path).
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>
GITHUB_TOKEN containing newlines, tabs, or carriage returns could
corrupt ~/.config/gh/hosts.yml before permissions are set (line 314)
and bypass validation in downstream consumers. Defense-in-depth fix
following the pattern established in sh/shared/key-request.sh:78.
Fixes#2239
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Remove sh/e2e/aws-e2e.sh: dead backwards-compat wrapper with no
references (superseded by unified e2e.sh --cloud aws)
- Remove getStatusDescription from commands/shared.ts: defined and
tested but never called in production code
- Remove parseJsonRaw from packages/cli/src/shared/parse.ts: zero
production usages (still available in packages/shared if needed)
- Update corresponding test files to remove dead code tests
- Bump CLI version to 0.14.4
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- manifest.json: change aws auth to AWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY
so the key-request system includes AWS in its missing-key emails
- sh/e2e/e2e.sh: clouds missing credentials now SKIP (not FAIL), so
running --cloud all is safe and only tests what's configured
- qa.sh: include e2e mode in cloud credential loading (was fixtures+quality only)
- qa-quality-prompt.md: e2e-tester now runs e2e.sh --cloud all --parallel 6 --skip-input-test
- qa-e2e-prompt.md: standalone e2e bot now runs e2e.sh --cloud all --parallel 6
Also wires KEY_SERVER_URL + KEY_SERVER_SECRET into /etc/spawn-qa-auth.env
(system change, not in this commit) so missing-key emails are actually sent.
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>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Add key_request: false to Daytona in manifest.json and update
_parse_cloud_auths() to skip clouds with that flag set.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(e2e): add openclaw gateway kill/restart resilience test
Verifies that the openclaw gateway auto-restarts after being killed
with SIGKILL, validating the systemd Restart=always supervision.
The test runs as part of verify_openclaw:
1. Confirms gateway is listening on :18789
2. Kills it with SIGKILL (simulates a hard crash)
3. Waits up to 30s for systemd to auto-restart it
4. Verifies port 18789 comes back online
If the gateway isn't running (e.g. non-systemd env), the test is
skipped gracefully. On failure, dumps systemd status and gateway
logs for diagnostics.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Revert "test(e2e): add openclaw gateway kill/restart resilience test"
This reverts commit 39b79d5c12.
* test: add unit tests for openclaw gateway resilience config
Verifies that startGateway() produces correct systemd and cron
configuration for auto-restart after a gateway crash:
- Restart=always and RestartSec=5 in the systemd unit
- Cron heartbeat checks port 18789 and restarts if dead
- Wrapper script sources .spawnrc and execs openclaw gateway
- Multiple port-check fallbacks (ss, /dev/tcp, nc)
- Non-systemd fallback to setsid/nohup
- 300s startup timeout
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test(e2e): add openclaw gateway kill/restart resilience test
Kills the gateway with SIGKILL during verify_openclaw and verifies
systemd Restart=always brings it back within 30s. Skips gracefully
on non-systemd environments.
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>
1. promptSpawnName() now checks DO_DROPLET_NAME before generating a
random name, matching getServerName() behavior. This fixes the e2e
harness creating droplets as spawn-XXXX when it expects
e2e-digitalocean-AGENT-TIMESTAMP.
2. Replace BASH_REMATCH with sed-based parsing in provision.sh for
macOS bash 3.2 compatibility. BASH_REMATCH was returning empty
values, causing `export: '=': not a valid identifier`.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The Hermes installer downloads Node.js as a .tar.xz archive. Without
xz-utils, tar cannot decompress it (exit code 2).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat(docker): replace Packer snapshots with Docker-based agent delivery
Docker images on GHCR are public and cross-account, unlike DO snapshots
which are private/account-scoped. Cloud-init installs Docker + pulls the
agent image during boot. The install step extracts pre-built binaries via
`docker cp` and falls back to normal install if unavailable.
- Add Dockerfiles for all 7 agents (claude, codex, openclaw, opencode,
kilocode, zeroclaw, hermes)
- Convert docker.yml to matrix build for all agents
- Add tryInstallFromDocker() shared helper with Docker-first install
- Add Docker pull to DigitalOcean cloud-init userdata
- Remove Packer snapshot pipeline, lookup, and SSH-only wait
- Remove packer/ directory (HCL templates, tier scripts, agents.json)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* security: address review findings in docker agent delivery
- Add agentName validation regex (/^[a-z0-9-]+$/) in digitalocean.ts
before interpolation into cloud-init script
- Quote dockerImage variable in all docker command strings in
agent-setup.ts to prevent command injection
- Restrict docker cp to specific known directories (.claude, .bun,
.local, .npm, .cargo, .opencode) instead of blanket /root/.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- Drop unnecessary `export` from `createAgents` and `resolveAgent` in
agent-setup.ts — both are internal helpers only ever called within the
same module via `createCloudAgents`; no external caller imports them
- Fix misleading relative-path sourcing example in github-auth.sh header
comment — the shell-script rules ban relative `source ./` paths, and the
example is updated to show the correct CDN eval pattern
- Bump CLI patch version 0.12.17 → 0.12.18
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>
- Update key-request.sh comment that referenced non-existent
loadTokenFromConfig function in digitalocean.ts
- Update test comments referencing validateAgent/validateCloud
which were renamed to validateEntity
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: migrate shell script URLs to openrouter.ai/labs/spawn CDN
Users on older CLI versions can't auto-update because the repo was restructured
(cli/ → packages/cli/), so old version-check URLs 404. This decouples the CLI
from the repo's internal directory structure:
- Shell script URLs (install, agent scripts, github-auth) now use
openrouter.ai/labs/spawn/* as primary with GitHub raw as fallback
- Version checks now use GitHub release artifact (cli-latest/version)
as primary — a static URL that never changes regardless of repo layout
- CI workflow updated to publish a `version` file alongside cli.js
- Remove GITHUB_RAW_URL_PATTERN validation (no longer needed since
install URL is now a hardcoded CDN string, not interpolated)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix biome formatting in update-check test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: CLAUDE.md says biome lint but should say biome check
biome lint only checks lint rules, not formatting. biome check does both.
The hooks and CI already run biome check — the docs were out of sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(hooks): PostToolUse hook wasn't running biome on CLI source files
Two bugs in validate-file.ts:
1. Config search only checked 1-2 levels up from the edited file, but
biome.json is at packages/cli/ — 3 levels above src/__tests__/*.ts.
Fix: walk up directories until biome.json is found (or hit root).
2. Ran `biome format` (prints formatted output, always exits 0) instead
of `biome format --check` (exits non-zero if file needs formatting).
Fix: use `biome check` which does lint + format check in one pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): validate env values in cloud_headless_env parser
Reject values containing shell metacharacters ($, backtick, ;, &, |, <, >)
to prevent potential command injection if a cloud driver returns malicious output.
Fixes#2139
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): replace env value blacklist with whitelist regex
The blacklist approach missed dangerous characters like (), quotes,
backslash, newlines, {}, and !. Switch to a whitelist that only allows
[A-Za-z0-9@%+=:,./_-] — a strict safe set sufficient for env values.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The fallback .spawnrc construction (used when provision times out before
.spawnrc is written) had two bugs:
1. zeroclaw case wrongly included OPENAI_API_KEY and OPENAI_BASE_URL —
these are hermes env vars, not zeroclaw's. zeroclaw only needs
ZEROCLAW_PROVIDER=openrouter (plus the base OPENROUTER_API_KEY).
2. hermes and kilocode were missing from the case statement entirely.
- hermes needs OPENAI_BASE_URL and OPENAI_API_KEY (verify_hermes
checks for OPENAI_BASE_URL in .spawnrc)
- kilocode needs KILO_PROVIDER_TYPE=openrouter and
KILO_OPEN_ROUTER_API_KEY (verify_kilocode checks KILO_PROVIDER_TYPE)
Without these fixes, hermes and kilocode would fail verification whenever
provisioning timed out before the normal .spawnrc was written.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): add --proto '=https' to all curl bun installer calls
Fixes#2134
All _ensure_bun() functions across aws, hetzner, gcp, local, daytona,
and sprite scripts now enforce HTTPS-only downloads via --proto '=https'.
This prevents MITM attacks during bun installation on remote VMs.
DigitalOcean scripts were already correct and are not changed.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): add --proto '=https' to bun installer in TS files
Address security reviewer feedback: the same MITM vulnerability
existed in 5 TypeScript programmatic provisioning files.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): quote --proto '=https' in su -c curl calls
The aws.ts and gcp.ts files had --proto =https without quotes inside
su -c '...' blocks. Uses double quotes ("=https") to properly nest
inside the single-quoted su -c argument while maintaining protocol
restriction.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove `cleanup_stale_apps()` in `sh/e2e/lib/cleanup.sh` which was dead
code — defined but never called. The E2E orchestrator (`e2e.sh`) invokes
`cloud_cleanup_stale` directly on the active cloud driver; the wrapper
function and its file served no purpose.
Also remove the corresponding `source` call in `e2e.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>
* fix: address 4 reliability issues across codebase
1. sprite.ts: add --force to destroy command (stdin is "ignore" so
interactive prompts would hang until 60s timeout)
2. verify.sh: replace /dev/tcp port checks with ss -tln primary
(Debian/Ubuntu bash compiled without /dev/tcp support)
3. verify.sh: make _openclaw_restart_gateway a hard failure instead
of log_warn (matching _openclaw_ensure_gateway behavior)
4. agent-setup.ts: add ss -tln port check + "already running" early
exit + increase timeout from 120s to 300s (gateway takes ~3min
to initialize on AWS medium instances)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: biome format - use consistent double quotes in portCheck
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(e2e): improve openclaw reliability on AWS and other clouds
Three changes to make openclaw e2e tests more robust:
1. Increase PROVISION_TIMEOUT from 480s to 720s — AWS cloud-init
for "full" tier (Node.js + Bun + build-essential) can exceed 480s,
causing the CLI to be killed before .spawnrc is written.
2. Add .spawnrc manual fallback in provision.sh — if the CLI is killed
before writing .spawnrc, construct it via SSH using OPENROUTER_API_KEY
with agent-specific env vars (openclaw, zeroclaw).
3. Add retry logic to openclaw gateway input test — the gateway can
crash with 1006 websocket closure on resource-constrained instances.
Now retries once after killing and restarting the gateway process.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): fix command injection in e2e provision scripts
- Use printf %q and temp file for api_key handling in provision.sh to
prevent shell metachar injection (single quotes, backticks, $)
- Double-quote env_b64 interpolation in cloud_exec call to prevent
word splitting
- Replace echo with printf in bashrc append to avoid portability issues
- Replace overbroad pkill -f 'openclaw gateway' in verify.sh with
PID-targeted kill via lsof/fuser on port 18789
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
The SC2154 (referenced but not assigned) comment was leftover from a
prior version of the script. No such external variable is referenced in
the current implementation, making the suppression comment stale.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
14 agent shim scripts in sh/aws/ and sh/hetzner/ were missing error
handlers on the curl command that downloads the JS bundle from GitHub
releases. If the download failed (network issue, 404, etc.), the script
would silently proceed to exec an empty/corrupt file via bun, producing
a confusing error instead of a clear "Failed to download" message.
All other clouds (GCP, Daytona, DigitalOcean, Sprite) already had this
error handling pattern. This brings AWS and Hetzner into consistency.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Removes eval-based function creation pattern in e2e/lib/common.sh.
Uses variable indirection (ACTIVE_CLOUD global + wrapper functions)
instead of eval to reduce attack surface.
Fixes#2118
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes unquoted ${timeout} in _sprite_exec_long that could allow
command injection if timeout contained shell metacharacters.
Adds numeric validation before use.
Fixes#2117
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The outer cloud_exec_long already enforces a timeout via
INPUT_TEST_TIMEOUT. The inner --timeout 60 was redundant and could
cause premature kills before the outer timeout expired.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
The gateway startup was silently swallowed with log_warn, masking
real failures. Now tracks whether the port came up and fails the
test with the gateway log contents if it didn't.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Without --force, sprite destroy prompts for confirmation in
non-interactive E2E mode and silently fails ("Ok, come back later!"),
leaving stale instances running indefinitely.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- hetzner/hermes.sh: add thin-shim header comment, blank line after
_ensure_bun definition, and section comments (Local checkout, Remote)
to match the canonical pattern used by aws/gcp/sprite/daytona
- digitalocean/hermes.sh: add detailed _run_with_restart comment block
and inline section comments (Normal exit, SIGTERM, Other failure) to
match digitalocean/claude.sh
Both scripts now produce identical output to their cloud's reference
script (e.g. aws/hermes.sh, digitalocean/claude.sh) when the agent
name is substituted.
Fixes#2082
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#2079 — local/hermes was the only remaining missing entry in the
cloud×agent matrix. All 49 entries are now implemented.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add hermes shim scripts for GCP, Hetzner, DigitalOcean, and Daytona
- Update manifest.json matrix entries from "missing" to "implemented"
- Bump default INSTALL_WAIT from 300s to 600s to fix zeroclaw timeout
on small VMs where Rust compilation takes 8-12 minutes
- Update cloud READMEs with hermes usage docs
- Bump CLI version to 0.11.18
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
API tokens never contain spaces; allowing them risks word splitting
in downstream unquoted uses of these env vars. Updated both the shell
regex in key-request.sh and the corresponding TypeScript regexes in
digitalocean.ts to stay in sync.
Fixes#2072
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
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>