Commit graph

1892 commits

Author SHA1 Message Date
A
080ea5a705
fix(security): use heredoc for gh auth login to prevent token exposure (#2364)
Replaces the pipeline form with a heredoc to prevent the GitHub token
from appearing in the process list (ps aux) on multi-user systems.

Fixes #2363

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 22:10:15 -07:00
A
6b769e95ab
refactor: fix stale type-guards export list in type-safety rules (#2367)
The shared utilities section in type-safety.md listed `hasMessage` as an
export from type-guards.ts, but that function does not exist. Updated to
list the actual exports: `isString`, `isNumber`, `hasStatus`,
`getErrorMessage`, `toRecord`, `toObjectArray`.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-09 01:08:43 -04:00
Ahmed Abushagur
e855790a5d
feat: wrap cloud VM sessions in tmux for persistence (#2358)
* feat: wrap cloud VM sessions in tmux for session persistence

- Ctrl+C exits the agent → user lands at a shell prompt (can run CLI commands)
- SSH disconnect → tmux session persists, `spawn last` reattaches
- Install tmux automatically during env setup if not present
- Reconnect flow (`spawn last`, `spawn enter`) also uses tmux attach
- Replaces the restart loop — tmux gives users control over restarts

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

* feat: auto-tunnel gateway dashboard port over SSH

Forward port 18789 (OpenClaw gateway dashboard) to localhost so users
can access http://localhost:18789 from their browser during SSH sessions.

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

* fix: address PR review — command injection, port forwarding, tmux install order

1. wrapWithTmux: escape backslashes, $, and backticks in addition to
   double quotes to prevent command injection via tmux send-keys
2. SSH port forwarding: remove unconditional -L 18789 tunnel from
   SSH_INTERACTIVE_OPTS; export SSH_TUNNEL_OPTS for agent-specific use
3. tmux install: try sudo apt-get first (most cloud VMs need it on AWS)

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-09 00:22:23 -04:00
Ahmed Abushagur
57a7a9e033
feat: install Playwright Chromium for OpenClaw browser tool (#2362)
Ubuntu 24.04 replaced chromium-browser with a snap redirect that fails
on cloud VMs without snapd. Playwright's bundled Chromium is
self-contained (~170MB), works headless, and has no snap dependency.

Installed as a non-fatal post-install step — if it fails, the agent
still works but without browser capabilities.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 00:20:33 -04:00
A
3d7ad51f6d
fix: GCP billing retry fails because temp startup script is already deleted (#2361)
The startup script temp file was cleaned up immediately after the first
gcloud call, but the billing retry path re-used the same args array
referencing that file. This meant billing retries always failed with a
file-not-found error. Move cleanup to a try/finally block that runs
after all retry paths. Also add randomness and mode 0o600 to the temp
file path.

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-08 23:07:57 -04:00
Ahmed Abushagur
e02040e33e
fix: persist PATH in .spawnrc so agent binaries work on SSH reconnect (#2355)
Previously .spawnrc only exported env vars (API keys). The PATH entries
for agent binaries (~/.npm-global/bin, ~/.bun/bin, etc.) were only set
in per-agent launch commands, so reconnecting via SSH left users with
"command not found" errors.

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

Fixes #2354

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 18:46:48 -07:00
A
62e1df9be5
refactor: deduplicate PkgVersionSchema to shared/parse.ts (#2357)
Move the PkgVersionSchema (v.object({ version: v.string() })) from its
duplicate definitions in commands/shared.ts and update-check.ts into the
shared parse module. Both consumers now import from the single source.

Bump CLI version to 0.15.22.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 21:45:51 -04:00
A
8bc5581e62
fix: validate base64 encoding before embedding in remote command (#2360)
Adds defense-in-depth check to reject malformed base64 output
before it is embedded in the cloud_exec remote command.

Fixes #2353

Agent: code-health

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

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 22:43:25 +00:00
A
f159333ee9
refactor: remove dead code and stale references (#2349)
- Remove unused `getBillingUrl()` and `getSetupSteps()` from billing-guidance.ts
  (only called by their own tests, never by production code)
- Remove unused `validateModelId()` from ui.ts (same — test-only, no callers)
- Remove stale daytona entries from billing-guidance data structures
  (daytona is not in manifest.json and has no cloud module)
- Update tests README with 3 undocumented test files
- Remove corresponding dead test cases

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 16:44:33 -04:00
A
4396703615
refactor: use shared getErrorMessage() and deduplicate OAuth CSS (#2348)
Replace 4 inline `err instanceof Error ? err.message : String(err)`
patterns in aws.ts, digitalocean.ts, and hetzner.ts with the shared
getErrorMessage() helper. The shared helper uses duck-typing which is
more robust across realms/prototypes than instanceof checks.

Export OAUTH_CSS from shared/oauth.ts and import it in
digitalocean/digitalocean.ts instead of duplicating the 250+ char
CSS string.

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-08 13:42:08 -04:00
A
8ac2ae366f
refactor: remove unused hasMessage type guard (#2346)
hasMessage was exported from shared/type-guards.ts but never imported
outside of its own test file. getErrorMessage already covers the
message-extraction use case. Remove the dead function and its tests.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 12:51:18 -04:00
A
05492f5a88
fix: pin bun install to v1.3.9 in all agent scripts (#2345)
Agent: security-auditor

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

Fixes #2333

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 09:26:17 -07:00
A
36582b3b95
refactor: deduplicate getErrorMessage into shared/type-guards.ts (#2343)
Moves getErrorMessage to zero-dep shared module, eliminating 13 inline
copies and 2 hasMessage variant sites across the codebase.

Fixes #2341

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-08 07:45:11 -07:00
A
24e393817f
fix: harden env var parsing and pkill patterns in provision.sh (#2342)
- Block dangerous system env vars (PATH, LD_PRELOAD, etc.) before export
- Add explicit alphanumeric validation on env var names
- Validate app_name is non-empty and safe before pkill -f
- Tighten pkill regex from "sprite.*exec.*" to "sprite exec.*"

Fixes #2330 #2332

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 10:43:28 -04:00
A
48af1c3459
fix: resolve undefined variable refs in Hetzner billing retry path (#2340)
PR #2335 fixed this bug in digitalocean.ts, gcp.ts, and aws.ts but
missed hetzner.ts. The billing retry block assigned serverId/serverIp
to undefined local variables (hetznerServerId, hetznerServerIp) instead
of _state.serverId / _state.serverIp, so the retry always threw
"Server creation failed" even when the API call succeeded. This also
adds the missing saveVmConnection() call in the retry success path so
the VM is recorded in spawn history.

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-08 09:48:54 -04:00
A
bf03d9e593
test: add coverage for generateEnvConfig and type-guard helpers (#2336)
Five exported, production-used functions had zero direct test coverage:
- generateEnvConfig (security-critical env var validation/escaping)
- toRecord, toObjectArray, hasStatus, hasMessage (type narrowing)

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 06:46:22 -07:00
A
bfef29a1b3
fix: resolve undefined variable refs in billing retry paths (#2335)
Five undefined variable references across three cloud modules caused
billing retry paths to silently fail:

- digitalocean: doToken, doDropletId, doServerIp → _state.token/dropletId/serverIp
- gcp: gcpProject → _state.project
- aws: instanceName → _state.instanceName

These caused checkAccountStatus() and checkBillingEnabled() to always
return early, and billing retry saves to use wrong/undefined values.

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-08 08:56:21 -04:00
A
4f528b1c77
refactor: remove unnecessary exports and fix stale comment (#2338)
- Remove `export` from `verifyOpenrouterKey` in shared/oauth.ts (only used internally)
- Remove `export` from `tcpCheck` in shared/ssh.ts (only used internally)
- Fix stale comment in commands/index.ts referencing non-existent `./commands.js`

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 08:51:25 -04:00
A
90a5174181
fix: isolate cmd-interactive tests from host spawn history (#2337)
Tests were failing because getActiveServers() found real history
records in ~/.spawn/history.json, causing an extra p.select() call
that shifted the mock prompt index and made manifest.agents[agent]
resolve to undefined.

Set SPAWN_HOME to an isolated directory in beforeEach so tests
always see an empty history regardless of host state.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 08:50:46 -04:00
L
a77f70adfc
fix: update cloud picker prompt to 'Pick your cloud' (#2334)
* fix: update cloud picker prompt to "Pick your cloud"

The previous "Where should your agent run?" was vague. Simplify to
"Pick your cloud (type to filter)" for clarity.

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

* fix: use "Select a cloud" for cloud picker prompt

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-08 05:04:28 -07:00
A
2d69b2806b
fix: improve cloud descriptions for non-technical users (#2328)
Cherry-picks UX improvements from #2321: simplifies cloud descriptions
to plain language, adds account/payment requirements upfront so users
know what they need before starting.

Fixes #2323

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-08 04:07:25 -07:00
Ahmed Abushagur
bc0c1827bb
fix: reorder auth flow and persist OpenRouter API key (#2320)
* fix: reorder auth flow and persist OpenRouter API key across retries

Two onboarding issues reported by users:

1. After DigitalOcean OAuth, the message said "OpenRouter authentication
   in 5s..." but then a GitHub CLI prompt appeared first. Fix: move API
   key acquisition immediately after cloud auth, before preProvision
   hooks (which include the GitHub prompt). Remove the misleading 5s
   delay message.

2. On retry after billing failure, DigitalOcean token was remembered but
   the OpenRouter API key was lost (only stored in process.env). Fix:
   persist the key to ~/.config/spawn/openrouter.json and load it on
   subsequent runs, matching how cloud tokens are already persisted.

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

* fix: add mode 0o700 to config dir and await saveOpenRouterKey

- Add mode: 0o700 to mkdirSync in saveOpenRouterKey to match other cloud
  modules (aws, hetzner, digitalocean) and prevent directory permission leak
- Add missing await on saveOpenRouterKey(manualKey) to ensure manual API
  keys persist to disk before the function returns

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-08 06:48:14 -04:00
A
de732fa695
fix: prevent command injection in _sprite_exec via stdin piping (#2329)
Pipe the command via stdin to bash instead of embedding it in a bash -c
string. This eliminates shell injection risk from unquoted cmd parameter,
consistent with _sprite_exec_long in the same file and other cloud drivers.

Fixes #2327

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 06:44:19 -04:00
A
fedd024801
refactor: remove dead runServerCapture from all cloud modules (#2325)
The runServerCapture function was defined in aws, hetzner, gcp, and
digitalocean modules but never called anywhere in the codebase. All
cloud modules use runServer (which streams to stderr) and the
CloudRunner interface only requires runServer, not runServerCapture.

Bump CLI version 0.15.14 → 0.15.15.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 01:47:33 -08:00
Ahmed Abushagur
a215848cac
fix: skip SSH key selection prompt, use all keys automatically (#2326)
New users don't know which SSH key to pick. Just use all discovered
keys silently (ed25519 sorted first). If none exist, generate one.

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 05:45:13 -04:00
Ahmed Abushagur
dda6d53db7
fix: skip model selection prompt, default to openrouter/auto (#2322)
New users don't know what LLM models are — prompting them to pick one
with no context is confusing and openrouter/auto can route to weak
models. Remove the interactive model prompt entirely; agents use their
modelDefault silently (or MODEL_ID env var for power users).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 00:54:46 -08:00
Ahmed Abushagur
ff3a60267c
feat: add billing/payment setup guidance for new cloud users (#2319)
Detect billing-related server creation errors, open the cloud's billing
page in the browser, and prompt the user to retry after adding a payment
method. Adds pre-flight account checks for DigitalOcean (account status)
and GCP (billing enabled).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 04:50:51 -04:00
Ahmed Abushagur
c9792f1213
fix: remove banned as type assertions from key-server.ts (#2324)
Replace 3 `as` casts with runtime narrowing:
- `m.clouds as Record<string, any>` → toRecord() helper
- `body.providers as string[]` → Array.isArray + typeof guard
- `fd.get(...) as string` → typeof guard

Closes #2268

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 04:49:09 -04:00
A
26149d14b1
fix(spa): detect HTML auth redirects in Slack file downloads (#2316)
Slack file downloads fail silently when the bot token lacks the
files:read OAuth scope — Slack returns an HTML login page instead of
the actual file bytes. This causes Claude Code to send corrupt "images"
to the Anthropic API, which returns 400 "Could not process image".

Changes:
- Add files:read scope to slack-manifest.yml
- Add Content-Type header check in downloadSlackFile (catches text/html)
- Add magic-byte check via looksLikeHtml() as defense-in-depth
- Add tests for both validation paths and the looksLikeHtml helper

Note: After merging, the Slack app must be reinstalled to pick up the
new files:read scope on the bot token.

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-08 04:48:37 -04:00
Ahmed Abushagur
0ff1da1093
fix: remove redundant GitHub CLI prompt during provisioning (#2318)
Auto-detect GitHub credentials (GITHUB_TOKEN env var or `gh auth token`)
instead of interactively asking users. Rename promptGithubAuth → detectGithubAuth.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-08 00:01:09 -08:00
A
459e25a844
feat(cli): show connect-or-create menu when existing spawns are present (#2310)
* feat(cli): show connect-or-create menu when existing spawns are present

When the user runs `spawn` with no arguments and has active servers in
history, display a top-level menu before jumping into the create flow:

  What would you like to do?
  ❯ Connect to existing server
    Create a new server

Selecting "Connect to existing server" opens the same interactive picker
as `spawn list` (activeServerPicker). Selecting "Create a new server" or
having no existing spawns continues with the current create flow, so
there is no behaviour change for first-time users.

Fixes #2308

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

* chore(cli): bump version to 0.15.14

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-08 01:56:37 -05:00
A
053c0a8aec
test: remove 34 theatrical tests from manifest-cache-lifecycle.test.ts (#2317)
Remove tests that verify JavaScript language semantics rather than
application logic. These tests would pass even if the source code
were deleted:

- 18 isValidManifest tests (JS truthiness of null, 0, false, "", [])
- 7 matrixStatus edge cases (Object property lookup with hyphens,
  underscores, empty strings, long keys)
- 5 agentKeys/cloudKeys ordering tests (Object.keys insertion order,
  an ES2015 spec guarantee)
- 3 countImplemented tests (for-loop over 1000 items, single entry,
  non-standard statuses)

Kept 17 tests that exercise real application behavior: cache corruption
recovery, HTTP error fallback, in-memory cache, fallback chains, and
countImplemented case-sensitivity.

Closes #2315

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 01:18:54 -05:00
A
bb290c37df
docs: sync README matrix with manifest.json (add Junie) (#2312)
manifest.json has 8 agents (added Junie) and 48 implemented combinations,
but README tagline said "7 agents / 42 combinations" and the matrix table
was missing the Junie row.

-- qa/record-keeper

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 00:07:22 -05:00
A
23fea2df21
fix(e2e): add junie agent to E2E test harness (#2314)
The junie agent was added in #2300 but the E2E test scripts were not
updated. This adds junie to ALL_AGENTS, verify dispatch, input test
dispatch, and the provision.sh fallback env configuration.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 00:03:32 -05:00
A
bd41641c11
fix(cli): improve visual spacing in spawn list output (#2311)
- Interactive picker: add blank separator line between entries so label
  and subtitle are visually grouped (not blending into adjacent entries)
- Non-interactive table: wrap subtitle in pc.dim() for better contrast
  with the bold entry name
- Update pickerHeight to account for added separator lines

Fixes #2309

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-08 00:01:53 -05:00
A
252e8fc726
feat: add Junie CLI (JetBrains) agent across all 6 clouds (#2300)
Adds JetBrains' Junie CLI as a new agent in the spawn matrix.

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

Closes #2296

Agent: issue-fixer

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

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

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

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

Fixes #2304

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 22:05:44 -05:00
A
e7ac388110
fix: make credential hint tests environment-independent (#2303)
Tests for getScriptFailureGuidance were failing when cloud credential
env vars (HCLOUD_TOKEN, DO_API_TOKEN) were set in the environment.
The tests expected these vars to appear as "missing" in the output,
but only unset OPENROUTER_API_KEY. Now both the cloud-specific var
and OPENROUTER_API_KEY are saved/unset before each test.

Bump CLI version to 0.15.11.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-07 20:41:52 -05:00
A
90ae485c02
fix: add per-process timeout to SSH handshake probes in waitForSsh (#2299)
The Phase 2 SSH handshake loop in waitForSsh spawns SSH processes
without a per-process timeout. ConnectTimeout=10 only covers TCP
connect — if sshd accepts the connection but stalls during key
exchange or authentication, the process hangs indefinitely. This
causes the entire spawn command to freeze with no way to recover.

Add a 30s killWithTimeout guard to each probe, matching the pattern
already used in every cloud-specific runServer/uploadFile function.

-- refactor/code-health

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-07 18:40:48 -05:00
A
099ad8940e
feat(e2e): send agent x cloud matrix email on completion (#2297)
After every e2e run, send an HTML matrix report to KEY_REQUEST_EMAIL
via Resend showing pass/fail/skip per agent x cloud combination.

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

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

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 14:07:55 -08:00
A
1991ffcb15
fix: add timeout protection to uploadFile across all SSH-based clouds (#2298)
All four SSH-based uploadFile functions (Hetzner, DO, AWS, GCP) used
`await proc.exited` on SCP subprocesses without any timeout guard.
If SCP hangs due to a network issue, the CLI hangs indefinitely.

This adds the same killWithTimeout pattern already used by runServer
and runServerCapture in these same files: a 120-second timeout that
kills the SCP process if it stalls.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 13:48:11 -08:00
Ahmed Abushagur
7bebc6558f
feat: full marketplace compliance + automated Vendor API submission (#2295)
Packer template:
- Match official 90-cleanup.sh: remove SSH host keys, create
  revoked_keys, remove cloud-init instances, zero-fill free space,
  use --force-confold for upgrades, autoremove/autoclean
- Add Packer manifest post-processor for snapshot ID extraction
- Remove PACKER_LOG=1 (debug logging not needed in production)

Workflow:
- Add "Submit to DO Marketplace" step after successful build
- Reads agent→app_id mapping from MARKETPLACE_APP_IDS secret (JSON)
- Extracts snapshot ID from Packer manifest, PATCHes Vendor API
- Gracefully handles 400 (app already pending review)
- Skips silently if no MARKETPLACE_APP_IDS secret is configured

Setup: add MARKETPLACE_APP_IDS secret as JSON, e.g.:
  {"claude":"60089fc6...", "codex":"60089fc7..."}
App IDs come from the DO Vendor Portal after initial approval.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 16:40:04 -05:00
A
dadb2387e2
refactor: Fix stale references in qa-quality-prompt and test README (#2294)
- Fix qa-quality-prompt.md references to non-existent packages/shared/src/
  (only packages/cli/ exists; shared code lives in packages/cli/src/shared/)
- Add missing test file entries to __tests__/README.md:
  do-snapshot.test.ts and ui-utils.test.ts

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

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

Agent: security-auditor

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

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

Agent: security-auditor

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

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

Fixes #2286
Fixes #2287

Agent: code-health

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

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

Affected functions: input_test_claude, input_test_codex, input_test_openclaw,
input_test_zeroclaw.

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

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

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

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

* fix: restore timeout protection for input tests using cloud_exec

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

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-07 12:41:50 -05:00