From 074519c2e6faf530359757725233f77906d3d614 Mon Sep 17 00:00:00 2001 From: Avri Chen-Roth Date: Sun, 5 Jul 2026 22:37:52 +0300 Subject: [PATCH 1/6] feat: add claude ACP sandbox target Signed-off-by: Avri Chen-Roth --- docker/acp-sandbox/Dockerfile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docker/acp-sandbox/Dockerfile b/docker/acp-sandbox/Dockerfile index b99c3acc5..437d20216 100644 --- a/docker/acp-sandbox/Dockerfile +++ b/docker/acp-sandbox/Dockerfile @@ -10,6 +10,7 @@ # # docker build -f docker/acp-sandbox/Dockerfile --target hermes go/ # docker build -f docker/acp-sandbox/Dockerfile --target openclaw go/ +# docker build -f docker/acp-sandbox/Dockerfile --target claude go/ # # The contract between base and agent layers is intentionally tiny: # - ENTRYPOINT is acp-shim; it serves ws://0.0.0.0:9000/acp. @@ -132,3 +133,21 @@ COPY --chmod=755 \ USER agent ENTRYPOINT ["/usr/local/bin/openclaw-acp-entrypoint.sh"] CMD [] + +### Agent target: Claude — Anthropic's Claude Agent SDK exposed over ACP via +### the community adapter (@agentclientprotocol/claude-agent-acp, which wraps +### @anthropic-ai/claude-agent-sdk). Built on node-base because the adapter +### requires Node >=22 (trixie apt ships v20). Unlike openclaw it needs NO +### sandbox-local gateway: `claude-agent-acp` is a stdio ACP server, so the +### shim runs it directly as its child. +FROM node-base AS claude +ARG CLAUDE_AGENT_ACP_VERSION=0.55.0 +USER root +RUN npm install -g "@agentclientprotocol/claude-agent-acp@${CLAUDE_AGENT_ACP_VERSION}" +USER agent +# claude-agent-acp holds ACP session state in memory, so (like hermes) the +# child MUST be long-lived — the shim's default reconnect-grace=0 keeps it +# alive across client reconnects. Requires ANTHROPIC_API_KEY injected at +# runtime (credential injection belongs to the harness bootstrap, not this +# image). +CMD ["--", "claude-agent-acp"] From e81cdb030bf7303cfa649d5b2e44ec523e71e367 Mon Sep 17 00:00:00 2001 From: Avri Chen-Roth Date: Sun, 5 Jul 2026 22:41:03 +0300 Subject: [PATCH 2/6] docs: document the claude ACP sandbox target Signed-off-by: Avri Chen-Roth --- docker/acp-sandbox/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docker/acp-sandbox/README.md b/docker/acp-sandbox/README.md index 44851d121..e9ee255e0 100644 --- a/docker/acp-sandbox/README.md +++ b/docker/acp-sandbox/README.md @@ -18,6 +18,7 @@ The [Dockerfile](Dockerfile) defines: | `node` / `node-base` | internal | `base` + Node.js 22 (trixie apt ships v20, below OpenClaw's >=22.19 requirement). | | `hermes` | `--target hermes` | `base` + Hermes installed via pip (`hermes-agent[acp]`). Child command: `hermes acp`. | | `openclaw` | `--target openclaw` | `node-base` + the OpenClaw CLI (`npm install -g openclaw`). Runs a sandbox-local `openclaw gateway` alongside the shim via a small launcher. | +| `claude` | `--target claude` | `node-base` + the Claude Agent ACP adapter (`npm install -g @agentclientprotocol/claude-agent-acp`, wrapping `@anthropic-ai/claude-agent-sdk`). Child command: `claude-agent-acp`. No gateway — it speaks ACP over stdio directly. Requires `ANTHROPIC_API_KEY` at runtime. | The base↔agent contract is intentionally tiny: @@ -44,6 +45,7 @@ From the repo root (build context is `go/`): docker build -f docker/acp-sandbox/Dockerfile --target base -t kagent/acp-sandbox-base go/ docker build -f docker/acp-sandbox/Dockerfile --target hermes -t kagent/acp-sandbox-hermes go/ docker build -f docker/acp-sandbox/Dockerfile --target openclaw -t kagent/acp-sandbox-openclaw go/ +docker build -f docker/acp-sandbox/Dockerfile --target claude -t kagent/acp-sandbox-claude go/ ``` ## Smoke test (no cluster needed) @@ -58,6 +60,16 @@ websocat ws://localhost:9000/acp {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}} ``` +The `claude` target additionally needs an Anthropic API key at runtime — pass +`-e ANTHROPIC_API_KEY=sk-...` on `docker run`, then use the same `websocat` / +`initialize` handshake: + +```sh +docker run --rm -p 9000:9000 -e ANTHROPIC_API_KEY=sk-... kagent/acp-sandbox-claude +websocat ws://localhost:9000/acp +{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}} +``` + The shim does not authenticate the WebSocket handshake; in Substrate the actor's ingress is its only reachable surface and the controller proxies to it. @@ -92,3 +104,8 @@ controller. Each file's header comment has the full step-by-step; in short: belongs to the harness bootstrap, not these images. - Whether the shim is baked (this approach) or injected via init container + shared volume. +- Claude target: `@agentclientprotocol/claude-agent-acp` is community-maintained + and was recently renamed from `@zed-industries/claude-code-acp`. Verify the + ACP `initialize` handshake end-to-end at the pinned version (0.55.0) before + relying on it, and confirm the child's in-memory session model holds across + bridge reconnects. From 113a2b2ef884cebf84d001021decc63704741c32 Mon Sep 17 00:00:00 2001 From: Avri Chen-Roth Date: Sun, 5 Jul 2026 22:53:12 +0300 Subject: [PATCH 3/6] docs: mark claude ACP handshake verified at 0.55.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The initialize handshake round-trips against the built image (protocolVersion 1, authMethods empty → env-key auth, no separate authenticate step). Narrows the open item to the full prompt round-trip and reconnect behavior. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Avri Chen-Roth --- docker/acp-sandbox/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docker/acp-sandbox/README.md b/docker/acp-sandbox/README.md index e9ee255e0..1e3c3fe43 100644 --- a/docker/acp-sandbox/README.md +++ b/docker/acp-sandbox/README.md @@ -105,7 +105,9 @@ controller. Each file's header comment has the full step-by-step; in short: - Whether the shim is baked (this approach) or injected via init container + shared volume. - Claude target: `@agentclientprotocol/claude-agent-acp` is community-maintained - and was recently renamed from `@zed-industries/claude-code-acp`. Verify the - ACP `initialize` handshake end-to-end at the pinned version (0.55.0) before - relying on it, and confirm the child's in-memory session model holds across - bridge reconnects. + and was recently renamed from `@zed-industries/claude-code-acp`. The ACP + `initialize` handshake is verified at the pinned version (0.55.0) — it reports + `protocolVersion: 1`, `agentInfo.version: "0.55.0"`, and `authMethods: []` (so + it authenticates from `ANTHROPIC_API_KEY` with no separate `authenticate` + step). Still to verify: a full `session/prompt` round-trip reaches the model, + and the child's in-memory session model holds across bridge reconnects. From aa67df48bfe758c5650c7461be5dd95512534ff7 Mon Sep 17 00:00:00 2001 From: Avri Chen-Roth Date: Sun, 5 Jul 2026 23:07:22 +0300 Subject: [PATCH 4/6] ci: build and push acp-sandbox-claude image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the claude target to the CI build matrix (ci.yaml) and the release push matrix (tag.yaml), plus the supporting Makefile pieces: build-acp-sandbox-claude target, image-name/IMG vars, print-images echo, and the build-acp-sandbox aggregate dependency. Scope is the standalone image only — not wired into build-controller or the digest-embedding (that is harness-backend integration, tracked separately). Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Avri Chen-Roth --- .github/workflows/ci.yaml | 1 + .github/workflows/tag.yaml | 1 + Makefile | 13 +++++++++++-- docker/acp-sandbox/Dockerfile | 2 +- docker/acp-sandbox/README.md | 4 ++-- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ed3ee204b..8a435c1a9 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -365,6 +365,7 @@ jobs: - skills-init - acp-sandbox-hermes - acp-sandbox-openclaw + - acp-sandbox-claude runs-on: ubuntu-latest services: registry: diff --git a/.github/workflows/tag.yaml b/.github/workflows/tag.yaml index 72e67638a..9dc438daf 100644 --- a/.github/workflows/tag.yaml +++ b/.github/workflows/tag.yaml @@ -25,6 +25,7 @@ jobs: - skills-init - acp-sandbox-hermes - acp-sandbox-openclaw + - acp-sandbox-claude runs-on: ubuntu-latest permissions: contents: read diff --git a/Makefile b/Makefile index 6b610558a..94119f382 100644 --- a/Makefile +++ b/Makefile @@ -59,6 +59,7 @@ SKILLS_INIT_IMAGE_NAME ?= skills-init ACP_SANDBOX_BASE_IMAGE_NAME ?= acp-sandbox-base ACP_SANDBOX_HERMES_IMAGE_NAME ?= acp-sandbox-hermes ACP_SANDBOX_OPENCLAW_IMAGE_NAME ?= acp-sandbox-openclaw +ACP_SANDBOX_CLAUDE_IMAGE_NAME ?= acp-sandbox-claude CONTROLLER_IMAGE_TAG ?= $(VERSION) UI_IMAGE_TAG ?= $(VERSION) @@ -82,6 +83,7 @@ SKILLS_INIT_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(SKILLS_INIT_IMAGE_NAME):$ ACP_SANDBOX_BASE_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(ACP_SANDBOX_BASE_IMAGE_NAME):$(ACP_SANDBOX_IMAGE_TAG) ACP_SANDBOX_HERMES_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(ACP_SANDBOX_HERMES_IMAGE_NAME):$(ACP_SANDBOX_IMAGE_TAG) ACP_SANDBOX_OPENCLAW_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(ACP_SANDBOX_OPENCLAW_IMAGE_NAME):$(ACP_SANDBOX_IMAGE_TAG) +ACP_SANDBOX_CLAUDE_IMG ?= $(DOCKER_REGISTRY)/$(DOCKER_REPO)/$(ACP_SANDBOX_CLAUDE_IMAGE_NAME):$(ACP_SANDBOX_IMAGE_TAG) #take from go/go.mod AWK ?= $(shell command -v gawk || command -v awk) @@ -256,6 +258,7 @@ build-img-versions: ## Print the fully-qualified image tags for all components @echo acp-sandbox-base=$(ACP_SANDBOX_BASE_IMG) @echo acp-sandbox-hermes=$(ACP_SANDBOX_HERMES_IMG) @echo acp-sandbox-openclaw=$(ACP_SANDBOX_OPENCLAW_IMG) + @echo acp-sandbox-claude=$(ACP_SANDBOX_CLAUDE_IMG) .PHONY: controller-manifests controller-manifests: ## Regenerate CRD manifests and copy them into the Helm chart @@ -329,8 +332,8 @@ build-skills-init: buildx-create $(DOCKER_PUSH) $(SKILLS_INIT_IMG) .PHONY: build-acp-sandbox -build-acp-sandbox: ## Build and push all ACP sandbox agent images (hermes, openclaw) -build-acp-sandbox: build-acp-sandbox-hermes build-acp-sandbox-openclaw +build-acp-sandbox: ## Build and push all ACP sandbox agent images (hermes, openclaw, claude) +build-acp-sandbox: build-acp-sandbox-hermes build-acp-sandbox-openclaw build-acp-sandbox-claude .PHONY: build-acp-sandbox-base build-acp-sandbox-base: ## Build and push the ACP sandbox base image (acp-shim only, no agent) @@ -350,6 +353,12 @@ build-acp-sandbox-openclaw: buildx-create $(DOCKER_BUILDER) $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) --target openclaw -t $(ACP_SANDBOX_OPENCLAW_IMG) -f docker/acp-sandbox/Dockerfile ./go $(DOCKER_PUSH) $(ACP_SANDBOX_OPENCLAW_IMG) +.PHONY: build-acp-sandbox-claude +build-acp-sandbox-claude: ## Build and push the ACP sandbox Claude image +build-acp-sandbox-claude: buildx-create + $(DOCKER_BUILDER) $(DOCKER_BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) --target claude -t $(ACP_SANDBOX_CLAUDE_IMG) -f docker/acp-sandbox/Dockerfile ./go + $(DOCKER_PUSH) $(ACP_SANDBOX_CLAUDE_IMG) + .PHONY: push push: ## Push all component images (controller, ui, app, ADKs) push: push-controller push-ui push-app push-kagent-adk push-golang-adk push-golang-adk-full diff --git a/docker/acp-sandbox/Dockerfile b/docker/acp-sandbox/Dockerfile index 437d20216..8611a0d55 100644 --- a/docker/acp-sandbox/Dockerfile +++ b/docker/acp-sandbox/Dockerfile @@ -141,7 +141,7 @@ CMD [] ### sandbox-local gateway: `claude-agent-acp` is a stdio ACP server, so the ### shim runs it directly as its child. FROM node-base AS claude -ARG CLAUDE_AGENT_ACP_VERSION=0.55.0 +ARG CLAUDE_AGENT_ACP_VERSION=0.58.1 USER root RUN npm install -g "@agentclientprotocol/claude-agent-acp@${CLAUDE_AGENT_ACP_VERSION}" USER agent diff --git a/docker/acp-sandbox/README.md b/docker/acp-sandbox/README.md index 1e3c3fe43..eaf5b1c8f 100644 --- a/docker/acp-sandbox/README.md +++ b/docker/acp-sandbox/README.md @@ -106,8 +106,8 @@ controller. Each file's header comment has the full step-by-step; in short: + shared volume. - Claude target: `@agentclientprotocol/claude-agent-acp` is community-maintained and was recently renamed from `@zed-industries/claude-code-acp`. The ACP - `initialize` handshake is verified at the pinned version (0.55.0) — it reports - `protocolVersion: 1`, `agentInfo.version: "0.55.0"`, and `authMethods: []` (so + `initialize` handshake is verified at the pinned version (0.58.1) — it reports + `protocolVersion: 1`, `agentInfo.version: "0.58.1"`, and `authMethods: []` (so it authenticates from `ANTHROPIC_API_KEY` with no separate `authenticate` step). Still to verify: a full `session/prompt` round-trip reaches the model, and the child's in-memory session model holds across bridge reconnects. From e220d765220eb95116ad8f646e826a48bdf28bf9 Mon Sep 17 00:00:00 2001 From: Avri Chen-Roth Date: Thu, 9 Jul 2026 16:52:53 +0300 Subject: [PATCH 5/6] docs: fix claude ACP sandbox smoke-test and clarify transport - Add missing "from another shell" instruction to the claude smoke-test block (consistent with the hermes example above it) - Add note that a successful initialize handshake does not confirm end-to-end prompt processing - Clarify table entry: claude-agent-acp speaks to the shim over stdio; the shim still exposes ws://0.0.0.0:9000/acp externally Signed-off-by: Avri Chen-Roth Co-Authored-By: Claude Sonnet 4.6 --- docker/acp-sandbox/README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/acp-sandbox/README.md b/docker/acp-sandbox/README.md index eaf5b1c8f..0804bd1ca 100644 --- a/docker/acp-sandbox/README.md +++ b/docker/acp-sandbox/README.md @@ -18,7 +18,7 @@ The [Dockerfile](Dockerfile) defines: | `node` / `node-base` | internal | `base` + Node.js 22 (trixie apt ships v20, below OpenClaw's >=22.19 requirement). | | `hermes` | `--target hermes` | `base` + Hermes installed via pip (`hermes-agent[acp]`). Child command: `hermes acp`. | | `openclaw` | `--target openclaw` | `node-base` + the OpenClaw CLI (`npm install -g openclaw`). Runs a sandbox-local `openclaw gateway` alongside the shim via a small launcher. | -| `claude` | `--target claude` | `node-base` + the Claude Agent ACP adapter (`npm install -g @agentclientprotocol/claude-agent-acp`, wrapping `@anthropic-ai/claude-agent-sdk`). Child command: `claude-agent-acp`. No gateway — it speaks ACP over stdio directly. Requires `ANTHROPIC_API_KEY` at runtime. | +| `claude` | `--target claude` | `node-base` + the Claude Agent ACP adapter (`npm install -g @agentclientprotocol/claude-agent-acp`, wrapping `@anthropic-ai/claude-agent-sdk`). Child command: `claude-agent-acp`. No gateway — `claude-agent-acp` communicates with the shim over stdio; the shim still exposes `ws://0.0.0.0:9000/acp` externally. Requires `ANTHROPIC_API_KEY` at runtime. | The base↔agent contract is intentionally tiny: @@ -66,10 +66,13 @@ The `claude` target additionally needs an Anthropic API key at runtime — pass ```sh docker run --rm -p 9000:9000 -e ANTHROPIC_API_KEY=sk-... kagent/acp-sandbox-claude +# then from another shell, speak newline-delimited JSON-RPC over WS: websocat ws://localhost:9000/acp {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}} ``` +> **Note:** A successful `initialize` response only verifies the ACP transport layer. It does not confirm that prompts reach the model — see the open item in [Open items](#open-items-tracked-in-the-ep) below. + The shim does not authenticate the WebSocket handshake; in Substrate the actor's ingress is its only reachable surface and the controller proxies to it. From d75f1791820d38fda69ddf2b9694a98687b1ace5 Mon Sep 17 00:00:00 2001 From: Avri Chen-Roth Date: Fri, 10 Jul 2026 09:23:11 +0300 Subject: [PATCH 6/6] Improve open-items reference Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Avri Chen-Roth --- docker/acp-sandbox/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/acp-sandbox/README.md b/docker/acp-sandbox/README.md index 0804bd1ca..cfb701c0f 100644 --- a/docker/acp-sandbox/README.md +++ b/docker/acp-sandbox/README.md @@ -71,7 +71,7 @@ websocat ws://localhost:9000/acp {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{}}} ``` -> **Note:** A successful `initialize` response only verifies the ACP transport layer. It does not confirm that prompts reach the model — see the open item in [Open items](#open-items-tracked-in-the-ep) below. +> **Note:** A successful `initialize` response only verifies the ACP transport layer. It does not confirm that prompts reach the model — see the Claude open item in [Open items](#open-items-tracked-in-the-ep) below. The shim does not authenticate the WebSocket handshake; in Substrate the actor's ingress is its only reachable surface and the controller proxies to it.