* feat: migrate shell script URLs to openrouter.ai/labs/spawn CDN
Users on older CLI versions can't auto-update because the repo was restructured
(cli/ → packages/cli/), so old version-check URLs 404. This decouples the CLI
from the repo's internal directory structure:
- Shell script URLs (install, agent scripts, github-auth) now use
openrouter.ai/labs/spawn/* as primary with GitHub raw as fallback
- Version checks now use GitHub release artifact (cli-latest/version)
as primary — a static URL that never changes regardless of repo layout
- CI workflow updated to publish a `version` file alongside cli.js
- Remove GITHUB_RAW_URL_PATTERN validation (no longer needed since
install URL is now a hardcoded CDN string, not interpolated)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix biome formatting in update-check test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: CLAUDE.md says biome lint but should say biome check
biome lint only checks lint rules, not formatting. biome check does both.
The hooks and CI already run biome check — the docs were out of sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(hooks): PostToolUse hook wasn't running biome on CLI source files
Two bugs in validate-file.ts:
1. Config search only checked 1-2 levels up from the edited file, but
biome.json is at packages/cli/ — 3 levels above src/__tests__/*.ts.
Fix: walk up directories until biome.json is found (or hit root).
2. Ran `biome format` (prints formatted output, always exits 0) instead
of `biome format --check` (exits non-zero if file needs formatting).
Fix: use `biome check` which does lint + format check in one pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(history): smart trimming evicts deleted records first, archives overflow
When history exceeds 100 entries, deleted records (useless for `spawn ls`)
are now evicted first. If still over the limit, oldest non-deleted records
are also trimmed. All evicted records are archived to dated backup files
(history-YYYY-MM-DD.json) so nothing is permanently lost.
Previously, blind .slice() could silently discard records with active
connections that `spawn ls` depends on.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: fix biome formatting issues
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: Remove redundant loadTokenFromConfig wrappers in hetzner, daytona, digitalocean
The previous PR (#2151) introduced shared loadApiToken() in shared/ui.ts and
updated hetzner/daytona to delegate to it via thin wrapper functions. This
commit removes the now-unnecessary wrapper functions entirely, inlining the
loadApiToken() calls directly at the callsite.
Also removes the 16-line duplicate loadTokenFromConfig() implementation in
digitalocean.ts (which replicates the same api_key/token field reading and
regex validation logic as loadApiToken) and replaces it with a direct call to
loadApiToken("digitalocean").
-- qa/code-quality
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* bump version to 0.12.12 (main already has 0.12.11)
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Fixes#2156
The spinner was stopped with a success message before the HTTP response
body stream was fully consumed. If the stream failed mid-transfer (network
drop, truncation), users saw "Script downloaded" followed by a confusing
downstream error. Now both the primary and fallback paths await res.text()
before calling s.stop().
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Extract duplicate loadTokenFromConfig helper (hetzner + daytona) into
shared loadApiToken() in shared/ui.ts, eliminating 24 lines of
duplicate validation logic across two cloud modules
- Move misplaced FETCH_TIMEOUT and UPDATE_BACKOFF_MS constants in
update-check.ts from the Schemas section into the Constants section
where they belong (stale empty section header fix)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
After showing post-session messages, the local process now exits cleanly
instead of requiring an extra Ctrl+C. The root cause was that after main()
resolved, lingering event loop handles (from @clack/prompts stdin listeners,
fetch connections, etc.) prevented Node/Bun from exiting naturally.
The fix adds process.exit(0) on successful main() completion, which covers
all session paths (bash script execution via execScript, SSH reconnection
via cmdConnect, and agent re-entry via cmdEnterAgent).
Fixes#2145
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address 4 reliability issues across codebase
1. sprite.ts: add --force to destroy command (stdin is "ignore" so
interactive prompts would hang until 60s timeout)
2. verify.sh: replace /dev/tcp port checks with ss -tln primary
(Debian/Ubuntu bash compiled without /dev/tcp support)
3. verify.sh: make _openclaw_restart_gateway a hard failure instead
of log_warn (matching _openclaw_ensure_gateway behavior)
4. agent-setup.ts: add ss -tln port check + "already running" early
exit + increase timeout from 120s to 300s (gateway takes ~3min
to initialize on AWS medium instances)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: biome format - use consistent double quotes in portCheck
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- sprite/sprite.ts: Replace duplicate saveVmConnection implementation
with a call to the shared saveVmConnection from history.ts. The local
version duplicated the mkdir + writeFileSync logic already provided by
the shared function, just with Sprite-specific hardcoded values.
Remove now-unused writeFileSync, mkdirSync, and getSpawnDir imports.
- Bump CLI version 0.12.5 → 0.12.6 (patch)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The SC2154 (referenced but not assigned) comment was leftover from a
prior version of the script. No such external variable is referenced in
the current implementation, making the suppression comment stale.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes underscore and hyphen from the OAuth authorization code
validation regex, restricting it to alphanumeric characters only.
Defense in depth: if the code is ever used in logging or other
contexts, special characters won't create injection opportunities.
Fixes#2114
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add 30s AbortSignal.timeout to all cloud API fetch wrappers
All four cloud provider API client wrapper functions (lightsailRest,
hetznerApi, doFetch, daytonaApi) were missing fetch timeouts, while
every other fetch call in the codebase already used AbortSignal.timeout.
A stalled TCP connection to any cloud provider would cause the CLI to
hang indefinitely with no user feedback or recovery path.
Agent: team-lead
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: apply biome formatting to fetch timeout changes
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
daytonaApi() returned the raw response body on all final attempts regardless
of HTTP status. destroyServer() checked hasApiError() which only matched 4xx
patterns, so persistent 500/502/503 responses were silently treated as
success — users were told "Sandbox destroyed" when billing continued.
Fix: throw on !resp.ok after retries exhaust, consistent with other cloud
modules (aws, gcp). destroyServer() now uses try/catch. testDaytonaToken()
already had try/catch so the hasApiError() check was redundant.
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 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>
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>
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>
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>
- 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>
- 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>
- 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>
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>
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), 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>
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 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>
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>
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>
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): 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>
* 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>
* 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>
* feat!: remove Fly.io cloud provider support
Drop Fly.io as a supported cloud provider. Sprite (which uses Fly.io
infrastructure internally) is retained.
- Delete packages/cli/src/fly/ module, sh/fly/ scripts, fixtures/fly/
- Remove fly cloud entry and 6 fly matrix entries from manifest.json
- Remove fly imports, destroy cases, and connection handlers from commands.ts
- Remove fly-ssh sentinel from security.ts
- Port E2E test suite from Fly.io to AWS Lightsail (fly-e2e.sh → aws-e2e.sh)
- Update README (7 clouds, 42 combinations), CLAUDE.md, and skill prompts
- Clean up fly references in build config, gitignore, icon sources
- Bump CLI version to 0.11.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: restore Docker image build under sh/docker/
Move openclaw Dockerfile from sh/fly/docker/ to sh/docker/ and rename
workflow from fly-docker.yml to docker.yml with updated paths.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: fix extra blank lines in commands.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
* fix: add ~/.npm-global/bin to OpenClaw PATH for gateway, launch, and reconnect
OpenClaw installs to ~/.npm-global/bin/ via npm, but startGateway() and
launchCmd() only included ~/.bun/bin and ~/.local/bin in PATH — so the
`openclaw` binary was never found on non-Fly clouds (DigitalOcean, Hetzner,
AWS, GCP). Fly was unaffected because it uses setupOpenclawBatched() which
correctly includes the npm-global path.
Three fixes:
1. startGateway(): add $HOME/.npm-global/bin to PATH
2. launchCmd(): add $HOME/.npm-global/bin to PATH
3. install(): persist PATH to ~/.bashrc and ~/.zshrc (matching codex/kilocode
pattern) so reconnects via `spawn openclaw <cloud> --name ...` also work
Closes#1965
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct command chaining and idempotency in npm-global PATH setup
- Use curly braces to group grep||echo so PATH append only runs after
successful npm install (fixes operator precedence bug)
- Skip ~/.zshrc modification when file doesn't exist (avoids creating
it on non-zsh systems)
- Use grep -qF for literal string matching (no regex interpretation)
- Apply fix to all three affected agents: openclaw, codex, kilocode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
* fix: add npm-global/bin to PATH for openclaw startGateway and launchCmd
Fixes crash where openclaw gateway fails to start on non-Fly clouds
(DigitalOcean, Hetzner, AWS, GCP) because ~/.npm-global/bin was absent
from PATH in startGateway() and launchCmd(). Fly was unaffected because
setupOpenclawBatched() already included the correct PATH.
Fixes#1965
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: fix Biome format error on launchCmd line
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>
- Remove CACHE_DIR dead export from manifest.ts (was defined but never imported anywhere)
- Add parseJsonObj() to @openrouter/spawn-shared for parsing JSON objects
- Remove 4x duplicate local parseJson/LooseObject definitions from hetzner, digitalocean, daytona, fly cloud modules
- Remove now-unused `import * as v from "valibot"` from all 4 cloud modules
- Bump CLI to 0.10.24
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 the `runWithRetry` function exported from 4 cloud modules (aws, hetzner, gcp, digitalocean)
that were defined but never called anywhere in the codebase. Only `fly.ts` uses its own
`runWithRetry` internally, so that definition is preserved.
Also bump CLI version to 0.10.22 per version policy.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Both fly.ts and daytona.ts defined a local `sleep` helper identical to the
one already exported from shared/ssh.ts. Remove the local copies and import
the shared function instead, consistent with all other cloud modules.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>