diff --git a/docker/Dockerfile b/docker/Dockerfile index 1aeb43a4..efa4ecb6 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,9 +1,9 @@ # Dockerfile for Ubuntu 24.04 with C++ development tools. FROM ubuntu:24.04 +SHELL ["/bin/bash", "-o", "pipefail", "-c"] # Install gcc, clang and some supporting tools for downloading/installing later tools. RUN apt-get update && apt-get install -y --no-install-recommends \ - cmake \ curl \ g++ \ gdb \ @@ -21,6 +21,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ wget \ && rm -rf /var/lib/apt/lists/* +# Install newer CMake from kitware. +RUN wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /etc/apt/trusted.gpg.d/kitware.gpg >/dev/null \ +&& apt-add-repository -y 'deb https://apt.kitware.com/ubuntu/ noble main' \ +&& apt-get update && apt-get install -y --no-install-recommends cmake \ +&& rm -rf /var/lib/apt/lists/* + # Install bazelisk. RUN ARCH=$(dpkg --print-architecture) && \ wget https://github.com/bazelbuild/bazelisk/releases/download/v1.25.0/bazelisk-linux-${ARCH} -O /usr/local/bin/bazelisk \ @@ -32,26 +38,26 @@ RUN useradd -m -s /bin/bash vscode \ && mkdir -p /workspace \ && chown vscode:vscode /workspace -# Install Node.js (required for Gemini CLI) +# Install Node.js (required for Claude Code) RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && rm -rf /var/lib/apt/lists/* -# Install Gemini CLI globally -RUN npm install -g @google/gemini-cli - # Switch to non-root user USER vscode WORKDIR /workspace +# Configure npm for user-local global installs. +ENV NPM_CONFIG_PREFIX=/home/vscode/.npm-global +ENV PATH="/home/vscode/.npm-global/bin:/home/vscode/.local/bin:/usr/local/bin:${PATH}" +RUN mkdir -p /home/vscode/.npm-global + +# Install Claude Code CLI. +RUN npm install -g @anthropic-ai/claude-code + +# Install Antigravity CLI (installs binary as `agy` into ~/.local/bin). +RUN curl -fsSL https://antigravity.google/cli/install.sh | bash + # Set up uv and Python environment RUN curl -LsSf https://astral.sh/uv/install.sh | sh - -# Set up environment variables -ENV PATH="/home/vscode/.local/bin:/usr/local/bin:${PATH}" ENV UV_PROJECT_ENVIRONMENT="/home/vscode/.venv" - -# Pre-configure Gemini CLI -RUN mkdir -p /home/vscode/.gemini \ - && echo '{"/workspace": "TRUST_FOLDER"}' > /home/vscode/.gemini/trustedFolders.json \ - && echo '{"security": {"auth": {"selectedType": "gemini-api-key"}}}' > /home/vscode/.gemini/settings.json diff --git a/scripts/agentic-sandbox.py b/scripts/agentic-sandbox.py new file mode 100644 index 00000000..16c105b5 --- /dev/null +++ b/scripts/agentic-sandbox.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +""" +Script to run an AI agent in a Docker sandbox. + +Supported agents: Claude Code, Antigravity CLI. +""" + +import argparse +import os +import subprocess +import sys + + +def _seed_config_file(path: str, content: bytes) -> None: + """ + Create path with content and mode 0o600. + + Skips silently if it already exists. + """ + try: + fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + except FileExistsError: + return + try: + os.write(fd, content) + finally: + os.close(fd) + + +def main() -> None: + """Provide the main entry point for the agentic sandbox script.""" + parser = argparse.ArgumentParser( + description="Run an AI agent (claude or antigravity)" + " in a Docker sandbox." + ) + parser.add_argument( + "agent", + choices=["claude", "antigravity"], + help="AI agent to run inside the sandbox.", + ) + parser.add_argument( + "--update", + action="store_true", + help="Update the agent CLI inside the container before running.", + ) + parser.add_argument( + "--rebuild-docker", action="store_true", help="Rebuild the Docker image." + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable verbose logging." + ) + + args = parser.parse_args() + + def log(msg: str) -> None: + """Log a message if verbose mode is enabled.""" + if args.verbose: + print(msg) + + project_root = subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], text=True + ).strip() + + image_name = "cc-protocol-sandbox" + + image_exists = ( + subprocess.run( + ["docker", "image", "inspect", image_name], + capture_output=True, + ).returncode + == 0 + ) + + if args.rebuild_docker or not image_exists: + log(f"--- Building Docker Sandbox: {image_name} ---") + subprocess.check_call( + [ + "docker", + "build", + "-t", + image_name, + "-f", + os.path.join(project_root, "docker/Dockerfile"), + project_root, + ] + ) + + log(f"--- Starting Sandboxed {args.agent.capitalize()} Session ---") + log(f"Note: Your current directory {project_root} is mounted to /workspace") + + if args.agent == "antigravity": + npm_package = None + agent_cmd = "agy" + else: + npm_package = "@anthropic-ai/claude-code" + agent_cmd = "claude --dangerously-skip-permissions" + + if args.update: + if args.agent == "antigravity": + container_cmd = ( + "curl -fsSL https://antigravity.google/cli/install.sh | bash && " + f"{agent_cmd}" + ) + else: + container_cmd = ( + f"export NPM_CONFIG_PREFIX=~/.npm-global && " + f"export PATH=~/.npm-global/bin:$PATH && " + f"npm install -g {npm_package}@latest && {agent_cmd}" + ) + else: + container_cmd = agent_cmd + + run_args = [ + "docker", + "run", + "-it", + "--rm", + "-v", + f"{project_root}:/workspace", + ] + + if args.agent == "antigravity": + antigravity_config_dir = os.path.expanduser("~/.gemini") + os.makedirs(antigravity_config_dir, mode=0o700, exist_ok=True) + # Seed default config files if missing so they are accessible inside the + # container. + # Use os.open with restrictive permissions to avoid exposing credentials to + # other local users on multi-user systems. + _seed_config_file( + os.path.join(antigravity_config_dir, "trustedFolders.json"), + b'{"/workspace": "TRUST_FOLDER"}', + ) + _seed_config_file( + os.path.join(antigravity_config_dir, "settings.json"), + b'{"selectedAuthType": "oauth-personal"}', + ) + run_args.extend(["-v", f"{antigravity_config_dir}:/home/vscode/.gemini"]) + else: + host_claude_dir = os.path.expanduser("~/.claude") + host_claude_json = os.path.expanduser("~/.claude.json") + os.makedirs(host_claude_dir, mode=0o700, exist_ok=True) + # Ensure the file exists on the host so Docker doesn't create it as a directory. + # Use os.open with restrictive permissions to avoid exposing credentials to + # other local users on multi-user systems. + if os.path.exists(host_claude_json): + if not os.path.isfile(host_claude_json): + sys.exit( + f"Expected {host_claude_json} to be a regular file, but found a " + "different filesystem object. Remove or rename it and rerun." + ) + else: + _seed_config_file(host_claude_json, b"") + run_args.extend(["-v", f"{host_claude_dir}:/home/vscode/.claude"]) + run_args.extend(["-v", f"{host_claude_json}:/home/vscode/.claude.json"]) + + if "TERM" in os.environ: + run_args.extend(["-e", f"TERM={os.environ['TERM']}"]) + if "COLORTERM" in os.environ: + run_args.extend(["-e", f"COLORTERM={os.environ['COLORTERM']}"]) + + run_args.extend([image_name, "bash", "-c", container_cmd]) + + subprocess.run(run_args, check=True) + + +if __name__ == "__main__": + main() diff --git a/scripts/agentic-sandbox.sh b/scripts/agentic-sandbox.sh new file mode 100755 index 00000000..451ec5d4 --- /dev/null +++ b/scripts/agentic-sandbox.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +set -eu -o pipefail + +# Get the workspace root +WORKSPACE_ROOT=$(git rev-parse --show-toplevel) + +# Change directory to the workspace root and run the python script +cd "$WORKSPACE_ROOT" +uv run scripts/agentic-sandbox.py "$@" diff --git a/scripts/gemini-sandbox.sh b/scripts/gemini-sandbox.sh deleted file mode 100755 index 955c04c5..00000000 --- a/scripts/gemini-sandbox.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# Exit on error -set -euo pipefail - -# Check if GEMINI_API_KEY is set -if [ -z "$GEMINI_API_KEY" ]; then - echo "Error: GEMINI_API_KEY environment variable is not set." - echo "Please set it before running this script:" - echo " export GEMINI_API_KEY='your_api_key_here'" - exit 1 -fi - -IMAGE_NAME="value-types-sandbox" -DOCKERFILE="docker/Dockerfile" - -# Build the image -echo "--- Building Docker Sandbox: $IMAGE_NAME ---" -docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" . - -# Run the container -echo "--- Starting Sandboxed Gemini Session ---" -echo "Note: Your current directory $(pwd) is mounted to /workspace" - -docker run -it --rm \ - -v "$(pwd):/workspace" \ - -e GEMINI_API_KEY="$GEMINI_API_KEY" \ - -e TERM=${TERM:-} \ - -e COLORTERM=${COLORTERM:-} \ - "$IMAGE_NAME" \ - gemini