Commit graph

1328 commits

Author SHA1 Message Date
A
3af5005896
fix: pass response via env var in record.sh has_api_error (SC2259) (#1559)
The heredoc overrode piped stdin, so $response never reached python3.
sys.stdin.read() got empty input, making API error detection silently
fail during live fixture recording. Pass data via environment variables
instead.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 05:47:50 -05:00
A
c1f730c69a
fix: replace eval with declare and add base64 validation (#1557)
* fix: replace eval with declare and add base64 validation (issues #1554, #1555)

- shared/key-request.sh: replace eval with declare for defense-in-depth
  (eval avoided when safer declare alternative exists; validated vars stay safe)
- fly/lib/common.sh: add base64 output alphabet validation before shell
  interpolation, matching daytona/lib/common.sh proven-safe pattern

Fixes #1554
Fixes #1555

Agent: team-lead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: use printf -v instead of declare for safe variable assignment in key-request.sh

Addresses security review feedback on PR #1557. The declare approach
created a local variable whose export had no effect outside the function.
printf -v assigns directly in the current scope without eval or command
substitution.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 04:47:33 -05:00
A
e9431430dd
fix: report temp file leaks in _assert_no_temp_leaks test assertion (#1558)
The function only had a success branch — when temp files were leaked,
it silently returned without incrementing FAILED or printing output.
Add the missing else branch so leaked temp files are detected.

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 04:45:09 -05:00
L
9fc59ded1c
fix: handle raw m2. macaroon tokens from Fly.io CLI Sessions API (#1552)
Root cause of 'no tokens found in header' after browser OAuth:

The Fly.io CLI Sessions API returns raw macaroon tokens (e.g. m2.XXXX)
WITHOUT the 'FlyV1 ' prefix. _sanitize_fly_token only handled fm2_
tokens, so m2. tokens fell through unchanged and were sent as:
  Authorization: Bearer m2.XXXX
Fly.io's Machines API expects FlyV1 macaroon format, not Bearer.

Fixes:
- _sanitize_fly_token: add m2.* case that wraps as 'FlyV1 m2.XXX'
- _try_fly_browser_auth polling: eagerly wrap any non-FlyV1 token with
  'FlyV1 ' prefix at the source, before it's echoed back to the caller

Token format handling after fix:
  m2.XXXX         → FlyV1 m2.XXXX      ← CLI Sessions API (was broken)
  fm2_XXXX        → FlyV1 fm2_XXXX     ← still handled (unchanged)
  FlyV1 fm2_XXXX  → FlyV1 fm2_XXXX    ← already correct (unchanged)
  eyJhbGci...     → Bearer eyJ...      ← legacy JWT (fallback to manual)

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 23:54:34 -08:00
A
328e6a6da4
fix: replace bun -e with python3 in fly/lib/common.sh to fix 18 mock test failures (#1553)
bun is not installed in the mock test environment (CI or local test runs).
The mock harness stubs bun as a no-op logger, so _fly_json_get() always
returned empty string, causing "Failed to extract machine ID" and 18 fly
script test failures in bash test/mock.sh.

Replace all 4 bun -e invocations with equivalent python3 code:
- _fly_json_get: extract top-level JSON field from stdin
- _fly_build_machine_body: build machine creation JSON body
- _fly_destroy_app: extract machine IDs array
- list_servers: format apps table

python3 is always available and already has a pass-through mock in
test/mock.sh (like /usr/bin/python3). No behavior change for real runs.

Before: bash test/mock.sh fly → 18 passed, 18 failed
After:  bash test/mock.sh fly → 36 passed, 0 failed

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 02:19:46 -05:00
A
9acc239001
fix: validate token characters in _load_token_from_config to prevent curl injection (#1547)
* fix: validate token characters in _load_token_from_config to prevent curl injection

Tokens loaded from ~/.config/spawn/{cloud}.json were exported without
character validation. A tampered config file containing a token with
embedded newlines could exploit the _curl_api function's -K - (stdin
config) mechanism to inject arbitrary curl directives (e.g., output,
url), since curl interprets newlines in the config format as directive
separators.

Add allowlist validation (^[a-zA-Z0-9._/@:-]+$) matching the pattern
already used in key-request.sh _try_load_env_var and validate_api_token,
making all three token-loading paths consistent.

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

* fix: address review feedback on token validation PR

- Update backslash test to expect validation failure (backslashes not
  valid in any known API token format; the old expectation was wrong
  after validation was added)
- Fix test so exit code comes from _load_token_from_config directly,
  not the trailing echo which always exits 0
- Add comment in shared/common.sh explaining why the pattern includes
  colon vs key-request.sh pattern (Fly.io FlyV1 tokens use colons)

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address review feedback — widen token charset for base64 segments

The original regex rejected + and = which are valid base64 characters
found in API tokens (e.g. sk-or-v1-abc/def+ghi==). This caused a
pre-existing test to fail. Widen the allowlist to include + and =
while keeping the security comment documenting the pattern difference
with key-request.sh.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 01:18:34 -05:00
A
53e6de7f55
fix: validate mock-curl-script.sh stays in sync with mock.sh in test-infra-sync (#1550)
The test-infra-sync test validates that mock.sh's _strip_api_base() and
_validate_body() cover all clouds with fixtures. However, the actual
runtime mock used by tests is mock-curl-script.sh, which has its own
copies of these functions. Nothing enforced these copies staying in sync,
so a contributor could update mock.sh to pass validation while the
runtime mock silently fails to handle new cloud URLs.

Add cross-file sync tests that verify both files handle the same cloud
patterns for _strip_api_base() and _validate_body(). Also refactor
helpers to accept content as a parameter for reuse across both files.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 01:18:15 -05:00
L
fe2c0b024b
fix: prevent premature exit when Fly.io CLI session access_token is null (#1551)
The polling loop in _try_fly_browser_auth() was returning immediately
on the first poll (t=2s) because:

  access_token=$(... "d.get('access_token','')")

When the JSON has "access_token": null (before the user completes
browser auth), Python's print(None) outputs the string "None".
Bash $() captures "None" as non-empty, passes [[ -n "$access_token" ]],
and returns it as the token — before the user even sees the browser.

Then _validate_fly_token(FLY_API_TOKEN="None") sends:
  Authorization: Bearer None
which Fly.io rejects with:
  verify: invalid token: no tokens found in header

Fix:
  d.get('access_token') or ''   →  None or '' = ''  (empty, keeps polling)
  + explicit != "None" guard for belt-and-suspenders

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 21:37:34 -08:00
L
4ae781d2a8
fix: remove 2>/dev/null from token validation calls in auth flow (#1549)
Token validation functions (test_hcloud_token, test_do_token,
test_daytona_token, _validate_fly_token) contain rich diagnostic
log_error/log_warn messages with error details and fix instructions.
Calling them with 2>/dev/null silently discarded all that output,
leaving users with no explanation when their token was rejected.

shared/common.sh — ensure_api_token_with_provider():
  Remove 2>/dev/null from "${test_func}" in both the env-var and
  config-file validation branches, so callers like test_hcloud_token
  can print API error details and remediation steps.

fly/lib/common.sh — ensure_fly_token():
  Remove 2>/dev/null from both _validate_fly_token calls (config-file
  path and post-browser-OAuth path) so users see why validation failed.

Note: Issue 1 (API polling in _poll_instance_once) is intentionally
left with 2>/dev/null — suppressing curl errors during a 60-iteration
polling loop prevents terminal flooding and is handled by '|| true'.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 21:27:42 -08:00
L
8c437435eb
fix: show Fly.io login URL immediately by removing 2>/dev/null suppression (#1548)
2>/dev/null on _try_fly_browser_auth() was swallowing all stderr,
including the auth URL printf and log_step messages that the user
needs to see for sandbox/headless environments.

Also add a 'Fetching Fly.io login URL...' log_step before the API
call so the user gets immediate feedback while the session is created
(the curl call can take 1-2 seconds before the URL is available).

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 21:12:17 -08:00
A
dce55a3f6c
fix: prevent Python code injection in shared utility functions (#1544)
Pass field names via sys.argv instead of interpolating bash variables
directly into Python source strings in extract_ssh_key_ids() and
_load_json_config_fields(). This aligns with the secure pattern already
used elsewhere (e.g., _try_load_env_var in key-request.sh).

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 23:37:24 -05:00
A
d6c53d838f
fix: source .spawnrc directly in agent launch commands for reliable env loading (#1546)
24 agent scripts (codex, opencode, kilocode, openclaw across 6 clouds) used
`source ~/.zshrc && <agent>` which loads env vars indirectly via a hook.
This fails silently when .zshrc has errors or the hook install was non-fatal,
causing agents to launch without OPENROUTER_API_KEY.

Change to `source ~/.spawnrc 2>/dev/null; source ~/.zshrc 2>/dev/null; <agent>`
which loads env vars directly (matching claude/zeroclaw pattern) and tolerates
.zshrc failures without blocking the agent.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 23:37:03 -05:00
A
af475629d8
fix: exclude echo -n from macos-compat MC002 rule to eliminate false positives (#1545)
The MC002 regex matched both `echo -e` and `echo -n`, but only
`echo -e` is non-portable on macOS bash 3.2. `echo -n` works fine
as a bash builtin. This caused 3 false positive errors (all TTY
probe patterns using `echo -n "" > /dev/tty`) making the linter
exit non-zero incorrectly.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 23:36:47 -05:00
L
031b8fbcf0
update: Claude Code description to match Anthropic's official branding (#1542)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 20:19:28 -08:00
A
2c214c2f5b
fix: call ensure_jq before jq usage in hetzner and daytona libs (#1541)
Both cloud_authenticate() functions use jq for JSON construction in
create_server() but never verify jq is installed. On minimal Ubuntu/
Debian, Alpine, or fresh macOS without Homebrew, this causes a hard
failure with "jq: command not found" after the user has already entered
their API token. ensure_jq() in shared/common.sh auto-installs jq on
Linux/macOS -- wire it in before the first jq-dependent call.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 22:00:09 -05:00
A
aa0b182f71
fix: validate GCP_USERNAME before assignment to prevent injection (#1537)
* fix: validate GCP_USERNAME before assignment to prevent injection

Assign logname output to _username first, validate against
^[a-zA-Z0-9_-]+$ regex, then assign to GCP_USERNAME. This
ensures the validated value is what gets used in su commands.

Fixes #1536

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

* fix: validate whoami output in gcp/lib/common.sh main script

Apply same validation pattern to line 27 as was applied in cloud-init.
Assigns whoami output to temp var, validates against alphanumeric pattern,
then assigns to GCP_USERNAME only after validation passes.

Agent: security-auditor
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.6 <noreply@anthropic.com>
2026-02-20 20:38:34 -05:00
A
3c4d92cc9f
test: fix 25 test failures from mock.module global pollution and sandbox env (#1539)
Add autocomplete mock to 38 @clack/prompts mock.module declarations
that were missing it. Bun's mock.module is process-global, so when any
other test file's mock wins the race, p.autocomplete was undefined,
causing 17 cmd-interactive tests to fail non-deterministically.

Also guard sandbox-verification tests with describe.skipIf(!isSandboxed)
so the 8 meta-tests skip cleanly when running from repo root (where
bunfig.toml preload is not active) instead of failing.

Result: 6995 pass, 0 fail from cli/; 6978 pass, 0 fail, 17 skip from root.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:36:20 -05:00
A
38b972f5ce
fix: use destroy_server for sprite delete to support org users (#1538)
The sprite case in buildDeleteScript called `sprite destroy` directly,
bypassing ensure_sprite_authenticated and destroy_server. This meant
SPRITE_ORG was never detected, so org users got "sprite not found"
errors and orphaned sprites continued incurring charges.

Align with every other cloud (hetzner, digitalocean, fly, gcp, aws,
daytona) by calling ensure_sprite_authenticated then destroy_server,
which applies _sprite_org_flags automatically.

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:34:58 -05:00
A
c7e1c73c8a
fix: unbreak spawn delete and align error handling conventions (#1534)
spawn delete was broken for all clouds because execDeleteServer passed
inline scripts (without shebangs) through runBash, which calls
validateScriptContent requiring a #! prefix. Extract spawnBash helper
and add runBashTrusted for locally-generated delete scripts that already
validate their inputs via validateServerIdentifier/validateMetadataValue.

Also fix instanceof Error usage in manifest.ts and history.ts to use
duck typing, matching the convention documented in index.ts and
commands.ts. Fix stale comment in security.ts that claimed colons were
in the server ID allowlist when the regex excludes them.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 18:17:29 -05:00
A
7d83bb6191
test: sync 21 failing tests with current source behavior (#1535)
Tests fell out of sync with recent source changes:
- _display_and_select: check for "server types" (agnostic of UI path)
- opencode_install_cmd: check for "tr A-Z a-z" (new OS detection)
- _curl_api: test non-auth headers (auth now via -K stdin)
- ensure_gh_auth: use valid token prefix, match new log messages
- GITHUB_TOKEN piping: match _gh_token variable name
- daytona: remove from exec-based clouds (uses SSH)
- cmdrun/prompt-file: add --dry-run to prevent script execution timeouts
- sandbox: clean stale /root/subprocess-test.txt before assertion

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 18:17:21 -05:00
A
c69c12c8db
fix: validate RAW_BASE URL in update-check to prevent future injection (#1533)
Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 12:52:02 -05:00
A
2bb1b82bc3
fix: align tests with re-exec update behavior and sprite upload classification (#1532)
- 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>
2026-02-20 11:52:35 -05:00
A
4b9e6ae0b8
fix: validate agent.repo format in update.ts before passing to Bun.spawn (#1530)
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>
2026-02-20 11:51:31 -05:00
A
3570caa840
fix: accept localhost and hostnames in validateConnectionIP (#1531)
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>
2026-02-20 11:49:23 -05:00
A
6782618b7c
feat: add user-friendly cloud descriptions to manifest (#1529)
Replace technical API-focused cloud descriptions with short,
user-friendly ones that highlight pricing and key selling points.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 08:18:19 -08:00
L
eea43adcad
fix: re-exec with new binary after auto-update for all invocations (#1526)
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>
2026-02-20 10:26:02 -05:00
A
be48fe8576
fix: display spawn names in list output (#1523)
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>
2026-02-20 10:22:14 -05:00
A
fc87ebf939
fix: replace printf -v (bash 4.0+) with eval for macOS bash 3.2 compat (#1522)
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>
2026-02-20 10:20:12 -05:00
L
be176e4cdb
fix: confirm kebab resource name + improve Fly.io sandbox auth (#1525)
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>
2026-02-20 07:12:49 -08:00
L
f2df9bffa5
feat: add gcp and aws (Lightsail) to featured_cloud for all agents (#1524)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 07:08:02 -08:00
A
eff1dc2512
fix: repair Daytona delete and Fly.io reconnect in spawn list (#1521)
- 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>
2026-02-20 09:26:31 -05:00
A
7b6d6eed3b
fix: replace hardcoded history path in security.ts error messages (#1520)
* 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>
2026-02-20 08:37:01 -05:00
A
3225df305f
fix: hide cloud API tokens from process argument list (#1519)
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.
2026-02-20 12:51:55 +00:00
Ahmed Abushagur
05f1905294
fix: Daytona SSH gateway — resource overrides, base64 uploads, connection throttling (#1517)
* 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>
2026-02-20 05:52:39 -05:00
A
32db000ca4
fix: validate env-var tokens and align cloud_authenticate patterns (#1516)
- 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>
2026-02-20 04:55:35 -05:00
A
9de4cecfea
fix: add spawn delete hint to post-session summaries (#1515)
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>
2026-02-20 04:52:39 -05:00
Ahmed Abushagur
b5d174a472
fix: pin Codex to 0.94.0 + wire_api=chat for multi-turn stability (#1518)
* 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>
2026-02-20 04:49:35 -05:00
A
50dd2f26ed
fix: repair Fly.io saved token loading (_load_token_from_config misuse) (#1513)
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>
2026-02-20 03:54:41 -05:00
A
703ab4ea4e
fix: use bare 'bun' in cli-entry-edge-cases test subprocess (#1514)
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>
2026-02-20 03:49:35 -05:00
A
3097e5a153
fix: allow freeform display names in spawn name prompt (#1511)
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>
2026-02-20 02:56:59 -05:00
Ahmed Abushagur
95137ed2c7
fix: rewrite hetzner common.sh + fix token prompt bug (#1512)
* 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>
2026-02-20 02:52:49 -05:00
A
3ebc89d864
fix: correct spawn clear-history to spawn list --clear in error messages (#1508)
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>
2026-02-20 01:55:17 -05:00
A
b8f757f184
fix: resolve 8 cli-entry-edge-cases test failures (#1509)
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>
2026-02-20 01:51:36 -05:00
A
b094accb93
fix: save connection info for all Sprite agents in cloud_provision (#1510)
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>
2026-02-20 01:51:25 -05:00
A
3280a44c45
feat: add browser-based OAuth login for Fly.io + token sanitizer (#1506)
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>
2026-02-19 22:50:19 -08:00
L
d5690a8b11
feat: spawn name prompt + kebab resource naming across all clouds (#1507)
* 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>
2026-02-19 22:22:59 -08:00
L
ff261f3544
feat: add spawn pick to shared _display_and_select (Hetzner + all clouds) (#1505)
* 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>
2026-02-19 21:59:00 -08:00
A
d8785708c9
feat: add cloud provider icons and metadata support (#1503)
Download favicon/icons for all 8 cloud providers into assets/clouds/:
- local.png     — OpenRouter apple-touch-icon (6.4K)
- hetzner.png   — Hetzner 180x180 apple icon (1.9K)
- fly.png       — Fly.io apple-touch-icon (6.4K)
- aws.png       — AWS 144x144 touch icon (3.1K)
- daytona.png   — Daytona favicon from Framer CDN (1.2K)
- digitalocean.png — DigitalOcean apple-touch-icon (6.0K)
- gcp.png       — Google Cloud super_cloud icon (4.2K)
- sprite.png    — Sprites.dev apple-touch-icon (1.9K)

Add assets/clouds/.sources.json tracking canonical source URLs.
Add optional `icon` field to CloudDef interface.
Update manifest.json with raw.githubusercontent.com icon URLs.
Add icon URL type validation test for clouds.
Bump CLI version 0.5.13 → 0.5.14.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-20 00:51:40 -05:00
L
015446eee8
fix: remove 2>/dev/tty from spawn pick call in GCP picker (#1504)
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>
2026-02-19 21:44:01 -08:00
A
2a4e7ff983
feat: upgrade refresh-favicon skill to update-metadata (#1502)
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>
2026-02-19 21:27:31 -08:00