diff --git a/manifest.json b/manifest.json index 4e592693..458fcd69 100644 --- a/manifest.json +++ b/manifest.json @@ -603,6 +603,22 @@ "region": "was" }, "notes": "Serverless container platform. Free tier available (no credit card). Per-second billing. Requires koyeb CLI." + }, + "northflank": { + "name": "Northflank", + "description": "Northflank container platform via CLI with exec access", + "url": "https://northflank.com/", + "type": "cli", + "auth": "NORTHFLANK_TOKEN", + "provision_method": "northflank create service deployment with Docker image", + "exec_method": "northflank exec --command", + "interactive_method": "northflank exec (PTY)", + "defaults": { + "cpu": "0.5", + "memory": 1024, + "image": "ubuntu:24.04" + }, + "notes": "Container platform with shell access. Free tier: 2 services. Pay-per-second pricing. Requires API token from https://northflank.com/account/settings/api/tokens" } }, "matrix": { @@ -955,6 +971,20 @@ "koyeb/gptme": "implemented", "koyeb/opencode": "implemented", "koyeb/plandex": "implemented", - "koyeb/kilocode": "implemented" + "koyeb/kilocode": "implemented", + "northflank/claude": "implemented", + "northflank/openclaw": "implemented", + "northflank/nanoclaw": "missing", + "northflank/aider": "implemented", + "northflank/goose": "missing", + "northflank/codex": "missing", + "northflank/interpreter": "missing", + "northflank/gemini": "missing", + "northflank/amazonq": "missing", + "northflank/cline": "missing", + "northflank/gptme": "missing", + "northflank/opencode": "missing", + "northflank/plandex": "missing", + "northflank/kilocode": "missing" } } \ No newline at end of file diff --git a/northflank/README.md b/northflank/README.md new file mode 100644 index 00000000..333d8f36 --- /dev/null +++ b/northflank/README.md @@ -0,0 +1,61 @@ +# Northflank + +Northflank container platform via CLI with exec access. [Northflank](https://northflank.com/) + +> Uses Northflank CLI for container exec. Free tier: 2 services. Pay-per-second pricing. + +## Agents + +#### Claude Code + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/northflank/claude.sh) +``` + +#### OpenClaw + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/northflank/openclaw.sh) +``` + +#### Aider + +```bash +bash <(curl -fsSL https://openrouter.ai/lab/spawn/northflank/aider.sh) +``` + +## Setup + +1. Create a Northflank account at https://northflank.com +2. Generate an API token at https://northflank.com/account/settings/api/tokens +3. Install the Northflank CLI: + +```bash +npm install -g @northflank/cli +``` + +## Non-Interactive Mode + +```bash +NORTHFLANK_SERVICE_NAME=spawn-dev \ +NORTHFLANK_PROJECT_NAME=spawn-project \ +NORTHFLANK_TOKEN=your-token \ +OPENROUTER_API_KEY=sk-or-v1-xxxxx \ + bash <(curl -fsSL https://openrouter.ai/lab/spawn/northflank/claude.sh) +``` + +## Free Tier + +Northflank offers a Developer Sandbox with: +- 2 free services +- 2 free cron jobs +- 1 free database/add-on + +Perfect for testing and hobby projects. Production apps should use pay-as-you-go pricing. + +## Pricing + +Pay-per-second usage-based pricing after free tier: +- Compute: $0.01667 per vCPU/hour, $0.00833 per GB memory/hour +- Disk: $0.30/GB per month +- Network egress: $0.15/GB diff --git a/northflank/aider.sh b/northflank/aider.sh new file mode 100755 index 00000000..a9303b74 --- /dev/null +++ b/northflank/aider.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# shellcheck disable=SC2154 +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=northflank/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/northflank/lib/common.sh)" +fi + +log_info "Aider on Northflank" +echo "" + +# 1. Ensure Northflank CLI and API token +ensure_northflank_cli +ensure_northflank_token + +# 2. Get service and project names, then create service +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 3. Wait for base tools +wait_for_cloud_init + +# 4. Install Aider +log_warn "Installing Aider..." +run_server "pip install aider-chat 2>/dev/null || pip3 install aider-chat" +log_info "Aider installed" + +# 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. Get model preference +echo "" +log_warn "Browse models at: https://openrouter.ai/models" +log_warn "Which model would you like to use with Aider?" +MODEL_ID=$(safe_read "Enter model ID [openrouter/auto]: ") || MODEL_ID="" +MODEL_ID="${MODEL_ID:-openrouter/auto}" + +# 7. Inject environment variables into shell configs +log_warn "Setting up environment variables..." + +inject_env_vars_local upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" + +echo "" +log_info "Northflank service setup completed successfully!" +log_info "Service: ${SERVER_NAME} (Project: ${NORTHFLANK_PROJECT_NAME})" +echo "" + +# 8. Start Aider interactively +log_warn "Starting Aider..." +sleep 1 +clear +interactive_session "source ~/.bashrc && aider --model openrouter/${MODEL_ID}" diff --git a/northflank/claude.sh b/northflank/claude.sh new file mode 100755 index 00000000..b28ee08d --- /dev/null +++ b/northflank/claude.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# shellcheck disable=SC2154 +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=northflank/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/northflank/lib/common.sh)" +fi + +log_info "Claude Code on Northflank" +echo "" + +# 1. Ensure Northflank CLI and API token +ensure_northflank_cli +ensure_northflank_token + +# 2. Get service and project names, then create service +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 3. Wait for base tools +wait_for_cloud_init + +# 4. Install Claude Code +log_warn "Installing Claude Code..." +run_server "curl -fsSL https://claude.ai/install.sh | bash" + +# Verify installation +if ! run_server "command -v claude" >/dev/null 2>&1; then + log_error "Claude Code installation failed" + exit 1 +fi +log_info "Claude Code is installed" + +# 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 into shell configs +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" + +# 7. Configure Claude Code settings +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file" \ + "run_server" + +echo "" +log_info "Northflank service setup completed successfully!" +log_info "Service: ${SERVER_NAME} (Project: ${NORTHFLANK_PROJECT_NAME})" +echo "" + +# 8. Start Claude Code interactively +log_warn "Starting Claude Code..." +sleep 1 +clear +interactive_session "source ~/.bashrc && claude" diff --git a/northflank/lib/common.sh b/northflank/lib/common.sh new file mode 100644 index 00000000..5bf5c168 --- /dev/null +++ b/northflank/lib/common.sh @@ -0,0 +1,285 @@ +#!/bin/bash +# Common bash functions for Northflank spawn scripts +# Uses Northflank CLI (northflank) — https://northflank.com +# Containers with exec/shell access via CLI +# Free tier: 2 services, pay-per-second pricing after + +# Bash safety flags +set -eo pipefail + +# ============================================================ +# 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 + +# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh + +# ============================================================ +# Northflank specific functions +# ============================================================ + +ensure_northflank_cli() { + if ! command -v northflank &>/dev/null; then + log_warn "Installing Northflank CLI..." + npm install -g @northflank/cli 2>/dev/null || { + log_error "Failed to install Northflank CLI. Install manually: npm install -g @northflank/cli" + return 1 + } + fi + log_info "Northflank CLI available" +} + +test_northflank_token() { + local test_response + # Test token by listing projects (lightweight API call) + test_response=$(northflank list projects 2>&1) + local exit_code=$? + + if [[ ${exit_code} -ne 0 ]]; then + if echo "${test_response}" | grep -qi "unauthorized\|invalid.*token\|authentication"; then + log_error "Invalid API token" + log_warn "Remediation steps:" + log_warn " 1. Verify API token at: https://northflank.com/account/settings/api/tokens" + log_warn " 2. Ensure the token has appropriate permissions" + log_warn " 3. Check token hasn't expired (90 day limit)" + return 1 + fi + fi + return 0 +} + +ensure_northflank_token() { + check_python_available || return 1 + + # 1. Check environment variable + if [[ -n "${NORTHFLANK_TOKEN:-}" ]]; then + log_info "Using Northflank token from environment" + # Login with token + if ! northflank login -t "${NORTHFLANK_TOKEN}" &>/dev/null; then + log_error "Northflank token in environment is invalid" + return 1 + fi + return 0 + fi + + local config_file="${HOME}/.config/spawn/northflank.json" + + # 2. Check config file + if [[ -f "${config_file}" ]]; then + local saved_token + saved_token=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1])).get('token',''))" "${config_file}" 2>/dev/null) + if [[ -n "${saved_token}" ]]; then + export NORTHFLANK_TOKEN="${saved_token}" + log_info "Using Northflank token from ${config_file}" + if ! northflank login -t "${saved_token}" &>/dev/null; then + log_warn "Saved Northflank token is invalid, prompting for new one" + unset NORTHFLANK_TOKEN + else + return 0 + fi + fi + fi + + # 3. Prompt and validate + echo "" + log_warn "Northflank API Token Required" + printf '%b\n' "${YELLOW}Get your token at: https://northflank.com/account/settings/api/tokens${NC}" + echo "" + + local token + token=$(safe_read "Enter your Northflank token: ") || return 1 + if [[ -z "${token}" ]]; then + log_error "Northflank token cannot be empty" + log_warn "For non-interactive usage, set: NORTHFLANK_TOKEN=your-token" + return 1 + fi + + export NORTHFLANK_TOKEN="${token}" + if ! northflank login -t "${token}" &>/dev/null; then + log_error "Invalid Northflank token" + unset NORTHFLANK_TOKEN + return 1 + fi + + # Save token + local config_dir="${HOME}/.config/spawn" + mkdir -p "${config_dir}" + printf '{\n "token": "%s"\n}\n' "$(json_escape "${token}")" > "${config_file}" + chmod 600 "${config_file}" + log_info "Northflank token saved to ${config_file}" +} + +get_server_name() { + get_resource_name "NORTHFLANK_SERVICE_NAME" "Enter service name: " +} + +get_project_name() { + get_resource_name "NORTHFLANK_PROJECT_NAME" "Enter project name: " +} + +create_server() { + local name="${1}" + local image="${NORTHFLANK_IMAGE:-ubuntu:24.04}" + local project_name="${NORTHFLANK_PROJECT_NAME:-spawn-project}" + + log_warn "Creating Northflank project '${project_name}'..." + + # Create project (idempotent - won't fail if exists) + northflank create project \ + --name "${project_name}" \ + --description "Spawn AI agent deployment" 2>/dev/null || true + + log_info "Project '${project_name}' ready" + export NORTHFLANK_PROJECT_NAME="${project_name}" + + log_warn "Creating service '${name}' with image: ${image}" + + # Create deployment service with Docker image + local service_output + service_output=$(northflank create service deployment \ + --name "${name}" \ + --project "${project_name}" \ + --image "${image}" \ + --cpu 0.5 \ + --memory 1024 \ + --replicas 1 2>&1) + + if [[ $? -ne 0 ]]; then + log_error "Failed to create service: ${service_output}" + return 1 + fi + + export NORTHFLANK_SERVICE_NAME="${name}" + log_info "Service '${name}' created" + + # Wait for service to be running + log_warn "Waiting for service to start..." + local max_attempts=60 + local attempt=1 + while [[ "${attempt}" -le "${max_attempts}" ]]; do + local status + status=$(northflank get service --name "${name}" --project "${project_name}" 2>/dev/null | grep -i "status" || true) + if echo "${status}" | grep -qi "running\|active"; then + log_info "Service is running" + return 0 + fi + + log_warn "Waiting for service to start (${attempt}/${max_attempts})..." + sleep 3 + attempt=$((attempt + 1)) + done + + log_error "Service did not start in time" + return 1 +} + +wait_for_cloud_init() { + log_warn "Installing base tools in container..." + + # Update package lists and install essentials + run_server "apt-get update -y && apt-get install -y curl git unzip python3 pip" >/dev/null 2>&1 || true + + # Install bun for agent CLI tools + run_server "curl -fsSL https://bun.sh/install | bash" >/dev/null 2>&1 || true + run_server 'echo "export PATH=\"\$HOME/.bun/bin:\$PATH\"" >> ~/.bashrc' >/dev/null 2>&1 || true + run_server 'echo "export PATH=\"\$HOME/.bun/bin:\$PATH\"" >> ~/.zshrc' >/dev/null 2>&1 || true + + log_info "Base tools installed" +} + +# Run a command on the Northflank service via northflank exec +run_server() { + local cmd="${1}" + local project="${NORTHFLANK_PROJECT_NAME}" + local service="${NORTHFLANK_SERVICE_NAME}" + + # SECURITY: Properly escape command to prevent injection + local escaped_cmd + escaped_cmd=$(printf '%q' "${cmd}") + + # Use northflank exec with non-interactive mode + northflank exec \ + --project "${project}" \ + --service "${service}" \ + --command "bash -c ${escaped_cmd}" 2>/dev/null +} + +# Upload a file to the service via base64 encoding through exec +upload_file() { + local local_path="${1}" + local remote_path="${2}" + local content + content=$(base64 -w0 "${local_path}" 2>/dev/null || base64 "${local_path}") + + # SECURITY: Properly escape paths and content to prevent injection + local escaped_path + escaped_path=$(printf '%q' "${remote_path}") + local escaped_content + escaped_content=$(printf '%q' "${content}") + + run_server "echo ${escaped_content} | base64 -d > ${escaped_path}" +} + +# Start an interactive shell session on the Northflank service +interactive_session() { + local cmd="${1}" + local project="${NORTHFLANK_PROJECT_NAME}" + local service="${NORTHFLANK_SERVICE_NAME}" + + # SECURITY: Properly escape command to prevent injection + local escaped_cmd + escaped_cmd=$(printf '%q' "${cmd}") + + # Use northflank exec for interactive shell + northflank exec \ + --project "${project}" \ + --service "${service}" \ + --command "bash -c ${escaped_cmd}" +} + +# Destroy a Northflank service +destroy_server() { + local service_name="${1:-${NORTHFLANK_SERVICE_NAME}}" + local project_name="${NORTHFLANK_PROJECT_NAME}" + + log_warn "Destroying service '${service_name}'..." + + northflank delete service \ + --name "${service_name}" \ + --project "${project_name}" \ + --yes 2>/dev/null || true + + log_info "Service destroyed" +} + +# Inject environment variables into .bashrc and .zshrc +inject_env_vars_northflank() { + local env_temp + env_temp=$(mktemp) + chmod 600 "${env_temp}" + track_temp_file "${env_temp}" + + generate_env_config "$@" > "${env_temp}" + + # Upload and append to both .bashrc and .zshrc + upload_file "${env_temp}" "/tmp/env_config" + run_server "cat /tmp/env_config >> ~/.bashrc && cat /tmp/env_config >> ~/.zshrc && rm /tmp/env_config" + + # Note: temp file will be cleaned up by trap handler +} + +# List Northflank services +list_servers() { + log_info "Northflank services:" + northflank list services 2>/dev/null || { + log_error "Failed to list Northflank services" + return 1 + } +} diff --git a/northflank/openclaw.sh b/northflank/openclaw.sh new file mode 100755 index 00000000..cb6862fd --- /dev/null +++ b/northflank/openclaw.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# shellcheck disable=SC2154 +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=northflank/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/northflank/lib/common.sh)" +fi + +log_info "OpenClaw on Northflank" +echo "" + +# 1. Ensure Northflank CLI and API token +ensure_northflank_cli +ensure_northflank_token + +# 2. Get service and project names, then create service +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 3. Wait for base tools +wait_for_cloud_init + +# 4. Install openclaw via bun +log_warn "Installing openclaw..." +run_server "source ~/.bashrc && bun install -g openclaw" +log_info "OpenClaw installed" + +# 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. Get model preference +MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Openclaw") || exit 1 + +# 7. Inject environment variables into shell configs +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" + +# 8. Configure openclaw +setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" \ + upload_file \ + run_server + +echo "" +log_info "Northflank service setup completed successfully!" +log_info "Service: ${SERVER_NAME} (Project: ${NORTHFLANK_PROJECT_NAME})" +echo "" + +# 9. Start openclaw gateway in background and launch TUI +log_warn "Starting openclaw..." +run_server "source ~/.bashrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" +sleep 2 +interactive_session "source ~/.bashrc && openclaw tui"