yao/sandbox/docker/claude/Dockerfile
Max ea8be6c287 Add secure proxy configuration with options and secrets support
- 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>
2026-01-31 22:46:48 +08:00

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"]