* test: remove Bun.spawnSync subprocess calls from ssh-keys tests
Replace Bun.spawnSync calls to ssh-keygen in createFakeKeyPair helper
with plain file writes, and mock Bun.spawnSync via spyOn for all tests
that exercise getKeyType, generateSshKey, and getSshFingerprint.
Cuts test runtime from 1212ms to ~47ms (25x speedup) and brings the
test file into compliance with the CLAUDE.md no-subprocess-spawning
policy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: apply biome formatting to ssh-keys test
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>
The sprite destroy command can hang indefinitely when the Sprite API
is unresponsive. Add a 60s timeout using the existing killWithTimeout
utility (same pattern as runSprite).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
Rebased fix/issue-2083 onto main after commands.ts split (PR #2095).
Key resolutions:
- commands.ts: kept HEAD shim (re-exports from ./commands/index.ts)
- package.json: kept PR version 0.12.0 without @openrouter/spawn-shared dep
- Fixed @openrouter/spawn-shared imports in commands/shared.ts, commands/update.ts,
and __tests__/orchestrate.test.ts that were added after the PR branched
All 1390 tests pass, biome lint clean.
Agent: pr-maintainer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The README was referencing commands.test.ts and integration.test.ts which
no longer exist (split into 20+ specialized files), and incorrectly stated
the test runner was vitest (banned — project uses bun:test). Rewrote to
accurately document all 44 test files with their coverage scope.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: centralize @clack/prompts mock in test-helpers.ts
Adds mockClackPrompts() factory to test-helpers.ts, eliminating ~15-line
duplicate mock.module blocks from 19 test files. When @clack/prompts adds
a new export, only one file needs updating instead of 19.
Fixes#2080
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: fix Biome formatting after merge with main
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>
Six of seven cloud main.ts files had hardcoded agent lists that were
stale (missing hermes, added in #2084). Replace all hardcoded lists
with Object.keys(agents).join(", ") so they stay in sync automatically
when new agents are added.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
* refactor: eliminate 7 identical agents.ts boilerplate files
Adds createCloudAgents() factory to shared/agent-setup.ts, reducing
each cloud's agents.ts from 16-line copy-paste to a single call.
Net reduction of 49 lines across 9 files.
Fixes#2078
Agent: complexity-hunter
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* chore: apply biome formatting
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#2079 — local/hermes was the only remaining missing entry in the
cloud×agent matrix. All 49 entries are now implemented.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add getSpawnCloudConfigPath(cloud) helper to shared/ui.ts, eliminating
four identical 3-line getConfigPath() functions across hetzner, daytona,
digitalocean, and aws cloud modules
- Remove duplicate homedir/join imports from hetzner, daytona, digitalocean,
and aws now that the shared helper centralizes the path construction
- Update commands.ts hasCloudConfigCredentials to use the shared helper
and drop its stale homedir import
- Bump CLI to 0.11.24 (patch)
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 14 redundant tests across two files:
- check-entity.test.ts: Remove 6 individual "valid entities" tests
(claude, codex, cline as agents; sprite, hetzner, vultr as clouds) that
are fully covered by the loop-based "all manifest agents/clouds validate
correctly" describe blocks which exhaustively test all entities.
- check-entity.test.ts: Remove 6 individual "wrong-type detection" tests
(3 clouds-as-agents, 3 agents-as-clouds) that are covered by the loop
tests "should reject every agent key when checked as cloud" and
"should reject every cloud key when checked as agent".
- cloud-init.test.ts: Consolidate 3 NODE_INSTALL_CMD tests into 1.
"is a non-empty string" is theatrical (tests the constant exists, not
what it does). Merge with the two content checks into a single test.
Test count: 1385 → 1371.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add hermes shim scripts for GCP, Hetzner, DigitalOcean, and Daytona
- Update manifest.json matrix entries from "missing" to "implemented"
- Bump default INSTALL_WAIT from 300s to 600s to fix zeroclaw timeout
on small VMs where Rust compilation takes 8-12 minutes
- Update cloud READMEs with hermes usage docs
- Bump CLI version to 0.11.18
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
API tokens never contain spaces; allowing them risks word splitting
in downstream unquoted uses of these env vars. Updated both the shell
regex in key-request.sh and the corresponding TypeScript regexes in
digitalocean.ts to stay in sync.
Fixes#2072
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- 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>