Shows a non-intrusive "⭐ Enjoying Spawn? Star us on GitHub!" message
to returning users (2+ successful spawns) after a successful spawn
session completes. Shown at most once per 30 days.
- New `maybeShowStarPrompt()` in `shared/star-prompt.ts`
- Tracks `starPromptShownAt` in `~/.config/spawn/preferences.json`
- Called after `execScript()` returns success in cmdRun, cmdInteractive,
and cmdAgentInteractive (skipped in headless mode)
- The `execScript()` return type changed from `void` to `boolean`
to indicate whether the script ran successfully
- Added 7 unit tests covering all gate conditions
Fixes#3020
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#3019
Replace `grep -qx` with `grep -qxF` in the `ensure_in_path` function
to prevent regex pattern injection. Without -F, attacker-controlled
SPAWN_INSTALL_DIR or BUN_INSTALL env vars containing regex metacharacters
(e.g. `/.*`) could cause false positive/negative PATH matches, potentially
bypassing the symlink creation logic.
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
PR #3015 added --yes and -y flags to the delete command but didn't add
them to KNOWN_FLAGS in flags.ts. This caused `spawn delete --name foo --yes`
to fail with "Unknown flag: --yes" because checkUnknownFlags runs before
dispatchDeleteCommand strips these flags.
Also adds delete-specific flags to --help documentation.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Agents running on spawned VMs couldn't delete child spawns because
`spawn delete` requires an interactive terminal for the picker UI.
Added --name and --yes flags: when both are provided in non-interactive
mode, the server matching the name is deleted without prompts. This
enables agents to manage their own child VMs programmatically.
Updated all skill files to teach agents the headless delete syntax.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
sprite console does not accept arguments — it's a pure interactive shell.
When entering an agent on Sprite, use `sprite exec -s NAME -tty` which
supports passing commands via `-- bash -lc CMD`.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The GCP E2E cloud driver defaulted to us-central1-a when GCP_ZONE was
not set in the environment. The QA VM stores zone config in
~/.config/spawn/gcp.json (alongside GCP_PROJECT) but _gcp_validate_env
only read GCP_PROJECT from the environment — it never loaded GCP_ZONE.
This caused E2E failures when us-central1-a had insufficient resources:
3 agents (openclaw, opencode, kilocode) failed with "SSH port never
opened" because GCP couldn't provision instances in that zone.
Fix: load both GCP_PROJECT and GCP_ZONE from the config file in
_gcp_validate_env when they are not already set in the environment,
matching how key-request.sh loads GCP_PROJECT for provisioning.
Verified: all 3 previously failing agents now pass on europe-west1-b.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
remove 3 tests that duplicate scenarios already covered in
cmd-link.test.ts:
- "saves record" (same as "saves a spawn record when agent/cloud given")
- "exits with error for invalid IP" (same as in cmd-link)
- "generates default name" (same as "generates a default name")
remaining 7 tests cover unique paths (IMDS detection, which-binary
fallback, spinner behavior, short flags) not in cmd-link.test.ts.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add allowlist validation for the bun binary path resolved via `command -v bun`
before using it in symlink operations that may run with sudo privileges. If bun
is found at an unexpected location, skip the symlink and warn the user. This
prevents a privilege escalation attack where a malicious binary on PATH could be
symlinked to /usr/local/bin/bun with elevated privileges.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace hand-constructed openrouter.json path with getSpawnCloudConfigPath("openrouter")
for single-source-of-truth path resolution. Remove unused _cloudName parameter since
the function delegates ALL cloud credentials unconditionally.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add /^[A-Za-z0-9+/=]+$/ validation after each .toString("base64") call
in delegateCloudCredentials() and injectEnvVars(), consistent with the
pattern established in agent-setup.ts by #2988.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
ai-review.sh is sourced by e2e.sh but was missing from the bash -n
syntax check loop in sh/test/e2e-lib.sh. This means syntax errors in
ai-review.sh would not be caught by the test harness.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
the `validators` describe block in ui-cov.test.ts duplicated 6 tests
that already exist with full edge-case coverage in ui-utils.test.ts:
- validateServerName (2 tests) → duplicated by 5 tests in ui-utils.test.ts
- validateRegionName (2 tests) → duplicated by 4 tests in ui-utils.test.ts
- validateModelId (2 tests) → duplicated by 6 tests in ui-utils.test.ts
removed tests only checked one accept+one reject per validator, providing
no signal beyond what ui-utils.test.ts already covers exhaustively. also
removed the now-unused imports from the import statement.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ux): replace download spinner with stderr logging, reset terminal before SSH handoff
Fixes two UX issues from live E2E session (#3001):
1. Download spinner (p.spinner from @clack/prompts) wrote ANSI escape codes
to stdout. When stdout is captured (E2E harness, piped output), these
sequences appeared as raw text rather than rendered colors. Replace
p.spinner() in downloadScriptWithFallback and downloadBundle with
logStep/logInfo/logError from shared/ui.ts, which write to stderr and
correctly check isTTY before emitting ANSI codes.
2. Garbled output at start of interactive session (overlapping status lines
from the remote agent's TUI) may be caused by residual ANSI state from
@clack/prompts (hidden cursor, active color attributes). Emit
ESC[?25h ESC[0m to stderr before prepareStdinForHandoff() to explicitly
restore cursor visibility and reset all attributes before the SSH session
takes over.
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve ANSI spinner corruption and garbled output in interactive mode (#3001)
Three root causes fixed:
1. Spinner wrote to stdout while all other CLI status output goes to stderr,
causing ANSI escape sequence interleaving and corruption when both streams
are merged on a terminal. Redirected all p.spinner() calls to process.stderr.
2. unicode-detect.ts (which sets TERM=linux for SSH sessions to force ASCII
fallback) was only imported in commands/shared.ts but not in shared/ui.ts.
Cloud module entry points (hetzner/main.ts, etc.) that import shared/ui.ts
loaded @clack/prompts without the TERM override, causing Unicode spinner
frames in environments that can't render them.
3. After an interactive SSH session ends, the remote agent's TUI (e.g. Claude
Code) may leave the terminal in raw mode with altered attributes. Added
terminal reset (ANSI attribute reset + stty sane) after spawnInteractive()
returns to prevent garbled post-session output.
Agent: ux-engineer
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>
`spyOn(Bun, "serve")` works without the `as never` type assertion.
These casts violated the documented no-type-assertion rule
(`.claude/rules/type-safety.md`). Also removes the associated
`biome-ignore` directives that were suppressing lint warnings.
Agent: style-reviewer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds a non-empty check after mktemp and guards the EXIT trap so rm -rf
only fires when tmpdir is non-empty and still a directory. This is a
defense-in-depth hardening — the current code is safe due to set -e,
but explicit validation is best practice for rm -rf operations.
Fixes#2998
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: sync README commands table with help.ts source of truth
remove 5 command rows from the README commands table that are not present
in packages/cli/src/commands/help.ts getHelpUsageSection():
- spawn list --flat
- spawn list --json
- spawn tree
- spawn tree --json
- spawn history export
these commands exist in code (index.ts, list.ts) but are not listed in the
canonical help section, which is the Gate 2 source of truth per qa/record-keeper
protocol.
* fix: restore documentation for working commands (spawn tree, list --flat, --json, history export)
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The CLI help output only listed 3 of 5 beta features (tarball, images,
docker). The error output on invalid beta flags and the README both
correctly listed all 5. This adds the missing parallel and recursive
entries to --help for consistency.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
delegateCloudCredentials only copied the current cloud's config file
(e.g. sprite.json when spawning on Sprite). Child VMs couldn't spawn
on other clouds because their tokens weren't forwarded.
Now iterates all known clouds and copies every credential file that
exists locally, so the agent can spawn children on any cloud.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two tests in update-check-cov.test.ts were exact duplicates of tests in
update-check.test.ts:
- "skips when recently checked successfully" duplicated "should skip fetch
when last successful check was recent"
- "does not skip when checked timestamp is old (>1h)" duplicated "should
fetch when last successful check is older than 1 hour"
Also removed the now-unused writeUpdateChecked helper function.
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>
- Remove `export` from `getTerminalWidth` in commands/info.ts — only
used internally, not exported from commands/index.ts barrel
- Remove `export` from `makeDockerExec` in shared/orchestrate.ts — only
used internally by `makeDockerRunner`, no external callers
- Bump CLI version to 0.26.6
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Sprite has a bun shim at /.sprite/bin/bun that delegates to
$HOME/.bun/bin/bun, but that binary doesn't exist on fresh VMs.
`command -v bun` returns true (finds the shim) so the install script
skips bun installation, then bun fails when actually invoked.
Fixed in two places:
- installSpawnCli: source shell profiles, test `bun --version` (not
just existence), and install bun fresh if it doesn't work
- install.sh: replace `command -v bun` with `bun --version` to detect
broken shims
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: spawn step skipped when no explicit --steps passed
The spawn skill injection condition used `enabledSteps?.has("spawn")`
which is falsy when enabledSteps is undefined (no --steps flag). Now
checks the recursive beta flag directly and falls through when no
explicit steps are selected, matching how auto-update works.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed skill content in spawn-skill.ts instead of reading from disk
The skills/ directory exists in the repo but isn't bundled when the CLI
is installed via npm. readSkillContent() couldn't find the files at
runtime, causing "No spawn skill file for agent" on every deploy.
Fixed by embedding all skill content directly as string constants in the
module. Removed fs-based getSkillsDir/readSkillContent/getSpawnSkillSourceFile
in favor of a single AGENT_SKILLS config map with inline content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When `--beta recursive` is active, a new "Spawn CLI" setup step injects
agent-native instruction files teaching each agent how to use the `spawn`
CLI to create child VMs. Skill files live in `skills/` at the repo root
and use each agent's native format (YAML frontmatter for Claude/Codex/
OpenClaw, plain markdown for others, append mode for Hermes).
- Add `skills/` directory with 8 agent-specific skill files
- Add `spawn-skill.ts` module with path mapping, file reading, and injection
- Register "spawn" as a conditional setup step gated by `--beta recursive`
- Wire `injectSpawnSkill()` into orchestrate.ts postInstall flow
- Add 52 tests covering path mapping, append mode, file existence, injection
- Bump CLI version to 0.26.0 (minor: new feature)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds non-empty guard to makeDockerExec to make the security boundary
explicit and prevent silent misuse with empty commands.
Fixes#2985
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only `settingsB64` had a validation check. Added the same
`/^[A-Za-z0-9+/=]+$/` guard for wrapperB64, unitB64, and timerB64
before they are interpolated into shell commands, closing the consistency gap.
Fixes#2986
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The `skips when no credential files exist` test in recursive-spawn.test.ts
was failing in the full suite (1911 pass, 1 fail) because other test files
(oauth-cov.test.ts, cmd-uninstall-cov.test.ts) write openrouter.json and
hetzner.json to $HOME/.config/spawn/ without cleanup, contaminating the
shared sandbox HOME used by bun's test runner. The test passed in isolation
but failed 100% of the time in the full suite.
Fix: add a beforeEach inside the delegateCloudCredentials describe block
that removes $HOME/.config/spawn/ before each test, making the test
self-contained and immune to cross-file pollution.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add recursive spawn (--beta recursive)
Enables VMs to spawn child VMs. When --beta recursive is active:
- Injects SPAWN_PARENT_ID, SPAWN_DEPTH, SPAWN_BETA=recursive into .spawnrc
- Installs spawn CLI on the VM via install.sh
- Delegates cloud + OpenRouter credentials to the VM
- Tracks parent_id and depth on SpawnRecord for tree relationships
- Adds `spawn tree` command for full recursive tree view
- Adds `spawn history export` for pulling child history via SSH
- Adds `spawn list --json` and `spawn list --flat` flags
- Adds tree rendering in `spawn list` when parent-child relationships exist
- Adds cascade delete support in delete.ts
- Adds mergeChildHistory() for backward-pass history sync
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add recursive spawn to README
Add --beta recursive to beta features table, new commands
(spawn tree, spawn history export, spawn list --flat/--json)
to commands table, and a dedicated Recursive Spawn section
with usage examples for tree view and cascade delete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add cmdTree coverage tests to fix mock test CI
The CI coverage threshold (90% functions, 80% lines) was failing
because tree.ts had 0% coverage. Added tests that exercise cmdTree
with empty history, tree rendering, JSON output, flat records,
and deleted/depth labels. tree.ts now has 100% coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): validate cloudName and use valibot in pullChildHistory
- Add cloudName validation against ^[a-z0-9-]+$ to prevent
command injection in delegateCloudCredentials
- Export SpawnRecordSchema from history.ts and replace loose
type guard with valibot schema validation in pullChildHistory
- Resolve merge conflicts with main (include both docker and
recursive beta features)
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: add installSpawnCli and delegateCloudCredentials coverage
Export and test installSpawnCli (success + timeout failure paths)
and delegateCloudCredentials (no creds, with creds, write failure,
mkdir failure paths) to improve orchestrate.ts function coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: gritQL rule false positives and delete.ts coverage
- use TsAsExpression() AST node instead of backtick pattern to avoid
matching import aliases as type assertions
- export and test findDescendants() and pullChildHistory() to bring
delete.ts line coverage above the 35% threshold
- add 8 new tests for descendant finding and history pull edge cases
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
* fix: pin all GitHub Actions to commit SHAs and version-lock tools
Addresses supply chain hardening findings from issue #2982:
- Pin all 6 GitHub Actions to full commit SHAs with version comments:
- actions/checkout@v4 → SHA 34e1148...
- oven-sh/setup-bun@v2 → SHA 0c5077e...
- actions/github-script@v7 → SHA f28e40c...
- docker/login-action@v3 → SHA c94ce9f...
- docker/build-push-action@v6 → SHA 10e90e3...
- hashicorp/setup-packer@main → SHA c3d53c5... (v3.2.0)
- Pin Packer version: latest → 1.15.0 (in packer-snapshots.yml)
- Pin bun version: latest → 1.3.11 (in agent-tarballs.yml)
- Pin shellcheck: replace apt-get (no version) with pinned download
of v0.10.0 from GitHub releases with SHA256 integrity check
These changes eliminate the primary LiteLLM-style attack vector:
a compromised action maintainer can no longer force-push malicious
code to an existing tag and have it run in CI.
Fixes#2982
Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: exclude import aliases from no-type-assertion lint rule
The `JsNamedImportSpecifier` exclusion prevents `import { foo as bar }`
patterns from being flagged as type assertions. Previously, any `as`
keyword in import/export statements triggered the ban because the GritQL
pattern `$value as $type` matched import specifiers as well as actual
TypeScript type assertions.
This also removes the `as _foo` import aliases in the script-failure-guidance
test file (replaced with direct imports + distinctly-named wrapper functions)
which were the original manifestation of this bug.
All 1944 tests pass. Biome check clean across 169 files.
Agent: issue-fixer
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>
Remove weaker duplicates found during QA quality sweep:
orchestrate-cov.test.ts: remove "orchestrate restart loop" describe block
(2 tests) — duplicates tests already in orchestrate.test.ts with fewer
assertions (missing "my-agent --run" and "Restarting in 5s" checks).
cmd-delete-cov.test.ts: remove theatrical "intercepts stderr writes to
update spinner" test — handler was a no-op mock, only asserted return
value, never verified actual stderr interception. Duplicate of
"calls custom deleteHandler and reports success" in the same file.
Real stderr/spinner behavior is covered by delete-spinner.test.ts.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- Suppress remote command output in Hetzner runServer() by piping
stdout/stderr instead of inheriting. This prevents raw ANSI escape
sequences from remote install commands (spinners, progress bars)
from leaking into the local terminal as garbled characters, and
eliminates duplicate status messages that were repeated 15+ times.
Captured stderr is logged via logDebug on failure for debugging.
- Add LC_ALL=C.UTF-8 to both the interactive SSH session and the
.spawnrc env config to ensure consistent UTF-8 locale across all
locale categories, preventing garbled Unicode rendering in Claude
Code's TUI welcome interface.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: remove docker from --fast and fix docker cp into container
Two fixes for --beta docker:
1. Remove "docker" from --fast beta features — --fast was auto-enabling
--beta docker, pulling ghcr images that hang the session.
Users must now opt in explicitly with --beta docker.
2. Fix uploadFile in docker mode — .spawnrc was uploaded to the host
but never copied into the container. Add docker cp after SCP upload
so env vars and configs reach the agent inside the container.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: keep docker in --fast beta features
The docker cp fix resolves the hang — no need to remove docker from
--fast. The issue was missing file copy into the container, not the
docker mode itself.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: extract makeDockerRunner helper, fix uploadFile into container
Add makeDockerRunner() that wraps a CloudRunner so all commands and
file uploads target the Docker container. Replaces inline lambdas in
hetzner/main.ts and gcp/main.ts with a clean one-liner.
The key fix: uploadFile now docker cp's files into the container after
SCP — previously .spawnrc (API keys, env vars) only landed on the host,
so the agent inside the container had no config and hung.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(security): shellQuote remotePath in docker cp command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "falls back to most recent record with connection when no spawnId"
test in history-spawn-id.test.ts duplicates the same-named test in
history-cov.test.ts. The history-cov version is more thorough: it uses
two records where the first lacks a connection, exercising the
"skip records without connection" logic. The history-spawn-id version
only had one record, providing no additional signal.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: remove local tarball download, use remote-only tarball install
The local-download-then-SCP-upload path was unnecessary complexity —
downloading a tarball to the user's machine just to re-upload it to the
VM is wasteful. The VM downloads directly from GitHub instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: force zeroclaw native runtime to prevent Docker container hang
ZeroClaw auto-detects Docker and launches in a container (pulling
ghcr.io/openrouterteam/spawn-zeroclaw), which hangs the interactive
session. Force native mode via ZEROCLAW_RUNTIME=native env var and
adapter = "native" in config.toml.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: disable openclaw Docker sandbox to prevent container hang
Same issue as zeroclaw — openclaw auto-detects Docker and runs agents
in containers, hanging the interactive session. Disable via
agents.defaults.sandbox.mode = off in config and fallback JSON.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: disable codex Docker sandbox to prevent container hang
Codex CLI also auto-detects Docker for sandboxing. Set
sandbox_mode = "danger-full-access" in config.toml — the VM itself
provides isolation, Docker sandboxing just causes hangs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 'junie agent envVars include JUNIE_OPENROUTER_API_KEY' test in
agent-setup-cov.test.ts was a weaker duplicate of the more precise
coverage in junie-agent.test.ts, which verifies the exact env var value.
1890 → 1889 tests (1 duplicate removed, 0 regressions).
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract duplicate dockerExec helper from gcp/main.ts and hetzner/main.ts
into shared makeDockerExec() in orchestrate.ts. Both local functions were
identical — wrapping commands with docker exec using DOCKER_CONTAINER_NAME
and shellQuote.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove 5 duplicate test cases from orchestrate-cov.test.ts that were
already covered by orchestrate.test.ts with stronger assertions:
- orchestrate checkAccountReady throws (duplicate, weaker version)
- orchestrate preProvision throws (duplicate, weaker version)
- tarball falls back to install when tarball returns false (exact duplicate)
- tarball skips for local cloud (exact duplicate)
- skipTarball agent flag (exact duplicate)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 180s timeout to uploadFileSprite to prevent indefinite hangs during
tarball uploads. Without a timeout, large tarballs or stalled Sprite
connections block the entire provisioning pipeline past the 720s E2E
provision timeout, causing agent binary not-found failures for openclaw,
zeroclaw, and codex.
Also skip the redundant remote tarball download fallback when a local
tarball was already downloaded but its upload/extract failed -- the
remote download would face the same extraction issues. This saves ~150s
in the fallback chain, leaving enough time for the live install to
complete within the provision timeout.
Fixes#2960
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The hermes install script's mini-swe-agent pip dependency uses
git+ssh:// URLs that timeout on fresh cloud VMs (hetzner/gcp/digitalocean)
where outbound SSH to GitHub is blocked or slow.
Add `git config --global url.https://github.com/.insteadOf` rules
before the hermes install and update commands to force git to use
HTTPS instead of SSH for all GitHub URLs. This eliminates the SSH
connection timeout that was causing install failures.
Fixes#2955
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
safe_substitute: Switch sed delimiter from | to \x01 (SOH control char) across
qa.sh, refactor.sh, security.sh, and discovery.sh. This eliminates delimiter
injection regardless of value content, since \x01 cannot appear in normal input.
Values containing \x01 are explicitly rejected as defense-in-depth.
SPAWN_ISSUE: Fix qa.sh validation from ^[0-9]+$ to ^[1-9][0-9]*$ to reject
leading zeros and zero itself. Add 32-bit signed integer range check
(max 2147483647) to all three scripts (qa.sh, refactor.sh, security.sh)
to prevent integer overflow in downstream consumers.
Fixes#2961Fixes#2962
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Merged "createCloudAgents" and "createCloudAgents detailed" into a single
describe block. Both blocks tested the same function with no structural
distinction, causing duplicate organization without value.
Eliminated 26 repetitive inline runner object constructions by moving
runner and result setup into beforeEach. This removes ~115 lines of
boilerplate while keeping all 21 tests and their assertions intact.
1895 tests still pass.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
On interactive provision failure, save the harness log to a persistent
path (/tmp/spawn-interactive-harness-last.log) for post-mortem inspection,
and filter output to only show [harness] prefixed lines (30 lines) instead
of dumping 50 raw lines of mixed output.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Extracts the inline docker-mode condition from hetzner/main.ts and
gcp/main.ts into a testable exported function in shared/cloud-init.ts,
then adds real unit tests that import from the source. Fixes#2952.
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Hermes installs a Python virtualenv which takes 20+ min on fresh VMs.
The previous 300s install timeout caused the CLI to give up before
writing .spawnrc, leading to 30-min E2E timeouts on Hetzner, DigitalOcean,
and GCP (but not Sprite, which has a manual .spawnrc fallback).
Changes:
- agent-setup.ts: hermes installAgent timeout 300s → 600s
- common.sh: add hermes per-agent overrides (_PROVISION_TIMEOUT_hermes=720,
_AGENT_TIMEOUT_hermes=3600) to give the install enough headroom
- package.json: bump CLI version 0.25.26 → 0.25.27
-- qa/e2e-tester
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
docker-cloudinit-skip.test.ts was reading source file contents with readFileSync
and checking for the presence of specific string literals — a source-grep
anti-pattern that tests the text exists, not that the behavior works.
The waitForReady() closure in hetzner/main.ts and gcp/main.ts cannot be directly
unit tested without refactoring (tracked in #2952). The source-grep tests are
removed to avoid false confidence.
Filed https://github.com/OpenRouterTeam/spawn/issues/2952 to track proper
behavioral testing via extracting the skip-cloud-init condition into a testable
exported helper.
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>
`buildFixScript()` was missing `export LANG='C.UTF-8'` that was added to
the canonical `generateEnvConfig()` in commit f93c799d. Users running
`spawn fix` would get a `.spawnrc` without the UTF-8 locale export,
causing garbled Unicode in agent TUIs — the same regression that f93c799d
fixed for fresh provisioning.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
do-min-size.test.ts was reading source file contents with readFileSync
and checking for the presence of specific strings (bash-grep anti-pattern).
Fixes:
- Export slugRamGb and AGENT_MIN_SIZE from digitalocean.ts
- Import them in main.ts instead of re-defining
- Rewrite do-min-size tests to call functions with inputs and assert outputs
(3 source-grep tests → 6 behavior tests)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Suppress Claude Code curl installer stdout — the remote installer
prints its own "Installation complete!" which duplicated the local
"Claude Code agent installed successfully" message.
2. Export LANG=C.UTF-8 in both the interactive SSH session command and
the .spawnrc env config. Fresh cloud VMs often default to the C
locale which cannot render Unicode properly, causing garbled ANSI
output in agent TUIs (e.g. "⏵⏵bypasspermissionson" instead of
properly spaced text).
Fixes#2946
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Two test files (do-min-size.test.ts, docker-cloudinit-skip.test.ts) existed
on disk but were not documented in the README. Add entries for both.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When the quality cycle e2e-tester re-runs only failed agents
(e.g. `e2e.sh --cloud hetzner zeroclaw codex`), e2e.sh was firing
a matrix email showing only those 2 agents — both PASS if the retry
succeeded. This looked like "2 tests ran, all passed" when in reality
32 tests ran with 2 failures.
- Add SPAWN_E2E_SKIP_EMAIL=1 env var check at the top of send_matrix_email
- Update qa-quality-prompt.md to set SPAWN_E2E_SKIP_EMAIL=1 on re-runs
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>