spawn/e2b/lib/common.sh
A 5ebe3e5a13
fix: add actionable guidance to destroy_server failures and service timeouts (#959)
When server destruction fails, users are left with a bare error message and
no indication that they may still be billed for a running server. This adds
dashboard URLs and clear warnings to destroy_server errors across 9 clouds
(Hetzner, UpCloud, Contabo, Netcup, RamNode, Hostinger, HOSTKEY, OVH,
Latitude). Also improves error messages for Koyeb (app creation, service
deployment, deployment timeout, instance ID), GitHub Codespaces (creation
failure, readiness timeout), and E2B (sandbox creation failure).

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-13 09:38:58 -08:00

157 lines
5.4 KiB
Bash

#!/bin/bash
# Common bash functions for E2B sandbox spawn scripts
# Uses E2B CLI (e2b) — https://e2b.dev
# Sandboxes are lightweight VMs that start in ~150ms
# No SSH — uses `e2b sandbox exec` for commands
# 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
# ============================================================
# E2B specific functions
# ============================================================
ensure_e2b_cli() {
if ! command -v e2b &>/dev/null; then
log_step "Installing E2B CLI..."
npm install -g @e2b/cli 2>/dev/null || {
log_error "Failed to install E2B CLI. Install manually: npm install -g @e2b/cli"
return 1
}
fi
log_info "E2B CLI available"
}
test_e2b_token() {
local test_response
# Test token by listing sandboxes (lightweight API call)
test_response=$(e2b sandbox list 2>&1)
local exit_code=$?
if [[ ${exit_code} -ne 0 ]]; then
if echo "${test_response}" | grep -qi "unauthorized\|invalid.*key\|authentication"; then
log_error "Invalid API key"
log_error "How to fix:"
log_warn " 1. Verify API key at: https://e2b.dev/dashboard"
log_warn " 2. Ensure the key has appropriate permissions"
log_warn " 3. Check key hasn't been revoked"
return 1
fi
fi
return 0
}
ensure_e2b_token() {
ensure_api_token_with_provider \
"E2B" \
"E2B_API_KEY" \
"${HOME}/.config/spawn/e2b.json" \
"https://e2b.dev/dashboard" \
"test_e2b_token"
}
get_server_name() {
get_resource_name "E2B_SANDBOX_NAME" "Enter sandbox name: "
}
create_server() {
local name="${1}"
local template="${E2B_TEMPLATE:-base}"
log_step "Creating E2B sandbox '${name}' (template: ${template})..."
# Create sandbox and capture ID
local output
output=$(e2b sandbox create --template "${template}" --name "${name}" 2>&1)
E2B_SANDBOX_ID=$(echo "${output}" | grep -oE '[a-z0-9]{8}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{12}' | head -1)
if [[ -z "${E2B_SANDBOX_ID}" ]]; then
# Try alternate parsing
E2B_SANDBOX_ID=$(echo "${output}" | grep -oE 'sandbox_[a-zA-Z0-9]+' | head -1)
fi
if [[ -z "${E2B_SANDBOX_ID}" ]]; then
log_error "Failed to create E2B sandbox"
if [[ -n "${output}" ]]; then
log_error "Error: ${output}"
fi
log_error ""
log_error "Common causes:"
log_error " - Invalid or expired API key (verify at: https://e2b.dev/dashboard)"
log_error " - Sandbox limit reached for your account"
log_error " - Template '${template}' not found"
log_error " - Network connectivity issues"
return 1
fi
export E2B_SANDBOX_ID
log_info "Sandbox created: ID=${E2B_SANDBOX_ID}"
}
wait_for_cloud_init() {
log_step "Installing base tools in sandbox..."
run_server "apt-get update -y && apt-get install -y curl unzip git zsh" >/dev/null 2>&1 || true
run_server "curl -fsSL https://bun.sh/install | bash" >/dev/null 2>&1 || true
run_server "curl -fsSL https://claude.ai/install.sh | bash" >/dev/null 2>&1 || true
run_server 'echo "export PATH=\"${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}\"" >> ~/.bashrc' >/dev/null 2>&1 || true
run_server 'echo "export PATH=\"${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}\"" >> ~/.zshrc' >/dev/null 2>&1 || true
log_info "Base tools installed"
}
# E2B uses sandbox exec instead of SSH
# SECURITY: Uses printf %q to properly escape commands to prevent injection
run_server() {
local cmd="${1}"
local escaped_cmd
escaped_cmd=$(printf '%q' "${cmd}")
e2b sandbox exec "${E2B_SANDBOX_ID}" -- bash -c "${escaped_cmd}"
}
upload_file() {
local local_path="${1}"
local remote_path="${2}"
# SECURITY: Validate remote_path to prevent command injection via single-quote breakout
if [[ "$remote_path" == *"'"* || "$remote_path" == *'$'* || "$remote_path" == *'`'* || "$remote_path" == *$'\n'* ]]; then
log_error "Invalid remote path (contains unsafe characters): $remote_path"
return 1
fi
# base64 output is safe (alphanumeric + /+=) so no injection risk
local content
content=$(base64 -w0 "${local_path}" 2>/dev/null || base64 "${local_path}")
e2b sandbox exec "${E2B_SANDBOX_ID}" -- bash -c "printf '%s' '${content}' | base64 -d > '${remote_path}'"
}
interactive_session() {
local cmd="${1}"
local escaped_cmd
escaped_cmd=$(printf '%q' "${cmd}")
e2b sandbox exec "${E2B_SANDBOX_ID}" -- bash -c "${escaped_cmd}"
}
destroy_server() {
local sandbox_id="${1:-${E2B_SANDBOX_ID}}"
log_step "Destroying sandbox ${sandbox_id}..."
e2b sandbox kill "${sandbox_id}" 2>/dev/null || true
log_info "Sandbox destroyed"
}
list_servers() {
e2b sandbox list
}