- update-check.test.ts: mock execFileSync for re-exec path added in eea43ad,
account for findUpdatedBinary() "which spawn" call, update bare-spawn test
to expect re-exec instead of "Run your spawn command again"
- upload-file-security.test.ts: fix sprite classification to match
"sprite $(...) exec" with org flags; remove daytona from strict allowlist
regression list (uses printf %q escaping, validated by general exec tests)
- version-comparison.test.ts: mock execFileSync for auto-update integration test
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds a GITHUB_REPO_PATTERN allowlist check to refreshAgentStats() so that
only well-formed owner/repo strings reach `gh api repos/…`. A malicious
or corrupted manifest.json entry with shell metacharacters in the repo
field is now rejected before it is interpolated into the command argument.
Fixes#1527
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
validateConnectionIP rejected "localhost" (written by local cloud) and
hostnames like "ssh.app.daytona.io" (written by Daytona), causing
mergeLastConnection to silently discard connection data. This broke
spawn list and spawn delete for these providers.
- Add "localhost" to CONNECTION_SENTINELS
- Add HOSTNAME_PATTERN for valid multi-label DNS hostnames
- Update tests: localhost now valid, add hostname acceptance/rejection tests
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Two bugs in reExecWithArgs():
1. args.length === 0 early exit:
Running bare `spawn` (interactive picker) after an auto-update would
print "Run your spawn command again" and exit, requiring the user to
manually re-invoke. Now always re-exec so the new flow triggers
immediately.
2. process.argv[1] stale binary path:
If the installer places the updated binary in a different directory than
the currently running binary (e.g. old: ~/.local/bin, new: /usr/local/bin),
re-exec would run the old stale binary. Fix: add findUpdatedBinary() which
resolves via `which spawn` (PATH lookup) first, falling back to
process.argv[1] only if which fails.
Bump CLI version 0.5.17 → 0.5.18.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Users who name their spawns via the interactive "Name your spawn" prompt
cannot see those names in `spawn list` output. Multiple spawns of the
same agent/cloud combo (e.g. two "Claude Code on Hetzner") are
indistinguishable despite having different names.
Show the spawn name in both interactive picker labels and non-interactive
table output so users can tell their spawns apart.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
printf -v was introduced in bash 4.0 but macOS ships bash 3.2.
_update_retry_interval() in shared/common.sh used printf -v and is called
from generic_ssh_wait and _cloud_api_retry_loop — meaning ALL SSH
connectivity checks and cloud API retries would fail on macOS with:
"printf: -v: invalid option"
Changes:
- shared/common.sh: replace printf -v with eval in _update_retry_interval()
- shared/common.sh: remove dead code in calculate_retry_backoff() where
next_interval was computed but never used
- shared/key-request.sh: same printf -v fix
- test/macos-compat.sh: add MC013 rule to catch printf -v in future
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
shared/common.sh — prompt_spawn_name():
Replace log_info with safe_read so user confirms (or overrides) the
derived kebab-case resource name before it's used for any cloud resource:
Spawn name (e.g. "My Dev Box"): My Claude Box
Resource name [my-claude-box]: ⏎ ← press Enter to accept
fly/lib/common.sh — _try_fly_browser_auth():
- Print auth URL prominently on its own line (not just as a warning)
so sandbox users can copy-paste it into their local browser
- Suppress open_browser errors (|| true) so the script doesn't abort
if no browser is available
- Add explicit sandbox hint while polling
- After 120s timeout: offer manual API token entry as a last resort
with a direct link to fly.io/dashboard → Tokens
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove nonexistent `ensure_daytona_cli` call from Daytona delete script
(causes "command not found" error when running `spawn delete` on Daytona)
- Add Fly.io SSH handler in cmdConnect to use `fly ssh console -a NAME`
instead of falling through to broken `ssh root@fly-ssh` path
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: replace hardcoded ~/.spawn/history.json path in security.ts error messages
Error messages in security validation functions (validateConnectionIP,
validateUsername, validateServerIdentifier, validateMetadataValue) hardcoded
~/.spawn/history.json as the fix path. This is wrong when SPAWN_HOME is set,
directing users to a nonexistent file. Replace all 9 occurrences with
'spawn list --clear' which works regardless of SPAWN_HOME and is simpler
than manually editing JSON.
Agent: ux-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: bump cli version to 0.5.17
Required by CLAUDE.md: any change to cli/ needs a version bump.
PR #1520 changes security.ts error messages (cli/ change).
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>
Prevent cloud provider API tokens from being visible in ps aux output by passing Authorization headers via curl's -K - (config from stdin) instead of command-line arguments.
* fix: Daytona SSH gateway compatibility — resource overrides, base64 uploads, connection throttling
Daytona's SSH gateway has several limitations that caused hangs and failures:
1. **Resource overrides require image-based creation**: Snapshot-based sandboxes
reject cpu/memory/disk fields. Use buildInfo.dockerfileContent (FROM image)
to switch to image-based creation, which unlocks resource overrides.
Default: 2 vCPU, 4 GiB RAM, 30 GiB disk (configurable via env vars).
2. **SCP/SFTP not supported**: Gateway returns HTTP 404 for SCP subsystem.
Upload files via base64-encoded SSH command channel instead.
3. **Connection limit (~10-15 per token)**: Consolidated wait_for_cloud_init
from 6 SSH calls into 1. Added 1s sleep between SSH operations to let
the gateway release connection slots.
4. **Port flag incompatibility**: Changed -p PORT to -o Port=PORT so the
port works for both ssh and scp (scp interprets -p as preserve timestamps).
5. **install_claude_code improvements**: Added npm as install method (most
reliable for global installs), added .npm-global/bin to PATH.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address security review — escape remote_path, validate image name
- upload_file: escape single quotes in remote_path before embedding in
the SSH command string (b64 content is inherently safe — base64 alphabet
is [A-Za-z0-9+/=] only, no shell metacharacters)
- create_sandbox: validate DAYTONA_IMAGE against [a-zA-Z0-9./:_-] to
reject malformed image names before sending to the API
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: harden upload_file() — validate base64 + use printf %q for paths
Address security review feedback on PR #1517:
CRITICAL: Add explicit base64 alphabet validation before embedding
encoded content in SSH command string. While base64 output is
inherently safe ([A-Za-z0-9+/=]), the validation guards against
corrupted/unexpected encoder output.
MEDIUM: Replace manual single-quote escaping for remote_path with
printf %q, which is the standard shell-safe escaping mechanism and
handles all special characters including path traversal attempts.
Tests: 110/110 pass, bash -n clean.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- ensure_api_token_with_provider now validates env-var tokens with
the provider test function, matching config-file token behavior.
Previously, stale env-var tokens silently passed auth and failed
at server creation with cryptic API errors.
- Add prompt_spawn_name to Hetzner and Daytona cloud_authenticate,
matching the pattern used by AWS, Fly, GCP, DigitalOcean, and
Sprite. Without this, SPAWN_NAME_KEBAB is never set and server
name prompts have no pre-filled default on these two providers.
- Remove redundant register_cleanup_trap from DigitalOcean
cloud_authenticate. shared/common.sh auto-registers the trap at
source time (line 3696), making the explicit call dead code.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
After an SSH/exec session ends, the post-session summary warns users
their server is still running and directs them to the cloud dashboard.
It never mentions `spawn delete`, the CLI's own deletion command.
Add a "spawn delete" hint to both _show_post_session_summary (SSH
clouds) and _show_exec_post_session_summary (exec clouds) so users
discover the feature at the moment they most need it.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: switch Codex wire_api from "responses" to "chat" for multi-turn stability
The Responses API format causes "Invalid Responses API request" errors on
the second turn and beyond — conversation history items round-trip through
OpenRouter with null content fields and missing IDs that fail validation.
Chat Completions format is fully supported and avoids this issue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pin Codex to 0.94.0 + wire_api=chat for multi-turn stability
OpenRouter's Responses API proxy drops required fields (id, content) from
conversation-history items on multi-turn requests, causing "Invalid
Responses API request" at input[6]+. Codex >=0.97.0 removed wire_api=chat
support (openai/codex#10157), so we pin to 0.94.0 — the last release where
Chat Completions format still works.
Tracking: https://github.com/openai/codex/issues/12114
TODO: unpin once OpenRouter /responses handles round-trip correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
ensure_fly_token() called _load_token_from_config with only 1 argument
(config file path) but the function requires 3 (config_file, env_var_name,
provider_name). The empty env_var_name fails the security validation regex,
so the function always returns 1 silently. Users with saved Fly.io tokens
in ~/.config/spawn/fly.json were forced to re-authenticate every session.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The test's runCli() helper used \${process.env.HOME}/.bun/bin/bun as
the subprocess command. The test preload sandboxes HOME to a temp dir,
so this path resolves to a nonexistent file, causing ENOENT and 49/56
test failures.
Fix: use bare "bun" (resolved via PATH), matching the pattern in
cli-version-and-dispatch.test.ts and cmdrun-resolution.test.ts.
All 56 tests in cli-entry-edge-cases.test.ts now pass.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Change "Enter a name for this spawn (optional)" to "Name your spawn"
and remove the restrictive alphanumeric-only validation. Display names
can now include spaces, uppercase, and special characters (e.g.
"My Claude Box"). The shell scripts derive a kebab-case slug for the
actual cloud resource name via _to_kebab_case() in shared/common.sh.
Bump CLI version 0.5.14 → 0.5.15.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rewrite hetzner common.sh + fix token prompt bug in shared/common.sh
Hetzner: rewrote from 621 to 224 lines. Removed hcloud CLI dual-path
fallback, server type validation/fallback chain (11 functions), and
duplicate CLI+API implementations. Now API-only like DigitalOcean.
Shared: fixed echo "" in _prompt_for_api_token, get_openrouter_api_key_manual,
and get_openrouter_api_key_oauth writing to stdout instead of stderr.
These functions are called inside $(...) command substitutions, so the
newlines got prepended to the captured token, causing "unable to
authenticate" errors when pasting tokens at the prompt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rewrite daytona common.sh — API-only, drop CLI dependency
Rewrote from 312 to 174 lines. Removed daytona CLI dependency in
favor of direct REST API calls. Matches the same API-only pattern
used by Hetzner, DigitalOcean, and other clouds.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pass SSH port to control master exit in daytona interactive/destroy
The ssh -O exit command to close the multiplexed master was missing
the -p PORT flag when DAYTONA_SSH_PORT is set. This left the master
connection open, causing "mux_client: master did not respond" errors
when the interactive session tried to allocate a PTY.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Two error messages told users to run 'spawn clear-history' when
encountering corrupted history files, but that command does not exist.
The actual command is 'spawn list --clear'. Users got a confusing
"Unknown agent or cloud: clear-history" error when following the advice.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Switch runCli helper from execSync to spawnSync so stderr is always
captured (execSync only returns stderr on non-zero exits, causing
extra-arg warning tests to fail). Add --dry-run to tests that pass
valid agent+cloud combos to avoid triggering actual script execution
and timing out under bun's 5s per-test limit.
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
5 of 6 Sprite agent scripts silently skipped saving connection info
for 'spawn list', because only sprite/claude.sh defined the
agent_save_connection hook. All other clouds save connection info in
their create_server() equivalent; move save_vm_connection into
cloud_provision() in sprite/lib/common.sh to match that pattern and
cover all agents uniformly. Remove now-redundant agent_save_connection
from sprite/claude.sh.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace the prompt-first auth flow with a browser-based CLI session
flow (same as `fly auth login`). The new auth chain is:
1. Environment variable (FLY_API_TOKEN)
2. Saved config file (~/.config/spawn/fly.json)
3. flyctl CLI (`fly auth token`)
4. Browser OAuth via Fly.io CLI Sessions API (NEW)
5. Manual token prompt (last resort fallback)
The browser flow creates a CLI session via POST /api/v1/cli_sessions,
opens the auth URL in the user's browser, then polls for the access
token. This is the same mechanism flyctl uses internally.
Also add _sanitize_fly_token() to handle the Fly dashboard copy button
which includes the display name before the token (e.g. "Deploy Token
FlyV1 fm2_..."). The sanitizer strips everything before "FlyV1" or
extracts bare "fm2_" tokens, and trims whitespace/newlines. Applied
at every token entry point (env var, config, manual prompt).
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add spawn name prompt and project confirmation to GCP flow
Ask for spawn name upfront (before auth), derive kebab-case default for
VM naming, and confirm the current GCP project before using it.
New interaction order:
1. Spawn name: "My Dev Box" → kebab "my-dev-box" exported as
GCP_INSTANCE_NAME_KEBAB
2. gcloud auth + project confirm: "Current project: X Keep? [Y/n]"
If no → project picker shown
3. SSH key
4. Machine type picker (existing)
5. Zone picker (existing)
6. Instance name prompt: "Instance name [my-dev-box]: "
User can press Enter to accept or type a custom name
New functions:
_to_kebab_case() — lowercases, replaces non-alnum with hyphens
_gcp_prompt_spawn_name() — prompts for display name, exports kebab default;
honours SPAWN_NAME env var set by CLI (--name flag)
Modified:
_gcp_resolve_project() — adds Y/n confirmation when project already set
get_server_name() — shows kebab default in prompt, accepts Enter
cloud_authenticate() — calls _gcp_prompt_spawn_name first
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat: add spawn name prompt to all clouds via shared/common.sh
Move _to_kebab_case() and prompt_spawn_name() to shared/common.sh so all
clouds get upfront spawn name prompting and kebab-based resource naming.
shared/common.sh:
+ _to_kebab_case() — "My Dev Box" → "my-dev-box"
+ prompt_spawn_name() — asks for display name, exports SPAWN_NAME_DISPLAY
and SPAWN_NAME_KEBAB; skips if already set;
honours SPAWN_NAME env var from CLI --name flag
~ get_resource_name() — replaces silent SPAWN_NAME fallback with a visible
prefilled default: "Enter server name [my-dev-box]: "
Per-cloud changes (cloud_authenticate gains prompt_spawn_name first):
hetzner, fly, aws, daytona, digitalocean, sprite — one-line change each
gcp/lib/common.sh:
- Remove _to_kebab_case() (now in shared)
- Remove _gcp_prompt_spawn_name() (now in shared as prompt_spawn_name)
~ cloud_authenticate: _gcp_prompt_spawn_name → prompt_spawn_name
~ get_server_name: simplified back to get_validated_server_name
(shared get_resource_name now shows the kebab default in the prompt)
Result — every cloud shows this flow upfront:
Spawn name (e.g. "My Dev Box"): My Claude Box
ℹ Resource name: my-claude-box
...
Enter server name [my-claude-box]: ⏎
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* fix: use "Use project '...'?" instead of "Keep this project?" in GCP prompt
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* feat: add spawn pick to _display_and_select in shared/common.sh
All clouds using interactive_pick (Hetzner, DigitalOcean, AWS, fly, etc.)
now get the arrow-key picker UI when the user runs via `spawn`.
Placement: between fzf (rarely installed) and numbered list (plain fallback).
Priority: fzf > spawn pick > numbered list.
Pipe-delimited items "id|field2|field3..." are converted to tab-delimited
"id\tid\tfield2 · field3 · ..." so spawn pick displays:
> cx22 2 vCPU · 4.0 GB RAM · 40 GB disk · shared · $ 0.0057/hr
> fsn1 Falkenstein · DE
The --default flag uses default_id when set, otherwise default_value,
so the correct item is pre-selected when the picker opens.
No 2>/dev/tty redirect (avoids the zsh 'file exists' failure that broke
the GCP picker; spawn pick opens /dev/tty internally via fs.openSync).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace custom _gcp_interactive_pick with shared interactive_pick
- Remove _gcp_interactive_pick (60 lines of custom picker logic)
- Convert option functions to pipe-delimited format (id|detail)
to match what interactive_pick / _display_and_select expect
- Replace _gcp_pick_{machine_type,zone,project} with direct
interactive_pick calls — same pattern as Hetzner
- _gcp_project_options: awk now outputs id|name instead of id\tid\tname
GCP now gets fzf → spawn pick → numbered list for free via the
shared helper, with no cloud-specific picker code.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The 2>/dev/tty redirect caused spawn pick to exit 1 on zsh/macOS
with 'file exists: /dev/tty', silently breaking the picker and
always falling through to the numbered-list fallback.
spawn pick renders its arrow-key UI by opening /dev/tty directly
via fs.openSync() — it never uses stderr for the UI — so the
redirect served no purpose and only caused failures.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replace the icon-only refresh-favicon skill with a comprehensive
update-metadata skill using TypeScript + Bun. The script fetches
live GitHub stats (stars, license, language) and refreshes icons,
with metadata completeness validation.
- update.ts: runnable script (bun run .claude/skills/update-metadata/update.ts)
- Supports --agent, --dry-run, --icons-only, --stats-only flags
- Uses gh api for GitHub data, native fetch for icon downloads
- Validates all 12 metadata fields per agent
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enrich each agent entry with curated metadata fields: creator, repo,
license, created/added dates, GitHub stars, language, runtime, category,
tagline, and tags. This helps users compare and choose agents.
- Extend AgentDef interface with 12 optional metadata fields
- Add metadata to all 6 agents in manifest.json
- Add type validation tests for new fields
- Bump CLI version 0.5.12 → 0.5.13
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add SPAWN_SKIP_API_VALIDATION=1 and SPAWN_SKIP_GITHUB_AUTH=1 to
sprite test environment so verify_openrouter_key() doesn't make real
HTTP calls with the fake test key (which gets 401, clears the key,
and falls into OAuth — causing all sprite assertions to fail)
- Update agent iteration lists from stale "claude openclaw nanoclaw" to
current "claude openclaw codex opencode kilocode zeroclaw"
- Remove dead nanoclaw case from _assert_agent_specific
- Remove 5 dead agent cases (nanoclaw, cline, gptme, plandex, continue)
from _shared_agent_assertions.sh, add zeroclaw
Result: 108 passed, 0 failed (was: 48 passed, 18 failed)
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The json_escape fallback (used when python3 is unavailable) only escaped
backslashes and double quotes, producing invalid JSON when input contained
newlines, tabs, or carriage returns. This could cause JSON injection in
API request bodies sent to cloud providers (Hetzner, DigitalOcean, Fly.io)
and corrupt credential config files.
Add escaping for \n, \r, and \t in the fallback path. The python3 primary
path (json.dumps) was already correct.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Download favicon/icons for all 6 agents into assets/agents/:
- claude.png — Anthropic GitHub org avatar (4.5K)
- openclaw.png — openclaw.ai/apple-touch-icon.png (5.8K)
- zeroclaw.png — zeroclaw-labs GitHub org avatar (11K)
- codex.png — OpenAI GitHub org avatar (4.0K)
- opencode.svg — opencode.ai/favicon.svg (612B)
- kilocode.png — Kilo-Org GitHub org avatar (1.3K)
Update manifest.json icon fields to point to raw.githubusercontent.com
URLs for the local files (stable, CDN-served, versioned in repo).
Add assets/agents/.sources.json tracking each agent's canonical source
URL and extension for use by the refresh-favicon skill.
Add .claude/skills/refresh-favicon/SKILL.md — a skill that re-downloads
all agent icons from their source URLs, detects content types, updates
.sources.json, and syncs manifest.json icon fields.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Remove backslash before $ in regex pattern so it anchors to end-of-string
rather than matching a literal dollar sign. This restores proper validation
of OAuth codes (16-128 alphanumeric chars only).
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When server creation fails with account-level errors (server limit
reached, insufficient funds, quota exceeded), offer to switch to a
different Hetzner API token and retry instead of just failing.
- Add _is_hetzner_account_error() to detect account-level issues
- Return exit code 2 from create_server() for account errors
- cloud_provision() catches code 2, prompts "Try a different account?"
- On yes: re-prompts for new API key, re-registers SSH keys, retries
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replace both the trap-clobbering `trap 'rm -f ...' EXIT` calls and the
inline `rm -f` approach with `track_temp_file()` from shared/common.sh.
This registers temp files with the centralized cleanup handler that is
already set up on EXIT/INT/TERM, so:
- Temp files are cleaned up even on interrupt (not just success path)
- The calling script's EXIT trap is never clobbered
- _sprite_retry wrappers are preserved for transient error recovery
Agent: pr-maintainer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: persist gh auth credentials to disk for interactive sessions
When GITHUB_TOKEN is in the environment, gh auth status returns success
(gh checks env vars first), so ensure_gh_auth() short-circuits before
gh auth login --with-token writes credentials to ~/.config/gh/hosts.yml.
The interactive session starts without GITHUB_TOKEN in env, so gh reports
"not logged into any GitHub hosts".
Fix: always run gh auth login --with-token when GITHUB_TOKEN is set,
persisting credentials to disk regardless of gh auth status.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: unset GITHUB_TOKEN env var before gh auth login --with-token
gh refuses to store credentials when GITHUB_TOKEN is already set in
the environment: "The value of the GITHUB_TOKEN environment variable
is being used for authentication." Save the value, unset the env var,
pipe it to gh auth login, then re-export.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address security review — validate token format, skip if already persisted
- Add GITHUB_TOKEN format validation (ghp_, gho_, ghu_, ghs_, ghr_, github_pat_)
- Add fast path: check gh auth status with env var unset before persisting
- Document plaintext credential store behavior (standard gh CLI behavior)
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 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>
Codex CLI's OPENAI_BASE_URL env var approach causes "Invalid Responses
API request" errors because OpenRouter doesn't fully support the
Responses API wire format via base URL override. Switch all 8 codex
scripts to use ~/.codex/config.toml with model_provider="openrouter"
which uses the native OpenRouter integration.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Why: `set -eo pipefail` + `output=$(shellcheck ...)` on line 659 of
test/run.sh causes immediate exit when shellcheck finds any warning,
preventing the entire shell test suite from running. 53 CLI tests also
fail due to stale assertions after agents/clouds were removed in recent
PRs.
Fixes:
- test/run.sh:659 — add `|| true` to shellcheck command substitution so
shell test suite runs to completion even when scripts have warnings
- manifest-real-data.test.ts — lower agent count min from 10→5,
matrix count min from 80→40 (now 6 agents, 48 matrix entries)
- agent-env-injection-contract.test.ts — lower script count min
from 70→40 (now 47 implemented scripts)
- script-conventions.test.ts — same script count fix (70→40)
- cloud-lib-source-chain.test.ts — lower cloud lib min from 9→8
(OVH removed, now 8 clouds)
- commands-credential-display-internals.test.ts — add missing
@clack/prompts mock (tests call p.log.error but never mocked it)
- commands-exported-helpers-edges.test.ts — fix environment-dependent
assertion: only check credential-based hintOverrides, not
CLI-installed ones (sprite CLI is installed in CI/dev)
- agent-config-setup.test.ts — fix stale model ID assertion
("openrouter/anthropic/..." → "anthropic/...") and stale mkdir
command ("rm -rf && mkdir" → "mkdir -p")
- agent-info-quickstart.test.ts — remove sprite from singleAuthManifest
fixture (sprite CLI installed causes sprite to be prioritized over
hetzner, breaking 4 tests); update count assertions for single cloud
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Why: test/record.sh used local -n (bash 4.3+ namerefs) which crashes
on macOS's default bash 3.2, breaking contributor workflow for recording
API fixtures. Fixes#1480.
Inlines the _export_env_vars_from_fields helper directly into
_load_multi_config_from_file, eliminating the nameref dependency while
preserving the security validation of env var names.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Sprite API calls intermittently fail with TLS handshake timeouts and
connection resets. Add _sprite_retry() wrapper that retries up to 3
times with 3s delay on transient errors.
Wrapped calls: sprite create, sprite exec (run_sprite), sprite exec
-file (upload_file_sprite, setup_shell_environment uploads).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
READMEs across all 8 clouds still referenced 5 removed agents
(NanoClaw, Cline, gptme, Plandex, Continue) and were missing
ZeroClaw. Users following these docs got 404 errors.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Release assets use x64 not x86_64 (opencode-linux-x64.tar.gz) and
darwin not mac (opencode-darwin-arm64.tar.gz). The arch mapping only
handled aarch64→arm64 but missed x86_64→x64, causing 404 on all
x86_64 servers.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: sprite npm PATH resolution and gateway timeout
Sprites use nvm-managed node, so npm global bin is at
/.sprite/languages/node/nvm/.../bin/ which isn't in default PATH.
Dynamically resolve $(npm prefix -g)/bin in install, launch, and
gateway commands for all sprite agents.
Also increase openclaw gateway timeout from 30s to 60s — gateway
starts slowly on sprites but TUI connects once ready.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add opencode bin dir to PATH in sprite launch command
OpenCode installs to $HOME/.opencode/bin/ which isn't in the sprite's
default PATH or the npm prefix path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Why: local/opencode was listed as 'missing' in manifest.json — users
could not run OpenCode on their local machine via spawn.
- Add local/opencode.sh following the same pattern as other local scripts
(sources lib/common.sh, uses opencode_install_cmd from shared/common.sh,
injects OPENROUTER_API_KEY via generate_env_config)
- Update manifest.json matrix entry from 'missing' to 'implemented'
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: pass -o org flag to all sprite CLI commands
sprite create/exec/list/destroy fail with "authentication failed" when
the org isn't passed explicitly. Detect the selected org after login and
thread it through all sprite commands via _sprite_org_flags().
Also fix ensure_sprite_authenticated to fail loudly instead of
swallowing errors with || true.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: sprite scripts fail when zsh is not available
setup_shell_environment overwrites .bashrc with `exec zsh`, but sprites
don't have zsh installed. This breaks PATH and causes all agent launch
commands that source .zshrc to fail.
- Only switch to zsh if it's actually available on the sprite
- Replace `source ~/.zshrc` with explicit PATH in all sprite agent
launch commands (openclaw, opencode, codex, kilocode)
- Fix start_openclaw_gateway to use explicit PATH instead of .zshrc
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: openclaw not found on sprite — bashrc corruption from prior runs
On reused sprites, .bashrc still has `exec /usr/bin/zsh -l` from a prior
run. Sourcing it in the install command causes `&&` to short-circuit, so
`bun install -g openclaw` never runs.
- Clean up stale `exec zsh` lines from .bashrc at start of
setup_shell_environment (fixes reused sprites)
- Use explicit PATH in openclaw install command instead of relying on
.bashrc
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use npm instead of bun for openclaw install on sprite
bun 1.3.9 on sprites fails with "connection closed" during dependency
resolution. Other sprite agents (codex, kilocode) already use npm
successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: openclaw install — npm+bun fallback, verify binary exists
Try npm first (more reliable on sprites), fall back to bun, then verify
the binary is actually in PATH before continuing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: persist npm global bin path to .spawnrc on sprites
npm installs openclaw successfully but its global bin dir isn't in the
sprite's default PATH. Detect the npm bin path after install, write it
to .spawnrc so gateway and launch commands (which source .spawnrc) find
the binary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Point OpenClaw to https://github.com/openclaw/openclaw and OpenCode to
https://github.com/anomalyco/opencode. Update the OpenCode install command
and binary download URL to match the new repo.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>