Moved duplicate get_cloud_init_userdata() function from all 4 cloud
provider common.sh files to shared/common.sh. This eliminates 60+ lines
of duplication and centralizes cloud-init configuration.
Changes:
- Added get_cloud_init_userdata() to shared/common.sh with detailed comments
- Removed duplicate function from hetzner/lib/common.sh
- Removed duplicate function from digitalocean/lib/common.sh
- Removed duplicate function from vultr/lib/common.sh
- Removed duplicate function from linode/lib/common.sh
- Added comment that clouds can override if needed
All tests pass (42 passed, 0 failed).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Enhanced error messages across all cloud providers to be more actionable:
- Changed generic "API token/key is required" to "cannot be empty"
- Added specific authentication failure messages with provider URLs
- Included permission verification hints
- Added non-interactive mode environment variable suggestions
Benefits:
- Users get clear guidance on how to fix authentication issues
- Error messages now include direct links to token management pages
- Better UX for both interactive and non-interactive usage
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Moved duplicate SSH_OPTS constant from all 4 cloud provider common.sh
files to shared/common.sh. This removes 4 lines of duplication and
centralizes SSH configuration.
Changes:
- Added SSH_OPTS to shared/common.sh with comment explaining clouds can override
- Removed SSH_OPTS from hetzner/lib/common.sh
- Removed SSH_OPTS from digitalocean/lib/common.sh
- Removed SSH_OPTS from vultr/lib/common.sh
- Removed SSH_OPTS from linode/lib/common.sh
All tests pass (42 passed, 0 failed).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added chmod 600 to all temporary files that contain sensitive data (API keys, tokens, configs):
- ENV_TEMP: 35 files (all agent scripts across 5 clouds)
- OPENCLAW_CONFIG_TEMP: 5 files (already done in previous commit)
- SETTINGS_TEMP: 5 files (Claude Code settings)
- GLOBAL_STATE_TEMP: 5 files (Claude Code global state)
- DOTENV_TEMP: 5 files (NanoClaw .env files)
Total: 55 temp files secured
This prevents race conditions where sensitive data could be read by other users
between mktemp creation (mode 600 by default) and data being written.
Security hardening for task #23.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created helper functions in shared/common.sh to simplify ensure_ssh_key():
- generate_ssh_key_if_missing(key_path): Generate SSH key if needed
- get_ssh_fingerprint(pub_path): Get MD5 fingerprint
- json_escape(string): JSON-escape strings for API bodies
Refactored ensure_ssh_key() in all cloud providers to use these helpers:
- Reduced function length from 35-52 lines to 24-28 lines
- Eliminated nested conditionals
- Made the code flow more linear and readable
- Centralized SSH key generation and fingerprint logic
Benefits:
- Single source of truth for SSH key operations
- Reduced code duplication (~40 lines per provider → 3 helper functions)
- Easier to maintain and test
- More consistent error handling
All tests pass (42/42).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Created generic_ssh_wait() in shared/common.sh to eliminate ~100 lines
of duplicated waiting logic across all cloud providers.
Changes:
- Added generic_ssh_wait(ip, ssh_opts, test_cmd, description, max_attempts, interval)
to shared/common.sh
- Refactored verify_server_connectivity() in all clouds to use generic_ssh_wait
- Refactored wait_for_cloud_init() in all clouds to use generic_ssh_wait
Benefits:
- Single source of truth for SSH polling logic
- Consistent error messages across providers
- Reduced code duplication (~20 lines per provider → 2 lines)
- Easier to maintain and test
All tests pass (42/42).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Improved bash safety by quoting numeric comparison variables:
- $elapsed and $timeout in OAuth timeout loop
- $server_pid in process cleanup (kill and wait)
This prevents potential word splitting and follows bash best practices
for variable quoting in numeric contexts.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add set -euo pipefail for strict error handling
- Make color constants readonly (RED, GREEN, YELLOW, NC)
- Make VULTR_API_BASE and SSH_OPTS readonly
- Quote numeric comparison variables in verify_server_connectivity
- Quote numeric comparison variables in wait_for_cloud_init
- Quote numeric comparison variables in create_server
- Quote process IDs and timeout variables in try_oauth_flow
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add set -euo pipefail for strict error handling
- Make color constants readonly (RED, GREEN, YELLOW, NC)
- Make LINODE_API_BASE and SSH_OPTS readonly
- Quote numeric comparison variables in verify_server_connectivity
- Quote numeric comparison variables in wait_for_cloud_init
- Quote numeric comparison variables in create_server
- Quote process IDs and timeout variables in try_oauth_flow
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add set -euo pipefail for strict error handling
- Make color constants readonly (RED, GREEN, YELLOW, NC)
- Make DO_API_BASE and SSH_OPTS readonly
- Quote numeric comparison variables in verify_server_connectivity
- Quote numeric comparison variables in wait_for_cloud_init
- Quote numeric comparison variables in create_server
- Quote process IDs and timeout variables in try_oauth_flow
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added validate_model_id() function to all common.sh files to prevent
command injection via user-supplied MODEL_ID values. MODEL_ID is used
in JSON configs, shell commands, and exported to remote systems, so
validation is critical.
Validation enforces that MODEL_ID contains only safe characters:
- Letters (a-z, A-Z)
- Numbers (0-9)
- Separators: / - _ : .
Rejects dangerous characters like backticks, $(), quotes, semicolons
that could be used for command injection.
Changes:
- Added validate_model_id() to all lib/common.sh files
- Added validation calls after MODEL_ID input in all agent scripts
- Tests pass for all sprite scripts
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add set -euo pipefail for strict error handling
- Make color constants readonly (RED, GREEN, YELLOW, NC)
- Make HETZNER_API_BASE and SSH_OPTS readonly
- Quote numeric comparison variables in verify_server_connectivity
- Quote numeric comparison variables in wait_for_cloud_init
- Quote process IDs and timeout variables in try_oauth_flow
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added quotes to numeric and process ID variables for safer bash execution:
- Quoted $attempt, $max_attempts in while loop conditions
- Quoted $nc_status in conditional checks
- Quoted $server_pid in kill and wait commands
- Quoted $elapsed, $timeout in timeout loop
This prevents potential issues with word splitting and glob expansion.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed command injection vulnerability in sprite/openclaw.sh where
OPENCLAW_CONFIG was echoed directly into remote shell command with
user-controlled MODEL_ID variable. Changed to use temp file + secure
upload instead of inline echo.
Also added chmod 600 to all OPENCLAW_CONFIG_TEMP files across all
cloud providers (linode, vultr, digitalocean, hetzner, sprite) to
prevent race condition where credentials could be exposed in temp
files before being written.
Changes:
- sprite/openclaw.sh: Replaced echo with temp file + sprite exec -file
- All openclaw.sh: Added chmod 600 after mktemp for credentials
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added `set -euo pipefail` for safer bash execution:
- set -e: Exit immediately on error
- set -u: Exit on undefined variable usage
- set -o pipefail: Catch errors in piped commands
Made color constants readonly to prevent accidental modification.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Open Interpreter provides a natural language interface for computer control.
Works with OpenRouter via OPENAI_BASE_URL=https://openrouter.ai/api/v1.
- Implemented on all 5 clouds: sprite, hetzner, digitalocean, vultr, linode
- Matrix now 7 agents x 5 clouds = 35/35 implemented
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Linode instances via REST API v4, with cloud-init via metadata.user_data.
- linode/lib/common.sh: API wrapper, token management, instance lifecycle
- All 6 agents: claude, openclaw, nanoclaw, aider, goose, codex
Matrix now 6 agents x 5 clouds = 30/30 implemented.
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Goose is Block's open-source model-agnostic AI coding agent.
Supports OpenRouter via GOOSE_PROVIDER=openrouter env var.
- sprite/goose.sh, hetzner/goose.sh, digitalocean/goose.sh
- Matrix now 5 agents x 3 clouds = 15/15 implemented
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Aider is the most popular open-source AI pair programming CLI.
Natively supports OpenRouter via OPENROUTER_API_KEY env var
and --model openrouter/... flag.
- sprite/aider.sh, hetzner/aider.sh, digitalocean/aider.sh
- Matrix now 4 agents x 3 clouds = 12/12 implemented
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New cloud provider with full agent coverage:
- digitalocean/lib/common.sh: DO API wrapper, token management, droplet lifecycle
- digitalocean/claude.sh, openclaw.sh, nanoclaw.sh: all 3 agents
Matrix is now 3 agents x 3 clouds = 9/9 implemented.
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fills the remaining 2 gaps in the agents x clouds matrix:
- hetzner/openclaw.sh: provisions Hetzner server with OpenClaw gateway + TUI
- hetzner/nanoclaw.sh: provisions Hetzner server with NanoClaw WhatsApp agent
Matrix is now 100% complete (3 agents x 2 clouds = 6/6 implemented).
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- manifest.json: tracks agents x clouds matrix with metadata
- CLAUDE.md: instructions for Claude Code to fill gaps and discover new agents/clouds
- improve.sh: loop script that launches Claude Code to expand the matrix
Current matrix: 3 agents (claude, openclaw, nanoclaw) x 2 clouds (sprite, hetzner)
with 2 gaps remaining (hetzner/openclaw, hetzner/nanoclaw).
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parallel to the existing sprite/ directory, adds hetzner/ scripts that
provision Hetzner Cloud servers with Claude Code + OpenRouter using
curl-based API calls (no hcloud CLI dependency).
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add styled success page with CSS-animated checkmark, fade-in messaging,
and auto-close after 3 seconds with fallback text if browser blocks it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add nc_listen helper that detects busybox nc and uses -p flag accordingly
- Add termux-open-url support to open_browser
- Deduplicate inline browser opener in try_oauth_flow to use open_browser
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add nc_listen helper that detects busybox nc and uses -p flag accordingly
- Add termux-open-url support to open_browser
- Deduplicate inline browser opener in try_oauth_flow to use open_browser
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add NanoClaw spawn script
NanoClaw is a lightweight WhatsApp-based Claude AI assistant that runs
agents in isolated containers. This script sets up a sprite with
nanoclaw pre-configured: clones the repo, installs dependencies,
configures the API key, and launches in dev mode for WhatsApp QR auth.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* Fix verify_sprite_connectivity exiting script early after single failed check
Retry connectivity up to 6 attempts (30s) instead of trying once and
silently continuing, which caused the next sprite exec to fail under set -e.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add test harness for spawn scripts
Mocks the sprite CLI and runs each script end-to-end verifying:
- common.sh sources correctly and all functions resolve
- Log functions write to stderr (not stdout)
- Env var flow (SPRITE_NAME, OPENROUTER_API_KEY)
- Sprite commands called in correct order
- Temp files created and cleaned up
- Each script reaches its final interactive launch
Usage: bash test/run.sh
42 tests, all passing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Sprite <noreply@sprite.dev>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Retry connectivity up to 6 attempts (30s) instead of trying once and
silently continuing, which caused the next sprite exec to fail under set -e.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- safe_read(): Test /dev/tty is functional before using it (exists
but fails in containers/VMs)
- Log functions: Write to stderr so they don't pollute stdout in
command substitutions like $(get_sprite_name)
- ensure_sprite_exists(): Fix grep regex (use -E for ERE)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
BASH_SOURCE[0] is /proc/self/fd/N with process substitution, not
"-" or "bash". Instead of guessing execution context, just check
if the local lib/common.sh file actually exists.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
## Problem
The command `curl URL | bash` isn't interactive because curl's output
consumes bash's stdin, preventing user prompts from working.
## Solution
Use bash process substitution instead: `bash <(curl URL)`
This keeps stdin available for the script while downloading from curl.
## Changes
- Added INTERACTIVE_CURL.md - Complete guide to interactive execution
- Added NON_INTERACTIVE_MODE.md - Guide to automation/CI usage
- Updated README.md to recommend `bash <(curl ...)` format
- Documented OpenRouter URL alias pattern
## Recommended Usage
Interactive (best UX):
bash <(curl -fsSL https://openrouter.ai/lab/spawn/sprite/claude.sh)
Non-interactive (CI/CD):
SPRITE_NAME=dev-mk1 curl URL | bash
## Why Process Substitution?
- Stdin available for prompts ✅
- Works like normal bash script ✅
- No /dev/tty workarounds needed ✅
- Better user experience ✅
Both methods are supported for maximum compatibility.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The scripts were failing when run via curl | bash because they tried
to read from /dev/tty which doesn't exist in piped contexts.
## Changes
- Added safe_read() helper function that gracefully handles TTY absence
- Updated get_sprite_name() to support SPRITE_NAME env variable
- Updated all read commands to use safe_read()
- Added clear error messages for non-interactive usage
- Updated README with non-interactive mode documentation
## Usage
Interactive:
curl URL | bash
Non-interactive:
SPRITE_NAME=dev-mk1 curl URL | bash
SPRITE_NAME=dev-mk1 OPENROUTER_API_KEY=sk-xxx curl URL | bash
## Fixes
- /dev/tty: No such device or address error
- Scripts now work in CI/CD and automated contexts
- OAuth fallback still works via OPENROUTER_API_KEY env var
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- OAuth authentication flow with OpenRouter to mint API keys
- Automatic sprite creation and environment configuration
- Claude Code setup with bypassed onboarding and dark theme
- OpenClaw setup with gateway and TUI launch
- Shell environment setup (bun PATH, zsh switch)