mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
7 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
5de12f75e3
|
docs(ccr): correct stale 5-minute TTL hints to 30 minutes (#2224)
## Description The CCR store default TTL is `DEFAULT_TTL = 1800s` (30 minutes — see `crates/headroom-core/src/ccr/mod.rs` and `config.py store_ttl_seconds=1800`), but several user-facing hints and docstrings still said "5 minutes", the old default. The opencode/openclaw retrieve tools surfaced `(default TTL: 5 minutes)` in their expiry hint — exactly the misleading message reported in #1023. (The CCR cache itself works; the row-drop store bridge that populates the retrieve store landed for #389.) This corrects the two plugin hints, the `InMemoryCcrStore` docstrings, the SQLite/backend default TTL comments, and the `smart_crusher` mirror comment. The `mod.rs` comment that references "the *old* 5-minute default" is intentionally left unchanged — it correctly describes history. ## Type of Change - [x] Documentation update ## Changes Made - `plugins/openclaw/src/tools/headroom-retrieve.ts` + `plugins/opencode/src/retrieve.ts`: retrieve-failure hint `5 minutes` → `30 minutes`. - `crates/headroom-core/src/ccr/backends/in_memory.rs`: two docstrings (`5 minutes by default`, `5-minute TTL`) → `30 minutes` / `30-minute`. - `crates/headroom-core/src/ccr/backends/mod.rs` + `sqlite.rs`: SQLite/default backend TTL comments `5-minute` → `30-minute`. - `headroom/transforms/smart_crusher.py`: mirror comment `defaults to 5 minutes` → `30 minutes`. ## Testing - [x] Linting passes (`ruff` / `cargo check`) - [x] Manual verification (see Real Behavior Proof) ### Test Output ```text $ ruff format --check headroom/transforms/smart_crusher.py # clean $ cargo check -p headroom-core # Finished, no errors ``` ## Real Behavior Proof - Environment: macOS (Darwin), branch `feat/ccr-ttl-hint-fix` off `main`. - Exact command / steps: grepped every `5 minutes` / `5-minute` TTL reference across the repo; confirmed the real default is `DEFAULT_TTL = Duration::from_secs(1800)` (`ccr/mod.rs:66`), that `InMemoryCcrStore::new()` uses `DEFAULT_TTL` (not a local 300s), and that `config.py` sets `store_ttl_seconds = 1800 # 30 minutes`. - Observed result: all stale CCR default-TTL "5 minutes" references now read "30 minutes"; the one historical reference (`mod.rs`: "the old 5-minute default") is left as-is because it is accurate. - Not tested: nothing runtime changed — these are docstring/comment/hint string edits only, so there is no behavior to exercise. ## 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 - [ ] I have commented my code — N/A (this PR is comments/strings) - [x] I have made corresponding changes to the documentation (this *is* the doc change) - [x] My changes generate no new warnings - [ ] I have added tests — N/A (no behavior change) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A: user-facing hint/docstring correction, no functional change ## Additional Notes - Surfaced while root-causing #1023: the "cache permanently empty / TTL: 5 minutes" report is resolved on `main` (the store-bridge for #389 populates the retrieve store), but the stale "5 minutes" strings the reporter actually saw were still in the tree. This PR fixes those. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
e8151f059b
|
fix(opencode): expose headroom/* models in injected provider config (#1716)
## Description `headroom wrap opencode` (and `headroom install opencode`) injects a `provider.headroom` block into the OpenCode config, but the block contained **no `models` map**. OpenCode only resolves `<provider>/<model>` ids that are listed in a custom provider's `models` map, so every documented `headroom/*` model (see `plugins/opencode/README.md`) failed with: ```text Error: Model not found: headroom/claude-sonnet-4-6. ``` This PR adds the model map (mirroring `DEFAULT_MODELS` in `plugins/opencode/src/provider.ts` and the README table) via a single shared `headroom_provider_entry()` helper used by all three injection sites. It also fixes a latent bug in the TS helper `createHeadroomProvider`, which prefixed model **keys** with `headroom/` — OpenCode would have registered them as `headroom/headroom/<id>`. Not addressed here (flagged for maintainers): the `headroom-opencode` npm package referenced by the plugin docs is not published to npm (registry 404), so the transparent-transport interception path (which would capture `github-copilot/*` traffic in the dashboard) still depends on a locally built `plugins/opencode/dist/entry.opencode.js`. With this fix, the documented `headroom/*` provider route works, so wrapped OpenCode traffic is proxied and recorded when users select `headroom/*` models. Closes #1657 ## 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 - `headroom/providers/opencode/config.py`: added `HEADROOM_OPENCODE_MODELS` (claude-sonnet-4-6, claude-opus-4-6, claude-haiku-4-5-20251001, gpt-4o, gpt-4.1 — same names/limits as the TS plugin) and a `headroom_provider_entry(port)` helper that includes the `models` map; `_render_provider_block` and `inject_opencode_provider_config` now use it instead of duplicating the provider dict. - `headroom/providers/opencode/runtime.py`: `build_opencode_config_content` reuses `headroom_provider_entry()` so `OPENCODE_CONFIG_CONTENT` exposes the models too. - `plugins/opencode/src/provider.ts`: `createHeadroomProvider` no longer prefixes model keys with `headroom/` (OpenCode namespaces model ids by provider key; keys must be bare ids). - `tests/test_providers_opencode_config.py`: assertions that the injected provider block and `build_opencode_config_content` output contain a `models` map with bare-id keys including `claude-sonnet-4-6`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_providers_opencode_config.py -q 1 failed, rest passed — test_build_launch_env_with_project is a pre-existing Windows-only failure (json.dumps escapes backslashes in the plugin path); it fails identically on upstream/main without this change and passes on Linux. $ ruff check headroom/providers/opencode tests/test_providers_opencode_config.py All checks passed! $ ruff format --check . 5 files already formatted $ mypy headroom --ignore-missing-imports Success (notes only, no errors) $ cd plugins/opencode && npm run typecheck && npm test tsc --noEmit: OK Test Files 2 passed (2) Tests 13 passed (13) ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, Node v26.3.0, this branch with the Rust core built locally. - Exact command / steps: `python -c "from headroom.providers.opencode.runtime import build_opencode_config_content; import json; print(json.dumps(build_opencode_config_content(port=8787, include_mcp=False)['provider']['headroom'], indent=1))"` - Observed result: the generated `headroom` provider block now contains `"models"` with bare-id keys (`claude-sonnet-4-6`, `claude-opus-4-6`, `claude-haiku-4-5-20251001`, `gpt-4o`, `gpt-4.1`), each with name and context/output limits; previously the block had no `models` key, which is exactly why OpenCode returned `Model not found: headroom/claude-sonnet-4-6`. - Not tested: a live `opencode run` round-trip against a real OpenCode install (no OpenCode binary in this environment); dashboard event capture for `github-copilot/*` models via the transport plugin (blocked on the unpublished `headroom-opencode` artifact, see Description). ## 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 - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - Docs: `plugins/opencode/README.md` already documents these models; no doc change needed. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c19347c310
|
fix(opencode): preserve custom OpenAI gateway paths (#1596)
## Description Custom OpenAI-compatible gateways mounted under provider-specific prefixes could miss Headroom's dedicated OpenAI compression routes when used through the OpenCode transport. A request such as `https://open.bigmodel.cn/api/coding/paas/v4/chat/completions` was replayed to the proxy at `/api/coding/paas/v4/chat/completions`, so the proxy selected catch-all passthrough instead of `/v1/chat/completions`. This change keeps the proxy-facing entrypoints stable on `/v1/chat/completions` and `/v1/responses` for OpenAI-compatible suffixes, while preserving the original upstream path in an internal header so the dedicated OpenAI handlers can reconstruct the real provider URL. Closes #1582 ## 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 - Normalize opencode-routed OpenAI-compatible `/chat/completions` and `/responses` requests onto the proxy's stable `/v1/*` routes. - Preserve the original upstream pathname in an internal `x-headroom-original-path` signal for dedicated OpenAI handler reconstruction. - Reconstruct dedicated OpenAI upstream URLs from `x-headroom-base-url` plus the preserved path prefix, while preserving request query strings and rejecting non-HTTP base hints. - Keep nearby non-OpenAI paths such as `/base/v1/messages` on existing passthrough behavior. - Add focused transport and proxy regression coverage for prefixed gateway paths, invalid fallback cases, and internal-header stripping. ## Testing - [x] Transport regression tests pass (`npm --prefix plugins/opencode test -- src/transport.test.ts`) - [x] Proxy regression tests pass (`uv run pytest tests/test_proxy/test_openai_transport_path_prefix.py -q`) - [x] Linting passes (`uv run ruff check headroom/proxy/handlers/openai.py tests/test_proxy/test_openai_transport_path_prefix.py`) - [x] Type checking passes (`npm --prefix plugins/opencode run typecheck`) - [x] New tests added for the bugfix - [ ] Manual testing performed ### Test Output ```text npm --prefix plugins/opencode test -- src/transport.test.ts PASS, 11 tests passed. npm --prefix plugins/opencode run typecheck PASS uv run pytest tests/test_proxy/test_openai_transport_path_prefix.py -q PASS, 7 tests passed. uv run ruff check headroom/proxy/handlers/openai.py tests/test_proxy/test_openai_transport_path_prefix.py PASS, all checks passed. ``` ## Real Behavior Proof - Environment: Windows, Python 3.12 via `uv`, Node 18+, focused OpenCode transport and proxy handler tests. - Exact command / steps: on `origin/main`, copy the updated `plugins/opencode/src/transport.test.ts` into a base worktree and run `npm --prefix plugins/opencode test -- src/transport.test.ts`; on this branch, rerun that transport test plus `npm --prefix plugins/opencode run typecheck` and `uv run pytest tests/test_proxy/test_openai_transport_path_prefix.py -q`. - Observed result: the base worktree fails because prefixed `/chat/completions` and `/responses` requests still enter the proxy at their provider path, while this branch passes with `/v1/chat/completions` and `/v1/responses`, preserves `x-headroom-original-path`, reconstructs the provider-prefixed upstream URL and query string, falls back safely on invalid hints, and keeps nearby `/base/v1/messages` traffic on passthrough. - Not tested: full CI suite, live BigModel traffic, and generic catch-all passthrough compression. ## 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 - [ ] 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 - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes This completes the transport contract introduced in https://github.com/headroomlabs-ai/headroom/pull/1573 by keeping prefixed OpenAI-compatible traffic on Headroom's stable `/v1/*` surface while preserving the real upstream path for dedicated-handler reconstruction. https://github.com/headroomlabs-ai/headroom/pull/1367 is adjacent global proxy configuration work for direct deployments; this PR is the per-request OpenCode transport fix for custom upstream path prefixes. `CHANGELOG.md` is intentionally unchanged because this repo's release pipeline generates changelog entries from conventional commits. This stays scoped to `/chat/completions` and `/responses` suffixes. Generic catch-all passthrough compression remains separate from this bugfix slice. |
||
|
|
ad0034f981
|
fix(opencode): route native providers + load transport plugin, fix Serena context (#1573)
## Description `headroom wrap opencode` looked like it worked (proxy started, opencode launched) but **no inference reached the proxy**, so users saw zero savings (#1572). Root causes: 1. The injected synthetic `headroom` provider (`@ai-sdk/openai-compatible`) had **no `models` and no `apiKey`** → opencode raised `ProviderModelNotFoundError`, and it only ever targets OpenAI. 2. The wrap injected a reference to the **unpublished `headroom-opencode` npm plugin**, which opencode silently failed to resolve → the transparent transport never loaded. 3. Serena was launched with `--context opencode`, a context Serena does not ship → crash on launch (#1549). This PR makes `headroom wrap opencode` route opencode's traffic through the proxy with the user's **own API key** (no key written to disk), and gets the transparent transport plugin actually loading. Closes #1572 Closes #1549 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - **`runtime.py`** — two complementary routing layers (both verified against opencode 1.17): 1. Override opencode's native `anthropic`/`openai` provider `baseURL` to the proxy. Reliable, credential-independent (covers API key **and** subscription), keeps native model metadata/limits, reuses the user's existing key. This is the always-on layer and the only one a pip-only install needs. 2. Load the transport plugin **by absolute path** when it has been built (`headroom_opencode_plugin_path()`), self-configured via `HEADROOM_PROXY_URL`. Covers providers we don't name (Gemini, Copilot, custom gateways) and providers added mid-session. Loopback URLs aren't double-routed, so the two layers coexist. - **`wrap.py`** — Serena context `opencode` → `agent` (valid context). - **`plugins/opencode/`** — new `src/entry.opencode.ts` loader entry that exports **only** the plugin function (opencode rejects a module with non-function exports: "Plugin export is not a function"); tsup builds it as a second entry. - **tests** — updated `test_providers_opencode_config.py` for path-based plugin injection + a skip-when-unbuilt case. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ python -m pytest tests/test_providers_opencode_config.py tests/test_cli/test_wrap_opencode.py -q 72 passed in 0.59s $ (cd plugins/opencode && npm test) Test Files 2 passed (2) Tests 9 passed (9) $ ruff check headroom/providers/opencode/runtime.py headroom/cli/wrap.py tests/test_providers_opencode_config.py ✓ Ruff: No issues found $ mypy headroom/providers/opencode/runtime.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - **Environment:** macOS, opencode 1.17.11 (npm), headroom proxy 0.28.0 (local), Anthropic API key from `.env`. - **Exact command:** ``` headroom wrap opencode --no-serena --no-context-tool --no-proxy --port 8787 \ -- run -m anthropic/claude-haiku-4-5-20251001 "Reply with exactly: WRAPWORKS" ``` - **Observed result:** opencode printed `plugin=headroom-opencode` (loaded, no error) and returned `WRAPWORKS`. The proxy log shows the request routed through it: ``` event=outbound_request method=POST path=https://api.anthropic.com/v1/messages source=passthrough event=proxy_inbound_response path=/v1/messages status=200 PERF model=claude-haiku-4-5-20251001 cache_hit_pct=97 client=opencode ``` Compression verified on a large tool_result (`client=opencode`): ``` Pipeline complete: 170653 -> 77 tokens (saved 170576, 100.0% reduction) PERF tok_before=151309 tok_after=67 tok_saved=151242 transforms=router:tool_result:log client=opencode ``` - **Not tested:** custom OpenAI-compatible gateways (need the proxy to honor `x-headroom-base-url` in the dedicated OpenAI handler — open PR #1502); interactive TUI (verified the headless `opencode run` path). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes - **Plugin shipping:** the plugin loads by repo-relative path, which works for source/editable installs. `plugins/opencode/dist/` is gitignored, so the plugin must be built (`cd plugins/opencode && npm install && npm run build`) for layer 2 to activate; pip-only installs gracefully fall back to layer 1 (native baseURL override). Bundling `dist/` into the package or publishing `headroom-opencode` to npm is a follow-up for universal shipping. - **CHANGELOG:** N/A — handled by Release Please from the conventional commit. - Custom-gateway support depends on existing PR #1502 (honor `x-headroom-base-url` in the dedicated OpenAI handlers); not duplicated here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
c2fc4d3753
|
fix(ccr): make headroom_retrieve a hash-only full-content lookup (#1532)
The optional `query` parameter on headroom_retrieve routed retrieval through CompressionStore.search(), which BM25-scored the items inside a single cached blob and dropped everything below a 0.3 relevance floor. On small per-blob corpora with conversational queries this returned an empty result the large majority of the time, so the LLM saw "nothing found" for content that was actually present — pushing users to turn compression off entirely. Retrieval is fundamentally a hash lookup (this already matches the Rust proxy's CCR store, which is put/get only — "no BM25 search"). Remove the query/search path end to end and always return the full original content: Core (Python proxy): - tool schemas (anthropic/openai/google) drop the `query` property - parse_tool_call returns the hash (str | None) instead of (hash, query) - response handler, proxy POST/GET/tool-call handlers, the MCP retrieve tool, and the streaming feedback recorders retrieve by hash only - proactive context-tracker expansion always restores full content - delete CompressionStore.search() and its BM25 machinery (the bm25 module stays — it is still used by relevance/) - CCRToolCall.query, CCRToolResult.was_search, and ExpansionRecommendation.expand_full/search_query are removed Plugins (advertised a now-defunct query param to the LLM): - hermes (Python), openclaw + opencode (TypeScript) retrieve tools drop `query` from their schemas, signatures, request URLs, and tests Benchmarks/docs: - ccr_regression + adversarial benchmarks switch from store.search() to full hash retrieval (search input-injection tests repurposed to the hash, the only remaining input surface) - wiki/ARCHITECTURE.md, wiki/ccr.md, docs/content/docs/ccr.mdx, config.py and store docstrings updated to describe hash-only retrieval Tests updated to assert full-content retrieval and guard the removed surface; the full CCR/proxy/store/TOIN suite passes. ruff + mypy clean. ## Description <!-- Briefly explain the change and why it is needed. --> Closes # ## Type of Change - [ ] 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 - ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text # Paste relevant command output or artifact links here ``` ## Real Behavior Proof - Environment: - Exact command / steps: - Observed result: - Not tested: ## Review Readiness - [ ] I have performed a self-review - [ ] This PR is ready for human review ## Checklist - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) Add screenshots to help explain your changes. ## Additional Notes <!-- Mention any N/A checklist items, tradeoffs, follow-ups, or maintainer context. --> |
||
|
|
b4571cc346
|
feat: headroom wrap opencode / unwrap opencode CLI (#1105)
## Summary This PR implements transparent `headroom wrap opencode` support without asking users to edit OpenCode provider URLs, choose an extra CLI flag, or maintain a static provider list. The wrapper now lives at the runtime transport boundary: OpenCode keeps its user/provider config, while Headroom intercepts outbound provider traffic in-process and routes it through the local Headroom proxy. ## What changed ### Transparent OpenCode wrapping - `headroom wrap opencode` injects the `headroom-opencode` plugin through `OPENCODE_CONFIG_CONTENT`. - Existing OpenCode provider URLs are preserved. We do not rewrite user config URLs to point at Headroom. - Existing `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` env vars are preserved. - Local OpenCode traffic, localhost traffic, and Headroom proxy traffic bypass the shim to avoid loops. ### Runtime transport interception - Added an OpenCode plugin transport shim that wraps: - `globalThis.fetch` - `http.request` / `http.get` - `https.request` / `https.get` - External provider calls are routed to the local Headroom proxy. - The original upstream origin is passed through `x-headroom-base-url`, so the proxy can forward to the real provider without changing OpenCode config. - External `http2.connect` is blocked loudly instead of allowing direct provider traffic to leak outside Headroom. ### Live provider additions Provider coverage is no longer based on a static config scan. Because routing happens at outbound request time, providers added mid-session are routed through Headroom automatically as long as they use the covered Node transport paths. ### Subagent and child-process coverage - The parent OpenCode plugin sets a packaged Node preload shim through `NODE_OPTIONS=--import=.../hook-shim/handler.js`. - The transport shim patches `child_process.spawn`, `exec`, `execFile`, and `fork` so child Node processes receive the Headroom preload even when OpenCode passes a custom `env`. - The child-process shim fails closed if it loads without `HEADROOM_OPENCODE_TRANSPORT_PROXY_URL`. - This closes the subagent leak path where a child Node process could otherwise start without Headroom transport interception. ## Why this goes beyond PR #1089 PR #1089 improves OpenCode provider registration, but it still focuses on provider config shape. This PR moves the enforcement boundary to runtime transport interception. This PR goes further because: - No provider URL rewriting is required. - New providers added mid-session are covered automatically. - Subagents and child Node processes inherit the Headroom transport shim. - Direct external HTTP/2 paths fail loudly instead of leaking. - The wrap remains transparent to the user's OpenCode provider config. - The wrapper is fail-closed for unsupported child-process preload state. ## Additional robustness fixes While validating the change in Docker, the full Python suite exposed unrelated Linux/container robustness issues. These are fixed in this PR so the suite is green: - Binary cache handling now treats cache paths under a non-writable existing parent as unavailable, including when tests run as root in Docker. - `release_version.py` honors `MANUAL_VER` before git calls so direct script execution works outside a `.git` checkout. - Test logger isolation now resets relevant Headroom child loggers so proxy logging setup cannot poison later `caplog` tests. - The scanner missing-path test now uses a guaranteed missing `tmp_path` child instead of relying on `/nonexistent/path`. ## Validation All implementation validation was run inside Docker. - Full Python suite from a fresh Docker copy: `6605 passed, 523 skipped`. - Ruff on changed Python/OpenCode paths: passed. - OpenCode plugin typecheck: passed. - OpenCode plugin tests: `9 passed`. - OpenCode plugin build: passed. - Hook shim preload smoke test: passed. ## Notes This PR intentionally does not add a CLI option. `headroom wrap opencode` means full wrap. Either Headroom wraps OpenCode transparently, or the path fails loudly instead of silently leaking provider traffic. --------- Co-authored-by: Rudimar Ronsoni <6081613+rudironsoni@users.noreply.github.com> |