feat: Add Cherry Servers cloud provider with openclaw and goose (#286)

Add Cherry Servers as a new cloud provider with:
- REST API-based server provisioning
- SSH key management via API
- Full root access to cloud VPS instances
- Hourly billing with no commitments

Implementation includes:
- cherry/lib/common.sh with Cherry Servers API primitives
- cherry/openclaw.sh for OpenClaw deployment
- cherry/goose.sh for Goose deployment
- cherry/README.md with authentication and usage docs
- manifest.json updates (cloud entry + 14 matrix entries)

Agent: cloud-scout

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
This commit is contained in:
A 2026-02-10 15:27:23 -08:00 committed by GitHub
parent 7aaf55ddb5
commit 004aafaca0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 615 additions and 1 deletions

105
cherry/README.md Normal file
View file

@ -0,0 +1,105 @@
# Cherry Servers
Cherry Servers is a European cloud provider offering bare metal and cloud VPS with full root access, hourly billing, and a REST API.
## Authentication
All Cherry Servers scripts require a `CHERRY_AUTH_TOKEN` environment variable.
### Getting your API token
1. Visit [Cherry Servers Portal](https://portal.cherryservers.com/)
2. Click on your profile in the top right
3. Navigate to API Tokens
4. Create a new token or copy an existing one
### Setting the token
```bash
export CHERRY_AUTH_TOKEN="your-token-here"
```
## Configuration
Optional environment variables:
- `CHERRY_AUTH_TOKEN` - API authentication token (required)
- `CHERRY_DEFAULT_PLAN` - Server plan (default: `cloud_vps_1`)
- `CHERRY_DEFAULT_REGION` - Deployment region (default: `eu_nord_1`)
- `CHERRY_DEFAULT_IMAGE` - OS image (default: `Ubuntu 24.04 64bit`)
- `CHERRY_SERVER_NAME` - Custom server hostname
## Available Plans
Cherry Servers offers various cloud VPS and bare metal plans:
- `cloud_vps_1` - 1 vCPU, 2GB RAM, 40GB SSD (default)
- `cloud_vps_2` - 2 vCPU, 4GB RAM, 80GB SSD
- `cloud_vps_3` - 4 vCPU, 8GB RAM, 160GB SSD
- Bare metal plans available through the API
View all plans: https://portal.cherryservers.com/
## Regions
Available regions:
- `eu_nord_1` - Lithuania (default)
- `eu_west_1` - Netherlands
- `us_east_1` - USA East Coast
- `us_west_1` - USA West Coast
- `ap_southeast_1` - Singapore
## Usage Examples
### OpenClaw on Cherry Servers
```bash
export CHERRY_AUTH_TOKEN="your-token"
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cherry/openclaw.sh)
```
### Goose on Cherry Servers
```bash
export CHERRY_AUTH_TOKEN="your-token"
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cherry/goose.sh)
```
### Custom configuration
```bash
export CHERRY_AUTH_TOKEN="your-token"
export CHERRY_DEFAULT_PLAN="cloud_vps_2"
export CHERRY_DEFAULT_REGION="us_east_1"
bash <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cherry/openclaw.sh)
```
## How it works
1. **Authentication** - Validates `CHERRY_AUTH_TOKEN` with Cherry Servers API
2. **SSH Key** - Generates SSH key pair if needed and registers public key
3. **Project** - Fetches your default project ID from Cherry Servers account
4. **Provisioning** - Creates cloud VPS with specified plan, region, and image
5. **Connectivity** - Waits for SSH access and cloud-init completion
6. **Agent Setup** - Installs agent, injects OpenRouter credentials, launches interactive session
## API Documentation
- API Docs: https://api.cherryservers.com/doc/
- CLI (cherryctl): https://github.com/cherryservers/cherryctl
- Portal: https://portal.cherryservers.com/
## Pricing
Cherry Servers uses hourly billing with no long-term commitments. Prices vary by plan and region.
View current pricing: https://www.cherryservers.com/pricing
## Notes
- All Cherry Servers instances use `root` user for SSH access
- Servers are created with your registered SSH key automatically attached
- Full root access and IP-KVM available
- Cloud-init is supported for automated setup
- Servers can be managed via API, CLI (cherryctl), or web portal

64
cherry/goose.sh Executable file
View file

@ -0,0 +1,64 @@
#!/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)"
# shellcheck source=cherry/lib/common.sh
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/cherry/lib/common.sh)"
fi
log_info "Goose on Cherry Servers"
echo ""
# 1. Resolve Cherry Servers API token
ensure_cherry_token
# 2. Generate + register SSH key
ensure_ssh_key
# 3. Get server name and create server
SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
# 4. Wait for SSH and cloud-init
verify_server_connectivity "${CHERRY_SERVER_IP}"
wait_for_cloud_init "${CHERRY_SERVER_IP}" 60
# 5. Install Goose
log_warn "Installing Goose..."
run_server "${CHERRY_SERVER_IP}" "CONFIGURE=false curl -fsSL https://github.com/block/goose/releases/latest/download/download_cli.sh | bash"
# Verify installation succeeded
if ! run_server "${CHERRY_SERVER_IP}" "command -v goose &> /dev/null && goose --version &> /dev/null"; then
log_error "Goose installation verification failed"
log_error "The 'goose' command is not available or not working properly on server ${CHERRY_SERVER_IP}"
exit 1
fi
log_info "Goose installation verified successfully"
# 6. 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
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${CHERRY_SERVER_IP}" upload_file run_server \
"GOOSE_PROVIDER=openrouter" \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
echo ""
log_info "Cherry Servers server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${CHERRY_SERVER_ID}, IP: ${CHERRY_SERVER_IP})"
echo ""
# 7. Start Goose interactively
log_warn "Starting Goose..."
sleep 1
clear
interactive_session "${CHERRY_SERVER_IP}" "source ~/.zshrc && goose"

345
cherry/lib/common.sh Executable file
View file

@ -0,0 +1,345 @@
#!/bin/bash
# Cherry Servers-specific functions for Spawn
# Source shared provider-agnostic functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
if [[ -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
# ============================================================
# Cherry Servers Configuration
# ============================================================
CHERRY_API_BASE="https://api.cherryservers.com/v1"
CHERRY_DEFAULT_PLAN="${CHERRY_DEFAULT_PLAN:-cloud_vps_1}"
CHERRY_DEFAULT_REGION="${CHERRY_DEFAULT_REGION:-eu_nord_1}"
CHERRY_DEFAULT_IMAGE="${CHERRY_DEFAULT_IMAGE:-Ubuntu 24.04 64bit}"
# ============================================================
# Authentication
# ============================================================
# Get Cherry Servers API token
ensure_cherry_token() {
local token="${CHERRY_AUTH_TOKEN:-}"
if [[ -z "$token" ]]; then
log_warn "CHERRY_AUTH_TOKEN not found in environment"
log_info "Get your API token from: https://portal.cherryservers.com/"
printf "Enter your Cherry Servers API token: "
read -r token
fi
if [[ -z "$token" ]]; then
log_error "API token is required"
exit 1
fi
CHERRY_AUTH_TOKEN="$token"
export CHERRY_AUTH_TOKEN
}
# ============================================================
# SSH Key Management
# ============================================================
# Ensure SSH key exists and is registered with Cherry Servers
ensure_ssh_key() {
check_python_available
generate_ssh_key_if_missing
local ssh_pub_key
ssh_pub_key=$(cat ~/.ssh/id_rsa.pub)
# Check if key already exists
log_info "Checking for existing SSH key in Cherry Servers..."
local existing_keys
existing_keys=$(curl -s -X GET \
-H "Authorization: Bearer ${CHERRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
"${CHERRY_API_BASE}/ssh-keys" 2>&1)
local key_fingerprint
key_fingerprint=$(get_ssh_fingerprint)
# Check if our key is already registered
local key_id
key_id=$(printf '%s' "$existing_keys" | python3 -c "
import sys, json
try:
keys = json.load(sys.stdin)
fingerprint = '${key_fingerprint}'
for key in keys:
if key.get('fingerprint', '') == fingerprint:
print(key.get('id', ''))
break
except:
pass
" 2>&1)
if [[ -n "$key_id" ]]; then
log_info "SSH key already registered (ID: $key_id)"
CHERRY_SSH_KEY_ID="$key_id"
export CHERRY_SSH_KEY_ID
return 0
fi
# Register new SSH key
log_info "Registering new SSH key with Cherry Servers..."
local label="spawn-$(date +%s)"
local response
response=$(curl -s -X POST \
-H "Authorization: Bearer ${CHERRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"label\": \"$label\", \"key\": \"$ssh_pub_key\"}" \
"${CHERRY_API_BASE}/ssh-keys" 2>&1)
key_id=$(printf '%s' "$response" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
print(data.get('id', ''))
except:
pass
" 2>&1)
if [[ -z "$key_id" ]]; then
log_error "Failed to register SSH key"
log_error "Response: $response"
exit 1
fi
log_info "SSH key registered successfully (ID: $key_id)"
CHERRY_SSH_KEY_ID="$key_id"
export CHERRY_SSH_KEY_ID
}
# ============================================================
# Server Management
# ============================================================
# Get project ID (required for server creation)
get_cherry_project_id() {
check_python_available
local projects
projects=$(curl -s -X GET \
-H "Authorization: Bearer ${CHERRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
"${CHERRY_API_BASE}/projects" 2>&1)
local project_id
project_id=$(printf '%s' "$projects" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
if isinstance(data, list) and len(data) > 0:
print(data[0].get('id', ''))
except:
pass
" 2>&1)
if [[ -z "$project_id" ]]; then
log_error "No project found in Cherry Servers account"
log_error "Create a project at https://portal.cherryservers.com/"
exit 1
fi
printf '%s' "$project_id"
}
# Get server name (generate or prompt)
get_server_name() {
local server_name="${CHERRY_SERVER_NAME:-}"
if [[ -z "$server_name" ]]; then
server_name="spawn-$(date +%s)"
fi
printf '%s' "$server_name"
}
# Create server
# Sets CHERRY_SERVER_ID and CHERRY_SERVER_IP as exports
create_server() {
local hostname="$1"
local plan="${CHERRY_DEFAULT_PLAN}"
local region="${CHERRY_DEFAULT_REGION}"
local image="${CHERRY_DEFAULT_IMAGE}"
check_python_available
local project_id
project_id=$(get_cherry_project_id)
log_info "Creating Cherry Servers server..."
log_info "Plan: $plan, Region: $region, Image: $image"
local payload
payload=$(python3 -c "
import json
data = {
'plan': '$plan',
'region': '$region',
'image': '$image',
'hostname': '$hostname',
'ssh_keys': [${CHERRY_SSH_KEY_ID}]
}
print(json.dumps(data))
")
local response
response=$(curl -s -X POST \
-H "Authorization: Bearer ${CHERRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" \
"${CHERRY_API_BASE}/projects/${project_id}/servers" 2>&1)
local server_id
server_id=$(printf '%s' "$response" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
print(data.get('id', ''))
except:
pass
" 2>&1)
if [[ -z "$server_id" ]]; then
log_error "Failed to create server"
log_error "Response: $response"
exit 1
fi
log_info "Server created with ID: $server_id"
CHERRY_SERVER_ID="$server_id"
export CHERRY_SERVER_ID
# Wait for IP assignment
log_info "Waiting for IP address assignment..."
local ip_address=""
local attempts=0
local max_attempts=60
while [[ -z "$ip_address" ]] && [[ $attempts -lt $max_attempts ]]; do
sleep "${POLL_INTERVAL}"
local server_info
server_info=$(curl -s -X GET \
-H "Authorization: Bearer ${CHERRY_AUTH_TOKEN}" \
-H "Content-Type: application/json" \
"${CHERRY_API_BASE}/servers/${server_id}" 2>&1)
ip_address=$(printf '%s' "$server_info" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
addresses = data.get('ip_addresses', [])
for addr in addresses:
if addr.get('type') == 'primary-ip':
print(addr.get('address', ''))
break
except:
pass
" 2>&1)
attempts=$((attempts + 1))
done
if [[ -z "$ip_address" ]]; then
log_error "Failed to get server IP address"
exit 1
fi
log_info "Server IP: $ip_address"
CHERRY_SERVER_IP="$ip_address"
export CHERRY_SERVER_IP
}
# ============================================================
# Execution Functions
# ============================================================
# Run command on server via SSH
run_server() {
local ip="$1"
local command="$2"
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -o ConnectTimeout=10 \
"root@${ip}" "$command"
}
# Upload file to server via SCP
upload_file() {
local ip="$1"
local local_path="$2"
local remote_path="$3"
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -o ConnectTimeout=10 \
"$local_path" "root@${ip}:${remote_path}"
}
# Start interactive SSH session
interactive_session() {
local ip="$1"
local command="${2:-}"
if [[ -n "$command" ]]; then
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -t \
"root@${ip}" "$command"
else
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -t \
"root@${ip}"
fi
}
# ============================================================
# Connectivity and Readiness
# ============================================================
# Verify server is accessible via SSH
verify_server_connectivity() {
local ip="$1"
local max_attempts=60
local attempt=0
log_info "Waiting for SSH connectivity..."
while [[ $attempt -lt $max_attempts ]]; do
if ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR -o ConnectTimeout=5 \
"root@${ip}" "echo 'SSH ready'" &> /dev/null; then
log_info "SSH connection established"
return 0
fi
attempt=$((attempt + 1))
sleep "${POLL_INTERVAL}"
done
log_error "Failed to connect to server via SSH"
exit 1
}
# Wait for cloud-init to complete
wait_for_cloud_init() {
local ip="$1"
local timeout="${2:-300}"
log_info "Waiting for system initialization..."
if ! run_server "$ip" "cloud-init status --wait --long" 2>/dev/null; then
log_warn "cloud-init wait timed out or not available, proceeding anyway"
else
log_info "System initialization complete"
fi
}

70
cherry/openclaw.sh Executable file
View file

@ -0,0 +1,70 @@
#!/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)"
# shellcheck source=cherry/lib/common.sh
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/cherry/lib/common.sh)"
fi
log_info "OpenClaw on Cherry Servers"
echo ""
# 1. Resolve Cherry Servers API token
ensure_cherry_token
# 2. Generate + register SSH key
ensure_ssh_key
# 3. Get server name and create server
SERVER_NAME=$(get_server_name)
create_server "${SERVER_NAME}"
# 4. Wait for SSH and cloud-init
verify_server_connectivity "${CHERRY_SERVER_IP}"
wait_for_cloud_init "${CHERRY_SERVER_IP}" 60
# 5. Install dependencies (Node.js + Bun)
log_warn "Installing Node.js and Bun..."
run_server "${CHERRY_SERVER_IP}" "curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs && curl -fsSL https://bun.sh/install | bash"
# 6. Install openclaw via bun
log_warn "Installing openclaw..."
run_server "${CHERRY_SERVER_IP}" "source ~/.bashrc && bun install -g openclaw"
log_info "OpenClaw installed"
# 7. 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
# Get model preference
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Openclaw") || exit 1
log_warn "Setting up environment variables..."
inject_env_vars_ssh "${CHERRY_SERVER_IP}" upload_file run_server \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api"
# 8. Configure openclaw
setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" \
"upload_file ${CHERRY_SERVER_IP}" \
"run_server ${CHERRY_SERVER_IP}"
echo ""
log_info "Cherry Servers server setup completed successfully!"
log_info "Server: ${SERVER_NAME} (ID: ${CHERRY_SERVER_ID}, IP: ${CHERRY_SERVER_IP})"
echo ""
# 9. Start openclaw gateway in background and launch TUI
log_warn "Starting openclaw..."
run_server "${CHERRY_SERVER_IP}" "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &"
sleep 2
interactive_session "${CHERRY_SERVER_IP}" "source ~/.zshrc && openclaw tui"

View file

@ -573,6 +573,22 @@
},
"notes": "Global cloud provider with 25+ datacenters. Hourly billing. Uses AuthClientId/AuthSecret headers for API auth. Async operations via command queue. Requires KAMATERA_API_CLIENT_ID and KAMATERA_API_SECRET from https://console.kamatera.com/keys"
},
"cherry": {
"name": "Cherry Servers",
"description": "Cherry Servers bare metal and cloud VPS via REST API",
"url": "https://www.cherryservers.com/",
"type": "api",
"auth": "CHERRY_AUTH_TOKEN",
"provision_method": "POST /v1/projects/{project_id}/servers with ssh_keys",
"exec_method": "ssh root@IP",
"interactive_method": "ssh -t root@IP",
"defaults": {
"plan": "cloud_vps_1",
"region": "eu_nord_1",
"image": "Ubuntu 24.04 64bit"
},
"notes": "European cloud provider with bare metal and VPS. Hourly billing. Full root access. Requires CHERRY_AUTH_TOKEN from https://portal.cherryservers.com/"
},
"oracle": {
"name": "Oracle Cloud Infrastructure",
"description": "Oracle Cloud compute instances via OCI CLI",
@ -1101,6 +1117,20 @@
"render/gptme": "implemented",
"render/opencode": "implemented",
"render/plandex": "implemented",
"render/kilocode": "implemented"
"render/kilocode": "implemented",
"cherry/claude": "implemented",
"cherry/openclaw": "implemented",
"cherry/nanoclaw": "implemented",
"cherry/aider": "implemented",
"cherry/goose": "implemented",
"cherry/codex": "implemented",
"cherry/interpreter": "implemented",
"cherry/gemini": "implemented",
"cherry/amazonq": "implemented",
"cherry/cline": "implemented",
"cherry/gptme": "implemented",
"cherry/opencode": "implemented",
"cherry/plandex": "implemented",
"cherry/kilocode": "implemented"
}
}