Commit graph

109 commits

Author SHA1 Message Date
A
e9f8d5ec2d
fix: secure curl header args and provision.sh export whitelist (fixes #2464, fixes #2465) (#2471)
- Replace `-H "Authorization: Bearer ..."` curl args with temp curl config
  files (`-K`) in digitalocean.sh and hetzner.sh e2e drivers, keeping API
  tokens out of `ps` output
- Replace dangerous-var blocklist in provision.sh with a positive whitelist
  of allowed cloud_headless_env variable names

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-10 17:54:32 -07:00
A
1bddd713ea
fix: base64-encode commands in SSH exec to prevent injection (#2448)
All four SSH-based cloud drivers (aws, digitalocean, gcp, hetzner)
passed the command string directly as an SSH argument, which gets
interpreted by the remote shell. While current callers pass trusted
E2E test code, this creates a security footgun for future changes.

Fix: base64-encode the command locally and decode it on the remote
side before piping to bash. The encoded string contains only safe
characters [A-Za-z0-9+/=], eliminating any injection vector. Stdin
is preserved for callers that pipe data into cloud_exec.

Closes #2432, closes #2433, closes #2434, closes #2435

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-10 13:22:33 -04:00
A
47b26deafa
fix: harden Sprite exec against injection via org flags and grep patterns (#2446)
- Replace word-split _sprite_org_flags() call sites with _sprite_cmd()
  helper that uses a proper bash array for the -o flag, eliminating
  injection risk from org names with spaces or shell metacharacters
- Validate _SPRITE_ORG against [A-Za-z0-9_-]+ in _sprite_validate_env
- Use grep -qF (fixed-string) instead of grep -q for app name matching
  to prevent regex metacharacters in names from causing false matches
- Use mktemp for _stderr_tmp in _sprite_exec instead of predictable
  PID-based path (/tmp/sprite-exec-err.$$) to prevent symlink attacks

Closes #2436

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-10 10:08:17 -07:00
A
9bf3c216e8
fix: harden provision.sh against command injection in env_b64 and app_name (#2444)
- Validate app_name at function entry (alphanumeric, dots, hyphens, underscores
  only) before it's used in file paths or passed to cloud_exec
- Add trap-based cleanup for the temp file used during .spawnrc fallback creation
- Add security comments documenting the three-layer defense model: printf %q
  quoting, base64 encoding, and stdin piping (no interpolation into command
  strings)

The core vulnerability (env_b64 interpolated into the cloud_exec command string)
was already fixed in a prior commit that switched to stdin piping. This change
adds defense-in-depth and documentation.

Fixes #2437, #2441

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 10:07:23 -07:00
A
a22fe9010c
fix: safe printf format strings and document e2e source usage (#2445)
install.sh: Replace color variable interpolation in printf format strings
with %b arguments to prevent format string injection (fixes #2443).

common.sh: Use %b for color escapes in logging functions. Document that
BASH_SOURCE and source usage in load_cloud_driver is intentional since
e2e scripts are filesystem-only, not curl|bash (fixes #2438).

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 12:28:45 -04:00
A
3724bb8ba4
fix: address SSH command injection risks in e2e cloud drivers (#2447)
Add defense-in-depth validation across all e2e cloud driver scripts:

- Validate IP addresses match IPv4 format before use in SSH commands
  (aws, digitalocean, gcp, hetzner)
- Validate SSH username contains only safe characters (gcp)
- Validate resource IDs are numeric before interpolating into API URLs
  (digitalocean droplet IDs, hetzner server IDs)
- URL-encode app name in Hetzner API query parameter to prevent
  query parameter injection
- Validate numeric env vars (INPUT_TEST_TIMEOUT, PROVISION_TIMEOUT,
  INSTALL_WAIT) that get interpolated into remote command strings

Fixes #2432, #2433, #2434, #2435, #2442

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 12:27:47 -04:00
A
a9cd3b700c
security: escape pkill regex metacharacters in app_name (#2412)
* security: escape pkill regex metacharacters in app_name

Fixes #2409 - escape regex metacharacters (., [, \, *, ^, $) in
app_name before using in pkill -f pattern to prevent unintended
process termination. Even though app_name is validated against a
safe character whitelist, . and - are regex metacharacters that
could match broader patterns than intended.

Note: #2410 (unquoted regex in bash conditional) was already fixed
by a prior commit that refactored the code to use sed instead of
[[ =~ BASH_REMATCH ]].

Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove dead exec_long functions reintroduced from pre-#2407 code

Remove cloud_exec_long dispatcher and all _*_exec_long() functions
from common.sh and cloud driver files (aws, digitalocean, gcp,
hetzner, sprite). These were explicitly removed as dead code in
PR #2407 (commit c4ae1684) and must not be reintroduced.

Issue #2410 (unquoted regex in bash conditional) is already resolved:
the [[ =~ ]] pattern was previously replaced with case/sed parsing.

Fixes #2409
Fixes #2410

Agent: pr-maintainer
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.5 <noreply@anthropic.com>
2026-03-09 21:23:33 -07:00
A
c4ae16849d
refactor: remove dead cloud_exec_long and _*_exec_long functions (#2407)
The cloud_exec_long dispatcher in common.sh and all five cloud-specific
_exec_long implementations (aws, digitalocean, gcp, hetzner, sprite)
were defined but never called by any code in the e2e test suite.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 19:39:53 -07:00
A
7801c263bb
security: verify symlink targets before overwrite in install.sh (#2404)
Before creating symlinks in /usr/local/bin, verify that any existing
symlink points to a safe location ($HOME/.local/*, $HOME/.bun/*,
/usr/local/*, $HOME/.npm-global/*). If a symlink points to an
unexpected location, warn the user and skip to prevent malicious
symlink persistence through reinstalls.

Uses portable `readlink` (without -f) for macOS bash 3.2 compatibility.

Fixes #2402

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-09 18:37:58 -07:00
A
705687de17
fix: persist npm-global PATH to .profile/.bash_profile/.bashrc for SSH reconnect (#2399)
After SSH reconnect, agent commands (openclaw, codex, kilocode, junie) were
not found because PATH was only written to ~/.bashrc, which is not sourced
by login shells. Login shells (used by SSH) source ~/.profile or
~/.bash_profile instead.

Changes:
- Write .spawnrc sourcing to ~/.profile and ~/.bash_profile in addition
  to ~/.bashrc and ~/.zshrc (orchestrate.ts)
- Write npm-global PATH export to ~/.profile and ~/.bash_profile for all
  npm-installed agents: OpenClaw, Codex, Kilo Code, Junie (agent-setup.ts)
- Write Claude Code PATH to ~/.profile and ~/.bash_profile (agent-setup.ts)
- Write OpenCode PATH to ~/.profile and ~/.bash_profile (agent-setup.ts)
- Extract NPM_GLOBAL_PATH_PERSIST constant to DRY up repeated shell snippets
- Fix e2e provision.sh to also write .spawnrc sourcing to login shell configs
- Bump CLI version to 0.15.32

Fixes #2394

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-09 16:26:49 -07:00
A
26d95e54bc
security: validate SPAWN_INSTALL_DIR against path traversal (Fixes #2385) (#2386)
Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 10:55:00 -07:00
A
f23da1523b
fix(security): fail on chmod error in github-auth.sh token persistence (#2375)
Remove `|| true` from chmod call that restricts token file permissions.
If chmod fails, authentication now aborts with an error instead of
silently leaving ~/.config/gh/hosts.yml world-readable.

Fixes #2374

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-09 08:18:07 -04:00
A
080ea5a705
fix(security): use heredoc for gh auth login to prevent token exposure (#2364)
Replaces the pipeline form with a heredoc to prevent the GitHub token
from appearing in the process list (ps aux) on multi-user systems.

Fixes #2363

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 22:10:15 -07:00
A
bd1399c861
fix: use mktemp in _sprite_fix_config to prevent race conditions (#2359)
Replaces ${cfg}.fix$$ temp pattern with mktemp for guaranteed uniqueness.
Both temp file usages in the function are updated.

Fixes #2354

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 18:46:48 -07:00
A
8bc5581e62
fix: validate base64 encoding before embedding in remote command (#2360)
Adds defense-in-depth check to reject malformed base64 output
before it is embedded in the cloud_exec remote command.

Fixes #2353

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 21:44:55 -04:00
A
e11918be59
fix: add --proto '=https' to remaining curl commands in install.sh and github-auth.sh (#2351)
Fixes #2350: Cloud agent scripts (AWS, GCP, Hetzner, Local, Sprite) already
had this flag from prior fixes. This commit adds the missing --proto '=https'
to user-facing curl instructions in sh/cli/install.sh (3 echo lines, 2 comment
lines) and usage comments in sh/shared/github-auth.sh (3 comment lines) to
prevent protocol downgrade attacks.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 22:43:25 +00:00
A
05492f5a88
fix: pin bun install to v1.3.9 in all agent scripts (#2345)
Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 12:47:18 -04:00
A
6e81186295
fix: pipe base64 credentials directly to avoid shell variable exposure (#2344)
Remove intermediate $env_b64 shell variable that stored base64-encoded
credentials. Pipe directly from base64 to cloud_exec, preventing any
credential data from appearing in process listings or shell traces.

Fixes #2333

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 09:26:17 -07:00
A
24e393817f
fix: harden env var parsing and pkill patterns in provision.sh (#2342)
- Block dangerous system env vars (PATH, LD_PRELOAD, etc.) before export
- Add explicit alphanumeric validation on env var names
- Validate app_name is non-empty and safe before pkill -f
- Tighten pkill regex from "sprite.*exec.*" to "sprite exec.*"

Fixes #2330 #2332

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 10:43:28 -04:00
A
de732fa695
fix: prevent command injection in _sprite_exec via stdin piping (#2329)
Pipe the command via stdin to bash instead of embedding it in a bash -c
string. This eliminates shell injection risk from unquoted cmd parameter,
consistent with _sprite_exec_long in the same file and other cloud drivers.

Fixes #2327

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 06:44:19 -04:00
A
23fea2df21
fix(e2e): add junie agent to E2E test harness (#2314)
The junie agent was added in #2300 but the E2E test scripts were not
updated. This adds junie to ALL_AGENTS, verify dispatch, input test
dispatch, and the provision.sh fallback env configuration.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 00:03:32 -05:00
A
252e8fc726
feat: add Junie CLI (JetBrains) agent across all 6 clouds (#2300)
Adds JetBrains' Junie CLI as a new agent in the spawn matrix.

- agent: npm install -g @jetbrains/junie-cli, launched via `junie`
- env: JUNIE_OPENROUTER_API_KEY (native OpenRouter BYOK support)
- cloudInitTier: node (npm-based install)
- matrix: all 6 clouds implemented (local, hetzner, aws, digitalocean, gcp, sprite)
- icon: JetBrains org avatar (assets/agents/junie.png)
- tests: 7 unit tests in junie-agent.test.ts
- version bump: 0.15.9 → 0.15.10

Closes #2296

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 19:38:45 -08:00
A
51dec6e877
fix: E2E failures - SSH key gen race, hetzner 409, hermes binary path (#2305)
Three distinct E2E bugs fixed:

1. SSH key generation race condition: When multiple agents provision in
   parallel, concurrent processes all call generateSshKey() and race to
   create ~/.ssh/id_ed25519. ssh-keygen won't overwrite an existing file
   (prompts on stdin which is "ignore"), causing zeroclaw/codex to fail
   with "SSH key generation failed". Fix: check if key already exists
   before generating, and re-check after a failed generation attempt.

2. Hetzner SSH key 409 uniqueness_error: The Hetzner API returns HTTP 409
   with "SSH key not unique" when the same key content is registered under
   a different name. The hetznerApi() function throws on non-2xx before
   the error-parsing code runs, and the regex /already/ didn't match
   "not unique". Fix: catch 409 in ensureSshKey() and match against
   uniqueness_error/not unique/already patterns.

3. Hermes binary not found: The hermes install script (uv tool) creates
   the actual binary + venv at ~/.hermes/hermes-agent/venv/ with a symlink
   at ~/.local/bin/hermes. The tarball capture script only captured the
   symlink + ~/.local/share/, leaving a dangling symlink. Fix: include
   ~/.hermes/ in capture paths, add venv/bin to verify.sh PATH check,
   and update hermes launchCmd to include the venv PATH.

Fixes #2304

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 22:05:44 -05:00
A
099ad8940e
feat(e2e): send agent x cloud matrix email on completion (#2297)
After every e2e run, send an HTML matrix report to KEY_REQUEST_EMAIL
via Resend showing pass/fail/skip per agent x cloud combination.

- e2e.sh: add send_matrix_email() — builds result table from LOG_DIR
  result files, writes temp TS, calls bun run to POST to Resend API.
  Called just before exit so LOG_DIR is still available.
- qa.sh (e2e mode): load RESEND_API_KEY + KEY_REQUEST_EMAIL from
  /etc/spawn-key-server-auth.env before launching Claude so the creds
  are inherited by the e2e.sh subprocess.

Both changes are no-ops when credentials are absent (silent skip).

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 14:07:55 -08:00
A
ce06492cb7
fix: use exact-line match for INPUT_TEST_MARKER in E2E verify functions (#2293)
Fixes #2292

Unanchored grep -q would match the marker anywhere in output, including
error messages like "Expected SPAWN_E2E_OK but got...". Using grep -qx
requires the marker to appear as a complete line, preventing false passes.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 14:40:06 -05:00
A
52addf16e5
fix: remove BASH_SOURCE usage from all cloud agent scripts (Fixes #2285) (#2289)
All 42 agent scripts across 6 clouds used BASH_SOURCE[0] with dirname
for local checkout detection. This breaks curl|bash execution because
BASH_SOURCE resolves to /dev/fd/XX instead of a real path.

Remove the BASH_SOURCE-based SCRIPT_DIR detection and the "Local checkout"
code path from all scripts. The SPAWN_CLI_DIR env var (used by e2e tests)
is the correct mechanism for running from source. Local cloud scripts
that previously lacked SPAWN_CLI_DIR support now have it.

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 14:12:10 -05:00
A
1740274323
fix: replace base64 interpolation with stdin piping in all cloud exec_long functions (#2290)
Replace unsafe pattern where base64-encoded commands were interpolated
into remote command strings with secure stdin piping — command data now
travels as stdin rather than as part of the command string, eliminating
injection risk from shell metacharacter interpretation.

Affected functions across all 5 cloud drivers:
- _hetzner_exec_long
- _aws_exec_long
- _gcp_exec_long
- _digitalocean_exec_long
- _sprite_exec_long

Fixes #2286
Fixes #2287

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 14:09:15 -05:00
A
735e80e376
fix: replace base64 interpolation with stdin piping in verify.sh (Fixes #2283) (#2284)
* fix: replace base64 interpolation with stdin piping in verify.sh (Fixes #2283)

Replace unsafe pattern where encoded prompt was interpolated into remote
command strings with secure stdin piping — prompt data now travels as stdin
rather than as part of the command string, eliminating injection risk.

Affected functions: input_test_claude, input_test_codex, input_test_openclaw,
input_test_zeroclaw.

Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: use cloud_exec (not cloud_exec_long) for stdin piping

cloud_exec_long ignores stdin - remote base64 -d would hang.
cloud_exec passes cmd to bash -c, which preserves stdin piping.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: restore timeout protection for input tests using cloud_exec

Wraps each agent command in `timeout ${INPUT_TEST_TIMEOUT}` on the remote
side so tests cannot hang indefinitely after switching from cloud_exec_long
to cloud_exec.  Updates stale comment referencing cloud_exec_long.

Agent: pr-maintainer
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.5 <noreply@anthropic.com>
2026-03-07 12:41:50 -05:00
A
3a1de9d4cf
refactor: remove packages/shared, deduplicate with CLI shared (#2257)
* refactor: remove packages/shared, deduplicate with packages/cli/src/shared

packages/shared duplicated packages/cli/src/shared (parse.ts, result.ts,
type-guards.ts) with the CLI never importing from the shared package.
The only consumer was .claude/skills/setup-spa, which now imports directly
from packages/cli/src/shared via relative paths.

- Delete packages/shared entirely
- Update setup-spa imports to use relative paths to CLI shared
- Remove @openrouter/spawn-shared workspace dependency from setup-spa
- Update CLAUDE.md and type-safety.md references

Agent: complexity-hunter
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove packages/shared from lint workflow, fix import sorting

The Biome Lint CI step referenced packages/shared/src/ which no longer
exists after this PR removes the package. Also fix import ordering in
setup-spa files to satisfy Biome's organizeImports rule.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address Devin review — update stale packages/shared references

- Update type-safety.md line 67: packages/shared/src/parse.ts → packages/cli/src/shared/parse.ts
- Update install.ps1 sparse-checkout: remove packages/shared reference

Agent: pr-maintainer
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.5 <noreply@anthropic.com>
2026-03-06 21:58:42 -05:00
A
035e4bf830
Remove Daytona cloud provider from codebase (#2261)
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>
2026-03-06 18:53:08 -05:00
Ahmed Abushagur
4ac19a375a
fix: capture claude symlink target + verify PATH (#2245)
* 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>
2026-03-06 10:55:09 -05:00
A
ee9ae46221
fix: reject control characters in GITHUB_TOKEN validation (#2241)
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>
2026-03-06 03:11:23 -08:00
A
8bc45b4283
refactor: Remove dead code and stale references (#2238)
- 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>
2026-03-06 03:49:47 -05:00
A
df0593fb21
feat(e2e): Run E2E tests on all configured clouds, not just AWS (#2236)
- 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>
2026-03-06 00:06:36 -08:00
A
9291dd9c76
fix: Exclude Daytona from key-request emails (#2237)
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>
2026-03-06 00:04:48 -08:00
Ahmed Abushagur
c71f01725b
test: unit tests for openclaw gateway resilience config (#2224)
* 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>
2026-03-05 19:38:10 -05:00
Ahmed Abushagur
08cf5e6d8a
fix(e2e): DigitalOcean name mismatch and bash 3.2 compat (#2218)
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>
2026-03-05 13:44:32 -05:00
Ahmed Abushagur
89e5e980c0
fix(docker): add xz-utils to Hermes Dockerfile for Node.js extraction (#2217)
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>
2026-03-05 13:41:53 -05:00
Ahmed Abushagur
77c3e34803
feat(docker): replace Packer snapshots with Docker-based agent delivery (#2206)
* 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>
2026-03-05 11:23:56 -05:00
A
5dfb91b747
security: fix checksum grep anchoring and tar path traversal in github-auth.sh (#2213)
* security: fix checksum grep anchoring and tar path traversal in github-auth.sh

- Anchor grep with -F " ${tarball}" to prevent partial filename matches
  in checksum validation (e.g. foo.tar.gz matching foo.tar.gz.sig)
- Add pre-extraction validation rejecting tarballs with absolute paths
  or ../ traversal components (CWE-22), cross-platform (GNU + BSD tar)

Fixes #2211
Fixes #2212

Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: anchor checksum grep with two-space prefix and EOL to prevent partial match

Agent: pr-maintainer
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.5 <noreply@anthropic.com>
2026-03-05 05:41:48 -08:00
A
02931cfa32
security: verify gh binary checksum and safe JSON parsing in github-auth.sh (#2210)
Fixes #2209

- Replace sed-based JSON parsing with jq/bun-eval for safe tag_name extraction
- Add SHA256 checksum verification before extracting gh binary tarball
- Add semver format validation for parsed version strings

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-05 05:49:23 -05:00
A
48da6d8735
refactor: Remove dead code and stale references (#2189)
- 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>
2026-03-04 11:56:37 -05:00
A
cb91b5d236
refactor: fix stale comments referencing renamed functions (#2182)
- 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>
2026-03-04 01:35:18 -08:00
L
61bcedc0eb
feat: migrate to openrouter.ai/labs/spawn CDN + release artifact version checks (#2178)
* 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>
2026-03-03 23:34:58 -08:00
A
1097f055c3
fix(security): add --proto '=https' to all curl executable downloads (#2160)
42 curl calls downloading JS bundles, CLI binaries, and gh CLI tarballs
were missing --proto '=https', allowing protocol downgrade attacks on
hostile networks. PR #2138 fixed bun installer calls; this closes the
remaining gap for executable downloads.

Fixes applied:
- sh/{sprite,aws,gcp,hetzner,daytona,local}/{claude,codex,openclaw,opencode,kilocode,hermes,zeroclaw}.sh (42 files)
- sh/cli/install.sh (cli.js download)
- sh/shared/github-auth.sh (keyring, API, tarball downloads)

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 23:38:03 -05:00
A
251ddf2967
fix(e2e): pass env_b64 via printf stdin to eliminate interpolation risk (#2159)
Fixes #2152

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 19:34:31 -08:00
A
6423eb51f5
fix(security): validate env values in cloud_headless_env parser (#2146)
* 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>
2026-03-03 20:49:45 +00:00
A
ebe8148177
fix(e2e): correct provision.sh fallback .spawnrc env vars for zeroclaw, hermes, kilocode (#2149)
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>
2026-03-03 15:46:57 -05:00
A
cfa1ae7a08
fix(security): add --proto '=https' to all curl bun installer calls (#2138)
* 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>
2026-03-03 11:52:54 -08:00
A
83d68c6a37
refactor: Remove dead code and stale references (#2137)
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>
2026-03-03 11:51:59 -05:00