Commit graph

2007 commits

Author SHA1 Message Date
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
6fda75ccc8
security: validate base64 output in cloud_exec and soak.sh (defense-in-depth) (#2532)
Add base64 character validation ([A-Za-z0-9+/=]) before use in SSH
command strings for gcp.sh, aws.sh, and hetzner.sh cloud_exec
functions -- matching the existing fix in digitalocean.sh (#2528).

Also add a validated _encode_b64 helper to soak.sh and use it for
all Telegram bot token encoding, preventing corrupted base64 from
breaking out of single-quoted SSH command strings.

Closes #2527

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 09:32:48 -04:00
A
76399eafd9
security: validate base64 in digitalocean.sh SSH exec (defense-in-depth) (#2528)
Add explicit base64 character validation in _digitalocean_exec after
encoding the command, matching the existing pattern in provision.sh.
This ensures the encoded value contains only [A-Za-z0-9+/=] before
embedding it in the SSH command string.

Note: #2527 (provision.sh base64 validation) was already fixed in a
prior commit — the validation at lines 284-289 already rejects
non-base64 characters and empty output.

Fixes #2526

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-12 08:16:48 -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
5b5e7d4706
test: add cron-triggered Telegram reminder to soak test (#2519)
* test: add cron-triggered Telegram reminder to soak test

Tests OpenClaw's ability to stay alive and execute scheduled tasks.
Installs a one-shot cron on the VM before the 1h soak wait that sends
a Telegram message at ~55 min, then verifies the message was sent
after the wait completes. Also moves Telegram config injection before
the soak wait so the cron can use the bot token immediately.

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

* test: use OpenClaw's cron scheduler instead of system crontab

Replaces the raw system cron approach with OpenClaw's built-in cron
scheduler (`openclaw cron add`). This properly tests that OpenClaw's
gateway stays alive after 1 hour and can execute scheduled tasks.

The test now:
1. Injects Telegram config + schedules an OpenClaw cron job (--at +55min)
2. Waits 1 hour (soak)
3. Verifies the job fired via `openclaw cron runs` and `openclaw cron list`

Uses --delete-after-run for one-shot semantics. Verification checks both
the run history and the auto-deletion as proof of execution.

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

* test: verify cron message on Telegram side via forwardMessage

Instead of trusting OpenClaw's self-reported cron status, we now verify
the message actually exists in the Telegram chat:

1. Extract message_id from OpenClaw's cron execution logs (tries
   `openclaw cron runs`, then ~/.openclaw/cron/ directory)
2. Call Telegram's forwardMessage API with that message_id
3. If Telegram can forward it → message EXISTS in the chat (proof
   from Telegram itself, not OpenClaw)

This catches cases where OpenClaw reports success but the message
never actually reached Telegram.

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

* fix: address security review findings in soak test

- Add validate_positive_int() and validate SOAK_WAIT_SECONDS +
  SOAK_CRON_DELAY_SECONDS at startup (prevents command injection via
  crafted env vars)
- Validate TELEGRAM_TEST_CHAT_ID is numeric in soak_validate_telegram_env
- Use per-app marker file /tmp/.spawn-cron-scheduled-${app} to avoid
  race conditions when multiple soak tests run on the same VM

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 04:49:42 -04:00
A
c5a40b04a6
fix(e2e): add retry-with-backoff for DigitalOcean 422 droplet limit errors (#2520)
When provisioning hits a 422 "droplet limit exceeded" response, wait 30s
and retry up to 3 times. Makes E2E suite resilient to transient limit hits
during parallel batch provisioning.

Fixes #2516

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 07:49:47 +00:00
A
85a2289bb0
fix(e2e): dynamically calculate DigitalOcean parallel capacity from account limit (#2518)
Previously, _digitalocean_max_parallel() always returned 3, assuming all
quota slots were available. When pre-existing droplets occupy slots, the
batch-3 parallel runs fail with "droplet limit exceeded" API errors.

Now queries /v2/account for the actual droplet_limit and subtracts the
current droplet count to compute available capacity. Falls back to 3 if
the API is unreachable.

-- qa/e2e-tester

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-12 02:50:48 -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
A
d4328f38c2
fix: correct DigitalOcean default droplet size in README and stale getUserHome path (#2513)
DO_DROPLET_SIZE default documented as s-2vcpu-4gb ($24/mo) but code and manifest
both use s-2vcpu-2gb ($18/mo). Also fixes stale getUserHome() source reference in
testing rules (shared/paths.ts, not shared/ui.ts).

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-11 21:25:30 -07: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
A
6ef7dfc99d
fix(e2e): add claude and codex to .spawnrc fallback in provision.sh (#2511)
When Sprite (or another cloud) times out during provisioning, provision.sh
falls back to constructing .spawnrc manually over SSH. The claude and codex
agents were missing from the agent-specific case block, so:

- claude: ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN were never written,
  causing verify_claude's openrouter.ai check to fail
- codex: OPENAI_API_KEY and OPENAI_BASE_URL were never written

Discovered during E2E run: sprite/claude failed with .spawnrc timeout +
missing openrouter.ai in fallback .spawnrc.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 21:40:03 -04:00
A
6c535ac1e8
fix: replace stale bun -e with bun eval in key-request.sh (#2506)
PR #2505 migrated all bun -e → bun eval across shell scripts but
missed 2 instances in sh/shared/key-request.sh (lines 32 and 61).
This completes the migration for consistency.

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-11 17:22:56 -07: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
529a1b3b02
fix: bump quality cycle timeout to 90 min and recognize gcp cli auth (#2501)
* fix: bump quality cycle timeout to 90 min and recognize gcp cli auth

- Quality cycle was hitting the 45 min hard limit mid-run; bumped
  CYCLE_TIMEOUT from 2400s (40 min) to 5400s (90 min) so E2E tests
  (provision + install + verify across multiple clouds) have room to
  complete without getting killed
- Updated qa-quality-prompt time budget from 35 min to 85 min to match
- Added _check_cli_auth_clouds() to key-request.sh: for clouds that use
  CLI auth (gcp via gcloud), check if the CLI has an active account
  instead of reporting them as missing and sending key-request emails
- GCP_PROJECT is loaded from ~/.config/spawn/gcp.json when gcloud is
  authenticated; other CLI-auth clouds (sprite) are excluded from the
  count since they are not auto-checkable

* fix: replace local -n namerefs with eval for bash 3.2 compatibility

local -n (namerefs) requires bash 4.3+ and breaks on macOS which ships
bash 3.2. Replace with eval-based variable indirection that works on
all supported bash versions.

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

* fix: validate GCP_PROJECT format before export to prevent shell injection

Security: project ID from config now validated against ^[a-z][a-z0-9-]*$
pattern before export. Invalid IDs are rejected with a log message.

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

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-11 17:48:21 -04:00
A
1d2bf324c4
refactor: replace bun -e with bun eval and require() with ESM imports in shell scripts (#2505)
Per shell-scripts.md rules: always use `bun eval` (not `bun -e`) and ESM-only
(never `require()`). Fixed in:
- sh/shared/key-request.sh: 3 instances of `bun -e` → `bun eval`
- sh/e2e/lib/soak.sh: `bun -e` → `bun eval`; `require("fs")`/`require("path")` →
  named ESM imports from node:fs and node:path

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 14:03:46 -07: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
794fd1f950
Update Junie icon to official JetBrains logo (#2497)
Replace the GitHub avatar with the official Junie icon SVG
(converted to 200x200 PNG to match existing format).

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 09:35:16 -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
1a9cd59ae8
fix: correct stale GritQL plugin path in type-safety rules (#2495)
The `.claude/rules/type-safety.md` referenced the GritQL no-type-assertion
plugin at `packages/cli/no-type-assertion.grit`, but the actual location is
`lint/no-type-assertion.grit` (root-level lint/ directory, not packages/cli/).

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 08:50:42 -04:00
Ahmed Abushagur
330c10fcd2
feat: add Telegram soak test for OpenClaw (--soak mode) (#2492)
Add a soak test that provisions OpenClaw on Sprite, waits 1 hour for
stabilization, injects a Telegram bot token, and runs integration tests
against the Telegram Bot API (getMe, sendMessage, getWebhookInfo).

- New: sh/e2e/lib/soak.sh — soak test library with all Telegram-specific logic
- Modified: sh/e2e/e2e.sh — add --soak flag to arg parser
- Modified: qa.sh — add soak run mode (bypasses Claude, runs e2e.sh directly)
- Modified: trigger-server.ts — add "soak" to VALID_REASONS
- Modified: qa.yml — add soak to workflow_dispatch options

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
2026-03-11 05:51:53 -04:00
A
c0cedc3887
docs: add missing agent entries to all cloud READMEs (#2494)
Junie was added to all 6 clouds (scripts + matrix) but none of the
READMEs documented it. Sprite README was also missing Hermes, and
local README was missing OpenCode and Junie.

All 6 cloud READMEs now list all 8 agents consistently.

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-11 05:49:50 -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
Ahmed Abushagur
a209daf492
fix: upgrade code-health teammate to do post-merge sweeps and gap detection (#2489)
Replaces the generic "scan for code smells" prompt with a structured
3-step process: (1) post-merge consistency sweep — fix lint violations
and straggler patterns left behind by recent PRs, (2) implementation
gap detection — manifest.json vs actual scripts, missing READMEs, orphaned
entries, (3) general health scan as fallback.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 04:15:59 -04:00
A
68abbee4df
fix(e2e): fix OPENROUTER_API_KEY fallback and sprite env whitelist (#2491)
On QA VMs running Claude Code via OpenRouter, the API key is stored as
ANTHROPIC_AUTH_TOKEN. Add a fallback in common.sh so e2e.sh picks up
the key from ANTHROPIC_AUTH_TOKEN when ANTHROPIC_BASE_URL points to
openrouter.ai and OPENROUTER_API_KEY is unset.

Also add SPRITE_NAME and SPRITE_ORG to the headless env var whitelist
in provision.sh — these are emitted by _sprite_headless_env() but were
missing from the positive whitelist, causing every Sprite provisioning
attempt to log errors and silently skip the env vars.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 03:23:46 -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
7444c3bbc6
fix: verify bun installer SHA-256 before executing in install.sh (#2463) (#2473)
Why: The curl|bash pattern for bun installation was an unverified supply
chain dependency. Now the installer is downloaded to a temp file and its
SHA-256 hash is verified against a known-good value before execution.
Falls back gracefully if sha256sum/shasum is unavailable.

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-10 18:39:41 -07:00
A
7af389387d
fix: eliminate release race condition causing 404 on cloud bundle downloads (#2475)
The cli-release workflow was deleting releases before recreating them,
leaving a window where users downloading cloud bundles (gcp.js, aws.js,
etc.) would get a 404. This affected all clouds on every push to main.

Switch to gh release upload --clobber which atomically replaces assets
without removing the release, and only create releases if they don't
already exist.

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:13:13 -07:00