mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-08 10:09:30 +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>
257 lines
9.2 KiB
Bash
257 lines
9.2 KiB
Bash
#!/bin/bash
|
|
# Common bash functions for CodeSandbox spawn scripts
|
|
# Uses CodeSandbox SDK + CLI — https://codesandbox.io
|
|
# Sandboxes are Firecracker microVMs with ~2 second start times
|
|
# No SSH — uses CodeSandbox SDK for exec
|
|
|
|
# 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
|
|
|
|
# ============================================================
|
|
# CodeSandbox specific functions
|
|
# ============================================================
|
|
|
|
SPAWN_DASHBOARD_URL="https://codesandbox.io/dashboard"
|
|
|
|
ensure_codesandbox_cli() {
|
|
if ! command -v node &>/dev/null; then
|
|
log_step "Installing Node.js..."
|
|
if command -v curl &>/dev/null; then
|
|
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs 2>/dev/null || {
|
|
log_error "Failed to install Node.js automatically"
|
|
log_error ""
|
|
log_error "Please install Node.js manually:"
|
|
log_error " Ubuntu/Debian: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo bash - && sudo apt-get install -y nodejs"
|
|
log_error " macOS: brew install node"
|
|
log_error " Fedora/RHEL: sudo dnf install nodejs"
|
|
return 1
|
|
}
|
|
else
|
|
log_error "Node.js is required but not installed"
|
|
log_error "Install Node.js: https://nodejs.org/"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if ! npm list -g @codesandbox/sdk &>/dev/null; then
|
|
log_step "Installing CodeSandbox SDK/CLI..."
|
|
npm install -g @codesandbox/sdk 2>/dev/null || {
|
|
log_error "Failed to install CodeSandbox SDK"
|
|
log_error ""
|
|
log_error "Manual installation:"
|
|
log_error " npm install -g @codesandbox/sdk"
|
|
return 1
|
|
}
|
|
fi
|
|
log_info "CodeSandbox SDK/CLI ready"
|
|
}
|
|
|
|
test_codesandbox_token() {
|
|
# Test token by attempting to list sandboxes (lightweight API call)
|
|
local test_output
|
|
test_output=$(CSB_API_KEY="${CSB_API_KEY}" npx -y @codesandbox/sdk sandboxes list 2>&1)
|
|
local exit_code=$?
|
|
|
|
if [[ ${exit_code} -ne 0 ]]; then
|
|
if echo "${test_output}" | grep -qi "unauthorized\|invalid.*key\|authentication\|401"; then
|
|
log_error "Invalid API key"
|
|
log_error "How to fix:"
|
|
log_warn " 1. Get a new API key at: https://codesandbox.io/t/api"
|
|
log_warn " 2. Enable all scopes when creating the key"
|
|
log_warn " 3. Export it as: export CSB_API_KEY=your-key-here"
|
|
return 1
|
|
fi
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
ensure_codesandbox_token() {
|
|
ensure_api_token_with_provider \
|
|
"CodeSandbox" \
|
|
"CSB_API_KEY" \
|
|
"${HOME}/.config/spawn/codesandbox.json" \
|
|
"https://codesandbox.io/t/api" \
|
|
"test_codesandbox_token"
|
|
}
|
|
|
|
get_server_name() {
|
|
get_resource_name "CODESANDBOX_SANDBOX_NAME" "Enter sandbox name: "
|
|
}
|
|
|
|
# Run a JS snippet that uses the CodeSandbox SDK.
|
|
# The snippet receives `sdk` (authenticated SDK instance) in scope.
|
|
# All extra env vars are forwarded so callers can pass data safely.
|
|
# Usage: _csb_sdk_eval 'await sdk.sandboxes.list()'
|
|
_csb_sdk_eval() {
|
|
local js_body="${1}"
|
|
node -e "
|
|
const { CodeSandbox } = require('@codesandbox/sdk');
|
|
const sdk = new CodeSandbox(process.env.CSB_API_KEY);
|
|
(async () => {
|
|
try { ${js_body} }
|
|
catch (e) { console.error('ERROR:', e.message); process.exit(1); }
|
|
})();
|
|
"
|
|
}
|
|
|
|
# Connect to an existing sandbox, run a command, stream output.
|
|
# Receives sandbox ID via _CSB_SB_ID env var, command via _CSB_CMD.
|
|
_csb_run_cmd() {
|
|
_csb_sdk_eval "
|
|
const sb = await sdk.sandboxes.get(process.env._CSB_SB_ID);
|
|
const c = await sb.connect();
|
|
const r = await c.commands.run(process.env._CSB_CMD);
|
|
if (r.output) process.stdout.write(r.output);
|
|
if (r.stderr) process.stderr.write(r.stderr);
|
|
process.exit(r.exitCode || 0);
|
|
"
|
|
}
|
|
|
|
# Invoke Node.js script to create sandbox via SDK
|
|
# SECURITY: Pass name and template via environment variables to prevent injection
|
|
_invoke_codesandbox_create() {
|
|
local name="${1}"
|
|
local template="${2:-base}"
|
|
|
|
CSB_API_KEY="${CSB_API_KEY}" _CSB_NAME="${name}" _CSB_TEMPLATE="${template}" \
|
|
_csb_sdk_eval "
|
|
const sb = await sdk.sandboxes.create({
|
|
name: process.env._CSB_NAME,
|
|
template: process.env._CSB_TEMPLATE || 'base'
|
|
});
|
|
console.log(sb.id);
|
|
" 2>&1
|
|
}
|
|
|
|
create_server() {
|
|
local name="${1}"
|
|
local template="${CODESANDBOX_TEMPLATE:-base}"
|
|
|
|
log_step "Creating CodeSandbox sandbox '${name}'..."
|
|
|
|
local output
|
|
output=$(_invoke_codesandbox_create "${name}" "${template}")
|
|
local exit_code=$?
|
|
|
|
if [[ ${exit_code} -ne 0 ]] || [[ -z "${output}" ]] || [[ "${output}" =~ ERROR ]]; then
|
|
log_error "Failed to create sandbox"
|
|
log_error ""
|
|
if [[ -n "${output}" ]]; then
|
|
log_error "Error details: ${output}"
|
|
fi
|
|
log_error ""
|
|
log_error "Possible causes:"
|
|
log_error " - Invalid API key or expired authentication"
|
|
log_error " - Insufficient quota or credits (check: https://codesandbox.io/settings)"
|
|
log_error " - Network connectivity issues"
|
|
log_error " - Invalid sandbox name or template"
|
|
return 1
|
|
fi
|
|
|
|
CODESANDBOX_SANDBOX_ID="${output}"
|
|
export CODESANDBOX_SANDBOX_ID
|
|
log_info "Sandbox created: ID=${CODESANDBOX_SANDBOX_ID}"
|
|
}
|
|
|
|
wait_for_cloud_init() {
|
|
log_step "Installing tools in sandbox..."
|
|
# CodeSandbox comes with Node.js pre-installed
|
|
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
|
|
log_info "Tools installed"
|
|
}
|
|
|
|
# Validate CodeSandbox sandbox ID format
|
|
validate_sandbox_id() {
|
|
local sid="${1}"
|
|
if [[ ! "${sid}" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
log_error "Invalid CODESANDBOX_SANDBOX_ID format: expected alphanumeric with dashes/underscores"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Execute command via CodeSandbox SDK
|
|
run_server() {
|
|
local cmd="${1}"
|
|
validate_sandbox_id "${CODESANDBOX_SANDBOX_ID}" || return 1
|
|
|
|
# SECURITY: Pass sandbox ID and command via environment variables to prevent injection
|
|
CSB_API_KEY="${CSB_API_KEY}" _CSB_SB_ID="${CODESANDBOX_SANDBOX_ID}" _CSB_CMD="${cmd}" _csb_run_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
|
|
|
|
local content
|
|
content=$(base64 -w0 "${local_path}" 2>/dev/null || base64 "${local_path}")
|
|
|
|
# SECURITY: Use SDK filesystem API via env vars — no shell interpolation
|
|
validate_sandbox_id "${CODESANDBOX_SANDBOX_ID}" || return 1
|
|
CSB_API_KEY="${CSB_API_KEY}" _CSB_SB_ID="${CODESANDBOX_SANDBOX_ID}" \
|
|
_CSB_REMOTE_PATH="${remote_path}" _CSB_CONTENT="${content}" \
|
|
_csb_sdk_eval "
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const sb = await sdk.sandboxes.get(process.env._CSB_SB_ID);
|
|
const c = await sb.connect();
|
|
const remotePath = process.env._CSB_REMOTE_PATH;
|
|
const dir = path.dirname(remotePath);
|
|
if (dir !== '.' && dir !== '/') {
|
|
await c.commands.run('mkdir -p ' + dir);
|
|
}
|
|
const content = Buffer.from(process.env._CSB_CONTENT, 'base64');
|
|
await c.fs.writeFile(remotePath, content);
|
|
"
|
|
}
|
|
|
|
interactive_session() {
|
|
log_info "Starting interactive session..."
|
|
log_step "For a full terminal, open your sandbox at: https://codesandbox.io/dashboard"
|
|
local session_exit=0
|
|
run_server "$1" || session_exit=$?
|
|
SERVER_NAME="${CODESANDBOX_SANDBOX_ID:-}" _show_exec_post_session_summary
|
|
return "${session_exit}"
|
|
}
|
|
|
|
destroy_server() {
|
|
local sandbox_id="${1:-${CODESANDBOX_SANDBOX_ID}}"
|
|
validate_sandbox_id "${sandbox_id}" || return 1
|
|
log_step "Shutting down sandbox..."
|
|
|
|
# SECURITY: Pass sandbox ID via environment variable
|
|
CSB_API_KEY="${CSB_API_KEY}" _CSB_SB_ID="${sandbox_id}" \
|
|
_csb_sdk_eval "
|
|
await sdk.sandboxes.shutdown(process.env._CSB_SB_ID);
|
|
console.log('Sandbox shut down');
|
|
" 2>/dev/null || true
|
|
log_info "Sandbox shut down"
|
|
}
|
|
|
|
list_servers() {
|
|
CSB_API_KEY="${CSB_API_KEY}" \
|
|
_csb_sdk_eval "
|
|
const sbs = await sdk.sandboxes.list();
|
|
sbs.forEach(sb => console.log(sb.id + ' ' + (sb.name || 'unnamed')));
|
|
" 2>/dev/null || echo "No sandboxes found"
|
|
}
|