Commit graph

163 commits

Author SHA1 Message Date
Ahmed Abushagur
39622b68ab
feat: add --beta images for DO marketplace images (#2593)
* feat: add --beta images for DO marketplace images

Gate pre-built DigitalOcean marketplace images behind --beta images.
When active, uses hardcoded marketplace slugs (e.g. openrouter-spawnclaude)
instead of fresh Ubuntu + cloud-init, skipping agent install entirely.

All 8 images verified working via e2e smoke test (2026-03-13).

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

* fix: sort exports to satisfy biome organizeImports

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 15:45:25 -04:00
A
8f02646b4c
feat: add spawn feedback subcommand (#2585)
* feat: add `spawn feedback` subcommand

Sends anonymous feedback to the Spawn team via PostHog survey API.
Usage: spawn feedback "your message here"

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

* fix: update feedback survey ID and response key

Use the correct PostHog survey ID and $survey_response property.

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

* fix: use asyncTryCatch instead of try/catch in feedback command

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>
2026-03-13 10:19:37 -07:00
A
d1bbd6cac9
refactor: remove dead parameters from internal functions (#2581)
Remove 5 unused underscore-prefixed parameters that were accepted but
never read: extractFlagValue._flagLabel, performUpdate._remoteVersion,
reportDownloadFailure._primaryUrl/_fallbackUrl, buildRecordLabel._manifest,
and setupCodexConfig._apiKey. All callers updated accordingly.

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-13 09:55:03 -07:00
A
cb0ed08da0
security: add shell quoting around TERM in cloud module commands (#2579)
Defense-in-depth: wrap sanitized TERM values in single quotes in all
four SSH-based cloud modules (aws, hetzner, digitalocean, gcp). The
allowlist in sanitizeTermValue() already prevents injection, but quoting
the interpolated value adds a second layer of protection.

Also extends test coverage with additional injection vectors (pipes,
redirects, variable expansion, empty strings) and a test verifying the
complete allowlist.

Fixes #2577

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-13 08:17:46 -04:00
A
13538cfa98
fix: re-assert gateway auth token after openclaw browser config set calls (#2571)
Each `openclaw config set` does a read-modify-write on the config file,
which can drop fields written by uploadConfigFile — including
gateway.auth.token. This caused the OpenClaw dashboard to return
"Unauthorized" on every fresh deploy.

Fix: after the browser config set and plugin enable blocks, re-set
gateway.auth.token via `openclaw config set` (same non-fatal pattern as
the existing Telegram token call), ensuring the token survives all
read-modify-write cycles.

Fixes #2570

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-13 04:17:34 -04:00
Ahmed Abushagur
6c8c098ba7
fix: enable OpenClaw channel plugins before configuring them (#2564)
Telegram and WhatsApp plugins are disabled by default in OpenClaw.
Setting a bot token without enabling the plugin causes the gateway
to hang on startup. Running `openclaw channels login --channel
whatsapp` without the plugin enabled fails with "Unsupported channel".

Now runs `openclaw plugins enable telegram/whatsapp` before any
channel configuration. Also adds step-by-step instructions for
getting a Telegram bot token from @BotFather.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-13 03:50:22 -04:00
A
6839e34395
fix: remove duplicate --model flag from help and error output (#2562)
The --model flag was listed twice in two user-facing outputs:
- help.ts USAGE section: lines 11 and 20 both showed --model <id>
  with different descriptions
- index.ts unknown-flag error: lines 118 and 121 both showed --model
  with different descriptions

Both duplicates were introduced when --model support was added.
Combined the two entries into one clear line each.

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-13 02:50:36 -04:00
A
d578e614e2
refactor: remove dead HeadlessOptions re-export from commands barrel (#2560)
HeadlessOptions is defined and used internally in commands/run.ts but
re-exported from commands/index.ts with no consumer — index.ts imports
cmdRunHeadless but passes options inline without importing the type.
This is a CLI binary, not a library, so unused re-exports add surface
area without value.

Also move the run.ts comment to be adjacent to the run.ts exports.

Bump CLI version to 0.17.4.

-- qa/code-quality

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-12 22:37:55 -07:00
Ahmed Abushagur
8a5908acd2
fix: add step-by-step instructions for getting a Telegram bot token (#2558)
New users don't know how to get a bot token. Show instructions
before the prompt: open @BotFather, send /newbot, copy the token.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 23:03:13 -04:00
A
ecc876f3bc
fix: remove dead shellQuote re-export from gcp/gcp.ts (#2551)
Dead backwards-compat re-export left over from the shellQuote
consolidation (PRs #2533, #2535, #2546). Zero consumers import
shellQuote from gcp/gcp.ts — all correctly import from shared/ui.ts.
Per CLAUDE.md: avoid backwards-compatibility hacks; delete unused code.

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-12 21:42:09 -04:00
A
2b83a8106d
security: use shellQuote() in agent-setup.ts for consistent null-byte defense (#2546)
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-12 19:44:50 -04:00
Ahmed Abushagur
d2d71b17ef
feat: add --model flag and preferences file for LLM model override (#2543)
Adds --model / -m CLI flag to override the agent's default LLM model:
  spawn codex gcp --model openai/gpt-5.3-codex

Also supports persistent per-agent model preferences via config file at
~/.config/spawn/preferences.json:
  { "models": { "codex": "openai/gpt-5.3-codex" } }

Priority: --model flag > preferences file > agent default.

This enables a future web UI to pass model selection via CLI args when
invoking spawn programmatically to provision machines.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-12 18:47:09 -04:00
A
f6f36cc452
security: add DO_CLIENT_SECRET env var override (#2538)
* security: add DO_CLIENT_SECRET env var override

Allows users/organizations to supply their own DigitalOcean OAuth
client secret via DO_CLIENT_SECRET env var rather than relying on
the bundled default. The bundled secret remains as fallback.

Fixes #2537

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

* chore: bump CLI version to 0.16.19

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-12 15:48:36 -04:00
A
dfd08ad48c
security: consolidate shellQuote across all clouds (defense-in-depth) (#2535)
PR #2533 hardened GCP with shellQuote() and null-byte rejection, but
left Hetzner, DigitalOcean, AWS, and connect.ts using inline
.replace(/'/g, "'\\''") without null-byte validation.

- Move shellQuote to shared/ui.ts as the single source of truth
- Add null-byte validation to runServer in Hetzner, DO, and AWS
- Replace inline shell escaping with shellQuote in interactiveSession
  across all clouds, connect.ts, and agents.ts buildEnvBlock
- Re-export shellQuote from gcp.ts for backwards compatibility

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-12 12:54:31 -04:00
A
868ebbe4fe
security: harden shellQuote and consolidate shell escaping in gcp.ts (#2533)
- Add null-byte rejection to shellQuote (defense-in-depth)
- Export shellQuote for testability
- Refactor interactiveSession to use shellQuote instead of inline escaping
- Add comprehensive test suite for shellQuote security properties

Fixes #2529

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-12 10:27:48 -04:00
A
7278638a31
security: validate localPath in uploadFile() and harden runServer() in gcp.ts (#2524)
Fixes #2521 - Add path traversal and argument injection protection for localPath
Fixes #2522 - Add validation for cmd parameter before SSH execution

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-12 04:50:56 -04:00
Ahmed Abushagur
553cbad7bf
fix: revert OpenClaw default model to openrouter/auto (#2509)
OpenClaw requires the openrouter/ provider prefix for model IDs.
The previous default (moonshotai/kimi-k2.5) was missing the prefix,
causing "Unknown model" warnings. Reverted to openrouter/openrouter/auto
which uses OpenRouter's auto-router to pick the best model per prompt.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-12 01:06:50 -04:00
Ahmed Abushagur
b548c5b75a
fix: only pre-select Chrome browser in setup picker (#2512)
#2507 pre-selected all setup options. Only browser should default to
enabled — GitHub CLI and reuse-saved-key are opt-in.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 23:05:31 -04:00
Ahmed Abushagur
aa6e7dd1fc
fix: default all setup options to enabled in picker (#2507)
The multiselect picker for setup options (Chrome browser, GitHub CLI,
etc.) started with nothing selected. Now all available options are
pre-selected so users get the full setup by default.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 19:43:03 -04:00
A
65a2efd5ba
fix: gcp use root SSH user instead of whoami (#2503)
The `resolveUsername()` function called `whoami` and validated against a
regex that rejected dots in usernames (e.g. `adrian.hale`), causing
"Invalid username" errors. All other clouds use a static SSH user
(root for Hetzner/DO, ubuntu for AWS).

Switch GCP to use `root` consistently:
- Replace dynamic `whoami` lookup with static `GCP_SSH_USER = "root"`
- Simplify cloud-init startup script (already runs as root)
- Fix bun symlink path to use /root instead of /home/${username}
- Remove unused `username` field from GcpState

Closes #2502

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-11 13:48:49 -07:00
A
150d094ef2
fix: fallback to manual project entry when gcloud projects list fails (#2500)
* fix: fallback to manual project entry when gcloud projects list fails

When the user declines the suggested default GCP project and
`gcloud projects list` fails (e.g. lacking resourcemanager.projects.list
permission), prompt for a manual project ID instead of hard-failing.

Also fix selectFromList() to return "" on cancel (Ctrl+C/Escape) rather
than defaultValue, so canceling a project picker is treated as "no
selection" rather than silently re-using the first project.

Fixes #2499

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

* fix: add GCP project ID format validation for manual entry

Validates user-entered GCP project IDs against the required format
(^[a-z][a-z0-9-]{4,28}[a-z0-9]$) before accepting them. Invalid
entries are rejected with a helpful message and the user is re-prompted.

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-11 15:47:53 -04:00
A
479cbbc009
fix: pass --skip-setup to hermes installer for headless installs (#2496)
The Hermes Agent installer's setup wizard tries to read from /dev/tty,
which fails in headless/non-interactive cloud VM environments. The
installer supports --skip-setup to bypass the wizard; pass it via
bash -s -- --skip-setup.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 09:33:27 -04:00
A
5031d84e6c
refactor: eliminate process-global mock.module() pollution in tests (#2490)
Replace mock.module() calls with dependency injection to prevent
cross-file test pollution in Bun's shared worker process. Changes:

- orchestrate.ts: add getApiKey to OrchestrationOptions
- billing-guidance.ts: add injectable BillingGuidanceDeps parameter
- delete.ts: add optional deleteHandler parameter to confirmAndDelete
- update.ts: add UpdateOptions with injectable runUpdate function
- sprite.ts: add optional spawnFn parameter to interactiveSession
- Remove unnecessary oauth mocks from junie-agent and do-snapshot tests

Only @clack/prompts mock (shared via test-helpers.ts) and
do-payment-warning.test.ts (safe spread pattern) remain.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 23:57:57 -07:00
A
6439cba58c
fix: remove spinner from delete to prevent output overlap (#2487)
* fix: remove spinner from delete command to prevent output overlap

The delete spinner in confirmAndDelete collided with cloud-specific
destroy functions that print their own progress (logStep/logInfo).
This caused the "Instance destroyed" message to overwrite the spinner
line without a newline, producing garbled output.

Remove the spinner and let the cloud destroy functions handle progress
output directly, then show a clean success/failure message after.

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

* fix: redirect cloud destroy output into delete spinner

Cloud destroy functions (logStep/logInfo) write progress to stderr,
which collided with the @clack spinner on the terminal. Now stderr
writes during the delete are intercepted and fed into s.message()
so the spinner text updates in place instead of garbling the output.

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

* test: add delete spinner behavior tests

Verify that confirmAndDelete:
- Feeds stderr output from cloud destroy functions into spinner.message()
- Calls spinner.clear() (not stop) so no spinner chrome remains
- Shows p.log.success with the last stderr message as detail
- Shows p.log.error on failure
- Always restores process.stderr.write, even on error
- Works when destroy produces no stderr output

Also adds spinnerClear to the shared test-helpers mock.

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

* fix: remove global cloud module mocks that polluted other tests

Only mock hetzner (the cloud used by test records). Other cloud modules
are left un-mocked since they're never called for hetzner records. This
fixes the DO payment warning test failures caused by mock.module being
process-global in Bun.

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 23:35:12 -07:00
A
0e265d65d7
fix: use parseJsonObj instead of JSON.parse to prevent SyntaxError crashes on corrupted config (#2486)
Five call sites wrapped JSON.parse inside tryCatchIf(isFileError), causing
SyntaxError (from corrupted JSON) to escape uncaught since SyntaxError has no
.code property. Replace with parseJsonObj() which catches SyntaxError internally
and returns null, restoring graceful recovery.

Affected: loadApiToken(), loadSavedOpenRouterKey(), readCache(),
tryLoadLocalManifest(), hasCloudConfigCredentials()

Fixes #2485

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-11 01:27:07 -04:00
A
9a1dad7fcb
feat: gate tarball install behind --beta=tarball flag (#2482)
* feat: gate tarball install behind --beta=tarball flag

Tarball install is not yet reliable enough to be the default.
Move it behind an opt-in --beta=tarball flag so users can test it
explicitly while live install remains the default path.

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

* feat: support multiple --beta flags (repeatable)

Parse all --beta flags from args in a loop, collecting them into a
comma-separated SPAWN_BETA env var. Consumers check for their feature
with Set.has() so multiple beta features can be active simultaneously.

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

* refactor: replace for(;;) loop with extractAllFlagValues helper

Cleaner approach: a dedicated helper mutates args in place and returns
all values for a repeatable flag, replacing the infinite loop pattern.

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 21:24:51 -07:00
A
014d591e68
refactor: convert remaining 5 try/catch blocks to Result helpers (#2480)
Convert the last convertible catch blocks:
- digitalocean.ts: SSH key registration fallback
- sprite.ts: keep-alive soft-dependency install
- agent-tarball.ts: tarball metadata fetch fallback
- list.ts: enter/reconnect connection error recovery (2 blocks)

The remaining ~43 try blocks are all try/finally cleanup (21),
security/billing validation (10), or top-level handlers — none
are candidates for Result helper conversion.

Bumps CLI to 0.16.5.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
2026-03-10 23:01:10 -04:00
A
a7a2032584
refactor: replace ~50 try/catch blocks with Result helpers across 20 files (#2479)
Convert catch-all, catch-swallow, catch-return-fallback, and catch-classify
patterns to use tryCatch/asyncTryCatch/unwrapOr from @openrouter/spawn-shared.

Files changed: aws.ts, hetzner.ts, digitalocean.ts, gcp.ts, run.ts, delete.ts,
shared.ts, ssh.ts, agent-setup.ts, orchestrate.ts, ui.ts, index.ts,
update-check.ts, update.ts, status.ts, picker.ts, interactive.ts, list.ts,
pick.ts, ssh-keys.ts, billing-guidance.ts, oauth.ts, sprite.ts

Preserved all try/finally-only blocks, security-validation-exit blocks,
billing/classify blocks, spinner cleanup, and top-level handleError blocks.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
2026-03-10 19:26:41 -07:00
A
5289a87043
fix: use asyncTryCatch for tarball install + add chown ownership fix (#2478)
Replace try/catch in agent-tarball.ts with asyncTryCatch Result helpers:
- Phase 3 (download/extract): asyncTryCatch → returns false on any failure
- Phase 4 (mirror): asyncTryCatch → non-fatal, logs warning on failure

Add chown ownership fix for non-root SSH users (GCP, AWS Lightsail):
files extracted as root need ownership corrected after mirroring.

Add 5 anti-regression tests for non-root home directory mirroring.

Supersedes #2466.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 19:04:20 -07:00
A
3fd17e3d1d
refactor: replace indiscriminate try/catch with guarded Result helpers (#2477)
Add tryCatchIf/asyncTryCatchIf with error predicates (isFileError,
isNetworkError, isOperationalError) so operational errors are handled
explicitly while programming bugs (TypeError, ReferenceError) propagate
and crash visibly instead of being silently swallowed.

Transforms ~40 try/catch blocks across 14 files:
- File I/O (manifest cache, config loading, history) → tryCatchIf(isFileError)
- Network/fetch (API calls, version checks, OAuth) → asyncTryCatchIf(isNetworkError)
- SSH/subprocess (agent setup, tunnel) → asyncTryCatchIf(isOperationalError)
- API retry loops (DO, Hetzner) → guard retries with isNetworkError

Intentionally keeps ~85 try/catch blocks as-is (cleanup/finally, retry
loops, user-facing error handlers, catch-classify-rethrow patterns).

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 18:55:07 -07:00
A
b3938144b7
fix: validate model ID before shell interpolation (fixes #2460) (#2472)
Add validateModelId() to reject model IDs containing shell metacharacters.
The validation is applied in orchestrate.ts immediately after resolving
MODEL_ID from env/agent defaults, before the value reaches any agent
configure function or runServer call. Invalid model IDs are dropped to
undefined with a warning.

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-10 20:31:32 -04:00
Ahmed Abushagur
d82dea811d
feat: unified arrow-key selection + setup checkboxes (#2459)
* feat: unified arrow-key selection + setup checkboxes

Replace p.autocomplete (type-ahead) with p.select (arrow-key navigation)
for agent and cloud selection. Add p.multiselect checkboxes for optional
post-provision setup steps (GitHub CLI, Chrome browser), all ON by default.

Three fast prompts: agent → cloud → setup options. Defaults: OpenClaw,
first cloud with credentials, all steps enabled.

Key changes:
- interactive.ts: p.autocomplete → p.select with initialValue defaults
- interactive.ts: promptSetupOptions() with p.multiselect, exported for reuse
- run.ts: wire setup options into cmdRun direct path
- agents.ts: OptionalStep type, getAgentOptionalSteps() static metadata
- orchestrate.ts: read SPAWN_ENABLED_STEPS env var, gate GitHub auth + configure
- agent-setup.ts: gate Chrome install with enabledSteps in setupOpenclawConfig
- Version bump 0.15.40 → 0.16.0

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

* fix: mirror tarball files to $HOME for non-root SSH users (GCP, AWS)

Tarballs are built with absolute /root/ paths, but GCP and AWS Lightsail
SSH as a regular user whose $HOME is /home/<user>/. After extraction,
binaries like `claude` end up at /root/.claude/local/bin/ but the
launchCmd looks in $HOME/.claude/local/bin/ — causing "command not found".

Add a post-extraction step that copies /root/ dotfiles to $HOME/ when
the SSH user isn't root. This fixes `spawn claude gcp` failing with
exit code 127 after tarball install.

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-10 14:19:08 -07:00
A
5db9cc2a80
fix: show history table directly when no active servers found in spawn list (#2451)
Instead of telling users to pipe through `spawn list | cat` to view their
spawn history, render the history table inline when no active connections
exist. The | cat workaround was needed because non-interactive mode skips
the picker; now interactive mode falls through to renderListTable directly,
consistent with what `spawn list | cat` was already doing.

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-10 15:21:00 -04:00
A
a46a92a8a4
fix: add missing PATH entries in Hetzner and DigitalOcean runServer/interactiveSession (#2450)
AWS and GCP both include $HOME/.npm-global/bin and $HOME/.claude/local/bin in the
PATH exported before running remote commands. Hetzner and DO were missing these two
entries, causing "command not found" errors for Claude Code and npm-global packages
on those clouds.

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-10 14:24:16 -04:00
A
0380ad33f9
refactor: remove dead exports only used within their own files (#2431)
- withSpinner in commands/shared.ts
- ENTITY_DEFS in commands/shared.ts
- isValidManifest in manifest.ts
- waitForInstance in aws/aws.ts
- SignalEntry, ExitCodeEntry in guidance-data.ts

Bump version: 0.15.37 -> 0.15.38

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-10 08:51:15 -04:00
A
15e4715555
fix: validate server ID in status.ts before API calls (#2430)
status.ts passed server_id from history directly into Hetzner/DO API
URLs without calling validateServerIdentifier(). Both delete.ts and
connect.ts validate first; status.ts was the only gap. A tampered
~/.spawn/history.json could craft a server_id with path traversal
characters (e.g. "../v2/account") causing the Bearer token to be
sent to an unintended API endpoint (SSRF via URL path manipulation).

Fix: call validateServerIdentifier() after extracting serverId,
returning "unknown" gracefully on failure.

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-10 07:17:07 -04:00
A
72ccb098ab
feat: integrate Sprite keep-alive tasks for all Sprite agents (#2428)
Adds sprite-keep-running support so sprites stay alive during long
agent sessions instead of shutting down due to inactivity.

- Add installSpriteKeepAlive() to sprite/sprite.ts: downloads and
  installs the sprite-keep-running script (~/.local/bin) on the sprite
  during setup. Non-fatal: logs a warning if download fails so
  deployment still proceeds.

- Modify interactiveSession() to wrap the session command in a temp
  script (base64-encoded to handle multi-line restart loops) and exec
  it via sprite-keep-running if available, with plain bash fallback.

- Call installSpriteKeepAlive() in sprite/main.ts createServer() step
  after setupShellEnvironment(), applying to all Sprite agents.

- Add sprite-keep-alive.test.ts: 11 unit tests covering download URL,
  install path, error resilience, session script structure, and
  keep-alive wrapper inclusion.

Fixes #2424

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-10 02:24:18 -07:00
A
de76599b39
refactor: centralize path resolution into shared/paths.ts (#2422)
Move all filesystem path helpers (getUserHome, getSpawnDir, getHistoryPath,
getSpawnCloudConfigPath, getCacheDir, getCacheFile, getUpdateFailedPath,
getSshDir, getTmpDir) into a single shared/paths.ts module. This eliminates
scattered homedir()/process.env.HOME patterns across 8+ files and provides
a single import source for all path resolution.

- Create packages/cli/src/shared/paths.ts with 9 exported functions
- Update 17 source files to import from paths.ts
- Add re-exports in ui.ts and history.ts for backward compatibility
- Remove direct homedir() imports from gcp, sprite, local, ssh-keys, etc.
- Add comprehensive unit tests in paths.test.ts
- Bump CLI version to 0.15.34

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 00:48:03 -07:00
A
486aba49f6
fix: use process.env.HOME instead of os.homedir() for test sandboxing (#2417)
Bun's os.homedir() reads from getpwuid() and ignores runtime changes to
process.env.HOME. Named imports capture the native function binding, so
patching os.homedir on the default export doesn't propagate. This caused
all test files using homedir() to write .spawn-test-* dirs to the real
home directory instead of the preload sandbox.

Add getUserHome() helper to shared/ui.ts that prefers process.env.HOME,
replace all direct homedir() calls in production and test code.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-10 00:20:19 -07:00
A
f272294902
refactor: Deduplicate getServerName and promptSpawnName across cloud modules (#2415)
Consolidates duplicate server naming logic from 5 cloud modules into shared utilities in src/shared/ui.ts. No behavioral changes - purely structural refactor.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-10 05:26:25 +00:00
L
2da9e6cd46
refactor: restore @openrouter/spawn-shared workspace package (#2405)
* refactor: restore @openrouter/spawn-shared workspace package

Restore packages/shared/ as canonical location for parse.ts, result.ts,
and type-guards.ts. CLI shared files become thin re-exports, preserving
all existing import paths. SPA imports switch from fragile relative paths
to the workspace package.

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

* fix: sort exports in shared package barrel to satisfy biome

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

* fix: sort SPA imports to satisfy biome organizeImports

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-09 17:14:26 -07: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
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
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
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
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
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
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
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