diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..216f02d7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +# Build artifacts +target/ +*.rlib +*.d + +# Version control +.git/ +.gitignore + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Environment and secrets +.env +.env.* +secrets/ + +# Documentation build output +docs/book/ + +# CI/CD +.github/ + +# Node modules (if any local tooling) +node_modules/ + +# Test databases and temp files +*.db +*.db-shm +*.db-wal + +# UI build artifacts +ui/node_modules/ +ui/dist/ diff --git a/.github/workflows/docker-claude-code.yml b/.github/workflows/docker-claude-code.yml new file mode 100644 index 00000000..be487335 --- /dev/null +++ b/.github/workflows/docker-claude-code.yml @@ -0,0 +1,81 @@ +name: Docker — Claude Code Image + +on: + push: + branches: [main] + paths: + - "docker/claude-code/**" + - ".github/workflows/docker-claude-code.yml" + pull_request: + branches: [main] + paths: + - "docker/claude-code/**" + - ".github/workflows/docker-claude-code.yml" + release: + types: [published] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: geoffjay/agentd-claude + +jobs: + build: + name: Build & Push + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Set up QEMU (for multi-platform builds) + uses: docker/setup-qemu-action@v3 + + - name: Log in to GitHub Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + # latest on main branch pushes + type=raw,value=latest,enable={{is_default_branch}} + # short SHA on every build + type=sha,prefix=sha- + # semver tags on releases (v1.2.3 → 1.2.3, 1.2, 1) + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: docker/claude-code + file: docker/claude-code/Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # NOTE: This only verifies the amd64 variant. The arm64 layer is built + # and pushed but not smoke-tested here because ubuntu-latest is amd64. + # A future improvement could add a QEMU-based arm64 verify step. + - name: Verify image (amd64) + if: github.event_name != 'pull_request' + run: | + docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${GITHUB_SHA::7} + docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${GITHUB_SHA::7} --version diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..545a846f --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +# agentd — top-level build automation +# +# Usage: +# make help Show available targets +# make build Build the Rust workspace +# make test Run all tests +# make docker-build-claude Build the Claude Code Docker image locally + +.PHONY: help build test clippy fmt fmt-fix docker-build-claude docker-build-claude-multiarch docker-run-claude + +# Default image name — matches the DEFAULT_IMAGE constant in crates/wrap/src/docker.rs +CLAUDE_IMAGE ?= agentd-claude:latest + +help: ## Show this help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-24s\033[0m %s\n", $$1, $$2}' + +# ── Rust ───────────────────────────────────────────────────────────── + +build: ## Build the Rust workspace + cargo build --workspace + +test: ## Run all workspace tests + cargo test --workspace + +clippy: ## Run clippy lints + cargo clippy --workspace -- -D warnings + +fmt: ## Check formatting + cargo fmt --all -- --check + +fmt-fix: ## Auto-fix formatting + cargo fmt --all + +# ── Docker ─────────────────────────────────────────────────────────── + +docker-build-claude: ## Build the Claude Code agent Docker image locally + docker build -t $(CLAUDE_IMAGE) docker/claude-code/ + +docker-build-claude-multiarch: ## Build multi-platform Claude Code image (requires buildx) + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t $(CLAUDE_IMAGE) \ + docker/claude-code/ + +docker-run-claude: ## Run claude --version in the agent image (smoke test) + docker run --rm $(CLAUDE_IMAGE) --version diff --git a/docker/claude-code/Dockerfile b/docker/claude-code/Dockerfile new file mode 100644 index 00000000..fa911f70 --- /dev/null +++ b/docker/claude-code/Dockerfile @@ -0,0 +1,70 @@ +# syntax=docker/dockerfile:1 +# +# Claude Code agent execution image for agentd. +# +# This image provides a minimal environment for running the Claude Code CLI +# inside Docker containers managed by the agentd orchestrator's DockerBackend. +# +# Build: +# docker build -t agentd-claude:latest docker/claude-code/ +# +# Run (requires ANTHROPIC_API_KEY): +# docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" agentd-claude:latest --version +# +# Usage with agentd: +# The orchestrator's DockerBackend creates containers from this image +# automatically. API keys are passed as environment variables at runtime — +# nothing secret is baked into the image. + +FROM node:22-slim + +# ── System dependencies ─────────────────────────────────────────────── +# Install common dev tools needed by Claude Code and agent workflows. +# - git: repository operations, diffs, commits +# - ca-certificates: HTTPS connections to APIs and registries +# - curl: health checks, API calls, downloading tools +# - jq: JSON processing in shell scripts +# - openssh-client: git operations over SSH +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + ca-certificates \ + curl \ + jq \ + openssh-client \ + && rm -rf /var/lib/apt/lists/* + +# ── Claude Code CLI ────────────────────────────────────────────────── +# Install globally so it's available on PATH for all users. +# Pin the version for reproducible builds. Update deliberately or via +# Renovate / Dependabot when a new release is needed. +ARG CLAUDE_CODE_VERSION=0.2.70 +RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} && \ + npm cache clean --force + +# ── Non-root user ──────────────────────────────────────────────────── +# The node:22-slim base image already has a `node` user at UID 1000. +# Rename it to `agent` for clarity and to match DockerBackend's default +# user (1000:1000). Using usermod/groupmod avoids the "UID already +# exists" error from useradd. +RUN groupmod --new-name agent node && \ + usermod --login agent --home /home/agent --move-home --comment "agentd agent" node + +# ── Git configuration ──────────────────────────────────────────────── +# Set safe.directory so git works in bind-mounted /workspace regardless +# of ownership. Also set a default identity for commits made by agents. +RUN git config --system safe.directory /workspace && \ + git config --system user.email "agent@agentd.local" && \ + git config --system user.name "agentd agent" + +# ── Workspace ──────────────────────────────────────────────────────── +RUN mkdir -p /workspace && chown agent:agent /workspace +WORKDIR /workspace + +# ── Runtime ────────────────────────────────────────────────────────── +USER agent + +# Verify the CLI is installed and accessible. +RUN claude --version + +CMD ["claude"] diff --git a/docker/claude-code/README.md b/docker/claude-code/README.md new file mode 100644 index 00000000..ca804a5a --- /dev/null +++ b/docker/claude-code/README.md @@ -0,0 +1,86 @@ +# Claude Code Agent Image + +Docker image for running Claude Code CLI agents inside containers managed by +the agentd orchestrator's `DockerBackend`. + +## Quick Start + +```bash +# Build locally +make docker-build-claude + +# Or build directly +docker build -t agentd-claude:latest docker/claude-code/ + +# Verify +docker run --rm agentd-claude:latest --version + +# Run with API key +docker run --rm \ + -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ + -v "$(pwd):/workspace" \ + agentd-claude:latest --version +``` + +## What's Included + +| Component | Purpose | +|-----------|---------| +| Node.js 22 (slim) | Runtime for Claude Code CLI | +| `@anthropic-ai/claude-code` | The Claude Code CLI itself | +| `git` | Repository operations | +| `curl` | HTTP requests, health checks | +| `jq` | JSON processing | +| `openssh-client` | Git over SSH | +| `ca-certificates` | TLS certificate validation | + +## Security + +- **Non-root**: Runs as `agent` (UID 1000) by default +- **No secrets**: API keys are passed at runtime via environment variables +- **Minimal base**: Uses `node:22-slim` to reduce attack surface + +## Customization + +### Adding tools + +Create a derived Dockerfile: + +```dockerfile +FROM ghcr.io/geoffjay/agentd-claude:latest + +USER root +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 \ + ripgrep \ + && rm -rf /var/lib/apt/lists/* +USER agent +``` + +### Using a different base image + +Fork this Dockerfile and change the `FROM` line. The key requirements are: +- Node.js (for the Claude Code CLI) +- A non-root user with UID 1000 +- `/workspace` as the working directory + +## Image Tags + +| Tag | Description | +|-----|-------------| +| `latest` | Latest build from `main` branch | +| `sha-` | Pinned to a specific git commit | +| `v1.2.3` | Pinned to a semver release tag | + +## How It Works with agentd + +The `DockerBackend` in the orchestrator creates containers from this image: + +1. The orchestrator calls `docker create` with this image +2. The host project directory is bind-mounted to `/workspace` +3. API keys are passed as environment variables +4. The `NetworkPolicy` controls container networking +5. Claude Code connects back to the orchestrator via WebSocket + +The container's entrypoint is `claude`, and the orchestrator passes additional +arguments (like `--sdk-url`, `--output-format stream-json`) via `docker exec`.