Commit graph

2250 commits

Author SHA1 Message Date
A
da07fd4031
fix(security): prevent command injection in sprite uploadFile (#2889)
Replace shell string interpolation with array-based exec arguments in
uploadFileSprite. Previously, remotePath and tempRemote were interpolated
into a bash -c string (`mkdir -p $(dirname '${normalizedRemote}') && mv
'${tempRemote}' '${normalizedRemote}'`), which is inherently unsafe
even with regex validation.

Now uses two separate sprite exec calls with paths passed as discrete
array arguments after `--`, and computes dirname in TypeScript using
node:path/posix instead of shell command substitution. Also fixes the
mockBunSpawn test helper to return fresh ReadableStream instances per
call, preventing "ReadableStream already used" errors.

Fixes #2880

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-22 18:51:51 -07:00
A
0224b56a4d
fix(digitalocean): detect droplet limit before creation, clear error on 422 (#2891)
checkAccountStatus() now queries the account's droplet_limit and
current droplet count. When at capacity it warns interactively and
throws immediately in headless/E2E mode with a clear message instead
of attempting creation and getting a cryptic 422.

Also adds specific detection of droplet limit 422 errors in
createServer() with actionable guidance (limit increase URL).

Bump CLI to 0.25.14.

Fixes #2865

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-22 18:49:17 -07:00
A
83cd6bc6df
test: remove duplicate generateCodeVerifier/generateCodeChallenge tests from oauth-cov (#2885)
These two describe blocks in oauth-cov.test.ts were redundant subsets of the more
comprehensive coverage already in oauth-pkce.test.ts (which includes RFC 7636 test
vectors, uniqueness checks, padding validation, and base64url character checks).

Duplicates found: 1 function pair (generateCodeVerifier + generateCodeChallenge)
Tests removed: 2
Tests rewritten: 0

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:43:14 +07:00
A
d046a9bfdf
fix: tighten character whitelist for cloud_headless_env values (#2890)
The env value whitelist allowed @, %, +, =, :, and , characters that
are unnecessary for cloud resource names (server names, regions, sizes)
and could be used as shell metacharacters in certain contexts. Restrict
to only [A-Za-z0-9._/-] which matches all legitimate cloud resource
identifiers.

Fixes #2883

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-23 08:41:50 +07:00
A
fa79d34a47
fix(security): properly quote remote cmd construction in verify.sh (#2887)
Prevent shell metacharacter interpretation in test prompt handling
by staging INPUT_TEST_TIMEOUT and attempt number to remote temp files
instead of interpolating them into remote command strings.

Previously, _TIMEOUT='${INPUT_TEST_TIMEOUT}' and --session-id
e2e-test-${attempt} were interpolated directly into double-quoted
remote command strings. While _validate_timeout enforces digits-only,
the structural pattern of local-to-remote variable interpolation is
inherently risky. Now all dynamic values (prompt, timeout, attempt)
are piped to remote temp files via stdin and read back on the remote
side, eliminating the injection surface entirely.

Fixes #2884

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:39:36 +07:00
A
054a740e5a
refactor: remove stale Packer comment in hetzner.ts (#2878)
The reference to "Hetzner Packer" was removed in #2869.
Updated the comment to accurately describe the snapshot naming convention.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-23 04:14:00 +07:00
A
76afe9546b
test: add missing assertions to no-op smoke tests (#2879)
19 tests across 7 files were calling functions with no expect() calls —
they verified "does not throw" implicitly but provided zero signal on
side effects or return values.

Added assertions to each:
- agent-setup-cov: expect runServer called after graceful failure
- auto-update: expect runServer called on non-fatal SSH error
- aws-cov: assert state.awsRegion set by promptRegion env var paths,
  spawnSync call counts for ensureAwsCli, fetch called for destroyServer
- do-cov: assert SPAWN_NAME_KEBAB preserved on early return,
  fetch NOT called when no token in checkAccountStatus
- gcp-cov: assert spy call counts for authenticate, destroyInstance,
  ensureGcloudCli; spawnSync NOT called when GCP_PROJECT env set;
  fetch NOT called when no project in checkBillingEnabled
- hetzner-cov: assert fetch called for ensureHcloudToken validation
  and for destroyServer REST calls
- ssh-cov: assert connectSpy and bunSpawnSpy called in waitForSsh

All 1925 tests pass. expect() calls increased from 4555 to 4575.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 04:12:18 +07:00
A
db6c44be9c
fix(e2e): update input tests for new agent CLIs + auto-load email creds (#2877)
* fix(e2e): update input tests for latest agent CLI interfaces + auto-load email creds

claude: add --dangerously-skip-permissions --no-session-persistence to bypass
trust dialog when running in /tmp/e2e-test (not in ~/.claude.json trusted
projects list written during install)

codex: replace `codex exec --full-auto` (removed in new @openai/codex) with
`codex -q -a full-auto` — quiet mode + full-auto approval, no exec subcommand

email: auto-load RESEND_API_KEY + KEY_REQUEST_EMAIL from
/etc/spawn-key-server-auth.env (QA VM) or ~/.config/spawn/resend.env (local)
so send_matrix_email fires on every e2e run, not just QA-cycle runs

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

* fix(e2e): correct claude and codex input test commands

- claude: pass prompt as positional arg to claude -p instead of piping
  via stdin (stdin pipe breaks through SSH exec chain, causing
  "Input must be provided either through stdin or as a prompt argument"
  error)
- codex: revert to `codex exec --full-auto` subcommand (correct for
  v0.116.0 — previous -q -a full-auto flags don't exist)

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>
2026-03-23 03:08:37 +07:00
Ahmed Abushagur
48163ea2ee
feat(e2e): AI-powered log review catches non-fatal issues (#2875)
* feat(e2e): add AI-powered log review after provisioning

Feeds provision stderr/stdout logs to an LLM after each agent deploys.
Catches non-fatal issues that binary pass/fail checks miss: silent 404s,
failed component installs, connection instability, swallowed warnings.

This would have caught the keep-alive 404 and the sprite idle shutdown
that the existing E2E tests missed because installSpriteKeepAlive() is
non-fatal and the binary checks only verify final state.

- Uses gemini-flash-lite-2.0 via OpenRouter (cheap, fast)
- Advisory only — never fails the test, reports findings as warnings
- Truncates logs to last 200 lines to stay within token limits
- Skips gracefully if OPENROUTER_API_KEY is missing or API fails

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

* feat(e2e): add AI log review and --fast mode testing

AI log review:
- After each agent provisions, feeds stderr/stdout to gemini-flash-lite
  to catch non-fatal issues binary checks miss (404s, failed installs,
  connection drops, swallowed warnings)
- Advisory only — never fails the test, surfaces findings as warnings
- Would have caught the keep-alive 404 and sprite idle shutdown

--fast mode E2E:
- Add --fast flag to e2e.sh, passed through to spawn CLI during provision
- Update QA e2e-tester protocol to run both normal and --fast passes
- --fast enables images + tarballs + parallel boot

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-23 02:15:09 +07:00
Ahmed Abushagur
baf03ce47b
fix: prevent sprite idle shutdown during agent install (#2874)
The sprite was going idle and shutting down during long npm install
operations because the remote keep-alive script wasn't installed yet
and sprite exec alone doesn't count as activity.

- Add local keep-alive that pings the sprite's public URL every 30s
  from the client machine during provisioning and agent install
- Stop it when the interactive session starts (remote script takes over)
- Add i/o timeout to spriteRetry's transient error regex so connection
  timeouts are retried instead of failing immediately

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 02:13:07 +07:00
Ahmed Abushagur
66a1749b4b
fix: add sprite-keep-running.sh, remove Hetzner from Packer, cleanup on cancel (#2869)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
* fix: destroy orphaned Packer builder instances on workflow cancel

When a Packer Snapshots workflow is cancelled mid-build, Packer's process
is killed before it can clean up its temporary builder droplet/server.
This leaves orphaned packer-* instances running and costing money.

Add `if: cancelled()` cleanup steps for both DigitalOcean and Hetzner
that destroy any packer-* prefixed instances after cancellation.

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

* chore: remove Hetzner cleanup step — only DO needed

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

* fix: remove Hetzner from Packer snapshots, add cancel cleanup

Remove Hetzner from the Packer workflow entirely — only DigitalOcean
snapshots are built. Deletes packer/hetzner.pkr.hcl and simplifies the
workflow by removing all Hetzner-specific steps and cloud conditionals.

Also adds a cancelled() cleanup step that destroys orphaned packer-*
builder droplets when a workflow run is cancelled mid-build.

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

* fix: add missing sprite-keep-running.sh script

The keep-alive install was 404ing because sh/shared/sprite-keep-running.sh
never existed in the repo. The TypeScript code downloaded it from the CDN
(which maps to sh/shared/) but the file was never created.

The script wraps a command and pings the sprite's own public URL every 30s
to prevent inactivity shutdown. It resolves the URL via sprite-env info
(available on all sprites) and falls back to exec without keep-alive if
the URL can't be determined.

Also removes Hetzner from the Packer snapshots workflow entirely — only
DigitalOcean snapshots are built.

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

* fix: address security review — scope cleanup filter, fix JSON injection

1. Add `spawn-packer` tag to DO builder droplets in Packer template and
   filter cleanup by tag instead of broad `packer-` name prefix. Prevents
   accidentally destroying builder instances from other concurrent builds.

2. Use `jq --arg` for SINGLE_AGENT_INPUT instead of string interpolation
   to prevent JSON injection via crafted agent names.

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-22 18:13:38 +00:00
A
87f49eba48
test: remove duplicate and theatrical tests (#2873)
Remove 7 redundant tests that test the same code paths as existing tests:

- history.test.ts: consolidate 4 separate "unrecognized JSON value" tests
  (non-array object, JSON string, null, number) into one data-driven test.
  All 4 hit the identical parseHistoryData "Unrecognized format" branch.

- cmd-link-cov.test.ts: remove "exits with error when no IP provided" —
  duplicate of the same test in cmd-link.test.ts with identical behavior.

- update-check-cov.test.ts: remove "skips in test environment" and "skips
  when SPAWN_NO_UPDATE_CHECK=1" — both already covered in update-check.test.ts.

- orchestrate-cov.test.ts: remove "calls preLaunch when defined" — identical
  to the same test in orchestrate.test.ts (same mock setup, same assertion).

All 1866 remaining tests pass. Lint clean.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:22:47 +07:00
A
c25594cf09
test: Remove duplicate killWithTimeout tests (#2870)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
* test: remove duplicate and theatrical tests

- cmd-fix-cov.test.ts: remove 6 duplicate fixSpawn tests already covered
  in cmd-fix.test.ts; keep only the unique success message assertion
- icon-integrity.test.ts: consolidate 54 per-entity it() blocks into 4
  data-driven tests (same 67 expect() calls, 50 fewer test cases)
- manifest-type-contracts.test.ts: consolidate per-field for-loop it()
  blocks into 3 grouped tests (same 662 expect() calls, 15 fewer cases)

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

* test: remove duplicate killWithTimeout tests from ssh-cov.test.ts

The `killWithTimeout additional` describe block in ssh-cov.test.ts
duplicated scenarios already covered in kill-with-timeout.test.ts:
- "sends SIGTERM then SIGKILL" == kill-with-timeout's SIGKILL grace test
- "does nothing when first kill throws" == kill-with-timeout's SIGTERM throw test

Removed the 2 duplicate tests from ssh-cov.test.ts. The dedicated
kill-with-timeout.test.ts file is the canonical location for
killWithTimeout coverage.

---------

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-22 16:47:59 +07:00
A
57e06bab4a
fix(e2e): fix manual .spawnrc creation on Sprite (stdin piping broken) (#2872)
The manual .spawnrc fallback in provision.sh was using `printf '%s' "${env_b64}" | cloud_exec ...`,
which works for SSH-based clouds (Hetzner, GCP, AWS) where stdin is passed through the SSH
connection. However, Sprite's exec driver replaces stdin with the command pipe:
  `printf '%s' "${cmd}" | sprite exec -s NAME -- bash`
This causes the outer env_b64 pipe to be lost — `base64 -d` receives no input and writes an
empty .spawnrc, which then fails the OPENROUTER_API_KEY and openrouter.ai verification checks.

Fix: embed the base64 data directly in the command string using `printf '%s' '${env_b64}'`.
This is safe because env_b64 is validated to contain only [A-Za-z0-9+/=] — the standard
base64 alphabet — which cannot break out of single quotes or cause shell injection.

Confirmed by E2E run where sprite/claude and sprite/openclaw both failed with:
  [FAIL] OPENROUTER_API_KEY not found in .spawnrc
  [FAIL] Failed to create manual .spawnrc

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 16:46:05 +07:00
A
cc8b6601ec
refactor: remove stale references and add missing entries to test README (#2871)
- remove stale reference to `commands-update-download.test.ts` (renamed to `cmd-update-cov.test.ts`)
- remove stale reference to `picker.test.ts` (renamed to `picker-cov.test.ts`)
- add 25 missing `-cov.test.ts` files that exist on disk but were undocumented

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-22 15:47:58 +07:00
A
7e56e1839b
test: remove duplicate and theatrical tests (#2868)
- cmd-fix-cov.test.ts: remove 6 duplicate fixSpawn tests already covered
  in cmd-fix.test.ts; keep only the unique success message assertion
- icon-integrity.test.ts: consolidate 54 per-entity it() blocks into 4
  data-driven tests (same 67 expect() calls, 50 fewer test cases)
- manifest-type-contracts.test.ts: consolidate per-field for-loop it()
  blocks into 3 grouped tests (same 662 expect() calls, 15 fewer cases)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 12:06:55 +07:00
A
c1363b138c
feat(gcp): default boot disk to 40 GB, configurable via GCP_DISK_SIZE (#2867)
GCP's default 10 GB boot disk is insufficient for coding agents — node_modules,
apt packages, and build caches easily exceed it. Default to 40 GB and allow
override via GCP_DISK_SIZE env var.

Closes #2866

Co-authored-by: Claude <claude@anthropic.com>
2026-03-22 11:21:05 +07:00
A
92f2de4036
test: remove theatrical tests — replace no-op assertions with real signal (#2863)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
preflight-credentials.test.ts: all 7 tests had zero expect() calls with
comments like "// No crash = pass". Rewrote to capture logWarn mock calls
from mockClackPrompts() and assert on warning presence and credential names.

sprite-cov.test.ts: 13 out of 23 tests had no expect/rejects calls (just
called functions and discarded results). Added assertions on Bun.spawn call
counts to verify: authenticated paths skip login, unauthenticated paths
trigger login, createSprite reuses vs creates based on list output,
verifySpriteConnectivity calls sprite twice, setupShellEnvironment runs
multiple exec commands.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 08:38:39 +07:00
A
300e2fc221
fix(security): shellQuote cmd in runServer() across all cloud providers (#2862)
Defense-in-depth: explicitly shellQuote(cmd) inside runServer() so the
cmd parameter is always protected by single-quote escaping, regardless
of how the surrounding command string is constructed.

Previously, cmd was interpolated raw into fullCmd before the outer
shellQuote() wrapper. While the outer wrapper did protect it, this
made the safety non-obvious and fragile against future refactors.
The new pattern matches interactiveSession() where cmd gets its own
shellQuote() call.

Fixes #2859

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-21 14:48:37 -07:00
A
3f12cb9ee8
refactor: remove duplicate docker constants into shared orchestrate module (#2860)
Consolidate DOCKER_CONTAINER_NAME and DOCKER_REGISTRY constants from
gcp/main.ts and hetzner/main.ts into shared/orchestrate.ts. Both files
defined identical values ("spawn-agent" and "ghcr.io/openrouterteam"); they
now import the shared exports instead.

Bumps CLI patch version to 0.25.11.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-21 14:27:21 -07:00
A
d480a7fec4
test: remove duplicate and theatrical tests (#2861)
- manifest.test.ts: remove 4 duplicate loadManifest error/fallback tests
  (HTTP 500 stale-cache, no-cache-HTTP500-throws, invalid-manifest-throws,
  network-error-throws) — all covered more thoroughly by
  manifest-cache-lifecycle.test.ts

- ssh-keys.test.ts: remove 2-key sorting test superseded by ssh-keys-cov.test.ts
  which validates the full 3-way sort order (ED25519 > RSA > ECDSA)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 03:43:47 +07:00
A
7ab6c693d3
fix: add --beta docker to help output and update description (#2857)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
The --beta docker feature (PR #2854) was missing from `spawn help`
output, and its error description said "Hetzner" only but it also
works on GCP.

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-21 06:20:35 -07:00
A
2f329684e0
test: remove duplicate and theatrical tests (#2858)
- aws-cov.test.ts: remove aws/BUNDLES (3 tests) and aws/credential-persistence
  (6 tests) — all scenarios already covered by aws.test.ts with stronger
  assertions (>= 5 tiers vs >= 3, pricing format, naming convention, etc.)

- cmd-run-cov.test.ts: remove "cmdRun dry run" and "cmdRun validation" (3 tests)
  — dry-run is covered more thoroughly in cmdrun-happy-path.test.ts;
  validation tests duplicate commands-error-paths.test.ts exactly

- agent-setup-cov.test.ts: remove "agents return non-empty launch commands"
  (weaker duplicate of "all agents have launchCmd") and "agents have configure
  functions" (no expect() calls — theatrical)

Total: 5 tests removed, 162 lines deleted, 0 regressions (1951 pass)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 19:49:27 +07:00
Ahmed Abushagur
6d2c4746f5
feat: add --beta docker for Hetzner Docker CE app image (#2854)
* feat: add --beta docker for Hetzner Docker CE app image

Uses Hetzner's pre-built docker-ce app image when --beta docker
(or --fast) is active, giving faster boot times similar to DO
marketplace images. Snapshots still take priority when available.

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

* feat: pull and run pre-built agent Docker images on Hetzner

When --beta docker (or --fast) is active, boots Hetzner with docker-ce
app image, then pulls ghcr.io/openrouterteam/spawn-{agent}:latest and
runs it. All runServer commands are routed through docker exec into
the container, and the interactive session uses docker exec -it.
Skips agent install since the agent is pre-baked in the image.

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

* feat: add --beta docker support for GCP with Container-Optimized OS

When --beta docker (or --fast) is active on GCP, uses cos-stable
from cos-cloud (Docker pre-installed, read-only OS). Skips cloud-init
startup script (incompatible with COS), pulls the pre-built agent
image from ghcr.io, and routes all commands through docker exec.

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

* fix: correct import path for logInfo/logStep (shared/log.js -> shared/ui.js)

The log.js module does not exist; these functions are exported from ui.ts.
Also merge duplicate ui.js imports per biome organizeImports.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-21 17:10:19 +07:00
A
bfe9fb9808
test: remove duplicate and theatrical tests (#2856)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
- Replace 10x `expect(true).toBe(true)` in update-check-cov.test.ts with
  meaningful assertions: skip-condition tests now verify fetch was NOT called,
  fetch-failure tests use `resolves.toBeUndefined()`, backoff edge-case tests
  verify fetch WAS called (proving the skip was bypassed)
- Remove theatrical executor existence check (`typeof executor.execFileSync === "function"`)
  that proved nothing about behavior
- Replace structural `typeof agent.install/envVars/launchCmd === "function"` checks in
  agent-setup-cov.test.ts with assertion that agent names are non-empty strings;
  the downstream tests already prove the functions work by calling them

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-21 15:48:44 +07:00
Ahmed Abushagur
8c7a381375
fix: auto-reconnect on Sprite connection drops (#2855)
Sprite CLI exits with code 1 on "connection closed" (not 255 like SSH).
The reconnect loop now treats exit code 1 on Sprite as a connection
drop, retrying up to 5 times with a 3s delay between attempts.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:13:14 +07:00
A
a3e0dbd4dd
test: remove duplicate and theatrical tests (#2853)
- Remove `digitalocean/findSpawnSnapshot` describe from do-cov.test.ts
  (3 basic tests) — fully superseded by do-snapshot.test.ts (7 thorough
  tests covering name filtering, invalid IDs, network failure, etc.)

- Remove `setupAutoUpdate` describe from agent-setup-cov.test.ts
  (2 shallow tests checking only "systemd" string presence) — fully
  superseded by auto-update.test.ts which verifies exact systemd unit
  content, base64-encoded scripts, timer schedules, and error handling

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 12:24:00 +07:00
Ahmed Abushagur
26332afa56
fix: prevent silent exit in --fast mode on Sprite (#2852)
In fast mode, Promise.allSettled runs server boot, OAuth, and tarball
download concurrently. When all operations complete — especially after
Bun.serve.stop(true) in the OAuth flow removes its event loop handle —
the event loop can appear empty before the await continuation starts
new I/O operations. This causes Bun to exit silently with code 0,
dropping the user back to their shell after "Successfully obtained
OpenRouter API key via OAuth!" with no error.

Fix: keep a dummy setInterval handle alive during the fast-mode
concurrent section so the event loop never drains prematurely.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:51:02 -07:00
A
9a98589cef
fix(security): prevent command injection via INPUT_TEST_TIMEOUT in verify.sh (#2851)
Add defense-in-depth validation of INPUT_TEST_TIMEOUT directly in verify.sh
(not just relying on common.sh). Each input test function now calls
_validate_timeout() to ensure the value contains only digits before use.

Additionally, instead of interpolating INPUT_TEST_TIMEOUT directly into
remote command strings passed to cloud_exec, the timeout value is now
assigned to a single-quoted remote variable (_TIMEOUT) and referenced via
"$_TIMEOUT" on the remote side. This eliminates the injection surface even
if validation were somehow bypassed.

Affected functions: input_test_claude(), input_test_codex(),
input_test_openclaw(), input_test_zeroclaw().

Fixes #2849

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-20 19:58:52 -07:00
A
acfc31027b
test: delete theatrical unicode-cov.test.ts (#2848)
Fixes #2847

Removes 273 lines of false-confidence tests that copy-paste
shouldForceAscii() logic inline 9x with zero imports from
unicode-detect.ts. Every test passed even if the real source
was deleted — a theatrical test is worse than no test.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 19:29:14 -07:00
A
84e78a0274
fix(test): prevent flaky timeout in checkBillingEnabled test (#2845)
The test assumed _state.project would be empty, but module-level state
persists across tests due to import caching. Prior resolveProject tests
set _state.project, so checkBillingEnabled would attempt a real
gcloudSync call and time out at 5s. Mock spawnSync to handle both cases.

Agent: pr-maintainer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-20 18:38:48 -07:00
A
a7690f8400
test: remove duplicate and theatrical tests (#2846)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
- history-cov.test.ts: remove duplicate filterHistory ordering test and
  no-cap saveSpawnRecord test — both are already covered more thoroughly
  in history-trimming.test.ts

- unicode-cov.test.ts: remove theatrical pattern where each test
  re-implemented shouldForceAscii as an inline lambda (testing an inline
  copy instead of the real function). consolidate into a single shared
  helper that mirrors the actual module logic, tested once per scenario.

-- qa/dedup-scanner

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-20 17:57:40 -07:00
A
858e348a24
fix(security): add HOME validation before rm -rf in cleanup (#2842)
Add safe_cleanup_test_dirs() helper to qa.sh and security.sh that
validates HOME is set, exists, and is not "/" before running
find + rm -rf for test directory cleanup. Prevents unintended
deletions if HOME is unset or maliciously set.

Fixes #2838

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-20 17:36:53 -07:00
A
62e5918078
fix(security): wrap runServer SSH commands with shellQuote in DO and Hetzner (#2843)
DigitalOcean and Hetzner runServer() passed the command string directly
to SSH without shell-quoting, allowing metacharacters (;, |, $(), etc.)
to be interpreted by the remote shell. AWS and GCP already used
`bash -c ${shellQuote(fullCmd)}` — this applies the same pattern to the
two affected modules.

Fixes #2836

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-20 17:34:43 -07:00
A
ffb4cbeb11
fix(security): prevent path traversal in uploadFile/downloadFile across all cloud providers (#2844)
Check for ".." path traversal in the raw input BEFORE normalize() strips
it, fixing CWE-22 where crafted paths like "/tmp/../../etc/passwd"
normalized to "/etc/passwd" and bypassed the post-normalize ".." check.

Extracts a shared validateRemotePath() into shared/ssh.ts and replaces
the duplicated inline validation in all 5 providers (DigitalOcean,
Hetzner, GCP, AWS, Sprite) plus agent-setup.ts.

Fixes #2835

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-20 16:48:58 -07:00
A
b9e326d649
fix: use base64 encoding for GITHUB_TOKEN to prevent injection (#2840)
* fix: use base64 encoding for GITHUB_TOKEN to prevent injection

Aligns GITHUB_TOKEN handling with the existing base64 pattern used for
OPENROUTER_API_KEY in orchestrate.ts, eliminating the single-quote
escaping vulnerability.

Fixes #2834

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

* fix: apply shellQuote to base64-encoded GITHUB_TOKEN

Address security review feedback: wrap the base64-encoded token in
shellQuote() for defense-in-depth, preventing any theoretical shell
metacharacter escape from the interpolated value.

Co-Authored-By: Claude Opus 4.6 <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-20 16:46:49 -07:00
A
0c13679dde
fix(security): quote branch name variables to prevent word-splitting (#2841)
Replace `for branch in $VAR` with `while IFS= read -r branch` loops
in qa.sh and security.sh to prevent word-splitting on branch names
containing spaces or special characters. This closes a MEDIUM severity
vulnerability where a malicious branch name like `qa/test main` could
cause the loop to iterate over split tokens separately.

Fixes #2837

Agent: style-reviewer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-20 23:44:22 +00:00
A
5acf598615
fix: use stdin piping in _stage_prompt_remotely to prevent injection (#2839)
Replaces command string interpolation with stdin piping for the base64
prompt in verify.sh. Also anchors the _validate_base64 regex.

Fixes #2833

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-20 15:46:00 -07:00
A
3551995aa1
refactor: remove dead code and stale references (#2832)
Deduplicate identical mockBunSpawn helper that was copy-pasted across
five test files (aws-cov, gcp-cov, do-cov, hetzner-cov, sprite-cov).
Centralise it in test-helpers.ts and import from there instead.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 14:12:20 -07:00
A
5e263dd12f
test: remove duplicate and theatrical tests (#2831)
Remove 10 duplicate test cases from cmd-list-cov.test.ts and
cmd-run-cov.test.ts that were already covered by dedicated test files:

- buildRecordLabel (3 tests) — duplicated from cmdlast.test.ts
- buildRecordSubtitle (3 tests) — duplicated from cmdlast.test.ts
- cmdListClear (2 tests) — weaker duplicates of clear-history.test.ts
- cmdLast (1 test) — duplicated from cmdlast.test.ts
- cmdRun detectAndFixSwappedArgs (1 test) — duplicated from
  commands-swap-resolve.test.ts which has 10 thorough swap tests

-- qa/dedup-scanner

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-20 13:47:17 -07:00
A
32525f5dd7
test: remove duplicate and theatrical tests (#2830)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
- delete manifest-cov.test.ts: it duplicated stripDangerousKeys,
  agentKeys/cloudKeys/matrixStatus/countImplemented from manifest.test.ts;
  unique tests (isStaleCache, getCacheAge, richer loadManifest edge cases)
  consolidated into manifest.test.ts
- remove sprite/interactiveSession from sprite-cov.test.ts: superseded by
  sprite-keep-alive.test.ts which tests actual script content
- remove sprite/installSpriteKeepAlive from sprite-cov.test.ts: superseded
  by sprite-keep-alive.test.ts
- remove startGateway from agent-setup-cov.test.ts: superseded by
  gateway-resilience.test.ts which checks systemd config, cron, and port-wait

all 2050 tests pass

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-20 09:51:57 -07:00
A
8be8b650b0
docs: sync README commands table with help.ts (add spawn link) (#2829)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-20 09:49:38 -07:00
A
1dc9c04eeb
fix: standardize ESM import extensions across 35 production files (#2827)
Add .js extensions to 124 relative imports that were missing them.
The codebase is "type": "module" (ESM) and the dominant pattern already
used .js extensions, but 35 files had a mix of extensionless and .js
imports — sometimes within the same file. Standardize to .js everywhere.

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-20 08:51:40 -07:00
A
f4e2cd80a4
fix(ux): add spawn link to help output and --fast to KNOWN_FLAGS (#2828)
spawn link is a fully implemented command (440 lines) that was
completely missing from `spawn help`. Users had no way to discover
it through the CLI's self-documentation.

Also adds --fast to the KNOWN_FLAGS set for consistency — it was
accepted by the CLI but not registered in the flag validation set.

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-20 08:49:26 -07:00
Ahmed Abushagur
21c0e1511c
fix: remove 100-entry history cap — keep all records (#2819)
The MAX_HISTORY_ENTRIES=100 cap silently archived records when you
spawned more than 100 times, making older active servers vanish from
`spawn list`. The cap was solving a non-problem — 1000 records is ~500KB.

Removed:
- MAX_HISTORY_ENTRIES constant and trimming logic
- archiveRecords() and readExistingArchive() (no longer needed)
- Smart trim tests (history-trimming.test.ts rewritten to test ordering only)

Existing archive files (~/.spawn/history-YYYY-MM-DD.json) are still
readable by recoverFromArchives() for corruption recovery.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 06:32:08 -07:00
A
a7cebd4054
test: remove duplicate and theatrical tests (#2826)
- delete commands-update-download.test.ts (7 tests): superseded by
  cmd-update-cov.test.ts which has 13 tests with better fallback URL
  coverage and uses clack mocks properly

- remove saveSpawnRecord id generation describe from history-cov.test.ts
  (1 test): superseded by history-spawn-id.test.ts which has 3 more
  thorough tests covering the same scenario

- remove 4 describe blocks from cmd-run-cov.test.ts (18 tests):
  getSignalGuidance, getScriptFailureGuidance, getScriptFailureGuidance
  additional, and getSignalGuidance additional are all covered more
  thoroughly by the dedicated script-failure-guidance.test.ts; the
  "additional" blocks were theatrical (only checked joined.length > 0)

- delete picker.test.ts and merge its 8 parsePickerInput tests into
  picker-cov.test.ts to eliminate duplicate describe name collision

2063 -> 2036 tests (-27), 0 failures

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-20 06:11:57 -07:00
A
c323f10ae9
fix(gcp): add /usr/local/bin to PATH for kilocode binary detection (#2825)
Fixes #2823: npm installs kilocode to /usr/local/bin when running as
root on GCP, but the E2E binary verify step didn't include /usr/local/bin
in PATH, causing false "binary not found" failures.

The .spawnrc PATH (generated by generateEnvConfig) already includes
/usr/local/bin, but verify_kilocode used a hardcoded PATH that omitted
it. This aligns kilocode and codex verify checks with openclaw and junie
which already include /usr/local/bin.

Also fixes the same latent issue in verify_codex.

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-20 05:25:15 -07:00
A
8f24067336
test: remove duplicate and theatrical tests (#2820)
Remove thin duplicate test blocks that were redundant with more comprehensive
coverage elsewhere:

- ui-cov.test.ts: drop shellQuote (4 tests → gcp-shellquote.test.ts has 11),
  jsonEscape (1 test → ui-utils.test.ts has 4), toKebabCase (2 tests →
  ui-utils.test.ts has 5), sanitizeTermValue (2 tests → ui-utils.test.ts has
  6), withRetry (3 tests → with-retry-result.test.ts has 8)
- agent-setup-cov.test.ts: drop wrapSshCall (5 tests → with-retry-result.test.ts
  has 7 plus integration tests)
- run-path-credential-display.test.ts: drop isRetryableExitCode (2 tests →
  cmd-run-cov.test.ts has 5)
- history-cov.test.ts: drop generateSpawnId (2 tests → history-spawn-id.test.ts
  has 2 with UUID format check) and clearHistory (2 tests →
  clear-history.test.ts has extensive coverage)
- cmd-list-cov.test.ts: drop formatRelativeTime (9 tests →
  commands-exported-utils.test.ts has 10 with an extra boundary case)

All 2063 tests pass, biome lint clean.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-20 05:00:22 -07:00
A
9ddf8b67b0
fix(ux): remove -n short flag from spawn link --name to prevent silent conflict (#2822)
The top-level arg parser in index.ts:820 claims -n for --dry-run before
any subcommand sees it. Running `spawn link 1.2.3.4 -n my-server` silently
drops the intended name value — the user gets no error, the spawn is
registered without the name they specified.

Removing -n from link's --name extractFlag call eliminates the conflict.
The --name long form is unaffected and documented in the usage string.

Also updates cmd-link-cov.test.ts to use --name in the short-flags test.

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-20 04:01:00 -07:00
A
24bdf664ab
fix(types): resolve TypeScript strict mode errors in production code (#2824)
Fix 24 TypeScript strict mode errors across 7 production files:

- interactive.ts: guard against undefined `val` in validate callback
- list.ts: use already-narrowed `conn` variable instead of `selected.connection`
- run.ts: widen `buildCloudLines` defaults param to `Record<string, unknown>`
- digitalocean.ts: use `toRecord()` to safely drill into nested API responses;
  capture narrowed `oauthCode` in const for async closure
- history.ts: backfill missing record IDs via `backfillRecordIds()` helper;
  use `v.safeParse` output directly to get properly typed records
- index.ts: use `Manifest` type for `showUnknownCommandError` parameter
- orchestrate.ts: capture narrowed `tunnel` and `getConnectionInfo` in const
  variables before async closures

Fixes #2821

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-20 03:17:04 -07:00