Skip to content

feat(agents): add opencode as a code agent runtime - #3018

Merged
nessie993 merged 4 commits into
mainfrom
feat/opencode-agent-runtime
Aug 13, 2026
Merged

feat(agents): add opencode as a code agent runtime#3018
nessie993 merged 4 commits into
mainfrom
feat/opencode-agent-runtime

Conversation

@nessie993

@nessie993 nessie993 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Adds opencode alongside 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 neededthread_service.rs:zed_agent_server_id() only special-cases "claude"claude-acp and "codex"codex-acp; everything else passes straight through to the matching agent_servers entry, and ExternalAgent::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.
  • CodeAgentRuntimeOpenCode in types; buildCodeAgentConfigFromAssistant case mirroring qwen (everything through the Helix OpenAI-compatible proxy, model stays provider-prefixed because that prefix is what the proxy routes on).
  • settings-sync-daemon emits agent_servers.opencode, passing the whole LLM config in OPENCODE_CONFIG_CONTENT rather than writing a config file — nothing to write to disk, nothing to clean up between agent switches.
  • Frontend: runtime selector in AppSettings / CodingAgentForm / BotRuntimeForm, AgentHarness mark (official SVG from the ACP registry CDN), tooltip label.

Two settings are load-bearing

  • enabled_providers: ["helix"] — the container already exports OPENAI_API_KEY / ANTHROPIC_API_KEY for 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 answer session/request_permission, so the agent would stall on its first edit. This is opencode's equivalent of the --yolo we 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).

  • Validated at save time, in the settings form where a human can act on the error: bare-semver only (the value ends up in an outbound release URL), must be newer than the bundled floor, and must resolve to a real release with a sha256 for both linux arches.
  • The API ships the resolved artifact, not just the number (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).
  • The daemon downloads, verifies the digest, and caches into a host-shared /data/agent-cache mount — 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_servers entry at all — it does not fall back to the bundled binary. It logs and returns nil, the same deferral the claude_code branch 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 because generateAgentServerConfig also 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 persists opencode_version and preserves it on partial updates.
  • pkg/server - runtime-selection coverage includes OpenCode.
  • Required builds passed: go build ./api/pkg/server/ ./api/pkg/store/ ./api/pkg/types/ and cd frontend && yarn build.

Live end-to-end:

  • Rebuilt helix-ubuntu:227cc1 with OpenCode 1.18.18. The binary is root-owned and executable, the agent cache is retro-writable, and the image no longer leaves a root-owned /home/retro/.local from its build-time smoke test.
  • Resumed task spt_01kzx26k7nnwwrpjpxjvh4a2fb with the OpenCode runtime under Zed's ACP client.
  • Zed launched /usr/local/bin/opencode acp, created thread ses_005ce3035ffeqQAxiNg8XyYQey, completed a real tool call reporting 1.18.18, recorded token usage, and completed a second turn after stop/resume.
  • The exact task UI shows the active OpenCode mark and name on composer hover and groups Task Details into labeled Harness, Model (with reasoning), and Compute rows. The running container reports /usr/local/bin/opencode acp, matching Zed's generated agent-server command.

What is NOT verified

  • The version override has not been exercised against a real newer release - 1.18.18 is the newest published version, so ValidateVersion rejects every real value today. Synthetic tarballs exercise the install path.
  • A real newer-version download through the shared host cache has not been exercised, and the cache has no GC yet.
  • The UI model picker and clear-thread lifecycle remain unchecked in the live task.

Note on "opencode v2"

Worth recording because the press coverage is misleading: opencode-ai@latest is 1.18.18; there is no published 2.x. "v2" is the @beta line (0.0.0-beta-*) out of anomalyco/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: identical protocolVersion: 1, identical agentCapabilities, 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

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>

@helixml-bot helixml-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/pkg/types/system_settings.go
Comment thread api/pkg/opencode/opencode.go Outdated
Comment thread api/cmd/settings-sync-daemon/opencode.go

@helixml-bot helixml-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental review — db5b9f6 addresses all findings

All three issues from the initial review are resolved with appropriate test coverage:

  1. store_system_settings.goOpenCodeVersion persistence (was blocking): Fixed. The if req.OpenCodeVersion != nil block is added. Test verifies both the update-and-readback path and partial-update preservation.

  2. opencode.go — thundering herd on cache miss: Fixed with a version-keyed singleflight.Group. Inner function correctly re-checks cache under lock. Regression test fires 20 goroutines and asserts exactly 1 upstream request.

  3. opencode.go (daemon) — extractOpenCodeBinary accepting any regular file: Fixed. Now requires filepath.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

@helixml-bot helixml-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Optional showHarness prop, threaded through from SpecTaskDetailContent.
  • 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.

@helixml-bot helixml-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@nessie993
nessie993 merged commit bcd3cba into main Aug 13, 2026
1 check passed
@nessie993
nessie993 deleted the feat/opencode-agent-runtime branch August 13, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants