- manifest.ts: Reset _staleCache on successful fetch/cache load so
isStaleCache() doesn't falsely report stale data after reconnecting
- gcp.ts: Replace getGcloudCmd()! with requireGcloudCmd() that throws
a descriptive error instead of crashing with null dereference
- digitalocean.ts: Replace unvalidated JSON.parse return with
parseJsonObj() + isString()/isNumber() guards for type safety
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove 4 duplicate tests spread across security and command resolution test files:
- security-edge-cases.test.ts: Remove "should accept prompts with dollar signs in
safe contexts" (duplicate of security.test.ts "should accept dollar signs in
non-expansion contexts")
- security-edge-cases.test.ts: Remove "should accept prompts with pipe to non-shell
commands" (duplicate of security.test.ts "should accept prompts with pipes to
other commands")
- security-edge-cases.test.ts: Remove "should accept prompts with semicolons not
followed by rm" (duplicate of security-encoding.test.ts "should accept semicolons
not followed by rm")
- commands-swap-resolve.test.ts: Remove "should not log resolution for already-
lowercase exact keys" (duplicate of commands-resolve-run.test.ts "should not log
resolution when exact keys are used" — identical cmdRun("claude", "sprite") call)
No functional behavior changes. Test count: 1389 → 1385.
* fix: remove trailing blank line for biome format
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The Sprite saveVmConnection() wrote ~/.spawn/last-connection.json without
restrictive permissions (defaulting to umask 0o644/0o755), unlike the shared
saveVmConnection() in history.ts which correctly uses mode 0o700 for the
directory and 0o600 for the file. On multi-user systems this could expose
server names and connection metadata to other users.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Delete the exported `setupOpenclawBatched` function from `agent-setup.ts` — it was
never imported or called anywhere in the codebase (confirmed via exhaustive grep)
- Remove the unused `setup?` field from the `AgentConfig` interface in `agents.ts` —
no agent implementation ever assigned this property
- Remove the dead `if (agent.setup)` branch from `orchestrate.ts` — the batched path
was always unreachable because no agent provided a `setup` callback
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
runServer and runServerCapture on Hetzner and DigitalOcean used stdio:["pipe",...]
for stdin but called proc.stdin!.end() AFTER await proc.exited. If a remote SSH
command reads from stdin (apt prompts, read calls), the process deadlocks until the
5-minute timeout fires. AWS and GCP correctly use stdio:["ignore",...].
Fix: change stdin from "pipe" to "ignore" in runServer and runServerCapture for
both Hetzner and DigitalOcean, removing the now-unnecessary stdin.end() calls.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Every other cloud provider (GCP, DO, Daytona) gates their size/type
picker behind SPAWN_CUSTOM !== "1" so users get a fast default launch.
Hetzner's promptLocation had the guard but promptServerType was missing
it, causing an unexpected interactive picker on the cheapest/most-used
cloud when running without --custom.
Bump CLI to 0.11.19.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
- Remove 3 duplicate/always-pass tests from commands-update-download.test.ts:
"should reject script without shebang via validateScriptContent" (already covered
in download-and-failure.test.ts and cmdrun-happy-path.test.ts),
"should reject script with dangerous pattern" (duplicate + always-pass or-chain),
"should show script-not-found message when both URLs 404" (duplicate of existing 404 test)
- Remove 5 theatrical tests from custom-flag.test.ts that only verify
constant arrays have entries with defined id/label fields (SERVER_TYPES,
LOCATIONS, DROPLET_SIZES, DO_REGIONS, SANDBOX_SIZES) — these test constant
existence, not behavior, and fail due to @openrouter/spawn-shared import error
- Bump CLI version to 0.11.18
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Remove trailing blank lines in custom-flag.test.ts for biome format
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: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
runSprite was wired as CloudRunner.runServer but silently dropped the
timeoutSecs parameter. All other clouds (Hetzner, DO, AWS, GCP, Daytona)
implement kill-on-timeout via setTimeout+killWithTimeout; Sprite had zero
timeout protection, so a hung agent install (e.g. ZeroClaw's 600s Rust
compile, Claude Code's 300s install) would hang forever on Sprite.
Matches the pattern used by every other cloud provider.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- manifest-type-contracts.test.ts: Replace 42 per-agent/per-cloud
silently-skipping tests (if field === undefined { return }) with 6
aggregate tests that filter to entries that actually have the field
and assert the field count > 0 so the test can't pass vacuously.
Affected: pre_launch, config_files, notes (agents); defaults, notes,
icon (clouds).
- history.test.ts: Remove always-pass test "throws for SPAWN_HOME
pointing to /root when user home is different" — it silently returns
early whenever the CI environment runs as root (which it always does),
providing zero signal. The adjacent "throws for SPAWN_HOME outside
home directory" test already covers this semantic.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(sprite): fix all 6 Sprite agent installs for E2E
- Use `npm install -g --prefix` instead of `npm config set prefix` to
avoid creating .npmrc that conflicts with nvm on Sprite VMs
- Fix shell environment setup to only modify .bash_profile (not .bashrc)
so non-interactive bash -c commands retain PATH config
- Add $HOME/.cargo/bin to PATH for zeroclaw (Sprite has no ~/.cargo/env)
- Add $HOME/.local/bin to PATH config for Sprite shell environment
- Add sprite E2E cloud driver with org detection, config corruption fix,
direct command embedding (not $1 positional), and retry logic
- Fix provision.sh to kill full process tree after timeout (prevents
orphaned sprite exec sessions from corrupting config)
- Fix verify.sh zeroclaw check to not rely on ~/.cargo/env existing
Tested: 6/6 Sprite agents pass E2E (claude, codex, openclaw, zeroclaw,
opencode, kilocode). Hermes is not in the Sprite manifest.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: biome format - collapse runSprite call to single line
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>
* test: Remove duplicate and theatrical tests
- check-entity.test.ts: Remove 'kind parameter consistency' describe block
(9 tests) that fully duplicated coverage already provided by 'valid entities',
'wrong-type detection: cloud given as agent', and 'wrong-type detection: agent
given as cloud' describes. Also remove redundant loop assertions ('should
return true for all three agent keys' etc.) that repeated what the individual
named tests already covered.
- manifest-cache-lifecycle.test.ts: Replace Record<string, any> with
Record<string, AgentDef> and Record<string, CloudDef> for type safety.
1401 tests pass, 0 fail. Lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove extra blank line to pass Biome format check
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove redundant if-guards around always-present agent metadata fields in
manifest-type-contracts.test.ts. All 12 metadata fields (creator, repo,
license, created, added, github_stars, stars_updated, language, runtime,
category, tagline, tags) are present on all 7 agents, making the
if (agent.X !== undefined) guards always-truthy dead code that misleads
readers into thinking tests might be skipped. Restructure into proper
per-agent describe blocks to make the test structure honest and clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Apply Biome formatting to array literal
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Fix always-pass anti-pattern in manifest-type-contracts.test.ts where
optional field type tests were gated by `if (field !== undefined)` OUTSIDE
the `it()` block. When no agent/cloud had the field, zero tests registered,
giving false confidence.
Changes:
- Agent optional field types: move condition inside `it()`, test always runs
- Cloud optional field types: same fix, tests always register for all clouds
- Interactive prompts structure: consolidate filtered loop into one `it()` that
iterates internally, avoiding silently-absent test registrations
- Config files structure: same consolidation pattern
Before: 551 pass, 64 fail (optional field tests only registered per-agent)
After: 566 pass, 64 fail (optional field tests register for every agent/cloud)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix biome lint errors - add block statements to early returns
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: apply biome formatter to block statements
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: L <6723574+louisgv@users.noreply.github.com>
Adds explicit monitoring obligation and step-by-step migration
checklist to the DO_CLIENT_SECRET comment. Tracks when PKCE was last
verified unsupported (2026-03) and what to do when it becomes
available, addressing the technical debt tracking request from #2041.
Fixes#2041
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): replace validateLaunchCmd blocklist with allowlist
The blocklist pattern />\\s*\\// (redirection to absolute path) matched
2>/dev/null, which appears in every valid launch command generated by
agent-setup.ts. This caused mergeLastConnection() to reject and discard
all connection data, breaking the spawn list → "Enter agent" reconnect
flow and spawn last.
Replace the blocklist with a strict allowlist: each semicolon-separated
segment must match one of:
- source ~/.<rc-file> [2>/dev/null]
- export PATH=<safe-path>
- <binary> [simple-args]
This simultaneously fixes the false-positive and closes the latent
injection gap (the old blocklist only blocked '; rm' but not arbitrary
'; <other-cmd>').
Fixes#2052
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: apply biome formatter to fix CI format check
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>
`cmdLast()` was always calling `cmdRun()`, creating a brand-new VM every
time. Wire it into `handleRecordAction()` instead, which already contains
the reconnect-vs-rerun logic used by `spawn list`: if the latest history
record has a live connection (IP + server ID), the user is offered options
to enter the agent or SSH in; only if no connection info exists (or the
user chooses "Spawn a new VM") does it provision a fresh instance.
Also bumps CLI version 0.11.13 → 0.11.14.
Fixes#2050
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
- check-entity.test.ts: Remove 'kind parameter consistency' describe block
(9 tests) that fully duplicated coverage already provided by 'valid entities',
'wrong-type detection: cloud given as agent', and 'wrong-type detection: agent
given as cloud' describes. Also remove redundant loop assertions ('should
return true for all three agent keys' etc.) that repeated what the individual
named tests already covered.
- manifest-cache-lifecycle.test.ts: Replace Record<string, any> with
Record<string, AgentDef> and Record<string, CloudDef> for type safety.
1401 tests pass, 0 fail. Lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove extra blank line to pass Biome format check
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
Remove redundant if-guards around always-present agent metadata fields in
manifest-type-contracts.test.ts. All 12 metadata fields (creator, repo,
license, created, added, github_stars, stars_updated, language, runtime,
category, tagline, tags) are present on all 7 agents, making the
if (agent.X !== undefined) guards always-truthy dead code that misleads
readers into thinking tests might be skipped. Restructure into proper
per-agent describe blocks to make the test structure honest and clear.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: Apply Biome formatting to array literal
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: L <6723574+louisgv@users.noreply.github.com>
Remove `export` from functions that are only used internally within their
own file and never imported elsewhere. Affected modules:
- `history.ts`: `mergeLastConnection` (only called internally by `getActiveServers`/`filterHistory`)
- `update-check.ts`: `isUpdateBackedOff` (only called internally by `checkForUpdates`)
- `aws/aws.ts`: `waitForSsh` (only called internally by `waitForCloudInit`)
- `gcp/gcp.ts`: `waitForSsh` (only called internally by `waitForCloudInit`)
- `daytona/daytona.ts`: `waitForSsh` (only called internally by `waitForCloudInit`)
- `shared/agent-setup.ts`: 11 implementation helpers (`installAgent`, `uploadConfigFile`,
`installClaudeCode`, `setupClaudeCodeConfig`, `promptGithubAuth`, `setupCodexConfig`,
`setupOpenclawConfig`, `startGateway`, `setupZeroclawConfig`, `ensureSwapSpace`,
`openCodeInstallCmd`) — all only used within `createAgents()`
All 1410 tests pass, biome lint clean (0 errors).
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 HOME is unset (containers, systemd, cron, some CI), two files still used
`process.env.HOME || ""` which produces broken paths:
- local/local.ts:38 — uploadFile() expands ~ to "", writing config files to
filesystem root (e.g. /.openclaw/openclaw.json) instead of ~/.openclaw/
- commands.ts:898 — hasCloudConfigCredentials() checks "" + .config/spawn/
resolving to /.config/spawn/{cloud}.json, silently failing credential
detection and causing false "Missing credentials" warnings on every run
Fix: add `import { homedir } from "node:os"` to both files and change
`process.env.HOME || ""` to `process.env.HOME || homedir()`.
Completes the HOME hardening series started in #2026 and #2036.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
When a user denies OAuth access on OpenRouter or DigitalOcean, the CLI
now immediately shows a clear error message and falls back to manual
key entry, instead of silently waiting the full 120s poll timeout.
Changes:
- OpenRouter OAuth: check for `error` query param on callback, set
`oauthDenied` flag, show denial-specific HTML page in browser, break
polling loop early, and log a clear terminal error
- DigitalOcean OAuth: add `oauthDenied` flag (error detection already
existed but the polling loop still waited 120s), break loop early
Fixes#2038
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
When HOME is unset (containers, systemd, cron), process.env.HOME produces
literal "undefined" in path strings:
- ssh-keys.ts: SSH discovery/generation writes to "undefined/.ssh/"
- sprite.ts: CLI detection misses ~/.local/bin, PATH update corrupted
- gcp.ts: gcloud detection misses ~/google-cloud-sdk/bin, PATH corrupted
Same fix as #2026: use `process.env.HOME || homedir()` via `join()` for
robust OS-level fallback when HOME is unset.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Display a hint before launching `openclaw tui` warning users to set
up channels one at a time. Concurrent token pastes trigger a race
condition inside OpenClaw's TUI that causes setup to hang.
Adds an optional `preLaunchMsg` field to `AgentConfig` so any agent
can surface a user-visible tip just before its interactive session
starts. OpenClaw sets this to advise sequential channel onboarding.
Fixes#2030
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#2025
Silent credential loss in Docker/CI when HOME is unset. Use node:os
homedir() which has OS-level fallbacks and matches history.ts pattern.
Prefer process.env.HOME when set to respect test sandboxing overrides.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove 18 redundant/theatrical tests from unknown-flags.test.ts:
- Removed duplicate 'should detect --verbose as unknown' test (same name,
same assertion, nearly identical inputs as the test 28 lines above it)
- Consolidated 14 individual 'allows known flags' tests — each called
findUnknownFlag([flag]) with a single flag and expected null — into one
data-driven loop over all 17 flags; same coverage, 13 fewer test cases
- Removed 'should contain --name flag' which is fully subsumed by the
immediately following 'should contain all expected flags' test that
already verifies --name along with 22 other flags
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The hermes agent was added to manifest.json and sh/sprite/hermes.sh in
feat #2023, but createAgents() in shared/agent-setup.ts was not updated.
This caused sh/sprite/hermes.sh to throw "Unknown agent: hermes" when
resolveAgent() was called.
- Add hermes entry to createAgents() with correct install cmd, envVars, and launchCmd
- Update sprite/main.ts usage error message to include hermes
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove 162 lines of duplicate/theatrical test code across 5 files:
- manifest-integrity.test.ts: Drop 5 crude toBeTruthy() field checks
superseded by manifest-type-contracts.test.ts with precise type assertions
- manifest-type-contracts.test.ts: Remove dead deps block (0 agents have
deps) and Dotenv configuration describe block (0 agents have dotenv) —
both generated zero tests at runtime
- commands-error-paths.test.ts: Remove 5-test "swapped arguments detection"
block duplicated by the more thorough commands-swap-resolve.test.ts
- run-path-credential-display.test.ts: Remove 5-test "implementation checks"
block that retested getImplementedClouds/Agents already covered by
commands-exported-utils.test.ts; also remove now-unused imports
- manifest.test.ts: Remove redundant toBeDefined() assertion after an
already-present toHaveProperty() check on the same field
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove stale comments in test files that referenced deleted test files
(commands-untested.test.ts, commands-helpers.test.ts) and remove
"Agent: X" metadata annotations that became obsolete after the
theatrical test cleanup.
All 1424 tests pass, biome lint clean.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
* feat: SPAWN_CLI_DIR env var to force local source in e2e and shell scripts
When SPAWN_CLI_DIR is set, the entire toolchain uses local TypeScript
source instead of downloading pre-bundled scripts from GitHub releases:
- e2e.sh: auto-sets SPAWN_CLI_DIR to repo root when running locally
- provision.sh: exports SPAWN_CLI_DIR into the headless subshell
- commands.ts: reads local shell scripts instead of fetching from CDN
- All 36 cloud/agent shell scripts: exec local main.ts when set
This enables e2e tests to validate local changes before they're released.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): add path traversal defense to SPAWN_CLI_DIR script loading
Canonicalize the path via realpathSync and verify it stays inside the
resolved CLI directory before reading. Prevents SPAWN_CLI_DIR from
being used to read arbitrary files via ../ traversal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): harden SPAWN_CLI_DIR path traversal defense
- Validate cloud/agent names don't contain '..', '/' or '\' before
constructing file paths
- Fix root-directory edge case in prefix check by handling trailing
separator correctly
Agent: pr-maintainer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Remove 3 duplicate tests from security-edge-cases.test.ts that were
already covered in security.test.ts:
- validateIdentifier: "reject 65-char identifier" (duplicate of
security.test.ts "should reject overly long identifiers")
- validateScriptContent: "accept wget|sh" (duplicate of
security.test.ts "should accept scripts with wget|bash")
- validatePrompt: "accept prompts at exactly the max length" (duplicate
of security.test.ts "should accept prompts at the size limit")
The edge-cases file retains unique tests: 64-char boundary check,
single char identifiers, mkfs with multiple filesystems, extra-whitespace
pipe detection, etc.
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>
* test: Remove duplicate and theatrical tests
Found and removed three categories of test anti-patterns:
1. **Conditional always-pass guard** (`with-retry-result.test.ts`): The
`if (!result.ok)` wrapper around `expect(result.error).toBeInstanceOf(Error)`
was silently skippable if the condition ever evaluates false. Replaced with a
type-narrowing early return (`if (result.ok) { return; }`) so the assertions
always execute when the code path is reached.
2. **Duplicate `loadManifest` tests** (`manifest.test.ts`): Five tests covering
stale-cache fallback, no-cache-network-fail, invalid-fetch-fallback, fetch
timeout, and cached-instance reuse were exact duplicates of tests already
in `manifest-cache-lifecycle.test.ts` (which covers these scenarios more
thoroughly). Removed the duplicates; kept the three tests unique to
`manifest.test.ts` (fetch URL validation, fresh-cache skips network,
forceRefresh behavior).
3. **Duplicate manifest structural checks** (`manifest-type-contracts.test.ts`):
The "Cross-referential consistency" (matrix coverage, no-invalid-refs, valid
status values) and "Display name uniqueness" (unique agent/cloud names, no
key collisions) describe blocks duplicated tests already present in
`manifest-integrity.test.ts`. Removed the 6 redundant tests; the unique type
validation tests (per-field `typeof` checks) remain.
Net result: -11 tests, 0 new failures, lint clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: biome format issues in test files
Remove trailing blank line in manifest.test.ts and expand single-line
if block in with-retry-result.test.ts to satisfy biome formatter.
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: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
When a user selects a Daytona sandbox in `spawn list` and chooses
"SSH into VM", cmdConnect was missing the daytona-sandbox sentinel
handler. It fell through to the generic SSH path and tried to run
`ssh daytona@daytona-sandbox`, which fails with a cryptic error.
Added the handler mirroring the existing pattern in cmdEnterAgent.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
`gcloud compute instances create` doesn't accept `--subnet-region`.
The subnet region is automatically inferred from `--zone`. This flag
causes all GCP provisioning to fail.
Bump CLI to 0.11.3.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Dead code removed:
- `cleanup_stale_apps` function in `sh/e2e/lib/cleanup.sh` — defined but
never called; `e2e.sh` calls `cloud_cleanup_stale` directly instead
- `generateEnvConfig` and `AgentConfig` re-exports from all 7 cloud-specific
`agents.ts` modules (aws, hetzner, gcp, digitalocean, daytona, local,
sprite) — nothing imported these from the cloud modules; they were already
available via `@openrouter/spawn-shared` and `../shared/agents`
All 1435 tests pass, biome lint is clean (0 errors).
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Delete manifest-helpers.test.ts: all 5 describe blocks (isValidManifest,
readCache corruption, matrixStatus edge cases, countImplemented case
sensitivity, agentKeys/cloudKeys) are fully covered by the more
comprehensive manifest-cache-lifecycle.test.ts
- Fix always-pass assertion in manifest.test.ts: replaced
expect(typeof existsSync(...)).toBe("boolean") (always true) with
expect(manifest.agents).toBeDefined() which actually tests the outcome
-- 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: B <6723574+louisgv@users.noreply.github.com>
Remove `setupOpenclawBatched` from `packages/cli/src/shared/agent-setup.ts`.
This function was exported but never called anywhere in the codebase — it was
superseded by the composable `setupOpenclawConfig` + `startGateway` approach
used in `createAgents()`.
Bump CLI patch version to 0.11.7.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): validate launch_cmd from history before shell execution
launch_cmd from history.json was passed directly to bash -lc via SSH with no
validation, enabling command injection if the history file was tampered with.
Adds validateLaunchCmd() that blocks $(...), backticks, pipes, command chaining,
redirections, and variable expansion. Validation is applied at both merge time
(history.ts:mergeLastConnection) and execution time (commands.ts:cmdEnterAgent).
Fixes#2005
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: apply biome formatting to security.ts
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>
- Remove `runLocalCapture` from local/local.ts (exported but never called)
- Remove `listServers` from aws, hetzner, digitalocean, daytona modules
(all exported but never imported or called anywhere)
- Remove `InstanceListSchema` from aws.ts (only used in removed listServers)
- Remove now-unused imports in daytona.ts (parseJsonRaw, toObjectArray, toRecord)
- Bump CLI version 0.11.4 → 0.11.5
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>
* test: Remove duplicate and theatrical tests
- Remove duplicate `prioritizeCloudsByCredentials with real-world patterns`
describe block from run-path-credential-display.test.ts (84 lines). All
four of its scenarios were already covered by the primary
`prioritizeCloudsByCredentials` describe block in the same file.
- Remove theatrical `SPAWN_CUSTOM env var propagation` describe block from
custom-flag.test.ts (21 lines). Its two tests only verified that
`process.env` assignment works, not any application code.
No test scenarios lost; pre-existing 64 failures are unchanged.
-- qa/dedup-scanner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix trailing blank lines in test files for biome format
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: B <6723574+louisgv@users.noreply.github.com>
spawn delete hetzner was silently returning "No active servers to delete"
even when the user had active Hetzner servers. The positional arg was
parsed as agentFilter, but no agent is named "hetzner", so the filter
matched nothing. cmdList already calls resolveListFilters() which
auto-promotes a bare arg to cloudFilter when no agent matches — cmdDelete
was missing this call entirely.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(security): restrict file permissions on history and config directories
History files (history.json, last-connection.json) were created with
default permissions (0644), making server IPs, usernames, and cloud
provider details readable by other local users on shared systems.
Config directories (~/.config/spawn/) were created via mkdir -p with
default umask (0755), making them world-listable.
- Add mode: 0o600 to all writeFileSync calls in history.ts
- Add mode: 0o700 to mkdirSync calls for ~/.spawn/ directory
- Replace Bun.spawn(["mkdir","-p",...]) with mkdirSync({mode:0o700})
in hetzner, aws, digitalocean, and daytona modules
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: fix Biome formatting for multiline object literals
Expand inline `{ mode: 0o600 }` and `{ recursive: true, mode: 0o700 }`
to multiline format to satisfy Biome's formatter.
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.5 <noreply@anthropic.com>
* refactor: extract shared TTY scaffolding in picker.ts
pickToTTYWithActions (248 lines) and multiPickToTTY (204 lines) shared
~120 lines of identical TTY lifecycle code: open /dev/tty, save/restore
stty settings, raw mode, write helper, restore helper, key-read buffer,
and the read loop skeleton.
Extract withTTYKeyLoop<T>() that owns the entire TTY lifecycle and
delegates rendering and key handling via callbacks. Both picker functions
now focus solely on their mode-specific logic.
Net: 672 -> 561 lines (-111), with TTY management in a single place.
Agent: complexity-hunter
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: apply biome formatting to picker.ts
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>
- cmdlist-integration.test.ts: removed references to list-display.test.ts,
list-table-rendering.test.ts, list-empty-footer.test.ts,
list-filter-suggestions.test.ts, and list-prompt-display.test.ts (none exist)
- history.test.ts: removed stale formatTimestamp reference; the function was
removed from production code, leaving the describe block name misleading
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): prevent flag injection via hyphen-leading path segments in uploadFile
Reject remote paths where any segment starts with "-" (e.g., "-e", "/tmp/-evil")
across all 6 cloud providers. This prevents potential CLI flag injection in
commands like base64, printf, mv, and scp.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: fix biome format for path validation conditions
Break long if-conditions across multiple lines and add parentheses
around arrow function parameters to satisfy biome formatter.
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>
* test: remove duplicate listing command tests from commands-display.test.ts
The cmdMatrix, cmdAgents, cmdClouds, and related edge-case tests were
duplicated between commands-display.test.ts and cmd-listing-output.test.ts.
The listing-output file provides more thorough end-to-end coverage with
grid/compact view testing, type grouping, cross-command consistency checks,
and better edge case coverage. Remove the weaker duplicates (21 tests) from
commands-display.test.ts, keeping the unique cmdAgentInfo and cmdHelp tests.
-- qa/dedup-scanner
* style: fix trailing blank line in commands-display.test.ts
Remove extra blank line before closing brace that caused Biome format check failure.
Co-Authored-By: Claude Sonnet 4.6 <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.6 <noreply@anthropic.com>
- Remove `formatTimestamp` from commands.ts: exported but never imported
or called anywhere in the codebase
- Remove `mockFetchWithStatus` from test-helpers.ts: exported but never
imported by any test file
- Remove `createProcessExitMock` from test-helpers.ts: exported but
never imported by any test file
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: remove false-confidence do-oauth.test.ts
This file imported zero functions/constants from the DigitalOcean
source module. Every test re-created private constants (tokenRegex,
codeRegex, DO_SCOPES, SUCCESS_HTML, etc.) inline and tested them
against themselves. If source code changed, these tests still passed
silently — providing false confidence worse than no tests.
Per CLAUDE.md: "If a function is not exported, do NOT test it
(don't re-implement it inline)."
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* chore: remove plan file from commit
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>
Add `|| remotePath.includes("..")` check to hetzner, digitalocean,
and aws uploadFile functions. The regex `/^[a-zA-Z0-9/_.~-]+$/`
allows `.` characters, so paths like `../../etc/passwd` pass the
regex but are path traversal attempts. gcp, daytona, and sprite
already include this explicit check — this makes all providers
consistent.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: Remove duplicate and theatrical tests
- cmd-listing-output: Fix always-pass guard (if (localLine) → expect defined then check)
- with-retry-result: Replace conditional if (!r.ok) expects with toMatchObject
- run-path-credential-display: Remove 96 lines of duplicate tests
- parseAuthEnvVars for credential status (duplicate of commands-exported-utils.test.ts)
- credential function edge cases with weak OR assertions (duplicate of credential-hints.test.ts)
- Migrated 2 unique edge cases (whitespace trimming, empty separator) to commands-exported-utils.test.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: apply biome format to test files in qa/dedup-scanner
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>
* fix(hetzner): update deprecated cx22/cpx21 server types to cx23/cpx22
Hetzner deprecated the entire cx*2 and cpx*1 server lines on Jan 1, 2026.
New orders fail with "server type is deprecated". Updates to the current
gen3 CX and gen2 CPX lines (cx23, cx33, cx43, cx53, cpx22, cpx32).
Also shows the server type picker by default instead of requiring --custom,
so users can choose their instance size on every deploy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(zeroclaw): append autonomy config instead of overwriting onboard output
zeroclaw onboard generates a complete config with required fields like
default_temperature. Our setup was overwriting that with a partial config
missing required fields, causing a crash loop on startup. Now appends
the security/shell settings instead so onboard's fields are preserved.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix biome formatting in agent-setup.ts
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Agent: pr-maintainer
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
DigitalOcean's token exchange endpoint requires client_secret and does
not support PKCE-only public client flows. The embedded secret follows
the same pattern used by gh CLI, doctl, gcloud, and az CLI. Expanded
the comment to explain:
- Why client_secret is required (no PKCE support)
- Why embedding it is acceptable (public client, RFC 6749 §2.1)
- What security mechanisms are actually relied upon
- When the secret should be removed (if DO adds PKCE)
Fixes#1980
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(aws): increase OpenClaw gateway timeout to 120s and default to medium bundle
OpenClaw gateway consistently times out on AWS Lightsail because the 60s
timeout is too short for cold starts (npm install of 713 packages + gateway
init). Doubles the timeout to 120s and sets the default bundle for OpenClaw
to medium_3_0 (4 GB RAM) since it's too heavy for nano (512 MB).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve openclaw binary path for setsid and add npm-global to Sprite PATH
setsid replaces the process image and doesn't inherit the parent shell's
exported PATH, causing "No such file or directory" on Sprite (and potentially
other clouds). Fix by resolving the full binary path with `command -v` before
passing it to setsid. Also adds ~/.npm-global/bin to Sprite's persisted shell
PATH config so openclaw is discoverable in all session types.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(codex): update wire_api from "chat" to "responses"
Codex CLI dropped support for wire_api = "chat" — it now requires
"responses". This was never updated since the original codex integration,
causing an immediate crash loop on launch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: enable GitHub CLI auth for all agents, not just Claude Code
Only Claude Code had preProvision: promptGithubAuth — all other agents
(codex, openclaw, opencode, kilocode, zeroclaw) skipped GitHub auth
entirely. These are all coding agents that need gh access for PRs,
cloning, etc.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add missing spawn import that crashes headless mode (#1981)
runBashHeadless calls spawn() from node:child_process at line 1112,
but only spawnSync was imported. This causes a ReferenceError crash
whenever --headless mode is used.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
runBashHeadless calls spawn() from node:child_process at line 1112,
but only spawnSync was imported. This causes a ReferenceError crash
whenever --headless mode is used.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>