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
42 changes: 42 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -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/
81 changes: 81 additions & 0 deletions .github/workflows/docker-claude-code.yml
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions docker/claude-code/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
86 changes: 86 additions & 0 deletions docker/claude-code/README.md
Original file line number Diff line number Diff line change
@@ -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-<hash>` | 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`.
Loading