Files
OpenHands/examples/acp-docker/docker-compose.yml
T
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

76 lines
3.9 KiB
YAML

# Containerized ACP agent-server for Agent Canvas (agent-canvas#1014).
#
# Brings up the OpenHands Agent Server image — which pre-installs the ACP CLI
# wrappers (claude-agent-acp / codex-acp / gemini) — with a persistent volume so
# conversations and materialised credential files survive restarts. Point Canvas
# at it with VITE_BACKEND_BASE_URL=http://localhost:8010 (the image's CORS allows
# localhost, so the browser talks to it directly).
#
# 1. Copy .env.example to .env and fill in the credentials for the provider(s)
# you want to run (see that file + ../../docs/ACP_AGENTS.md → "Running ACP
# agents in a Docker container").
# 2. `docker compose up` (from this directory)
# 3. Run Canvas pointed at it: VITE_BACKEND_BASE_URL=http://localhost:8010 npm run dev:frontend
#
# The credentials below are read from your shell/.env and handed to Canvas via
# the onboarding "Set up credentials" step; this file only needs them if you
# prefer to bake host logins into the container instead of entering them in the
# UI. Leave them unset to supply everything through Canvas.
services:
agent-server:
# 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.
- "8010:8000"
environment:
# Canvas's start request always references the bundled ``canvas_ui`` tool
# via ``tool_module_qualnames``; the agent-server imports that module from
# OH_EXTRA_PYTHON_PATH at conversation creation. Without the mount below +
# this var, conversation creation fails to import ``canvas_ui_tool``.
- OH_EXTRA_PYTHON_PATH=/canvas-tools
# Optional cipher key. ACP conversations work without it (Canvas sends ACP
# provider credentials as loopback LookupSecrets resolved from the
# agent-server's own secret store, and does NOT flag the request
# secrets_encrypted). Set it to (a) persist saved secrets across
# container restarts and (b) enable the encrypted-settings path used by
# OpenHands-agent (non-ACP) conversations. Generate one with
# `python -c "import secrets;print(secrets.token_urlsafe(32))"`.
# - OH_SECRET_KEY=${OH_SECRET_KEY}
#
# The agent-server's CORS already allows localhost origins, so no extra
# config is needed for the browser to reach it directly.
#
# Optionally bake provider logins into the container instead of entering
# them in the Canvas onboarding step. These are passed through from .env;
# unset values are simply not exported. NOTE: the recommended path is to
# enter credentials in Canvas (they ride the start request as secrets) —
# this is here for non-interactive / CI setups.
- ANTHROPIC_API_KEY
- CLAUDE_CODE_OAUTH_TOKEN
- OPENAI_API_KEY
- GEMINI_API_KEY
- GOOGLE_CLOUD_PROJECT
- GOOGLE_CLOUD_LOCATION
- GOOGLE_GENAI_USE_VERTEXAI
# Set a session key to require auth; mirror it into Canvas via
# VITE_SESSION_API_KEY. Leave unset for an open local backend.
# - SESSION_API_KEY
volumes:
# Persist conversations AND the credential files the SDK materialises
# (Codex auth.json under CODEX_HOME, Gemini ADC/SA JSON) across restarts.
- acp-data:/workspace
# Canvas-specific Python tools (the canvas_ui tool) the agent-server loads
# via OH_EXTRA_PYTHON_PATH. Path is relative to this compose file.
- ../../tools:/canvas-tools:ro
restart: unless-stopped
volumes:
acp-data: