From de9092ececb11ee0efce1d0feeec53700ee4ef64 Mon Sep 17 00:00:00 2001 From: JBD Date: Sat, 24 Oct 2026 13:56:18 -0700 Subject: [PATCH] Point Claude Code example at ax-images repo and document it - Push the Claude Code image to gcr.io/ax-images/ax-images and update examples/claude-code.yaml to reference it. - Write the agent's output to /workspace/claude-output.md. - Add a TODO to inject ANTHROPIC_API_KEY into outbound calls automatically. - Add a short Claude Code example pointer to the README and runner docs, including the amd64 build note. --- Dockerfile.claude-code | 57 +++++++++++++++++++++++++ Makefile | 17 +++++++- README.md | 2 + docs/manifests.md | 2 +- docs/runner.md | 2 + examples/claude-code.yaml | 90 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.claude-code create mode 100644 examples/claude-code.yaml diff --git a/Dockerfile.claude-code b/Dockerfile.claude-code new file mode 100644 index 00000000..f5094824 --- /dev/null +++ b/Dockerfile.claude-code @@ -0,0 +1,57 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Task image with Claude Code preinstalled. +# +# Extends the default ax-task-runner image, so the runner contract +# (/usr/local/bin/ax-task-runner as PID 1, probes, metadata server) is +# unchanged. Only the tools available to spec.command differ. +# +# make build-claude-code-image # build locally +# make push-claude-code-image # push and print the digest +# make push-claude-code-image CLAUDE_CODE_VERSION=2.1.310 +# +# Pin the digest the Makefile prints in your Task's spec.image. See +# examples/claude-code.yaml for a Task that uses this image. +# +# Build on a linux/amd64 host (or in CI). The installer executes the +# downloaded binary to finish setup, and that binary aborts under qemu +# emulation, so cross-building for amd64 on Apple Silicon fails. + +# Pinned to the same digest the examples use so runner behavior is +# reproducible. Override with --build-arg BASE_IMAGE=... (or +# CLAUDE_CODE_BASE_IMAGE= for the Makefile target) to build on your own runner. +ARG BASE_IMAGE=gcr.io/ax-substrate/ate-images/ax-task-runner@sha256:3a0dea6ad8b55278685db58aca6e37dc4ba04056831d45bef3aaeafdca43cac6 +FROM ${BASE_IMAGE} + +# Claude Code's native installer ships a standalone binary, so no Node.js is +# needed. "latest", "stable", or an exact version such as 2.1.310. +ARG CLAUDE_CODE_VERSION=latest + +# The installer puts the launcher in ~/.local/bin and versioned binaries in +# ~/.local/share/claude. Install as root (the task runs as root) and expose the +# launcher on the default PATH so spec.command can call `claude` directly. +RUN curl -fsSL https://claude.ai/install.sh | bash -s -- "${CLAUDE_CODE_VERSION}" \ + && ln -s /root/.local/bin/claude /usr/local/bin/claude \ + && claude --version + +# The sandbox is the isolation boundary, so headless runs use +# --dangerously-skip-permissions. Claude Code refuses that flag when running +# as root unless IS_SANDBOX is set. +ENV IS_SANDBOX=1 \ + CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ + DISABLE_AUTOUPDATER=1 + +# Keep the default runner entrypoint. +ENTRYPOINT ["/usr/local/bin/ax-task-runner"] diff --git a/Makefile b/Makefile index 2d918f50..fe86db51 100644 --- a/Makefile +++ b/Makefile @@ -17,9 +17,12 @@ SHELL := /bin/bash # Configuration AX_IMAGE_REPO ?= gcr.io/ax-substrate/ate-images TASK_RUNNER_REPO ?= $(AX_IMAGE_REPO)/ax-task-runner +CLAUDE_CODE_REPO ?= gcr.io/ax-images/ax-images +CLAUDE_CODE_VERSION ?= latest +CLAUDE_CODE_BASE_IMAGE ?= CONTAINER_CLI ?= $(shell which podman 2>/dev/null || which docker 2>/dev/null) -.PHONY: all build build-binaries build-task-runner install push push-task-runner deploy deploy-server deploy-redis apply-example test test-e2e test-e2e-head e2e-setup e2e-run e2e-cleanup clean +.PHONY: all build build-binaries build-task-runner build-claude-code-image install push push-task-runner push-claude-code-image deploy deploy-server deploy-redis apply-example test test-e2e test-e2e-head e2e-setup e2e-run e2e-cleanup clean all: build @@ -56,6 +59,18 @@ push-task-runner: build-task-runner @echo "==> Current pushed digest:" @gcloud container images list-tags $(TASK_RUNNER_REPO) --filter="tags=latest" --format="get(digest)" +# Build a task image with Claude Code preinstalled, on top of the default runner image +build-claude-code-image: + @echo "==> Building container image $(CLAUDE_CODE_REPO):latest (Claude Code $(CLAUDE_CODE_VERSION)) using $(CONTAINER_CLI)..." + $(CONTAINER_CLI) build --platform linux/amd64 --build-arg CLAUDE_CODE_VERSION=$(CLAUDE_CODE_VERSION) $(if $(CLAUDE_CODE_BASE_IMAGE),--build-arg BASE_IMAGE=$(CLAUDE_CODE_BASE_IMAGE),) -t $(CLAUDE_CODE_REPO):latest -f Dockerfile.claude-code . + +# Push the Claude Code task image to the registry and print the digest to pin in spec.image +push-claude-code-image: build-claude-code-image + @echo "==> Pushing Claude Code task image to $(CLAUDE_CODE_REPO):latest..." + $(CONTAINER_CLI) push $(CLAUDE_CODE_REPO):latest + @echo "==> Current pushed digest:" + @gcloud container images list-tags $(CLAUDE_CODE_REPO) --filter="tags=latest" --format="get(digest)" + # Build and push all images push: push-task-runner diff --git a/README.md b/README.md index bc8a01d7..e5f1c7f9 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ ax suspend task task123 # checkpoint and pause ax resume task task123 # pick up where it left off ``` +To run Claude Code against a repo, apply [`examples/claude-code.yaml`](examples/claude-code.yaml). + Want to see the whole lifecycle end to end? Run [`./demo.sh`](demo.sh). It applies a custom workspace, waits for readiness, runs commands over `ax ssh`, and suspends the task. ## Documentation diff --git a/docs/manifests.md b/docs/manifests.md index 17897623..ceacbeb1 100644 --- a/docs/manifests.md +++ b/docs/manifests.md @@ -1,6 +1,6 @@ # Writing manifests -All four kinds can live in one multi-document YAML file. See [`examples/task.yaml`](../examples/task.yaml) for a complete, working set. +All four kinds can live in one multi-document YAML file. See [`examples/task.yaml`](../examples/task.yaml) for a complete, working set. [`examples/claude-code.yaml`](../examples/claude-code.yaml) shows a Task that runs Claude Code headlessly against a cloned repository. `metadata.name` and `metadata.atespace` become Substrate resource names, so they must be lowercase RFC 1123 labels: at most 63 lowercase alphanumeric characters or `-`, starting and ending with an alphanumeric character. `ax apply` rejects anything else up front rather than letting the task fail later with `ActorCreationFailed`. diff --git a/docs/runner.md b/docs/runner.md index 02b3ec88..eb4c3ab9 100644 --- a/docs/runner.md +++ b/docs/runner.md @@ -79,6 +79,8 @@ RUN npm install -g my-agent The runner binary stays at `/usr/local/bin/ax-task-runner`, so nothing else changes. +For a working example that layers Claude Code onto the default image, see [`Dockerfile.claude-code`](../Dockerfile.claude-code). `make push-claude-code-image` builds it, pushes it, and prints the digest to pin in `spec.image`; [`examples/claude-code.yaml`](../examples/claude-code.yaml) is a Task that uses it. Build it on an amd64 host: the Claude Code installer runs the binary it downloads, which aborts under qemu emulation. + ### 2. Embed the runner package in your own binary If you want the standard lifecycle but need to run code around it, import `github.com/google/ax/runner` and call `runner.Run` yourself. This gives you a hook for the command's exit and a place to do your own setup before or after the metadata server starts: diff --git a/examples/claude-code.yaml b/examples/claude-code.yaml new file mode 100644 index 00000000..e14caf0e --- /dev/null +++ b/examples/claude-code.yaml @@ -0,0 +1,90 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A Task that runs Claude Code headlessly against a cloned repository. +# +# The Workspace clones the repo and drops a CLAUDE.md into it so the agent +# starts with project guidance. The Task uses an image with Claude Code +# preinstalled (Dockerfile.claude-code) and runs one non-interactive +# `claude -p` session in /workspace, writing the agent's report to +# /workspace/claude-output.md. +# +# Before applying: +# +# 1. Build and push the image, then pin its digest in spec.image below. +# Substrate rejects tag-only image references. +# +# make push-claude-code-image AX_IMAGE_REPO= +# +# 2. Credentials. The control plane only injects GEMINI_API_KEY from a Model +# resource; an Anthropic Model is not injected into the task container yet. +# So ANTHROPIC_API_KEY has to be set through spec.env. Substitute it at +# apply time instead of committing it: +# +# ANTHROPIC_API_KEY=sk-ant-... envsubst < examples/claude-code.yaml | ax apply -f - +# +# ax watch task claude-code +# ax ssh claude-code -- cat /workspace/claude-output.md +# ax ssh claude-code -- git -C /workspace diff +apiVersion: ax.io/v1alpha1 +kind: Workspace +metadata: + name: itsdangerous + atespace: default +spec: + git: + - name: origin + repo: "https://github.com/pallets/itsdangerous.git" + branch: "main" + files: + - path: "CLAUDE.md" + content: | + # Working in this repo + - Pure-Python library. Install with `pip install -e . pytest`, test with `pytest`. + - Keep changes small and focused; do not add runtime dependencies. + - Do not push or open pull requests. Leave changes in the working tree. +--- +apiVersion: ax.io/v1alpha1 +kind: Task +metadata: + name: claude-code + atespace: default +spec: + # Built from Dockerfile.claude-code; replace the digest with the one + # `make push-claude-code-image` prints. + image: "gcr.io/ax-images/ax-images@sha256:REPLACE_WITH_PUSHED_DIGEST" + command: + # Run one headless session in the workspace. The sandbox is the isolation + # boundary, so permission prompts are skipped; the image sets IS_SANDBOX=1, + # which that flag requires when running as root. + - sh + - -c + - claude -p --model claude-opus-5-5 --dangerously-skip-permissions --max-turns 50 "$PROMPT" > /workspace/claude-output.md + env: + - name: PROMPT + value: | + Read the repository, install it with its test dependencies, and run the + test suite. Fix any failing or flaky tests you find. If everything passes, + make one small, clearly correct improvement to the README. Finish with a + short markdown report of what you checked and what you changed. + # TODO: Inject the ANTHROPIC_API_KEY to the outbound calls automatically. + - name: ANTHROPIC_API_KEY + value: "${ANTHROPIC_API_KEY}" + resources: + limits: + cpu: "2" + memory: "4Gi" + workspace: + name: itsdangerous + debug: true # serve guest services so `ax ssh` works; off by default