mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-05 07:20:28 +00:00
Add CodeSandbox as a new sandbox cloud provider for running AI agents. CodeSandbox features: - Firecracker microVMs with ~2 second start times - SDK/CLI-based exec (no SSH) - Free tier: 40 hours/month on Build plan - Secure isolated environments Implementation: - Created codesandbox/lib/common.sh with SDK wrapper functions - Implemented 3 agent scripts: claude, aider, openclaw - Added CodeSandbox to manifest.json clouds - Created matrix entries (3 implemented, 12 missing) - Updated test/record.sh to list as non-recordable CLI cloud - Added codesandbox/README.md with usage instructions The implementation follows the existing pattern from e2b and modal, using Node.js SDK (@codesandbox/sdk) for sandbox lifecycle management. Agent: cloud-scout Co-authored-by: B (Discovery Team) <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
287 lines
9.7 KiB
Bash
287 lines
9.7 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
|
|
# ============================================================
|
|
|
|
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_warn "Remediation steps:"
|
|
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: "
|
|
}
|
|
|
|
# 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}" node -e "
|
|
const { CodeSandbox } = require('@codesandbox/sdk');
|
|
const sdk = new CodeSandbox(process.env.CSB_API_KEY);
|
|
|
|
(async () => {
|
|
try {
|
|
const sandbox = await sdk.sandboxes.create({
|
|
name: process.env._CSB_NAME,
|
|
template: process.env._CSB_TEMPLATE || 'base'
|
|
});
|
|
console.log(sandbox.id);
|
|
} catch (error) {
|
|
console.error('ERROR:', error.message);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
" 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}" node -e "
|
|
const { CodeSandbox } = require('@codesandbox/sdk');
|
|
const sdk = new CodeSandbox(process.env.CSB_API_KEY);
|
|
|
|
(async () => {
|
|
try {
|
|
const sandbox = await sdk.sandboxes.get(process.env._CSB_SB_ID);
|
|
const client = await sandbox.connect();
|
|
const result = await client.commands.run(process.env._CSB_CMD);
|
|
if (result.output) {
|
|
process.stdout.write(result.output);
|
|
}
|
|
if (result.stderr) {
|
|
process.stderr.write(result.stderr);
|
|
}
|
|
process.exit(result.exitCode || 0);
|
|
} catch (error) {
|
|
console.error('ERROR:', error.message);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
"
|
|
}
|
|
|
|
upload_file() {
|
|
local local_path="${1}"
|
|
local remote_path="${2}"
|
|
|
|
# Validate remote_path to prevent command injection
|
|
if [[ "$remote_path" == *"'"* || "$remote_path" == *'$'* || "$remote_path" == *'`'* || "$remote_path" == *$'\n'* ]]; then
|
|
log_error "Invalid remote path (contains unsafe characters): $remote_path"
|
|
return 1
|
|
fi
|
|
|
|
local content
|
|
content=$(base64 -w0 "${local_path}" 2>/dev/null || base64 "${local_path}")
|
|
|
|
# SECURITY: Properly escape remote_path to prevent injection
|
|
local escaped_path
|
|
escaped_path=$(printf '%q' "${remote_path}")
|
|
# base64 output is safe (alphanumeric + /+=) so no injection risk
|
|
run_server "printf '%s' '${content}' | base64 -d > ${escaped_path}"
|
|
}
|
|
|
|
interactive_session() {
|
|
local cmd="${1}"
|
|
validate_sandbox_id "${CODESANDBOX_SANDBOX_ID}" || return 1
|
|
|
|
# Use CodeSandbox shell for interactive sessions
|
|
log_info "Starting interactive session..."
|
|
log_warn "Note: Use 'csb' CLI dashboard for full terminal experience"
|
|
|
|
# SECURITY: Pass sandbox ID and command via environment variables
|
|
CSB_API_KEY="${CSB_API_KEY}" _CSB_SB_ID="${CODESANDBOX_SANDBOX_ID}" _CSB_CMD="${cmd}" node -e "
|
|
const { CodeSandbox } = require('@codesandbox/sdk');
|
|
const sdk = new CodeSandbox(process.env.CSB_API_KEY);
|
|
|
|
(async () => {
|
|
try {
|
|
const sandbox = await sdk.sandboxes.get(process.env._CSB_SB_ID);
|
|
const client = await sandbox.connect();
|
|
const result = await client.commands.run(process.env._CSB_CMD);
|
|
if (result.output) {
|
|
process.stdout.write(result.output);
|
|
}
|
|
if (result.stderr) {
|
|
process.stderr.write(result.stderr);
|
|
}
|
|
process.exit(result.exitCode || 0);
|
|
} catch (error) {
|
|
console.error('ERROR:', error.message);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
"
|
|
}
|
|
|
|
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}" node -e "
|
|
const { CodeSandbox } = require('@codesandbox/sdk');
|
|
const sdk = new CodeSandbox(process.env.CSB_API_KEY);
|
|
|
|
(async () => {
|
|
try {
|
|
await sdk.sandboxes.shutdown(process.env._CSB_SB_ID);
|
|
console.log('Sandbox shut down');
|
|
} catch (error) {
|
|
console.error('ERROR:', error.message);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
" 2>/dev/null || true
|
|
log_info "Sandbox shut down"
|
|
}
|
|
|
|
list_servers() {
|
|
CSB_API_KEY="${CSB_API_KEY}" node -e "
|
|
const { CodeSandbox } = require('@codesandbox/sdk');
|
|
const sdk = new CodeSandbox(process.env.CSB_API_KEY);
|
|
|
|
(async () => {
|
|
try {
|
|
const sandboxes = await sdk.sandboxes.list();
|
|
sandboxes.forEach(sb => {
|
|
console.log(\`\${sb.id} \${sb.name || 'unnamed'}\`);
|
|
});
|
|
} catch (error) {
|
|
console.error('No sandboxes found');
|
|
}
|
|
})();
|
|
" 2>/dev/null || echo "No sandboxes found"
|
|
}
|