Commit graph

2145 commits

Author SHA1 Message Date
A
7fe1bdf6b3
fix(junie): remove JUNIE_MODEL env var to fix 'Unknown model: openrouter/auto' crash (#2735)
Junie only accepts its own shorthand model names (gpt, opus, sonnet, etc.)
and not OpenRouter model IDs. Removing modelEnvVar lets junie handle its
own model routing via the OpenRouter API key instead.

Fixes #2734

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-17 21:22:32 -07:00
A
e1617fdc01
fix(e2e): add /usr/local/bin to openclaw PATH in verify.sh for GCP (#2736)
On GCP VMs (running as root), npm installs openclaw to /usr/local/bin
instead of ~/.npm-global/bin because the system npm prefix is writable
and already in PATH. The E2E verify_openclaw() and related gateway
helper functions only explicitly listed ~/.npm-global/bin, ~/.bun/bin,
and ~/.local/bin — missing /usr/local/bin when .spawnrc sourcing
silently fails in the piped-bash SSH exec context.

Add /usr/local/bin explicitly to all openclaw-related PATH exports in
verify.sh so the binary check succeeds regardless of .spawnrc state.

Fixes #2732

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-17 21:21:02 -07:00
Ahmed Abushagur
c11879d547
fix(windows): download JS bundle instead of bash wrapper on Windows (#2730)
The bash wrapper scripts (.sh) contain bash syntax that PowerShell
cannot parse. On Windows, download the pre-built JS bundle from
GitHub releases and run it directly via `bun run {cloud}.js {agent}`,
which is exactly what the bash wrapper ultimately does.

Affects both interactive (execScript) and headless (cmdRunHeadless)
code paths. macOS/Linux behavior unchanged.

Closes #2726

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 19:09:44 -07:00
A
b1de116690
refactor: replace manual multi-level type guards with toRecord/isString in index.ts (#2731)
Two instances of the pattern `err && typeof err === "object" && "code" in err`
violated the type-safety rule requiring valibot or shared type-guard utilities
instead of manual multi-level type checks. Replaced with `toRecord(err)` and
`isString()` from @openrouter/spawn-shared for consistent, rule-compliant error
code extraction. Also bumps CLI patch version per cli-version.md.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 18:40:16 -07:00
A
234dd5e6e1
docs: sync README with source of truth (#2729)
Add missing 'spawn uninstall' command to the Commands table. The command
exists in packages/cli/src/commands/help.ts (getHelpUsageSection) but was
absent from the README commands table.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-17 18:38:56 -07:00
Ahmed Abushagur
6e92cc832b
feat: add systemd auto-update service for agents on cloud VMs (#2728)
Installs a systemd timer + oneshot service that updates the agent binary
and system packages every 6 hours without disrupting running instances.

Agent update safety:
- Binary agents (Go, Rust): Linux keeps old inode in memory; safe to replace
- npm agents: Node.js caches modules at startup; running processes unaffected
- New version takes effect on next restart via the existing restart loop

System update safety:
- Disables Ubuntu's unattended-upgrades to prevent dpkg lock contention
- Uses flock -w 300 on /var/lib/dpkg/lock-frontend before apt operations
- DEBIAN_FRONTEND=noninteractive with --force-confdef/--force-confold

User-facing:
- "Auto-update" option in setup multiselect (default on, user can uncheck)
- Skipped for local cloud and non-systemd systems
- Non-fatal: setup failure doesn't block agent launch
- Logs to /var/log/spawn-auto-update.log

Timer: 15min after boot, then every 6h with 30min random jitter.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:34:12 -07:00
Ahmed Abushagur
66b16d8651
feat: add Windows PowerShell support — remove bash dependency for local execution (#2727)
Replace hardcoded "bash" shell references with platform-aware utilities so
spawn works natively from PowerShell on Windows without WSL or Git Bash.

- New shared/shell.ts: isWindows(), getLocalShell(), getInstallScriptUrl(),
  getInstallCmd(), getWhichCommand() with platform override for testability
- local/local.ts: use getLocalShell() for runLocal() and interactiveSession()
- commands/run.ts: spawnScript/runScriptHeadless use getLocalShell()
- commands/update.ts: Windows downloads install.ps1, runs via PowerShell
- update-check.ts: Windows auto-update uses install.ps1; "where" replaces "which"
- shared/orchestrate.ts: PowerShell-compatible .spawnrc setup for local Windows
- Remote SSH commands unchanged — remote servers are always Linux

Closes #2726

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-17 16:35:23 -07:00
A
ba94f681b3
feat(cli): add spawn uninstall command (#2724)
* feat(cli): add `spawn uninstall` command

Adds a new `uninstall` subcommand that cleanly reverses the install:
- Removes ~/.local/bin/spawn binary and /usr/local/bin/spawn symlink
- Cleans spawn PATH entries from shell RC files (.bashrc, .zshrc, etc.)
- Removes ~/.cache/spawn/ cache directory
- Optionally removes ~/.spawn/ (history) and ~/.config/spawn/ (keys/config)
- Shows confirmation prompt before any destructive action

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

* refactor: use start/end markers for shell RC blocks

- Add shared RC_MARKER_START/RC_MARKER_END constants in paths.ts
- Update install.sh to write `# >>> spawn >>>` / `# <<< spawn <<<` block markers
- Update uninstall.ts to remove content between markers (with legacy fallback)
- Addresses review feedback: shared markers make RC entries easier to audit/remove

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

* refactor: share legacy RC marker from paths.ts

Move the legacy "# Added by spawn installer" string to RC_MARKER_LEGACY
in shared/paths.ts so both install.sh and uninstall.ts reference the
same source of truth for all marker strings.

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

---------

Co-authored-by: Claude <claude@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-17 16:33:09 -07:00
A
1733903a1f
fix(digitalocean): add OAuth recovery in doApi for mid-session 401 errors (#2723)
When a DigitalOcean token expires mid-session (after ensureDoToken succeeds),
API calls like ensureSshKey, createServer, listServers, destroyServer would
crash with "Fatal: DigitalOcean API error 401" because doApi had no recovery
path for 401 responses.

Now doApi detects 401, attempts OAuth browser flow recovery via tryDoOAuth(),
and retries the request with the new token. A re-entrancy guard prevents
infinite loops (doApi → tryDoOAuth → doApi → ...). If OAuth recovery fails,
the original 401 error is thrown as before.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:13:42 -07:00
A
00863b0172
fix(digitalocean): handle 401 gracefully in testDoToken instead of crashing (#2722)
testDoToken() used asyncTryCatchIf(isNetworkError, ...) which only caught
network errors. A 401 HTTP response threw a regular Error that escaped the
guard, propagating to main().catch() and printing "Fatal: DigitalOcean API
error 401...". Changed to asyncTryCatch() to catch all errors, returning
false for invalid tokens so ensureDoToken() naturally falls through to
OAuth recovery.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:14:30 -07:00
A
6509973154
test: remove duplicate terminal-width boilerplate in cmd-listing-output tests (#2721)
Consolidate 10 single-assertion cmdMatrix tests (5 wide-terminal + 5
narrow-terminal) into 2 comprehensive tests using beforeEach/afterEach for
terminal-width setup. Also fix a pre-existing environment-dependent failure
where HCLOUD_TOKEN being set on the host caused the auth-hint test to see
"ready" instead of "needs".

Changes:
- "grid view (wide terminal)": 5 tests → 1 test (8 fewer cmdMatrix() calls)
- "compact view (narrow terminal)": 5 tests → 1 test (same)
- Fix "should display auth hints" to clear host env vars before asserting

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 14:22:05 -07:00
A
3630c07c70
fix(e2e): add per-agent timeout to prevent silent hangs in E2E runs (#2720)
The E2E framework's run_single_agent function had no overall timeout.
When provision/verify/input_test steps hung (e.g. cloud_exec blocking
on sprite-zeroclaw or digitalocean-opencode), the process would stall
indefinitely without writing a .result file, causing silent test failures.

Add a per-agent wall-clock timeout (default 1800s, 2400s for junie) that
wraps the core provision/verify/input_test logic in a killable subshell.
If the timeout expires, the subshell is killed and a "fail" result is
written, ensuring E2E batches always complete.

Fixes #2714

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-17 13:16:09 -07:00
A
ce91953649
fix(shell): quote CLAUDE_MODEL_FLAG expansion in security.sh (#2717)
Use ${CLAUDE_MODEL_FLAG:+"${CLAUDE_MODEL_FLAG}"} to prevent word-splitting
and glob expansion on values containing spaces or special characters.
When the variable is empty/unset, this expands to nothing (no empty arg).

Note: qa.sh does not use CLAUDE_MODEL_FLAG so no change needed there.

Fixes #2698

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-17 12:30:56 -07:00
A
c6087534aa
fix: populate connection fields in --headless --output json result (#2716)
After runBashHeadless() succeeds, read the spawn record saved during
orchestration and populate ip_address, ssh_user, server_id, and
server_name in the SpawnResult output.

Closes #2715

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 12:29:44 -07:00
A
2e26d56625
fix(security): escape newlines in safe_substitute to prevent sed injection (#2718)
The safe_substitute() function in discovery.sh, qa.sh, refactor.sh, and
security.sh escaped \, &, and | but not newlines. A newline in the
replacement value would break the sed s command, causing failure or
unexpected behavior. Add newline escaping (backslash + literal newline)
after the existing metacharacter escaping.

Fixes #2702

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-17 12:28:39 -07:00
A
0e5bfd830b
fix(e2e): double GCP cloud-init wait timeout to 10 minutes for Node install (#2713)
* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* fix(gcp): double cloud-init wait timeout to 120 attempts (10 min)

GCP startup scripts installing Node.js 22 via `n` from curl take longer
than 5 min on cold starts. The previous 60-attempt (5 min) poll timed
out with "Startup script may not have completed, continuing..." and
proceeded to run `npm install -g @kilocode/cli` before npm was available,
causing `npm: command not found` errors.

Increase `maxAttempts` from 60 to 120 (10 min) in `waitForCloudInit` to
give the Node install enough time to complete on GCP cold starts.

Confirmed by E2E run: GCP kilocode failed with npm not found after all 60
poll attempts exhausted; all other GCP agents passed (they don't need Node).

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-17 11:51:41 -07:00
Ahmed Abushagur
34785a9a63
feat(hermes): add YOLO mode toggle to setup menu (#2711)
Add HERMES_YOLO_MODE as a setup option for Hermes Agent, enabled by
default. This disables Hermes's security approval prompts so it can
self-install skill dependencies (e.g. himalaya for email) at runtime
on dedicated cloud VMs.

Users can uncheck it in the setup multiselect if they prefer Hermes
to prompt before installing tools.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 10:09:41 -07:00
A
5004a4db52
test: replace loose cloud-type count assertion with enumerated known-set check (#2709)
The "should have a reasonable number of distinct cloud types" test used
toBeGreaterThanOrEqual(2) and toBeLessThanOrEqual(10) — bounds so wide
they would never catch a real type-naming mistake. Replace it with an
explicit allowlist check so adding an unknown type fails immediately.

Current valid types (api, cli, local) are all in the set; vm, container,
sandbox, and cloud are pre-approved to avoid blocking planned additions.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 09:55:15 -07:00
A
eec83898e4
fix(kilocode): add binary verification after npm install to recover from silent postinstall failures (#2707)
@kilocode/cli v7+ uses a native binary postinstall that downloads a
platform-specific binary. On some clouds (notably GCP with cloudInitTier
"node"), this postinstall can fail silently, leaving the npm bin symlink
pointing to a JS wrapper with no actual native binary to exec.

The fix adds a KILOCODE_BINARY_VERIFY shell snippet that runs after npm
install and:
1. Checks if kilocode is already working (fast path)
2. If not, finds the npm package dir and re-runs the postinstall
3. If still not found, searches for the native binary in the package dir
   and symlinks it into a PATH-accessible location

Fixes #2706

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 08:30:00 -07:00
A
33014e3eed
docs: add cmd-link.test.ts to test README index (#2705)
cmd-link.test.ts was added but omitted from the test file index in README.md.
This keeps the index accurate as a reference for all 68 test files.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 05:51:51 -07:00
A
e8471b6136
test: remove duplicate Result constructors describe block (#2704)
The "Result constructors" describe block in with-retry-result.test.ts
(testing Ok/Err from shared/ui.js) was a duplicate of coverage already
provided by result-helpers.test.ts, which tests the same Ok/Err exports
from shared/result.ts (ui.ts re-exports them). The 3 trivial constructor
tests add no signal beyond what the withRetry and wrapSshCall tests
already exercise implicitly.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 02:23:24 -07:00
A
7f43e6bb9b
test: fix theatrical promptBundle test with real assertion (#2703)
promptBundle sets _state.selectedBundle via env var but the test was
calling promptBundle() without asserting anything about the result.
Added selectedBundle to getState() return value so tests can verify
the env var path is actually exercised.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 22:08:10 -07:00
A
8dd1db7fbb
test: remove duplicate and theatrical tests (#2700)
- Remove 2-test "flag registration" block from custom-flag.test.ts — both
  assertions (KNOWN_FLAGS.has("--custom") and findUnknownFlag returning null)
  were already covered by the KNOWN_FLAGS completeness test in unknown-flags.test.ts.

- Fix stale KNOWN_FLAGS completeness test: it was testing only 18 of 26 known
  flags, making it always-pass when new flags are added to flags.ts without
  updating the test. Now the test is bidirectionally exhaustive — every flag in
  the expected list must be in KNOWN_FLAGS, and every flag in KNOWN_FLAGS must
  be in the expected list. This absorbs the --steps/--config coverage.

- Remove findUnknownFlag(["--steps"]) / findUnknownFlag(["--config"]) test from
  steps-flag.test.ts — now redundant since the exhaustive completeness test
  already exercises those flags.

Net: -3 tests removed, +18 expect() calls added (exhaustive bidirectional check).

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-16 20:12:47 -07:00
A
5b2eddb763
fix(sprite): replace personal VM URL with official CDN for keep-alive script (#2701)
The sprite-keep-running.sh script was downloaded from a hardcoded personal
VM URL (kurt-claw-f.sprites.app) which would break all Sprite deployments
if that VM goes offline. Use the official CDN proxy at openrouter.ai/labs/spawn/.

Fixes #2699

-- refactor/code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 20:04:49 -07:00
A
b854917186
fix(security): validate tunnel URL and port from history before openBrowser() (#2697)
Add validateTunnelUrl() and validateTunnelPort() in security.ts to prevent
phishing attacks via tampered ~/.spawn/history.json. Apply both validations
in cmdEnterAgent() and cmdOpenDashboard() in connect.ts before any tunnel
data is used.

- validateTunnelUrl: enforce URL starts with http://localhost: or
  http://127.0.0.1: only (blocks external/phishing URLs)
- validateTunnelPort: enforce numeric value in range 1-65535
- Add comprehensive test cases for both validators

Fixes #2696

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-16 15:22:29 -07:00
A
644593eaea
fix(security): propagate path normalization to all cloud modules (#2693)
* fix(security): propagate path normalization to all cloud upload/download functions

PR #2690 added normalize() before path traversal checks in AWS but not
the other clouds. Apply the same defense-in-depth to GCP, DigitalOcean,
Hetzner, Sprite, and shared validateRemotePath.

Agent: code-health

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

* fix(security): use normalized path in all file transfer operations

Addresses code review: replace original remotePath with normalizedRemote
in scp commands and bash operations to prevent validation bypass.

- digitalocean: use normalizedRemote in uploadFile scp and derive
  expandedPath from normalizedRemote in downloadFile
- hetzner: same pattern for uploadFile/downloadFile
- gcp: derive expandedPath from normalizedRemote.replace(...) in both
  uploadFile and downloadFile
- sprite: use normalizedRemote in bash mkdir/mv command and derive
  expandedPath from normalizedRemote in downloadFile

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

* fix(security): close validation bypass in agent-setup and AWS file ops

validateRemotePath() validated the normalized path but returned void,
so the caller still used the original unsanitized remotePath in shell
commands — bypassing the normalization check entirely.

Fix: return the normalized path and use it in all file operations.

Also fix AWS uploadFile/downloadFile which validated normalizedRemote
but used the original remotePath in scp commands.

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.5 <noreply@anthropic.com>
2026-03-16 14:48:59 -07:00
A
bae921a295
fix(digitalocean): retry on 404 in waitForDropletActive (#2695)
DigitalOcean sometimes returns 404 immediately after droplet creation
before the resource propagates across their API. Previously this caused
an immediate fatal error, failing all DO agent provisions.

Now 404 responses are treated as transient and retried with the same
5s polling interval, consistent with how non-active statuses are handled.

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-16 14:19:02 -07:00
A
0b346dffcc
test: remove duplicate and theatrical tests (#2694)
Consolidated 3 separate per-exit-code dashboard URL tests (130, 137, 42)
into a single data-driven loop. Merged 2 per-signal tests (SIGTERM, SIGINT)
into one. Removed a weak always-true test ("always return a non-empty array")
that was already implied by the adjacent test above it. Net: 4 fewer tests,
no coverage loss.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 14:17:27 -07:00
A
9e627dff29
refactor: remove dead code and stale references (#2691)
Remove stale '// --- Swap Space Setup' section header from agent-setup.ts
that had no associated code. Swap space setup was moved to cloud init
userdata scripts (aws.ts, hetzner.ts etc.) but the empty section header
was left behind.

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-16 05:53:01 -07:00
A
64a181c8ea
test: remove theatrical NODE_INSTALL_CMD test and fix banned homedir import (#2692)
- cloud-init.test.ts: remove the NODE_INSTALL_CMD describe block that just
  checked if a string constant contains "curl" and "22". This is a snapshot
  test of a string literal with no behavioral signal.

- paths.test.ts: remove the banned `import { homedir } from "node:os"`.
  Per testing rules, named imports of homedir() bypass the preload sandbox
  mock (os.homedir default-export patch) and return the real home directory,
  making tests non-isolated. Replace the "falls back to os.homedir()" test
  with a behavioral assertion (result is a non-empty string) instead of
  comparing against the banned homedir() call.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-16 05:51:32 -07:00
A
1696ecdaa9
fix(security): add defense-in-depth username validation in GCP startup script (#2689)
Add explicit username format validation (`/^[a-zA-Z0-9_-]+$/`) as
defense-in-depth in `getStartupScript()` and `createInstance()`. While
`resolveUsername()` currently returns a constant, this belt-and-suspenders
check prevents shell injection if the function is ever changed to accept
dynamic input.

Fixes #2688

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-16 01:38:21 -07:00
A
085759aeaf
fix(security): add AWS secret key validation and harden path traversal (#2690)
- Add validateAwsSecretKey() function checking 40-char format
- Validate secret key in loadCredsFromConfig() and lightsailRest()
- Add normalize() to canonicalize paths before traversal check
- Harden both uploadFile() and downloadFile() path validation
- Update test fixtures with properly-formatted mock secret keys
- Add test for invalid secret key format rejection

Fixes #2686
Fixes #2687

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-16 01:29:01 -07:00
A
8fe6450485
fix(e2e): increase provision timeout for junie on hetzner (#2683)
* fix(e2e): increase provision timeout for junie on hetzner

junie's install takes >720s on Hetzner, exceeding the default
PROVISION_TIMEOUT and causing 100% E2E failure for hetzner-junie.

Add a per-agent provision timeout mechanism in common.sh via
get_provision_timeout(). This checks (in order):
  1. PROVISION_TIMEOUT_<agent> env var override
  2. Built-in per-agent default (_PROVISION_TIMEOUT_junie=1200)
  3. Global PROVISION_TIMEOUT (720s)

provision.sh now calls get_provision_timeout() to resolve the
effective timeout per agent instead of using the flat global.

Fixes #2680

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

* fix(security): whitelist-sanitize agent name before eval in get_provision_timeout

tr '-' '_' only replaced hyphens, allowing metacharacters like $, backticks,
and ; to pass through into eval, enabling shell injection via a crafted agent
name. Replace with sed whitelist [A-Za-z0-9_] to strip all unsafe chars.

Agent: team-lead
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-16 00:54:03 -07:00
A
ab51b09a03
feat(agents): add style-reviewer teammate and auto-update Claude Code (#2685)
Add a new style-reviewer agent to the refactor team that enforces project
rules from CLAUDE.md and .claude/rules/ (biome lint, shell script compat,
type safety, test conventions). Runs proactively during refactor cycles.

Also add `claude update --yes` to all 4 launcher scripts (refactor.sh,
discovery.sh, security.sh, qa.sh) so agents always run on the latest
Claude Code version before each cycle.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 00:41:17 -07:00
A
c9c662aaba
feat(security): add line-level inline comments to PR review protocol (#2684)
Update the pr-reviewer protocol to use the GitHub Pull Request Review API
(POST /repos/.../pulls/NUMBER/reviews) with an inline comments array,
pinning each security finding to the exact file:line in the PR diff.

The summary body is preserved for overview, while each finding also
appears as an inline comment on the specific code location.

Co-authored-by: Claude <claude@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-16 00:13:32 -07:00
A
e5725b9a66
fix(gcp): add /usr/local/bin to .spawnrc PATH for npm-global agents (#2681)
GCP VMs install kilocode (and other npm-global agents) to /usr/local/bin
via `npm install -g`. The .spawnrc PATH export relied on $PATH inheriting
/usr/local/bin from the SSH/login shell chain, but on GCP VMs the PATH
can be minimal depending on how the session is initiated (login shell
sourcing order, /etc/profile.d availability). Explicitly include
/usr/local/bin to ensure npm globally-installed binaries are always
findable regardless of base PATH.

Also updates fix.ts to keep its PATH in sync with generateEnvConfig().

Fixes #2679

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 00:00:09 -07:00
A
09576f16ef
fix(ui): remove confusing "None" checkbox from setup options (#2682)
The "None" sentinel option stayed checked alongside real selections,
which was confusing. Remove it — the multiselect already supports
submitting with nothing selected via `required: false`.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 23:43:01 -07:00
A
5cc9930769
feat(cli): add spawn link command to reconnect existing deployments (#2675)
Adds `spawn link <ip>` command that re-registers an existing cloud VM
in spawn's local state, so commands like `spawn list`, `spawn delete`,
and `spawn fix` work on it without reprovisioning.

Features:
- Auto-detects running agent via SSH (ps aux + which checks)
- Auto-detects cloud provider via IMDS metadata endpoints (Hetzner,
  AWS, DigitalOcean, GCP)
- Accepts --agent, --cloud, --user, --name flags to skip auto-detection
- TCP connectivity pre-check before SSH attempts
- Creates a SpawnRecord in history with full connection info
- Offers to connect immediately after linking
- Interactive picker fallback when auto-detection fails
- Non-interactive mode support (exits with clear error if detection
  fails without --agent/--cloud flags)

Also adds --user / -u to KNOWN_FLAGS for the unknown-flag checker.

Fixes #2673

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-15 23:11:13 -07:00
Ahmed Abushagur
6ef20ed437
fix(aws): auto-select server size by agent (#2676)
* fix(aws): auto-select server size instead of prompting

OpenClaw gets 4GB (medium_3_0), all other agents get 2GB (small_3_0).
Users can still override with SPAWN_CUSTOM=1 or LIGHTSAIL_BUNDLE env var.
Matches the auto-select behavior already used by DO and Hetzner.

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

* feat: guide Windows users to WSL at startup

Detects win32 platform and prints step-by-step WSL setup instructions
instead of failing with a confusing error.

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

* Revert "feat: guide Windows users to WSL at startup"

This reverts commit 8db72880ae.

* test: update DEFAULT_BUNDLE assertion to small_3_0

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-15 23:08:41 -07:00
A
c4961eb5cd
fix(e2e): prevent concurrent history write race and fix GCP HOME env (#2678)
* fix(history): use process-unique tmp file to prevent concurrent write race

Multiple spawn processes running in parallel (e.g. during E2E tests with
--parallel 6) all write to the same history.json.tmp path, causing ENOENT
when one process renames the file before another can. Use a pid+timestamp
suffix so each process writes to its own unique tmp file.

Fixes provision crashes seen in hetzner-junie E2E runs where the fatal
"rename history.json.tmp -> history.json" error aborted the session.

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

* fix(gcp): export HOME=/root in startup script to match cloud-init behavior

DigitalOcean and Hetzner cloud-init scripts both set `export HOME=/root`
before running Node installation. GCP's startup script did not, which
could cause `n` (the Node.js version manager) to install Node to an
unexpected location when HOME is unset or points elsewhere.

Without a consistent HOME, `npm prefix -g` may return a path that doesn't
match what the subsequent `npm install -g @kilocode/cli` expects, causing
the install to fail silently and leaving the kilocode binary absent.

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-15 23:06:23 -07:00
A
0f0bdf229f
test: remove duplicate and theatrical tests (#2677)
Consolidated redundant test setups in agent-tarball and cmdrun-happy-path
test suites:

- agent-tarball.test.ts: merged 4 mirror-cmd tests (all invoking the same
  tryTarballInstall call and inspecting the same mirrorCmd string) into a
  single test with shared beforeEach setup. Retained the non-fatal failure
  test separately since it has a different mock setup.

- cmdrun-happy-path.test.ts: collapsed 3 identical-setup dry-run tests into
  one consolidated test, and merged the two same-invocation launch-message
  tests into one. Each removed test was a pure duplicate of setup + assertion
  that could be expressed as additional expects in the same test.

Net: 1417 → 1411 tests (-6), 0 regressions.

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-15 22:20:53 -07:00
A
0ea2692e1e
fix(github-auth): always run gh setup when user explicitly opts in (#2674)
When the user selects the GitHub CLI step in setup options (interactive
prompt or --steps github), offerGithubAuth() was silently returning early
if no local gh token was found by detectGithubAuth(). This made the step
unreachable for users without gh installed locally — exactly the ones who
need remote setup most.

Fix: accept an `explicitlyRequested` parameter in offerGithubAuth(). When
true, skip the githubAuthRequested guard and always run the remote install.
The orchestrator passes enabledSteps?.has("github") as this flag.

detectGithubAuth() still auto-enables the step when a local token exists
(convenience forwarding), but can no longer block a user-explicit request.

Fixes #2672

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-15 22:19:38 -07:00
A
00df240f49
feat(openclaw): add channel selection to setup options (#2671)
Add BlueBubbles, Discord, Slack, Signal, and Google Chat to the
multi-select setup options for OpenClaw. Selected channels get
`enabled: true` stubs written via `openclaw config set`, so the
dashboard renders channel cards properly instead of showing
"Unsupported type: . Use Raw mode."

Channels are gated by enabledSteps — only user-selected channels
get stubbed. WhatsApp and Telegram remain in the list as before.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 20:21:42 -07:00
A
4f4b535c8d
fix(security): validate remotePath and harden base64 interpolation in uploadConfigFile (#2669)
Add strict character validation for remotePath to prevent command injection
via crafted paths. Use shellQuote for tempRemote in the shell command. Add
a base64 output assertion to document and enforce the safety of single-quoted
interpolation for settingsB64.

Fixes #2668

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-15 19:04:19 -07:00
A
6b2001def4
docs(tests): document 8 undocumented test files in __tests__/README.md (#2670)
The test README was missing entries for 8 test files that were added
after the initial documentation was written:
- cmd-feedback.test.ts
- cmd-fix.test.ts
- config-priority.test.ts
- delete-spinner.test.ts
- gcp-shellquote.test.ts
- oauth-pkce.test.ts
- result-helpers.test.ts
- steps-flag.test.ts
- spawn-config.test.ts

Added descriptions under the appropriate section headers so the README
accurately reflects all test coverage.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-15 18:39:57 -07:00
A
0efc4e89f0
fix(security): eliminate single-quote injection risk in verify.sh (#2667)
Pass base64-encoded prompts via _ENCODED_PROMPT shell variable assignment
at the start of remote command strings instead of interpolating directly
into single-quoted decode contexts. This prevents quote-escaping
vulnerabilities if INPUT_TEST_PROMPT or the encoding mechanism ever
changes to produce characters that break single-quote delimiters.

Fixes #2666

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-15 15:10:22 -07:00
A
9ad89a414a
fix(cli): replace "spawn update" launch hint with "spawn feedback" (#2665)
Replace startup banner message from "Run spawn update to check for
updates." to "Run spawn feedback to tell us what to improve."

Bumps CLI patch version to 0.19.1.

Fixes #2664

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-15 14:11:46 -07:00
A
4bff229238
refactor: remove dead deepMerge export from parse.ts (#2663)
deepMerge was exported from shared/parse.ts but never imported or called
from any other module. Biome confirms it as an unused variable. Removing
it eliminates dead code and the now-unused isPlainObject import.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 13:57:47 -07:00
A
52eaa19466
fix: allow empty string values for CLI flags like --steps "" (#2662)
extractFlagValue() used `!args[idx + 1]` to detect a missing value,
which treated empty strings as missing. Change to `=== undefined` so
that `--steps ""` passes through correctly as documented.

Fixes #2661

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-15 13:45:39 -07:00
A
548f41ed47
fix(e2e): source .bashrc in openclaw verify to resolve binary path on Sprite (#2660)
On Sprite VMs, npm's global prefix (from nvm) is writable and in PATH
after sourcing .bashrc, so openclaw installs to the nvm bin dir instead
of ~/.npm-global/bin. The E2E verify_openclaw() binary check only
prepended ~/.npm-global/bin, ~/.bun/bin, and ~/.local/bin — missing the
nvm bin path entirely.

Source .bashrc (in addition to .spawnrc) before the command -v check so
the verify PATH matches the install-time PATH. Applied the same fix to
the ensure/restart gateway helpers and the openclaw input test.

Fixes #2656

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-15 12:46:37 -07:00