Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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 \
Expand All @@ -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
167 changes: 167 additions & 0 deletions scripts/agentic-sandbox.py
Original file line number Diff line number Diff line change
@@ -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()
10 changes: 10 additions & 0 deletions scripts/agentic-sandbox.sh
Original file line number Diff line number Diff line change
@@ -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 "$@"
31 changes: 0 additions & 31 deletions scripts/gemini-sandbox.sh

This file was deleted.

Loading