- All multiselect setup options now default to unchecked (was all checked)
- Added "Reuse saved OpenRouter key" option (off by default) so users
get a fresh OAuth key each run unless they explicitly opt in
- GitHub CLI option was already filtered when no token detected; now
reuse-api-key is filtered when no saved key exists
- Cancel on setup options now returns empty set (matching new defaults)
- Env var OPENROUTER_API_KEY still takes priority unconditionally
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add two new GritQL biome plugins (matching ori repo patterns) that ban
all try/catch and try/finally in TypeScript code. Convert all remaining
blocks across production and test files to use tryCatch/asyncTryCatch
from @openrouter/spawn-shared.
no-try-catch.grit covers all 4 variants:
- try/catch with binding, try/catch without binding
- try/catch/finally with binding, try/catch/finally without binding
no-try-finally.grit covers bare try/finally.
Both exclude shared/result.ts and shared/parse.ts (the implementation layer).
Production files (18): aws, hetzner, digitalocean, gcp, sprite, index,
update-check, ui, ssh, agent-setup, picker, agent-tarball, shared,
run, connect, delete, list
Test files (12): cmdlast, cmd-interactive, cmdrun-happy-path,
commands-resolve-run, commands-swap-resolve, commands-error-paths,
download-and-failure, preload, ssh-keys, update-check, orchestrate,
fs-sandbox, prompt-file-security, security, script-failure-guidance
Bumps CLI version to 0.16.6
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: gate tarball install behind --beta=tarball flag
Tarball install is not yet reliable enough to be the default.
Move it behind an opt-in --beta=tarball flag so users can test it
explicitly while live install remains the default path.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: support multiple --beta flags (repeatable)
Parse all --beta flags from args in a loop, collecting them into a
comma-separated SPAWN_BETA env var. Consumers check for their feature
with Set.has() so multiple beta features can be active simultaneously.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace for(;;) loop with extractAllFlagValues helper
Cleaner approach: a dedicated helper mutates args in place and returns
all values for a repeatable flag, replacing the infinite loop pattern.
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>
Why: The curl|bash pattern for bun installation was an unverified supply
chain dependency. Now the installer is downloaded to a temp file and its
SHA-256 hash is verified against a known-good value before execution.
Falls back gracefully if sha256sum/shasum is unavailable.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The cli-release workflow was deleting releases before recreating them,
leaving a window where users downloading cloud bundles (gcp.js, aws.js,
etc.) would get a 404. This affected all clouds on every push to main.
Switch to gh release upload --clobber which atomically replaces assets
without removing the release, and only create releases if they don't
already exist.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The AWS module had CLI-vs-REST branching duplicated in ensureSshKey (2x),
createInstance (4x), and waitForInstance (2x). Extracted 4 private helpers
(lightsailGetKeyPair, lightsailImportKeyPair, lightsailCreateInstances,
lightsailGetInstance) so each consumer is a single linear flow. A bug fix
in one mode can no longer be missed in the other.
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Replace `-H "Authorization: Bearer ..."` curl args with temp curl config
files (`-K`) in digitalocean.sh and hetzner.sh e2e drivers, keeping API
tokens out of `ps` output
- Replace dangerous-var blocklist in provision.sh with a positive whitelist
of allowed cloud_headless_env variable names
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Pass GITHUB_TOKEN directly via inline `export` in the remote SSH command
instead of writing it to local/remote temp files. This removes the race
condition window where tokens could be read from disk.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add validateModelId() to reject model IDs containing shell metacharacters.
The validation is applied in orchestrate.ts immediately after resolving
MODEL_ID from env/agent defaults, before the value reaches any agent
configure function or runServer call. Invalid model IDs are dropped to
undefined with a warning.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: unified arrow-key selection + setup checkboxes
Replace p.autocomplete (type-ahead) with p.select (arrow-key navigation)
for agent and cloud selection. Add p.multiselect checkboxes for optional
post-provision setup steps (GitHub CLI, Chrome browser), all ON by default.
Three fast prompts: agent → cloud → setup options. Defaults: OpenClaw,
first cloud with credentials, all steps enabled.
Key changes:
- interactive.ts: p.autocomplete → p.select with initialValue defaults
- interactive.ts: promptSetupOptions() with p.multiselect, exported for reuse
- run.ts: wire setup options into cmdRun direct path
- agents.ts: OptionalStep type, getAgentOptionalSteps() static metadata
- orchestrate.ts: read SPAWN_ENABLED_STEPS env var, gate GitHub auth + configure
- agent-setup.ts: gate Chrome install with enabledSteps in setupOpenclawConfig
- Version bump 0.15.40 → 0.16.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: mirror tarball files to $HOME for non-root SSH users (GCP, AWS)
Tarballs are built with absolute /root/ paths, but GCP and AWS Lightsail
SSH as a regular user whose $HOME is /home/<user>/. After extraction,
binaries like `claude` end up at /root/.claude/local/bin/ but the
launchCmd looks in $HOME/.claude/local/bin/ — causing "command not found".
Add a post-extraction step that copies /root/ dotfiles to $HOME/ when
the SSH user isn't root. This fixes `spawn claude gcp` failing with
exit code 127 after tarball install.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
Add 6 undocumented test files to the test index README:
- do-payment-warning.test.ts (Cloud-specific)
- sprite-keep-alive.test.ts (Cloud-specific)
- history-corruption.test.ts (Infrastructure)
- paths.test.ts (Infrastructure)
- fs-sandbox.test.ts (Infrastructure)
- picker.test.ts (Parsing and type utilities)
Also remove duplicate manifest-cache-lifecycle.test.ts entry
that appeared in both Core manifest and Infrastructure sections.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The 'create a spawn first' message was shown even when active servers
existed but none matched the filter. Now shows 'Run spawn delete without
filters to see all servers.' for the unmatched-filter case and reserves
the create hint for when no servers exist at all.
Fixes#2454
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Security: the manifest-derived fallback path in connect.ts bypassed the
validateLaunchCmd() allowlist that guards history-derived commands. A
malicious or modified manifest.json cache could inject arbitrary commands
executed on the remote VM via SSH.
Fixes#2453
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Instead of telling users to pipe through `spawn list | cat` to view their
spawn history, render the history table inline when no active connections
exist. The | cat workaround was needed because non-interactive mode skips
the picker; now interactive mode falls through to renderListTable directly,
consistent with what `spawn list | cat` was already doing.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
OpenClaw runs a web dashboard on port 18791 of the remote VM. This
change SSH-tunnels that port to localhost and auto-opens the browser,
giving users a web UI with zero CLI knowledge needed.
- Add TunnelConfig to AgentConfig interface (agents.ts)
- Add startSshTunnel function with port-finding logic (ssh.ts)
- Capture gateway token in closure so the same token is used for both
the remote config and the browser URL (agent-setup.ts)
- Wire tunnel into orchestration pipeline between preLaunch and
interactiveSession (orchestrate.ts)
- Add getConnectionInfo to CloudOrchestrator interface and implement
in all SSH-based clouds (DO, Hetzner, AWS, GCP)
- Local: opens browser directly at localhost:18791
- Sprite: gracefully skipped (no standard SSH)
- Add USER.md bootstrap to guide OpenClaw users to web dashboard
Closes#2449
Supersedes #2418
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
AWS and GCP both include $HOME/.npm-global/bin and $HOME/.claude/local/bin in the
PATH exported before running remote commands. Hetzner and DO were missing these two
entries, causing "command not found" errors for Claude Code and npm-global packages
on those clouds.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
All four SSH-based cloud drivers (aws, digitalocean, gcp, hetzner)
passed the command string directly as an SSH argument, which gets
interpreted by the remote shell. While current callers pass trusted
E2E test code, this creates a security footgun for future changes.
Fix: base64-encode the command locally and decode it on the remote
side before piping to bash. The encoded string contains only safe
characters [A-Za-z0-9+/=], eliminating any injection vector. Stdin
is preserved for callers that pipe data into cloud_exec.
Closes#2432, closes#2433, closes#2434, closes#2435
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- Replace word-split _sprite_org_flags() call sites with _sprite_cmd()
helper that uses a proper bash array for the -o flag, eliminating
injection risk from org names with spaces or shell metacharacters
- Validate _SPRITE_ORG against [A-Za-z0-9_-]+ in _sprite_validate_env
- Use grep -qF (fixed-string) instead of grep -q for app name matching
to prevent regex metacharacters in names from causing false matches
- Use mktemp for _stderr_tmp in _sprite_exec instead of predictable
PID-based path (/tmp/sprite-exec-err.$$) to prevent symlink attacks
Closes#2436
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
- Validate app_name at function entry (alphanumeric, dots, hyphens, underscores
only) before it's used in file paths or passed to cloud_exec
- Add trap-based cleanup for the temp file used during .spawnrc fallback creation
- Add security comments documenting the three-layer defense model: printf %q
quoting, base64 encoding, and stdin piping (no interpolation into command
strings)
The core vulnerability (env_b64 interpolated into the cloud_exec command string)
was already fixed in a prior commit that switched to stdin piping. This change
adds defense-in-depth and documentation.
Fixes#2437, #2441
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
install.sh: Replace color variable interpolation in printf format strings
with %b arguments to prevent format string injection (fixes#2443).
common.sh: Use %b for color escapes in logging functions. Document that
BASH_SOURCE and source usage in load_cloud_driver is intentional since
e2e scripts are filesystem-only, not curl|bash (fixes#2438).
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add defense-in-depth validation across all e2e cloud driver scripts:
- Validate IP addresses match IPv4 format before use in SSH commands
(aws, digitalocean, gcp, hetzner)
- Validate SSH username contains only safe characters (gcp)
- Validate resource IDs are numeric before interpolating into API URLs
(digitalocean droplet IDs, hetzner server IDs)
- URL-encode app name in Hetzner API query parameter to prevent
query parameter injection
- Validate numeric env vars (INPUT_TEST_TIMEOUT, PROVISION_TIMEOUT,
INSTALL_WAIT) that get interpolated into remote command strings
Fixes#2432, #2433, #2434, #2435, #2442
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
status.ts passed server_id from history directly into Hetzner/DO API
URLs without calling validateServerIdentifier(). Both delete.ts and
connect.ts validate first; status.ts was the only gap. A tampered
~/.spawn/history.json could craft a server_id with path traversal
characters (e.g. "../v2/account") causing the Bearer token to be
sent to an unintended API endpoint (SSRF via URL path manipulation).
Fix: call validateServerIdentifier() after extracting serverId,
returning "unknown" gracefully on failure.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The validate-file.ts hook previously only blocked `set -u` when
`set -eo pipefail` was absent from the file. This allowed scripts
with both `set -eo pipefail` and `set -u` to pass validation,
contradicting the shell rules that unconditionally ban nounset.
Fix the regex to always reject `set -u` variants on actual set
invocation lines (not comments or strings), and update the error
message to recommend `${VAR:-}` instead.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
These path-utility tests were duplicated between history.test.ts and
paths.test.ts. Consolidate into paths.test.ts (the canonical location)
and move 4 unique test cases (dot-relative path, .. resolution, outside
home rejection, home-as-SPAWN_HOME) that only existed in history.test.ts.
Removes 64 lines of duplicate test code with zero coverage loss.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Without per-process timeouts, if the user's network drops during
cloud-init polling, the CLI hangs forever while billing continues.
Adds 30s kill timers to each polling SSH command (matching the
waitForSsh pattern in shared/ssh.ts) and 330s to DO's streaming SSH.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds sprite-keep-running support so sprites stay alive during long
agent sessions instead of shutting down due to inactivity.
- Add installSpriteKeepAlive() to sprite/sprite.ts: downloads and
installs the sprite-keep-running script (~/.local/bin) on the sprite
during setup. Non-fatal: logs a warning if download fails so
deployment still proceeds.
- Modify interactiveSession() to wrap the session command in a temp
script (base64-encoded to handle multi-line restart loops) and exec
it via sprite-keep-running if available, with plain bash fallback.
- Call installSpriteKeepAlive() in sprite/main.ts createServer() step
after setupShellEnvironment(), applying to all Sprite agents.
- Add sprite-keep-alive.test.ts: 11 unit tests covering download URL,
install path, error resilience, session script structure, and
keep-alive wrapper inclusion.
Fixes#2424
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: set SPAWN_HOME in preload and add fs-sandbox guardrail test
The test preload now sets SPAWN_HOME to the sandbox directory by default,
so tests that call cmdRun/saveSpawnRecord without explicitly setting
SPAWN_HOME no longer write to the real ~/.spawn/history.json.
Add fs-sandbox.test.ts that verifies the sandbox is correctly configured
(HOME, SPAWN_HOME, XDG vars all point to temp). Update testing.md with
mandatory filesystem isolation rules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: add root bunfig.toml and fix biome formatting
Add root-level bunfig.toml with test preload so `bun test` works from
the repo root. Fix biome formatting in orchestrate.test.ts afterEach.
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>
Co-authored-by: Claude <claude@anthropic.com>
Move all filesystem path helpers (getUserHome, getSpawnDir, getHistoryPath,
getSpawnCloudConfigPath, getCacheDir, getCacheFile, getUpdateFailedPath,
getSshDir, getTmpDir) into a single shared/paths.ts module. This eliminates
scattered homedir()/process.env.HOME patterns across 8+ files and provides
a single import source for all path resolution.
- Create packages/cli/src/shared/paths.ts with 9 exported functions
- Update 17 source files to import from paths.ts
- Add re-exports in ui.ts and history.ts for backward compatibility
- Remove direct homedir() imports from gcp, sprite, local, ssh-keys, etc.
- Add comprehensive unit tests in paths.test.ts
- Bump CLI version to 0.15.34
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The manifest was updated to moonshotai/kimi-k2.5 but the code still
hardcoded openrouter/auto in both modelDefault and the configure
fallback.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Bun's os.homedir() reads from getpwuid() and ignores runtime changes to
process.env.HOME. Named imports capture the native function binding, so
patching os.homedir on the default export doesn't propagate. This caused
all test files using homedir() to write .spawn-test-* dirs to the real
home directory instead of the preload sandbox.
Add getUserHome() helper to shared/ui.ts that prefers process.env.HOME,
replace all direct homedir() calls in production and test code.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @commitlint/cli and @commitlint/config-conventional at repo root
- Configure commitlint with project-specific types (security, etc.)
- Set up Husky v9 with commit-msg hook running commitlint
- Add pre-commit hook running biome check on CLI source
Fixes#2406
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The "recovers from corrupted existing history file and creates backup"
test was a subset of the more thorough coverage in
history-corruption.test.ts. Removed the duplicate and its unused
readdirSync import.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Consolidates duplicate server naming logic from 5 cloud modules into shared utilities in src/shared/ui.ts. No behavioral changes - purely structural refactor.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* security: escape pkill regex metacharacters in app_name
Fixes#2409 - escape regex metacharacters (., [, \, *, ^, $) in
app_name before using in pkill -f pattern to prevent unintended
process termination. Even though app_name is validated against a
safe character whitelist, . and - are regex metacharacters that
could match broader patterns than intended.
Note: #2410 (unquoted regex in bash conditional) was already fixed
by a prior commit that refactored the code to use sed instead of
[[ =~ BASH_REMATCH ]].
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: remove dead exec_long functions reintroduced from pre-#2407 code
Remove cloud_exec_long dispatcher and all _*_exec_long() functions
from common.sh and cloud driver files (aws, digitalocean, gcp,
hetzner, sprite). These were explicitly removed as dead code in
PR #2407 (commit c4ae1684) and must not be reintroduced.
Issue #2410 (unquoted regex in bash conditional) is already resolved:
the [[ =~ ]] pattern was previously replaced with case/sed parsing.
Fixes#2409Fixes#2410
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>
The worktree path regex in pre-merge-check.ts used [^\s/]+ which only
matched a single path segment after /tmp/spawn-worktrees/. This blocked
PR merges from nested worktrees like refactor/fix/issue-N used by the
automated refactoring service.
Fix both the TypeScript regex ([^\s/]+ -> [^\s]+) and the inline bash
grep pattern in settings.json ([a-zA-Z0-9._-]+ -> [a-zA-Z0-9._/-]+).
Closes#2401
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The cloud_exec_long dispatcher in common.sh and all five cloud-specific
_exec_long implementations (aws, digitalocean, gcp, hetzner, sprite)
were defined but never called by any code in the e2e test suite.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Before creating symlinks in /usr/local/bin, verify that any existing
symlink points to a safe location ($HOME/.local/*, $HOME/.bun/*,
/usr/local/*, $HOME/.npm-global/*). If a symlink points to an
unexpected location, warn the user and skip to prevent malicious
symlink persistence through reinstalls.
Uses portable `readlink` (without -f) for macOS bash 3.2 compatibility.
Fixes#2402
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add logDebug() function gated on SPAWN_DEBUG=1 for surfacing error
details without cluttering normal output. Refactor 6 silent/overly-broad
catch blocks:
- agent-tarball.ts: split 70-line try into fetch+parse and remote exec
- update-check.ts: remove outer try, wrap only performAutoUpdate
- history.ts: add warnings to swallowed tryCatch results
- oauth.ts: warn when API key save fails
- orchestrate.ts: warn on checkAccountReady and preProvision failures
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: restore @openrouter/spawn-shared workspace package
Restore packages/shared/ as canonical location for parse.ts, result.ts,
and type-guards.ts. CLI shared files become thin re-exports, preserving
all existing import paths. SPA imports switch from fragile relative paths
to the workspace package.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort exports in shared package barrel to satisfy biome
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort SPA imports to satisfy biome organizeImports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Show a proactive warning before the OAuth/token entry flow when the user
has no saved DigitalOcean config and no DO_API_TOKEN env var. This prevents
new users from completing the full setup flow only to fail at provisioning
because their account has no payment method on file.
Warning is shown only once per first-time setup — returning users (who have
a saved token, even if expired or invalid) skip the reminder.
Closes#2395
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>