- Implement secure config file location (/tmp/.yao/proxy.json) instead of user-visible /workspace/
- Add generic options map support for backend-specific parameters (e.g., thinking for Volcengine GLM-4.7)
- Add secrets support for passing sensitive env vars (e.g., GITHUB_TOKEN) to sandbox container
- Remove excessive debug logs, keep critical ones with log.Printf("[Sandbox]...")
- Fix test assertions for system prompt passing via CLI args instead of env var
- Update i18n messages for sandbox loading states
Co-authored-by: Cursor <cursoragent@cursor.com>
308 lines
10 KiB
Docker
308 lines
10 KiB
Docker
# Claude sandbox image: Claude CLI + Node.js + Python + claude-proxy
|
|
# Supports both amd64 and arm64 architectures
|
|
# Base: Ubuntu 24.04 LTS
|
|
ARG REGISTRY=yaoapp
|
|
FROM ${REGISTRY}/sandbox-base:latest
|
|
|
|
USER root
|
|
|
|
# Use MIT mirror (USA) for ARM64 - in case base image cache is old
|
|
RUN sed -i 's|http://ports.ubuntu.com/ubuntu-ports|http://mirrors.mit.edu/ubuntu-ports|g' /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || \
|
|
sed -i 's|http://ports.ubuntu.com/ubuntu-ports|http://mirrors.mit.edu/ubuntu-ports|g' /etc/apt/sources.list 2>/dev/null || true
|
|
|
|
# Python 3.12 (default in Ubuntu 24.04) - install first as Node.js depends on it
|
|
RUN apt-get update && apt-get install -y \
|
|
python3 \
|
|
python3-pip \
|
|
python3-venv \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& ln -sf /usr/bin/python3 /usr/bin/python
|
|
|
|
# Node.js 22 LTS (automatically detects architecture)
|
|
# Run apt-get update again to refresh package lists after nodesource setup
|
|
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
|
&& apt-get update \
|
|
&& apt-get install -y nodejs \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# GitHub CLI (gh) for repository operations
|
|
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
|
&& apt-get update \
|
|
&& apt-get install -y gh \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# npm global packages directory for sandbox user
|
|
RUN mkdir -p /home/sandbox/.npm-global && \
|
|
chown -R sandbox:sandbox /home/sandbox/.npm-global
|
|
|
|
USER sandbox
|
|
|
|
# Configure npm to use user directory
|
|
RUN npm config set prefix '/home/sandbox/.npm-global'
|
|
ENV PATH="/home/sandbox/.npm-global/bin:${PATH}"
|
|
|
|
# Install Claude CLI
|
|
RUN npm install -g @anthropic-ai/claude-code || \
|
|
echo "Claude CLI installation skipped (may not be available yet)"
|
|
|
|
# Create Claude CLI configuration for auto-approve all operations
|
|
# This is CRITICAL for non-interactive sandbox usage
|
|
# The --dangerously-skip-permissions flag alone is not enough;
|
|
# we also need the settings.json to fully bypass permission prompts
|
|
RUN mkdir -p /home/sandbox/.claude && \
|
|
cat > /home/sandbox/.claude/settings.json << 'EOF'
|
|
{
|
|
"permissions": {
|
|
"defaultMode": "bypassPermissions",
|
|
"allow": ["*"],
|
|
"deny": []
|
|
}
|
|
}
|
|
EOF
|
|
|
|
USER root
|
|
|
|
# Install claude-proxy (architecture-specific binary)
|
|
ARG TARGETARCH
|
|
COPY claude-proxy-${TARGETARCH} /usr/local/bin/claude-proxy
|
|
RUN chmod +x /usr/local/bin/claude-proxy
|
|
|
|
# Create claude-proxy startup script
|
|
RUN cat > /usr/local/bin/start-proxy << 'SCRIPT'
|
|
#!/bin/bash
|
|
# Claude Proxy startup script
|
|
# Usage: start-proxy [options]
|
|
# Options are passed directly to claude-proxy
|
|
|
|
LOG_DIR="${WORKSPACE:-/workspace}"
|
|
LOG_FILE="${LOG_DIR}/proxy.log"
|
|
|
|
# Ensure log directory exists
|
|
mkdir -p "$LOG_DIR" 2>/dev/null || true
|
|
|
|
# Default environment variables (can be overridden)
|
|
export CLAUDE_PROXY_PORT="${CLAUDE_PROXY_PORT:-3456}"
|
|
|
|
# Start proxy with logging
|
|
exec /usr/local/bin/claude-proxy -v -l "$LOG_FILE" "$@"
|
|
SCRIPT
|
|
RUN chmod +x /usr/local/bin/start-proxy
|
|
|
|
# Create claude-run wrapper for easy usage (manual mode)
|
|
RUN cat > /usr/local/bin/claude-run << 'SCRIPT'
|
|
#!/bin/bash
|
|
# Claude CLI wrapper with proxy auto-start
|
|
# Usage: claude-run [claude options] "prompt"
|
|
#
|
|
# Environment variables:
|
|
# CLAUDE_PROXY_BACKEND - Backend API URL (required)
|
|
# CLAUDE_PROXY_API_KEY - Backend API Key (required)
|
|
# CLAUDE_PROXY_MODEL - Backend model name (required)
|
|
# CLAUDE_PROXY_PORT - Proxy port (default: 3456)
|
|
# WORKSPACE - Working directory (default: /workspace)
|
|
|
|
set -e
|
|
|
|
# Check required environment variables
|
|
if [ -z "$CLAUDE_PROXY_BACKEND" ]; then
|
|
echo "Error: CLAUDE_PROXY_BACKEND is not set"
|
|
echo "Example: export CLAUDE_PROXY_BACKEND=https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$CLAUDE_PROXY_API_KEY" ]; then
|
|
echo "Error: CLAUDE_PROXY_API_KEY is not set"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$CLAUDE_PROXY_MODEL" ]; then
|
|
echo "Error: CLAUDE_PROXY_MODEL is not set"
|
|
echo "Example: export CLAUDE_PROXY_MODEL=glm-4-7-251222"
|
|
exit 1
|
|
fi
|
|
|
|
PORT="${CLAUDE_PROXY_PORT:-3456}"
|
|
WORKSPACE="${WORKSPACE:-/workspace}"
|
|
LOG_FILE="${WORKSPACE}/proxy.log"
|
|
|
|
# Check if proxy is already running
|
|
if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
echo "Proxy already running on port $PORT"
|
|
else
|
|
echo "Starting claude-proxy..."
|
|
mkdir -p "$WORKSPACE" 2>/dev/null || true
|
|
nohup /usr/local/bin/claude-proxy -v -l "$LOG_FILE" > /dev/null 2>&1 &
|
|
|
|
# Wait for proxy to start
|
|
for i in {1..10}; do
|
|
if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
echo "Proxy started successfully"
|
|
break
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
if ! curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
echo "Error: Failed to start proxy"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Set Claude CLI environment
|
|
export ANTHROPIC_BASE_URL="http://127.0.0.1:${PORT}"
|
|
export ANTHROPIC_API_KEY="dummy"
|
|
|
|
# Change to workspace directory
|
|
cd "$WORKSPACE"
|
|
|
|
# Run Claude CLI with all arguments
|
|
exec claude "$@"
|
|
SCRIPT
|
|
RUN chmod +x /usr/local/bin/claude-run
|
|
|
|
# Create start-claude-proxy script for programmatic use (called by Yao)
|
|
# Config is read from /tmp/.yao/proxy.json (NOT /workspace/ for security - api_key/secrets hidden from user)
|
|
RUN cat > /usr/local/bin/start-claude-proxy << 'SCRIPT'
|
|
#!/bin/bash
|
|
# Start claude-proxy from config file or environment variables
|
|
# Config file: /tmp/.yao/proxy.json (secure location, not visible to user file manager)
|
|
# Format: {"backend": "...", "api_key": "...", "model": "...", "options": {...}, "secrets": {...}}
|
|
|
|
CONFIG_FILE="/tmp/.yao/proxy.json"
|
|
LOG_FILE="${WORKSPACE:-/workspace}/proxy.log"
|
|
PORT="${CLAUDE_PROXY_PORT:-3456}"
|
|
|
|
# Try to read from config file first
|
|
if [ -f "$CONFIG_FILE" ]; then
|
|
BACKEND=$(jq -r '.backend // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
API_KEY=$(jq -r '.api_key // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
MODEL=$(jq -r '.model // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
# Read extra options as JSON string (e.g., {"thinking":{"type":"enabled"}})
|
|
OPTIONS=$(jq -c '.options // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
|
|
if [ -n "$BACKEND" ]; then
|
|
export CLAUDE_PROXY_BACKEND="$BACKEND"
|
|
fi
|
|
if [ -n "$API_KEY" ]; then
|
|
export CLAUDE_PROXY_API_KEY="$API_KEY"
|
|
fi
|
|
if [ -n "$MODEL" ]; then
|
|
export CLAUDE_PROXY_MODEL="$MODEL"
|
|
fi
|
|
# Only set options if it's a valid non-empty JSON object
|
|
if [ -n "$OPTIONS" ] && [ "$OPTIONS" != "null" ] && [ "$OPTIONS" != "" ]; then
|
|
export CLAUDE_PROXY_OPTIONS="$OPTIONS"
|
|
fi
|
|
|
|
# Export secrets as environment variables for Claude CLI to use
|
|
# e.g., {"GITHUB_TOKEN": "ghp_xxx"} -> export GITHUB_TOKEN=ghp_xxx
|
|
SECRETS=$(jq -c '.secrets // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
if [ -n "$SECRETS" ] && [ "$SECRETS" != "null" ] && [ "$SECRETS" != "" ] && [ "$SECRETS" != "{}" ]; then
|
|
# Parse each key-value pair and export
|
|
for key in $(echo "$SECRETS" | jq -r 'keys[]' 2>/dev/null); do
|
|
value=$(echo "$SECRETS" | jq -r --arg k "$key" '.[$k]' 2>/dev/null)
|
|
if [ -n "$value" ] && [ "$value" != "null" ]; then
|
|
export "$key"="$value"
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# Check if we have the required config
|
|
if [ -z "$CLAUDE_PROXY_BACKEND" ] || [ -z "$CLAUDE_PROXY_API_KEY" ] || [ -z "$CLAUDE_PROXY_MODEL" ]; then
|
|
echo "Error: Missing proxy configuration"
|
|
echo "Either set environment variables or create $CONFIG_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
# Check if already running
|
|
if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
echo "claude-proxy already running"
|
|
exit 0
|
|
fi
|
|
|
|
# Start proxy with environment variables explicitly passed
|
|
mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
|
|
nohup env \
|
|
CLAUDE_PROXY_BACKEND="$CLAUDE_PROXY_BACKEND" \
|
|
CLAUDE_PROXY_API_KEY="$CLAUDE_PROXY_API_KEY" \
|
|
CLAUDE_PROXY_MODEL="$CLAUDE_PROXY_MODEL" \
|
|
CLAUDE_PROXY_OPTIONS="$CLAUDE_PROXY_OPTIONS" \
|
|
/usr/local/bin/claude-proxy -v -l "$LOG_FILE" > /dev/null 2>&1 &
|
|
|
|
# Wait for startup
|
|
for i in {1..20}; do
|
|
if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
echo "claude-proxy started on port $PORT"
|
|
exit 0
|
|
fi
|
|
sleep 0.5
|
|
done
|
|
|
|
echo "Error: claude-proxy failed to start"
|
|
exit 1
|
|
SCRIPT
|
|
RUN chmod +x /usr/local/bin/start-claude-proxy
|
|
|
|
# Create entrypoint script
|
|
# Note: claude-proxy is started on-demand by Yao (via start-claude-proxy)
|
|
# or manually by user (via claude-run)
|
|
RUN cat > /usr/local/bin/entrypoint.sh << 'SCRIPT'
|
|
#!/bin/bash
|
|
# Container entrypoint
|
|
# claude-proxy is NOT auto-started here - it's started by:
|
|
# 1. Yao's sandbox executor (writes config to .claude-proxy.json, calls start-claude-proxy)
|
|
# 2. Manual usage via claude-run command
|
|
# 3. Direct invocation of start-claude-proxy
|
|
|
|
WORKSPACE="${WORKSPACE:-/workspace}"
|
|
PORT="${CLAUDE_PROXY_PORT:-3456}"
|
|
ENV_FILE="/tmp/claude-proxy-env"
|
|
|
|
# If proxy env vars are set AND proxy is not running, start it
|
|
# This supports docker run -e CLAUDE_PROXY_BACKEND=... usage
|
|
if [ -n "$CLAUDE_PROXY_BACKEND" ] && [ -n "$CLAUDE_PROXY_API_KEY" ] && [ -n "$CLAUDE_PROXY_MODEL" ]; then
|
|
if ! curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
/usr/local/bin/start-claude-proxy
|
|
fi
|
|
|
|
# Write env vars to a file that can be sourced
|
|
if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
|
|
echo "export ANTHROPIC_BASE_URL=http://127.0.0.1:${PORT}" > "$ENV_FILE"
|
|
echo "export ANTHROPIC_API_KEY=dummy" >> "$ENV_FILE"
|
|
chmod 644 "$ENV_FILE"
|
|
fi
|
|
fi
|
|
|
|
# Execute the command passed to docker run
|
|
exec "$@"
|
|
SCRIPT
|
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
|
|
# Create a wrapper that sources the env file
|
|
RUN cat > /usr/local/bin/claude-env << 'SCRIPT'
|
|
#!/bin/bash
|
|
# Source claude-proxy environment if available
|
|
if [ -f /tmp/claude-proxy-env ]; then
|
|
source /tmp/claude-proxy-env
|
|
fi
|
|
exec "$@"
|
|
SCRIPT
|
|
RUN chmod +x /usr/local/bin/claude-env
|
|
|
|
# Add sourcing to global bashrc so docker exec gets the vars
|
|
RUN echo '[ -f /tmp/claude-proxy-env ] && source /tmp/claude-proxy-env' >> /etc/bash.bashrc
|
|
|
|
USER sandbox
|
|
|
|
# Verify installations
|
|
RUN node --version && npm --version && python3 --version && \
|
|
claude --version || true && \
|
|
claude-proxy --help 2>&1 | head -1 || true
|
|
|
|
WORKDIR /workspace
|
|
|
|
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
|
|
CMD ["sleep", "infinity"]
|