Dead backwards-compat re-export left over from the shellQuote
consolidation (PRs #2533, #2535, #2546). Zero consumers import
shellQuote from gcp/gcp.ts — all correctly import from shared/ui.ts.
Per CLAUDE.md: avoid backwards-compatibility hacks; delete unused code.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove 2 tests from the manifest-integrity.test.ts "structure" describe
block that can never fail:
- "should parse as valid JSON": manifest.json is already parsed via
JSON.parse() at module scope (line 23). If parsing fails, the module
throws and ALL tests fail — this individual test can never provide
an independent failure signal.
- "should have agents, clouds, and matrix top-level keys": after parsing,
Object.keys(manifest.agents/clouds) and Object.entries(manifest.matrix)
are called at module scope (lines 25-27). If those properties were
missing, the module load itself would throw. This test is also guaranteed
to pass whenever any test in the file runs.
Removing these 2 theatrical tests leaves 1403 tests (down from 1405).
All remaining tests provide real signal.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add Telegram and WhatsApp options to OpenClaw setup picker
Adds separate "Telegram" and "WhatsApp" checkboxes to the OpenClaw
setup screen:
- Telegram: prompts for bot token from @BotFather, injects into
OpenClaw config via `openclaw config set`
- WhatsApp: reminds user to scan QR code via the web dashboard
after launch (no CLI setup possible)
Updates USER.md with channel-specific guidance when either is selected.
Bump CLI version to 0.16.16.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: run WhatsApp QR scan interactively before TUI launch
Instead of punting WhatsApp setup to "after launch", runs
`openclaw channels login --channel whatsapp` as an interactive SSH
session between gateway start and TUI launch. The user scans the
QR code with their phone during provisioning setup.
Flow: gateway starts → tunnel set up → WhatsApp QR scan → TUI launch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update WhatsApp hint to reflect pre-TUI QR scanning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add --config and --steps CLI flags for programmatic setup
Add --config <path> flag to load spawn options from a JSON config file
(model, steps, name, setup data like telegram_bot_token). Add --steps
<list> flag for comma-separated setup step control. Both enable the
web UI and headless automation to control which setup steps run.
Priority order: CLI flags > --config file > env vars > defaults.
- New spawn-config.ts module with valibot validation
- OptionalStep extended with dataEnvVar and interactive metadata
- validateStepNames() for step name validation with warnings
- Telegram setup reads TELEGRAM_BOT_TOKEN env var before prompting
- WhatsApp auto-skipped in headless mode with warning
- promptSetupOptions() skipped when SPAWN_ENABLED_STEPS already set
- E2E verify helpers for github, browser, telegram setup artifacts
- QA reference file documenting all agent setup options
- Version bump to 0.17.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add --model flag and priority order tests
- Add --model <id> CLI flag that sets MODEL_ID env var
- --model is extracted before --config so it takes priority
- Add config-priority.test.ts with 8 tests verifying:
- --model overrides config model
- --steps overrides config steps
- --steps "" disables all steps
- --name overrides config name
- Config tokens apply as defaults
- Explicit env vars override config tokens
- Remove preferences.json from priority order docs (not needed)
- Add --model to help text and unknown-flag guidance
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add --model, --config, --steps to README
Document config file format, setup steps table, and new CLI flags
in the commands table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address security review feedback
- Move null byte check before path resolution (defense-in-depth)
- Move agent-setup-options.md from .claude/rules/ to .docs/ (git-ignored)
per documentation policy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve rebase conflicts and deduplicate --model flag extraction
Rebase on main introduced a duplicate --model flag extraction block
(one from the PR at line 804, one from main at line 941). Consolidated
into the single early extraction point with -m shorthand support.
Also removed duplicate --model entry from KNOWN_FLAGS set.
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>
The previous PR (#2536) set the Codex default to gpt-5.1-codex, but the
latest available on OpenRouter is gpt-5.3-codex. Also adds a rules file
documenting each agent's default model to prevent future regressions.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Adds --model / -m CLI flag to override the agent's default LLM model:
spawn codex gcp --model openai/gpt-5.3-codex
Also supports persistent per-agent model preferences via config file at
~/.config/spawn/preferences.json:
{ "models": { "codex": "openai/gpt-5.3-codex" } }
Priority: --model flag > preferences file > agent default.
This enables a future web UI to pass model selection via CLI args when
invoking spawn programmatically to provision machines.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Remove redundant existsSync check inside icon-integrity "is actual PNG
data" tests — the file existence is already verified in the preceding
test, and isPng() will throw if the file is missing.
Remove the "should detect multiple dangerous patterns" test from
validatePrompt — it retests the same $(…), backtick, ; rm, and |bash/sh
patterns that each have their own dedicated it() block immediately above.
Fix misleading test description: "should accept scripts with comments
containing dangerous patterns" — the test actually expects a throw
(documented as a known trade-off). Rename to "should reject…".
Removes 1 test (1381 → 1380) and 18 expect() calls.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* security: add DO_CLIENT_SECRET env var override
Allows users/organizations to supply their own DigitalOcean OAuth
client secret via DO_CLIENT_SECRET env var rather than relying on
the bundled default. The bundled secret remains as fallback.
Fixes#2537
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* chore: bump CLI version to 0.16.19
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.5 <noreply@anthropic.com>
Three root-cause bugs in input test functions:
1. Stdin pass-through broken: cloud_exec uses "printf '...' | base64 -d | bash"
on the remote, meaning bash reads the script from its own stdin — not the
outer process's stdin. "PROMPT=$(base64 -d)" inside the script was reading
from the already-consumed pipe, always producing an empty prompt.
Fix: embed the base64-encoded prompt directly in the remote command string.
Base64 output is [A-Za-z0-9+/=] only — safe to embed in single-quoted strings.
2. Zeroclaw flag wrong: "zeroclaw agent -p" was passing the prompt as
--provider (not --prompt). The correct flag for non-interactive single-message
mode is "-m"/"--message".
3. Codex model stale: "openai/gpt-5-codex" does not exist on OpenRouter.
Updated to "openai/gpt-5.1-codex" which is available.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
PR #2533 hardened GCP with shellQuote() and null-byte rejection, but
left Hetzner, DigitalOcean, AWS, and connect.ts using inline
.replace(/'/g, "'\\''") without null-byte validation.
- Move shellQuote to shared/ui.ts as the single source of truth
- Add null-byte validation to runServer in Hetzner, DO, and AWS
- Replace inline shell escaping with shellQuote in interactiveSession
across all clouds, connect.ts, and agents.ts buildEnvBlock
- Re-export shellQuote from gcp.ts for backwards compatibility
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Consolidate 9 per-credential-type it() blocks in prompt-file-security.test.ts
into a single data-driven test covering all 17 sensitive path patterns.
Merge 2 validatePromptFileStats "accept" tests into one.
Consolidate 4 unicode/encoding-attack it() blocks in security.test.ts
into a single data-driven test. Merge 3 "accept identifier" it() blocks into one.
Removes 19 redundant tests (1400 → 1381) with no loss of coverage.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add null-byte rejection to shellQuote (defense-in-depth)
- Export shellQuote for testability
- Refactor interactiveSession to use shellQuote instead of inline escaping
- Add comprehensive test suite for shellQuote security properties
Fixes#2529
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Consolidate 8 fragmented pipe-to-bash/sh tests in validatePrompt into 2
data-driven tests covering all inputs (with/without whitespace, complex
pipelines, and standalone word acceptance). Merge 3 backtick tests into 1.
Merge 2 whitespace tests into 1. Removes 19 lines of duplicate test setup.
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>
The identical generateCsrfState() helper existed in both
digitalocean/digitalocean.ts and shared/oauth.ts. Export it from
oauth.ts (which digitalocean.ts already imports) and remove the
duplicate copy.
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>
Replace `if (!r.ok) { expect(...) }` and `if (result.ok) { return }` guards
with unconditional assertions using toThrow() or toMatchObject(). These
conditional blocks silently skipped assertions when the condition evaluated
the wrong way, providing false confidence. Also remove now-unused tryCatch
imports from prompt-file-security.test.ts and security.test.ts.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
OpenClaw requires the openrouter/ provider prefix for model IDs.
The previous default (moonshotai/kimi-k2.5) was missing the prefix,
causing "Unknown model" warnings. Reverted to openrouter/openrouter/auto
which uses OpenRouter's auto-router to pick the best model per prompt.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Replace `if (result.ok) { expect(result.data)... }` guards with
`expect(result).toMatchObject({ ok: true, data: ... })`. The old pattern
silently skips inner expects when the condition is false — `toMatchObject`
asserts both discriminant and value in a single unconditional call.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
#2507 pre-selected all setup options. Only browser should default to
enabled — GitHub CLI and reuse-saved-key are opt-in.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The two getTerminalWidth tests only checked that the function returns
a number >= 80. Since the implementation is `process.stdout.columns || 80`,
both assertions are trivially satisfied in any environment and provide
zero regression signal. Removed them along with the unused 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>
The multiselect picker for setup options (Chrome browser, GitHub CLI,
etc.) started with nothing selected. Now all available options are
pre-selected so users get the full setup by default.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The `resolveUsername()` function called `whoami` and validated against a
regex that rejected dots in usernames (e.g. `adrian.hale`), causing
"Invalid username" errors. All other clouds use a static SSH user
(root for Hetzner/DO, ubuntu for AWS).
Switch GCP to use `root` consistently:
- Replace dynamic `whoami` lookup with static `GCP_SSH_USER = "root"`
- Simplify cloud-init startup script (already runs as root)
- Fix bun symlink path to use /root instead of /home/${username}
- Remove unused `username` field from GcpState
Closes#2502
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The "real home ~/.spawn/history.json should not be modified" test was a
false signal: if the file doesn't exist it does `expect(true).toBe(true)`,
and if it does exist it only checks `stat.isFile()` while admitting in
comments that it "can't detect retroactively" whether the file was modified.
This test could never catch the regression it claimed to guard against.
Remove it and drop the unused `statSync` import.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: fallback to manual project entry when gcloud projects list fails
When the user declines the suggested default GCP project and
`gcloud projects list` fails (e.g. lacking resourcemanager.projects.list
permission), prompt for a manual project ID instead of hard-failing.
Also fix selectFromList() to return "" on cancel (Ctrl+C/Escape) rather
than defaultValue, so canceling a project picker is treated as "no
selection" rather than silently re-using the first project.
Fixes#2499
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add GCP project ID format validation for manual entry
Validates user-entered GCP project IDs against the required format
(^[a-z][a-z0-9-]{4,28}[a-z0-9]$) before accepting them. Invalid
entries are rejected with a helpful message and the user is re-prompted.
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>
Replace nested describe-per-agent/cloud loops with data-driven it() blocks
that loop over all entities internally. Reduces test count by 192 (235→43)
while preserving all 659 expect() calls and identical coverage. Failures
now include the entity key in the assertion message for debuggability.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The Hermes Agent installer's setup wizard tries to read from /dev/tty,
which fails in headless/non-interactive cloud VM environments. The
installer supports --skip-setup to bypass the wizard; pass it via
bash -s -- --skip-setup.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The "should accept all example prompts from issue #2249" test block
contained 3 assertions already covered by surrounding tests:
- "Fix the merge conflict >> registration flow" (duplicated)
- "Run tests && deploy if they pass" (duplicated)
- "The output where X > Y is slow" (duplicated)
The one unique assertion ("Add a heredoc to the Dockerfile") has been
folded into the existing "developer phrases" test, which covers the
same false-positive category (prose containing shell-like syntax).
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: navigate back to list after delete/remove errors instead of exiting
Previously, choosing "Delete this server" or "Remove from history" from
the action menu would always exit the picker — even if the operation
failed. Now handleRecordAction returns "back" for delete/remove actions,
and activeServerPicker refreshes the remaining list and loops back to
the picker. Cancel on the action menu also returns to the list.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add ValueOf<T> type helper and GritQL enum ban rule
- Add shared ValueOf<T> type that extracts value unions from const objects
and readonly tuples
- Update RecordActionOutcome to use ValueOf<typeof RecordActionOutcome>
- Add lint/no-ts-enum.grit GritQL rule that bans TypeScript enum keyword
- Register new rule in biome.json plugins
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sort type export before value exports in shared index
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add biome config for shared package, fix export sort order
Add biome.json to packages/shared so lint + format + import organization
is enforced on the shared library. Fix ValueOf export position to match
biome's organizeImports sort order (type specifiers after value exports).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: hoist type re-exports to top of shared index
Split inline `type Result` and `type ValueOf` out of mixed export
statements into separate `export type { ... }` re-exports, hoisted
to the top per biome's organizeImports group config.
biome's useExportType rule doesn't flag re-exports (only locally
defined types), so these must be manually separated.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: consolidate biome config to single root biome.json
Remove per-package biome.json files (packages/cli, packages/shared,
.claude/scripts, .claude/skills/setup-spa) and consolidate into a
single root config with includes glob covering packages/**/*.ts.
Update GritQL rule exclusions to also match shared/src/ paths now
that the shared package is covered by the root config. Fix build-clouds.ts
lint issues (node: protocol, block statements, import sort) that were
newly caught.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: replace grit filename exclusions with biome-ignore comments
Remove all $filename exclusion logic from GritQL rules and instead add
biome-ignore-all comments at the top of files that legitimately need
the banned patterns (result.ts, parse.ts, type-guards.ts).
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>
* fix: remove spinner from delete command to prevent output overlap
The delete spinner in confirmAndDelete collided with cloud-specific
destroy functions that print their own progress (logStep/logInfo).
This caused the "Instance destroyed" message to overwrite the spinner
line without a newline, producing garbled output.
Remove the spinner and let the cloud destroy functions handle progress
output directly, then show a clean success/failure message after.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: redirect cloud destroy output into delete spinner
Cloud destroy functions (logStep/logInfo) write progress to stderr,
which collided with the @clack spinner on the terminal. Now stderr
writes during the delete are intercepted and fed into s.message()
so the spinner text updates in place instead of garbling the output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add delete spinner behavior tests
Verify that confirmAndDelete:
- Feeds stderr output from cloud destroy functions into spinner.message()
- Calls spinner.clear() (not stop) so no spinner chrome remains
- Shows p.log.success with the last stderr message as detail
- Shows p.log.error on failure
- Always restores process.stderr.write, even on error
- Works when destroy produces no stderr output
Also adds spinnerClear to the shared test-helpers mock.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove global cloud module mocks that polluted other tests
Only mock hetzner (the cloud used by test records). Other cloud modules
are left un-mocked since they're never called for hetzner records. This
fixes the DO payment warning test failures caused by mock.module being
process-global in Bun.
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>
* fix: prompt to enable Compute Engine API on GCP SERVICE_DISABLED error
New GCP users hit SERVICE_DISABLED because the Compute Engine API isn't
enabled by default. Detects this error, opens the activation URL in
the browser, and prompts the user to retry after enabling it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add beta flags section to README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
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>
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>