feat: add Claude Code image support (#16)

* feat: add Claude Code image support

Add a new Cubbi image for Claude Code (Anthropic's official CLI) with:
- Full Claude Code CLI functionality via NPM package
- Secure API key management with multiple authentication options
- Enterprise support (Bedrock, Vertex AI, proxy configuration)
- Persistent configuration and cache directories
- Comprehensive test suite and documentation

The image allows users to run Claude Code in containers with proper
isolation, persistent settings, and seamless Cubbi integration. It
gracefully handles missing API keys to allow flexible authentication.

Also adds optional Claude Code API keys to container.py for enterprise
deployments.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Pre-commit fixes

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Your Name <you@example.com>
This commit is contained in:
Xavier Bouthillier
2025-06-26 18:24:55 -04:00
committed by GitHub
parent e70ec3538b
commit b28c2bd63e
6 changed files with 808 additions and 0 deletions

View File

@@ -203,6 +203,8 @@ class ContainerManager:
api_keys = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_CUSTOM_HEADERS",
"OPENROUTER_API_KEY",
"GOOGLE_API_KEY",
"LANGFUSE_INIT_PROJECT_PUBLIC_KEY",

View File

@@ -0,0 +1,72 @@
FROM python:3.12-slim
LABEL maintainer="team@monadical.com"
LABEL description="Claude Code for Cubbi"
# Install system dependencies including gosu for user switching
RUN apt-get update && apt-get install -y --no-install-recommends \
gosu \
passwd \
bash \
curl \
bzip2 \
iputils-ping \
iproute2 \
libxcb1 \
libdbus-1-3 \
nano \
tmux \
git-core \
ripgrep \
openssh-client \
vim \
&& rm -rf /var/lib/apt/lists/*
# Install uv (Python package manager)
WORKDIR /tmp
RUN curl -fsSL https://astral.sh/uv/install.sh -o install.sh && \
sh install.sh && \
mv /root/.local/bin/uv /usr/local/bin/uv && \
mv /root/.local/bin/uvx /usr/local/bin/uvx && \
rm install.sh
# Install Node.js (for Claude Code NPM package)
RUN mkdir -p /opt/node && \
curl -fsSL https://nodejs.org/dist/v22.16.0/node-v22.16.0-linux-x64.tar.gz -o node.tar.gz && \
tar -xf node.tar.gz -C /opt/node --strip-components=1 && \
rm node.tar.gz
ENV PATH="/opt/node/bin:$PATH"
# Install Claude Code globally
RUN npm install -g @anthropic-ai/claude-code
# Create app directory
WORKDIR /app
# Copy initialization system
COPY cubbi_init.py /cubbi/cubbi_init.py
COPY claudecode_plugin.py /cubbi/claudecode_plugin.py
COPY cubbi_image.yaml /cubbi/cubbi_image.yaml
COPY init-status.sh /cubbi/init-status.sh
# Make scripts executable
RUN chmod +x /cubbi/cubbi_init.py /cubbi/init-status.sh
# Add Node.js to PATH in bashrc and init status check
RUN echo 'PATH="/opt/node/bin:$PATH"' >> /etc/bash.bashrc
RUN echo '[ -x /cubbi/init-status.sh ] && /cubbi/init-status.sh' >> /etc/bash.bashrc
# Set up environment
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV UV_LINK_MODE=copy
# Pre-install the cubbi_init
RUN /cubbi/cubbi_init.py --help
# Set WORKDIR to /app
WORKDIR /app
ENTRYPOINT ["/cubbi/cubbi_init.py"]
CMD ["tail", "-f", "/dev/null"]

View File

@@ -0,0 +1,222 @@
# Claude Code for Cubbi
This image provides Claude Code (Anthropic's official CLI for Claude) in a Cubbi container environment.
## Overview
Claude Code is an interactive CLI tool that helps with software engineering tasks. This Cubbi image integrates Claude Code with secure API key management, persistent configuration, and enterprise features.
## Features
- **Claude Code CLI**: Full access to Claude's coding capabilities
- **Secure Authentication**: API key management through Cubbi's secure environment system
- **Persistent Configuration**: Settings and cache preserved across container restarts
- **Enterprise Support**: Bedrock and Vertex AI integration
- **Network Support**: Proxy configuration for corporate environments
- **Tool Permissions**: Pre-configured permissions for all Claude Code tools
## Quick Start
### 1. Set up API Key
```bash
# Set your Anthropic API key in Cubbi configuration
cubbi config set services.anthropic.api_key "your-api-key-here"
```
### 2. Run Claude Code Environment
```bash
# Start Claude Code container
cubbi run claudecode
# Execute Claude Code commands
cubbi exec claudecode "claude 'help me write a Python function'"
# Start interactive session
cubbi exec claudecode "claude"
```
## Configuration
### Required Environment Variables
- `ANTHROPIC_API_KEY`: Your Anthropic API key (required)
### Optional Environment Variables
- `ANTHROPIC_AUTH_TOKEN`: Custom authorization token for enterprise deployments
- `ANTHROPIC_CUSTOM_HEADERS`: Additional HTTP headers (JSON format)
- `CLAUDE_CODE_USE_BEDROCK`: Set to "true" to use Amazon Bedrock
- `CLAUDE_CODE_USE_VERTEX`: Set to "true" to use Google Vertex AI
- `HTTP_PROXY`: HTTP proxy server URL
- `HTTPS_PROXY`: HTTPS proxy server URL
- `DISABLE_TELEMETRY`: Set to "true" to disable telemetry
### Advanced Configuration
```bash
# Enterprise deployment with Bedrock
cubbi config set environment.claude_code_use_bedrock true
cubbi run claudecode
# With custom proxy
cubbi config set network.https_proxy "https://proxy.company.com:8080"
cubbi run claudecode
# Disable telemetry
cubbi config set environment.disable_telemetry true
cubbi run claudecode
```
## Usage Examples
### Basic Usage
```bash
# Get help
cubbi exec claudecode "claude --help"
# One-time task
cubbi exec claudecode "claude 'write a unit test for this function'"
# Interactive mode
cubbi exec claudecode "claude"
```
### Working with Projects
```bash
# Start Claude Code in your project directory
cubbi run claudecode --mount /path/to/your/project:/app
cubbi exec claudecode "cd /app && claude"
# Create a commit
cubbi exec claudecode "cd /app && claude commit"
```
### Advanced Features
```bash
# Run with specific model configuration
cubbi exec claudecode "claude -m claude-3-5-sonnet-20241022 'analyze this code'"
# Use with plan mode
cubbi exec claudecode "claude -p 'refactor this function'"
```
## Persistent Configuration
The following directories are automatically persisted:
- `~/.claude/`: Claude Code settings and configuration
- `~/.cache/claude/`: Claude Code cache and temporary files
Configuration files are maintained across container restarts, ensuring your settings and preferences are preserved.
## File Structure
```
cubbi/images/claudecode/
├── Dockerfile # Container image definition
├── cubbi_image.yaml # Cubbi image configuration
├── claudecode_plugin.py # Authentication and setup plugin
├── cubbi_init.py # Initialization script (shared)
├── init-status.sh # Status check script (shared)
└── README.md # This documentation
```
## Authentication Flow
1. **Environment Variables**: API key passed from Cubbi configuration
2. **Plugin Setup**: `claudecode_plugin.py` creates `~/.claude/settings.json`
3. **Verification**: Plugin verifies Claude Code installation and configuration
4. **Ready**: Claude Code is ready for use with configured authentication
## Troubleshooting
### Common Issues
**API Key Not Set**
```
⚠️ No authentication configuration found
Please set ANTHROPIC_API_KEY environment variable
```
**Solution**: Set API key in Cubbi configuration:
```bash
cubbi config set services.anthropic.api_key "your-api-key-here"
```
**Claude Code Not Found**
```
❌ Claude Code not properly installed
```
**Solution**: Rebuild the container image:
```bash
docker build -t cubbi-claudecode:latest cubbi/images/claudecode/
```
**Network Issues**
```
Connection timeout or proxy errors
```
**Solution**: Configure proxy settings:
```bash
cubbi config set network.https_proxy "your-proxy-url"
```
### Debug Mode
Enable verbose output for debugging:
```bash
# Check configuration
cubbi exec claudecode "cat ~/.claude/settings.json"
# Verify installation
cubbi exec claudecode "claude --version"
cubbi exec claudecode "which claude"
cubbi exec claudecode "node --version"
```
## Security Considerations
- **API Keys**: Stored securely with 0o600 permissions
- **Configuration**: Settings files have restricted access
- **Environment**: Isolated container environment
- **Telemetry**: Can be disabled for privacy
## Development
### Building the Image
```bash
# Build locally
docker build -t cubbi-claudecode:test cubbi/images/claudecode/
# Test basic functionality
docker run --rm -it \
-e ANTHROPIC_API_KEY="your-api-key" \
cubbi-claudecode:test \
bash -c "claude --version"
```
### Testing
```bash
# Run through Cubbi
cubbi run claudecode --name test-claude
cubbi exec test-claude "claude --version"
cubbi stop test-claude
```
## Support
For issues related to:
- **Cubbi Integration**: Check Cubbi documentation or open an issue
- **Claude Code**: Visit [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code)
- **API Keys**: Visit [Anthropic Console](https://console.anthropic.com/)
## License
This image configuration is provided under the same license as the Cubbi project. Claude Code is licensed separately by Anthropic.

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env python3
"""
Claude Code Plugin for Cubbi
Handles authentication setup and configuration for Claude Code
"""
import json
import os
import stat
from pathlib import Path
from typing import Any, Dict, Optional
from cubbi_init import ToolPlugin
# API key mappings from environment variables to Claude Code configuration
API_KEY_MAPPINGS = {
"ANTHROPIC_API_KEY": "api_key",
"ANTHROPIC_AUTH_TOKEN": "auth_token",
"ANTHROPIC_CUSTOM_HEADERS": "custom_headers",
}
# Enterprise integration environment variables
ENTERPRISE_MAPPINGS = {
"CLAUDE_CODE_USE_BEDROCK": "use_bedrock",
"CLAUDE_CODE_USE_VERTEX": "use_vertex",
"HTTP_PROXY": "http_proxy",
"HTTPS_PROXY": "https_proxy",
"DISABLE_TELEMETRY": "disable_telemetry",
}
class ClaudeCodePlugin(ToolPlugin):
"""Plugin for setting up Claude Code authentication and configuration"""
@property
def tool_name(self) -> str:
return "claudecode"
def _get_user_ids(self) -> tuple[int, int]:
"""Get the cubbi user and group IDs from environment"""
user_id = int(os.environ.get("CUBBI_USER_ID", "1000"))
group_id = int(os.environ.get("CUBBI_GROUP_ID", "1000"))
return user_id, group_id
def _set_ownership(self, path: Path) -> None:
"""Set ownership of a path to the cubbi user"""
user_id, group_id = self._get_user_ids()
try:
os.chown(path, user_id, group_id)
except OSError as e:
self.status.log(f"Failed to set ownership for {path}: {e}", "WARNING")
def _get_claude_dir(self) -> Path:
"""Get the Claude Code configuration directory"""
return Path("/home/cubbi/.claude")
def _ensure_claude_dir(self) -> Path:
"""Ensure Claude directory exists with correct ownership"""
claude_dir = self._get_claude_dir()
try:
claude_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
self._set_ownership(claude_dir)
except OSError as e:
self.status.log(
f"Failed to create Claude directory {claude_dir}: {e}", "ERROR"
)
return claude_dir
def initialize(self) -> bool:
"""Initialize Claude Code configuration"""
self.status.log("Setting up Claude Code authentication...")
# Ensure Claude directory exists
claude_dir = self._ensure_claude_dir()
# Create settings configuration
settings = self._create_settings()
if settings:
settings_file = claude_dir / "settings.json"
success = self._write_settings(settings_file, settings)
if success:
self.status.log("✅ Claude Code authentication configured successfully")
return True
else:
return False
else:
self.status.log("⚠️ No authentication configuration found", "WARNING")
self.status.log(
" Please set ANTHROPIC_API_KEY environment variable", "WARNING"
)
self.status.log(" Claude Code will run without authentication", "INFO")
# Return True to allow container to start without API key
# Users can still use Claude Code with their own authentication methods
return True
def _create_settings(self) -> Optional[Dict]:
"""Create Claude Code settings configuration"""
settings = {}
# Core authentication
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
return None
# Basic authentication setup
settings["apiKey"] = api_key
# Custom authorization token (optional)
auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN")
if auth_token:
settings["authToken"] = auth_token
# Custom headers (optional)
custom_headers = os.environ.get("ANTHROPIC_CUSTOM_HEADERS")
if custom_headers:
try:
# Expect JSON string format
settings["customHeaders"] = json.loads(custom_headers)
except json.JSONDecodeError:
self.status.log(
"⚠️ Invalid ANTHROPIC_CUSTOM_HEADERS format, skipping", "WARNING"
)
# Enterprise integration settings
if os.environ.get("CLAUDE_CODE_USE_BEDROCK") == "true":
settings["provider"] = "bedrock"
if os.environ.get("CLAUDE_CODE_USE_VERTEX") == "true":
settings["provider"] = "vertex"
# Network proxy settings
http_proxy = os.environ.get("HTTP_PROXY")
https_proxy = os.environ.get("HTTPS_PROXY")
if http_proxy or https_proxy:
settings["proxy"] = {}
if http_proxy:
settings["proxy"]["http"] = http_proxy
if https_proxy:
settings["proxy"]["https"] = https_proxy
# Telemetry settings
if os.environ.get("DISABLE_TELEMETRY") == "true":
settings["telemetry"] = {"enabled": False}
# Tool permissions (allow all by default in Cubbi environment)
settings["permissions"] = {
"tools": {
"read": {"allowed": True},
"write": {"allowed": True},
"edit": {"allowed": True},
"bash": {"allowed": True},
"webfetch": {"allowed": True},
"websearch": {"allowed": True},
}
}
return settings
def _write_settings(self, settings_file: Path, settings: Dict) -> bool:
"""Write settings to Claude Code configuration file"""
try:
# Write settings with secure permissions
with open(settings_file, "w") as f:
json.dump(settings, f, indent=2)
# Set ownership and secure file permissions (read/write for owner only)
self._set_ownership(settings_file)
os.chmod(settings_file, stat.S_IRUSR | stat.S_IWUSR)
self.status.log(f"Created Claude Code settings at {settings_file}")
return True
except Exception as e:
self.status.log(f"Failed to write Claude Code settings: {e}", "ERROR")
return False
def setup_tool_configuration(self) -> bool:
"""Set up Claude Code configuration - called by base class"""
# Additional tool configuration can be added here if needed
return True
def integrate_mcp_servers(self, mcp_config: Dict[str, Any]) -> bool:
"""Integrate Claude Code with available MCP servers"""
if mcp_config["count"] == 0:
self.status.log("No MCP servers to integrate")
return True
# Claude Code has built-in MCP support, so we can potentially
# configure MCP servers in the settings if needed
self.status.log("MCP server integration available for Claude Code")
return True

View File

@@ -0,0 +1,68 @@
name: claudecode
description: Claude Code AI environment
version: 1.0.0
maintainer: team@monadical.com
image: monadical/cubbi-claudecode:latest
init:
pre_command: /cubbi-init.sh
command: /entrypoint.sh
environment:
# Core Anthropic Authentication
- name: ANTHROPIC_API_KEY
description: Anthropic API key for Claude
required: true
sensitive: true
# Optional Enterprise Integration
- name: ANTHROPIC_AUTH_TOKEN
description: Custom authorization token for Claude
required: false
sensitive: true
- name: ANTHROPIC_CUSTOM_HEADERS
description: Additional HTTP headers for Claude API requests
required: false
sensitive: true
# Enterprise Deployment Options
- name: CLAUDE_CODE_USE_BEDROCK
description: Use Amazon Bedrock instead of direct API
required: false
- name: CLAUDE_CODE_USE_VERTEX
description: Use Google Vertex AI instead of direct API
required: false
# Network Configuration
- name: HTTP_PROXY
description: HTTP proxy server URL
required: false
- name: HTTPS_PROXY
description: HTTPS proxy server URL
required: false
# Optional Telemetry Control
- name: DISABLE_TELEMETRY
description: Disable Claude Code telemetry
required: false
default: "false"
ports: []
volumes:
- mountPath: /app
description: Application directory
persistent_configs:
- source: "/home/cubbi/.claude"
target: "/cubbi-config/claude-settings"
type: "directory"
description: "Claude Code settings and configuration"
- source: "/home/cubbi/.cache/claude"
target: "/cubbi-config/claude-cache"
type: "directory"
description: "Claude Code cache directory"

View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""
Automated test suite for Claude Code Cubbi integration
"""
import subprocess
def run_test(description: str, command: list, timeout: int = 30) -> bool:
"""Run a test command and return success status"""
print(f"🧪 Testing: {description}")
try:
result = subprocess.run(
command, capture_output=True, text=True, timeout=timeout
)
if result.returncode == 0:
print(" ✅ PASS")
return True
else:
print(f" ❌ FAIL: {result.stderr}")
if result.stdout:
print(f" 📋 stdout: {result.stdout}")
return False
except subprocess.TimeoutExpired:
print(f" ⏰ TIMEOUT: Command exceeded {timeout}s")
return False
except Exception as e:
print(f" ❌ ERROR: {e}")
return False
def test_suite():
"""Run complete test suite"""
tests_passed = 0
total_tests = 0
print("🚀 Starting Claude Code Cubbi Integration Test Suite")
print("=" * 60)
# Test 1: Build image
total_tests += 1
if run_test(
"Build Claude Code image",
["docker", "build", "-t", "cubbi-claudecode:test", "cubbi/images/claudecode/"],
timeout=180,
):
tests_passed += 1
# Test 2: Tag image for Cubbi
total_tests += 1
if run_test(
"Tag image for Cubbi",
["docker", "tag", "cubbi-claudecode:test", "monadical/cubbi-claudecode:latest"],
):
tests_passed += 1
# Test 3: Basic container startup
total_tests += 1
if run_test(
"Container startup with test API key",
[
"docker",
"run",
"--rm",
"-e",
"ANTHROPIC_API_KEY=test-key",
"cubbi-claudecode:test",
"bash",
"-c",
"claude --version",
],
):
tests_passed += 1
# Test 4: Cubbi image list
total_tests += 1
if run_test(
"Cubbi image list includes claudecode",
["uv", "run", "-m", "cubbi.cli", "image", "list"],
):
tests_passed += 1
# Test 5: Cubbi session creation
total_tests += 1
session_result = subprocess.run(
[
"uv",
"run",
"-m",
"cubbi.cli",
"session",
"create",
"--image",
"claudecode",
"--name",
"test-automation",
"--no-connect",
"--env",
"ANTHROPIC_API_KEY=test-key",
"--run",
"claude --version",
],
capture_output=True,
text=True,
timeout=60,
)
if session_result.returncode == 0:
print("🧪 Testing: Cubbi session creation")
print(" ✅ PASS")
tests_passed += 1
# Extract session ID for cleanup
session_id = None
for line in session_result.stdout.split("\n"):
if "Session ID:" in line:
session_id = line.split("Session ID: ")[1].strip()
break
if session_id:
# Test 6: Session cleanup
total_tests += 1
if run_test(
"Clean up test session",
["uv", "run", "-m", "cubbi.cli", "session", "close", session_id],
):
tests_passed += 1
else:
print("🧪 Testing: Clean up test session")
print(" ⚠️ SKIP: Could not extract session ID")
total_tests += 1
else:
print("🧪 Testing: Cubbi session creation")
print(f" ❌ FAIL: {session_result.stderr}")
total_tests += 2 # This test and cleanup test both fail
# Test 7: Session without API key
total_tests += 1
no_key_result = subprocess.run(
[
"uv",
"run",
"-m",
"cubbi.cli",
"session",
"create",
"--image",
"claudecode",
"--name",
"test-no-key",
"--no-connect",
"--run",
"claude --version",
],
capture_output=True,
text=True,
timeout=60,
)
if no_key_result.returncode == 0:
print("🧪 Testing: Session without API key")
print(" ✅ PASS")
tests_passed += 1
# Extract session ID and close
session_id = None
for line in no_key_result.stdout.split("\n"):
if "Session ID:" in line:
session_id = line.split("Session ID: ")[1].strip()
break
if session_id:
subprocess.run(
["uv", "run", "-m", "cubbi.cli", "session", "close", session_id],
capture_output=True,
timeout=30,
)
else:
print("🧪 Testing: Session without API key")
print(f" ❌ FAIL: {no_key_result.stderr}")
# Test 8: Persistent configuration test
total_tests += 1
persist_result = subprocess.run(
[
"uv",
"run",
"-m",
"cubbi.cli",
"session",
"create",
"--image",
"claudecode",
"--name",
"test-persist-auto",
"--project",
"test-automation",
"--no-connect",
"--env",
"ANTHROPIC_API_KEY=test-key",
"--run",
"echo 'automation test' > ~/.claude/automation.txt && cat ~/.claude/automation.txt",
],
capture_output=True,
text=True,
timeout=60,
)
if persist_result.returncode == 0:
print("🧪 Testing: Persistent configuration")
print(" ✅ PASS")
tests_passed += 1
# Extract session ID and close
session_id = None
for line in persist_result.stdout.split("\n"):
if "Session ID:" in line:
session_id = line.split("Session ID: ")[1].strip()
break
if session_id:
subprocess.run(
["uv", "run", "-m", "cubbi.cli", "session", "close", session_id],
capture_output=True,
timeout=30,
)
else:
print("🧪 Testing: Persistent configuration")
print(f" ❌ FAIL: {persist_result.stderr}")
print("=" * 60)
print(f"📊 Test Results: {tests_passed}/{total_tests} tests passed")
if tests_passed == total_tests:
print("🎉 All tests passed! Claude Code integration is working correctly.")
return True
else:
print(
f"{total_tests - tests_passed} test(s) failed. Please check the output above."
)
return False
def main():
"""Main test entry point"""
success = test_suite()
exit(0 if success else 1)
if __name__ == "__main__":
main()