OpenHands / OpenHands/software-agent-sdk
[Epic] Built-in ACP harness parity: Kimi, OpenCode, Pi, Hermes, Antigravity
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
Goal
Ship Kimi, OpenCode, Pi, Hermes and Antigravity as built-in ACP providers with the same features, capabilities and implementation patterns as claude-code, codex and gemini-cli — not as one-off partial additions.
Status (2026-09-04): 2 of 4 landed. Kimi (#4714) and Pi (#4419) merged 2026-09-03; OpenCode is open as #4827; Antigravity is deferred as a watch item (#4624), and #4823 is closed with it. Each was originally scoped independently, so "supported" meant something different for each — this issue defines a single bar and tracks all four against it.
Harnesses
| Harness | Issue | PR in flight | Distribution | Blocked by |
|---|---|---|---|---|
| Kimi | #4714 (@ysntony) merged | @moonshot-ai/kimi-code (npm) |
done | |
| Pi | #4635 | #4419 (@Deep070203) merged | pi-acp + @earendil-works/pi-coding-agent (npm) |
SDK done; eval wiring open |
| OpenCode | #4639 | #4827 open | opencode-ai (npm; per-platform compiled binaries, ~185 MB linux) |
in review |
| Hermes | #4634 | — | git+NousResearch/hermes-agent@<tag> (Python, via uvx) |
needs a catalog entry for the non-npm shape |
| Antigravity | #4624 | — | binary — 650.4 MiB from dl.google.com |
⏸️ watch item, unscheduled |
Distribution decides how much of this applies
#4805 changed the economics for npm providers: a provider absent from the image is no longer broken, because resolve_acp_command() leaves the version-pinned npx -y <pkg>@<ver> invocation in place, and it installs on first use and caches under ~/.openhands. So an npm provider costs nothing in image size unless deliberately added to the preinstall list.
[!IMPORTANT]
Registry membership is not image membership (constraint carried from #4643). Registering a provider makes it selectable; it must not put it in the image. Joining the defaultINSTALL_ACP_PROVIDERS=claude-code,codex,gemini-cligrows every published image for every user and is a separate, deliberate decision per provider — the default for a new npm harness is registry-only.
That escape hatch is npm-only, and Antigravity is not npm. It is first-party Google, listed in the canonical ACP registry ("authors": ["Google LLC"]), but its distribution block contains only binary: five platform archives from dl.google.com, cmd: ./agy_acp_server.par. There is no download-on-demand path for that shape — _prefer_pinned_binary() returns the command unchanged and the launch simply fails if the binary is absent. So Antigravity is preinstalled or unavailable, and preinstalling means ~518 MiB on every image, against the effort in #4643 that just removed 324 MB.
Antigravity is now a watch item, not scheduled (#4624, notice added 2026-09-04). The artifact grew to 650.4 MiB (registry v1.1.1, unpacking to ~1.9 GiB), so it would need both a non-npm install mechanism and a dedicated image variant — "neither is justified without demand." #4823, which specced that mechanism, is closed as a watch item for the same reason: with Antigravity deferred and Hermes needing only a small piece of it — a registry entry without an npm catalog entry, now a P0 on #4634 — the full runtime-fetch scope had no consumer. Its reduced design is recorded there — a preinstall-only recipe needing zero SDK runtime changes, roughly a week — so nothing is re-derived if a gate trips.
So this epic is three harnesses in practice: Kimi and Pi merged, OpenCode in review. Antigravity is tracked, not queued.
[!NOTE]
Beware a name collision on this one: npmantigravity-acpis a 184 KB third-party wrapper published by an individual, not Google's server. Verify by thedl.google.comURL, never by package name.
The parity contract
A harness is done when it does everything the three reference providers do. Every item is a real touchpoint in the current code.
1. Registry record — openhands-sdk/openhands/sdk/settings/acp_providers.py, the single source of truth:
| Field | What it must specify |
|---|---|
key / display_name |
settings discriminator + UI label |
default_command |
npx -y --prefer-offline <pkg>@<VERSION>; may carry trailing args |
binary_name |
wrapper binary _prefer_pinned_binary() looks for; without it the provider always uses npx |
api_key_env_var / base_url_env_var |
API key env var; proxy/base-URL routing var, or None |
data_dir_env_var |
env var pointed at a per-conversation dir so credentials/sessions/caches don't leak between runs |
default_session_mode |
the mode ID that disables permission prompts |
agent_name_patterns |
lowercase substrings for runtime auto-detection |
supports_set_session_model |
whether the initial model is applied via a protocol call |
supports_runtime_model_switch |
whether mid-conversation model switching works |
session_meta_key |
top-level _meta key for model selection, or None |
available_models / default_model |
curated model-picker list and preselection |
file_secrets |
ACPFileSecretSpec if the CLI authenticates from a file on disk rather than an env var |
The two supports_* flags and file_secrets are where partial additions usually stop. A provider that registers but reports supports_runtime_model_switch=False when the server actually supports it silently loses a user-visible feature — determine each flag from the server's real behaviour, don't copy a neighbour's value.
2. Version constant — <PROVIDER>_VERSION, alongside CLAUDE_AGENT_ACP_VERSION / CODEX_ACP_VERSION / GEMINI_CLI_VERSION.
3. Pins — nothing to add per provider any more. ✅ Resolved by #4832. ACP_INSTALL_CATALOG (acp_install_catalog.py) is now the single source of truth for every pinned npm package; the registry derives default_command/binary_name from it, and the Dockerfile renders its install plan from it rather than carrying hardcoded case arms. _REGISTRY_ACP_PACKAGES and the fail-open Dockerfile-regex test were deleted, not hardened — the drift they failed to catch is no longer representable. --prefer-offline is centralised there too, so no provider can omit it.
Adding an npm provider is now a catalog edit. Two further gates work unchanged: tests/sdk/settings/test_acp_providers.py asserts the registry key set exactly, so a registry addition cannot be silent; and check-acp-drift.py compares the registry to the TS mirror field-for-field.
4. TypeScript mirror — clients/typescript/src/models/acp-providers.json is hand-maintained and enforced by clients/typescript/scripts/check-acp-drift.py. CI fails if you skip it.
5. Dockerfile — only if preinstalling. A case branch (package@version + wrapper bin) plus the expected one of: error text, in openhands-agent-server/openhands/agent_server/docker/Dockerfile. Note the default list is also hardcoded in .github/workflows/server.yml and asserted verbatim by a test.
6. Tests — the paths that already cover the three reference providers: tests/sdk/agent/test_acp_agent.py, tests/agent_server/test_credential_binding.py, test_settings_router.py, test_conversation_info_model.py. Also ACPServerKind in openhands-sdk/openhands/sdk/settings/model.py.
7. Live verification — a real ACP turn against the actual server: session created, a tool call executed, a reply returned. The registry record is a claim about a server's behaviour, and only a live run substantiates it.
What the existing providers actually set
The contract above is abstract; this is the concrete shape, read from the registry on main.
| Field | claude-code |
codex |
gemini-cli |
|---|---|---|---|
display_name |
Claude Code | Codex | Gemini CLI |
default_command |
npx -y --prefer-offline @agentclientprotocol/claude-agent-acp@0.63.0 |
npx -y --prefer-offline @agentclientprotocol/codex-acp@1.1.7 |
npx -y --prefer-offline @google/gemini-cli@0.46.0 --acp |
binary_name |
claude-agent-acp |
codex-acp |
gemini |
api_key_env_var |
ANTHROPIC_API_KEY |
OPENAI_API_KEY |
GEMINI_API_KEY |
base_url_env_var |
ANTHROPIC_BASE_URL |
OPENAI_BASE_URL |
GEMINI_BASE_URL |
data_dir_env_var |
CLAUDE_CONFIG_DIR |
CODEX_HOME |
HOME |
default_session_mode |
bypassPermissions |
agent-full-access |
default |
agent_name_patterns |
["claude-agent"] |
["codex-acp"] |
["gemini-cli"] |
supports_set_session_model |
True |
True |
True |
supports_runtime_model_switch |
True |
True |
True |
session_meta_key |
"claudeCode" |
None |
None |
default_model |
opus[1m] |
gpt-5.5 |
auto |
available_models |
5 | 7 | 7 |
file_secrets |
none | CODEX_AUTH_JSON |
GOOGLE_APPLICATION_CREDENTIALS_JSON |
What to take from it
- All three set both
supports_*flags toTrue. That makesFalsethe value that needs justifying: it should mean this server genuinely cannot do it, verified against the running server, not not implemented yet. session_meta_keyis the exception, not the rule — onlyclaude-codesets one, becauseclaude-agent-acpignores session-_metamodel selection and needs the model pushed via a protocol call. Default toNoneunless the server actually reads a_metakey.data_dir_env_varis not always a dedicated variable.gemini-cliuses plainHOME, because the CLI has no config-dir override — so isolation for it means relocatingHOME. If a harness only respectsHOME, say so explicitly on its issue.default_commandmay carry arguments, not just a package:gemini-cliappends--acp.binary_nameis easy to omit and quietly costly — without it the provider always pays thenpxpath even when the pinned binary is installed.
file_secrets, if the CLI authenticates from a file
// codex — env var points at the DIRECTORY containing auth.json
{ "secret_name": "CODEX_AUTH_JSON", "filename": "auth.json",
"env_var": "CODEX_HOME", "subdir": "codex",
"env_points_to": "dir", "warn_if_unset": [] }
// gemini-cli — env var points at the FILE itself
{ "secret_name": "GOOGLE_APPLICATION_CREDENTIALS_JSON", "filename": "gcloud-credentials.json",
"env_var": "GOOGLE_APPLICATION_CREDENTIALS", "subdir": "gemini-cli",
"env_points_to": "file", "warn_if_unset": ["GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_LOCATION"] }
env_points_to distinguishes the two shapes and is the field most likely to be set wrong — Codex wants a directory, Gemini wants the file. warn_if_unset names companion variables that are useless alone. The SDK owns the mechanism — materialising the file under the conversation's durable root, seed-if-absent, setting the env var; the spec is only policy.
Relationship to other tracked work
#4823—install_methodfor the provider registry. Closed as a watch item; reopen if #4624's gate 1 or 3 trips (gate 2 would obviate it).- #4830 — ACP CI hardening backlog (intra-SDK): conformance, model-acceptance and pin-installability probes. #4832 and #4834 landed its first block. Its measured baseline also found live drift in the existing records —
_CODEX_MODELSlists agpt-5.6the server rejects,_GEMINI_MODELSlists an absentgemini-2.5-flashand omits a livegemini-3.5-flash. - #4833 — what each consuming repo (benchmarks, evaluation, canvas, enterprise) must assert against the registry.
- OpenHands/automation#414 — weekly automation to detect harness drift. Adding four providers roughly doubles a maintenance surface nothing currently watches: all three existing pins are 12–14 stable releases behind (measured 2026-09-02), against ~78 upstream releases per 90 days. No dependency tooling can see these pins — they are Python constants and Dockerfile shell strings — and drift is behavioural as well as numeric (gemini-cli ≥0.43 broke the
set_session_modedefault we had recorded). Worth having before the count grows. - #4627 — [Harness Watch] Epic: automated paired ACP harness comparison. Overlapping but distinct: that epic needs OpenCode/Pi/Hermes working well enough to measure, this one needs providers shipped as product. #4635 and #4639 are shared children carrying both sets of criteria.
- #4643 — image reorganisation. Source of the registry-vs-image constraint above, and of the #4805 fallback this depends on.
- OpenHands/enterprise#235 — the user-facing half of Antigravity (frontend secrets, branding, analytics).
[!NOTE]
Hermes (#4634) belongs here too. It is a built-in provider like the others — usable by OSS users in Canvas, and kept out of the defaultINSTALL_ACP_PROVIDERSso it adds no image weight. The difference is only the install mechanism: it is a Python project installed from a pinned git tag, souvx(already in the image) stands in fornpx. That needs a small catalog change, tracked on #4634.
Definition of done
All four harnesses satisfy every item in the parity contract, each backed by a live ACP turn rather than a registry entry alone; no harness is in the default preinstall list without an explicit, recorded decision to put it there.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with openhands-sdk/openhands/sdk/settings/acp_providers.py and acp_install_catalog.py, then compare the TypeScript mirror at clients/typescript/src/models/acp-providers.json and run the ACP settings tests. Check the referenced provider issues and live ACP servers before choosing a remaining harness; completion requires registry parity, synchronized metadata, passing tests, and a verified ACP turn.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, python, typescript
- Domain
- backend-api-design, build-system, devtools
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100