From 5181f28704a9c2c24820c61fffa4e5448e0bdaf1 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:50:05 -0800 Subject: [PATCH] feat: Add local cloud provider for running agents on local machine (#381) Adds a "local" cloud provider that installs and runs agents directly on the user's machine without any cloud provisioning. This is useful for local development and testing. - local/lib/common.sh: Cloud lib with local execution functions - local/claude.sh: Claude Code agent script - local/openclaw.sh: OpenClaw agent script - local/nanoclaw.sh: NanoClaw agent script - manifest.json: Added local cloud + matrix entries - test/: Updated record.sh and mock.sh for local cloud support Fixes #378 Agent: issue-fixer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- local/README.md | 39 +++++++++++++++++++++ local/claude.sh | 76 ++++++++++++++++++++++++++++++++++++++++ local/lib/common.sh | 85 +++++++++++++++++++++++++++++++++++++++++++++ local/nanoclaw.sh | 78 +++++++++++++++++++++++++++++++++++++++++ local/openclaw.sh | 65 ++++++++++++++++++++++++++++++++++ manifest.json | 28 ++++++++++++++- test/mock.sh | 11 ++++-- test/record.sh | 2 +- 8 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 local/README.md create mode 100644 local/claude.sh create mode 100644 local/lib/common.sh create mode 100644 local/nanoclaw.sh create mode 100644 local/openclaw.sh diff --git a/local/README.md b/local/README.md new file mode 100644 index 00000000..758305ef --- /dev/null +++ b/local/README.md @@ -0,0 +1,39 @@ +# Local Machine + +Run agents directly on your local machine without any cloud provisioning. + +> No server creation or destruction. Installs agents and injects OpenRouter credentials locally. Useful for local development and testing. + +## Agents + +#### Claude Code + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/local/claude.sh) +``` + +#### OpenClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/local/openclaw.sh) +``` + +#### NanoClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/local/nanoclaw.sh) +``` + +## Non-Interactive Mode + +```bash +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + bash <(curl -fsSL https://openrouter.ai/lab/spawn/local/claude.sh) +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `OPENROUTER_API_KEY` | OpenRouter API key (prompted via OAuth if not set) | +| `SPAWN_PROMPT` | If set, runs the agent non-interactively with this prompt | diff --git a/local/claude.sh b/local/claude.sh new file mode 100644 index 00000000..dd32408a --- /dev/null +++ b/local/claude.sh @@ -0,0 +1,76 @@ +#!/bin/bash +set -eo pipefail + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/local/lib/common.sh)" +fi + +log_info "Claude Code on local machine" +echo "" + +# 1. Ensure local prerequisites +ensure_local_ready + +# 2. Install Claude Code if not already installed +if command -v claude &>/dev/null; then + log_info "Claude Code already installed" +else + log_warn "Installing Claude Code..." + curl -fsSL https://claude.ai/install.sh | bash + export PATH="${HOME}/.local/bin:${PATH}" +fi + +# Verify installation +if ! command -v claude &>/dev/null; then + log_error "Claude Code installation failed" + log_error "The 'claude' command is not available" + log_error "Try installing manually: curl -fsSL https://claude.ai/install.sh | bash" + exit 1 +fi +log_info "Claude Code installation verified" + +# 3. Get OpenRouter API key +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +# 4. Inject environment variables +log_warn "Setting up environment variables..." +inject_env_vars_local upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ + "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=" \ + "CLAUDE_CODE_SKIP_ONBOARDING=1" \ + "CLAUDE_CODE_ENABLE_TELEMETRY=0" + +# 5. Configure Claude Code settings +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file" \ + "run_server" + +echo "" +log_info "Local setup completed successfully!" +echo "" + +# 6. Start Claude Code +if [[ -n "${SPAWN_PROMPT:-}" ]]; then + log_warn "Executing Claude Code with prompt..." + export PATH="${HOME}/.local/bin:${PATH}" + source ~/.zshrc 2>/dev/null || true + claude -p "${SPAWN_PROMPT}" +else + log_warn "Starting Claude Code..." + sleep 1 + clear 2>/dev/null || true + export PATH="${HOME}/.local/bin:${PATH}" + source ~/.zshrc 2>/dev/null || true + exec claude +fi diff --git a/local/lib/common.sh b/local/lib/common.sh new file mode 100644 index 00000000..5d85827c --- /dev/null +++ b/local/lib/common.sh @@ -0,0 +1,85 @@ +#!/bin/bash +set -eo pipefail +# Common bash functions for local machine spawn scripts +# No cloud provisioning — runs agents directly on the user's machine + +# ============================================================ +# Provider-agnostic functions +# ============================================================ + +# Source shared provider-agnostic functions (local or remote fallback) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -n "${SCRIPT_DIR}" && -f "${SCRIPT_DIR}/../../shared/common.sh" ]]; then + source "${SCRIPT_DIR}/../../shared/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)" +fi + +# ============================================================ +# Local machine functions +# ============================================================ + +# No authentication needed for local machine +ensure_local_ready() { + log_info "Running on local machine" + + # Ensure basic tools are available + if ! command -v curl &>/dev/null; then + log_error "curl is required but not installed" + return 1 + fi + + check_python_available || return 1 +} + +# No server name needed — use hostname +get_server_name() { + local name + name=$(hostname 2>/dev/null || echo "local") + echo "${name}" +} + +# No server creation — it's the local machine +create_server() { + local name="${1}" + log_info "Using local machine: ${name}" +} + +# No cloud-init needed +wait_for_cloud_init() { + : +} + +# Run a command locally +# The command string is passed directly to bash -c for shell parsing. +# All callers pass trusted, hardcoded command strings (not user input). +run_server() { + local cmd="${1}" + bash -c "${cmd}" +} + +# Copy a file locally +upload_file() { + local local_path="${1}" + local remote_path="${2}" + # Expand ~ in remote_path + local expanded_path="${remote_path/#\~/$HOME}" + mkdir -p "$(dirname "${expanded_path}")" + cp "${local_path}" "${expanded_path}" +} + +# Start an interactive session locally +interactive_session() { + local cmd="${1}" + bash -c "${cmd}" +} + +# No server to destroy +destroy_server() { + log_info "Nothing to destroy (local machine)" +} + +# No servers to list +list_servers() { + printf '%s\n' "$(hostname 2>/dev/null || echo "local")" +} diff --git a/local/nanoclaw.sh b/local/nanoclaw.sh new file mode 100644 index 00000000..afb8a9e0 --- /dev/null +++ b/local/nanoclaw.sh @@ -0,0 +1,78 @@ +#!/bin/bash +set -eo pipefail + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/local/lib/common.sh)" +fi + +log_info "NanoClaw on local machine" +echo "" + +# 1. Ensure local prerequisites +ensure_local_ready + +# 2. Install Node.js/npm if not available +if ! command -v npm &>/dev/null; then + if command -v bun &>/dev/null; then + log_info "Using bun as package manager" + else + log_warn "Installing bun..." + curl -fsSL https://bun.sh/install | bash + export PATH="${HOME}/.bun/bin:${PATH}" + fi +fi + +# 3. Install tsx dependency +log_warn "Installing tsx..." +if command -v bun &>/dev/null; then + bun install -g tsx +elif command -v npm &>/dev/null; then + npm install -g tsx +fi + +# 4. Clone and build nanoclaw +if [[ -d "${HOME}/nanoclaw" ]]; then + log_info "NanoClaw already cloned" +else + log_warn "Cloning nanoclaw..." + git clone https://github.com/gavrielc/nanoclaw.git "${HOME}/nanoclaw" + cd "${HOME}/nanoclaw" && npm install && npm run build +fi + +# 5. Get OpenRouter API key +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +# 6. Inject environment variables +log_warn "Setting up environment variables..." +inject_env_vars_local upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" + +# 7. Create nanoclaw .env file +log_warn "Configuring nanoclaw..." +DOTENV_TEMP=$(mktemp) +chmod 600 "${DOTENV_TEMP}" +track_temp_file "${DOTENV_TEMP}" +printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "${DOTENV_TEMP}" +cp "${DOTENV_TEMP}" "${HOME}/nanoclaw/.env" + +echo "" +log_info "Local setup completed successfully!" +echo "" + +# 8. Start nanoclaw +log_warn "Starting nanoclaw..." +log_warn "You will need to scan a WhatsApp QR code to authenticate." +echo "" +source ~/.zshrc 2>/dev/null || true +cd "${HOME}/nanoclaw" && exec npm run dev diff --git a/local/openclaw.sh b/local/openclaw.sh new file mode 100644 index 00000000..7dc4209a --- /dev/null +++ b/local/openclaw.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -eo pipefail + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/local/lib/common.sh)" +fi + +log_info "OpenClaw on local machine" +echo "" + +# 1. Ensure local prerequisites +ensure_local_ready + +# 2. Install bun if not available +if ! command -v bun &>/dev/null; then + log_warn "Installing bun..." + curl -fsSL https://bun.sh/install | bash + export PATH="${HOME}/.bun/bin:${PATH}" +fi + +# 3. Install openclaw +if command -v openclaw &>/dev/null; then + log_info "OpenClaw already installed" +else + log_warn "Installing openclaw..." + bun install -g openclaw +fi + +# 4. Get OpenRouter API key +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +# 5. Get model preference +MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Openclaw") || exit 1 + +# 6. Inject environment variables +log_warn "Setting up environment variables..." +inject_env_vars_local upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" + +# 7. Configure openclaw +setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" \ + "upload_file" \ + "run_server" + +echo "" +log_info "Local setup completed successfully!" +echo "" + +# 8. Start openclaw gateway and TUI +log_warn "Starting openclaw..." +source ~/.zshrc 2>/dev/null || true +nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 & +sleep 2 +exec openclaw tui diff --git a/manifest.json b/manifest.json index c5ad444d..a446c92c 100644 --- a/manifest.json +++ b/manifest.json @@ -691,6 +691,17 @@ "os_template": "ubuntu-24.04" }, "notes": "Budget VPS provider with cloud-init pre-installed. Starting at $4.95/mo. Requires HOSTINGER_API_KEY from hPanel → Profile → Account Information → API. API docs at developers.hostinger.com" + }, + "local": { + "name": "Local Machine", + "description": "Run agents directly on your local machine without cloud provisioning", + "url": "https://github.com/OpenRouterTeam/spawn", + "type": "local", + "auth": "none", + "provision_method": "none (local execution)", + "exec_method": "bash -c", + "interactive_method": "exec", + "notes": "No cloud provisioning needed. Installs agents and injects OpenRouter credentials locally. Useful for local development and testing." } }, "matrix": { @@ -1128,6 +1139,21 @@ "hostinger/opencode": "missing", "hostinger/plandex": "missing", "hostinger/kilocode": "missing", - "hostinger/continue": "missing" + "hostinger/continue": "missing", + "local/claude": "implemented", + "local/openclaw": "implemented", + "local/nanoclaw": "implemented", + "local/aider": "missing", + "local/goose": "missing", + "local/codex": "missing", + "local/interpreter": "missing", + "local/gemini": "missing", + "local/amazonq": "missing", + "local/cline": "missing", + "local/gptme": "missing", + "local/opencode": "missing", + "local/plandex": "missing", + "local/kilocode": "missing", + "local/continue": "missing" } } \ No newline at end of file diff --git a/test/mock.sh b/test/mock.sh index 9cc632e4..a6107d3a 100644 --- a/test/mock.sh +++ b/test/mock.sh @@ -449,6 +449,9 @@ setup_env_for_cloud() { export HYPERSTACK_API_KEY="test-token-hyper" export HYPERSTACK_SERVER_NAME="test-srv" ;; + local) + # No cloud credentials needed for local + ;; esac } @@ -542,11 +545,13 @@ run_test() { # --- Assertions --- assert_exit_code "${exit_code}" 0 "exits successfully" - # Check that API calls were made + # Check that API calls were made (curl for installs or cloud APIs) assert_log_contains "curl (GET|POST) https://" "makes API calls" - # Check that SSH was used (for remote execution) - assert_log_contains "ssh " "uses SSH" + # Check that SSH was used (for remote execution) — skip for local cloud + if [[ "$cloud" != "local" ]]; then + assert_log_contains "ssh " "uses SSH" + fi # Append result to RESULTS_FILE if set if [[ -n "${RESULTS_FILE:-}" ]]; then diff --git a/test/record.sh b/test/record.sh index c3e8fcda..a4683af3 100644 --- a/test/record.sh +++ b/test/record.sh @@ -1400,7 +1400,7 @@ list_clouds() { total_count=$(echo "$ALL_RECORDABLE_CLOUDS" | wc -w | tr -d ' ') printf '%b\n' " ${ready_count}/${total_count} clouds have credentials set" printf '\n' - printf " CLI-based clouds (not recordable): sprite, gcp, e2b, modal, fly, daytona, northflank, runpod, vastai, koyeb\n" + printf " CLI-based clouds (not recordable): sprite, gcp, e2b, modal, fly, daytona, northflank, runpod, vastai, koyeb, local\n" } # --- Main ---