fix(opencode): spawn real node for the bridge, not process.execPath (Bun-runtime fail-close)#427
Merged
lantiscooperdev merged 1 commit intoJun 28, 2026
Conversation
…Bun-runtime fail-close) Under opencode's Bun-compiled runtime, process.execPath is the opencode binary, not node. spawnSync(process.execPath, [bridge.mjs]) made opencode treat the .mjs path as a project dir, fail to cd, exit 0 with EMPTY stdout -> the adapter JSON.parse threw -> 'fail-closed: bridge stdout not valid JSON' on EVERY gated tool call. Enforcement never actually decided; it blocked everything. Root cause confirmed empirically by driving the live opencode TUI. The bridge-proxy and node-import conformance tests masked it: they run under node, where process.execPath IS node. Fix: resolveNodeExe() uses process.execPath only when it is node, else locates node on PATH. Regression: tests/test-opencode-enforcement-live-e2e.mjs drives the ACTUAL opencode binary via tmux and asserts the hook decides (no fail-close) and the write lands. UNGUARDED-IN-CI (real binary + reachable model + TTY). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lantiscooperdev
left a comment
Collaborator
There was a problem hiding this comment.
Bot code-review pass
Scope: the 2-file diff — enforcement.ts (resolveNodeExe) + tests/test-opencode-enforcement-live-e2e.mjs.
Correctness
- ACCEPT —
resolveNodeExegates onbasename(exe)matching^node(\.exe)?$, else resolvesnodeviawhich/where, with a"node"last resort. This addresses the confirmed root cause (process.execPathis the opencode binary, not node, under Bun). The live E2E goes green and the manual before/after (fail-closed → decided + write landed) corroborates. - The per-process cache only short-circuits the default
process.execPathpath; explicit-arg calls bypass it, which keeps the function testable. Good.
Residuals (non-blocking, documentation-tier)
- PATH dependency: if
nodeis not on opencode's spawn PATH, the"node"last-resort ENOENTs →r.error→ throw → fail-closed. Safe (blocks, never opens), but "enforcement actually decides" now requires node on PATH. Worth a one-line runbook note. - Version skew:
which nodetakes the first PATH node, which may differ from a project's intended toolchain node. Low risk for a zero-dep stdlib bridge. - Test approval keypress: the E2E sends
Enterassuming "Allow once" is the default-highlighted permission option. If opencode reorders that menu, the keypress could land elsewhere — but thewrote=trueassertion fails-closed on a wrong approval, so the test stays sound.
Tests
- The actual-terminal E2E is the correct call: the existing
test-opencode-adapter-conformance.mjs/-adapter.mjsrun under node and structurally cannot reproduce this class. SKIP-on-missing-precondition (no binary / model / tmux) is honest, not a false pass.
Verdict: ACCEPT. Real shipped bug, correct minimal fix, real-runtime regression guard. Residuals are doc-tier, not blockers. User approval in the UI satisfies the required-review gate (Rule 17).
lantiscooperdev
approved these changes
Jun 28, 2026
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Fix the opencode enforcement adapter fail-closing on every gated tool call in the real opencode runtime.
The bug
plugins/opencode/capabilities/enforcement.tsspawned the node bridge withspawnSync(process.execPath, [BRIDGE]). Under opencode's Bun-compiled runtime,process.execPathis the opencode binary, not node. opencode treats the.mjspath as a project directory, fails tocdinto it, exits 0 with empty stdout → the adapter'sJSON.parsethrows →"opencode-enforce: fail-closed: bridge stdout not valid JSON"on every gated tool call. The enforcement never actually decides; it blocks everything (the agent cannot run bash or write files at all).Why it shipped green (#424)
Every existing test is a proxy that runs under node, where
process.execPathIS node:test-opencode-adapter-conformance.mjsimports the adapter under node.test-opencode-adapter.mjsdrives the bridge directly under node.The bug only manifests in the real opencode/Bun runtime — the recurring "real-runtime E2E was actually a bridge proxy" gap.
Root cause (empirically reproduced)
Driving the real opencode binary against the deployed bridge:
Fix
resolveNodeExe()— useprocess.execPathonly when its basename isnode; otherwise locatenodeon PATH (which/where), with a"node"last resort. Spawn the resolved node for the bridge.Regression test (actual terminal, not proxy)
tests/test-opencode-enforcement-live-e2e.mjsdrives the actual opencode binary via tmux against a mock project with enforcement installed, then asserts the hook decides (no fail-close) and the write lands. It guards the exactfail-closed|not valid JSONsignature, so it goes red if anyone reverts toprocess.execPath.Observed both states this change:
fail-closed→ write blocked.echo, read_only) → opencode's own permission prompt → write lands (src/x.mjs=BYPASS_OK). Automated test:PASS (noFailClosed=true, wrote=true).UNGUARDED-IN-CI(needs the real binary + a reachable model + a TTY).Deploy
Post-merge, opencode-enforcement projects need a re-install to pick up the fixed adapter:
node install.mjs --tool opencode --install-enforcement --project <dir>.🤖 Generated with Claude Code