fix(examples): inherit acp-docker image from config/defaults.json (#1434)

* fix(examples): inherit acp-docker image from config/defaults.json

examples/acp-docker/docker-compose.yml hardcoded the agent-server image at
`1.25.0-python`. Canvas enforces `compatibility.minimumAgentServer` (1.28.0)
from the repo's single source of truth, so the example default fell below the
floor and rendered "Disconnected — requires 1.28.0 or newer" — a reviewer
following the quickstart as written never reached the feature.

examples/acp-docker was the lone in-repo file hardcoding a version instead of
inheriting from config/defaults.json (14 other files read it; check-sdk-version
-sync only validates the released PyPI package, not in-repo files).

- scripts/gen-acp-docker-env.mjs: read defaults.json, pin AGENT_SERVER_IMAGE to
  `${images.agentServer}:${versions.agentServer}-python` in examples/acp-docker
  /.env (idempotent upsert; mirrors scripts/docker-build.mjs).
- package.json: `npm run example:acp-docker:env`.
- docker-compose.yml: no-config fallback `1.25.0-python` -> `latest-python`,
  always >= the compatibility floor, so zero-config `docker compose up` never
  shows "Disconnected"; the generated .env overrides with the pinned SoT
  version for the reproducible path.
- .env.example / README.md: document both paths; correct the version narrative
  (floor is the defaults.json compatibility pin; #3510 is the deeper functional
  floor at/below it).
- __tests__/scripts/acp-docker-env-sync.test.ts: assert the generator's tag
  matches defaults.json, the pin satisfies the floor, and the compose fallback
  stays `latest-python`. Mirrors docs-version-sync.test.ts — the guard that
  makes "can't silently drift" true.

* test(examples): harden acp-docker env-sync per review

Addresses the cli-review-panel findings worth acting on (the rest were
cosmetic or matched the no-validation idiom of scripts/docker-build.mjs):

- gte() in the test guarded with parseSemver — a non-numeric pin (sha /
  pre-release) now fails the floor check loudly instead of silently
  comparing NaN. The floor check is a CI gate; its one piece of logic
  shouldn't mis-compare in silence.
- compose-fallback assertion derives the registry from config.images
  .agentServer instead of hardcoding ghcr.io/openhands/... — a registry
  change no longer false-fails a test that only cares about the latest-python
  tag.
- upsertEnvLine now has unit tests (append / replace-in-place+preserve /
  idempotent / commented-template-line / keyless-line guard), making the
  "idempotent upsert" claim defensible. It was the one untested piece of real
  logic.
- upsertEnvLine guards a keyless line (no "=") with a clear throw, instead of
  an empty key matching every line and rewriting the whole file.

* fix(examples): guard acp-docker env-sync entrypoint against undefined argv[1]

The CLI entrypoint guard called pathToFileURL(process.argv[1]) unconditionally.
process.argv[1] is undefined in some ESM contexts (e.g. importing the module for
its exports via `node --input-type=module -e "import(...)"`), so the guard threw
ERR_INVALID_ARG_TYPE at import, before any exported helper was reachable.

Short-circuit on process.argv[1] before pathToFileURL so importing the module is
side-effect-free while the CLI path is unchanged. Add a regression test that
reproduces the bare-import context and asserts a clean exit.

Addresses the review finding on #1434.

* docs(acp-docker): trim verbose comments per review

Address all-hands-bot's review suggestions on #1434:
- test header describes the current invariant, not the prior-state history
  (that narration belonged in the PR description)
- docker-compose.yml: condense the image-pin comment to the how-to-override;
  the compatibility-floor / #3510 rationale already lives in README §1 + the test
- .env.example: 7-line pin explainer down to 2

Comment-only; env-sync test still 10/10 green, prettier clean.

* Clarify ACP Docker image version guidance

Co-authored-by: openhands <openhands@all-hands.dev>

---------

Co-authored-by: enyst <engel.nyst@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
george larson
2026-06-26 23:21:32 +00:00
committed by GitHub
co-authored by openhands enyst
parent 3520cf1821
commit b2ba5889d3
6 changed files with 278 additions and 25 deletions
+3 -6
View File
@@ -4,12 +4,9 @@
# credentials" step (they ride the conversation start request as secrets).
# Set values here only if you want them baked into the container instead.
# Pin the agent-server image. Default in docker-compose.yml is 1.25.0-python,
# the first release that includes software-agent-sdk#3510 (required — Canvas
# delivers ACP creds as loopback LookupSecrets that only #3510 resolves without
# deadlocking). Bump to a newer release, or a post-#3510 main commit:
# gh api repos/OpenHands/software-agent-sdk/commits/main --jq '.sha[0:7]'
# AGENT_SERVER_IMAGE=ghcr.io/openhands/agent-server:1.25.0-python
# Pin the agent-server image. The compose default is `latest-python`.
# For a reproducible pin (driven by config/defaults.json): npm run example:acp-docker:env
# AGENT_SERVER_IMAGE=ghcr.io/openhands/agent-server:latest-python
# --- Claude Code ---
# A Pro/Max OAuth token, OR an API key. Do NOT set ANTHROPIC_BASE_URL with the
+20 -7
View File
@@ -12,27 +12,40 @@ for the full walkthrough; this is the quick start.
```bash
cd examples/acp-docker
cp .env.example .env # optional — only if baking creds into the container
docker compose up
```
This starts `ghcr.io/openhands/agent-server:1.25.0-python` on
This starts `ghcr.io/openhands/agent-server:latest-python` on
`http://localhost:8010` with a persistent `acp-data` volume. The image
pre-installs the ACP CLI wrappers and the SDK rewrites `npx -y <pkg>` to those
pinned binaries in-pod, so Canvas can keep sending the default `npx` command
unchanged.
> **Minimum version:** `1.25.0-python`. Canvas delivers every ACP credential as
> a loopback `LookupSecret`; only software-agent-sdk#3510 (first released in
> v1.25.0) resolves it off the event loop. An older image deadlocks the first
> ACP turn with `Failed to start ACP server: timed out`.
For a **reproducible, pinned** image, generate `.env` from the repo's single
source of truth (`config/defaults.json`) first — it pins `AGENT_SERVER_IMAGE`
to the exact `versions.agentServer` release, so two people get the same build:
To pin a newer release or a post-#3510 main build:
```bash
npm run example:acp-docker:env # from the repo root; writes examples/acp-docker/.env
cd examples/acp-docker && docker compose up
```
> **Version compatibility.** The common paths keep Canvas and agent-server in
> sync: zero-config Compose uses `latest-python`, while the pinned path reads
> `versions.agentServer` from the same `config/defaults.json` used by the Canvas
> launchers. If you carry an old hand-written `.env` with `AGENT_SERVER_IMAGE`,
> rerun `npm run example:acp-docker:env` or remove that override so the example
> does not stay pinned below `compatibility.minimumAgentServer`.
To pin a newer release or a current main build by hand instead:
```bash
AGENT_SERVER_IMAGE=ghcr.io/openhands/agent-server:$(gh api repos/OpenHands/software-agent-sdk/commits/main --jq '.sha[0:7]')-python docker compose up
```
To bake credentials into the container instead of entering them in Canvas, copy
the env template first: `cp .env.example .env` (optional — see [§3](#3-onboard-with-credentials)).
## 2. Point Canvas at it
```bash
+6 -11
View File
@@ -19,17 +19,12 @@
services:
agent-server:
# Pin to a software-agent-sdk release that includes the containerized ACP
# merges (#1020 file-secret materialisation, #3490 npx→pinned-binary, #3492
# per-conversation data-dir) AND #3510 (ACP cold-start off the event loop),
# which is REQUIRED: Canvas now delivers every ACP credential as a loopback
# LookupSecret, and only #3510 resolves it without self-deadlocking the
# conversation ("Failed to start ACP server: timed out"). #3510 first ships
# in v1.25.0, so 1.25.0-python is the minimum. Override with a newer release
# or a post-#3510 main sha:
# AGENT_SERVER_IMAGE=ghcr.io/openhands/agent-server:<sha>-python docker compose up
# gh api repos/OpenHands/software-agent-sdk/commits/main --jq '.sha[0:7]'
image: ${AGENT_SERVER_IMAGE:-ghcr.io/openhands/agent-server:1.25.0-python}
# Default `latest-python` is always >= the version Canvas requires. To pin a
# reproducible image (driven by config/defaults.json) or override per-run:
# npm run example:acp-docker:env # writes AGENT_SERVER_IMAGE to .env
# (Rationale — the compatibility floor and the #3510 LookupSecret fix — is in
# README.md §1 and the env-sync test.)
image: ${AGENT_SERVER_IMAGE:-ghcr.io/openhands/agent-server:latest-python}
container_name: oh-acp
ports:
# host:container — Canvas points VITE_BACKEND_BASE_URL at http://localhost:8010.