Commit graph

1917 commits

Author SHA1 Message Date
A
705687de17
fix: persist npm-global PATH to .profile/.bash_profile/.bashrc for SSH reconnect (#2399)
After SSH reconnect, agent commands (openclaw, codex, kilocode, junie) were
not found because PATH was only written to ~/.bashrc, which is not sourced
by login shells. Login shells (used by SSH) source ~/.profile or
~/.bash_profile instead.

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

Fixes #2394

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-09 16:26:49 -07:00
A
5b47ad8da9
fix(ux): clarify auth ordering and remote machine context in setup messages (#2400)
- Reword preflight OpenRouter credential message to not imply it happens
  immediately (cloud auth runs first in the orchestration pipeline)
- Clarify GitHub CLI setup messages to specify "remote server" instead of
  leaving ambiguous "this machine" context for cloud users

Fixes #2396

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-09 18:46:53 -04:00
Ahmed Abushagur
06796ec95c
fix: isolate orchestrate tests from user's ~/.spawn history (#2398)
The orchestrate test suite called runOrchestration (which internally
calls saveSpawnRecord) without setting SPAWN_HOME to a temp directory.
Every test run wrote ~20 fake records into the user's real history,
eventually filling it with 100 connectionless "testagent" entries
and wiping all real spawn history.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:46:19 -04:00
L
e182806eee
fix: graceful recovery from corrupted history.json (#2391)
* fix: graceful recovery from corrupted history.json

- Atomic writes (write to .tmp, rename into place) to prevent corruption
- Backup corrupted files with .corrupt suffix before discarding
- Per-record salvaging: if some v1 records are malformed, keep the valid ones
- Archive recovery: when history.json is corrupted, try loading from archives
- Stderr warnings when corruption is detected or records are recovered

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

* refactor: replace try/catch with Result tryCatch wrapper in history.ts

Add tryCatch() to shared/result.ts and use it throughout history.ts to
eliminate all 7 try/catch blocks. Errors are now handled via Result
pattern matching instead of exception control flow.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-09 14:50:29 -07:00
A
d73027eed4
fix(status): guard against empty serverId to avoid list-all-servers API calls (#2392)
When both server_id and server_name are missing from a connection record,
serverId falls back to "". Passing "" to fetchHetznerStatus/fetchDoStatus
constructs URLs like /v1/servers/ (list all), wasting rate-limit quota and
sending auth tokens to the wrong endpoint. Early-return "unknown" instead.

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-09 17:46:29 -04:00
Ahmed Abushagur
05f744e052
fix: atomic single-save for history records (createServer returns VMConnection) (#2388)
The two-phase save architecture was fundamentally broken: saveVmConnection()
was called inside createServer() BEFORE saveSpawnRecord() created the record,
so the merge-by-spawnId silently failed every time — resulting in records
with no connection data and `spawn ls` showing nothing.

Replace with atomic single-save: createServer() now returns VMConnection,
and the orchestrator calls saveSpawnRecord() once with connection data
included. Removes saveVmConnection(), getConnectionPath(),
mergeLastConnection(), and last-connection.json entirely.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-09 14:32:45 -07:00
L
d9a25a4720
fix: ESC/Ctrl-C in picker falls back to numbered list instead of cancelling (#2390)
The TTY key loop treated explicit user cancellation (ESC/Ctrl-C) the same
as a TTY failure — both called fallback() which renders a numbered-list
picker. Now the key loop distinguishes between the two: cancel() exits
cleanly, fallback() is only used when /dev/tty is unavailable.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-09 14:28:02 -07:00
A
b8ca943592
test: consolidate repetitive check-entity tests into data-driven loops (#2389)
Replace 30+ individual it() blocks that each tested a single typo input
with data-driven loops using arrays of test cases. Same coverage, less
boilerplate. Reduces check-entity.test.ts from 401 to 330 lines.

Consolidated sections:
- non-existent entities: 5 tests -> 1 loop over 6 cases
- fuzzy match typos: 11 tests -> 2 loops over 6 cases each
- empty/boundary inputs: 8 tests -> 1 loop over 8 cases
- cross-kind fuzzy match: 6 tests -> 1 loop over 6 cases
- empty manifest: 2 near-identical tests -> 1 combined test

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 16:53:08 -04:00
Ahmed Abushagur
e38f4483d6
fix: align cloud defaults with manifest (DO size, Hetzner location) (#2387)
DO default was s-2vcpu-4gb which isn't available in nyc3, causing 422
errors. Changed to s-2vcpu-2gb to match manifest.json. Also aligned
Hetzner default location from nbg1 to fsn1 to match manifest.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:23:22 +00:00
A
26d95e54bc
security: validate SPAWN_INSTALL_DIR against path traversal (Fixes #2385) (#2386)
Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 10:55:00 -07:00
A
9af9d5669b
docs: add spawn status commands to README commands table (#2381)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-09 10:38:23 -07:00
A
fb6d13d5d2
test: consolidate duplicate security validation tests (#2382)
Merge security-edge-cases.test.ts and security-encoding.test.ts into
security.test.ts. Move stripDangerousKeys tests to manifest.test.ts
(where the function is defined). All 1447 tests pass, zero regressions.

-- qa/dedup-scanner

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-09 10:37:01 -07:00
A
b87361bd27
refactor: remove dead code and unnecessary exports (#2376)
- Remove unused multiPickToTTY function, MultiPickOption interface, and
  MultiPickConfig interface from picker.ts (never called anywhere)
- Remove export keyword from 7 internal-only functions in commands/shared.ts
  that are used within the file but never imported externally:
  getEntityCollection, getEntityKeys, formatAuthVarLine,
  hasCloudConfigCredentials, getCredentialGuidance,
  checkAllCredentialsReady, printAuthVariableStatus

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-09 13:25:50 -04:00
L
5a86c4fc28
feat: migrate ori-basic rendering improvements into SPA bot (#2383)
Port all core architectural and rendering upgrades from ori-basic into
the setup-spa skill, bringing it to full parity.

## helpers.ts
- Replace JSON state (loadState/saveState/slack-issues.json) with SQLite
  (openDb/findThread/upsertThread/updateThread) using WAL mode and
  busy_timeout; add migrateFromJson() for legacy data
- Add full rich_text rendering pipeline: parseInlineMarkdown(),
  parseMarkdownBlock(), markdownToRichTextBlocks() — renders bold, italic,
  code, links, strikethrough, bullet/ordered lists, blockquotes, headers,
  fenced code blocks without Slack "See more" collapse
- Add extractMarkdownTables() + markdownTableToSlackBlock() for native
  Slack table blocks
- Add plainTextFallback() for push notification text
- Add PR_URL_REGEX constant
- Add flattenToolResultContent() for web_search_tool_result array content
- Update extractToolHint to handle query and url fields
- Update formatToolHistory to use emoji format:  *Name* `hint`
- Add tableBlocks field to SlackSegment interface

## main.ts
- Remove SLACK_CHANNEL_ID restriction — bot now responds in any channel + DMs
- Replace JSON state with SQLite throughout
- Add pendingQueues Map for FIFO concurrent message handling (no more dropped messages)
- Add buildPlanBlock() — structured task display with in_progress/complete
  status for all tools, interleaved with text via commitSegment()
- Replace mrkdwn section blocks with rich_text blocks via markdownToRichTextBlocks()
- Add overflow posting: when >47 blocks, extra content posts as follow-up messages
- Add firePrButtonIfNew() + buildPrButtonBlock() for immediate PR buttons during streaming
- Add cancel button (ActionsBlock) + cancel_run action handler + SIGTERM on process
- Add DM event handler (message.im channel_type)
- Track userId for thread state; pass SLACK_USER_ID to Claude subprocess env
- End-of-run: await prButtonPromise, delete mid-stream button, repost push-to-latest

## spa.test.ts
- Add SQLite tests (openDb, upsertThread, findThread, idempotency)
- Add parseInlineMarkdown tests (bold, code, link, italic, strikethrough, mixed)
- Add parseMarkdownBlock tests (paragraph, bullet list, ordered list, blockquote, header)
- Add markdownToRichTextBlocks tests (empty, plain, code fences, multiple fences)
- Add plainTextFallback tests
- Add extractMarkdownTables + markdownTableToSlackBlock tests
- Add web_search_tool_result handling test
- Update formatToolHistory + extractToolHint tests for new format
- Total: 94 tests, 0 fail

## package.json
- Add @slack/types and @slack/web-api dependencies (needed for Block types)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 10:24:31 -07:00
Ahmed Abushagur
7bab1c3289
fix: set browser.defaultProfile to openclaw for managed browser mode (#2384)
On headless VMs there's no Chrome extension to attach to. Setting
defaultProfile to "openclaw" tells OpenClaw to launch and manage
the browser itself via CDP instead of waiting for an extension relay.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:23:23 -04:00
A
f81ef1da4c
fix(status): add -a/--agent and -c/--cloud filter flags to spawn status (#2379)
`spawn status` silently ignored -a and -c flags, showing all servers
regardless. This is inconsistent with `spawn list` and `spawn delete`
which both support these filters.

- Update `cmdStatus` to accept `agentFilter`/`cloudFilter` options and
  pass them to `filterHistory()`
- Update `dispatchStatusCommand` to parse filter flags using the shared
  `parseListFilters` helper (same as list/delete)
- Document filter flags in help text for `spawn status`
- Bump version to 0.15.27

Fixes #2377

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-09 07:10:05 -07:00
A
2074211d13
fix: wire maxAttempts parameter in waitForCloudInit for hetzner and digitalocean (#2380)
The `_maxAttempts` parameter in both Hetzner and DigitalOcean's
`waitForCloudInit()` was silently ignored — loop bounds and early-exit
checks were hardcoded. Rename to `maxAttempts` and use it consistently,
matching the AWS/GCP implementations.

Fixes #2378

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-09 09:35:43 -04:00
A
f23da1523b
fix(security): fail on chmod error in github-auth.sh token persistence (#2375)
Remove `|| true` from chmod call that restricts token file permissions.
If chmod fails, authentication now aborts with an error instead of
silently leaving ~/.config/gh/hosts.yml world-readable.

Fixes #2374

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-09 08:18:07 -04:00
A
882f404bb1
test: consolidate duplicate function calls in test assertions (#2373)
Merge 9 test cases that called the same function with the same arguments
into adjacent tests, each checking a different assertion. Consolidated
them into single tests that verify all assertions in one call, removing
redundant setup/teardown overhead.

Files changed:
- commands-error-paths.test.ts: merge unknown agent/cloud and unimplemented combo tests
- commands-cloud-info.test.ts: merge unknown cloud error + suggestion tests
- commands-resolve-run.test.ts: merge many-clouds suggestion and no-clouds tests
- commands-name-suggestions.test.ts: merge display name suggestion + error tests

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 04:49:53 -04:00
A
68f3590c7b
fix(security): base64-encode cmd in _sprite_exec() to prevent command injection (#2371)
* fix(security): base64-encode cmd in _sprite_exec to prevent injection

Applies base64 encoding to both _sprite_exec() and _sprite_exec_long()
so that shell metacharacters in the cmd parameter cannot break out of
context during remote execution on Sprite instances. The command is
base64-encoded locally and decoded on the remote side before execution.

Fixes #2369

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

* revert: restore stdin-piping approach per security review feedback

The base64 approach introduced ${_encoded} interpolation into shell context,
which is less secure than the existing stdin-piping approach on main.
Restores the original secure pattern: pipe cmd via stdin to avoid interpolation.

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-09 03:51:41 -04:00
Ahmed Abushagur
4004b51f6d
fix: use curl for Chrome download + capture google-chrome-stable in tarball (#2370)
- wget not available on many cloud VMs, use curl instead
- Remove 2>/dev/null from dpkg/apt so install errors are visible
- Capture /usr/bin/google-chrome-stable in tarball (actual .deb binary name)
- Use curl in packer/agents.json tarball build too

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-08 23:59:32 -07:00
Ahmed Abushagur
24a3c7328d
feat: show cloud prices as lead indicator (#2347)
* feat: show cloud prices as lead indicator, default OpenClaw to Kimi K2.5

- Add `price` field to all clouds in manifest.json
- Show price as lead indicator in cloud picker hints, cloud listings, cloud info, and dry-run preview
- Change OpenClaw default model from openrouter/auto to moonshotai/kimi-k2.5 (top used model by OpenClaw users)

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

* fix: add defensive guards for undefined cloud price in cached manifests

When users upgrade CLI but have cached manifests from before the price
field was added, c.price is undefined. Add ?? "" fallbacks and an
if-guard to prevent runtime crashes.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-08 23:41:39 -07:00
A
c61852d689
test: remove duplicate findClosestMatch tests from commands-name-suggestions (#2356)
The findClosestMatch unit tests (distance matching, case insensitivity,
null for distant strings, closest-among-multiple) were duplicated between
commands-name-suggestions.test.ts and fuzzy-key-matching.test.ts. Remove
the redundant section from commands-name-suggestions.test.ts since
fuzzy-key-matching.test.ts is the dedicated unit test file for that
function. The integration tests via cmdRun/cmdAgentInfo/cmdCloudInfo
remain in commands-name-suggestions.test.ts.

-- 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-08 23:40:15 -07:00
Ahmed Abushagur
7e2f9f45fc
fix: use Google Chrome .deb for OpenClaw browser tool (#2368)
* fix: use Google Chrome .deb instead of Playwright for OpenClaw browser

Snap Chromium on Ubuntu 24.04 fails because AppArmor confinement blocks
CDP control. OpenClaw's own docs recommend installing Google Chrome via
.deb package which bypasses snap entirely.

Also adds browser.noSandbox and browser.executablePath to the OpenClaw
config so the browser tool works out of the box on Linux VMs.

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

* fix: remove unnecessary confirmation prompt when OAuth fails

If OAuth didn't complete, the user obviously wants to paste a key.
The "Paste your API key manually? (Y/n)" prompt was pointless friction.

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

* fix: remove unnecessary "Continue anyway?" credential confirmation

If the user selected a cloud, they obviously want to continue.
The warning + setup guidance is sufficient — no need to block on a confirm.

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

* fix: move Chrome install to configure step so it runs after tarball

The tarball path skips agent.install() entirely, so Chrome never got
installed. Moving it to configure() (setupOpenclawConfig) ensures it
always runs regardless of install method.

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

* feat: bundle Google Chrome in openclaw tarball

Add Chrome .deb install to openclaw's tarball build so it ships
pre-installed. Capture /usr/bin/google-chrome and /opt/google/chrome/
in the tarball. Add dl.google.com to the workflow domain allowlist.

The configure() step still has a fallback install with idempotency
check (command -v google-chrome) for non-tarball installs.

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

* fix: use openclaw config set for browser setup + correct binary name

- Use `google-chrome-stable` (actual .deb binary name) not `google-chrome`
- Set browser config via `openclaw config set` CLI (the supported way)
  instead of writing JSON directly which wasn't being picked up
- Remove browser section from JSON config to avoid conflicts

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 01:52:08 -04:00
Ahmed Abushagur
3c029be108
Revert "feat: wrap cloud VM sessions in tmux for persistence (#2358)" (#2366)
This reverts commit e855790a5d.

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

Fixes #2363

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 22:10:15 -07:00
A
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