Files
b2ba5889d3 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>
2026-06-26 23:21:32 +00:00

4.4 KiB
Raw Permalink Blame History

Containerized ACP agent-server for Agent Canvas

Run an ACP agent (Codex / Claude Code / Gemini CLI) against a containerized Agent Server and drive it from Agent Canvas, with credentials supplied through the Canvas UI. This is the local-Docker counterpart of the cloud path — a fresh container has no host CLI login, so credentials come from you instead.

See ../../docs/ACP_AGENTS.md for the full walkthrough; this is the quick start.

1. Bring up the agent-server

cd examples/acp-docker
docker compose up

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.

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:

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:

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).

2. Point Canvas at it

cd ../..                      # repo root
VITE_BACKEND_BASE_URL=http://localhost:8010 npm run dev:frontend

The image's CORS allows localhost, so the browser talks to the container directly. (You can also add it as a backend in the Canvas backend selector with host http://localhost:8010.)

3. Onboard with credentials

Pick the ACP provider in onboarding and fill in the Set up credentials step. On a containerized backend this step is required (there's no host login to fall back on):

Provider What to paste
Codex (subscription) CODEX_AUTH_JSON — the full contents of ~/.codex/auth.json
Claude Code (subscription) CLAUDE_CODE_OAUTH_TOKEN — your Pro/Max OAuth token
Gemini CLI (Vertex) GOOGLE_APPLICATION_CREDENTIALS_JSON (SA / ADC JSON) + GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_LOCATION + GOOGLE_GENAI_USE_VERTEXAI=true

Each provider also accepts an API-key path (OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY). Canvas saves these to the agent-server's secret store and the start request references them as LookupSecrets; the SDK resolves each value at spawn time (off the event loop, per #3510), materialises the *_JSON blobs to disk, and points the CLI's data-dir env at them automatically.

⚠️ Do not set ANTHROPIC_BASE_URL with the Claude OAuth token. An inherited LiteLLM base URL silently breaks bearer auth. Canvas never sets it for you, but a saved ANTHROPIC_BASE_URL secret rides along on every start request — the credential form warns about the pair.

⚠️ Gemini Vertex ADC must be freshly logged in. Run gcloud auth application-default login — a stale token returns invalid_rapt.

ℹ️ Baked creds in .env may not satisfy the onboarding gate. The login probe checks CLI login state (claude auth status / codex login status / Gemini's OAuth credentials file), not container env vars — a container with only e.g. GEMINI_API_KEY baked via .env typically still probes as logged-out, and the credentials step then blocks "Next". Enter (or re-enter) a credential in the UI to proceed; the baked env var still works for the agent itself.

Tear down

docker compose down           # keep the volume
docker compose down -v        # also drop credentials/conversations