mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-16 20:01:08 +00:00
Users on exec-based clouds (Fly, Render, Koyeb, Northflank, Railway, Modal, Daytona, E2B, CodeSandbox, GitHub Codespaces) got no warning when their session ended that their service was still running and incurring charges. This adds: - _show_exec_post_session_summary() in shared/common.sh for non-SSH providers that use CLI exec commands instead of direct SSH - SPAWN_DASHBOARD_URL for all 10 exec-based clouds so users get actionable dashboard links - Post-session summary calls in each cloud's interactive_session() - 33 new tests covering the exec post-session summary feature 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>
162 lines
5.5 KiB
Bash
162 lines
5.5 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
|
|
# ============================================================
|
|
|
|
SPAWN_DASHBOARD_URL="https://e2b.dev/dashboard"
|
|
|
|
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: Strict allowlist validation — only safe path characters
|
|
if [[ ! "${remote_path}" =~ ^[a-zA-Z0-9/_.~-]+$ ]]; then
|
|
log_error "Invalid remote path (must contain only alphanumeric, /, _, ., ~, -): ${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}")
|
|
local session_exit=0
|
|
e2b sandbox exec "${E2B_SANDBOX_ID}" -- bash -c "${escaped_cmd}" || session_exit=$?
|
|
SERVER_NAME="${E2B_SANDBOX_ID:-}" _show_exec_post_session_summary
|
|
return "${session_exit}"
|
|
}
|
|
|
|
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
|
|
}
|