## 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.
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.19 to
8.5.25.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08c989c43c"><code>08c989c</code></a>
Release 8.5.25 version</li>
<li><a
href="24f6814716"><code>24f6814</code></a>
Fix 8.5.17 visitor regression</li>
<li><a
href="f2fa53f11d"><code>f2fa53f</code></a>
Add supply chain security requirement to PostCSS plugin guide</li>
<li><a
href="10edf0b060"><code>10edf0b</code></a>
fix: return empty array for empty string in list.split (<a
href="https://redirect.github.com/postcss/postcss/issues/2121">#2121</a>)</li>
<li><a
href="0ebe8ad591"><code>0ebe8ad</code></a>
Release 8.5.24 version</li>
<li><a
href="73218c6424"><code>73218c6</code></a>
Update dependencies</li>
<li><a
href="9a114f62b0"><code>9a114f6</code></a>
Preserve the BOM when stringifying (<a
href="https://redirect.github.com/postcss/postcss/issues/2119">#2119</a>)</li>
<li><a
href="9069261912"><code>9069261</code></a>
Fix types check</li>
<li><a
href="eb9e1fe793"><code>eb9e1fe</code></a>
Release 8.5.23 version</li>
<li><a
href="9d19c78ac9"><code>9d19c78</code></a>
Update dependencies</li>
<li>Additional commits viewable in <a
href="https://github.com/postcss/postcss/compare/8.5.19...8.5.25">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/headroomlabs-ai/headroom/network/alerts).
</details>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.19 to
8.5.25.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.25</h2>
<ul>
<li>Fixed 8.5.17 visitor regression.</li>
<li>Fixed <code>list.split()</code> for non-string values (by <a
href="https://github.com/amir-rezaei"><code>@amir-rezaei</code></a>).</li>
</ul>
<h2>8.5.24</h2>
<ul>
<li>Preserve the BOM after the processing (by <a
href="https://github.com/hdimer"><code>@hdimer</code></a>).</li>
</ul>
<h2>8.5.23</h2>
<ul>
<li>Do not load source map without <code>opts.from</code> for security
reasons.</li>
</ul>
<h2>8.5.22</h2>
<ul>
<li>Fixed custom property losing semicolon before a comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
</ul>
<h2>8.5.21</h2>
<ul>
<li>Fixed childless at-rule losing semicolon before comment (by <a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed docs (by <a
href="https://github.com/isker"><code>@isker</code></a>).</li>
</ul>
<h2>8.5.20</h2>
<ul>
<li>Fixed missing space if <code>AtRule#params</code> is set after (by
<a
href="https://github.com/sarathfrancis90"><code>@sarathfrancis90</code></a>).</li>
<li>Fixed mixing AST error on warnings (by <a
href="https://github.com/MahinAnowar"><code>@MahinAnowar</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08c989c43c"><code>08c989c</code></a>
Release 8.5.25 version</li>
<li><a
href="24f6814716"><code>24f6814</code></a>
Fix 8.5.17 visitor regression</li>
<li><a
href="f2fa53f11d"><code>f2fa53f</code></a>
Add supply chain security requirement to PostCSS plugin guide</li>
<li><a
href="10edf0b060"><code>10edf0b</code></a>
fix: return empty array for empty string in list.split (<a
href="https://redirect.github.com/postcss/postcss/issues/2121">#2121</a>)</li>
<li><a
href="0ebe8ad591"><code>0ebe8ad</code></a>
Release 8.5.24 version</li>
<li><a
href="73218c6424"><code>73218c6</code></a>
Update dependencies</li>
<li><a
href="9a114f62b0"><code>9a114f6</code></a>
Preserve the BOM when stringifying (<a
href="https://redirect.github.com/postcss/postcss/issues/2119">#2119</a>)</li>
<li><a
href="9069261912"><code>9069261</code></a>
Fix types check</li>
<li><a
href="eb9e1fe793"><code>eb9e1fe</code></a>
Release 8.5.23 version</li>
<li><a
href="9d19c78ac9"><code>9d19c78</code></a>
Update dependencies</li>
<li>Additional commits viewable in <a
href="https://github.com/postcss/postcss/compare/8.5.19...8.5.25">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/headroomlabs-ai/headroom/network/alerts).
</details>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Description
The 0.33.0 release PR (#2339) has sat in `changes-requested` since
2026-07-17. Root cause: **release-please only rewrites `pyproject.toml`
and its configured `extra-files`**, but other tracked files also carry
the version — and `server.json` is asserted byte-for-byte against
`render_server_json()`, which derives its version from `pyproject.toml`.
So the bump alone fails
`tests/test_mcp_registry/test_server_json.py::test_root_server_json_matches_builder`
(the `test (2)` shard) on every regenerated release PR.
Nothing in the repo regenerated `server.json` at all, so it fell behind
every release.
Unblocks #2339.
### Why the release *build* passes but the release PR does not
`release.yml` already runs `scripts/version-sync.py` immediately before
its own `verify-versions.py` gate (lines 145 and 278). That is why
`build` and `build-wheels` are green on #2339 despite the drift — it
syncs in the workspace, uncommitted. The regular CI test job does
**not** sync, so the fix has to be committed to the branch.
This also explains why reviewers kept seeing `verify-versions.py` fail
locally while CI's build jobs passed: the verifier is never run
un-synced inside `release.yml`.
## 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
- **`scripts/version-sync.py`**: also write `server.json`. It was the
one version-carrying file with no writer anywhere. Values are rewritten
in place so key order and formatting keep matching the builder's
byte-for-byte output (verified: the file is pure ASCII and round-trips
exactly through `json.dumps(..., indent=2) + "\n"`).
- **`.github/workflows/release-metadata-sync.yml`** (new): on a push to
`release-please--branches--**`, run version-sync → gate on
verify-versions → commit if changed.
- **Keyed off the branch push** because release-please force-regenerates
that branch on every merge to main. That is precisely what wiped the
hand-pushed metadata fixes on #2339 (`2a86c8ff`, `d5ea4dc5`) — a push
trigger re-heals after every regeneration instead of being lost.
- **Uses the same PAT as `release-please.yml`**: a `GITHUB_TOKEN` push
does not trigger workflows, so the release PR's checks would never
re-run against the synced commit and would stay red.
- **Idempotent**: the self-triggered rerun finds no diff and exits
before pushing, so the loop terminates after one no-op run.
- **Corrected pre-existing drift on `main`**: the agent-hooks plugin
manifests, both marketplace manifests, and `.releasemetadata` were
stranded at **0.31.0** — never bumped for 0.32.0 either.
`verify-versions.py` now passes on `main`.
### Why not more `extra-files` entries
That would need ~13 jsonpath entries restating what `version-sync.py`
already knows, and a jsonpath that fails to match **fails silently** —
the same class of failure this PR removes, discoverable only after a
real release PR regenerates. There is also no precedent for nested
jsonpath (`$.packages[0].version`, `$.metadata.version`) in the config
today; both existing entries are plain `$.version`. Running the script
keeps one source of truth, and files added to it later are covered with
no change here.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`) — no `headroom/` sources
touched
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest scripts/tests/ tests/test_release_workflows.py tests/test_mcp_registry/ -q
207 passed in 2.69s
$ ruff check scripts/version-sync.py scripts/tests/test_version_sync.py tests/test_release_workflows.py
All checks passed!
$ ruff format --check <same>
3 files already formatted
$ actionlint .github/workflows/release-metadata-sync.yml
(clean)
```
New tests:
- `test_server_json_version_is_synchronized` — version-sync moves both
`server.json` version fields and preserves the other keys.
- `test_release_metadata_sync_runs_on_release_please_branch` — asserts
the trigger, the sync→verify→commit ordering, the no-op guard, and the
PAT.
- `test_version_sync_covers_every_file_the_verifier_gates` — guards
`version-sync.py` and `verify-versions.py` against drifting apart again,
which is the root cause here.
## Real Behavior Proof
- **Environment:** macOS (Darwin arm64), Python 3.12, repo venv.
- **Exact command / steps:** reproduced the CI failure locally by
simulating release-please's partial bump, then applying the fix.
**Reproducing the exact `test (2)` failure** — set `pyproject` to 0.33.0
while `server.json` stays at 0.32.0, as release-please leaves it:
```text
$ python -m pytest tests/test_mcp_registry/test_server_json.py -q
FAILED tests/test_mcp_registry/test_server_json.py::test_root_server_json_matches_builder
1 failed, 3 passed
```
**After `version-sync.py`:**
```text
$ python scripts/version-sync.py && python -m pytest tests/test_mcp_registry/test_server_json.py -q
4 passed
```
**Both gates green on a simulated 0.33.0 bump:**
```text
$ python scripts/version-sync.py --version 0.33.0
Version synchronized to 0.33.0
$ python scripts/verify-versions.py
All versions aligned at 0.33.0
$ python -m pytest tests/test_mcp_registry/test_server_json.py -q
4 passed
```
**Idempotency** (the property the workflow's loop-termination relies
on): re-running against an already-synced tree leaves `pyproject.toml`,
`server.json`, `openclaw`, and `sdk/typescript` untouched.
- **Not tested:** the workflow has not executed on a real release-please
branch regeneration — that can only be exercised once this is on `main`
and release-please next updates #2339. The PAT push path and the
self-trigger no-op are reasoned from `release-please.yml`'s existing
token comment and from local idempotency, not observed in CI.
## 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
**Context on the v0.32.0 release failure, since it is easy to misread as
"images never build".** Every artifact built for v0.32.0 — all 5 wheel
platforms including Windows, all 16 Docker builds + 8 manifests +
`promote-latest`, npm, and GitHub Packages. Only `publish-pypi` failed
(PyPI attestations, already fixed by `f9cbdd6e` / #2405), and
`create-release` was skipped because it depends on it. That is why the
release looked like it produced nothing.
**Separate, approaching blocker — not addressed here.** PyPI is at
**9.69 GB of its 10 GB project cap (96.9%)**, leaving ~305 MB against
~68 MB per release, so roughly 4 more releases fit. The `0.21.x` series
alone holds **6.58 GB across 31 releases**, from the old
every-push-is-a-release era; pruning it would reclaim two thirds of the
quota. Worth a separate issue.
**`.releasemetadata` is written but never read** by anything outside
`version-sync.py` and its test. It is kept in sync here for internal
consistency, but it may be a deletion candidate.
## Description
The OpenCode transport plugin - the piece that gives `wrap opencode`
all-provider routing by tagging each request with `x-headroom-base-url`
- only exists in repo checkouts today. `headroom_opencode_plugin_path()`
resolves `plugins/opencode/dist/entry.opencode.js`, which pip wheels do
not ship, so every pip install silently degrades to the two-provider
(anthropic/openai) baseURL fallback. The function's own docstring
documents the gap ("a pip-only install that does not ship `plugins/`").
Shipping the existing build output is not enough: the regular tsup build
leaves `headroom-ai` and `@opencode-ai/plugin` as bare external imports,
which only resolve next to the checkout's `node_modules`. Copied into
site-packages, the file fails to load. This PR ships a self-contained
bundle inside the wheel instead.
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] 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/tsup.standalone.config.ts` + `npm run
build:standalone`: a second build of the loader entry with `noExternal:
[/.*/]` and `splitting: false` - a single self-contained file whose only
imports are node builtins.
- `headroom/providers/opencode/_dist/entry.opencode.js`: the committed
standalone bundle (452 KB). It sits inside the package directory, so
maturin's `python-source = "."` packaging picks it up into the wheel
with no build-system changes.
- `headroom_opencode_plugin_path()`: falls back to the packaged bundle.
Precedence otherwise unchanged: `HEADROOM_OPENCODE_PLUGIN_PATH` env
override, then a repo-checkout build (fresher during development), then
the packaged bundle.
- CI (`opencode-plugin.yml`): rebuilds the standalone bundle and fails
the run if the committed artifact drifted from source, with a one-line
fix instruction; workflow path triggers extended to
`headroom/providers/opencode/_dist/**`.
- `tests/test_providers_opencode_plugin_path.py`: packaged bundle exists
and is self-contained (no bare npm imports), env override wins, fallback
resolution order.
## 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
$ uv run --frozen --extra dev pytest tests/test_providers_opencode_plugin_path.py \
tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py
============================== 49 passed in 0.31s ==============================
$ uvx ruff check headroom/providers/opencode/runtime.py tests/test_providers_opencode_plugin_path.py
All checks passed!
$ uvx ruff format --check headroom/providers/opencode/runtime.py tests/test_providers_opencode_plugin_path.py
2 files already formatted
$ uv run --frozen --extra dev mypy headroom/providers/opencode/runtime.py
Success: no issues found in 1 source file
$ cd plugins/opencode && npm run build:standalone
ESM dist-standalone/entry.opencode.js 452.28 KB
ESM Build success in 28ms
```
## Real Behavior Proof
- Environment: macOS 15 (arm64), opencode 1.18.5 (Homebrew), node 22 /
npm 10, isolated `XDG_*` dirs so no real user config was touched.
- Exact command / steps:
1. `npm run build:standalone` in `plugins/opencode`.
2. Started a local header-logging HTTP listener on `127.0.0.1:9977`
(stands in for the proxy; logs method, path, headers, returns 401).
3. Registered the standalone bundle by absolute path in a scratch
`opencode.json` (`"plugin":
["<abs>/dist-standalone/entry.opencode.js"]`) with a `google` provider
entry and a fake API key. Note: the bundle's directory has **no**
`node_modules` - this is exactly the site-packages situation.
4. `HEADROOM_PROXY_URL=http://127.0.0.1:9977 opencode run -m
google/gemini-2.5-flash "say hi"`.
- Observed result: the listener received `POST
/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse` with
`User-Agent: opencode/1.18.5 ...` - i.e. the plugin loaded standalone
and rerouted a provider that the baseURL fallback cannot cover (native
Gemini wire format) to the proxy URL from `HEADROOM_PROXY_URL`.
- Not tested: Windows path resolution (pure `pathlib`, no platform
branches); wheel-build byte-determinism of the tsup output across OSes
(the CI drift check will surface it on the first divergent build).
## 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` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)
## Screenshots (if applicable)
Not applicable - CLI/packaging change.
## Additional Notes
- Documentation checklist item: unchecked because the only doc surface I
found is the `headroom_opencode_plugin_path()` docstring, which this PR
rewrites to describe the three-step resolution order. Happy to add a
line to `docs/content/docs/` if there is a preferred page.
- A committed build artifact is not free: the CI drift check keeps it
honest, and the byte-compare relies on tsup/esbuild determinism under
`npm ci` (pinned lockfile). If you'd rather avoid the committed artifact
entirely, the alternative is publishing `headroom-opencode` to npm (its
`package.json` is publish-ready) and registering the plugin by package
name - happy to rework in that direction; the wheel-bundled path has the
advantage of version-locking the plugin to the backend it ships with.
- Downstream motivation: Headroom Desktop manages a long-lived shared
proxy (no `wrap` launcher) and wants to register this plugin from the
installed wheel path so OpenCode users get all-provider routing there
too.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Description
Release Please generated the 0.33.0 release PR for main. This updates
release metadata, package versions, and the generated changelog for the
0.33.0 release.
I also aligned the agent-hook plugin manifests, marketplace metadata,
editable lockfile package version, and canonical MCP `server.json`
descriptor to 0.33.0 so all package/plugin/registry version declarations
match the Release Please version bump.
## Type of Change
- [x] Documentation update
- [x] Release / packaging metadata
## Changes Made
- Updated `.release-please-manifest.json`, `pyproject.toml`,
`plugins/openclaw/package.json`, and `sdk/typescript/package.json` to
0.33.0.
- Updated the generated `CHANGELOG.md` release notes for 0.33.0.
- Synced `plugins/headroom-agent-hooks` plugin manifests and marketplace
metadata to 0.33.0.
- Synced `uv.lock` editable `headroom-ai` package version to 0.33.0.
- Regenerated the canonical MCP `server.json` descriptor to 0.33.0.
## Testing
- [x] Version verification passes
- [x] Version-sync tests pass
- [x] MCP server descriptor test passes
- [x] Whitespace check passes
### Test Output
```text
uv run python scripts/verify-versions.py
All versions aligned at 0.33.0
uv run pytest scripts/tests/test_version_sync.py scripts/tests/test_sync_plugin_versions.py -q
14 passed in 0.80s
uv run pytest tests/test_mcp_registry/test_server_json.py -q
4 passed in 0.42s
git diff --check
# no output
```
## Real Behavior Proof
- Environment: Windows 11, local checkout of the Release Please branch.
- Exact command / steps: Ran version verification and MCP descriptor
tests after syncing release metadata, plugin marketplace versions,
lockfile version, and `server.json`.
- Observed result: All package, plugin manifest, marketplace, lockfile,
and MCP descriptor release versions are aligned at 0.33.0.
## Description
**Fixes the Release Please pipeline so it emits `v0.32.0`.** pip /
Docker / npm are out of sync because 0.32.0 was never actually released.
### Root cause
Same failure mode as #1916. #2175 (a `fix(deps)` PR) bumped
`pyproject.toml` + `.release-please-manifest.json` to **0.32.0
out-of-band**, so release-please reads 0.32.0 as the *current* version
and computes the next release as **0.33.0** (#1923) — **skipping 0.32.0,
which was never tagged, GitHub-released, or published to PyPI/Docker.**
The last real release is `v0.31.0` (2026-07-09). The plugin/marketplace
manifests were also left at 0.31.0, so version state was split-brained:
```
pyproject.toml / openclaw / sdk-typescript : 0.32.0 <- #2175
plugin.json (x2) / marketplace.json (x2) : 0.31.0
manifest : 0.32.0
```
### Fix
Realign every version-tracked file **and** the RP manifest to the last
real release, **0.31.0**, via the repo's own `scripts/version-sync.py
--version 0.31.0`. Versions only — no code change.
## What happens after merge
1. Release Please runs on `main`, sees `manifest = 0.31.0` + releasable
commits since `v0.31.0`, and **rewrites its release PR (#1923) to
`chore: release 0.32.0`** (bumping every version file).
2. Merging that PR tags `v0.32.0` and fires `release: published`, which
publishes **PyPI + npm (SDK + openclaw) + Docker** at 0.32.0 in one shot
— bringing all registries back in sync.
## Changes Made
- `.release-please-manifest.json` → `0.31.0`
- `pyproject.toml`, `sdk/typescript/package.json`,
`plugins/openclaw/package.json`, `.releasemetadata` → `0.31.0` (via
`version-sync.py`)
- Plugin/marketplace manifests were already `0.31.0` (unchanged).
## Testing
```text
$ python scripts/verify-versions.py
All versions aligned at 0.31.0
```
## Real Behavior Proof
- Environment: local `.venv`.
- Steps: `version-sync.py --version 0.31.0`, reset manifest,
`verify-versions.py`.
- Observed: all 9 version entries aligned at 0.31.0; no CHANGELOG
touched (Changelog Guard passes).
- Not tested: the live release-please recompute (will run on merge —
expected to rewrite #1923 to `chore: release 0.32.0`).
## Note on downstream publish
The release-please workflow only triggers `release.yml`/`docker.yml` if
`RELEASE_PLEASE_TOKEN` (a PAT) is set — with the `GITHUB_TOKEN` fallback
the release is created but downstream publishes don't fire. #1916
shipped 0.31.0 fully via this path, so the PAT was set then; if the
0.32.0 publish doesn't fire on merge, verify that secret still exists.
## 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>
## Description
Clears the current dependency/security-audit blockers that are making
unrelated PRs red:
- `transformers 5.3.0` / `CVE-2026-5241`, fixed by requiring
`transformers>=5.5.0` in the locked optional dependency set.
- `sqlitedict <=2.1.0` via the optional `benchmark` extra's
`lm-eval[api]` dependency. There is no patched `sqlitedict` release, so
this PR removes the published/locked `benchmark` extra instead of
shipping a known-vulnerable transitive dependency.
- `esbuild >=0.27.3,<0.28.1` in the OpenCode plugin lockfile, fixed by
forcing `esbuild@0.28.1` through the OpenCode npm override and
regenerated lockfile.
The benchmark code still invokes `python -m lm_eval`; researchers who
need that harness should install `lm-eval[api]` in their benchmark
environment until its transitive vulnerability has a patched release.
## 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
- `pyproject.toml`: remove the `benchmark` optional extra, document
external `lm-eval[api]` installation guidance, and require
`transformers>=5.5.0`.
- `uv.lock`: regenerate without the `benchmark` extra, removing
`lm-eval` and `sqlitedict` lock entries and locking the patched
transformers floor.
- `plugins/opencode/package.json`: add an `overrides` entry for
`esbuild@0.28.1`.
- `plugins/opencode/package-lock.json`: regenerate the OpenCode lockfile
with `esbuild@0.28.1`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
uv lock --check
rg -n -F 'sqlitedict' uv.lock # no matches
rg -n -F 'name = "lm-eval"' uv.lock # no matches
rg -n -F "extra == 'benchmark'" uv.lock # no matches
rg -n -F '0.27.7' plugins/opencode/package-lock.json plugins/opencode/package.json # no matches
npm ls esbuild --package-lock-only
npm audit --package-lock-only # found 0 vulnerabilities
git diff --check
```
Previous GitHub checks were green. After merging current `main`, fresh
GitHub checks are running again; local targeted validation still passes.
## Real Behavior Proof
- Environment: Windows 11, Python 3.13.3, uv, npm in `plugins/opencode`,
Dependabot/pip-audit alert metadata from the failing PR jobs.
- Exact command / steps: inspected the regenerated Python and npm
lockfiles with `rg`, checked the uv lock with `uv lock --check`, checked
OpenCode's dependency tree with `npm ls esbuild --package-lock-only`,
and ran `npm audit --package-lock-only`.
- Observed result: `uv.lock` no longer contains `sqlitedict`, `lm-eval`,
or a `benchmark` extra marker; `transformers` resolves at the patched
`>=5.5.0` floor; OpenCode's lock resolves `esbuild@0.28.1`; `npm audit
--package-lock-only` reports 0 vulnerabilities; GitHub `Dependency audit
(pip-audit)` passes.
- Not tested: running the external `lm-eval` harness after installing it
separately.
## 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
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] 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 have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A - dependency and lockfile security fix.
## Additional Notes
The `benchmark` extra can be restored once the upstream `lm-eval[api]`
dependency chain stops pulling a vulnerable `sqlitedict` release.
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Description
Harden the release artifact workflow so maintainers can reproduce npm
and Python release checks locally and so OpenClaw packaging does not
depend on a not-yet-published SDK version. This follow-up also keeps the
OpenClaw loader export contract explicit and updates the PR after the
branch was merged with current `headroomlabs/main`.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [x] Documentation update
## Changes Made
- Added reusable local release smoke scripts for npm assets, Python
wheel/sdist artifacts, and the combined release gate.
- Switched the release workflow npm asset build to the reusable npm
builder.
- Made OpenClaw release asset building install against the just-built
local SDK tarball before rewriting packed metadata to the release
dependency range.
- Kept the OpenClaw source dependency registry-installable at
`headroom-ai@^0.22.3` while the packed artifact still ships
`^<release-version>`.
- Added `registerHeadroomPlugin` as a named export while preserving the
default `{ register }` OpenClaw loader contract.
- Added Windows development bootstrap docs/script and regression tests
for version sync, npm asset ordering, OpenClaw source installability,
and Python wheel smoke import isolation.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
uv run --with pytest python -m pytest tests/test_release_workflows.py scripts/tests/test_version_sync.py -q
44 passed, 1 warning
node --check scripts/build_npm_release_assets.mjs
node --check scripts/verify_npm_release_assets.mjs
python -m py_compile scripts/build_python_release_smoke.py scripts/release_smoke_all.py scripts/version-sync.py
python scripts/verify-versions.py
All versions aligned at 0.31.0
npm ci (plugins/openclaw)
added 88 packages, audited 89 packages, found 0 vulnerabilities
python scripts/release_smoke_all.py --out release-assets-local/all-pr1824-postmerge-fixed-20260710-104739
Verified npm release assets for 0.31.0
wheel metadata OK: headroom_ai-0.31.0-cp310-abi3-win_amd64.whl contains headroom/_core.pyd
sdist License-File metadata OK: ['LICENSE', 'NOTICE']
smoke-import OK: version=0.31.0 hello=headroom-core
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.14.3, Node v24.14.0, npm 11.9.0, uv
0.11.15, Rust/Cargo already installed in the local development
environment.
- Exact command / steps: Ran `python scripts/release_smoke_all.py --out
release-assets-local/all-pr1824-postmerge-fixed-20260710-104739` after
syncing this branch with current `headroomlabs/main`.
- Observed result: The command built `headroom-ai-0.31.0.tgz`,
`headroom-openclaw-0.31.0.tgz`,
`headroom_ai-0.31.0-cp310-abi3-win_amd64.whl`, and
`headroom_ai-0.31.0.tar.gz`; npm verifier passed; the wheel installed
into a fresh venv and imported `headroom._core`.
- Not tested: Full GitHub Actions release matrix and publish jobs with
real PyPI/npm/GitHub Packages credentials.
## 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
- [x] 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
## Screenshots (if applicable)
N/A.
## Additional Notes
The post-merge full smoke initially failed because the OpenClaw source
package depended on `headroom-ai@^0.31.0`, which is not available on
public npm yet. The builder now installs OpenClaw against the just-built
local SDK tarball before build/pack, and then rewrites packed metadata
to `^0.31.0`.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
## Description
Current `main` has advanced the core package versions to `0.31.0`, but
the plugin marketplace manifests and hook plugin manifests were still
left at `0.30.0`. This PR now keeps the original version-sync intent
while updating the remaining metadata to the current release line.
It also preserves the previously-added `lxml-html-clean>=0.4.5` security
floor in `pyproject.toml` / `uv.lock` so the security audit remains
unblocked.
Closes#1872
## 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
- Synced `pyproject.toml`, SDK/package manifests, plugin manifests,
marketplaces, `.release-please-manifest.json`, and editable package lock
metadata to `0.31.0`.
- Updated the OpenClaw plugin dependency on `headroom-ai` to `^0.31.0`.
- Merged current `main` and resolved the version metadata conflicts in
favor of current `0.31.0` alignment.
## Testing
- [x] Unit tests pass (`pytest tests/test_plugin_manifests.py
scripts/tests/test_version_sync.py -q`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed (`python scripts/verify-versions.py`)
### Test Output
```text
python scripts/verify-versions.py
All versions aligned at 0.31.0
pytest tests/test_plugin_manifests.py scripts/tests/test_version_sync.py -q
10 passed in 0.42s
```
## Real Behavior Proof
- Environment: Windows, Python 3.13.13, PR head after merging current
`main`.
- Exact command / steps: Ran `python scripts/verify-versions.py` and
`pytest tests/test_plugin_manifests.py
scripts/tests/test_version_sync.py -q`.
- Observed result: Version verification exits successfully with `All
versions aligned at 0.31.0`; focused manifest/version-sync tests pass.
- Not tested: Full wheel/build matrix; this is metadata-only version
alignment and CI will cover the broader matrix.
## 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] My changes generate no new warnings
- [x] New and existing focused tests pass locally with my changes
- [ ] I have made corresponding changes to the documentation
- [ ] I have updated the CHANGELOG.md if applicable
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
## Description
**Fixes the Release Please pipeline**, which stopped opening the release
PR, leaving pip / Docker / npm out of sync.
### Root cause
Recent releases (0.28 → 0.30) were cut **out-of-band** (manual tag +
release), so Release Please's state drifted from reality:
- `.release-please-manifest.json` was frozen at **0.29.0**, and the
in-repo version files at **0.29.0** — even the `v0.30.0` tag commit has
`pyproject.toml = 0.29.0`. The plugin/marketplace manifests were frozen
even further back, at **0.22.3**.
- `v0.30.0` was tagged and published to PyPI (CI stamps the version from
the tag at build time via `version-sync.py`, which is why PyPI got
0.30.0 despite the committed 0.29.0).
- With the manifest at 0.29.0, RP kept computing the next version as
**0.30.0**, saw that tag already exists, and produced **no PR** — so
nothing new could ship, and Docker/npm fell behind.
### Fix
Realign the repo with the last real release (0.30.0) so RP can drive the
next one:
- `.release-please-manifest.json` → `0.30.0`
- `pyproject.toml`, `sdk/typescript/package.json`,
`plugins/openclaw/package.json`, both `plugin.json`, both
`marketplace.json` → `0.30.0` (via `scripts/version-sync.py --version
0.30.0`, which also fixes the 0.22.3 drift).
### What happens after merge
1. Release Please runs on `main`, sees manifest = 0.30.0 + 69 releasable
commits since `v0.30.0`, and opens a clean **`chore: release 0.31.0`**
PR (bumping every version file).
2. Merging that PR tags `v0.31.0` and fires `release: published`, which
publishes **PyPI + npm (SDK + openclaw) + Docker** at 0.31.0 in one shot
— bringing all three registries back in sync.
No behavior/code change — versions only.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Advance the Release Please manifest to the last released version
(0.30.0).
- Sync all 7 version-tracked files to 0.30.0 with the repo's own
`version-sync.py`.
## Testing
- [x] Linting passes (`ruff check .`)
- [x] Manual testing performed (`scripts/verify-versions.py`)
### Test Output
```text
$ python scripts/version-sync.py --version 0.30.0
Version synchronized to 0.30.0
$ python scripts/verify-versions.py
All versions aligned at 0.30.0
Packages: pyproject.toml, plugins/openclaw/package.json, sdk/typescript/package.json,
plugins/headroom-agent-hooks/.claude-plugin/plugin.json,
plugins/headroom-agent-hooks/.github/plugin/plugin.json,
.claude-plugin/marketplace.json, .github/plugin/marketplace.json
```
## Real Behavior Proof
- Environment: local macOS, project `.venv` (Python 3.12.6).
- Exact command / steps: ran `scripts/version-sync.py --version 0.30.0`,
set the RP manifest to 0.30.0, then `scripts/verify-versions.py`.
- Observed result: `verify-versions.py` reports all seven version
locations aligned at 0.30.0; `git diff` shows version-field changes only
(no code). Confirmed PyPI latest is 0.30.0 and a `v0.30.0` tag/release
exists, while the manifest was 0.29.0 — the drift this PR corrects.
- Not tested: the downstream release itself (that runs when the
follow-up `chore: release 0.31.0` PR is merged); npm registry state
could not be read from this environment (network), but the `publish-npm`
job in `release.yml` publishes both npm packages on release.
## 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] My changes generate no new warnings
- [x] New and existing unit tests pass locally with my changes
## 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>
## Description
When `headroom-ai` is installed via `uv tool install headroom-ai`, the
binary lands at `~/.local/bin/headroom`. The plugin's autoStart launcher
detection did not find it because the PATH check used `sh -lc` which may
not source user shell config (`.zshrc`, `.bash_profile`) on all systems.
This PR adds explicit uv path detection and fixes the shell invocation
flag.
Related to #419
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring
## Changes Made
- Switch PATH check from `sh -lc` to `sh -c` in `proxy-manager.ts`
- Add explicit uv tool install detection checking
`~/.local/bin/headroom`
## Testing
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] Manual testing performed
### Test Output
```text
Status: loaded
Version: 0.27.0
Capabilities:
context-engine: headroom
Tools:
headroom_retrieve
```
## Real Behavior Proof
- Environment: EndeavourOS x86_64, OpenClaw 2026.6.10, headroom-ai
0.27.0 via uv
- Exact command / steps: `uv tool install headroom-ai` then `openclaw
gateway restart`
- Observed result: Before fix — autoStart failed with "Headroom proxy
not detected on default endpoints" even though binary exists at
`~/.local/bin/headroom`. After fix — plugin loads and connects
correctly.
- Not tested: Windows, Docker runtime
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Additional Notes
Reproduced while debugging the npm package staleness issue in #419.
Affects any user following the standard `uv tool install` workflow on
Linux/macOS.
## 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.
## 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#1572Closes#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)
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. -->
## Description
Repo hygiene for a public OSS project: removes committed `node_modules`,
stray/internal/draft markdown, and commercial-surface references —
keeping every real doc (the published docs site, the wiki guides, and
all component READMEs) intact. Every file was content-audited before
removal, and load-bearing files were verified against the code/CI and
kept.
Net: **1,695 files changed, +23 / −266,409** (the deletions are
dominated by a committed `node_modules` tree).
Closes # (no tracking issue)
## 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)
- [x] Documentation update
- [x] Code refactoring (no functional changes)
## Changes Made
**Removed (verified to have no code/CI dependencies):**
- `examples/vercel-ai-sdk-pr/` — 1,649 committed `node_modules` files
(zero example source); `node_modules/` added to `.gitignore`.
- `docs/spec/` (23 draft "Living Specification" files — orphaned,
`1.0.0-draft`, drifted from the code), `docs/superpowers/` (2 agent
plans), `docs/proposals/` (2 internal/commercial memos).
- 6 orphan `docs/*.md` (auth-modes, bedrock,
claude-code-vertex-headroom, cortex-code, output-token-reduction-guide,
rtk-loop-weighting).
- `PR.md` (committed PR draft), `ENTERPRISE.md`, `.github/FUNDING.yml`.
**Content scrubs:**
- Removed unreleased "Headroom Cloud" / `api.headroom.ai` / `hr_`
references from `configuration.mdx`, `wiki/configuration.md`,
`wiki/typescript-sdk.md`, `sdk/typescript/README.md` (reworded to
neutral, accurate phrasing).
- Dropped a stale "awaiting maintainer before merge" line from
`plugins/headroom-oauth2/SPEC.md`; tidied `.gitignore` comments (kept
the protective `headroom-managed/` ignore rule).
- Fixed the now-dangling links into removed files (README
nav/`output-token-reduction` link, `scripts/README`, `wiki/vertex`).
**Explicitly KEPT (load-bearing — would orphan in-code citations if
removed):**
- `.changelog.md` — consumed by `.github/workflows/release.yml` (read as
the release-notes file).
- `REALIGNMENT/`, `docs/observability.md`, `docs/rtk-architecture.md`,
`wiki/plans/`, `TESTING-copilot-subscription.md` — referenced by the
Rust core / Python / tests as design docs.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [ ] Type checking passes (`mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
# Docs/markdown + .gitignore only — no Python/Rust source changed, so the
# behavioral test suite is unaffected. Verified the cleanup did not orphan
# references or break the published docs site:
$ git ls-files 'docs/content/docs/*.mdx' | wc -l # published site intact
42
$ # meta.json nav unchanged; no published page removed.
$ grep -rnI "Headroom Cloud|api.headroom.ai|'hr_" $(git ls-files '*.md' '*.mdx')
>>> none
$ # dangling refs to removed files (excl pre-existing P0/P2 spec stubs that
$ # never existed in git): none remaining.
```
## Real Behavior Proof
- Environment: macOS, local git clone of the repo (markdown/.gitignore
changes only — no runtime).
- Exact command / steps: 4 read-only content-audit agents classified
every `.md`/`.mdx` file; each removal candidate was cross-checked
against the codebase (`grep` for citations in `.rs`/`.py`/tests,
workflows, and configs); only files with no dependents were removed; the
tree was re-grepped after removal to confirm no new dangling references;
verified the published docs site page count (`git ls-files
'docs/content/docs/*.mdx' | wc -l` = 42, unchanged).
- Observed result: the 42-page published docs site and all wiki guides
are untouched; no source or workflow references a removed file;
`.changelog.md` (consumed by release.yml) and the code-cited design docs
were detected as dependencies and kept; the committed `node_modules`
tree is removed and `node_modules/` is gitignored so it can't be
re-committed; zero "Headroom Cloud"/`headroom.dev` references remain.
- Not tested: N/A — no executable code changed (only markdown, `.mdx`,
and `.gitignore`), so the behavioral test suite is unaffected.
## 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
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [ ] 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 branch deletes `.github/FUNDING.yml` while PR #1526 edits it —
the two will be sequenced at merge (delete wins).
- A follow-up option (not in this PR): also remove the internal design
docs that are currently cited by the code (`REALIGNMENT/`,
`docs/observability.md`, `docs/rtk-architecture.md`, `wiki/plans/`) —
that requires scrubbing ~15–20 in-code citations so nothing dangles, so
it's deliberately deferred.
- Untracked local working files (`benchmarks/hf_pilot/`,
`tools/copilot-test/`) are intentionally left out of git (not
committed).
## Description
Supply-chain hardening: takes the **shipped** dependency surface from
**26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with
no known vulnerabilities (verified with Anchore syft + grype). Also
publishes a checked-in SBOM package (`sbom/`) so any user — especially
pilots running their own security review — can verify what's inside and
that we track it.
This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7
low at time of writing).
Closes #
## 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)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
**Rust**
- `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp
Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and
added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both
required by the 0.25+ API).
- `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j).
**Python**
- `torch` → 2.12.1, `mem0ai` → 2.x.
- Floor-pinned transitive CVE deps via `[tool.uv]
constraint-dependencies`: `pygments>=2.20.0`,
`pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`.
- **Removed `benchmark` from the `[all]` aggregate** so the default
install is CVE-free. `lm-eval` is invoked as an external subprocess
(`python -m lm_eval`) and never imported, so it is not a true runtime
dep — it remains available via the opt-in `[benchmark]` extra. See
[Accepted Risks](#additional-notes).
**npm (build/test tooling — never shipped in the
wheel/container/published SDK)**
- `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw`
(GHSA-g7r4-m6w7-qqqr).
- `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf),
`postcss` override to force Next.js's bundled copy ≥8.5.10
(GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a
**Critical** vitest/vite.
**CI**
- Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0`
(GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`.
**SBOM**
- New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan
evidence, 330-package license inventory, and a regeneration guide.
## Testing
- [ ] Unit tests pass (`pytest`) — N/A, no Python source changed
(deps/config only)
- [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the
changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is
unaffected
- [x] Type checking passes — `cargo check --workspace` (0 errors)
- [ ] New tests added — N/A (dependency bumps; covered by existing
suites)
- [x] Manual testing performed — see Real Behavior Proof
### Test Output
```text
# headroom-ai[all] product surface — the number that matters
$ grype sbom:sbom/headroom-sbom-all-extra.cdx.json
No vulnerabilities found
# full repo scan (universal lock incl. opt-in [benchmark] + dev)
$ grype sbom:sbom/headroom-sbom.cdx.json
NAME INSTALLED TYPE VULNERABILITY SEVERITY
sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted
nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted
# pyo3 0.29 migration — extension builds + imports + runs
$ cargo check --workspace
Finished `dev` profile [unoptimized + debuginfo] target(s)
$ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..."
extension OK — detach + from_py_object paths exercised
# lru 0.18 — eviction path
$ cargo test -p headroom-proxy --lib drift
14 passed, 213 filtered out
# per-ecosystem npm audits
$ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities
$ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities
$ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found
```
## Real Behavior Proof
- Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust
1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3.
- Exact command / steps: (1) `uv export --extra all --no-dev
--no-emit-project | syft → grype` for the product surface; (2) `cargo
check --workspace` + `maturin develop` + extension import/compress smoke
test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt
--check` + `cargo clippy -p headroom-py`; (5) `npm audit` in
sdk/openclaw/docs + `bun audit` in docs.
- Observed result: `headroom-ai[all]` resolution scans clean — "No
vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2
documented accepted CVEs; pyo3 0.29 extension imports and runs (detach +
from_py_object paths exercised); drift tests 14/14 pass; cargo fmt +
clippy clean; all npm/bun audits report 0.
- Not tested: full `pytest` suite (no Python source changed);
release-profile wheel build (used dev-profile `maturin develop` for the
import proof — the extension is semantically identical).
## 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
- [x] I have made corresponding changes to the documentation
(`sbom/README.md`)
- [x] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective — N/A
(dependency bumps; existing suites + scans cover it)
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md — N/A (Release Please
auto-generates from the conventional commit)
## Additional Notes
**Accepted risks (the 2 residual CVEs).** Both originate solely from the
EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]`
extra**, which Headroom invokes as a subprocess (never imports):
- `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package
abandoned (last release 2021), **no upstream fix exists**.
- `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`;
affects ≤3.9.4 (current latest), **no patched release**.
Neither is in `[all]`, the published wheel, or the container. They are
documented in `sbom/README.md` and will be picked up automatically once
upstream ships fixes.
**Release/CHANGELOG:** N/A items above are because this is a
dependency/security PR with no Python source changes; CHANGELOG is
Release-Please-managed via the conventional commit message.
## Description
Clarifies the OpenCode documentation follow-up for PR #1105 so users can
install `headroom-opencode`, configure provider routing, use the native
plugin, and copy working retrieve/compression helper examples.
## Type of Change
- [x] Documentation update
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
## Changes Made
- Documented how `headroom wrap opencode` wires provider config, MCP
tools, and runtime environment.
- Documented the native `HeadroomPlugin` path, `HEADROOM_PROXY_URL`,
retrieve tooling, and programmatic config helpers.
- Fixed `plugins/opencode/README.md` examples so `compressWithHeadroom`
uses the exported options-object API and `headroom_retrieve` uses
`hash`.
## Testing
- [x] Type checks pass.
- [x] Unit tests pass.
- [x] Whitespace check passes.
### Test Output
```text
plugins/opencode: npm run typecheck
> tsc --noEmit
plugins/opencode: npm test
Test Files 2 passed (2)
Tests 9 passed (9)
docs: npm run types:check
✓ Types generated successfully
repo: git diff --check
(no output)
```
## Real Behavior Proof
- Environment: Local macOS worktree at
`docs/pr-1105-documentation-followup`, Node/npm project commands run
from `plugins/opencode` and `docs`.
- Exact command / steps: Updated the README snippets, ran `npm run
typecheck`, reran `npm test` with elevated permissions after the sandbox
blocked a local `127.0.0.1` listener, ran `npm run types:check` in
`docs`, and ran `git diff --check`.
- Observed result: Typecheck completed with `tsc --noEmit`; the OpenCode
package test suite reported 2 files and 9 tests passed; docs type
generation completed successfully; `git diff --check` produced no
output.
- Not tested: Browser-rendered documentation preview. `docs: npm run
build` was started locally but produced no output for roughly 90 seconds
and was stopped, so this follow-up does not claim a fresh local docs
build result.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Additional Notes
- The linked review comment asked for README examples to match
`compressWithHeadroom(messages, options)` and
`createHeadroomRetrieveTool` requiring `hash`; both snippets now match
the exported API.
## Description
Hardens the bundled OpenClaw plugin so configured proxy routing is
fail-closed and `autoStart` is opt-in.
Closes: N/A
This follow-up is intentionally separate from the ContentRouter cache
fix because it changes plugin/gateway behavior rather than core
compression routing.
The plugin should not mutate upstream provider routing unless a
configured proxy URL is reachable and looks like Headroom. It should
also avoid unhandled startup promise rejections when proxy startup is
fire-and-forget.
Why this shape:
- `autoStart: false` by default matches deployments where Headroom is
supervised externally, for example by systemd. The plugin should not
silently start or assume ownership of a proxy unless the operator opted
in.
- Provider routing is fail-closed: a configured URL must first respond
like Headroom, not merely expose a generic liveness endpoint. This
prevents accidentally routing model traffic through the wrong local
service.
- `/readyz` is treated as liveness, not identity. Identity comes from
Headroom-shaped stats endpoints (`/v1/retrieve/stats` or `/stats`)
because those are harder for unrelated services to satisfy by accident.
- Startup remains asynchronous, but errors are captured and exposed
instead of becoming unhandled promise rejections.
- This is a separate PR because the core cache fix is about compression
correctness, while this patch is about integration safety around
OpenClaw gateway routing.
## Type of Change
- [x] Bug fix (non-breaking change fixes issue)
- [ ] New feature (non-breaking change adds functionality)
- [ ] Breaking change (fix or feature would cause existing functionality
change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Make proxy `autoStart` opt-in (`default: false`).
- Probe configured `proxyUrl` before applying provider routing.
- Treat `/readyz` as liveness only; require Headroom-shaped
`/v1/retrieve/stats` or `/stats` for identity.
- Observe fire-and-forget startup promise rejection and expose startup
error for callers.
- Isolate proxy-ready listener failures.
- Keep provider routing deferred when no active/probed Headroom proxy
exists.
- Register retrieve tool with explicit `headroom_retrieve` name.
- Extend plugin/unit tests for configured proxy failures, generic
non-Headroom endpoints, path collisions, and routing behavior.
Changed files:
- `plugins/openclaw/README.md`
- `plugins/openclaw/openclaw.plugin.json`
- `plugins/openclaw/src/engine.ts`
- `plugins/openclaw/src/plugin/index.ts`
- `plugins/openclaw/src/proxy-manager.ts`
- `plugins/openclaw/test/engine.test.ts`
- `plugins/openclaw/test/gateway-config.test.ts`
- `plugins/openclaw/test/plugin-runtime-routing.test.ts`
- `plugins/openclaw/test/proxy-manager.test.ts`
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added new functionality
- [x] Manual testing performed
### Test Output
```text
$ npm test
Test Files 6 passed (6)
Tests 74 passed (74)
$ npm run typecheck
tsc --noEmit
$ npm run build
tsup && node prepare-dist.mjs
Build success
```
## Real Behavior Proof
- Environment: local OpenClaw plugin package in the Headroom repo.
- Exact command / steps:
- Run plugin test suite.
- Run TypeScript typecheck.
- Run plugin build.
- Observed result:
- Tests passed: `74/74`.
- Typecheck passed.
- Build passed.
- Not tested:
- Full OpenClaw Gateway integration as part of this standalone PR prep.
## Review Readiness
- [x] I performed self-review
- [x] This PR ready for human review
## Checklist
- [x] My code follows project's style guidelines
- [x] I performed self-review my code
- [ ] I commented my code, particularly in hard-to-understand areas
- [x] I made corresponding changes documentation
- [x] My changes generate no new warnings
- [x] I added tests prove fix is effective or feature works
- [x] New and existing unit tests pass locally my changes
- [ ] I updated CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A.
## Additional Notes
Checklist items left unchecked intentionally:
- No CHANGELOG update included.
- No extra comments were needed beyond existing code structure.
Co-authored-by: Björn-Christian Bönkost <bjoern@v2202603344248440850.hotsrv.de>
## 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>
## Description
The `headroom-openclaw` plugin silently fails to load on OpenClaw
2026.x. OpenClaw's plugin loader calls `setupRegistration.register(api)`
on the plugin's default export. The current plugin exports a **bare
function** as its default — which has no `.register()` method — so the
loader skips it silently and the plugin never initializes. No error is
thrown, no warning logged. The plugin appears "enabled" in the registry
but does nothing.
Fix: wrap the function in a `{ register: headroomPlugin }` object. One
structural change, plugin body unchanged.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- Wrapped `export default function headroomPlugin(api)` in a `{
register: headroomPlugin }` object so OpenClaw's loader can call
`.register(api)` on it
## Testing
- [ ] Unit tests pass (`pytest`)
- [ ] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [ ] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ node -e "
const mod = require('./plugins/openclaw/dist/index.js');
const plugin = mod.default;
console.log('type:', typeof plugin);
console.log('has register:', typeof plugin.register);
const mockApi = {
config: { plugins: { entries: { headroom: { config: { proxyUrl: 'http://127.0.0.1:8787' } } } } },
logger: { info: (m) => console.log('INFO:', m), warn: console.warn, error: console.error },
registerContextEngine: (id) => console.log('registerContextEngine:', id),
registerTool: () => console.log('registerTool called'),
on: (e) => console.log('on:', e),
};
plugin.register(mockApi);
"
type: object
has register: function
registerContextEngine: headroom
registerTool called
on: gateway_start
INFO: [headroom] Plugin registered
INFO: Headroom proxy ready at http://127.0.0.1:8787
```
## Real Behavior Proof
- Environment: macOS 15.x arm64, OpenClaw 2026.6.8, Node.js v25.8.0,
headroom-openclaw 0.1.0
- Exact command / steps: `openclaw plugins install headroom-ai/openclaw`
→ restart OpenClaw → check gateway logs for `[headroom]` entries → check
`curl http://localhost:8787/stats` for `api_requests > 0`
- Observed result: Before fix — no `[headroom]` log entries, proxy never
started, `api_requests: 0`. After fix — `[headroom] Plugin registered`
in gateway log, proxy starts, `api_requests: 135`, `$10.11` saved in one
session.
- Observed result (after fix): `[headroom] Plugin registered` in gateway
log. Proxy starts on port 8787. Real Anthropic API calls intercepted —
`/stats` showed `api_requests: 135`, `total_saved_usd: 10.11` after one
session.
- Not tested: Windows, Linux
## 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
- [ ] 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
Found while integrating headroom into
[Vespera](https://github.com/problemsolverai2026-svg/vespera), a
persistent local AI system. The workaround was manually setting
`models.providers.anthropic.baseUrl = "http://127.0.0.1:8787"` in
OpenClaw config — which works but bypasses the plugin entirely. The
proper plugin path should work out of the box.
Bumps the npm_and_yarn group with 1 update in the /docs directory:
[esbuild](https://github.com/evanw/esbuild).
Updates `esbuild` from 0.27.7 to 0.28.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/evanw/esbuild/releases">esbuild's
releases</a>.</em></p>
<blockquote>
<h2>v0.28.1</h2>
<ul>
<li>
<p>Disallow <code>\</code> in local development server HTTP requests (<a
href="https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr">GHSA-g7r4-m6w7-qqqr</a>)</p>
<p>This release fixes a security issue where HTTP requests to esbuild's
local development server could traverse outside of the serve directory
on Windows using a <code>\</code> backslash character. It happened due
to the use of Go's <code>path.Clean()</code> function, which only
handles Unix-style <code>/</code> characters. HTTP requests with paths
containing <code>\</code> are no longer allowed.</p>
<p>Thanks to <a
href="https://github.com/dellalibera"><code>@dellalibera</code></a> for
reporting this issue.</p>
</li>
<li>
<p>Add integrity checks to the Deno API (<a
href="https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr">GHSA-gv7w-rqvm-qjhr</a>)</p>
<p>The previous release of esbuild added integrity checks to esbuild's
npm install script. This release also adds integrity checks to esbuild's
Deno install script. Now esbuild's Deno API will also fail with an error
if the downloaded esbuild binary contains something other than the
expected content.</p>
<p>Note that esbuild's Deno API installs from
<code>registry.npmjs.org</code> by default, but allows the
<code>NPM_CONFIG_REGISTRY</code> environment variable to override this
with a custom package registry. This change means that the esbuild
executable served by <code>NPM_CONFIG_REGISTRY</code> must now match the
expected content.</p>
<p>Thanks to <a
href="https://github.com/sondt99"><code>@sondt99</code></a> for
reporting this issue.</p>
</li>
<li>
<p>Avoid inlining <code>using</code> and <code>await using</code>
declarations (<a
href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>)</p>
<p>Previously esbuild's minifier sometimes incorrectly inlined
<code>using</code> and <code>await using</code> declarations into
subsequent uses of that declaration, which then fails to dispose of the
resource correctly. This bug happened because inlining was done for
<code>let</code> and <code>const</code> declarations by avoiding doing
it for <code>var</code> declarations, which no longer worked when more
declaration types were added. Here's an example:</p>
<pre lang="js"><code>// Original code
{
using x = new Resource()
x.activate()
}
<p>// Old output (with --minify)<br />
new Resource().activate();</p>
<p>// New output (with --minify)<br />
{using e=new Resource;e.activate()}<br />
</code></pre></p>
</li>
<li>
<p>Fix module evaluation when an error is thrown (<a
href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>,
<a
href="https://redirect.github.com/evanw/esbuild/pull/4467">#4467</a>)</p>
<p>If an error is thrown during module evaluation, esbuild previously
didn't preserve the state of the module for subsequent module
references. This was observable if <code>import()</code> or
<code>require()</code> is used to import a module multiple times. The
thrown error is supposed to be thrown by every call to
<code>import()</code> or <code>require()</code>, not just the first.
With this release, esbuild will now throw the same error every time you
call <code>import()</code> or <code>require()</code> on a module that
throws during its evaluation.</p>
</li>
<li>
<p>Fix some edge cases around the <code>new</code> operator (<a
href="https://redirect.github.com/evanw/esbuild/issues/4477">#4477</a>)</p>
<p>Previously esbuild incorrectly printed certain edge cases involving
complex expressions inside the target of a <code>new</code> expression
(specifically an optional chain and/or a tagged template literal). The
generated code for the <code>new</code> target was not correctly wrapped
with parentheses, and either contained a syntax error or had different
semantics. These edge cases have been fixed so that they now correctly
wrap the <code>new</code> target in parentheses. Here is an example of
some affected code:</p>
<pre lang="js"><code>// Original code
new (foo()`bar`)()
new (foo()?.bar)()
<p>// Old output<br />
new foo()<code>bar</code>();<br />
new (foo())?.bar();</p>
<p></code></pre></p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/evanw/esbuild/blob/main/CHANGELOG.md">esbuild's
changelog</a>.</em></p>
<blockquote>
<h2>0.28.1</h2>
<ul>
<li>
<p>Disallow <code>\</code> in local development server HTTP requests (<a
href="https://github.com/evanw/esbuild/security/advisories/GHSA-g7r4-m6w7-qqqr">GHSA-g7r4-m6w7-qqqr</a>)</p>
<p>This release fixes a security issue where HTTP requests to esbuild's
local development server could traverse outside of the serve directory
on Windows using a <code>\</code> backslash character. It happened due
to the use of Go's <code>path.Clean()</code> function, which only
handles Unix-style <code>/</code> characters. HTTP requests with paths
containing <code>\</code> are no longer allowed.</p>
<p>Thanks to <a
href="https://github.com/dellalibera"><code>@dellalibera</code></a> for
reporting this issue.</p>
</li>
<li>
<p>Add integrity checks to the Deno API (<a
href="https://github.com/evanw/esbuild/security/advisories/GHSA-gv7w-rqvm-qjhr">GHSA-gv7w-rqvm-qjhr</a>)</p>
<p>The previous release of esbuild added integrity checks to esbuild's
npm install script. This release also adds integrity checks to esbuild's
Deno install script. Now esbuild's Deno API will also fail with an error
if the downloaded esbuild binary contains something other than the
expected content.</p>
<p>Note that esbuild's Deno API installs from
<code>registry.npmjs.org</code> by default, but allows the
<code>NPM_CONFIG_REGISTRY</code> environment variable to override this
with a custom package registry. This change means that the esbuild
executable served by <code>NPM_CONFIG_REGISTRY</code> must now match the
expected content.</p>
<p>Thanks to <a
href="https://github.com/sondt99"><code>@sondt99</code></a> for
reporting this issue.</p>
</li>
<li>
<p>Avoid inlining <code>using</code> and <code>await using</code>
declarations (<a
href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>)</p>
<p>Previously esbuild's minifier sometimes incorrectly inlined
<code>using</code> and <code>await using</code> declarations into
subsequent uses of that declaration, which then fails to dispose of the
resource correctly. This bug happened because inlining was done for
<code>let</code> and <code>const</code> declarations by avoiding doing
it for <code>var</code> declarations, which no longer worked when more
declaration types were added. Here's an example:</p>
<pre lang="js"><code>// Original code
{
using x = new Resource()
x.activate()
}
<p>// Old output (with --minify)<br />
new Resource().activate();</p>
<p>// New output (with --minify)<br />
{using e=new Resource;e.activate()}<br />
</code></pre></p>
</li>
<li>
<p>Fix module evaluation when an error is thrown (<a
href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>,
<a
href="https://redirect.github.com/evanw/esbuild/pull/4467">#4467</a>)</p>
<p>If an error is thrown during module evaluation, esbuild previously
didn't preserve the state of the module for subsequent module
references. This was observable if <code>import()</code> or
<code>require()</code> is used to import a module multiple times. The
thrown error is supposed to be thrown by every call to
<code>import()</code> or <code>require()</code>, not just the first.
With this release, esbuild will now throw the same error every time you
call <code>import()</code> or <code>require()</code> on a module that
throws during its evaluation.</p>
</li>
<li>
<p>Fix some edge cases around the <code>new</code> operator (<a
href="https://redirect.github.com/evanw/esbuild/issues/4477">#4477</a>)</p>
<p>Previously esbuild incorrectly printed certain edge cases involving
complex expressions inside the target of a <code>new</code> expression
(specifically an optional chain and/or a tagged template literal). The
generated code for the <code>new</code> target was not correctly wrapped
with parentheses, and either contained a syntax error or had different
semantics. These edge cases have been fixed so that they now correctly
wrap the <code>new</code> target in parentheses. Here is an example of
some affected code:</p>
<pre lang="js"><code>// Original code
new (foo()`bar`)()
new (foo()?.bar)()
<p>// Old output<br />
new foo()<code>bar</code>();<br />
new (foo())?.bar();<br />
</code></pre></p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="bb9db84c02"><code>bb9db84</code></a>
publish 0.28.1 to npm</li>
<li><a
href="9ff053e53b"><code>9ff053e</code></a>
security: add integrity checks to the Deno API</li>
<li><a
href="0a9bf2135b"><code>0a9bf21</code></a>
enforce non-negative size in gzip parser</li>
<li><a
href="e2a1a71320"><code>e2a1a71</code></a>
security: forbid <code>\\</code> in local dev server requests</li>
<li><a
href="83a2cbfc35"><code>83a2cbf</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4482">#4482</a>:
don't inline <code>using</code> declarations</li>
<li><a
href="308ad745d8"><code>308ad74</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4471">#4471</a>:
renaming of nested <code>var</code> declarations</li>
<li><a
href="f013f5f99a"><code>f013f5f</code></a>
fix some typos</li>
<li><a
href="aafd6e48b1"><code>aafd6e4</code></a>
chore: fix some minor issues in comments (<a
href="https://redirect.github.com/evanw/esbuild/issues/4462">#4462</a>)</li>
<li><a
href="15300c30b5"><code>15300c3</code></a>
follow up: cjs evaluation fixes</li>
<li><a
href="1bda0c31d7"><code>1bda0c3</code></a>
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4461">#4461</a>,
fix <a
href="https://redirect.github.com/evanw/esbuild/issues/4467">#4467</a>:
esm evaluation fixes</li>
<li>Additional commits viewable in <a
href="https://github.com/evanw/esbuild/compare/v0.27.7...v0.28.1">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/chopratejas/headroom/network/alerts).
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary
Implements the Hermes-side retrieval plugin proposed in #796 (as invited
— thanks for the quick response!).
When Hermes routes traffic through `headroom proxy`, compressed markers
are a one-way street: Hermes registers its own tools, so it never gets
the `headroom_retrieve` CCR tool that Claude Code receives via MCP
injection. In practice the model either re-runs the original command or
— observed in the wild — treats `ccr:abc123` as a file path and tries to
`cat` it.
This plugin uses Hermes's user-plugin system (`~/.hermes/plugins/`) to
register a native `headroom_retrieve` tool that calls the proxy's `POST
/v1/retrieve` endpoint.
## What's included
- `plugins/hermes/headroom_retrieve/` — `plugin.yaml` + `__init__.py`
(single-file, httpx, ~100 lines)
- `plugins/hermes/README.md` — install steps and proxy-side
recommendations
## Design notes
- **Both marker formats covered**: Kompress emits `[N items compressed
... hash=KEY]`, SmartCrusher's opaque-blob walker emits
`<<ccr:HASH[,KIND,SIZE]>>`. The tool description teaches both and
explicitly says markers are NOT file paths; the handler normalizes
whole-marker input (`<<ccr:abc,base64,4.5KB>>` → `abc`).
- **Re-compression loop guard**: retrieved originals travel back through
the proxy on the next request and get re-compressed into a fresh marker,
looping forever. README documents
`HEADROOM_EXCLUDE_TOOLS=read_file,headroom_retrieve` as the fix (Hermes
tool names don't match `DEFAULT_EXCLUDE_TOOLS`, which targets Claude
Code's `Read`/`Grep`/...).
- **Actionable failure modes**: 404 (TTL expired / proxy restarted) and
connection-refused both return guidance to re-run the original command
rather than retry.
## Relationship to existing PRs
Complementary to #707 / #556 (`headroom wrap hermes`, proxy-side): those
launch/route Hermes through the proxy; this gives the agent the
retrieval capability once it's routed. Notably #707 disables CCR tool
injection in Hermes mode precisely because Hermes must register its own
tool — this plugin is that registration.
## Testing
Running in production on macOS (headroom 0.23.0, pipx) and Linux
(0.22.4, systemd) for a day. Verified: fresh-marker retrieval roundtrip,
whole-marker hash normalization (6 input shapes), expired-hash 404
messaging, proxy-down messaging, and end-to-end via live Hermes sessions
(fresh ≥500B `read_file` returns original with the documented exclude
config).
Closes#796🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: akb4q <zhunyunjiang@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## What & why
Adds **`headroom-oauth2`** under `plugins/` — a generic, vendor-neutral
proxy extension that mints an OAuth2 **client-credentials** (RFC 6749
§4.4) bearer from a configured token endpoint and injects it as the
upstream `Authorization` on each proxied request, via the opt-in
`headroom.proxy_extension` seam. **No core changes.**
This lets headroom front any gateway that requires a *minted,
short-lived machine token* rather than a static API key. It complements
**#510** (env-var/static-key auth) rather than replacing it.
Implements **#778** (feature request). Opening the implementation
alongside the issue so there's something concrete to react to — **happy
to hold/rework pending a 👍 from a maintainer**, per CONTRIBUTING.
## Spec
Full spec in
[`plugins/headroom-oauth2/SPEC.md`](plugins/headroom-oauth2/SPEC.md)
(API surface, behavior/compat, user stories, failure modes, resilience
incl. multi-process, security, observability, rollback). Highlights:
- **Opt-in & no-op by default:** dormant until `--proxy-extension
oauth2`, and a no-op unless `HEADROOM_OAUTH2_TOKEN_URL` is set. No
change to defaults, body, routing, or compression.
- **Config is 100% env** (no new CLI flags):
token_url/client_id/secret/scopes/audience, RFC 8707 `resource`,
`post`|`basic` auth style, static upstream headers, timeout/skew.
- **Token caching + single-flight refresh**; `expires_in` clamped to a
positive TTL.
- **Fails closed** on misconfig; returns `502 upstream_auth_error` on
mint failure **without leaking the IdP error body**; `token_url` is
**https-enforced** (loopback exempt for tests).
- **Standard-library only** (token minted via `urllib` → system cert
store, so it works behind corporate SSL inspection). `litellm` is
touched only for static headers and is an optional extra, not a core
dep.
- **Effective for** OpenAI-compatible / passthrough litellm backends.
`bedrock`/`vertex`/`sagemaker` auth from env and ignore a forwarded
bearer → the extension **warns loudly** and is a no-op there.
## Tests
37 tests covering behavior **and** failure modes (`ruff check`/`format`
clean, **98% coverage**): post/basic mint, caching, single-flight (cold
+ on-refresh, exact mint counts under concurrency), https enforcement +
`localhost` rejection + `::1`, `expires_in`
clamp/float/missing/non-numeric, `extra_params` cannot clobber canonical
fields, bad-status/non-JSON/no-token/unreachable (asserting no
secret/body leak), ASGI middleware (inject, non-http passthrough, 502 +
`no-store`, missing `headers` key), and `install()`
(no-op/fail-closed/bad-timeout/env-auth-backend-warning/static-headers).
## Real behavior proof
- **Setup:** Linux aarch64, Python 3.13.5, `headroom-ai` 0.23.0, real
`headroom proxy` process.
- **Steps:** started `headroom proxy --backend litellm-openai
--proxy-extension oauth2` with `HEADROOM_OAUTH2_*` env pointed at a
local OAuth2 token endpoint; an upstream echo server captured what the
backend received; sent two `/v1/messages` requests through the proxy.
- **Observed (copied output):**
```
PROXY: headroom-oauth2: client-credentials auth installed
(token_url=…/token, style=post)
MINTS (across 2 requests): 1 # token cached + reused -> 1 mint for 2
requests
UPSTREAM RECEIVED: auth=Bearer MINTED-FROM-IDP-…
static=generic-static-header
SECRET LEAK CHECK (client_secret in proxy logs): 0
```
→ The minted bearer (not the placeholder backend key) and the configured
static header reached the upstream; the client's inbound credential was
replaced; the client secret never appeared in logs; caching worked.
- **What I did *not* test:** a live commercial IdP
(Entra/Okta/Auth0/etc.) and a live cloud gateway — the token endpoint
and upstream here are local stand-ins. Also not tested: multi-worker
(gunicorn) deployment, and Python 3.10/3.11 (developed on 3.13).
## Placement
Proposed as a standalone installable package under
`plugins/headroom-oauth2/` (registers via the entry-point seam; `pip
install -e plugins/headroom-oauth2`). Open to baking it into core or
publishing it separately — maintainer's call.
The repo had drifted: pyproject.toml said 0.9.1 but PyPI's latest
published headroom-ai was 0.22.3. release_version.py papered over
this by taking max(canonical, latest_tag) at release time;
release-please does NOT do that — it trusts the manifest verbatim.
Left as-is, release-please would propose 0.9.2 on the next merge
and PyPI would reject it ("400 Cannot publish version lower than
latest"), looping the bot forever.
Fix: align every version-bearing file to 0.22.3 (the truth on
PyPI). Done via `scripts/version-sync.py --version 0.22.3`:
- .release-please-manifest.json
- pyproject.toml
- sdk/typescript/package.json
- plugins/openclaw/package.json (+ headroom-ai dep range -> ^0.22.3)
- .claude-plugin/marketplace.json
- .github/plugin/marketplace.json
- plugins/headroom-agent-hooks/.claude-plugin/plugin.json
- plugins/headroom-agent-hooks/.github/plugin/plugin.json
After this lands, the bot's next release PR will propose 0.22.4
(patch) or 0.23.0 (minor) depending on conventional-commit traffic
since v0.22.3.
Three fixes bundled; all in admin / cache-hit paths where tests didn't
catch the regression.
## (A) 13 RequestOutcome sites missing tags=
An AST audit found that 13 of 21 ``RequestOutcome(...)`` construction
sites across the four handler files emitted outcomes without threading
``tags=``. Affected paths:
* ``handle_anthropic_messages`` — the ``from_response_cache=True``
early-return outcome (Claude Code cache-hit turns dashboard-blind)
* ``handle_openai_chat`` — same cache-hit early-return (Codex +
Cursor + Continue cache-hit turns dashboard-blind)
* ``handle_openai_responses_ws`` — the per-turn outcome inside the
Codex WS session. The stale comment that said "ws_session_tags is
not yet bound" was wrong — ``ws_tags`` was already extracted at
handler entry
* ``handle_anthropic_batch_create / batch_passthrough / batch_results``
* ``handle_passthrough`` (OpenAI Models / Files / List-Batches)
* ``handle_google_batch_create / batch_passthrough / batch_results``
* ``_google_batch_passthrough`` (internal helper)
* ``handle_batch_create`` (OpenAI batch entry)
* ``handle_gemini_count_tokens`` (also fixed in #479; identical)
Pattern of the fix is uniform: pull tags from headers and thread
them into the ``RequestOutcome`` construction.
New contract test ``test_handler_outcome_tag_invariant.py`` walks each
handler file's AST and asserts every ``RequestOutcome`` site inside any
``handle_*`` or ``*_passthrough`` method passes both ``tags=`` and
``client=``. Future handlers get a clear test failure with file +
line + method name if they regress.
## (B) Issue #478 — /v1/models 403 under Codex ChatGPT auth
Codex Desktop with ChatGPT-subscription OAuth polls ``/v1/models`` to
populate its model picker. Forwarding to ``chatgpt.com/backend-api/
models`` returned 403 to OAuth tokens. Fix: synthesize an OpenAI-
compatible payload locally from a known-supported model set
(``gpt-5.5`` through ``gpt-5``). All other ChatGPT-auth paths still
forward as before — only model-metadata gets the local response.
## (C) Move _extract_tags to free function (mixin-isolation test compat)
Handlers called ``self._extract_tags(headers)``. That worked in
production where ``HeadroomProxy`` composes every mixin and defines
the method, but broke tests that instantiate a single mixin via
``object.__new__(OpenAIHandlerMixin)``. The free-function form
removes that coupling — handlers import ``extract_tags`` from
``headroom.proxy.helpers`` and call directly. ``HeadroomProxy.
_extract_tags`` is kept as a thin wrapper for any external caller
still using the method form. 17 call sites migrated.
## Zero behavior change for existing users
Claude Code, Codex, Cursor, Continue, Aider, Gemini-routed harnesses
all hit handlers that already extracted tags. Their wire bytes to
upstream LLMs are byte-identical. Only the dashboard view gains tags
on previously-blind paths.
Closes#478.
Adds ``CompressionDecision.apply_to_tags(tags)`` — a one-liner mutator
that stamps the passthrough reason into a tags dict for downstream
observability. Each migrated handler now calls
``_decision.apply_to_tags(tags)`` immediately after
``CompressionDecision.decide(...)``. The tags dict flows unchanged
into every downstream ``RequestOutcome(tags=tags, ...)`` construction,
which the funnel surfaces in ``RequestLog.tags`` — same mechanism the
funnel already uses for ``client``.
Dashboards can now slice passthrough traffic by cause:
* tags["passthrough_reason"] == "bypass_header"
* tags["passthrough_reason"] == "compression_disabled"
* tags["passthrough_reason"] == "no_messages"
* tags["passthrough_reason"] == "license_denied"
No-op when ``should_compress=True`` — compressing requests don't
carry the tag, so absence vs presence is itself the signal.
Bonus fix: ``handle_gemini_count_tokens`` was the one Gemini handler
that never pulled tags out of headers, so its emitted
``RequestOutcome`` reached the dashboard without any of the per-
request slicing keys. Added the missing ``tags = self._extract_tags
(request.headers)`` and threaded ``tags=tags`` into its outcome.
Closes the observability loop opened by PR #477: the four Gemini-
bypass-bug fixes are now visible in the request-log feed the moment
they fire.
Three streaming finalizers — ``_finalize_stream_response``,
``_stream_response_bedrock``, ``_stream_openai_via_backend`` — each
duplicated the same set of body- and config-derived fields when
constructing a ``RequestOutcome``:
* ``attempted_input_tokens = optimized_tokens + tokens_saved``
* ``num_messages = len(body.get("messages", []))``
* ``request_messages`` conditional on ``config.log_full_messages``
* ``transforms_applied`` list → tuple (frozen-dataclass contract)
* ``tags or {}`` normalization
* ``turn_id`` via ``compute_turn_id``
The last one was a real bug. Only the Bedrock site computed
``turn_id`` — sites 1 and 3 silently dropped it, breaking the
dashboard's multi-turn-session grouping for every Anthropic-SSE and
OpenAI-via-backend request. The new ``RequestOutcome.from_stream``
classmethod computes it uniformly so the three finalizers cannot
drift apart on derivation logic again.
Each call site now hands ``from_stream`` the body + provider-specific
cache/timing fields and gets a fully-constructed outcome back. The
funnel call after it stays identical (``await
self._record_request_outcome(outcome)``).
PyPI rejected the v0.21.37 release publish with:
HTTPError: 400 Bad Request from https://upload.pypi.org/legacy/
Project size too large. Limit for project 'headroom-ai' total size is 10 GB.
PyPI inventory check confirmed: **191 versions × ~213 MB/release =
10.00 GB exactly** — at the cumulative project storage ceiling. Each
recent release ships 12 wheels × ~16-18 MB each.
Post-mortem inspection of a production wheel
(``headroom_ai-0.21.36-cp311-cp311-manylinux_2_28_x86_64.whl``)
showed the binary was ``not stripped``:
.text 18.3 MB (code)
.rodata 11.4 MB (Magika model + ONNX runtime data)
.strtab 4.9 MB (debug strings — strippable)
.eh_frame 1.9 MB (unwind tables)
.symtab 1.5 MB (debug symbols — strippable)
.gcc_except_table 1.2 MB
This commit adds a release profile:
[profile.release]
strip = "symbols"
lto = "thin"
codegen-units = 1
That:
* Strips ``.symtab`` + ``.strtab`` (~6.4 MB direct savings per wheel)
* Enables thin link-time optimization for cross-crate dead-code
elimination (~5-10% ``.text`` savings)
* Single codegen unit for better inlining + DCE at the cost of
~30-50% slower release builds (acceptable for CI)
Deliberately NOT setting ``panic = "abort"``:
* The proxy is a long-lived async process. A panic on one bad
request triggering process abort would disconnect every concurrent
client. Accept the smaller savings; keep unwind behaviour.
Estimated impact
* Per wheel: ~16-18 MB → ~10-11 MB (40% smaller)
* Per release (12 wheels): ~213 MB → ~130 MB
* PyPI capacity: ~30+ more releases before hitting 10 GB again
Verification
* Local build of ``headroom._core`` with new profile:
``.so`` size 29 MB on macOS arm64 (was ~45 MB pre-fix; final wheel
compressed will be smaller on Linux which also benefits from the
``strip`` directive).
* 77 Rust-parity tests pass — extension still functional.
* Single-codegen-unit slows build by ~30-50% but maturin/cibuildwheel
build time was never the bottleneck.
Forward strategy (separate work)
* Submit a PyPI project-size-limit-increase request to unblock the
immediate release.
* Adopt a release-deprecation policy: yank versions older than N
patches per minor; consider dropping Python 3.10 wheels (EOL'd
October 2026) and manylinux_2_28_aarch64 wheels (niche audience,
largest at 18.75 MB).
* Investigate runtime-download for Magika model (~10 MB further
savings) — same pattern Kompress already uses.