Commit graph

1767 commits

Author SHA1 Message Date
A
083c103ab9
fix(run): suppress duplicate failure output on SSH disconnect (exit 255) (#2186)
When SSH disconnects with exit code 255, the server is still running.
Previously the warn message ("SSH connection lost") was followed by the
full reportScriptFailure block, which was contradictory. Now we return
undefined after the warn so reportScriptFailure is skipped entirely.

Fixes #2185

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-04 07:16:49 -05:00
A
03cc7f4132
fix(interactive): honour --dry-run flag in single-agent interactive path (#2184)
When a user ran `spawn claude --dry-run`, the dry-run flag was silently
ignored and a real server was provisioned. `cmdAgentInteractive` was
passing `dryRun` in the `debug` parameter position of `execScript`, so
no preview was shown and `SPAWN_DEBUG=1` was set instead.

Fix:
- Export `showDryRunPreview` from `run.ts`
- Import and call it in `cmdAgentInteractive` after cloud selection
- Return early when `dryRun` is set (matches `cmdRun` behaviour)
- Pass `undefined` for the `debug` argument (interactive path has no
  debug flag)

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-04 06:14:50 -05:00
Ahmed Abushagur
c9ea6384da
fix(history): merge connection into history.json immediately at provision time (#2177)
Previously, saveVmConnection wrote to a single last-connection.json temp
file that was only merged into history.json lazily when spawn ls was run.
This caused connections to be silently dropped when:
- Two servers spawned before running spawn ls (file overwritten)
- The last history record already had a connection (merge skipped)

Now saveVmConnection writes directly into history.json by finding the
most recent record matching the cloud with no connection yet. The temp
file is still written for backward compatibility but is no longer the
primary storage.

Also fixes saveLaunchCmd to update history.json directly, and
consolidates sprite's local saveVmConnection to use the shared one.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-04 01:36:28 -08: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
A
c77cab0fff
fix(security): add --proto '=https' to update-check and update command curl calls (#2183)
The auto-update path in update-check.ts and the manual `spawn update` command
in commands/update.ts were missing --proto '=https' on their curl calls that
download and execute the install script. Without it, curl may follow redirects
to non-HTTPS URLs on hostile networks (MITM/DNS hijacking).

- update-check.ts: add --proto =https to execFileSync curl args
- commands/update.ts: replace execSync shell pipe with safe two-step
  execFileSync pattern (fetch script via curl --proto =https, then
  execute via bash -c) — matches the pattern already in update-check.ts

Same vulnerability class as PR #2172 (TypeScript files) and PR #2160 (shell
scripts); those PRs missed these two code paths.

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-04 04:18:56 -05:00
A
9376884009
feat(qa): add record-keeper teammate to QA bot (#2176)
Add a 5th teammate (record-keeper) to the QA quality cycle that keeps
README.md in sync with source-of-truth files. Uses a conservative
three-gate check (matrix drift, commands drift, troubleshooting gaps)
and only makes changes when drift is detected. Includes safeguards:
30-line diff limit, prohibited sections list, and source citations
required in PR body.

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:23:03 -08:00
A
c8581b7958
fix(security): add --proto '=https' to TypeScript curl provisioning calls (#2172)
* fix(security): add --proto '=https' to curl calls in TypeScript provisioning

Fixes #2169

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

* fix(lint): break long lines for biome format compliance

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-04 00:21:37 -08:00
A
3f8a9d7432
fix: use caduceus ⚕ icon for hermes agent (#2181)
Replace the Nous Research org avatar with the actual Staff of Hermes
(⚕) symbol from the hermes-agent page favicon. Sourced from the
WordPress emoji SVG and converted to 180x180 PNG.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 00:07:48 -08:00
A
1f879e1d1e
chore: refresh agent GitHub stars and icon metadata (#2179)
Notable star count changes:
- openclaw: 212,334 → 256,970 (+44,636)
- opencode: 107,223 → 115,408 (+8,185)
- zeroclaw: 15,177 → 21,867 (+6,690)
- claude: 67,857 → 73,410 (+5,553)
- codex: 61,159 → 62,925 (+1,766)
- hermes: 1,016 → 1,617 (+601)
- kilocode: 15,619 → 16,172 (+553)

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:49:15 -08:00
A
bfe53524c1
docs: add mandatory bun install step to worktree workflow (#2180)
Worktrees don't share node_modules with the main checkout. Without
`bun install`, tests and biome fail with "Cannot find package" errors
that block the pre-merge hook.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:48:38 -08:00
A
8eb5f8476c
chore: rename fixture files from snake_case to kebab-case (#2167)
Renames create_server, delete_server, ssh_keys, and server_types
fixture JSON files to kebab-case for consistency with codebase
conventions. Updates _metadata.json keys and qa-fixtures-prompt
naming convention accordingly.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:35:43 -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
67443aa4b3
chore: add pre-merge hook to gate gh pr merge/ready on lint + tests (#2173)
Adds a PreToolUse hook for Bash that intercepts `gh pr merge` and
`gh pr ready` commands and runs `biome check src/` + `bun test` before
allowing them. Blocks the command if either check fails.

The hook finds the worktree from the command path or falls back to
git rev-parse --show-toplevel.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:29:26 -08:00
A
446923c447
refactor: extract inline hook commands to TypeScript scripts (#2174)
* refactor: extract inline hook commands to TypeScript scripts in .claude/scripts/

Replace long inline `bash -c '...'` one-liners in .claude/settings.json with
standalone TypeScript scripts that are easier to read, debug, and maintain:

- enforce-worktree.ts: PreToolUse hook ensuring edits happen in worktrees
- validate-file.ts: PostToolUse hook for .sh/.ts file validation
- pre-merge-check.ts: PreToolUse hook running biome + tests before merge

Add .claude/scripts as a bun workspace package (@spawn/hooks).

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

* refactor: replace manual typeguards with valibot schemas in hook scripts

- Extract shared schemas (FilePathInput, CommandInput, parseStdin) to schemas.ts
- Replace inline multi-level typeof/in checks with v.safeParse() calls
- Add valibot dependency to @spawn/hooks package
- Add CLAUDE.md rule: always prefer valibot over manual typeguards, share schemas

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

* refactor: split CLAUDE.md into modular .claude/rules/ files

Split the 437-line monolithic CLAUDE.md into a lean 89-line project overview
plus 9 focused rules files in .claude/rules/ (auto-loaded by Claude Code):

- culture.md — embrace bold changes, parallelize, verify exhaustively
- shell-scripts.md — curl|bash compat, macOS bash 3.x, ESM only, bun not python
- type-safety.md — no `as` assertions, ALWAYS use valibot (never manual typeguards)
- testing.md — bun:test only, no vitest, no subprocess spawning
- git-workflow.md — worktree-first mandatory workflow
- autonomous-loops.md — discovery/refactor service architecture
- discovery.md — how to fill matrix gaps, add clouds/agents
- documentation.md — never commit docs, use .docs/
- cli-version.md — bump version on every CLI change

The type-safety rule now explicitly mandates valibot schemas over manual
typeguard chains in all cases beyond single-primitive narrowing.

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

* fix(lint): run biome check across all packages in CI

The lint workflow only checked packages/cli/src/. Now it checks all
TypeScript locations in a single biome check command:

- packages/cli/src/ (with GritQL plugins)
- packages/shared/src/ (new biome.json)
- .claude/scripts/ (new biome.json)
- .claude/skills/setup-spa/

Fixed all pre-existing lint/format errors:
- node: protocol on all Node.js built-in imports in hook scripts
- useBlockStatements in packages/shared/src/type-guards.ts
- expand formatting in .claude/skills/setup-spa/main.ts and spa.test.ts

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:05:41 -08:00
A
6e375d8b29
fix: restore hyphens in OAuth code regex + add regression test (#2171)
* fix: restore hyphens/underscores in OAuth code regex + add test (#2116)

PR #2116 broke OAuth by restricting the auth code regex to alphanumeric
only. OAuth providers (GitHub, Google, etc.) use hyphens and underscores
in their auth codes, so the stricter regex rejected valid codes.

Changes:
- Extract OAUTH_CODE_REGEX as an exported constant from oauth.ts
- Restore `_-` in the character class: [a-zA-Z0-9_-]{16,128}
- Add oauth-code-validation.test.ts with 20 tests covering:
  - Real-world provider formats (hyphens, underscores, mixed)
  - Length bounds (16–128)
  - Injection prevention (shell, XSS, path traversal, null bytes)
  - Character class completeness (explicit regression test for #2116)

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

* ci: retrigger checks

* fix: restore hyphens in OAuth code regex + add regression test

PR #2116 broke OAuth by restricting the auth code regex to alphanumeric
only. OAuth providers (GitHub, Google) use hyphens and underscores in
their auth codes, so the stricter regex rejected valid codes.

- Extract OAUTH_CODE_REGEX to oauth-constants.ts (zero-dep, testable)
- Restore `_-` in character class: [a-zA-Z0-9_-]{16,128}
- Add regression test covering valid formats, length bounds, injection

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

---------

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:52:29 -08:00
A
7c516ac887
fix(history): smart trimming evicts deleted records first, archives overflow (#2168)
* fix(history): smart trimming evicts deleted records first, archives overflow

When history exceeds 100 entries, deleted records (useless for `spawn ls`)
are now evicted first. If still over the limit, oldest non-deleted records
are also trimmed. All evicted records are archived to dated backup files
(history-YYYY-MM-DD.json) so nothing is permanently lost.

Previously, blind .slice() could silently discard records with active
connections that `spawn ls` depends on.

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

* style: fix biome formatting issues

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 22:37:57 -08:00
A
47ba11aa50
test: Remove duplicate resolveAndLog tests from commands-swap-resolve (#2165)
The resolveAndLog via cmdRun describe block in commands-swap-resolve.test.ts
(~113 lines, 5 tests) duplicated display-name resolution coverage already
provided by commands-resolve-run.test.ts. Both files tested case-insensitive
key resolution (CLAUDE->claude, HETZNER->hetzner) and display name resolution
(Codex->codex, Sprite->sprite) on the same code path.

Removed the entire duplicate resolveAndLog section. The detectAndFixSwappedArgs
and prompt-handling-with-swapped-args sections remain, as those test distinct
behavior not covered elsewhere.

-- qa/dedup-scanner

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 21:49:37 -08:00
A
523ae3284d
fix(security): replace execSync shell interpolation with execFileSync array args in auto-update (#2162)
* fix(security): replace execSync shell interpolation with execFileSync array args in auto-update

Fixes #2161

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

* style: format update-check files to pass Biome lint

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.6 <noreply@anthropic.com>
2026-03-04 00:25:31 -05:00
A
6334abe5a6
refactor: Remove stale comment listing untested functions in run-path test (#2166)
The block comment in run-path-credential-display.test.ts listed five
functions it claimed to test, but the file only tests two:
  - prioritizeCloudsByCredentials
  - isRetryableExitCode

Functions buildCredentialStatusLines, formatAuthVarLine, validateRunSecurity,
and validateEntities were never imported or exercised in this file. Removed
the misleading entries so the comment accurately reflects test coverage.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-04 00:24:44 -05: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
85e0c932c7
fix(ssh): use boolean flag to detect TCP probe success in waitForSsh (#2157)
When the TCP probe succeeds on the final attempt, `attempt` equals
`maxAttempts` after the loop increments it. The previous guard
`attempt >= maxAttempts` then incorrectly threw a timeout error even
though the port was open.

Fix by tracking TCP success with a `tcpOpen` boolean flag and checking
that instead of the attempt counter.

Fixes #2155

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-03 20:47:02 -05:00
A
4d3f2ba54a
refactor: Remove dead code and stale references (#2154)
* refactor: Remove redundant loadTokenFromConfig wrappers in hetzner, daytona, digitalocean

The previous PR (#2151) introduced shared loadApiToken() in shared/ui.ts and
updated hetzner/daytona to delegate to it via thin wrapper functions. This
commit removes the now-unnecessary wrapper functions entirely, inlining the
loadApiToken() calls directly at the callsite.

Also removes the 16-line duplicate loadTokenFromConfig() implementation in
digitalocean.ts (which replicates the same api_key/token field reading and
regex validation logic as loadApiToken) and replaces it with a direct call to
loadApiToken("digitalocean").

-- qa/code-quality

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

* bump version to 0.12.12 (main already has 0.12.11)

---------

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 20:43:19 -05:00
A
a6b92e1e86
test: Remove theatrical test that timeouts waiting for interactive prompt (#2153)
The 'promptBundle should skip prompt without --custom' test expected
promptBundle() to return immediately when SPAWN_CUSTOM is unset. But
promptBundle() has no SPAWN_CUSTOM guard — it always shows an interactive
selection prompt unless LIGHTSAIL_BUNDLE or SPAWN_NON_INTERACTIVE=1 is set.

Without SPAWN_NON_INTERACTIVE=1, the test blocks on stdin input and hits
the 5-second bun:test timeout. When run in the full test suite it
appeared to pass due to module import caching from previous tests, making
it a flaky, non-deterministic test.

Remove the test entirely since it tests non-existent behavior.

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 20:41:47 -05:00
A
a76bcaf7d1
fix(run): await res.text() before stopping spinner in downloadScriptWithFallback (#2158)
Fixes #2156

The spinner was stopped with a success message before the HTTP response
body stream was fully consumed. If the stream failed mid-transfer (network
drop, truncation), users saw "Script downloaded" followed by a confusing
downstream error. Now both the primary and fallback paths await res.text()
before calling s.stop().

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-03 20:39:52 -05:00
A
796eaa9d02
refactor: Remove dead code and stale references (#2151)
- Extract duplicate loadTokenFromConfig helper (hetzner + daytona) into
  shared loadApiToken() in shared/ui.ts, eliminating 24 lines of
  duplicate validation logic across two cloud modules
- Move misplaced FETCH_TIMEOUT and UPDATE_BACKOFF_MS constants in
  update-check.ts from the Schemas section into the Constants section
  where they belong (stale empty section header fix)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 13:19:01 -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
5b1c3f777e
test: Remove duplicate install-field test in manifest-type-contracts (#2150)
The "install commands should be strings" test in the "Agent launch
command consistency" describe block was a full duplicate of the
per-agent "install should be a non-empty string" test already
present in the "Agent required field types" loop.

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 15:48:10 -05: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
0aea348b8f
fix(ux): stop spinner before credential prompts during delete (#2144)
When credentials expire during server deletion, the spinner was running
simultaneously with interactive credential prompts, creating confusing
overlapping UI. Extract ensureDeleteCredentials() to run all credential
checks (which may prompt the user) before starting the deletion spinner.

All 6 cloud providers are covered: AWS, Hetzner, DigitalOcean, GCP,
Daytona, and Sprite.

Fixes #2141

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 11:55:45 -08:00
A
931fbed8b3
fix(ux): clarify credential detection messages to avoid confusion (#2147)
Distinguish between 'no local credentials' and 'using spawn-cached credentials'
so users understand why an initial failure message is followed by a success.

Fixes #2142

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-03 11:54:18 -08: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
f1ca7808c4
fix(ux): remove duplicate OAuth browser fallback URL message (#2143)
The DigitalOcean OAuth flow printed two near-identical fallback URL
messages: one manually before calling openBrowser(), and one from
openBrowser() itself. Remove the manual one since openBrowser()
already handles the fallback.

Fixes #2140

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 14:50:30 -05:00
A
91960b5e80
fix: exit process when remote session ends (#2148)
After showing post-session messages, the local process now exits cleanly
instead of requiring an extra Ctrl+C. The root cause was that after main()
resolved, lingering event loop handles (from @clack/prompts stdin listeners,
fetch connections, etc.) prevented Node/Bun from exiting naturally.

The fix adds process.exit(0) on successful main() completion, which covers
all session paths (bash script execution via execScript, SSH reconnection
via cmdConnect, and agent re-entry via cmdEnterAgent).

Fixes #2145

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-03 11:48:43 -08:00
A
79aa70c390
test: add coverage for untested ui utility functions (#2135)
* test: add coverage for 6 untested pure utility functions in shared/ui.ts

Adds tests for validateServerName, validateRegionName, validateModelId,
toKebabCase, sanitizeTermValue (security-critical), and jsonEscape.
These exported functions previously had zero test coverage.

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

* style: apply biome formatting to ui-utils test file

Address formatting review feedback: reformats destructuring import
to match project style.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 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-03 11:22:06 -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
A
22a06e3237
test: Remove duplicate and theatrical tests (#2136)
- Remove cmdlast "should not call cmdRun when no history exists" test which
  admitted in its own comment that it could not verify its stated intent and
  simply duplicated the assertion from the previous test in the same describe block.

- Fix always-pass risk in manifest-type-contracts: "Interactive prompts
  structure" and "Config files structure" tests iterated over optional agent
  fields with a bare continue when the field was absent, meaning both tests
  would vacuously pass if no agents had those fields. Added guard assertions
  (expect(length).toBeGreaterThan(0)) matching the pattern used by sibling
  tests in the same file.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 11:50:54 -05:00
A
8de2c17c99
refactor: Remove dead code and stale references (#2132)
* refactor: Remove dead code and stale references

- Remove unused variables and functions in test files:
  - cmdlast.test.ts: remove unused cmdRunMock and consoleOutput function
  - cmdlist-integration.test.ts: remove unused resolveDisplayName import and consoleErrorOutput function
  - cmd-listing-output.test.ts: remove unused getTerminalWidth import
  - commands-update-download.test.ts: remove unused callIndex variable
  - download-and-failure.test.ts: remove unused callCount variable and unused init parameter
  - manifest-cache-lifecycle.test.ts: remove unused m1 variable
  - manifest-integrity.test.ts: fix unused key in for-loop destructuring
  - manifest-type-contracts.test.ts: fix 9 unused loop variables, remove implicit any let,
    replace while-exec loop with matchAll to resolve noAssignInExpressions error
- Fixes biome lint errors from 22 down to 0

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

* style: apply biome format to fix CI check

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-03 09:49:53 -05:00
A
6881719b1a
fix(security): pipe base64 via stdin in daytona uploadFile (#2133)
Eliminates b64 interpolation into the remote shell command string,
providing defense-in-depth alongside existing path validation.

Fixes #2130

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 08:32:40 -05:00
A
99a0f58937
test: fix always-pass in-memory cache test to assert fetch not called again (#2131)
The "should use fresh disk cache without calling fetch" test only checked
toHaveProperty("agents"), which would pass even if fetch was called again.
Renamed to reflect actual behavior (in-memory cache path) and added
assertions: expect(m2).toBe(m1) and fetch call count unchanged.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 08:30:33 -05:00
Ahmed Abushagur
300b330106
fix: address 4 reliability issues across codebase (#2129)
* 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>
2026-03-03 03:18:44 -05:00
A
c9b8ee5997
refactor: Remove dead code and stale references (#2128)
- sprite/sprite.ts: Replace duplicate saveVmConnection implementation
  with a call to the shared saveVmConnection from history.ts. The local
  version duplicated the mkdir + writeFileSync logic already provided by
  the shared function, just with Sprite-specific hardcoded values.
  Remove now-unused writeFileSync, mkdirSync, and getSpawnDir imports.
- Bump CLI version 0.12.5 → 0.12.6 (patch)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 22:05:38 -08:00
A
8bc0a0291b
test: fix always-pass cache test to assert fetch was not called (#2127)
The "should use disk cache when fresh" test in manifest.test.ts set up
a mock fetch with a comment saying it "should not be called" but never
asserted expect(global.fetch).not.toHaveBeenCalled(). The test passed
whether or not the cache was actually used, providing no signal.


-- qa/dedup-scanner

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:04:48 -05:00
Ahmed Abushagur
4a90abdaa2
fix(e2e): improve openclaw reliability on AWS and other clouds (#2123)
* 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>
2026-03-02 23:19:34 -05:00
A
7b650a0103
test: Remove duplicate and theatrical tests (#2124)
* test: Remove duplicate and theatrical tests

Remove 18 duplicate tests from run-path-credential-display.test.ts
that repeated coverage already provided by dedicated test files:
- "entity validation for run path" (7 tests) duplicated check-entity.test.ts
- "key resolution for run path" (6 tests) duplicated fuzzy-key-matching.test.ts
- "run-path validation sequence integration" (5 tests) duplicated
  check-entity.test.ts, fuzzy-key-matching.test.ts, and script-failure-guidance.test.ts

Replace the three duplicate describe blocks with a focused 2-test
describe("isRetryableExitCode") block that covers the only unique
assertions in that section. Also remove unused spyOn import and
unused mockExit variable.

Bump version 0.12.4 → 0.12.5.

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

* fix(fmt): collapse import to single line for biome format compliance

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-02 22:06:38 -05:00
A
ffe4cf8c9e
refactor: Remove stale shellcheck disable comment from aws/kilocode.sh (#2125)
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>
2026-03-02 20:40:14 -05:00
Ahmed Abushagur
f7b3bc91c9
fix: show all polling status on single updating line (#2115)
Add logStepInline/logStepDone helpers to ui.ts and convert all 9
polling loops (DO droplet, DO cloud-init, AWS instance, AWS cloud-init,
Hetzner cloud-init, Daytona SSH, Sprite connectivity, GCP startup,
shared SSH port) from multi-line spam to a single line that updates
in place.

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-02 18:10:27 -05:00
A
4aaf125a2c
fix(reliability): add curl download error handling to AWS and Hetzner shims (#2122)
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>
2026-03-02 18:09:25 -05:00
A
37c1881613
fix(security): validate AWS region immediately after reading from env (#2119)
Adds validateRegionName() check immediately wherever awsRegion is
assigned from environment variables, rather than waiting until
createInstance(). Prevents malicious region values from being used
in SigV4 signing and shell commands.

Fixes #2113

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-02 13:52:20 -08:00
A
277c4236a3
fix(security): replace eval with direct indirection in load_cloud_driver (#2121)
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>
2026-03-02 16:50:27 -05:00