headroom/plugins
Tejas Chopra 303e0522c4
fix(opencode): don't preload a missing transport shim into child processes (#2806)
## Description

`headroom wrap opencode` broke third-party MCP servers in pip/wheel
installs. The wrap transport plugin appended
`NODE_OPTIONS=--import=<plugin dir>/../hook-shim/handler.js` to its own
env (and injected it into every child it spawns), but that path only
resolves in a repo checkout. Wheel installs load the standalone bundle
from `headroom/providers/opencode/_dist/`, which has no `hook-shim/`
sibling — the shim lives under `plugins/` and maturin only ships files
under `headroom/` (pyproject.toml `python-source`/package-dir behavior).

Every Node child then aborted with `ERR_MODULE_NOT_FOUND` before
executing a line, including OpenCode's stdio MCP servers. OpenCode
reports that as `<server> MCP error -32000: Connection closed`.
Headroom's own MCP server is a Python process, so it stayed connected —
which is why the breakage looked selective, and why nothing appeared in
the proxy logs (the failure is entirely inside OpenCode's child
process). Docker and `--no-proxy` are incidental: the plugin installs
the transport on load in every wrap mode.

Fix: resolve the shim only when it exists on disk, and skip the
`NODE_OPTIONS` mutation otherwise. Children go direct instead of dying.
Checkout builds still get child-process transport hooking, unchanged.

Closes #2798

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- `plugins/opencode/src/transport.ts`: `shimImportSpecifier()` returns
`string | undefined`, gated on `fs.existsSync`; `installProcessEnv()`
and `withShimEnv()` leave `NODE_OPTIONS` untouched when the shim is
absent.
- `plugins/opencode/src/transport.test.ts`: new regression test — with
the shim missing, the parent's `NODE_OPTIONS` is unmodified and a
spawned `npx -y firecrawl-mcp` receives no `--import`.
- `headroom/providers/opencode/_dist/entry.opencode.js`: regenerated via
`npm run build:standalone` (the bundle that wheel installs actually
load).

## Testing

- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

No Python source changed, so `pytest` / `ruff` / `mypy` are N/A here;
the TypeScript equivalents were run instead.

### Test Output

```text
$ npm run typecheck
> tsc --noEmit
(no output)

$ npm test
 RUN  v4.1.9 /private/tmp/hr-pr-2798/plugins/opencode
 Test Files  2 passed (2)
      Tests  14 passed (14)
   Duration  416ms

# The new test is not vacuous — reverting the guard to `return shim.href` reddens it:
$ npx vitest run -t "#2798"
 Test Files  1 failed | 1 skipped (2)
      Tests  1 failed | 13 skipped (14)
```

## Real Behavior Proof

- Environment: macOS (darwin 25.4.0), Node v24, Bun present; both
bundles loaded directly from disk.
- Exact command / steps: load each built bundle, invoke the default
plugin export, print `process.env.NODE_OPTIONS`, then
`spawnSync(process.execPath, ["-e", "console.log('mcp server handshake
ok')"])` — the same way OpenCode launches a stdio MCP server.

```text
### BEFORE (wheel layout, shim missing) ###
NODE_OPTIONS: "--import=file:///…/headroom/providers/opencode/hook-shim/handler.js"
child: Error [ERR_MODULE_NOT_FOUND]: Cannot find module
  '…/headroom/providers/opencode/hook-shim/handler.js'   <-- becomes MCP -32000

### AFTER — wheel layout (headroom/providers/opencode/_dist/) ###
NODE_OPTIONS after plugin load: undefined
child status: 0 | stdout: mcp server handshake ok

### AFTER — checkout layout (plugins/opencode/dist/, shim present) ###
NODE_OPTIONS after plugin load: "--import=file:///…/plugins/opencode/hook-shim/handler.js"
child status: 0 | stdout: mcp server handshake ok
```

- Observed result: wheel installs no longer poison child env, so Node
MCP servers start; checkout builds keep the preload and still start
children cleanly.
- Not tested: no reproduction against a live `opencode` +
codegraph/firecrawl session on Ubuntu (no OpenCode install on this
machine); the child-process failure was reproduced directly instead,
which is the exact mechanism behind the reported `-32000`. Docker proxy
path not re-tested — it is unrelated to the fix.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md`

## Additional Notes

Docs unchanged: this is an internal packaging/runtime bug with no
documented behavior attached.

Follow-up (deliberately not in this PR): wheel installs now lose
child-process transport hooking rather than crashing — the same coverage
they effectively had, since the preload never once loaded from a wheel.
Restoring it means a standalone shim build emitted into `_dist/` plus
exporting `installHeadroomTransport` from that bundle;
`hook-shim/handler.js` also imports `../dist/index.js`, which does not
exist in the wheel layout, so copying the file alone would not be
enough. Worth doing only if something needs a subprocess's LLM traffic
proxied.
2026-08-05 12:42:10 -07:00
..
headroom-agent-hooks chore: release main (#2679) 2026-08-04 19:39:34 -07:00
headroom-oauth2 chore: remove committed node_modules + stray/internal markdown (repo hygiene) (#1528) 2026-06-27 23:32:54 -07:00
hermes fix(ccr): make headroom_retrieve a hash-only full-content lookup (#1532) 2026-06-28 10:32:43 -07:00
openclaw deps: bump postcss from 8.5.19 to 8.5.25 in /plugins/openclaw (#2749) 2026-08-04 21:58:42 -05:00
opencode fix(opencode): don't preload a missing transport shim into child processes (#2806) 2026-08-05 12:42:10 -07:00