diff --git a/cubbi/container.py b/cubbi/container.py index 3c35a3e..dbf75cc 100644 --- a/cubbi/container.py +++ b/cubbi/container.py @@ -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", diff --git a/cubbi/images/claudecode/Dockerfile b/cubbi/images/claudecode/Dockerfile new file mode 100644 index 0000000..3f15e3d --- /dev/null +++ b/cubbi/images/claudecode/Dockerfile @@ -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"] \ No newline at end of file diff --git a/cubbi/images/claudecode/README.md b/cubbi/images/claudecode/README.md new file mode 100644 index 0000000..91ce57a --- /dev/null +++ b/cubbi/images/claudecode/README.md @@ -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. \ No newline at end of file diff --git a/cubbi/images/claudecode/claudecode_plugin.py b/cubbi/images/claudecode/claudecode_plugin.py new file mode 100755 index 0000000..881bbcc --- /dev/null +++ b/cubbi/images/claudecode/claudecode_plugin.py @@ -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 diff --git a/cubbi/images/claudecode/cubbi_image.yaml b/cubbi/images/claudecode/cubbi_image.yaml new file mode 100644 index 0000000..2e858ea --- /dev/null +++ b/cubbi/images/claudecode/cubbi_image.yaml @@ -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" \ No newline at end of file diff --git a/cubbi/images/claudecode/test_claudecode.py b/cubbi/images/claudecode/test_claudecode.py new file mode 100755 index 0000000..38380c9 --- /dev/null +++ b/cubbi/images/claudecode/test_claudecode.py @@ -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()