The test-engineer agent was generating hundreds of fake tests that
copy-pasted source functions inline instead of importing them. These
tests pass even when the real code is broken (2,497 removed in #1620).
Three layers of defense:
- Global: ban bulk test generation with replica pattern from any agent
- Team lead: explicit rejection criteria for inline-replica test plans
- Test-engineer: 6 strict non-negotiable quality rules (must import
from source, max 1 new file per cycle, prioritize fixing over adding)
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_load_token_from_config intentionally rejects non-ASCII tokens via
regex validation to prevent curl injection. The test expected unicode
tokens to pass, contradicting the code's security design.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI runs `bun test` from the repo root, not `cli/`, so the
bunfig.toml preload that sets up the sandbox never loads. All 17
tests skip silently — they verify preload infrastructure, not
application code.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These test files were auto-generated by an AI agent and test copy-pasted
"replica" functions defined inline — not the real source code. They pass
even when the actual code is broken, providing false confidence.
Two categories removed:
1. Replica-only files (34 files, ~1,482 tests): Define inline copies of
functions and test those copies instead of importing from source.
Examples: key-server.test.ts, trigger-server.test.ts,
index-dispatch-routing.test.ts, verb-aliases.test.ts
2. Duplicate-with-imports files (4 files, ~631 tests): Import real
functions but duplicate coverage already in
commands-exported-utils.test.ts. Examples:
commands-credential-display-internals.test.ts (178 tests),
cli-core-edge-cases.test.ts (237 tests)
Before: 131 files, 6,966 tests (5 failing)
After: 93 files, 4,469 tests (1 pre-existing failure)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GCP's destroy_server redirected both stdout and stderr to /dev/null
without checking the exit code, so deletion failures were invisible
to users. DigitalOcean's destroy_server never checked the API response
for error payloads, always reporting success.
Both bugs could leave cloud instances running (and charging money)
while telling users they were destroyed. Same class of bug fixed for
AWS in PR #1606.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
safe_read() outputs via stdout and takes only one argument (the prompt).
Three call sites in aws/lib/common.sh incorrectly passed a variable name
as a second argument instead of using command substitution:
safe_read "prompt" varname # BUG: varname never assigned
varname=$(safe_read "prompt") # CORRECT: captures stdout
This caused:
- Install prompt always defaulting to "y" (user's "n" was ignored)
- AWS credentials never being captured after CLI install, leaving
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY empty, so the
install-then-configure code path always failed silently
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The Fly Machines API enforces a [1s, 1m0s] range on
WaitMachineRequest.Timeout. We were passing 90s, which caused an
invalid_argument error and prevented machines from starting.
Lower the default to 60s (the API maximum) and retry up to 3 times
so slow-starting machines still have a full 3-minute window.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On CI (GitHub Actions), `CI=true` causes picocolors to enable ANSI
output. Tests comparing against plain text (e.g., `toContain("--prompt
requires a value")`) fail because the actual output wraps text in bold/
dim ANSI codes.
Fixes:
- Subprocess tests (runCli): add NO_COLOR=1 to child env
- Mock capture tests: add stripAnsi() helper to output getters
- Bash subprocess tests: add NO_COLOR=1 to execSync env
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
testFlyToken() fallback to /v1/user accepted 404 plain text responses
because hasError() only checks for JSON "error"/"errors" keys. Adding
resp.ok check ensures non-2xx responses are correctly rejected.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After the fly provider was converted to TypeScript (PR #1602), the bash
shim scripts no longer source lib/common.sh or reference OPENROUTER_API_KEY
directly -- that logic moved to TypeScript. Skip TypeScript shim scripts
in bash-specific convention checks.
Also fixes:
- URL regex in cloud-error-guidance to exclude backticks/commas from
template literals in heredocs
- aws added to skipProviders for destroy_server error check (uses set -e
and internal process.exit, not explicit return 1)
- inject_env_vars_local test regex updated to match semicolon separator
instead of && (matches actual shared/common.sh implementation)
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Move all fly TypeScript files from fly/lib/*.ts and fly/main.ts into
cli/src/fly/. This gives them access to cli/node_modules (@clack/prompts),
biome linting, and the existing bun:test infrastructure — no symlinks or
NODE_PATH hacks needed.
The org picker now uses @clack/prompts select() directly (static import,
bundled at build time).
New: cli/build-clouds.sh — auto-discovers cli/src/*/main.ts and bundles
each into {cloud}.js. Scalable to future cloud TS migrations:
bash cli/build-clouds.sh # build all
bash cli/build-clouds.sh fly # build one
Shims now check for cli/src/fly/main.ts (local) or download fly.js from
GitHub releases (remote curl|bash).
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The only existing installer (install.sh) is bash-only and fails silently
on Windows PowerShell — 'curl ... | bash' errors because bash.exe is not
available outside WSL.
install.ps1 implements the same logic as install.sh for PowerShell:
- Checks bun >= 1.2.0; installs via bun.sh/install.ps1 if missing
- Downloads CLI source via git sparse-checkout or GitHub API fallback
- Builds with 'bun install && bun run build'; falls back to pre-built binary
- Installs to %USERPROFILE%\.local\bin (or SPAWN_INSTALL_DIR override)
- Creates spawn.cmd wrapper for cmd.exe compatibility
- Adds install dir to the user's persistent PATH if not already present
Usage:
irm https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cli/install.ps1 | iex
README updated with Windows PowerShell install instructions alongside
the existing macOS/Linux/WSL command.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Reverts the 0.94.0 pin — install latest Codex and use the required
wire_api="responses" format.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The aws destroy_server function had conditional logic (if/else for CLI
vs REST mode) but no error handling - failures were silently ignored and
"Instance destroyed" was logged even on failure. This could leave
instances running and incurring charges without the user knowing.
Also fix the URL extraction regex in cloud-error-guidance.test.ts to
exclude backtick characters, preventing false positives from template
literals in embedded TypeScript code.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
promptSpawnName() used `placeholder` (visual hint only) without `defaultValue`,
so pressing Enter returned an empty string instead of applying the placeholder.
Now generates a unique default like `spawn-a3f2` with a random suffix to avoid
Fly.io global name collisions.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: fly auth token deprecated + org picker + macaroon discharge tokens
Three fixes for the fly/ TypeScript provider:
1. `fly auth token` is deprecated — newer flyctl outputs a message, not
a token. Now tries `fly tokens create org --expiry 24h` first, with
`fly auth token` as fallback. Uses org tokens (not deploy) since
spawn needs to create new apps.
2. Token sanitization stripped macaroon discharge tokens at commas
(`fm2_[^ ,]*` → `fm2_\S+`). The full composite token
`fm2_xxx,fm2_yyy,fo1_zzz` is now preserved.
3. Org picker upgraded from numbered 1/2 input to arrow-key interactive
selector with cursor navigation, scroll windowing, and fallback to
numbered list when TTY is unavailable.
Also fixes: testFlyToken fallback sent `Bearer FlyV1 ...` (double prefix)
for macaroon tokens — now dispatches FlyV1 vs Bearer correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: never run test/mock.sh locally — opens browser, CI only
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>
Replace fly/lib/common.sh (741 lines of bash) with a TypeScript
implementation using Bun runtime. The fly/ provider was the most
complex bash code in the project — recent fixes (#1597, #1599, #1600)
highlight the pain of debugging HTTP calls, JSON parsing, and multi-step
auth flows in shell.
New TypeScript modules:
- fly/lib/ui.ts — logging, prompts, validation (zero deps)
- fly/lib/fly.ts — API client (fetch), auth chain, org listing, provisioning
- fly/lib/oauth.ts — OpenRouter OAuth via Bun.serve(), key management
- fly/lib/agents.ts — typed agent configs for all 6 agents
- fly/main.ts — orchestrator entry point
Agent .sh files become thin shims (~30 lines) that install bun if needed,
download TS sources for curl|bash execution, and delegate to main.ts.
Test coverage:
- 44 TypeScript unit tests (bun test) for pure logic
- 4 fly failure-mode tests (mock.sh) for error scenarios
- All existing test suites pass (110 run.sh, 76 mock.sh)
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use fly auth login (OAuth) instead of manual token paste
The fly auth flow was falling back to ensure_api_token_with_provider
which prompts users to manually paste a token from the dashboard.
This is bad UX when `fly auth login` exists and handles browser-based
OAuth automatically.
New auth chain:
1. FLY_API_TOKEN env var (if set and valid)
2. Saved config (~/.config/spawn/fly.json)
3. Existing fly CLI session (fly auth token)
4. fly auth login — browser OAuth flow (NEW)
Removes the manual token paste fallback entirely. If fly CLI isn't
installed, fails with a clear install instruction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add manual token paste as final fallback after OAuth
Auth chain is now:
1. FLY_API_TOKEN env var
2. Saved config
3. fly auth token (existing session)
4. fly auth login (OAuth)
5. Manual token paste (last resort)
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>
Previously _fly_list_orgs silently swallowed all errors (2>/dev/null
everywhere) and _fly_prompt_org fell back to manual input with no
diagnostic info. Now both paths (fly CLI + GraphQL) surface specific
failure reasons — missing CLI, empty output, parse errors with raw
JSON, GraphQL errors — and _fly_prompt_org fails hard with actionable
debug hints instead of silently defaulting.
Also always show the org picker when fetch succeeds (no silent default).
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
_fly_list_orgs previously relied solely on `flyctl orgs list --json`.
When flyctl is absent or its output is unexpected, the user gets dumped
into a manual "Enter Fly.io org slug" prompt — even though we already
have a valid API token.
Now tries flyctl first, then falls back to the Fly.io GraphQL API
(`api.fly.io/graphql`) using the saved FLY_API_TOKEN. Works with
both Bearer and FlyV1 macaroon tokens.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The mock bun shim was broken on CI (ubuntu-latest, no real bun):
- Only passed $2 to node, dropping -- field default args needed by _fly_json
- Didn't strip TypeScript annotations (: any[], as any) that node can't parse
Fixes:
- shift 2 to preserve extra args, forward them to both real bun and node
- sed -E strips TS type annotations before passing to node --input-type=module
- All fly tests now pass under the node-only CI fallback path
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace python3 with bun+TS in fly/lib/common.sh, fix token validation
Three targeted fixes to the Fly.io library:
1. Replace all python3 with bun+TypeScript:
- _fly_json: stdin-piped field extractor via bun -e (no eval, no env var
size limits — handles arbitrarily large API responses)
- _fly_json_ids: dedicated machine ID extractor for destroy_server
- _fly_list_orgs: bun -e with flat dict + nodes/organizations support
- list_servers: bun -e formatted table output
Zero python3 invocations remain in the file.
2. Dual-endpoint _test_fly_token: tries Machines API first (deploy tokens),
falls back to api.fly.io/v1/user (OAuth/personal tokens). Prevents
rejecting valid personal tokens that lack Machines API access.
3. No more eval(): _fly_json uses direct property access (d[field]) instead
of python3 eval(expr), eliminating the code injection surface entirely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: always prompt user for Fly.io org, never silently default
_fly_prompt_org now asks the user directly when the org list can't be
fetched, instead of silently falling back to "personal".
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>
* feat: fall back to SigV4 REST API when AWS CLI is absent (aws/lightsail)
If `aws` CLI is not installed but AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY
are set, provision Lightsail instances directly via the REST API instead of
erroring out.
- Add _lightsail_rest(): inline Bun TypeScript that computes SigV4 signatures
via node:crypto and calls the Lightsail API with native fetch — no openssl
or curl gymnastics required
- Add _ls_json(): dot-path JSON parser, prefers jq, falls back to bun eval
- ensure_aws_cli() now sets LIGHTSAIL_MODE=cli|rest; REST mode requires bun
(already a project dependency) and shows a clear error if missing
- All API calls in ensure_ssh_key, create_server, _wait_for_lightsail_instance,
destroy_server, list_servers are gated on LIGHTSAIL_MODE
- Replace all python3 JSON encoding (key import, userdata, list table) with
bun eval — consistent with project tooling
- No more auto-install of the 200 MB AWS CLI binary
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add interactive AWS CLI install when CLI is missing
When neither aws CLI nor raw credentials are found, prompt the user
to install AWS CLI v2 on the spot (macOS .pkg / Linux zip installer).
After install, prompt for Access Key ID + Secret and validate via
sts:GetCallerIdentity before proceeding.
The decision cascade is now:
1. Existing aws CLI with valid creds → cli mode
2. Raw env-var creds + bun available → rest mode
3. Offer to install aws CLI → prompt for creds → cli mode
4. Creds collected during install + bun → rest mode fallback
5. Nothing worked → show manual instructions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: eliminate code/path injection in bun eval calls (aws/lib/common.sh)
Pass shell variables as process.argv arguments instead of interpolating
them into JavaScript string literals:
- _ls_json(): path parameter passed as process.argv[2] (was CRITICAL
code injection — attacker-controlled path could escape the string)
- ensure_ssh_key(): pub_path and key_name passed as process.argv[2..3]
(was HIGH — path injection via $HOME)
- create_server(): ud_tmp, name, az, bundle passed as process.argv[2..5]
(was MEDIUM — temp file path interpolation)
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
The nodesource setup_22.x script can run successfully but leave nodejs
uninstalled on Fly.io machines. Add post-install verification with
`which node && node --version`, fall back to default Debian nodejs
package if nodesource fails, increase timeout from 120s to 180s, and
report a clear error if node is unavailable after all attempts.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Real `fly auth token` returns comma-separated multi-segment macaroon
tokens (fm2_...,fm2_...,fo1_...). The token validation regex rejected
commas, forcing re-auth on every run. Add comma to the allowed charset.
`fly orgs list --json` returns a flat dict ({"slug": "Name"}) on some
flyctl versions, not the list/nodes format the parser expected. Detect
and handle both formats so the org picker works correctly.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Issue #1572: Replace bash 4+ ${//} pattern substitution in generate_env_config
with sed for macOS bash 3.2 compatibility.
Issue #1571: Split local var=$(cmd) declarations in fly/lib/common.sh so
exit codes propagate correctly with set -e on macOS bash 3.2.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Use semicolons instead of && for rm in inject_env_vars, inject_env_vars_sprite,
inject_env_vars_cb, and inject_env_vars_cloud so the temp file containing the
API key is always deleted even if ~/.zshrc doesn't exist or append fails.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add _fly_run_with_retry helper that wraps run_server with configurable
retry count, sleep interval, and timeout. Apply it to package manager
and installer commands in wait_for_cloud_init so transient failures
(network timeouts, apt lock contention) no longer abort the entire
cloud-init sequence.
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add log_info/log_warn messages at each step of the 5-step auth chain
so users can see which auth method is being tried and why fallbacks occur.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves sub-issues #1569, #1570, #1576, #1577, #1578, #1580.
#1569 — /wait endpoint replaces polling loop:
_fly_wait_for_machine_start now uses GET /apps/{app}/machines/{id}/wait
?state=started&timeout=90. One blocking API call instead of 30 polls.
#1570 — fly machine exec replaces fly ssh console for run_server:
run_server uses 'fly machine exec MACHINE_ID --app APP -- bash -c cmd'
(direct API, no WireGuard tunnel) when FLY_MACHINE_ID is set. Falls
back to 'fly ssh console -C' for environments without a machine ID.
#1576 — App name collision loop capped at 5 retries:
Prevents infinite re-prompt. Suggests FLY_APP_NAME env var after 5
failed attempts.
#1577 — destroy_server errors are now reported:
All fly_api calls check for error responses. Reports failed machine
deletions and exits non-zero on app deletion failure instead of
always logging "destroyed" regardless of outcome.
#1578 — bun replaced with python3 for all JSON parsing:
_fly_json_get, _fly_build_machine_body, _fly_list_orgs, destroy_server,
list_servers all use python3 -c now. python3 is universally available;
bun was only available after cloud-init completed on the target machine.
#1580 — upload_file uses stdin pipe instead of base64 string injection:
'fly machine exec ... -- bash -c "cat > path" < local_file' streams
file content directly. Eliminates the command-length/injection risk of
embedding base64 content in a shell argument string.
test/mock.sh: add 'fly machine exec' case to the fly CLI mock.
test/fixtures/fly/_env.sh: add FLY_MACHINE_ID to test env.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Some flyctl versions exit non-zero even on success. Removed '|| return 1'
so the output is always captured. Empty output is still a failure.
Also pass JSON as a bun argument (process.argv[1]) instead of piping via
stdin — avoids any Bun.stdin buffering issue in the _fly_list_orgs context.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
interactive_pick() echoes the selected value to stdout — it does NOT
export the env var. _fly_prompt_org was calling it without capturing
the output, so FLY_ORG was never set and the echo printed the org
slug as a raw string to the terminal.
Fix: org=$(interactive_pick ...) && export FLY_ORG.
Also guard with the standard FLY_ORG / SPAWN_NON_INTERACTIVE early-exit.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1. _fly_list_orgs: use 'fly orgs list --json' (flyctl) instead of the
non-existent api.fly.io/v1/organizations REST endpoint. Pipe through
interactive_pick (same pattern as Hetzner/GCP pickers) so org
selection uses the shared arrow-key / fzf / numbered-list picker.
2. fly auth token captures: add 'sed s/\x1b...//g' to strip ANSI color
escape codes. flyctl may output the token with terminal colors even
when stdout is piped; the ESC character (\033) fails the security
character check (^[a-zA-Z0-9._/@:+=\ -]+$) causing the token to be
marked malformed and cleared on the next run.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replace flyctl-based org listing with a direct API call to
api.fly.io/v1/organizations, feeding results into _display_and_select
(the shared arrow-key / fzf / numbered-list picker).
_fly_list_orgs():
- Calls GET /v1/organizations with Bearer auth
- Emits pipe-delimited "slug|name (type)" lines for _display_and_select
_fly_prompt_org():
- Single org: auto-selects silently
- Multiple orgs: shows arrow-key picker via _display_and_select
(defaults to "personal" if that slug is in the list)
- API unavailable: falls back to safe_read prompt with "personal" default
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Two fixes for persistent Fly.io auth failures:
1. shared/common.sh — _load_token_from_config():
When the saved token fails the security character check, auto-delete
the corrupt config file instead of silently returning 1. This prevents
the user from being stuck in a loop where every run loads a malformed
token (from a previous failed auth attempt) and immediately fails.
Message changed from error to warn: "Saved token is malformed —
clearing cached credentials."
2. fly/lib/common.sh — _try_flyctl_auth() and _try_fly_browser_auth():
Pipe 'fly auth token' output through 'head -1' to capture only the
first line. Newer flyctl versions may print warnings/metadata after
the token on subsequent lines; previously these got concatenated into
the token string via $() and could introduce characters that fail
the security validator (newlines stripped by _sanitize_fly_token, but
concatenated text from warning lines could contain unusual chars).
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1. Skip _validate_fly_token after 'fly auth login':
Token from flyctl is definitionally valid — calling the Machines API
(api.machines.dev) with a user OAuth token causes a false failure
because that API only accepts deploy tokens, not OAuth user tokens.
2. Fix _validate_fly_token endpoint:
Now tries api.fly.io/v1/user (Bearer, accepts OAuth tokens) first,
then falls back to the Machines API for deploy tokens. Prevents
'no tokens found in header' false failures for env/config tokens.
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Root cause of persistent 'no tokens found in header':
The CLI Sessions API returns a user-level OAuth code that requires
flyctl's internal token exchange step to become a valid API token.
We were using the raw access_token directly, bypassing that step.
_try_fly_browser_auth() — now delegates to flyctl:
- Calls 'fly auth login' directly (flyctl handles browser open,
polling, and token exchange internally)
- Gets the final token via 'fly auth token' (always correct format)
- Falls back to manual token entry if flyctl unavailable
_fly_prompt_org() — new function:
- Called after successful auth (flyctl, browser, or manual)
- Lists orgs via 'fly orgs list --json' if multiple exist
- Shows picker or simple prompt; defaults to "personal"
- Exports FLY_ORG for use in app creation / list_servers
- Skipped when FLY_ORG is already set or SPAWN_NON_INTERACTIVE=1
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* Revert "fix: handle raw m2. macaroon tokens from Fly.io CLI Sessions API (#1552)"
This reverts commit 9fc59ded1c.
* Revert "fix: replace bun -e with python3 in fly/lib/common.sh to fix 18 mock test failures (#1553)"
This reverts commit 328e6a6da4.
* fix: bun passthrough mock + restore Bun JSON parsing in fly/lib
Reverts PR #1553 (which reverted Bun in favour of Python to fix tests)
and instead fixes the root cause: the test/mock.sh bun mock was a dumb
no-op that discarded all output, causing _fly_json_get() to return empty
string and every fly script to fail with "Failed to extract machine ID".
test/mock.sh — smart bun mock:
- `bun -e "..."` (inline eval, used for JSON processing) → delegates to
the real bun binary so _fly_json_get() / _fly_build_machine_body()
actually produce correct output during tests
- All other bun invocations (install, run, etc.) → logged no-op as before
fly/lib/common.sh:
- Restores Bun-based _fly_json_get(), _fly_build_machine_body(),
destroy_server machine-ID extraction, and list_servers table formatter
- Re-applies m2. macaroon token fix from #1552 (which was lost when
#1553 reverted the whole file):
_sanitize_fly_token now wraps raw m2.* tokens as "FlyV1 m2.*" so
CLI Sessions OAuth tokens are sent with the correct auth header
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
* test: add node fallback to bun mock for CI environments
CI (GitHub Actions ubuntu-latest) has node but not bun, so the bun
passthrough mock silently returns empty string, causing _fly_json_get
to fail and 18 Fly.io tests to break. Add a fallback chain:
real bun -> node (with Bun.stdin.text() polyfill) -> exit 0.
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The _try_load_env_var regex in key-request.sh rejected tokens containing
spaces, colons, plus signs, or equals signs. This caused FlyV1 prefixed
tokens ("FlyV1 fm2_...") to fail validation during QA cycle key loading,
making Fly.io always appear as a missing key provider.
Updated regex to match _load_token_from_config in shared/common.sh which
already allows these characters.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
printf -v requires bash 4.0+; macOS ships bash 3.2, causing _try_load_env_var()
to fail with 'printf: -v: invalid option' and breaking saved API key loading for
all cloud providers. Both var_name and val are validated against strict regexes
immediately above, so export "NAME=VALUE" is injection-safe and works on bash 3.2+.
The macos-compat linter already flags this pattern as MC013 error.
Agent: team-lead
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The _load_token_from_config regex (added in #1547) rejects tokens
containing spaces, but Fly.io browser OAuth tokens are saved with
a "FlyV1 " prefix (e.g., "FlyV1 fm2_xxx"). This causes the token
to be silently rejected on reload, forcing re-authentication every
session. Space is safe inside curl -K double-quoted header values.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
* 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#1554Fixes#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>
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>
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>
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>