feat(agents): add opencode as a code agent runtime - #3018
Conversation
opencode ships a first-class ACP agent (`opencode acp`), so it slots in next to codex/claude/qwen/goose with no Zed changes: thread_service.rs only special-cases "claude" and "codex", and everything else passes through to the matching agent_servers entry. Phase 1 — runtime: - Bake the binary in Dockerfile.ubuntu-helix (sha256-pinned, both arches) and record the version at /opt/helix/opencode.version. - settings-sync-daemon emits agent_servers.opencode, passing the whole LLM config in OPENCODE_CONFIG_CONTENT rather than writing a file. Two settings are load-bearing: enabled_providers pins opencode to the single Helix-proxied provider (the container also exports OPENAI_API_KEY/ANTHROPIC_API_KEY, which opencode would otherwise auto-register, routing around the proxy and billing), and permission:"allow" stops the agent stalling on a permission prompt nobody can answer in a headless sandbox. Phase 1b — admin version override: - New system setting opencode_version pins a newer release without an image rebuild. Validated and resolved to per-arch URLs + digests when the admin saves it, so a typo or unreachable index fails in the settings form rather than later as sessions with no agent. - The daemon downloads, verifies the sha256 and caches into a host-shared mount so one download serves every container on the host. If a pinned release cannot be installed it emits no agent_servers entry at all: silently running the bundled build would leave an admin believing a rollout had landed. Verified: 18 unit tests plus a livetest-tagged test that drives a real `opencode acp` process with the generated config and asserts the handshake succeeds and exactly one model is offered. The Dockerfile block was built standalone. NOT yet run end-to-end in a live sandbox — see design/2026-08-13-opencode-agent-runtime.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Review — PR #3018: feat(agents): add opencode as a code agent runtime
Well-structured PR that follows the established pattern (goose_code) for adding a new code agent runtime. The Dockerfile stage, daemon config, provider gating, and frontend wiring are all solid. The design doc and test suite are thorough. One blocking issue below.
Issues
| # | Severity | File | Description |
|---|---|---|---|
| 1 | High — blocking | api/pkg/store/store_system_settings.go (missing) |
OpenCodeVersion is never persisted |
| 2 | Low | api/pkg/opencode/opencode.go |
Resolver.Resolve stampede on cache miss |
| 3 | Low | api/cmd/settings-sync-daemon/opencode.go |
extractOpenCodeBinary trusts the first regular file |
Verdict: Request Changes
Issue #1 is a silent data-loss bug that makes the entire admin version-override feature inoperative. The other two are minor.
There was a problem hiding this comment.
Incremental review — db5b9f6 addresses all findings
All three issues from the initial review are resolved with appropriate test coverage:
-
store_system_settings.go—OpenCodeVersionpersistence (was blocking): Fixed. Theif req.OpenCodeVersion != nilblock is added. Test verifies both the update-and-readback path and partial-update preservation. -
opencode.go— thundering herd on cache miss: Fixed with a version-keyedsingleflight.Group. Inner function correctly re-checks cache under lock. Regression test fires 20 goroutines and asserts exactly 1 upstream request. -
opencode.go(daemon) —extractOpenCodeBinaryaccepting any regular file: Fixed. Now requiresfilepath.Base == "opencode", rejects unexpected files and duplicates. Test validates with a README-only archive.
Bonus fix: the Dockerfile smoke check (opencode --version) now runs in an isolated temp home, preventing root-owned state under /home/retro/.local — a real issue caught during the live sandbox test.
Design doc updated with live verification results. No new issues introduced.
Verdict: Approve
There was a problem hiding this comment.
Incremental review of new commit 16858753 — UI polish: tooltip on the model picker showing the active harness mark, and inline harness badge in Task Details.
Clean, low-risk changes:
SpecTaskModelPicker.tsx: Tooltip wraps button in<Box component="span">— correct MUI pattern for disabled-button tooltips.SpecTaskExecutionControls.tsx: OptionalshowHarnessprop, threaded through fromSpecTaskDetailContent.- Two well-structured tests covering both the tooltip and inline harness rendering.
- Design doc updates describing the new surface and live verification.
No correctness, security, performance, or coverage concerns. Approval reaffirmed.
There was a problem hiding this comment.
Incremental review of 8a55fb4f — UI-only refactor grouping execution details into labeled rows.
Clean extraction of modelControl, reasoningControl, computeControl into shared variables used by both grouped (CSS grid with Harness/Model/Compute labels) and non-grouped (horizontal stack) layouts. No correctness, security, or performance issues. Tests updated to assert the new structure. LGTM.
Adds
opencodealongside codex / claude / qwen / goose as a selectable code agent runtime, plus an admin setting to roll forward to a newer opencode release without rebuilding the desktop image.Why this is a small change
opencode ships a first-class ACP agent (
opencode acp) and is a registered ACP-registry agent. No Zed changes are needed —thread_service.rs:zed_agent_server_id()only special-cases"claude"→claude-acpand"codex"→codex-acp; everything else passes straight through to the matchingagent_serversentry, andExternalAgent::Custom { name }is fully generic.Structurally this is the goose_code change (
55c6616c2) minus the Rust build stage — opencode ships prebuilt single-file binaries.Phase 1 — the runtime
Dockerfile.ubuntu-helix: sha256-pinned fetch for amd64 + arm64, installed root-owned so the agent user cannot overwrite its own runtime. Version recorded at/opt/helix/opencode.version.CodeAgentRuntimeOpenCodein types;buildCodeAgentConfigFromAssistantcase mirroring qwen (everything through the Helix OpenAI-compatible proxy, model stays provider-prefixed because that prefix is what the proxy routes on).settings-sync-daemonemitsagent_servers.opencode, passing the whole LLM config inOPENCODE_CONFIG_CONTENTrather than writing a config file — nothing to write to disk, nothing to clean up between agent switches.AppSettings/CodingAgentForm/BotRuntimeForm,AgentHarnessmark (official SVG from the ACP registry CDN), tooltip label.Two settings are load-bearing
enabled_providers: ["helix"]— the container already exportsOPENAI_API_KEY/ANTHROPIC_API_KEYfor other runtimes. Without this gate opencode auto-registers them as direct providers (plus its own free "Zen" models) and a user can select a model that bypasses the Helix proxy and billing entirely.permission: "allow"— a headless sandbox has nobody to answersession/request_permission, so the agent would stall on its first edit. This is opencode's equivalent of the--yolowe pass to qwen.Both have a dedicated test whose failure message explains the consequence.
Phase 1b — admin version override
New system setting
opencode_version(blank = use the bundled build).CodeAgentBinary{Version, Artifacts{arch → {URL, SHA256}}}), so the container never learns the release URL scheme and an air-gapped mirror is a one-place change (HELIX_OPENCODE_RELEASES_URL)./data/agent-cachemount — the archive is ~60MB, so without sharing every container on the host would re-download the same version. Writes are atomic (temp + rename), so a torn download can never be executed and concurrent containers cannot read each other's partial file.The one judgement call
On install failure the daemon emits no
agent_serversentry at all — it does not fall back to the bundled binary. It logs and returnsnil, the same deferral theclaude_codebranch already uses while waiting for credentials, so the daemon retries on the next poll and a transient outage self-heals. Silently running the bundled build would leave an admin believing a rollout had landed when it had not. Retries are throttled to one per poll interval becausegenerateAgentServerConfigalso runs on file-watcher events.Verification
Automated (all passing):
pkg/opencode(8 top-level tests) - semver validation and URL-safety checks; both-architecture resolution; missing-artifact/digest rejection; 404 handling; response caching; and a 20-caller cache-miss concurrency test asserting one upstream request.cmd/settings-sync-daemon(13 OpenCode tests) - baked binary config, provider/model/permission gates, install and digest verification, strict archive-member validation, atomic cache behavior, failure/retry behavior, and pin-equals-baked handling.pkg/store- the system-settings update persistsopencode_versionand preserves it on partial updates.pkg/server- runtime-selection coverage includes OpenCode.go build ./api/pkg/server/ ./api/pkg/store/ ./api/pkg/types/andcd frontend && yarn build.Live end-to-end:
helix-ubuntu:227cc1with OpenCode 1.18.18. The binary is root-owned and executable, the agent cache isretro-writable, and the image no longer leaves a root-owned/home/retro/.localfrom its build-time smoke test.spt_01kzx26k7nnwwrpjpxjvh4a2fbwith the OpenCode runtime under Zed's ACP client./usr/local/bin/opencode acp, created threadses_005ce3035ffeqQAxiNg8XyYQey, completed a real tool call reporting 1.18.18, recorded token usage, and completed a second turn after stop/resume./usr/local/bin/opencode acp, matching Zed's generated agent-server command.What is NOT verified
ValidateVersionrejects every real value today. Synthetic tarballs exercise the install path.Note on "opencode v2"
Worth recording because the press coverage is misleading:
opencode-ai@latestis 1.18.18; there is no published 2.x. "v2" is the@betaline (0.0.0-beta-*) out ofanomalyco/opencode-beta, and it is a runtime rewrite (Bun→Node, Tauri→Electron desktop, parallel session tabs), not an agent-protocol rewrite. I ran the same ACP probe against 1.18.18 and the v2 beta: identicalprotocolVersion: 1, identicalagentCapabilities, same entrypoint, same config format. v2 does not change this integration surface.Design doc:
design/2026-08-13-opencode-agent-runtime.md🤖 Generated with Claude Code