Commit graph

346 commits

Author SHA1 Message Date
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
9bb39a213a
test: remove theatrical tests from manifest-integrity (#2552)
Remove 2 tests from the manifest-integrity.test.ts "structure" describe
block that can never fail:

- "should parse as valid JSON": manifest.json is already parsed via
  JSON.parse() at module scope (line 23). If parsing fails, the module
  throws and ALL tests fail — this individual test can never provide
  an independent failure signal.

- "should have agents, clouds, and matrix top-level keys": after parsing,
  Object.keys(manifest.agents/clouds) and Object.entries(manifest.matrix)
  are called at module scope (lines 25-27). If those properties were
  missing, the module load itself would throw. This test is also guaranteed
  to pass whenever any test in the file runs.

Removing these 2 theatrical tests leaves 1403 tests (down from 1405).
All remaining tests provide real signal.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 21:41:03 -04:00
Ahmed Abushagur
f683dd857b
feat: add --config and --steps CLI flags for programmatic setup (#2545)
* feat: add Telegram and WhatsApp options to OpenClaw setup picker

Adds separate "Telegram" and "WhatsApp" checkboxes to the OpenClaw
setup screen:

- Telegram: prompts for bot token from @BotFather, injects into
  OpenClaw config via `openclaw config set`
- WhatsApp: reminds user to scan QR code via the web dashboard
  after launch (no CLI setup possible)

Updates USER.md with channel-specific guidance when either is selected.

Bump CLI version to 0.16.16.

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

* feat: run WhatsApp QR scan interactively before TUI launch

Instead of punting WhatsApp setup to "after launch", runs
`openclaw channels login --channel whatsapp` as an interactive SSH
session between gateway start and TUI launch. The user scans the
QR code with their phone during provisioning setup.

Flow: gateway starts → tunnel set up → WhatsApp QR scan → TUI launch

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

* fix: update WhatsApp hint to reflect pre-TUI QR scanning

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

* feat: add --config and --steps CLI flags for programmatic setup

Add --config <path> flag to load spawn options from a JSON config file
(model, steps, name, setup data like telegram_bot_token). Add --steps
<list> flag for comma-separated setup step control. Both enable the
web UI and headless automation to control which setup steps run.

Priority order: CLI flags > --config file > env vars > defaults.

- New spawn-config.ts module with valibot validation
- OptionalStep extended with dataEnvVar and interactive metadata
- validateStepNames() for step name validation with warnings
- Telegram setup reads TELEGRAM_BOT_TOKEN env var before prompting
- WhatsApp auto-skipped in headless mode with warning
- promptSetupOptions() skipped when SPAWN_ENABLED_STEPS already set
- E2E verify helpers for github, browser, telegram setup artifacts
- QA reference file documenting all agent setup options
- Version bump to 0.17.0

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

* feat: add --model flag and priority order tests

- Add --model <id> CLI flag that sets MODEL_ID env var
- --model is extracted before --config so it takes priority
- Add config-priority.test.ts with 8 tests verifying:
  - --model overrides config model
  - --steps overrides config steps
  - --steps "" disables all steps
  - --name overrides config name
  - Config tokens apply as defaults
  - Explicit env vars override config tokens
- Remove preferences.json from priority order docs (not needed)
- Add --model to help text and unknown-flag guidance

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

* docs: add --model, --config, --steps to README

Document config file format, setup steps table, and new CLI flags
in the commands table.

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

* fix: address security review feedback

- Move null byte check before path resolution (defense-in-depth)
- Move agent-setup-options.md from .claude/rules/ to .docs/ (git-ignored)
  per documentation policy

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

* fix: resolve rebase conflicts and deduplicate --model flag extraction

Rebase on main introduced a duplicate --model flag extraction block
(one from the PR at line 804, one from main at line 941). Consolidated
into the single early extraction point with -m shorthand support.
Also removed duplicate --model entry from KNOWN_FLAGS set.

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-13 00:32:58 +00: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
e640d1bfe5
fix: update Codex default model to gpt-5.3-codex and add agent model reference (#2540)
The previous PR (#2536) set the Codex default to gpt-5.1-codex, but the
latest available on OpenRouter is gpt-5.3-codex. Also adds a rules file
documenting each agent's default model to prevent future regressions.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-12 15:49:19 -07: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
0963f708b4
test: remove duplicate and theatrical tests (#2539)
Remove redundant existsSync check inside icon-integrity "is actual PNG
data" tests — the file existence is already verified in the preceding
test, and isPng() will throw if the file is missing.

Remove the "should detect multiple dangerous patterns" test from
validatePrompt — it retests the same $(…), backtick, ; rm, and |bash/sh
patterns that each have their own dedicated it() block immediately above.

Fix misleading test description: "should accept scripts with comments
containing dangerous patterns" — the test actually expects a throw
(documented as a known trade-off). Rename to "should reject…".

Removes 1 test (1381 → 1380) and 18 expect() calls.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 16:48:33 -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
91b66f4b40
fix(e2e): fix input test prompt delivery and agent flags (#2536)
Three root-cause bugs in input test functions:

1. Stdin pass-through broken: cloud_exec uses "printf '...' | base64 -d | bash"
   on the remote, meaning bash reads the script from its own stdin — not the
   outer process's stdin. "PROMPT=$(base64 -d)" inside the script was reading
   from the already-consumed pipe, always producing an empty prompt.
   Fix: embed the base64-encoded prompt directly in the remote command string.
   Base64 output is [A-Za-z0-9+/=] only — safe to embed in single-quoted strings.

2. Zeroclaw flag wrong: "zeroclaw agent -p" was passing the prompt as
   --provider (not --prompt). The correct flag for non-interactive single-message
   mode is "-m"/"--message".

3. Codex model stale: "openai/gpt-5-codex" does not exist on OpenRouter.
   Updated to "openai/gpt-5.1-codex" which is available.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 13:50:06 -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
58a2d3bf18
test: Remove duplicate and theatrical tests (#2534)
Consolidate 9 per-credential-type it() blocks in prompt-file-security.test.ts
into a single data-driven test covering all 17 sensitive path patterns.
Merge 2 validatePromptFileStats "accept" tests into one.

Consolidate 4 unicode/encoding-attack it() blocks in security.test.ts
into a single data-driven test. Merge 3 "accept identifier" it() blocks into one.

Removes 19 redundant tests (1400 → 1381) with no loss of coverage.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 12:52:45 -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
595e36ffb6
test: Remove duplicate and theatrical tests (#2531)
Consolidate 8 fragmented pipe-to-bash/sh tests in validatePrompt into 2
data-driven tests covering all inputs (with/without whitespace, complex
pipelines, and standalone word acceptance). Merge 3 backtick tests into 1.
Merge 2 whitespace tests into 1. Removes 19 lines of duplicate test setup.

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 09:35:21 -04:00
A
6bdef06351
refactor: deduplicate generateCsrfState into shared/oauth.ts (#2530)
The identical generateCsrfState() helper existed in both
digitalocean/digitalocean.ts and shared/oauth.ts. Export it from
oauth.ts (which digitalocean.ts already imports) and remove the
duplicate copy.

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 09:33:53 -04:00
A
afff57db5b
test: remove conditional-expect anti-patterns from 3 test files (#2525)
Replace `if (!r.ok) { expect(...) }` and `if (result.ok) { return }` guards
with unconditional assertions using toThrow() or toMatchObject(). These
conditional blocks silently skipped assertions when the condition evaluated
the wrong way, providing false confidence. Also remove now-unused tryCatch
imports from prompt-file-security.test.ts and security.test.ts.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 02:21:20 -07: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
A
129f72d8e1
test: remove conditional-expect anti-pattern in result-helpers.test.ts (#2514)
Replace `if (result.ok) { expect(result.data)... }` guards with
`expect(result).toMatchObject({ ok: true, data: ... })`. The old pattern
silently skips inner expects when the condition is false — `toMatchObject`
asserts both discriminant and value in a single unconditional call.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 01:04:57 -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
A
a6e3d3304d
test: remove theatrical getTerminalWidth tests that can never fail (#2510)
The two getTerminalWidth tests only checked that the function returns
a number >= 80. Since the implementation is `process.stdout.columns || 80`,
both assertions are trivially satisfied in any environment and provide
zero regression signal. Removed them along with the unused import.

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-11 21:42:23 -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
9859cc6a31
test: remove theatrical always-pass test from fs-sandbox (#2504)
The "real home ~/.spawn/history.json should not be modified" test was a
false signal: if the file doesn't exist it does `expect(true).toBe(true)`,
and if it does exist it only checks `stat.isFile()` while admitting in
comments that it "can't detect retroactively" whether the file was modified.
This test could never catch the regression it claimed to guard against.

Remove it and drop the unused `statSync` import.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 16:47:52 -04: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
25f46d4742
test: remove duplicate per-entity micro-tests in manifest-type-contracts (#2498)
Replace nested describe-per-agent/cloud loops with data-driven it() blocks
that loop over all entities internally. Reduces test count by 192 (235→43)
while preserving all 659 expect() calls and identical coverage. Failures
now include the entity key in the assertion message for debuggability.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 10:34:32 -07: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
d6c1140612
test: remove duplicate validatePrompt test cases (#2493)
The "should accept all example prompts from issue #2249" test block
contained 3 assertions already covered by surrounding tests:
- "Fix the merge conflict >> registration flow" (duplicated)
- "Run tests && deploy if they pass" (duplicated)
- "The output where X > Y is slow" (duplicated)

The one unique assertion ("Add a heredoc to the Dockerfile") has been
folded into the existing "developer phrases" test, which covers the
same false-positive category (prose containing shell-like syntax).

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 04:49:13 -04:00
A
37fa334d78
fix: navigate back to list after delete/remove errors (#2488)
* fix: navigate back to list after delete/remove errors instead of exiting

Previously, choosing "Delete this server" or "Remove from history" from
the action menu would always exit the picker — even if the operation
failed. Now handleRecordAction returns "back" for delete/remove actions,
and activeServerPicker refreshes the remaining list and loops back to
the picker. Cancel on the action menu also returns to the list.

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

* feat: add ValueOf<T> type helper and GritQL enum ban rule

- Add shared ValueOf<T> type that extracts value unions from const objects
  and readonly tuples
- Update RecordActionOutcome to use ValueOf<typeof RecordActionOutcome>
- Add lint/no-ts-enum.grit GritQL rule that bans TypeScript enum keyword
- Register new rule in biome.json plugins

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

* fix: sort type export before value exports in shared index

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

* feat: add biome config for shared package, fix export sort order

Add biome.json to packages/shared so lint + format + import organization
is enforced on the shared library. Fix ValueOf export position to match
biome's organizeImports sort order (type specifiers after value exports).

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

* fix: hoist type re-exports to top of shared index

Split inline `type Result` and `type ValueOf` out of mixed export
statements into separate `export type { ... }` re-exports, hoisted
to the top per biome's organizeImports group config.

biome's useExportType rule doesn't flag re-exports (only locally
defined types), so these must be manually separated.

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

* refactor: consolidate biome config to single root biome.json

Remove per-package biome.json files (packages/cli, packages/shared,
.claude/scripts, .claude/skills/setup-spa) and consolidate into a
single root config with includes glob covering packages/**/*.ts.

Update GritQL rule exclusions to also match shared/src/ paths now
that the shared package is covered by the root config. Fix build-clouds.ts
lint issues (node: protocol, block statements, import sort) that were
newly caught.

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

* refactor: replace grit filename exclusions with biome-ignore comments

Remove all $filename exclusion logic from GritQL rules and instead add
biome-ignore-all comments at the top of files that legitimately need
the banned patterns (result.ts, parse.ts, type-guards.ts).

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-11 00:04:51 -07: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
Ahmed Abushagur
4318acad19
fix: prompt to enable Compute Engine API for new GCP users (#2484)
* fix: prompt to enable Compute Engine API on GCP SERVICE_DISABLED error

New GCP users hit SERVICE_DISABLED because the Compute Engine API isn't
enabled by default. Detects this error, opens the activation URL in
the browser, and prompts the user to retry after enabling it.

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

* docs: add beta flags section to README

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 23:07:09 -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
7804727a1b
feat: default setup options to off, make API key reuse opt-in (#2483)
- All multiselect setup options now default to unchecked (was all checked)
- Added "Reuse saved OpenRouter key" option (off by default) so users
  get a fresh OAuth key each run unless they explicitly opt in
- GitHub CLI option was already filtered when no token detected; now
  reuse-api-key is filtered when no saved key exists
- Cancel on setup options now returns empty set (matching new defaults)
- Env var OPENROUTER_API_KEY still takes priority unconditionally

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 22:00:37 -07:00
A
46b1e9d42c
refactor: add no-try-catch + no-try-finally grit rules, eliminate all violations (#2481)
Add two new GritQL biome plugins (matching ori repo patterns) that ban
all try/catch and try/finally in TypeScript code. Convert all remaining
blocks across production and test files to use tryCatch/asyncTryCatch
from @openrouter/spawn-shared.

no-try-catch.grit covers all 4 variants:
- try/catch with binding, try/catch without binding
- try/catch/finally with binding, try/catch/finally without binding

no-try-finally.grit covers bare try/finally.

Both exclude shared/result.ts and shared/parse.ts (the implementation layer).

Production files (18): aws, hetzner, digitalocean, gcp, sprite, index,
update-check, ui, ssh, agent-setup, picker, agent-tarball, shared,
run, connect, delete, list

Test files (12): cmdlast, cmd-interactive, cmdrun-happy-path,
commands-resolve-run, commands-swap-resolve, commands-error-paths,
download-and-failure, preload, ssh-keys, update-check, orchestrate,
fs-sandbox, prompt-file-security, security, script-failure-guidance

Bumps CLI version to 0.16.6

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:27:25 -07: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
e127308af6
fix: extend launch cmd validation to support pre_launch shell patterns (#2474) (#2476)
* fix: extend launch cmd validation to support pre_launch shell patterns (#2474)

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

* fix: prevent path traversal in LAUNCH_PRE_LAUNCH_SEGMENT regex

Tighten log path pattern to disallow '..' sequences.
Previously [a-zA-Z0-9._/-]+ allowed '../etc/cron.d/evil' paths;
new pattern (/tmp/segments*/filename) blocks all traversal.

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-10 23:55:55 -04: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
6377b58bc1
refactor: extract Lightsail operation helpers to eliminate CLI/REST branching duplication (#2468)
The AWS module had CLI-vs-REST branching duplicated in ensureSshKey (2x),
createInstance (4x), and waitForInstance (2x). Extracted 4 private helpers
(lightsailGetKeyPair, lightsailImportKeyPair, lightsailCreateInstances,
lightsailGetInstance) so each consumer is a single linear flow. A bug fix
in one mode can no longer be missed in the other.

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-10 17:56:43 -07:00
A
95b5de040d
fix: replace open regex with explicit allowlist in sanitizeTermValue (fixes #2461) (#2469)
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 17:55:33 -07:00
A
58282f5727
fix: eliminate GitHub token temp file exposure in agent-setup (fixes #2462) (#2470)
Pass GITHUB_TOKEN directly via inline `export` in the remote SSH command
instead of writing it to local/remote temp files. This removes the race
condition window where tokens could be read from disk.

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 20:32:42 -04: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
A
f60cda67aa
test: add validateMetadataValue tests for GCP metadata injection protection (#2467)
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-10 20:30:10 -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
dc3e4650bb
refactor: update test README with missing test file entries (#2458)
Add 6 undocumented test files to the test index README:
- do-payment-warning.test.ts (Cloud-specific)
- sprite-keep-alive.test.ts (Cloud-specific)
- history-corruption.test.ts (Infrastructure)
- paths.test.ts (Infrastructure)
- fs-sandbox.test.ts (Infrastructure)
- picker.test.ts (Parsing and type utilities)

Also remove duplicate manifest-cache-lifecycle.test.ts entry
that appeared in both Core manifest and Infrastructure sections.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 16:47:50 -04:00
A
b4e0f575d3
fix: show correct hint when spawn delete filter matches nothing (#2456)
The 'create a spawn first' message was shown even when active servers
existed but none matched the filter. Now shows 'Run spawn delete without
filters to see all servers.' for the unmatched-filter case and reserves
the create hint for when no servers exist at all.

Fixes #2454

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 13:01:01 -07:00
A
3978ff6d4d
fix: apply validateLaunchCmd to manifest fallback path in connect.ts (#2455)
Security: the manifest-derived fallback path in connect.ts bypassed the
validateLaunchCmd() allowlist that guards history-derived commands. A
malicious or modified manifest.json cache could inject arbitrary commands
executed on the remote VM via SSH.

Fixes #2453

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 15:28:00 -04: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