Skip to content

feat(setup): bundle the OpenCode CLI as a managed runtime binary - #1002

Merged
Juliusolsson05 merged 5 commits into
mainfrom
feat/opencode-bundled-runtime
Sep 19, 2026
Merged

Juliusolsson05 merged 5 commits into
mainfrom
feat/opencode-bundled-runtime

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Problem

A fresh install of Agent Code cannot use OpenCode panes unless the opencode CLI is on PATH — but the binary typically lives at ~/.opencode/bin, added by a shell rc a Finder-launched app never sources (adjacent fallout observed in #993). The bundling pipeline already ships tmux/mitmproxy/cloudflared; OpenCode is the fourth tool and the first provider CLI.

Implemented behavior

  • Pin (feat(setup): bundle the OpenCode CLI as a managed runtime binary #994): third_party/opencode holds the manifest pinning upstream anomalyco/opencode v1.18.31 — darwin-arm64 (opencode-darwin-arm64.zip) and darwin-x86_64 (opencode-darwin-x64-baseline.zip, so one artifact covers every Intel Mac with no AVX2 assumption). Each entry pins archive sha256 + bytes + extracted binary sha256. No binary ever enters git; version bumps stay one-file manifest PRs.
  • Fetch/verify: fetch-opencode.mjs downloads, double-hash-verifies (archive, then extracted binary), extracts via /usr/bin/unzip, and smokes --version natively; verify-opencode.mjs --strict re-checks hashes for every arch plus a native otool -L no-Homebrew gate (upstream links only system dylibs). Wired into runtime:prepare:mac and the runtime:verify aggregate; staged by copy-packaged-resources.mjs under the existing asarUnpack glob.
  • Resolution: runtimeTools.ts gains an opencode resolver (tmux shape: find + chmod + real-exec probe). toolchain.ts layers it in front of cached/PATH lookups at the getToolPath choke point — every transcriptEngine call site and version probe gets bundled precedence with zero per-site edits. A valid manual override still beats the bundle; a dead one falls through. The override lives outside cachedPaths so PATH revalidation can never clobber the shipped binary, and dev builds without a staged cache keep today's behavior.
  • Setup gate: the OpenCode row reports found: true, source: 'bundled', no install prompt when the artifact ships (mitmdump precedent: bundled > PATH).
  • Update-flow awareness: opencode stays out of the CLI auto-update union — the bundled version moves with app updates via the manifest pin; the type note in cliUpdate.ts records why.

Design decisions and tradeoffs

  • Release zip instead of npm opencode-ai: per-asset checksum pinning, no postinstall indirection, identical lifecycle to the other bundled tools.
  • Bundled > PATH at spawn (not just display): the shipped version is the one we tested; a stale PATH install must not shadow it.
  • Manual override > bundled: the user's explicit word beats every probe — same policy as the gate.

Issues

Fixes #994

Plan: docs/superpowers/plans/2026-09-18-opencode-bundled-runtime.md (first commit on the branch)

Tests and verification

  • TDD red→green: toolchain.bundledOverride.test.ts — bundled beats cached PATH path; valid manual override wins (and skips the bundled resolve entirely); dead override falls back to bundled; null resolver keeps today's behavior.
  • npm run runtime:fetch:opencode -- --all + verify-opencode.mjs --strict — green against the real 46+48 MB assets (native arm64: hash + --version + linkage; x86_64: cross-arch hash).
  • npm run typecheck — clean. Setup + providerSwitch suites — 78/78.
  • npm run build:app — exit 0; staged out/main/runtime/opencode/{manifest.json, darwin-arm64/opencode, darwin-x86_64/opencode}; staged arm64 binary sha256 16c960ba… matches the manifest pin (the asarUnpack swap is the shared, tmux-proven path).
  • git status clean of binaries — cache/ ignored, manifest is the source of truth.

Known limitations / risks

  • darwin-only manifests (Linux/Windows opencode assets exist upstream; getPlatformKey returns null off-darwin, PATH fallback applies).
  • ~144 MB uncompressed per arch inside the app bundle — the cost of a self-contained CLI; acceptable for the beta, worth watching in DMG size.
  • The bundled version only moves when we bump the manifest pin; release cadence should include an opencode version check.

Adds the fourth managed runtime artifact alongside tmux, mitmproxy, and
cloudflared: the upstream anomalyco/opencode release zip, hash-pinned
per arch (archive sha256 + bytes + extracted binary sha256) in
third_party/opencode/manifest.json. Binaries never enter git; the
manifest plus fetch-opencode.mjs is the source of truth and version
bumps stay one-file PRs.

Darwin x86_64 pins the -baseline asset so one binary covers every
Intel Mac with no AVX2 assumption. fetch verifies the archive hash,
byte size, AND the extracted binary's hash before committing it to the
cache, then smokes the version natively; verify re-checks hashes for
every arch plus a no-Homebrew otool gate natively (upstream links only
system dylibs). runtime:prepare:mac and the runtime:verify aggregate
now include opencode, and copy-packaged-resources stages it under
out/main/runtime/opencode/ for the existing asarUnpack glob.

Refs #994
OpenCode panes on a fresh install need the CLI on a PATH the app never
sees — Finder-launched processes inherit no shell rc, so ~/.opencode/bin
is invisible exactly where it matters (#994; adjacent fallout in #993).
The binary now ships (previous commit), so this wires the resolution:

runtimeTools grows an opencode resolver in the proven tmux shape (find +
chmod + real-exec probe, --version); toolchain layers it in front of
cached/PATH lookups at the getToolPath choke point so all four
transcriptEngine call sites and every version probe get bundled
precedence without per-site edits. A valid manual override still beats
the bundle — the user's explicit word beats every probe, the same policy
the setup gate applies — and the override lives outside cachedPaths so
PATH revalidation can never clobber the shipped binary. The gate's
opencode row now clears as found with source 'bundled' instead of
nagging for a PATH binary. cliUpdate's type note records that the
bundled version moves with app updates, so it stays out of the npm
auto-update union.

Fixes #994
The #994 manifest ships 1.18.31 while the accepted record still said
1.18.30, and the file's own rule is that acceptance moves only after a
compatibility review. The review ran 2026-09-19: deterministic suites
green, hash-verified official asset, production panes on 1.18.31 since
Sep 14. The live suite could not discriminate the two versions (it
fails identically on both, including the recorded 1.18.30 — #1009), so
the acceptance note records that basis honestly instead of implying a
passing live check. npm latest is 1.18.31, which also clears the
upstream-watch drift.

Refs #994, Refs #1009
@Juliusolsson05

Copy link
Copy Markdown
Owner Author

Detailed review (pre-merge)

Verdict: approve. Architecture, security, and conventions all check out:

  • Manifest discipline — third_party/opencode follows the AGENTS.md shape exactly (manifest + README + LICENSE + .gitignore, no binaries in git; version bumps stay one-file PRs).
  • Fetch/verify chain — streaming sha256 over the archive, byte-size check, staging-dir extraction, extracted-binary hash verified before it becomes the cache, native-arch --version sanity, cache-hit by content hash rather than version output. verify-opencode.mjs adds the cross-arch-safe hash layer plus the otool -L no-Homebrew gate.
  • Precedence — bundled-over-PATH at the single getToolPath choke point, valid manual override still wins, override kept out of cachedPaths so revalidation can't clobber the shipped binary nor pollute child PATH. Tests pin all four precedence cases.
  • Pipeline — release.yml already runs runtime:prepare:mac, which now fetches+verifies opencode for both arches.

One finding, resolved in-branch: the pin (1.18.31) disagreed with support/upstream-versions.json (accepted: 1.18.30), whose own rule says acceptance moves only after a compatibility review. The review ran today:

  • Deterministic suites green, asset hash-verified, and production evidence (this app has driven 1.18.31 TUI panes daily since Sep 14).
  • The recorded live suite could not discriminate: it times out waiting for the permission condition identically on 1.18.31 and on a fresh 1.18.30 download — the same version its Stage 0 recordings passed against — so that failure is environmental, not a pin regression. Filed as bug(testing): opencode-terminal live suite times out waiting for the permission condition on every version #1009; the free model replies fine via opencode run, so the drift is in the tool-call/permission scenario.
  • 80ceedd bumps accepted to 1.18.31 with that basis recorded honestly in the notes.

Merging once CI on 80ceedd is green.

@Juliusolsson05
Juliusolsson05 merged commit 1e9ba1c into main Sep 19, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the feat/opencode-bundled-runtime branch September 19, 2026 02:31
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.

feat(setup): bundle the OpenCode CLI as a managed runtime binary

1 participant