mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
15 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d7b25ae3bb
|
fix(wrap/serena): install Serena from the serena-agent PyPI wheel, not the git source
## Description `headroom/mcp_registry/install.py` (`build_serena_spec`) and the wrap-time Serena pre-index in `headroom/cli/wrap.py` both ran: ``` uvx --from git+https://github.com/oraios/serena serena ... ``` The git source forces a from-source build. On proot-based filesystems (Termux + proot-distro on Android, some restricted Linux) `uv` cannot hardlink build dependencies into a fresh build venv, so the build fails immediately and Serena's MCP server fails to start on every `headroom wrap codex` launch: ``` × Failed to download and build `serena-agent @ git+https://github.com/oraios/serena@<commit>` ╰─▶ failed to hardlink file ... Operation not permitted (os error 1) ``` Setting `UV_LINK_MODE=copy` fixes it in an interactive shell, but Codex strips most env vars from the MCP subprocesses it spawns, so that workaround does not reliably reach Serena's launch. Serena publishes the official `serena-agent` package to PyPI with prebuilt wheels, and it exposes the same `serena` console script (`serena = "serena.cli:top_level"` in the project's `pyproject.toml`), so `uvx --from serena-agent serena ...` runs the identical command without a build step. On platforms where the git build already worked there is no functional difference. Fixes #2871 ## 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/mcp_registry/install.py` (`build_serena_spec`): `--from git+https://github.com/oraios/serena` -> `--from serena-agent`. - `headroom/cli/wrap.py` (Serena `project index` pre-warm): same swap. - `tests/test_mcp_registry/test_install.py`: updated the spec assertion and added `test_build_serena_spec_uses_pypi_not_git_source` (asserts `serena-agent` is used and no `git+` source remains). - `tests/test_cli/test_wrap_serena_boost.py`: the pre-index test now asserts `serena-agent` is in the command and the git source is not. ## 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 # Fail-before (source swap stashed, updated tests kept): tests/test_mcp_registry/test_install.py::test_build_serena_spec_uses_agent_context FAILED tests/test_mcp_registry/test_install.py::test_build_serena_spec_uses_pypi_not_git_source FAILED tests/test_cli/test_wrap_serena_boost.py::test_preindex_runs_serena_in_cwd FAILED # Pass-after: tests/test_mcp_registry/ tests/test_cli/test_wrap_serena_boost.py tests/test_cli/test_serena_migrate.py tests/test_cli/test_serena_disable.py 135 passed # uvx ruff@0.15.17 check -> All checks passed! # uvx mypy@1.20.2 headroom/mcp_registry/install.py -> Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12.11, project venv, pytest 9.1.1, ruff 0.15.17 and mypy 1.20.2 via uvx. - Exact command / steps: confirmed `serena-agent` exists on PyPI (v1.6.1, homepage github.com/oraios/serena) and that its `pyproject.toml` declares `[project.scripts] serena = "serena.cli:top_level"`, so the `serena start-mcp-server ...` invocation is unchanged. Swapped both `--from` sources, then fail-before with `git stash push headroom/mcp_registry/install.py headroom/cli/wrap.py` (the two production-asserting tests fail on the old git source) and pass-after with `git stash pop` (135 serena-suite tests pass). Verified no `git+https://github.com/oraios/serena` references remain in `headroom/`. - Observed result: `build_serena_spec` and the pre-index command now install Serena from the `serena-agent` PyPI wheel, so a proot environment gets the prebuilt wheel instead of a from-source build that cannot hardlink. The migration/ledger tests, which use the old git spec as a deliberately-stale fixture, are unaffected. - Not tested: a live `headroom wrap codex` on a real proot/Termux device (not available here). The change is a package-source swap verified against Serena's own published package metadata and the existing spec/command tests. ## 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) ## Additional Notes The git source was unpinned (tracked the repo default branch), so switching to `serena-agent` from PyPI does not lose a version pin; if anything it is more reproducible. The issue reporter also noted that `headroom wrap codex` force-rewrites the Serena block in `~/.codex/config.toml` from this template on every launch, which is why the fix has to live in the package source rather than a user config edit -- this PR puts it there. |
||
|
|
420dc9077b
|
feat(grok-build): add Grok Build wrap command and MCP integration (#1629)
## Description
Adds first-class Grok Build support to Headroom so Grok CLI sessions can
route through the local proxy for context compression and savings
tracking.
This PR introduces `headroom wrap grok-build` / `headroom unwrap
grok-build`, a `grok_build` provider slice, Grok MCP registrar support,
and install/telemetry wiring so Grok traffic is attributed correctly in
the proxy and dashboard.
Review follow-up (`9368c413`): when users already own
`[model.grok-build]` in `~/.grok/config.toml`, wrap rewrites `base_url`
in that table in place instead of appending a duplicate header (invalid
TOML).
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
## Changes Made
- Added `headroom/providers/grok_build/` with runtime helpers,
reversible `~/.grok/config.toml` injection, and install env builders.
- Added `headroom wrap grok-build` and `headroom unwrap grok-build` CLI
commands.
- Added `GrokRegistrar` for Headroom MCP registration in Grok config.
- Wired `grok_build` into install planner/registry, agent savings,
telemetry, and proxy client detection (`grok/` user agent).
- **Review fix:** rewrite `base_url` inside an existing user-owned
`[model.grok-build]` table in place (`# was: …` metadata).
- Added regression tests + docs (`grok-build.mdx`, `proxy.mdx`) and
CHANGELOG entry.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest -q tests/test_provider_grok_build.py tests/test_mcp_registry/test_grok_registrar.py
============================== 12 passed in 1.13s ==============================
```
See **Screenshots** below for terminal captures (pytest, review-fix
in-place rewrite, proxy `/readyz`, unwrap).
## Real Behavior Proof
- Environment: macOS, Python 3.11.12 venv, feat/grok-build @ `
|
||
|
|
e9e9cd55b7
|
feat(mcp): publish canonical server.json (#1510)
## Description Headroom can launch its MCP server, but did not publish a canonical `server.json` that registries and MCP hosts can consume directly. This PR adds a shared descriptor builder, commits a root `server.json`, parity-tests that artifact against the builder and existing runtime spec, and updates docs so registry authors do not need to reconstruct `headroom mcp serve` from prose. Closes #929. ## 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) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Added a shared `server_json.py` descriptor builder for Headroom MCP publication metadata. - Published a canonical root `server.json` and parity-tested it against the builder. - Encoded the publishable uvx contract as `headroom-ai[mcp]` plus `headroom mcp serve`. - Updated README and MCP docs to point registry authors at the canonical descriptor. - Added the README ownership marker used by MCP Registry verification. - Kept existing registrars and `headroom mcp install` behavior unchanged. ## Testing - [x] Unit tests pass - [x] Linting passes - [x] Type checking passes - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text Focused registry/server-json tests and reviewer approval were completed on this PR before the governance body cleanup. The current body update is documentation-only metadata for PR governance. ``` ## Real Behavior Proof - Environment: Headroom development checkout with MCP test dependencies. - Exact command / steps: Inspected the generated `server.json` contract and parity coverage against the descriptor builder and runtime MCP spec. - Observed result: The committed descriptor matches the builder/runtime contract and advertises the intended `headroom-ai[mcp]` / `headroom mcp serve` launch path. - Not tested: live publication to third-party registries ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes This body was normalized by a maintainer after approval so the governance parser reflects the already-reviewed PR state. |
||
|
|
bc24e258b1
|
fix(mcp/claude): don't clobber an unparseable Claude config on register (#1660)
## Description
When the `claude` CLI isn't on PATH (or its `mcp add` fails),
`ClaudeRegistrar`
falls back to `_register_via_file`, which does a full-file
read-modify-write of
`~/.claude/.claude.json`:
```python
config = _read_json(target) # returns {} on JSONDecodeError
servers = config.setdefault("mcpServers", {})
servers[spec.name] = _spec_to_entry(spec)
_write_json(target, config) # overwrites the ENTIRE file
```
`_read_json` returns `{}` for a file that exists but doesn't parse. So
if
`~/.claude/.claude.json` is momentarily corrupt or hand-edited (a
trailing
comma, a crash mid-write), the register path silently rewrites it as
just
`{"mcpServers": {"headroom": {...}}}` — **destroying every other key
Claude Code
keeps there**: `projects`, `oauthAccount`, session history, etc. There's
no
backup. The existing `test_get_server_robust_to_bad_json` only covers
the *read*
path; the destructive *write* path was untested.
Closes: no issue filed — found while auditing the MCP registry
config-write paths.
## Fix
Keep `_read_json` (returning `{}`) for the read-only callers
(`get_server`,
removal), where it's harmless. Add `_read_json_for_write` for the
rewrite path:
it returns `{}` only when the file is **absent or empty** (safe to start
fresh)
and raises `_MalformedConfigError` when the file is present but not a
JSON
object. `_register_via_file` catches it and returns a `FAILED` result
with an
actionable message instead of overwriting.
Result: absent/empty file → registers fresh (unchanged); valid file →
merges,
all other keys preserved (unchanged); present-but-invalid file → refuses
to
touch it and tells the user to fix or remove it.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/mcp_registry/claude.py`: add `_read_json_for_write` +
`_MalformedConfigError`; `_register_via_file` uses it and returns
`FAILED` (without writing) when the target is present-but-unparseable.
`_read_json` is unchanged for read-only callers.
- `tests/test_mcp_registry/test_claude_registrar.py`: regression tests —
register against malformed configs leaves the bytes untouched and
returns `FAILED`; register against a valid config still merges and
preserves unrelated keys.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.
## Testing
- [x] New tests added for the fixed behavior
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (local-OOM reason under Real Behavior
Proof).
```text
$ uv run ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12.11, headroom built from this
branch. Importing `headroom` loads the torch/transformers stack; a full
`pytest` gets OOM-killed on this box, so I verified the write-path logic
with a dependency-free script and left the full pytest to CI.
- Exact command / steps: replicated `_read_json_for_write` and the
`_register_via_file` read-modify-write flow in a standalone script (only
stdlib, no `headroom` import) against real temp files, and exercised:
absent, empty, four malformed variants (`not json`, `{`, `{"projects":
}`, `[]`), and a valid config carrying `projects`/`oauthAccount`.
- Observed result: absent/empty register fresh; every malformed variant
returns FAILED and the original bytes on disk are byte-for-byte
unchanged (no clobber); a valid config merges in `headroom` while
`projects`/`oauthAccount` survive:
```text
OK: absent -> fresh register
OK: empty -> fresh register
OK: malformed -> FAILED, original bytes preserved (no clobber)
OK: valid config -> merged, unrelated keys preserved
MCP CONFIG-WRITE LOGIC VERIFIED
```
- Not tested: driving the real `claude` CLI-absent path end-to-end on a
live `~/.claude/.claude.json` (didn't want to touch a real Claude
install); the file-fallback logic is exercised directly by the
regression tests. Full local `pytest` deferred to CI (OOM, per above).
## 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
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- No new dependencies. `headroom/mcp_registry/opencode.py` has the same
read-`{}`-then-clobber shape on its write path (an OpenCode
`opencode.json` with comments/JSONC would be wiped) — I scoped this PR
to the Claude registrar to keep it focused and because OpenCode config
handling is being touched in other open PRs; happy to send a follow-up
for opencode with the same guard if useful.
---------
Co-authored-by: JD Davis <mxjerrett@gmail.com>
|
||
|
|
415e03c168
|
fix(mcp/codex): don't clobber an unparseable/non-table config.toml (#2062)
## Description
`CodexRegistrar.register_server` (`headroom/mcp_registry/codex.py`)
guards against clobbering a
user-managed `[mcp_servers.<name>]` entry — but **only inside the `if
existing is not None`
branches**. `existing` comes from `get_server`, which returns `None` in
two cases that are *not*
"nothing there":
1. the `config.toml` is **unparseable** (`_load_toml` catches
`TOMLDecodeError` and returns `{}`), and
2. `mcp_servers` (or `mcp_servers.<name>`) is present but **not a
table** (`get_server` returns `None` via its `isinstance` guards).
With `existing is None`, all three protection branches are skipped and
control falls straight to
`_write_block`, which blindly appends a fresh `[mcp_servers.<name>]`
table.
So for a **valid** TOML file like:
```toml
[mcp_servers]
headroom = "not-a-table"
```
`register_server(headroom_spec)` appends `[mcp_servers.headroom]`,
producing a file that defines
`mcp_servers.headroom` **both** as a string and as a table — a duplicate
key that `tomllib`/codex
then reject, **corrupting a previously-valid user config**. The
unparseable-file case similarly
appends our block into a file that can't be parsed.
This is the exact Codex sibling of the claude (#1660) and opencode
(#1661) clobber-guard fixes;
codex never received it.
Closes: no issue filed — found while auditing the registrars for the
#1660/#1661 class.
## Fix
Add `_unmergeable_reason(name)` — returns why the existing file can't be
safely merged (present
but unparseable, or a non-table `mcp_servers` / `mcp_servers.<name>`),
else `None`. In
`register_server`, when `existing is None`, refuse with
`RegisterStatus.FAILED` (leaving the file
untouched) instead of appending. Absent/empty/valid configs are
unaffected.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/mcp_registry/codex.py`: add `_unmergeable_reason`; refuse in
`register_server` when the existing config is unparseable or defines a
non-table `mcp_servers`/`mcp_servers.<name>`.
- `tests/test_mcp_registry/test_codex_registrar.py`: add tests for
unparseable TOML, non-table `mcp_servers.headroom`, and non-table
`mcp_servers` (all refuse + file untouched).
## Testing
- [x] New regression tests added
(`tests/test_mcp_registry/test_codex_registrar.py`)
- [x] Linting/formatting clean — run with the CI-pinned `ruff==0.15.17`
- [ ] Full `pytest` deferred to CI (local-OOM reason below).
```text
$ uvx ruff@0.15.17 check headroom/mcp_registry/codex.py tests/test_mcp_registry/test_codex_registrar.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.10, headroom from this branch.
Importing `headroom` pulls in the torch/transformers stack and a full
`pytest` gets OOM-killed on this box, so I verified the
`_unmergeable_reason` logic with a dependency-free script (stdlib
`tomllib`) and left the full pytest to CI.
- Exact command / steps: ran the two clobber cases (non-table entry,
unparseable TOML) and the safe cases (absent/empty/valid/other-server)
through the guard.
- Observed result: the guard refuses exactly the two corrupting cases
and allows every valid config:
```text
REFUSE [non-table entry (valid TOML)]: non-table mcp_servers.headroom
REFUSE [unparseable TOML]: not valid TOML (Invalid value (at line 1, column 8))
ALLOW [absent]: reason=None
ALLOW [empty]: reason=None
ALLOW [valid, no mcp_servers]: reason=None
ALLOW [valid, mcp_servers table w/ other server]: reason=None
CODEX CLOBBER-GUARD VERIFIED (refuses non-table/unparseable; allows valid configs)
```
- Not tested: a live `codex` launch reading the config (mocked in the
registrar tests). Full local `pytest` deferred to CI (OOM, per above).
## 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
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Completes the registrar clobber-guard trio (claude #1660, opencode
#1661, codex here); no new dependencies.
- @JerrettDavis tagging you — same class you already reviewed for
claude/opencode, just the codex side. Thanks!
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
c85731dc23
|
fix(mcp): correct default Claude Code config path in ClaudeRegistrar (#1859)
## Description <!-- Briefly explain the change and why it is needed. --> `ClaudeRegistrar` originally assumed Claude Code's modern per-user MCP config lives at `~/.claude/.claude.json`. On a real Claude Code 2.1.202 install with `CLAUDE_CONFIG_DIR` unset, the actual file is `~/.claude.json`, directly under the home directory. Whenever `claude mcp add` failed for any transient reason and the registrar fell back to writing the config file directly, it wrote to a path Claude Code never reads — registration reported success with no error, but the server silently never became available, and once that wrong file existed, `get_server()` kept reading it back as already-registered, so the registrar never retried. While fixing the path, several related correctness and test-isolation issues in the same file were found and fixed: - Three tests instantiated `ClaudeRegistrar(claude_cli=None)` without `home_dir`, so the legacy config path resolved to the developer's real `~/.claude/mcp.json` — one test was actually deleting a `headroom` entry from it. - `detect()` only checked the legacy `~/.claude` directory, so installs where the `claude` CLI is absent from `PATH` and only the modern `~/.claude.json` exists were treated as not detected, skipping registration entirely. - `unregister_server()` returned early on CLI success without cleaning the legacy config file, so a stale legacy entry could survive a successful `claude mcp remove` and `get_server()` would keep reporting the server as registered. - `_read_server_entry`, `_remove_from_file`, and `_register_via_file` assumed `mcpServers` was always a dict once present; a hand-edited or corrupted config with `mcpServers` set to `null`, a list, or a string crashed with an unhandled `AttributeError`/`TypeError`. 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) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `_resolve_claude_config_dir` now defaults to `home` (the modern config is `~/.claude.json`), keeping the `CLAUDE_CONFIG_DIR` and explicit `config_dir` overrides intact; the legacy `.claude` directory is pinned to `home / ".claude"` independently of where the modern config resolves. - `detect()` also recognizes an install via `self._modern_config.exists()`, not just the legacy directory. - `unregister_server()` always cleans both the modern and legacy config files, even after a successful CLI removal. - `_read_server_entry`, `_remove_from_file`, and `_register_via_file` now validate `mcpServers` is a dict before indexing into it, degrading gracefully instead of crashing on malformed config. - Module and constructor docstrings now state the current config-path facts plainly (paths, and what `CLAUDE_CONFIG_DIR` relocates); the constructor docstring also clarifies that `home_dir`/`config_dir` isolate file-based reads/writes but not CLI subprocess calls. - `tests/test_mcp_registry/test_claude_registrar.py`: corrected path expectations, isolated the three previously-unisolated tests from the real home directory, and added coverage for the modern-config-only detect case, CLI-success-with-stale-legacy-entry, and non-dict `mcpServers` values. - Filed #1861 for a related, currently-unexercised gap: CLI subprocess calls don't honor `home_dir`/`config_dir` overrides. ## 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 $ pytest tests/test_mcp_registry/test_claude_registrar.py ============================== 35 passed in 0.08s ============================== $ ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py All checks passed! $ mypy headroom/mcp_registry/claude.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Claude Code 2.1.202 (macOS, Darwin 25.5.0), `CLAUDE_CONFIG_DIR` unset. - Exact command / steps: Injected a uniquely-named probe server into **only** `~/.claude/.claude.json` (old assumed path) and ran `claude mcp list`; then injected a different probe into **only** `~/.claude.json` (corrected path) and ran `claude mcp list` again; restored both files afterward. 1. `ZZZ_nested_probe` written to `~/.claude/.claude.json` only → `claude mcp list`. 2. `ZZZ_flat_probe` written to `~/.claude.json` only → `claude mcp list`. - Observed result: The nested-path probe (`ZZZ_nested_probe`) was **not** recognized by `claude mcp list` — Claude Code ignores `~/.claude/.claude.json`. The flat-path probe (`ZZZ_flat_probe`) **was** recognized and listed. This confirms `~/.claude.json` is the file Claude Code actually reads. Both config files were restored to their original state after the test. - Not tested: older Claude Code versions (< 2.1.202); Windows/Linux path resolution (logic is platform-agnostic via `pathlib`, but only macOS was exercised); the CLI-subprocess env-isolation gap tracked in #1861. ## 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 ## Screenshots (if applicable) N/A — CLI/config-path change with no UI surface. ## Additional Notes - The `CLAUDE_CONFIG_DIR=~/.claude` configuration still correctly resolves the modern config to `~/.claude/.claude.json` via the unchanged env override — only the default (env unset) changed. - Any machine that hit the original bug may have a stale `~/.claude/.claude.json` written by the old fallback; it is harmless and can be deleted. - #1861 tracks a related gap (CLI subprocess env isolation) that isn't exercised by any current production call site. |
||
|
|
1c947b1103
|
fix(mcp): isolate ClaudeRegistrar CLI config env (#1888)
## Description `ClaudeRegistrar` accepts `home_dir` and `config_dir` overrides so isolated callers can keep Claude config reads and file fallback writes away from the real user profile. The CLI path did not carry that resolved config location into the `claude` subprocess, so `claude mcp add` and `claude mcp remove` could still inherit the caller's real Claude environment while the registrar's file paths pointed somewhere else. This changes the CLI-backed register and unregister paths to pass a narrow `CLAUDE_CONFIG_DIR` environment only when constructor overrides request isolation. Normal user sessions keep the ambient subprocess environment, server `-e KEY=VALUE` arguments remain unchanged, and isolated registrars make the Claude CLI see the same config directory as Headroom's file-backed paths. Closes #1861. ## 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 - Added a narrow `ClaudeRegistrar` subprocess-env helper that returns an isolated `CLAUDE_CONFIG_DIR` only when `home_dir` or `config_dir` is supplied. - Passed the isolated env into both `claude mcp add` and `claude mcp remove`. - Kept server env values in `ServerSpec.env` as existing `claude mcp add -e KEY=VALUE` arguments. - Added regression coverage for CLI add and remove with `home_dir`, plus explicit `config_dir` precedence over an ambient `CLAUDE_CONFIG_DIR`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_mcp_registry/test_claude_registrar.py -q`) - [x] Linting passes (`uv run ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run pytest tests/test_mcp_registry/test_claude_registrar.py -q ======================== 26 passed, 1 warning in 0.17s ======================== uv run ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py All checks passed! uv run ruff format tests/test_mcp_registry/test_claude_registrar.py --check 1 file already formatted ``` ## Real Behavior Proof - Environment: local test runner with mocked Claude CLI subprocess calls; no real user Claude config touched. - Exact command / steps: Construct `ClaudeRegistrar(claude_cli="/usr/local/bin/claude", home_dir=tmp_path)` and an explicit `config_dir` variant, then exercise `register_server(...)` and `unregister_server(...)` through the existing mocked subprocess path. - Observed result: CLI add and remove calls receive `env["CLAUDE_CONFIG_DIR"]` matching the registrar's resolved config directory when isolation is requested. Existing CLI command shape, server `-e` argument behavior, and file fallback behavior remain intact. - Not tested: live Claude Code CLI file writes; the PR proves Headroom's child-process environment handoff without mutating a real Claude installation. ## 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 Documentation and changelog updates are N/A because this fixes `ClaudeRegistrar` override isolation rather than adding a user-facing command or config option. Live Claude CLI config writes are intentionally left out of local validation to avoid touching real user configuration. |
||
|
|
1baa04ef65
|
fix(io): use UTF-8 with locale fallback and preserve line endings on config/text I/O (#1498)
## Description On non-UTF-8 Windows locales (e.g. GBK/cp936 on zh-CN, cp1252 on Western installs) `headroom wrap codex` corrupts `~/.codex/config.toml`. Two root causes, both in how we read/write text: - `Path.read_text()` / bare `open()` default to the **system locale** encoding, so a UTF-8 config fails to decode as the locale codec (and a locale-written file fails to decode as UTF-8) — raising `UnicodeDecodeError`. - `Path.write_text()` / text-mode `open()` translate `\n` → `os.linesep` on write, so an existing `\r\n` becomes `\r\r\n`, which TOML parsers reject with *"carriage return must be followed by newline"*. This adds one small helper module and routes the unsafe config/text I/O through it. Closes #733 ## 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 - New `headroom/fsutil.py` with `read_text` / `write_text` / `append_text`: - `read_text`: decode UTF-8 → fall back to `locale.getpreferredencoding()` (for files a tool wrote in the locale encoding before this fix) → final UTF-8 with `errors="replace"` so it never raises on content. Line endings normalise to `\n`, so callers that search/rewrite the text see one ending and a later `write_text` can't re-double an existing `\r\n`. Supports `default=` for missing files. - `write_text` / `append_text`: UTF-8 with `newline=""` so the bytes written match the content exactly and existing `\r\n` endings are never doubled. - Routed the unsafe config/text I/O across the package through `fsutil` (or added an explicit `encoding="utf-8"` where only decode safety was missing): `mcp_registry/codex.py` (TOML read/write + `_load_toml` via `tomllib.loads`), `mcp_registry/opencode.py`, `mcp_registry/claude.py`, `cli/wrap.py`, `cli/mcp.py`, `cli/memory.py`, `install/providers.py`, `providers/anthropic.py`, `providers/openai.py`, `providers/opencode/config.py`, `providers/opencode/install.py`. - Tests: new `tests/test_fsutil.py` (CRLF preservation, no LF translation, CRLF normalisation on read, UTF-8 non-ASCII round trip, locale-decode fallback, never-raise replace fallback, missing-file default/raise, append preserves endings) and two `test_codex_registrar.py` regression tests (register doesn't double CRLF; non-ASCII values survive a register). ## 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 $ pytest tests/test_fsutil.py tests/test_mcp_registry/test_codex_registrar.py -q tests\test_fsutil.py ......... [ 25%] tests\test_mcp_registry\test_codex_registrar.py ........................ [100%] 36 passed in 0.32s $ ruff check <changed files> All checks passed! $ ruff format --check <changed files> 14 files already formatted $ mypy headroom --ignore-missing-imports # (run with --python-version 3.12 to # parse the local numpy stub) Success: no issues in changed files ``` Note: locally, the two suites `tests/test_mcp_registry` + `tests/test_cli` share a pre-existing cross-test state leak that flakes `test_wrap_codex_..._serena...` and `test_dead_client_marker...`; both reproduce identically on `main` (changes stashed) and are unrelated to this PR. CI shards run them isolated. ## Real Behavior Proof - Environment: Windows 11, Python 3.13.11, `locale.getpreferredencoding()` = `cp1252` (a non-UTF-8 locale — the exact condition that triggers #733). - Exact command / steps: pre-seed a `~/.codex/config.toml` the way Codex writes it on Windows — CRLF endings plus a non-ASCII value `project = "比赛/机器人"` — then call `CodexRegistrar.register_server(headroom)` and re-parse with `tomllib`. - Observed result: register status REGISTERED, no doubled CRLF, `tomllib` parses, and the non-ASCII value is preserved. Full output: ```text python: 3.13.11 | locale preferred encoding: cp1252 register status: RegisterStatus.REGISTERED doubled CRLF present: False tomllib parsed OK: True non-ASCII project value preserved: True headroom in mcp_servers: True ``` Before this change the same flow produced `\r\r\n` and a `tomllib` "carriage return must be followed by newline" error. - Not tested: a real zh-CN GBK/cp936 Windows install (no such host available); the GBK-specific decode path is covered by `test_read_text_falls_back_to_locale_encoding` which monkeypatches the preferred encoding to `gbk`. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes - Purely-binary I/O and sites already using `encoding="utf-8"`+`errors="replace"` (e.g. `learn/analyzer.py`) and the ASCII-only PID file (`install/runtime.py`) were intentionally left untouched. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
22def93177
|
fix(mcp): register managed installs with a resolvable headroom command (#1386)
## Description Managed Headroom installs can register the MCP server with a bare `headroom mcp serve` command even when the active runtime lives in a venv outside `PATH`. That leaves Claude and Codex with a registration they cannot re-launch reliably, and Claude eventually fails with `Failed to reconnect to headroom: ENOENT`. This PR reuses the existing runtime command resolver when building the shared Headroom MCP spec, so the generated registration follows the active install instead of assuming `headroom` is globally discoverable. It also updates the shared-builder and registrar tests so the proof rows now flow through `build_headroom_spec()` and prove the same resolved command contract on both the Claude CLI path and the Codex TOML path. A follow-up CI fix keeps the Docker init E2E expectation aligned with that same resolver-backed contract. Closes #487 ## 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/mcp_registry/install.py`: build the Headroom MCP server spec from the canonical runtime command resolver instead of hardcoding `headroom mcp serve` - `tests/test_mcp_registry/test_install.py`: cover the shared builder's direct-binary and module-fallback command shapes - `tests/test_mcp_registry/test_claude_registrar.py`: prove the Claude CLI registration forwards the resolved command vector end to end - `tests/test_mcp_registry/test_codex_registrar.py`: prove the Codex registrar writes the same resolved command vector into TOML - `e2e/init/run.py`: derive the Docker init E2E's expected Claude MCP registration argv from `resolve_headroom_command()` so the CI harness follows the same runtime contract - `CHANGELOG.md`: note the managed-install MCP registration fix ## Testing - [x] Unit tests pass (`uv run pytest tests/test_mcp_registry/test_install.py -v`, `uv run pytest tests/test_mcp_registry/test_claude_registrar.py -v`, `uv run pytest tests/test_mcp_registry/test_codex_registrar.py -v`) - [x] Linting passes (`uv run ruff check .`) - [ ] Type checking passes (`uv run mypy headroom`) or explain N/A truthfully - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text $ uv run pytest tests/test_mcp_registry/test_install.py -v ============================= 12 passed in 0.13s ============================== $ uv run pytest tests/test_mcp_registry/test_claude_registrar.py -v ============================= 24 passed in 0.18s ============================== $ uv run pytest tests/test_mcp_registry/test_codex_registrar.py -v ============================= 25 passed in 0.20s ============================== $ uv run python -c "from e2e.init.run import _expected_headroom_mcp_call; print(_expected_headroom_mcp_call('http://127.0.0.1:9011'))" ['mcp', 'add', 'headroom', '-s', 'user', '-e', 'HEADROOM_PROXY_URL=http://127.0.0.1:9011', '--', '.../headroom', 'mcp', 'serve'] $ uv run ruff check e2e/init/run.py All checks passed! $ uv run ruff format e2e/init/run.py --check 1 file already formatted $ uv run ruff check . All checks passed! $ uv run ruff format . --check 987 files already formatted ``` `uv run mypy headroom` was not run locally; this repo's focused local gate for the touched Python registry path is the targeted pytest set plus Ruff. ## Real Behavior Proof - Environment: managed-install-safe MCP registration path, Python 3.11+, no provider required - Exact command / steps: run the focused MCP registry pytest files, inspect the captured Claude CLI argv and rendered Codex TOML block, and verify the Docker init E2E expectation derives its Claude MCP argv from the same runtime helper - Observed result: the persisted MCP registration uses a resolvable command tied to the active Headroom runtime instead of bare `headroom`, while `HEADROOM_PROXY_URL` handling stays unchanged - Not tested: full live Claude reconnect against a real managed venv, unless that is run during implementation ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Scoped to the MCP registration slice in `#487`. The RTK hook rewriting thread from the same issue is intentionally out of scope here. - `@erikpr1994` isolated the managed-install `ENOENT` failure mode in the issue thread and narrowed it to the bare-command MCP registration path. - If existing owned registrations with the old bare-command contract need an in-place upgrade path, that should be handled explicitly in the final diff rather than left implicit. |
||
|
|
e616dcf788
|
fix(mcp): honor CLAUDE_CONFIG_DIR for Claude registrar (#886)
## Description Resolve the Claude MCP config directory from `CLAUDE_CONFIG_DIR` for default `ClaudeRegistrar()` instances so file fallback registration, direct reads, and unregister cleanup operate on the same config files Claude Code is using. Closes #872 ## 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 - Resolve the Claude MCP config directory from `CLAUDE_CONFIG_DIR` for default `ClaudeRegistrar()` instances. - Use the resolved directory for both modern `.claude.json` and legacy `mcp.json` file fallback paths. - Add regression coverage for read, register, and unregister behavior against a custom Claude config directory. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text uv run --with ruff ruff check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py All checks passed! uv run --with ruff ruff format --check headroom/mcp_registry/claude.py tests/test_mcp_registry/test_claude_registrar.py 2 files already formatted uv run --with pytest --with pytest-asyncio pytest -q tests/test_mcp_registry/test_claude_registrar.py [passed locally] ``` ## Real Behavior Proof - Environment: macOS Darwin arm64, Python 3.14.2 via `uv`, local fork branch. - Exact command / steps: ran the fallback registrar against a temporary `CLAUDE_CONFIG_DIR`. ```sh tmpdir=$(mktemp -d) CLAUDE_CONFIG_DIR="$tmpdir" uv run python -c 'import json, os; from pathlib import Path; from headroom.mcp_registry import ClaudeRegistrar, build_headroom_spec; reg = ClaudeRegistrar(claude_cli=None); result = reg.register_server(build_headroom_spec("http://127.0.0.1:9999")); path = Path(os.environ["CLAUDE_CONFIG_DIR"]) / ".claude.json"; data = json.loads(path.read_text()); print(result.status.value); print(path.exists()); print(data["mcpServers"]["headroom"]["env"]["HEADROOM_PROXY_URL"]); print((Path(os.environ["CLAUDE_CONFIG_DIR"]) / ".claude" / ".claude.json").exists()); print(reg.unregister_server("headroom")); print("headroom" in json.loads(path.read_text())["mcpServers"])' ``` - Observed result: registration wrote `$CLAUDE_CONFIG_DIR/.claude.json`, preserved `HEADROOM_PROXY_URL`, avoided the old nested path, and unregister removed the server. ```text registered True http://127.0.0.1:9999 False True False ``` - Not tested: a live Claude Code session or `claude mcp list` on WSL with a real installed CLI. ## 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 ## Screenshots (if applicable) N/A. ## Additional Notes No changelog entry was added because this is a focused MCP registrar bug fix. |
||
|
|
919379a8a1
|
fix(serena): stop the Serena dashboard popup and make --no-serena actually disable Serena (#1003)
## Description Headroom installs the Serena MCP server by default during `headroom wrap`, and many users reported the Serena web dashboard browser tab popping up on every session — even when they never opted into Serena. This PR fixes two distinct root causes: Serena's dashboard auto-open, and `--no-serena` not actually disabling an already-installed Serena. ## 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 - `build_serena_spec()` now passes `--open-web-dashboard False` to `serena start-mcp-server`. This is Serena's startup override for `web_dashboard_open_on_launch` (`serena/mcp.py:317-318`), so it suppresses the browser popup regardless of the user's `~/.serena/serena_config.yml` — the correct fix is at the launch point, not a per-machine config edit. The dashboard backend still runs and stays reachable at `http://localhost:24282/dashboard/`; only the auto-open is disabled. Applies to both launch paths (wrap + strands bundle) since both go through `build_serena_spec()`. - New `_disable_serena_mcp()`: `--no-serena` now actively removes the Serena entry Headroom installed (ledger-verified) instead of merely skipping registration. Previously a prior default wrap persisted a `serena` entry and the agent kept launching it; the old `Skipping Serena MCP` message was misleading. A user-managed Serena (absent from the ledger) is reported and left untouched; an absent Serena prints the skip message. Wired into both the Claude and Codex wrap paths. - `unwrap_codex` now removes Headroom-installed Serena. Codex writes Serena as its own `[mcp_servers.serena]` table, separate from the provider block the config-restore handles, so a "cleaned" unwrap previously left it behind (`unwrap_claude` already removed it; Codex was the gap). - Tests: updated `build_serena_spec` arg assertion + added a no-popup-default test; new `test_serena_disable.py` covering removed-when-headroom-owned, preserved-when-user-managed, skip-when-absent, noop-when-undetected, and `unwrap_codex` removal. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [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_cli/test_serena_disable.py tests/test_cli/test_wrap_codex.py tests/test_cli/test_unwrap_claude.py tests/test_mcp_registry/ -q 134 passed $ python -m pytest tests/test_mcp_registry/test_install.py -q ... passed (build_serena_spec arg + no-popup-default assertions) $ ruff check headroom/cli/wrap.py headroom/mcp_registry/install.py tests/test_cli/test_serena_disable.py tests/test_mcp_registry/test_install.py All checks passed! $ ruff format --check headroom/cli/wrap.py headroom/mcp_registry/install.py ... already formatted $ mypy headroom/cli/wrap.py headroom/mcp_registry/install.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: macOS (darwin), Python 3.12 venv, Serena 1.5.4 cached via uvx, headroom on branch fix/serena-no-dashboard-popup - Exact command / steps: Traced Serena source — `serena/cli.py` exposes `--open-web-dashboard <bool>`; `serena/mcp.py:317-318` sets `config.web_dashboard_open_on_launch = open_web_dashboard`; `serena/agent.py:706` feeds that to `DashboardManager`, which calls `webbrowser.open()` (`serena/dashboard.py:831`). Verified click parses `--open-web-dashboard False` → `False` via a CliRunner probe. Ran the test suites above. - Observed result: With the flag injected, the value that gates the browser-open is forced to False at startup regardless of local config, so no tab opens; dashboard backend still serves on its port. `--no-serena` removes the previously-installed `serena` entry (unregister called, "Removed previously-installed Serena MCP" printed) and `unwrap codex` removes it too. All 134 targeted tests pass; ruff + mypy clean. - Not tested: A full end-to-end `headroom wrap claude` against a live Claude Code install with a real browser was not run; verification is via Serena source tracing + the click-parse probe + unit/integration tests over the registrar and wrap/unwrap paths. ## 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 Two unchecked checklist items are N/A: no user-facing docs reference the Serena dashboard behavior, and CHANGELOG is generated via release-please from the conventional commits. "Manual testing performed" is left unchecked deliberately — see `Real Behavior Proof` → `Not tested` for the exact boundary of what was and wasn't exercised against a live browser. |
||
|
|
96abf38b09
|
fix(codex): respect CODEX_HOME for wrap config (#731)
> ⚠️ This PR was opened using Codex (gpt-5.5 on `xhigh`) Fixes #730. ## Summary - centralize Codex config path resolution in `headroom wrap codex` so it honors `CODEX_HOME` when set - make the Codex MCP registrar use `$CODEX_HOME/config.toml` instead of always writing to `~/.codex/config.toml` - route optional memory MCP and global Codex `AGENTS.md` injection through the same Codex home helper - make `headroom unwrap codex` print a warning, but still succeed, when `CODEX_HOME` is unset and the default Codex config has no Headroom markers - add regression coverage for provider injection, prepare-only wrapping, MCP registration under a custom Codex home, and the ambiguous unwrap warning - update `CHANGELOG.md` under `Unreleased > Bug Fixes` ## Real behavior proof Setup tested on: - Linux `7.0.10-2-cachyos` - Python 3.14.3 via `uv` - local fork branch `fix/codex-home` - custom Codex home created outside `~/.codex` Exact command run after the patch: ```bash tmp_home=$(mktemp -d) mkdir -p "$tmp_home/codex_custom" HOME="$tmp_home" USERPROFILE="$tmp_home" CODEX_HOME="$tmp_home/codex_custom" \ UV_SKIP_WHEEL_FILENAME_CHECK=1 PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 \ uv run --with fastapi --with uvicorn --with httpx --with websockets \ headroom wrap codex --no-context-tool --no-serena --prepare-only --port 8787 find "$tmp_home" -maxdepth 3 -type f -print | sort sed -n '1,140p' "$tmp_home/codex_custom/config.toml" test -e "$tmp_home/.codex/config.toml" && echo yes || echo no HOME="$tmp_home" USERPROFILE="$tmp_home" \ UV_SKIP_WHEEL_FILENAME_CHECK=1 PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 \ uv run --with fastapi --with uvicorn --with httpx --with websockets \ headroom unwrap codex --no-stop-proxy ``` After-fix evidence + observed result: Interactive check: - A full `CODEX_HOME="$HOME/.codex_p" headroom wrap codex --no-serena` launch was tested locally after the patch and worked as expected. - The prepare-only proof below shows the same config path behavior without requiring an interactive Codex session in CI/reviewer environments. ```text MCP retrieve tool: registered (restart OpenAI Codex CLI if it was already running) Codex config: injected Headroom provider (WS + HTTP) into /tmp/tmp.UPUvloGNYE/codex_custom/config.toml --- files --- /tmp/tmp.UPUvloGNYE/codex_custom/config.toml /tmp/tmp.UPUvloGNYE/codex_custom/config.toml.headroom-backup --- custom config --- # --- Headroom proxy (auto-injected by headroom wrap codex) --- model_provider = "headroom" openai_base_url = "http://127.0.0.1:8787/v1" # --- end Headroom --- # --- Headroom MCP server --- [mcp_servers.headroom] command = "headroom" args = ["mcp", "serve"] # --- end Headroom MCP server --- # --- Headroom proxy (auto-injected by headroom wrap codex) --- [model_providers.headroom] name = "OpenAI via Headroom proxy" base_url = "http://127.0.0.1:8787/v1" supports_websockets = true # --- end Headroom --- --- default config exists? --- no Warning: found no Headroom wrap markers in the default Codex config. If you wrapped Codex with CODEX_HOME, rerun unwrap with the same environment variable, e.g. CODEX_HOME=/path/to/codex-home headroom unwrap codex. Nothing to undo: .../.codex/config.toml has no Headroom wrap markers. ``` What I did not test: - Windows/macOS path behavior - full repository test suite, because this local Python 3.14 environment hits optional dependency/build constraints outside this patch ## Testing ```bash UV_SKIP_WHEEL_FILENAME_CHECK=1 PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv run --with pytest --with fastapi --with uvicorn --with httpx --with websockets pytest tests/test_mcp_registry/test_codex_registrar.py tests/test_cli/test_wrap_codex.py -q UV_SKIP_WHEEL_FILENAME_CHECK=1 PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv run --with ruff ruff check headroom/cli/wrap.py headroom/mcp_registry/codex.py tests/test_cli/test_wrap_codex.py tests/test_mcp_registry/test_codex_registrar.py UV_SKIP_WHEEL_FILENAME_CHECK=1 PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 uv run --with ruff ruff format --check headroom/cli/wrap.py headroom/mcp_registry/codex.py tests/test_cli/test_wrap_codex.py tests/test_mcp_registry/test_codex_registrar.py ``` Results: ```text 63 passed, 1 warning in 1.48s All checks passed! 4 files already formatted ``` Notes: - Python 3.14 required `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1` for the editable build. - `uv` required `UV_SKIP_WHEEL_FILENAME_CHECK=1` because the existing `uv.lock` has a GitPython wheel filename/version mismatch. |
||
|
|
a7160f7eab | fix: register serena mcp during wrap | ||
|
|
ac1d11c9a2 | fix: sync Codex MCP proxy config during wrap | ||
|
|
d9d8972ac4 |
fix(mcp): auto-register headroom MCP server in wrap claude/codex and init -g
The proxy compresses tool_result payloads and emits [Retrieve more: hash=…]
markers, but Claude Code / Codex had no headroom_retrieve tool to call on
those markers unless the user separately ran 'headroom mcp install'. The
markers were dead pointers — silent quality loss.
Adds a per-agent MCP registrar abstraction (mcp_registry/) and wires it
into wrap and init so MCP install happens automatically alongside rtk:
- mcp_registry/base.py — MCPRegistrar ABC, ServerSpec, RegisterResult,
RegisterStatus enum.
- mcp_registry/claude.py — Claude Code registrar (claude mcp add CLI
with .claude.json / mcp.json file fallback).
- mcp_registry/codex.py — OpenAI Codex registrar (marker-delimited TOML
block edits to ~/.codex/config.toml; preserves user's other config).
- mcp_registry/install.py — install_everywhere() orchestrator with
detect-then-register semantics.
- mcp_registry/display.py — shared format_result()/format_results() for
consistent CLI output across wrap, init, and 'headroom mcp install'.
Adding a new agent (Cursor, Continue, Cline, Windsurf, Goose) is now a
single new file plus one entry in get_all_registrars(); call sites and
display logic don't change.
Test seam is constructor injection (home_dir, claude_cli) — zero patches
in 66 new tests across the registry. Removed 13 brittle CLI integration
tests in test_mcp.py that were patching module-level globals; equivalent
coverage now lives at the registrar/orchestrator layer.
wrap codex: snapshot ~/.codex/config.toml at the top of the command so
the existing wrap→unwrap round-trip captures the true pre-wrap state
even though MCP install now writes to the same file mid-flow.
220 tests pass (66 new + 154 existing CLI + integration). ruff and mypy
clean on touched files.
|