Two "is actual PNG data" tests (agent and cloud) silently passed without
asserting anything when the PNG file was missing. The `if (!existsSync)
{ return; }` guard let the test return early with no expectations, so a
missing file would register as a green test instead of a failure.
Fix: replace the early-return guard with an unconditional
`expect(existsSync(pngPath)).toBe(true)` so missing files fail the test
immediately. The "is actual PNG data" test is now self-contained and
does not rely on its sibling "exists" test having already failed.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
DO snapshots are private and account-scoped — users on different
accounts cannot see snapshots built by the CI token. Docker images
are the better approach for cross-account pre-built agents.
Removes: packer/, packer-snapshots workflow, snapshot lookup code,
and snapshot test. Reverts DO CLI to plain cloud-init flow.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Three groups of tests in icon-integrity.test.ts silently passed without
asserting anything when their conditional guard was false:
- Agent manifest icon URL test: `if (parsed.success)` wrapped the only
expect, so a missing `icon` field on any agent would silently pass
- Agent .sources.json ext test: double-conditional (`id in AGENT_SOURCES`
then `if (parsed.success)`) hid both the membership check and parse
result, providing zero signal when either condition failed
- Cloud .sources.json ext test: same double-conditional pattern
Fix: add unconditional `expect(...).toBe(true)` assertions before each
guard so failures surface as actual test failures rather than silently
passing. The TypeScript narrowing guards remain for type safety.
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>
* feat(digitalocean): Packer nightly snapshot pipeline for fast boot
Add pre-built Packer snapshots for DigitalOcean droplets. Instead of
10-20 min cloud-init + agent install on every boot, snapshot-based
droplets boot in ~2-3 min (SSH only, agent pre-installed).
- Packer HCL2 template with parametrized agent/tier builds
- Agent build matrix (packer/agents.json) for all 7 agents
- Tier scripts mirroring cloud-init.ts package tiers
- Nightly GitHub Actions workflow (4 AM UTC, max-parallel: 3)
- Automatic cleanup: keeps only latest snapshot per agent
- CLI: findSpawnSnapshot() looks up pre-built images via DO API
- CLI: waitForSshOnly() skips cloud-init when using snapshots
- CLI: createServer() accepts optional snapshotId, skips user_data
- CLI: main.ts routes to fast path when snapshot detected
- Tests for findSpawnSnapshot() (5 cases, all passing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(packer): use var-file for install_commands to avoid shell quoting issues
The previous approach passed install_commands as `-var` inline, but
GitHub Actions expands `${{ }}` before shell evaluation — JSON arrays
with `|`, `&&`, and `"` characters break shell quoting.
Fix: generate a `.auto.pkrvars.json` file (auto-loaded by Packer)
using jq with --argjson for safe JSON handling. Also route all
`${{ inputs }}` and `${{ matrix }}` values through env vars to
prevent script injection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The OAuth callback URL (http://localhost:PORT/callback) was interpolated
directly into the auth URL query string without encoding. The colons and
slashes could cause ambiguous parsing on strict URL parsers or proxies,
potentially breaking the OAuth flow. Other parameters in the same URL
(spawn_agent, spawn_cloud) were already correctly encoded.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Unref the SIGKILL timer in killWithTimeout() so it doesn't keep the
event loop alive for 5 extra seconds after a timed-out process exits
- Wrap all setTimeout/clearTimeout pairs in try/finally across 6 cloud
providers (12 call sites) to guarantee cleanup on exceptions
- Add missing 60s timeout guard to runSpriteSilent() which could hang
indefinitely on unresponsive sprite processes
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
ensureDeleteCredentials() and execDeleteServer() were exported but never
imported outside of delete.ts itself. Remove the export keywords to match
their actual internal-only usage. No behavior change.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Consolidate redundant per-property tests in script-failure-guidance
Each describe block for an exit code (127, 126, 1, default, null, 130,
137, 255, 2) and signal (SIGKILL, SIGTERM, SIGINT, SIGHUP) had multiple
separate it() tests all calling the same pure function with the same
arguments — one assertion per test. Since the function is pure and
deterministic, these redundant calls add overhead without adding signal.
Merge per-argument test groups into single tests that check all
properties at once. All 3240 expect() calls are preserved; 38 redundant
test wrappers are removed (1395 → 1357 tests).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove two redundant structural tests from getScriptFailureGuidance:
- "should always return an array of strings" — proven by every
content-checking test above it (they all call the function and
assert on its elements)
- "should never return an empty array" — same: every toContain/
toHaveLength assertion already implies a non-empty result
Keeps the useful "different output per exit code" uniqueness test.
Test count: 1411 → 1409 (2 removed, 0 failures).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
- Remove theatrical "should always return string arrays" test from
getSignalGuidance: TypeScript already guarantees string[] return type;
testing it at runtime with Array.isArray/typeof adds zero signal
- Replace 149 (c: any[]) parameter annotations with (c: unknown[])
across 13 test files to comply with the no-as/no-any policy
- Fix mockSuccessfulFetch(data: any) → (data: unknown) in test-helpers.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
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>
The "Quick safety check: Is this a project you created or one you trust?"
prompt fires per-workspace and is not suppressed by hasCompletedOnboarding
or --dangerously-skip-permissions (anthropics/claude-code#28506).
Fix: inject a workspace trust entry keyed by $HOME into ~/.claude.json
with hasTrustDialogAccepted: true. The JSON is now constructed on the
remote side so $HOME resolves to the actual path (/root, /home/user, etc).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(openclaw): supervise gateway with systemd + cron heartbeat
The OpenClaw gateway daemon (port 18789) was started via setsid/nohup
with zero supervision — if it crashed, got OOM-killed, or exited, the
TUI became useless. This was the root cause of OpenClaw dying on
DigitalOcean and other clouds.
On Linux with systemd:
- Install a systemd service with Restart=always, RestartSec=5
- Add an hourly cron heartbeat that checks port 18789 and restarts
the service if dead (belt-and-suspenders for edge cases)
- Base64-encode the wrapper script and unit file to avoid
heredoc/quoting issues across cloud SSH implementations
On macOS/local (no systemd):
- Keep the existing setsid/nohup approach as fallback
Also adds a gateway pre-check to the TUI launch command so the
orchestrate.ts restart loop ensures the gateway is alive before
each TUI restart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix biome formatting (prefer single quotes for shell strings)
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>
Remove two redundant structural tests from getScriptFailureGuidance:
- "should always return an array of strings" — proven by every
content-checking test above it (they all call the function and
assert on its elements)
- "should never return an empty array" — same: every toContain/
toHaveLength assertion already implies a non-empty result
Keeps the useful "different output per exit code" uniqueness test.
Test count: 1411 → 1409 (2 removed, 0 failures).
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Both Hetzner and DigitalOcean were calling GET /ssh_keys inside the
per-key loop, causing N redundant API round-trips when a user had
multiple local SSH keys. Move the fetch outside the loop so it runs
exactly once regardless of how many keys are being registered.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When users opt into GitHub CLI setup, capture their local git
user.name and user.email and apply them on the remote VM via
git config --global, so spawned machines inherit the correct
identity instead of defaulting to generic values.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Convert zeroclaw icon from mislabeled JPEG to actual PNG
- Fix zeroclaw .sources.json ext from "jpg" to "png"
- Fix zeroclaw manifest icon URL from .jpg to .png
- Add icon-integrity.test.ts (54 tests) that validates:
- Every agent/cloud icon exists as .png in assets/
- Every .png file contains actual PNG data (magic bytes check)
- Manifest icon URLs end with {id}.png
- .sources.json ext fields are all "png"
- No .jpg files exist in asset directories
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Drop unnecessary `export` from `createAgents` and `resolveAgent` in
agent-setup.ts — both are internal helpers only ever called within the
same module via `createCloudAgents`; no external caller imports them
- Fix misleading relative-path sourcing example in github-auth.sh header
comment — the shell-script rules ban relative `source ./` paths, and the
example is updated to show the correct CDN eval pattern
- Bump CLI patch version 0.12.17 → 0.12.18
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>
Each describe block for an exit code (127, 126, 1, default, null, 130,
137, 255, 2) and signal (SIGKILL, SIGTERM, SIGINT, SIGHUP) had multiple
separate it() tests all calling the same pure function with the same
arguments — one assertion per test. Since the function is pure and
deterministic, these redundant calls add overhead without adding signal.
Merge per-argument test groups into single tests that check all
properties at once. All 3240 expect() calls are preserved; 38 redundant
test wrappers are removed (1395 → 1357 tests).
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When SSH disconnects with exit code 255, the server is still running.
Previously the warn message ("SSH connection lost") was followed by the
full reportScriptFailure block, which was contradictory. Now we return
undefined after the warn so reportScriptFailure is skipped entirely.
Fixes#2185
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
When a user ran `spawn claude --dry-run`, the dry-run flag was silently
ignored and a real server was provisioned. `cmdAgentInteractive` was
passing `dryRun` in the `debug` parameter position of `execScript`, so
no preview was shown and `SPAWN_DEBUG=1` was set instead.
Fix:
- Export `showDryRunPreview` from `run.ts`
- Import and call it in `cmdAgentInteractive` after cloud selection
- Return early when `dryRun` is set (matches `cmdRun` behaviour)
- Pass `undefined` for the `debug` argument (interactive path has no
debug flag)
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Previously, saveVmConnection wrote to a single last-connection.json temp
file that was only merged into history.json lazily when spawn ls was run.
This caused connections to be silently dropped when:
- Two servers spawned before running spawn ls (file overwritten)
- The last history record already had a connection (merge skipped)
Now saveVmConnection writes directly into history.json by finding the
most recent record matching the cloud with no connection yet. The temp
file is still written for backward compatibility but is no longer the
primary storage.
Also fixes saveLaunchCmd to update history.json directly, and
consolidates sprite's local saveVmConnection to use the shared one.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
- Update key-request.sh comment that referenced non-existent
loadTokenFromConfig function in digitalocean.ts
- Update test comments referencing validateAgent/validateCloud
which were renamed to validateEntity
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The auto-update path in update-check.ts and the manual `spawn update` command
in commands/update.ts were missing --proto '=https' on their curl calls that
download and execute the install script. Without it, curl may follow redirects
to non-HTTPS URLs on hostile networks (MITM/DNS hijacking).
- update-check.ts: add --proto =https to execFileSync curl args
- commands/update.ts: replace execSync shell pipe with safe two-step
execFileSync pattern (fetch script via curl --proto =https, then
execute via bash -c) — matches the pattern already in update-check.ts
Same vulnerability class as PR #2172 (TypeScript files) and PR #2160 (shell
scripts); those PRs missed these two code paths.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(security): add --proto '=https' to curl calls in TypeScript provisioning
Fixes#2169
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(lint): break long lines for biome format compliance
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>
* feat: migrate shell script URLs to openrouter.ai/labs/spawn CDN
Users on older CLI versions can't auto-update because the repo was restructured
(cli/ → packages/cli/), so old version-check URLs 404. This decouples the CLI
from the repo's internal directory structure:
- Shell script URLs (install, agent scripts, github-auth) now use
openrouter.ai/labs/spawn/* as primary with GitHub raw as fallback
- Version checks now use GitHub release artifact (cli-latest/version)
as primary — a static URL that never changes regardless of repo layout
- CI workflow updated to publish a `version` file alongside cli.js
- Remove GITHUB_RAW_URL_PATTERN validation (no longer needed since
install URL is now a hardcoded CDN string, not interpolated)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix biome formatting in update-check test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: CLAUDE.md says biome lint but should say biome check
biome lint only checks lint rules, not formatting. biome check does both.
The hooks and CI already run biome check — the docs were out of sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(hooks): PostToolUse hook wasn't running biome on CLI source files
Two bugs in validate-file.ts:
1. Config search only checked 1-2 levels up from the edited file, but
biome.json is at packages/cli/ — 3 levels above src/__tests__/*.ts.
Fix: walk up directories until biome.json is found (or hit root).
2. Ran `biome format` (prints formatted output, always exits 0) instead
of `biome format --check` (exits non-zero if file needs formatting).
Fix: use `biome check` which does lint + format check in one pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore hyphens/underscores in OAuth code regex + add test (#2116)
PR #2116 broke OAuth by restricting the auth code regex to alphanumeric
only. OAuth providers (GitHub, Google, etc.) use hyphens and underscores
in their auth codes, so the stricter regex rejected valid codes.
Changes:
- Extract OAUTH_CODE_REGEX as an exported constant from oauth.ts
- Restore `_-` in the character class: [a-zA-Z0-9_-]{16,128}
- Add oauth-code-validation.test.ts with 20 tests covering:
- Real-world provider formats (hyphens, underscores, mixed)
- Length bounds (16–128)
- Injection prevention (shell, XSS, path traversal, null bytes)
- Character class completeness (explicit regression test for #2116)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: retrigger checks
* fix: restore hyphens in OAuth code regex + add regression test
PR #2116 broke OAuth by restricting the auth code regex to alphanumeric
only. OAuth providers (GitHub, Google) use hyphens and underscores in
their auth codes, so the stricter regex rejected valid codes.
- Extract OAUTH_CODE_REGEX to oauth-constants.ts (zero-dep, testable)
- Restore `_-` in character class: [a-zA-Z0-9_-]{16,128}
- Add regression test covering valid formats, length bounds, injection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(history): smart trimming evicts deleted records first, archives overflow
When history exceeds 100 entries, deleted records (useless for `spawn ls`)
are now evicted first. If still over the limit, oldest non-deleted records
are also trimmed. All evicted records are archived to dated backup files
(history-YYYY-MM-DD.json) so nothing is permanently lost.
Previously, blind .slice() could silently discard records with active
connections that `spawn ls` depends on.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix biome formatting issues
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>
The resolveAndLog via cmdRun describe block in commands-swap-resolve.test.ts
(~113 lines, 5 tests) duplicated display-name resolution coverage already
provided by commands-resolve-run.test.ts. Both files tested case-insensitive
key resolution (CLAUDE->claude, HETZNER->hetzner) and display name resolution
(Codex->codex, Sprite->sprite) on the same code path.
Removed the entire duplicate resolveAndLog section. The detectAndFixSwappedArgs
and prompt-handling-with-swapped-args sections remain, as those test distinct
behavior not covered elsewhere.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The block comment in run-path-credential-display.test.ts listed five
functions it claimed to test, but the file only tests two:
- prioritizeCloudsByCredentials
- isRetryableExitCode
Functions buildCredentialStatusLines, formatAuthVarLine, validateRunSecurity,
and validateEntities were never imported or exercised in this file. Removed
the misleading entries so the comment accurately reflects test coverage.
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
When the TCP probe succeeds on the final attempt, `attempt` equals
`maxAttempts` after the loop increments it. The previous guard
`attempt >= maxAttempts` then incorrectly threw a timeout error even
though the port was open.
Fix by tracking TCP success with a `tcpOpen` boolean flag and checking
that instead of the attempt counter.
Fixes#2155
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* refactor: Remove redundant loadTokenFromConfig wrappers in hetzner, daytona, digitalocean
The previous PR (#2151) introduced shared loadApiToken() in shared/ui.ts and
updated hetzner/daytona to delegate to it via thin wrapper functions. This
commit removes the now-unnecessary wrapper functions entirely, inlining the
loadApiToken() calls directly at the callsite.
Also removes the 16-line duplicate loadTokenFromConfig() implementation in
digitalocean.ts (which replicates the same api_key/token field reading and
regex validation logic as loadApiToken) and replaces it with a direct call to
loadApiToken("digitalocean").
-- qa/code-quality
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* bump version to 0.12.12 (main already has 0.12.11)
---------
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>
The 'promptBundle should skip prompt without --custom' test expected
promptBundle() to return immediately when SPAWN_CUSTOM is unset. But
promptBundle() has no SPAWN_CUSTOM guard — it always shows an interactive
selection prompt unless LIGHTSAIL_BUNDLE or SPAWN_NON_INTERACTIVE=1 is set.
Without SPAWN_NON_INTERACTIVE=1, the test blocks on stdin input and hits
the 5-second bun:test timeout. When run in the full test suite it
appeared to pass due to module import caching from previous tests, making
it a flaky, non-deterministic test.
Remove the test entirely since it tests non-existent behavior.
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>
Fixes#2156
The spinner was stopped with a success message before the HTTP response
body stream was fully consumed. If the stream failed mid-transfer (network
drop, truncation), users saw "Script downloaded" followed by a confusing
downstream error. Now both the primary and fallback paths await res.text()
before calling s.stop().
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Extract duplicate loadTokenFromConfig helper (hetzner + daytona) into
shared loadApiToken() in shared/ui.ts, eliminating 24 lines of
duplicate validation logic across two cloud modules
- Move misplaced FETCH_TIMEOUT and UPDATE_BACKOFF_MS constants in
update-check.ts from the Schemas section into the Constants section
where they belong (stale empty section header fix)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The "install commands should be strings" test in the "Agent launch
command consistency" describe block was a full duplicate of the
per-agent "install should be a non-empty string" test already
present in the "Agent required field types" loop.
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>
When credentials expire during server deletion, the spinner was running
simultaneously with interactive credential prompts, creating confusing
overlapping UI. Extract ensureDeleteCredentials() to run all credential
checks (which may prompt the user) before starting the deletion spinner.
All 6 cloud providers are covered: AWS, Hetzner, DigitalOcean, GCP,
Daytona, and Sprite.
Fixes#2141
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Distinguish between 'no local credentials' and 'using spawn-cached credentials'
so users understand why an initial failure message is followed by a success.
Fixes#2142
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): add --proto '=https' to all curl bun installer calls
Fixes#2134
All _ensure_bun() functions across aws, hetzner, gcp, local, daytona,
and sprite scripts now enforce HTTPS-only downloads via --proto '=https'.
This prevents MITM attacks during bun installation on remote VMs.
DigitalOcean scripts were already correct and are not changed.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): add --proto '=https' to bun installer in TS files
Address security reviewer feedback: the same MITM vulnerability
existed in 5 TypeScript programmatic provisioning files.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): quote --proto '=https' in su -c curl calls
The aws.ts and gcp.ts files had --proto =https without quotes inside
su -c '...' blocks. Uses double quotes ("=https") to properly nest
inside the single-quoted su -c argument while maintaining protocol
restriction.
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.6 <noreply@anthropic.com>
The DigitalOcean OAuth flow printed two near-identical fallback URL
messages: one manually before calling openBrowser(), and one from
openBrowser() itself. Remove the manual one since openBrowser()
already handles the fallback.
Fixes#2140
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
After showing post-session messages, the local process now exits cleanly
instead of requiring an extra Ctrl+C. The root cause was that after main()
resolved, lingering event loop handles (from @clack/prompts stdin listeners,
fetch connections, etc.) prevented Node/Bun from exiting naturally.
The fix adds process.exit(0) on successful main() completion, which covers
all session paths (bash script execution via execScript, SSH reconnection
via cmdConnect, and agent re-entry via cmdEnterAgent).
Fixes#2145
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add coverage for 6 untested pure utility functions in shared/ui.ts
Adds tests for validateServerName, validateRegionName, validateModelId,
toKebabCase, sanitizeTermValue (security-critical), and jsonEscape.
These exported functions previously had zero test coverage.
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: apply biome formatting to ui-utils test file
Address formatting review feedback: reformats destructuring import
to match project style.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove cmdlast "should not call cmdRun when no history exists" test which
admitted in its own comment that it could not verify its stated intent and
simply duplicated the assertion from the previous test in the same describe block.
- Fix always-pass risk in manifest-type-contracts: "Interactive prompts
structure" and "Config files structure" tests iterated over optional agent
fields with a bare continue when the field was absent, meaning both tests
would vacuously pass if no agents had those fields. Added guard assertions
(expect(length).toBeGreaterThan(0)) matching the pattern used by sibling
tests in the same file.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The "should use fresh disk cache without calling fetch" test only checked
toHaveProperty("agents"), which would pass even if fetch was called again.
Renamed to reflect actual behavior (in-memory cache path) and added
assertions: expect(m2).toBe(m1) and fetch call count unchanged.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address 4 reliability issues across codebase
1. sprite.ts: add --force to destroy command (stdin is "ignore" so
interactive prompts would hang until 60s timeout)
2. verify.sh: replace /dev/tcp port checks with ss -tln primary
(Debian/Ubuntu bash compiled without /dev/tcp support)
3. verify.sh: make _openclaw_restart_gateway a hard failure instead
of log_warn (matching _openclaw_ensure_gateway behavior)
4. agent-setup.ts: add ss -tln port check + "already running" early
exit + increase timeout from 120s to 300s (gateway takes ~3min
to initialize on AWS medium instances)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: biome format - use consistent double quotes in portCheck
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- sprite/sprite.ts: Replace duplicate saveVmConnection implementation
with a call to the shared saveVmConnection from history.ts. The local
version duplicated the mkdir + writeFileSync logic already provided by
the shared function, just with Sprite-specific hardcoded values.
Remove now-unused writeFileSync, mkdirSync, and getSpawnDir imports.
- Bump CLI version 0.12.5 → 0.12.6 (patch)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The "should use disk cache when fresh" test in manifest.test.ts set up
a mock fetch with a comment saying it "should not be called" but never
asserted expect(global.fetch).not.toHaveBeenCalled(). The test passed
whether or not the cache was actually used, providing no signal.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove 18 duplicate tests from run-path-credential-display.test.ts
that repeated coverage already provided by dedicated test files:
- "entity validation for run path" (7 tests) duplicated check-entity.test.ts
- "key resolution for run path" (6 tests) duplicated fuzzy-key-matching.test.ts
- "run-path validation sequence integration" (5 tests) duplicated
check-entity.test.ts, fuzzy-key-matching.test.ts, and script-failure-guidance.test.ts
Replace the three duplicate describe blocks with a focused 2-test
describe("isRetryableExitCode") block that covers the only unique
assertions in that section. Also remove unused spyOn import and
unused mockExit variable.
Bump version 0.12.4 → 0.12.5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(fmt): collapse import to single line for biome format compliance
Agent: team-lead
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>