Commit graph

1804 commits

Author SHA1 Message Date
A
b6a0a8d28b
refactor: Remove dead isOAuthConfigured() stub from DigitalOcean module (#2229)
The isOAuthConfigured() function always returned true unconditionally,
making the two !isOAuthConfigured() guards in tryRefreshDoToken() and
tryDoOAuth() unreachable dead code. Remove the function and inline the
always-true behavior by dropping the dead branches entirely.

Bump CLI patch version to 0.14.1.

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-05 17:40:34 -08:00
A
989e9bee12
test: Remove duplicate and theatrical tests (#2230)
- Remove duplicate countImplemented empty-matrix test from
  manifest-cache-lifecycle.test.ts (already covered in manifest.test.ts)
- Remove duplicate agentKeys/cloudKeys empty-manifest test from
  manifest-cache-lifecycle.test.ts (already covered in manifest.test.ts)
- Consolidate gateway-resilience.test.ts from 9 identical startGateway()
  invocations into 3 grouped tests, reducing redundant async setup overhead
  while keeping the same assertion coverage (18 expects)
- Move stderrSpy.mockRestore() from each it() into afterEach() in
  gateway-resilience.test.ts

-- qa/dedup-scanner

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-05 20:39:33 -05:00
A
73a770236a
docs: Sync README with source of truth (#2228)
Add missing --zone and --size flag entries to the commands table.
Both flags exist in packages/cli/src/commands/help.ts getHelpUsageSection()
but were absent from the README commands table.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 20:38:42 -05:00
Ahmed Abushagur
c71f01725b
test: unit tests for openclaw gateway resilience config (#2224)
* test(e2e): add openclaw gateway kill/restart resilience test

Verifies that the openclaw gateway auto-restarts after being killed
with SIGKILL, validating the systemd Restart=always supervision.

The test runs as part of verify_openclaw:
1. Confirms gateway is listening on :18789
2. Kills it with SIGKILL (simulates a hard crash)
3. Waits up to 30s for systemd to auto-restart it
4. Verifies port 18789 comes back online

If the gateway isn't running (e.g. non-systemd env), the test is
skipped gracefully. On failure, dumps systemd status and gateway
logs for diagnostics.

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

* Revert "test(e2e): add openclaw gateway kill/restart resilience test"

This reverts commit 39b79d5c12.

* test: add unit tests for openclaw gateway resilience config

Verifies that startGateway() produces correct systemd and cron
configuration for auto-restart after a gateway crash:

- Restart=always and RestartSec=5 in the systemd unit
- Cron heartbeat checks port 18789 and restarts if dead
- Wrapper script sources .spawnrc and execs openclaw gateway
- Multiple port-check fallbacks (ss, /dev/tcp, nc)
- Non-systemd fallback to setsid/nohup
- 300s startup timeout

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

* test(e2e): add openclaw gateway kill/restart resilience test

Kills the gateway with SIGKILL during verify_openclaw and verifies
systemd Restart=always brings it back within 30s. Skips gracefully
on non-systemd environments.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-05 19:38:10 -05:00
Ahmed Abushagur
4cfdb0ad9b
feat: Docker-based agent delivery with optimized provisioning (#2225)
* feat(digitalocean): use Docker marketplace image for agent deployments

Use DigitalOcean's Docker marketplace image (docker-20-04) instead of
plain Ubuntu + installing Docker via cloud-init. Docker is pre-installed
so cloud-init only needs to `docker pull` the agent image.

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

* fix: use docker-22-04 marketplace image (Ubuntu 22.04)

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

* revert: back to docker-20-04 marketplace image

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

* feat(digitalocean): use Docker marketplace image with SSH/UFW setup

The docker-20-04 marketplace image has Docker pre-installed but our
user_data replaces its default first-boot script. Add UFW allow for
SSH + sshd restart at the top of cloud-init to restore SSH access.

Skip Docker installation when using the marketplace image since it's
already available.

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

* fix: remove SSH ForceCommand block from marketplace image

DO marketplace images ship with an SSH ForceCommand that blocks login
with "Please wait..." until the image's first-boot script removes it.
Since our user_data replaces that first-boot script, we must strip the
ForceCommand ourselves before sshd restarts.

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

* fix(digitalocean): don't provide user_data to Docker marketplace image

The Docker marketplace image (docker-20-04) has its own first-boot
process that removes the SSH ForceCommand and configures UFW. Providing
user_data conflicts with this and prevents SSH from ever becoming
accessible.

Instead, boot without user_data and run all setup (package install,
Node/bun, docker pull) via SSH after the marketplace image completes
its own initialization.

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

* fix(digitalocean): use docker-22-04 marketplace image slug

The Docker marketplace image is Ubuntu 22.04 based, not 20.04.
docker-20-04 was causing SSH timeouts due to deprecated first-boot process.

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

* fix(digitalocean): revert to docker-20-04 slug (is actually Ubuntu 22.04)

DO API confirms docker-20-04 is the correct slug — it maps to
"Docker on Ubuntu 22.04". docker-22-04 is not a valid slug.

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

* fix(digitalocean): use ubuntu + cloud-init Docker install instead of marketplace image

The Docker marketplace image (docker-20-04) has a slow first-boot
process (~90-180s before SSH opens). Using ubuntu-24-04-x64 with
Docker installed via cloud-init (get.docker.com) is faster end-to-end
because SSH opens in ~30-60s and Docker installs in parallel.

Cloud-init now installs Docker and starts docker pull in background
when an agentName is provided. tryInstallFromDocker() checks if the
image is ready at install time.

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

* fix: wait for in-progress docker pull before extraction

The docker pull started during cloud-init runs in background (&).
If tryInstallFromDocker() runs before the pull completes, it falls
back to normal install unnecessarily. Now waits for any in-progress
docker pull process to finish before checking image availability.

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

* fix: use nohup for background docker pull in cloud-init

The docker pull was backgrounded with bare & in the cloud-init script.
When the script exits after touching .cloud-init-complete, the
background process receives SIGHUP and gets killed. Using nohup
prevents this so the pull survives the script exit.

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

* debug: add diagnostic output to tryInstallFromDocker

Temporary debug logging to diagnose why docker pull isn't available.
Also increased timeout from 60s to 120s.

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

* perf: optimize provisioning — Docker only for slow agents, reorder cloud-init

- Only ZeroClaw (slow Rust build) gets Docker image extraction via
  withDockerInstall + slowInstall flag
- Fast agents (claude, codex, openclaw, opencode, kilocode, hermes)
  skip Docker entirely — their native install is faster than Docker overhead
- Reorder cloud-init: Docker install first, pull in background, then
  apt-get/node/bun run in parallel with the pull
- Remove debug output from tryInstallFromDocker()
- Version bump to 0.14.0

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

* fix: poll for Docker image availability instead of relying on pgrep

The docker CLI process exits while dockerd continues pulling layers
internally. pgrep-based wait exited early, then the image check failed.

Now polls `docker images -q` every 5s for up to 5min until the image
actually appears. Also increases SSH timeout to 600s to match.

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

* fix: clear pre-existing zeroclaw config before onboard

Docker image extraction copies ~/.zeroclaw/config.toml from the image,
which already contains [security]. Then setupZeroclawConfig appends
another [security] section → TOML duplicate key error.

Fix: rm the old config before zeroclaw onboard generates a fresh one.

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

* feat: re-add Docker image extraction for OpenClaw

OpenClaw benefits from Docker pre-pull since npm install is slower
than docker cp extraction. Add slowInstall + withDockerInstall back.

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

* fix: sed zeroclaw config in-place instead of appending duplicate sections

zeroclaw onboard already generates [security] and [shell] sections.
Appending duplicate sections causes TOML parse errors. Now uses sed
to modify existing values in-place, with fallback to append if the
sections don't exist.

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

---------

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:50:59 -05:00
L
9dff1296f0
feat: add --zone/--region and --size/--machine-type CLI flags (#2223)
Adds cross-cloud flags for specifying zone/region and instance size
directly from the command line instead of env vars:

  spawn claude gcp --zone us-east1-b --size e2-standard-4
  spawn claude digitalocean --region lon1 --size s-4vcpu-8gb
  spawn claude hetzner --zone ash --size cx32

Each flag maps to the appropriate cloud-specific env var:
  --zone/--region  → GCP_ZONE, DO_REGION, HETZNER_LOCATION, AWS_DEFAULT_REGION
  --size/--machine-type → GCP_MACHINE_TYPE, DO_DROPLET_SIZE, HETZNER_SERVER_TYPE, LIGHTSAIL_BUNDLE

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 14:06:06 -08:00
A
ae584632f1
test: remove duplicate download pipeline tests (#2222)
Remove 17 duplicate test cases from commands-update-download.test.ts and
download-and-failure.test.ts that were already covered by other test files:

- commands-update-download.test.ts: Removed entire "Script download and
  execution" describe block (8 tests) duplicated by download-and-failure.test.ts
  and cmdrun-happy-path.test.ts. File now only tests cmdUpdate.

- download-and-failure.test.ts: Removed "download - primary URL succeeds" (2 tests),
  "download - primary fails, fallback succeeds" (2 tests), and
  "execScript - script content validation" (2 tests) already covered by
  cmdrun-happy-path.test.ts with more thorough assertions.

Authoritative test homes:
- Success paths (primary/fallback download, script validation, env vars, history):
  cmdrun-happy-path.test.ts
- Failure paths (both-404, both-500, mixed errors, network errors):
  download-and-failure.test.ts
- Update command: commands-update-download.test.ts

398 lines removed, 0 test regressions. All 1395 tests pass.

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-05 16:15:46 -05:00
A
84ec491da0
refactor: remove dead mockFailedFetch test helper (#2221)
The mockFailedFetch function in test-helpers.ts was never imported or
used by any test file. Removed to reduce dead code.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-05 16:14:35 -05:00
L
9f00c26ef7
fix: nest workspace trust entry under "projects" key in .claude.json (#2220)
The hasTrustDialogAccepted entry was at the top level of .claude.json
but Claude Code expects it nested under "projects": { "/root": { ... } }.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 11:04:22 -08:00
Ahmed Abushagur
08cf5e6d8a
fix(e2e): DigitalOcean name mismatch and bash 3.2 compat (#2218)
1. promptSpawnName() now checks DO_DROPLET_NAME before generating a
   random name, matching getServerName() behavior. This fixes the e2e
   harness creating droplets as spawn-XXXX when it expects
   e2e-digitalocean-AGENT-TIMESTAMP.

2. Replace BASH_REMATCH with sed-based parsing in provision.sh for
   macOS bash 3.2 compatibility. BASH_REMATCH was returning empty
   values, causing `export: '=': not a valid identifier`.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-05 13:44:32 -05:00
Ahmed Abushagur
89e5e980c0
fix(docker): add xz-utils to Hermes Dockerfile for Node.js extraction (#2217)
The Hermes installer downloads Node.js as a .tar.xz archive. Without
xz-utils, tar cannot decompress it (exit code 2).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:41:53 -05:00
Ahmed Abushagur
77c3e34803
feat(docker): replace Packer snapshots with Docker-based agent delivery (#2206)
* feat(docker): replace Packer snapshots with Docker-based agent delivery

Docker images on GHCR are public and cross-account, unlike DO snapshots
which are private/account-scoped. Cloud-init installs Docker + pulls the
agent image during boot. The install step extracts pre-built binaries via
`docker cp` and falls back to normal install if unavailable.

- Add Dockerfiles for all 7 agents (claude, codex, openclaw, opencode,
  kilocode, zeroclaw, hermes)
- Convert docker.yml to matrix build for all agents
- Add tryInstallFromDocker() shared helper with Docker-first install
- Add Docker pull to DigitalOcean cloud-init userdata
- Remove Packer snapshot pipeline, lookup, and SSH-only wait
- Remove packer/ directory (HCL templates, tier scripts, agents.json)

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

* security: address review findings in docker agent delivery

- Add agentName validation regex (/^[a-z0-9-]+$/) in digitalocean.ts
  before interpolation into cloud-init script
- Quote dockerImage variable in all docker command strings in
  agent-setup.ts to prevent command injection
- Restrict docker cp to specific known directories (.claude, .bun,
  .local, .npm, .cargo, .opencode) instead of blanket /root/.

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-05 11:23:56 -05:00
A
0098e60688
security: add path traversal guard to opencode tar extraction (CWE-22) (#2216)
PR #2213 fixed the identical vulnerability in github-auth.sh but missed
openCodeInstallCmd() in agent-setup.ts. A compromised sst/opencode
tarball could write to arbitrary paths on the remote VM (runs as root).

Add the same tar -tzf | grep -qE '(^/|\.\.)' check before extraction,
matching the established pattern from github-auth.sh.

Agent: team-lead

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-05 10:51:32 -05:00
A
0e4f41fa15
fix: openclaw and zeroclaw reconnect broken by launch command validation (#2215)
The launchCmd() for openclaw contained inline shell logic (if/while/$())
that fails validateLaunchCmd() allowlist on reconnect. The zeroclaw
launchCmd() used quoted export PATH="..." which also fails validation.

Users running `spawn enter` for these agents got a hard exit with
"corrupted history" error.

Fix: simplify openclaw launchCmd to remove redundant gateway startup
logic (already handled by systemd supervision), and remove quotes from
zeroclaw export PATH value.

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-05 10:23:06 -05:00
A
5dfb91b747
security: fix checksum grep anchoring and tar path traversal in github-auth.sh (#2213)
* security: fix checksum grep anchoring and tar path traversal in github-auth.sh

- Anchor grep with -F " ${tarball}" to prevent partial filename matches
  in checksum validation (e.g. foo.tar.gz matching foo.tar.gz.sig)
- Add pre-extraction validation rejecting tarballs with absolute paths
  or ../ traversal components (CWE-22), cross-platform (GNU + BSD tar)

Fixes #2211
Fixes #2212

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

* fix: anchor checksum grep with two-space prefix and EOL to prevent partial match

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-05 05:41:48 -08:00
A
475a1772a7
test: Remove always-pass conditional guards in icon-integrity tests (#2214)
The `if (parsed.success)` and `if (id in SOURCES)` guards inside test
bodies were redundant — an `expect(...).toBe(true)` assertion always
precedes them, so the inner expects would only be skipped if the test
was already failing. Replace with early-return guards that make the
control flow explicit and remove the nested indirection.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 07:51:52 -05:00
A
02931cfa32
security: verify gh binary checksum and safe JSON parsing in github-auth.sh (#2210)
Fixes #2209

- Replace sed-based JSON parsing with jq/bun-eval for safe tag_name extraction
- Add SHA256 checksum verification before extracting gh binary tarball
- Add semver format validation for parsed version strings

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-05 05:49:23 -05:00
A
5cc75eb558
docs: Sync README with source of truth (#2207)
Remove --debug row from commands table: it exists in the codebase
(index.ts) but is not listed in getHelpUsageSection() in help.ts,
which is the source of truth for the README commands table.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-05 01:17:56 -08:00
A
a3ddb40f72
test: Remove remaining always-pass patterns in icon-integrity tests (#2208)
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>
2026-03-05 03:50:02 -05:00
Ahmed Abushagur
07c2c08e3a
revert: remove Packer snapshot pipeline (#2205)
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>
2026-03-05 02:48:52 -05:00
Ahmed Abushagur
96ffb3e201
fix(packer): pass var file explicitly to packer build (#2203)
Packer wasn't auto-loading build.auto.pkrvars.json, causing
"Unset variable" errors. Pass it explicitly with -var-file.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 01:16:41 -05:00
A
aa84bcf94c
test: Remove always-pass patterns in icon-integrity tests (#2202)
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>
2026-03-04 21:19:35 -08:00
Ahmed Abushagur
ed98a59318
feat(digitalocean): Packer nightly snapshot pipeline for fast boot (#2198)
* 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>
2026-03-04 20:47:46 -08:00
A
3242fa78f1
fix(oauth): URL-encode callback_url query parameter (#2201)
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>
2026-03-04 23:19:15 -05:00
A
701e3af56e
fix: prevent timer leaks and event-loop stalls in SSH timeout handling (#2200)
- 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>
2026-03-04 19:04:47 -08:00
A
2fea3de685
refactor: Remove dead exports from delete.ts helper functions (#2199)
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>
2026-03-04 22:03:39 -05:00
A
62a904b535
test: Remove duplicate and theatrical tests (#2197)
* 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>
2026-03-04 18:41:03 -08:00
L
4a3a6ed27f
fix: suppress Claude Code workspace trust prompt on provisioned VMs (#2192)
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>
2026-03-04 17:11:42 -08:00
Ahmed Abushagur
7cb33f9bd6
fix(openclaw): supervise gateway with systemd + cron heartbeat (#2196)
* 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>
2026-03-04 22:47:18 +00:00
A
49e1c80597
test: Remove duplicate and theatrical tests (#2195)
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>
2026-03-04 15:46:55 -05:00
L
846d655706
perf: fetch SSH key list once before loop in ensureSshKey() (#2194)
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>
2026-03-04 14:47:21 -05:00
A
711ed70b22
feat: propagate host git identity to remote VMs during GitHub setup (#2193)
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>
2026-03-04 13:50:19 -05:00
A
4d44353464
fix: ensure all icons are PNG and add icon-integrity test (#2191)
- 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>
2026-03-04 09:38:07 -08:00
A
48da6d8735
refactor: Remove dead code and stale references (#2189)
- 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>
2026-03-04 11:56:37 -05:00
A
50b096b02d
test: Consolidate redundant per-property tests in script-failure-guidance (#2190)
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>
2026-03-04 11:55:10 -05:00
A
193f3777a3
test: Remove duplicate and theatrical tests (#2187)
- Consolidate 3 identical-setup "both 404" tests into 1 test checking all assertions at once
- Consolidate 2 identical-setup "both 500" tests into 1 test
- Consolidate 4 near-identical network-error tests into 2 tests (error message + troubleshooting hints)
- Remove duplicate validatePrompt max-length test (10KB+1 bytes) already covered in security.test.ts

Removes 6 tests total (1401 → 1395). No behavior coverage lost.

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-04 13:32:01 +00:00
A
035c09c9bb
docs: Sync README with source of truth (#2188)
- Gate 1 (Matrix drift): manifest.json has 7 agents (hermes added) and
  49 implemented combinations; README tagline said "6 agents / 42
  combinations" and the matrix table was missing the Hermes Agent row
- Gate 2 (Commands drift): --headless, --output json, and --custom flags
  exist in help.ts getHelpUsageSection() but were absent from the
  README commands table

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 08:30:17 -05:00
A
083c103ab9
fix(run): suppress duplicate failure output on SSH disconnect (exit 255) (#2186)
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>
2026-03-04 07:16:49 -05:00
A
03cc7f4132
fix(interactive): honour --dry-run flag in single-agent interactive path (#2184)
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>
2026-03-04 06:14:50 -05:00
Ahmed Abushagur
c9ea6384da
fix(history): merge connection into history.json immediately at provision time (#2177)
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>
2026-03-04 01:36:28 -08:00
A
cb91b5d236
refactor: fix stale comments referencing renamed functions (#2182)
- 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>
2026-03-04 01:35:18 -08:00
A
c77cab0fff
fix(security): add --proto '=https' to update-check and update command curl calls (#2183)
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>
2026-03-04 04:18:56 -05:00
A
9376884009
feat(qa): add record-keeper teammate to QA bot (#2176)
Add a 5th teammate (record-keeper) to the QA quality cycle that keeps
README.md in sync with source-of-truth files. Uses a conservative
three-gate check (matrix drift, commands drift, troubleshooting gaps)
and only makes changes when drift is detected. Includes safeguards:
30-line diff limit, prohibited sections list, and source citations
required in PR body.

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 00:23:03 -08:00
A
c8581b7958
fix(security): add --proto '=https' to TypeScript curl provisioning calls (#2172)
* 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>
2026-03-04 00:21:37 -08:00
A
3f8a9d7432
fix: use caduceus ⚕ icon for hermes agent (#2181)
Replace the Nous Research org avatar with the actual Staff of Hermes
(⚕) symbol from the hermes-agent page favicon. Sourced from the
WordPress emoji SVG and converted to 180x180 PNG.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-04 00:07:48 -08:00
A
1f879e1d1e
chore: refresh agent GitHub stars and icon metadata (#2179)
Notable star count changes:
- openclaw: 212,334 → 256,970 (+44,636)
- opencode: 107,223 → 115,408 (+8,185)
- zeroclaw: 15,177 → 21,867 (+6,690)
- claude: 67,857 → 73,410 (+5,553)
- codex: 61,159 → 62,925 (+1,766)
- hermes: 1,016 → 1,617 (+601)
- kilocode: 15,619 → 16,172 (+553)

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:49:15 -08:00
A
bfe53524c1
docs: add mandatory bun install step to worktree workflow (#2180)
Worktrees don't share node_modules with the main checkout. Without
`bun install`, tests and biome fail with "Cannot find package" errors
that block the pre-merge hook.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:48:38 -08:00
A
8eb5f8476c
chore: rename fixture files from snake_case to kebab-case (#2167)
Renames create_server, delete_server, ssh_keys, and server_types
fixture JSON files to kebab-case for consistency with codebase
conventions. Updates _metadata.json keys and qa-fixtures-prompt
naming convention accordingly.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:35:43 -08:00
L
61bcedc0eb
feat: migrate to openrouter.ai/labs/spawn CDN + release artifact version checks (#2178)
* 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>
2026-03-03 23:34:58 -08:00
A
67443aa4b3
chore: add pre-merge hook to gate gh pr merge/ready on lint + tests (#2173)
Adds a PreToolUse hook for Bash that intercepts `gh pr merge` and
`gh pr ready` commands and runs `biome check src/` + `bun test` before
allowing them. Blocks the command if either check fails.

The hook finds the worktree from the command path or falls back to
git rev-parse --show-toplevel.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-03 23:29:26 -08:00