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
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ jobs:
- skills-init
- acp-sandbox-hermes
- acp-sandbox-openclaw
- acp-sandbox-claude
runs-on: ubuntu-latest
services:
registry:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tag.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ jobs:
- skills-init
- acp-sandbox-hermes
- acp-sandbox-openclaw
- acp-sandbox-claude
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -257,6 +259,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
Expand Down Expand Up @@ -330,8 +333,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)
Expand All @@ -351,6 +354,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
Expand Down
19 changes: 19 additions & 0 deletions docker/acp-sandbox/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.58.1
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"]
22 changes: 22 additions & 0 deletions docker/acp-sandbox/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 — `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:

Expand All @@ -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)
Expand All @@ -58,6 +60,19 @@ 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
# 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 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.

Expand Down Expand Up @@ -92,3 +107,10 @@ 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`. The ACP
`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.
Loading