mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
141 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8522fcbc40
|
fix(code): quarantine Perl parser from code-aware compression (#2204)
## Description `headroom proxy` can wedge when code-aware compression enters the tree-sitter Perl external scanner and the native scan keeps the GIL indefinitely. Current main still has two routes into that scanner: explicit `perl` or `pl` hints flow through `CodeAwareCompressor`, and `detect_language()` can nominate Perl on non-Perl code because its prefilter matches generic sigils such as decorators, JSDoc tags, and shell variables before phase 2 parses every surviving candidate grammar. This change quarantines Perl at the code-aware compression funnel without widening scope. Perl remains recognized at the input boundary, but live proxy compression no longer requests a Perl parser. Non-Perl code keeps its existing code-aware behavior. Real Perl falls back through the existing safe Kompress or passthrough contract instead of entering tree-sitter. The diff stays inside `headroom/transforms/code_compressor.py` plus focused parser-safety regressions. Refs #2185 ## 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 Perl quarantine list in `headroom/transforms/code_compressor.py` and used it to stop Perl candidate parsing during language detection. - Added a hard `_get_parser()` guard so no live code-aware path can construct a Perl parser. - Routed resolved explicit Perl hints through the existing safe fallback or passthrough contract before AST compression. - Added focused parser-safety regressions for non-Perl candidate bleed, explicit `perl` and `pl`, inferred real Perl, mixed fenced Perl content, fallback-disabled passthrough, and non-Perl negative space. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_perl_scanner_safety.py -q`) - [x] Linting passes (`uv run ruff check headroom/transforms/code_compressor.py tests/test_perl_scanner_safety.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_perl_scanner_safety.py -q 8 passed in 1.93s uv run ruff check headroom/transforms/code_compressor.py tests/test_perl_scanner_safety.py All checks passed! uv run ruff format headroom/transforms/code_compressor.py tests/test_perl_scanner_safety.py --check 2 files already formatted ``` ## Real Behavior Proof - Environment: Windows, Python from the synced `uv` environment, `dev` and `code` extras installed, no provider call - Exact command / steps: Run `uv run pytest tests/test_perl_scanner_safety.py -q`. - Observed result: `8 passed in 1.93s`; the suite proves explicit `perl` and `pl`, inferred real Perl, mixed fenced Perl, and non-Perl negative-space routes all avoid Perl parser entry. - Not tested: the reporter's macOS payload and long-running concurrent workload ## 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Use `Refs #2185`, not `Closes #2185`. The wedge surface is this PR's scope, but #2185 also carries a separate orphaned `headroom mcp serve` report that this slice does not address. - This is a reachability fix. It does not repair the upstream Perl scanner and it does not harden other grammars against the same class of native wedge. - `CHANGELOG.md` remains unchanged because Headroom generates release notes from conventional commits. - Merged PR https://github.com/headroomlabs-ai/headroom/pull/2114 addresses a different cooperative compression stall and stays separate from this native parser-entry slice. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
2a954b69b4
|
feat(wrap): add ZCode desktop app support (#1845)
## Description Add `headroom wrap zcode` and `headroom unwrap zcode` commands for the ZCode desktop app (zcode.z.ai). ZCode is a desktop Electron IDE built by Z.AI, optimized for GLM-5.2 models. It has no CLI binary, so this follows the Pattern-B (proxy-only, print instructions) approach — same as Cursor, Cline, and Continue. **Upstream auto-detection:** `headroom wrap zcode` now reads `~/.zcode/v2/config.json` to detect the enabled provider and automatically configures the proxy upstream — no manual flags needed. Closes #1844 ## 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 - New module: `headroom/providers/zcode/__init__.py` and `runtime.py` (ZCodeProxyTargets, ZCodeUpstream, build_proxy_targets, detect_upstream, upstream_to_proxy_urls, render_setup_lines) - New CLI command: `headroom wrap zcode` in `headroom/cli/wrap.py:4815` — starts proxy, injects RTK into AGENTS.md, prints Base URL setup instructions - New CLI command: `headroom unwrap zcode` in `headroom/cli/wrap.py:5960` — removes RTK markers, stops proxy - New helper: `zcode_config_dir()` in `headroom/install/paths.py` - Updated `_run_proxy_only_watcher` to accept `anthropic_api_url`/`openai_api_url` params - Updated README.md: ZCode row in compatibility matrix, unwrap list, wrap command list - Updated CHANGELOG.md: entry under [Unreleased] > Added ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) — pre-existing numpy type stubs issue prevents full mypy run - [x] New tests added for new functionality (24 tests in `tests/test_cli/test_wrap_zcode.py`) - [x] Manual testing performed ### Test Output ```text tests/test_cli/test_wrap_zcode.py ........................ [100%] 24 passed, 1 warning in 0.22s ``` ## Real Behavior Proof - Environment: macOS 15.5, Python 3.12.13, headroom installed via `pip install -e .[dev]` - Exact command / steps: `headroom wrap zcode --port 9000` then `headroom unwrap zcode --port 9000` - Observed result: Wrap detects provider from `~/.zcode/v2/config.json` (e.g. "Z.ai Coding"), starts proxy with correct upstream on port 9000, injects RTK into AGENTS.md, prints detected provider + upstream + Base URL setup instructions. Unwrap removes RTK markers, deletes empty AGENTS.md, stops proxy. - Not tested: Actual ZCode app integration (ZCode is a desktop Electron app; Base URL configuration is manual in Settings > Model Settings) ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project style guidelines - [x] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas — N/A: code follows existing patterns - [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 ## Screenshots (if applicable) N/A — CLI-only changes ## Additional Notes - **Pattern-B approach:** ZCode is a desktop Electron app with no CLI binary. Same pattern as Cursor, Cline, and Continue — proxy-only, print instructions. - **Upstream auto-detection:** Reads `~/.zcode/v2/config.json`, finds the enabled provider, and passes its `baseURL` to the proxy. Falls back to Z.ai Anthropic endpoint if no config found. - **httpProxy investigation:** ZCode has an `httpProxy` setting in `~/.zcode/v2/setting.json`, but it is an Electron-level forward proxy (CONNECT tunneling), incompatible with headroom reverse proxy. The Base URL approach in Model Settings is the correct integration point. - **No dependencies added:** This PR adds zero new dependencies. --------- Co-authored-by: Epicism <epicism@Epiphanie.local> Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
5dbe3314a1
|
fix(auth): support GitHub Enterprise Copilot OAuth domain (#2192)
## Description Adds GitHub Enterprise OAuth domain support for Copilot auth. When `GITHUB_COPILOT_ENTERPRISE_URL` is set, the default OAuth domain resolves to that enterprise host; explicit `--domain` values still take precedence. Closes #1152 ## 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 - `headroom/copilot_auth.py`: derive the default OAuth domain from `GITHUB_COPILOT_ENTERPRISE_URL` when present, falling back to `github.com` for unset or blank values. - `headroom/cli/copilot_auth.py`: keep explicit CLI domain overrides authoritative even when the enterprise env var is set. - `README.md`: document the enterprise OAuth environment setting and precedence. - Added regression tests for enterprise URL handling, blank/unset fallback, and explicit CLI override precedence. ## 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 pytest tests/test_copilot_auth.py tests/test_cli/test_copilot_auth.py -q 69 passed in 1.05s uvx ruff@0.15.17 check headroom/cli/copilot_auth.py headroom/copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_copilot_auth.py All checks passed! uvx ruff@0.15.17 format --check headroom/cli/copilot_auth.py headroom/copilot_auth.py tests/test_cli/test_copilot_auth.py tests/test_copilot_auth.py 4 files already formatted ``` ## Real Behavior Proof - Environment: Windows 11 development checkout, Python 3.13.3, with focused Copilot auth tests using monkeypatched enterprise env vars. - Exact command / steps: ran the Copilot auth unit/CLI tests plus ruff check and format-check against the touched auth files and tests. - Observed result: `GITHUB_COPILOT_ENTERPRISE_URL=https://ghe.example.com` resolves `default_oauth_domain()` to `ghe.example.com`; unset/blank enterprise env vars fall back to `github.com`; and `headroom copilot-auth login --domain github.com` still honors the explicit override when enterprise env vars are set. - Not tested: live OAuth against a real GitHub Enterprise Server instance. ## 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 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 ## Screenshots (if applicable) N/A - CLI/auth behavior and README update. ## Additional Notes `GITHUB_COPILOT_ENTERPRISE_URL` takes precedence over `GITHUB_COPILOT_ENTERPRISE_DOMAIN`; explicit `--domain` remains authoritative for the login command. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
14011b42dd
|
fix(wrap): drop -p short flag from wrap claude so claude's own -p/--print passes through (#2048)
## Description `headroom wrap claude` declares `--port/-p`, and click parses wrapper options anywhere in the argv before unknown options fall through to `CLAUDE_ARGS`. So a user running claude's headless print mode through the wrapper — `headroom wrap claude -p "some prompt"` — fails with `Invalid value for '--port' / '-p': 'some prompt' is not a valid integer range`, and claude's own `-p`/`--print` can never reach claude. This bites hardest when `claude` is shell-aliased to `headroom wrap claude ...`: every `claude -p` invocation breaks. This PR drops the `-p` short alias from `wrap claude`'s `--port` option (long form stays; other subcommands' `-p` are untouched), so `-p` now falls through to `CLAUDE_ARGS` like any other claude flag. 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 - `headroom/cli/wrap.py`: removed `"-p"` from the `wrap claude` command's `--port` option; added a comment stating why the short alias must not exist there. ## Testing - [x] Unit tests pass (`pytest`) — targeted CLI suites, see output - [x] Linting passes (`ruff check .`) — on the touched file - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_cli/test_wrap_codex.py tests/test_cli/test_wrap_claude_base_url.py tests/test_cli/test_unwrap_claude.py -q 125 passed in 6.68s $ ruff check headroom/cli/wrap.py All checks passed! Full tests/test_cli run: 549 passed, 2 failed — test_wrap_copilot_auto_detects_running_proxy_backend fails identically on a clean upstream/main checkout (pre-existing, environment-sensitive), and test_wrap_codex_prepare_only_registers_serena_when_uvx_exists passes in isolation on this branch (full-suite ordering interaction, not this change). ``` ## Real Behavior Proof - Environment: Fedora 44, Python 3.14 editable install, `claude` aliased to `systemd-run --user --scope ... headroom wrap claude --no-context-tool` via terminal shell integration - Exact command / steps: `claude --model sonnet -p "Say only: ALIAS-P-FIXED"` in a fresh interactive shell (alias → wrapper → proxy → claude) - Observed result: before the fix — `Error: Invalid value for '--port' / '-p': ... is not a valid integer range` (exit 2, claude never spawns). After — headroom banner, proxy attach, claude prints `ALIAS-P-FIXED`, exit 0; `Extra args: --model sonnet -p Say only: ALIAS-P-FIXED` shows the passthrough. - Not tested: Windows; other wrapped tools' `-p` flags (left untouched by design); mypy (not run) ## 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 ## Screenshots (if applicable) N/A — CLI flag parsing. ## Additional Notes Docs/CHANGELOG: no user-facing docs mention `-p` as a `wrap claude` port alias, so no doc change; happy to add a CHANGELOG entry if maintainers want one. No new test added because the passthrough behavior is covered by the manual end-to-end proof above; can add a click-runner test asserting `-p` lands in `CLAUDE_ARGS` if preferred. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
5d17e9addc
|
fix: check feature configuration before reusing persistent deployments (#1330)
## Description
A persistent proxy started for one use case (e.g. `--backend anthropic`)
would be silently reused for another (e.g. `--subscription
--provider-type openai`) causing 401 auth failures because
`_ensure_proxy()` only checked health + version, skipping the feature
configuration check (memory, openai_api_url, learn, code_graph).
Closes #N/A
## 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 feature configuration check in the persistent deployment path of
`_ensure_proxy()` in `headroom/cli/wrap.py:1726-1752`. When features
mismatch, the code now falls through to the non-persistent path which
handles proxy restart with upgraded config.
- Added three new tests in `tests/test_cli/test_wrap_persistent.py`:
-
`test_ensure_proxy_restarts_persistent_deployment_for_feature_mismatch`
— verifies proxy restart when openai_api_url differs
- `test_ensure_proxy_restarts_persistent_deployment_for_memory_mismatch`
— verifies proxy restart when memory is requested but not enabled
- `test_ensure_proxy_reuses_persistent_deployment_when_features_match` —
verifies proxy reuse when all features match
- Updated `CHANGELOG.md` with fix description
## 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
$ ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_persistent.py
All checks passed!
$ ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_persistent.py
2 files already formatted
$ python -c "
from tests.test_cli.test_wrap_persistent import *
import pytest
test_ensure_proxy_restarts_persistent_deployment_for_feature_mismatch(pytest.MonkeyPatch())
print('Test 1 passed: feature mismatch restarts proxy')
test_ensure_proxy_restarts_persistent_deployment_for_memory_mismatch(pytest.MonkeyPatch())
print('Test 2 passed: memory mismatch restarts proxy')
test_ensure_proxy_reuses_persistent_deployment_when_features_match(pytest.MonkeyPatch())
print('Test 3 passed: matching features reuse proxy')
print('All tests passed!')
"
Test 1 passed: feature mismatch restarts proxy
Test 2 passed: memory mismatch restarts proxy
Test 3 passed: matching features reuse proxy
All tests passed!
$ .venv/bin/mypy headroom
headroom/proxy/server.py:1230: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1301: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1305: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
Success: no issues found in 397 source files
```
## Real Behavior Proof
- Environment: Linux (WSL2) Ubuntu 24.04, Python 3.12, persistent
deployment via `headroom install agent run --profile default` with
`--backend anthropic`
- Exact command / steps:
1. Started persistent deployment: `headroom install agent run --profile
default`
2. Ran copilot wrapper: `headroom wrap copilot --subscription
--provider-type openai --wire-api responses --memory -- --model
gpt-5.4-mini`
- Observed result:
- Before fix: Proxy forwarded to `api.openai.com` instead of
`api.githubcopilot.com`, causing 401 auth errors
- After fix: Proxy correctly forwards to `api.githubcopilot.com` and
copilot works as expected
- Not tested: macOS, Windows, enterprise/data-residency accounts, other
wrapped tools (claude, codex, aider)
## 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
- [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
## Screenshots (if applicable)
N/A
## Additional Notes
The fix is minimal and targeted. It only adds a feature configuration
check in the persistent deployment path without changing any other
behavior. The non-persistent proxy path already had this check; this PR
brings the same logic to persistent deployments.
Co-authored-by: carlosduplar <[email protected]>
|
||
|
|
4ea96a417c
|
feat(mcp): add streamable HTTP MCP transport (#1773)
## Description `headroom mcp serve` only exposed stdio, which blocked MCP clients that require a Streamable HTTP endpoint. This PR adds an explicit HTTP transport mode around the existing Headroom MCP server while keeping stdio as the default and keeping tool registration single-sourced. Closes #1346. ## 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 - Add `headroom mcp serve --transport http` with host, port, and path options. - Serve `headroom_compress`, `headroom_retrieve`, and `headroom_stats` through the same MCP server instance used by stdio. - Keep `headroom mcp serve` defaulting to stdio for current Claude Code and local MCP host configs. - Update MCP docs for stdio and HTTP setup without implying the proxy automatically owns `/mcp`. - Keep the scope clean, rebased, and covered by focused tests. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q`) - [x] Linting passes (`uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py`) - [x] Type checking passes (`uv run mypy headroom --ignore-missing-imports`) - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text uv run pytest tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py -q 20 passed in 0.53s uv run ruff check headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py All checks passed! uv run ruff format headroom/cli/mcp.py headroom/ccr/mcp_server.py headroom/ccr/mcp_http.py tests/test_ccr_mcp_http.py tests/test_cli/test_mcp.py --check 5 files already formatted uv run mypy headroom --ignore-missing-imports Success: no issues found in 407 source files ``` ## Real Behavior Proof - Environment: Local Python environment with Headroom dev dependencies and MCP extra installed. - Exact command / steps: Start `headroom mcp serve --transport http --host 127.0.0.1 --port <test-port> --path /mcp`, then perform an MCP SDK Streamable HTTP initialize/list-tools exchange. - Observed result: The HTTP transport initializes and lists the existing Headroom MCP tools; `headroom mcp serve` without `--transport` still selects stdio, and mixed-case `--transport HTTP` routes to the HTTP transport. - Not tested: live validation against external MCP hosts ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Additional Notes `CHANGELOG.md` is not edited because this repository generates changelog entries from conventional commits. Full-suite validation is left to CI. |
||
|
|
541500811f
|
feat(cli): add wrap openclaude for OpenClaude CLI (#1416)
## Description
Adds `headroom wrap openclaude`, a Click subcommand that launches the
prose-format OpenClaude CLI through the local Headroom proxy using the
same OpenAI/Anthropic base URL environment shape as `wrap aider`.
Fixes #1411.
## Type of Change
- [x] Bug fix
- [x] New feature
- [ ] Breaking change
- [ ] Documentation update
- [x] Tests
## Changes Made
- Added the `wrap openclaude` command path for OpenClaude CLI launch env
routing.
- Kept `--no-context-tool` / `--no-rtk` support for proxy-only launch
behavior.
- Fixed the default RTK setup path requested in review: when RTK is
selected and installed, `wrap openclaude` now injects the RTK
instruction marker block into `CONVENTIONS.md` at the project root
instead of only downloading the binary.
- Added a regression test for the default RTK path so the PR fails if
OpenClaude stops receiving RTK instructions.
## 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
RED, with the production RTK injection path temporarily reverted while
keeping the new regression test:
```text
.venv/bin/python -m pytest tests/test_cli/test_wrap_openclaude.py::test_wrap_openclaude_default_rtk_injects_instructions -q
FAILED tests/test_cli/test_wrap_openclaude.py::test_wrap_openclaude_default_rtk_injects_instructions
E AssertionError: assert False
E + where False = exists()
E + where exists = PosixPath('/tmp/pytest-of-ousama/pytest-1/test_wrap_openclaude_default_r0/CONVENTIONS.md').exists
```
GREEN, after restoring the fix:
```text
.venv/bin/python -m pytest tests/test_cli/test_wrap_openclaude.py -q
3 passed in 0.46s
```
Additional validation on the pushed commit
`
|
||
|
|
8f867e4622
|
fix(install): guard non-dict health config in 'install status' (#2150)
## Description
`headroom install status` crashes with an `AttributeError` when the
probed health endpoint returns a non-dict `config`.
```python
if payload and isinstance(payload, dict):
click.echo(f"Health URL: {manifest.health_url.replace('/readyz', '/health')}")
click.echo(f"Backend: {payload.get('config', {}).get('backend', manifest.backend)}")
```
`payload` is guarded as a dict, but `payload['config']` is not.
`dict.get('config', {})` only substitutes the `{}` default when the key
is **absent** — a present-but-non-dict `config` (`null`, a string, a
list) is returned as-is, and the chained `.get('backend', ...)` then
raises `AttributeError`, crashing the command with a raw traceback.
Reachability: the Headroom proxy normally returns `config` as an object,
so this bites when `install status` probes a port that a different or
older service is occupying (which can emit `config: null` or a
non-object), or a build that emits `config: null`. The correctly-guarded
sibling already exists in the codebase — `wrap.py`'s
`_proxy_health_config` does `config = payload.get("config"); return
config if isinstance(config, dict) else None`.
## Fix
Guard the `config` value with `isinstance(config, dict)` before the
`.get('backend', ...)` lookup, mirroring `_proxy_health_config`. A
non-dict (or missing) `config` falls back to the manifest's backend.
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
- `headroom/cli/install.py`: `install status` guards `config` with
`isinstance(config, dict)` before reading `backend`.
- `tests/test_cli/test_install_cli.py`: add
`test_install_status_survives_non_dict_config` (health payload with
`config: null` must not crash; backend falls back to the manifest).
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ uvx ruff@0.15.17 check headroom/cli/install.py tests/test_cli/test_install_cli.py
All checks passed!
$ python -m py_compile headroom/cli/install.py tests/test_cli/test_install_cli.py
OK
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17`. Importing
`headroom` pulls in the torch/transformers stack and a full `pytest`
gets OOM-killed on this box, so I verified the access with a
dependency-free script that replicates the old vs guarded lookup, and
left the full pytest (including the new CLI test) to CI.
- Exact command / steps: ran the old `payload.get('config',
{}).get('backend', ...)` and the new guarded lookup against `config`
values of `null`, a string, a list, a proper object, and a missing key.
- Observed result: the old lookup raises `AttributeError` for every
non-dict `config`; the new lookup falls back to the manifest backend for
those and returns the real backend for a proper object (and the
missing-key case is unchanged). The new CLI test drives `install status`
with `probe_json` returning `{"config": null}` and asserts a clean exit
with the manifest backend.
- Not tested: a live foreign service occupying the port; 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
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" and "type checking" boxes are unchecked
because the full suite imports the ML stack, which I can't run in this
environment; the change adds an `isinstance` guard mirroring an existing
sibling, verified by the standalone proof and a new CLI test that reuses
the file's existing `install status` mocking harness.
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
84f66da36f
|
fix(init/codex): merge into hooks.json instead of overwriting it (#2173)
## Description
`headroom init codex` destroys a user's existing Codex hooks.
`_ensure_codex_hooks` builds a fresh payload containing only Headroom's
two hooks and writes it wholesale:
```python
payload = { "hooks": { "SessionStart": [...headroom...], "PreToolUse": [...headroom...] } }
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
```
It never reads the existing file, so any user-managed hooks (and any
other top-level keys) in `~/.codex/hooks.json` are silently replaced
with just Headroom's entries — data loss on every `init codex` run.
The sibling registrars do it correctly: `_ensure_claude_hooks` and
`_ensure_copilot_hooks` both read via `_json_file`, then merge per event
and dedup on the Headroom marker, preserving unrelated user entries. The
codex path was the lone writer that overwrote.
## Fix
Read-merge-write, mirroring `_ensure_claude_hooks`: load the existing
payload, keep each event's entries that don't carry the
`headroom-init-codex` marker, append Headroom's, and write back. User
hooks and other top-level keys survive; Headroom's are deduped
(idempotent re-runs).
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
- `headroom/cli/init.py`: `_ensure_codex_hooks` reads via `_json_file`,
merges per event with marker dedup, and writes via `_write_json` (no
more wholesale overwrite).
- `tests/test_cli/test_init_cli.py`: add
`test_ensure_codex_hooks_preserves_user_hooks` — a user hook and an
unrelated top-level key survive; Headroom's hook is appended once.
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ uvx ruff@0.15.17 check headroom/cli/init.py tests/test_cli/test_init_cli.py
All checks passed!
$ python -m py_compile headroom/cli/init.py tests/test_cli/test_init_cli.py
OK
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17`. Importing
`headroom` pulls in the torch/transformers stack and a full `pytest`
gets OOM-killed on this box, so I verified the merge with a
dependency-free script that replicates the old (overwrite) vs new
(merge) logic, and left the full pytest to CI.
- Exact command / steps: gave a config with a user `SessionStart` hook
(`echo my-own-hook`) and an unrelated top-level key (`notify: true`),
then ran both the old and new logic.
- Observed result: old drops both the user hook and the top-level key;
new keeps both and appends Headroom's hook exactly once. The new test
asserts this against the real `_ensure_codex_hooks`.
- Not tested: a live `headroom init codex` end to end; 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
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The "unit tests pass locally" and "type checking" boxes are unchecked
because the full suite imports the ML stack, which I can't run in this
environment; the change mirrors the existing `_ensure_claude_hooks`
merge logic, verified by the standalone proof and the new test (which
reuses the file's existing hooks-test harness).
|
||
|
|
8da4384bfc
|
fix(init/codex): don't delete per-profile provider settings (#2146)
## Description `headroom init codex` silently deletes a user's per-profile provider settings. `_ensure_codex_provider` owns the root-level `model_provider` / `openai_base_url` keys, and to avoid emitting a duplicate top-level key it strips any prior assignment before re-inserting its block: ```python content = re.sub(r"(?m)^[ \t]*model_provider[ \t]*=.*\r?\n", "", content) content = re.sub(r"(?m)^[ \t]*openai_base_url[ \t]*=.*\r?\n", "", content) ``` Those multiline regexes match the keys at any indentation, **in any TOML table**. Codex supports per-profile overrides: ```toml [profiles.work] model_provider = "azure" [profiles.gpt5] model_provider = "openai" ``` So a user with named Codex profiles who runs `headroom init codex` has every `[profiles.*]` `model_provider` / `openai_base_url` line silently removed. Those profiles then fall through to the injected root `model_provider = "headroom"` default — their routing is quietly changed. That collateral deletion isn't needed to prevent the root-level duplicate the strip exists for (#260); the unwrap-side sibling `_strip_codex_init_block` proves the intent is precise (it only removes the Headroom-owned value). ## Fix Scope the strip to the document root — everything before the first table header. Root-level `model_provider` / `openai_base_url` are still replaced (init owns them), but keys inside `[profiles.*]` (or any other table) are left untouched. 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 - `headroom/cli/init.py`: `_ensure_codex_provider` splits the config at the first table header and strips `model_provider`/`openai_base_url` only from the root section. - `tests/test_cli/test_init_cli.py`: add `test_ensure_codex_provider_preserves_profile_overrides` — a `[profiles.work]` override survives init while the root key is replaced by `headroom`. - `CHANGELOG.md`: Bug Fixes entry. ## Testing - [ ] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uvx ruff@0.15.17 check headroom/cli/init.py tests/test_cli/test_init_cli.py All checks passed! $ python -m py_compile headroom/cli/init.py tests/test_cli/test_init_cli.py OK ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17`. Importing `headroom` pulls in the torch/transformers stack and a full `pytest` gets OOM-killed on this box, so I verified the strip with a dependency-free script that replicates the old (whole-file) vs new (root-scoped) regex, and left the full pytest to CI. - Exact command / steps: ran both strippers on a config with a root `model_provider = "openai"` and a `[profiles.work]` block overriding `model_provider`/`openai_base_url`. - Observed result: the old strip deletes the `[profiles.work]` overrides too; the new strip keeps them and still removes the root assignment. The new test asserts the profile override survives and the root becomes `headroom`. - Not tested: a live `headroom init codex` end to end; 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes The "unit tests pass locally" and "type checking" boxes are unchecked because the full suite imports the ML stack, which I can't run in this environment; the change scopes an existing regex strip to the document root, verified by the standalone proof and the new test (the two existing `_ensure_codex_provider` tests only exercise root-level and block-placement behavior, both preserved). I kept the fix to root-scoping rather than also matching only the `"headroom"` value, since that preserves the #260 duplicate-key guard without the broad deletion. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
faed4dcfe7
|
fix(wrap/claude): bind _wrap_settings_path before the try (#2126)
## Description
`headroom wrap claude` crashes with an `UnboundLocalError` from its
cleanup `finally` whenever the proxy fails to start, which both hides
the real error and skips cleanup.
`claude()` initializes its cleanup state before the `try` so the
`finally` can always reference it — `proxy_holder`, `_saved_base_url`,
`_settings_foundry`, `port_holder`, `_settings_vertex` are all bound up
front. But `_wrap_settings_path` was the exception: it was assigned only
inside the `try`, after `_ensure_proxy`:
```python
try:
...
proxy_holder[0], actual_port = _ensure_proxy(port, ...) # can raise
...
_wrap_settings_path = Path.cwd() / ".claude" / "settings.local.json" # assigned here
...
finally:
_restore_claude_wrap_base_url(..., settings_path=_wrap_settings_path) # referenced here
cleanup()
```
`_ensure_proxy` raises when the requested port is unavailable and the
range is exhausted, or when the proxy subprocess fails to start. When it
does, control jumps to the `finally`, which evaluates
`settings_path=_wrap_settings_path` — a local that was never assigned —
and raises `UnboundLocalError`. That replaces the real failure with a
raw traceback, and because the `finally` aborts on that line,
`cleanup()` never runs, so proxy cleanup and wrap-marker clearing are
skipped too.
## Fix
Bind `_wrap_settings_path` before the `try`, next to the other cleanup
holders, so the `finally` can always reference it. The value is
unchanged (the in-`try` assignment is removed since it computed the same
path).
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
- `headroom/cli/wrap.py`: hoist the `_wrap_settings_path` initialization
to before the `try` (alongside `proxy_holder`/`_saved_base_url`/…) and
drop the redundant in-`try` assignment.
- `tests/test_cli/test_wrap_claude_finally_unbound.py`: new test — drive
`wrap claude` with `_ensure_proxy` patched to raise and assert the
`finally` completes (no `UnboundLocalError`, and both restore and
cleanup ran).
- `CHANGELOG.md`: Bug Fixes entry.
## Testing
- [x] Unit tests pass (`uv run --extra dev pytest
tests/test_cli/test_wrap_claude_finally_unbound.py -q`)
- [x] Linting passes (`uvx ruff@0.15.17 check headroom/cli/wrap.py
tests/test_cli/test_wrap_claude_finally_unbound.py
headroom/memory/factory.py`)
- [x] Type checking passes (`uvx mypy==1.20.2
headroom/memory/factory.py`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
$ uvx ruff@0.15.17 check headroom/cli/wrap.py tests/test_cli/test_wrap_claude_finally_unbound.py
All checks passed!
$ python -m py_compile headroom/cli/wrap.py tests/test_cli/test_wrap_claude_finally_unbound.py
OK
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17`. Importing
`headroom` pulls in the torch/transformers stack and running the CLI
test locally OOM-kills this box, so I verified the control flow with a
dependency-free script that reproduces the try/finally with the variable
assigned inside vs before the try, and left the full pytest (including
the new CLI test) to CI.
- Exact command / steps: ran the flow with the variable bound inside the
try (old) and before the try (new), each with an early failure that
fires before the in-try assignment.
- Observed result: old raises `UnboundLocalError` from the finally and
skips restore/cleanup; new runs the finally cleanly and lets the real
`RuntimeError` propagate. The new CLI test drives `wrap claude` with
`_ensure_proxy` raising and asserts no `UnboundLocalError` and that
restore and cleanup both ran.
- Not tested: a live proxy port-exhaustion end to end; 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
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
Merged current `main` to pick up the repository-wide mypy cache-key
annotation fix, then verified the focused regression locally. the change
hoists one assignment to before the `try` (mirroring the four sibling
holders three lines above), verified by the control-flow proof and a new
CLI test that reuses the same mocking pattern the existing `wrap claude`
vertex tests use.
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
|
||
|
|
d236b27c60
|
fix(wrap/codex): export the detected custom upstream base URL (#2125)
## Description `headroom wrap codex` detects a user's custom upstream gateway but never tells Codex to use it, so the user's gateway key is sent to `api.openai.com`. `_inject_codex_provider_config` handles a Codex user who has an OpenAI-compatible gateway declared in `~/.codex/config.toml`, e.g. ```toml model_provider = "freemodel" [model_providers.freemodel] base_url = "https://api.freemodel.dev" ``` It injects the Headroom provider with `env_http_headers = { ... "X-Headroom-Base-Url" = "HEADROOM_CODEX_UPSTREAM_BASE_URL" }` and **returns the preserved upstream URL** so the caller can export it. Its docstring even says: *"Callers that go on to launch Codex should export this value into `HEADROOM_CODEX_UPSTREAM_BASE_URL`."* But `_prepare_codex_wrap_state` called it as a bare statement and discarded the return, and `_run_codex_wrap` / `_build_codex_launch_env` only ever set `OPENAI_BASE_URL`. A repo-wide grep confirms `HEADROOM_CODEX_UPSTREAM_BASE_URL` (`_UPSTREAM_BASE_URL_ENV_VAR`) is never assigned into any process env — it appears only at its definition and in that docstring. Since Codex only emits the `X-Headroom-Base-Url` header when the env var exists, the header is omitted, the proxy's OpenAI handler falls back to its hardcoded `https://api.openai.com`, and the user's `freemodel.dev` key is sent to OpenAI, which rejects it. This is a regression: the wiring existed in the original `#1614` fix (`_codex_custom_upstream = _inject_codex_provider_config(...)` then `env[_UPSTREAM_BASE_URL_ENV_VAR] = ...`) and was dropped by a later refactor that extracted `_prepare_codex_wrap_state`. ## Fix Restore the wiring: `_prepare_codex_wrap_state` now captures and returns `_inject_codex_provider_config`'s value, and `_run_codex_wrap` exports it into the launch env (`env[_UPSTREAM_BASE_URL_ENV_VAR] = custom_upstream`) when it is non-None and not already set, so a user-provided value still wins. 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 - `headroom/cli/wrap.py`: `_prepare_codex_wrap_state` returns the detected custom upstream URL; `_run_codex_wrap` exports it into the launch env (and its display list) when set. - `tests/test_cli/test_wrap_codex.py`: add `TestCodexLaunchExportsCustomUpstream` — drives `_run_codex_wrap` with mocked prepare/launch and asserts the launch env carries `HEADROOM_CODEX_UPSTREAM_BASE_URL` when a custom upstream is detected, and does not when there isn't one. - `CHANGELOG.md`: Bug Fixes entry. ## Testing - [x] Unit tests pass (`uv run --extra dev pytest tests/test_cli/test_wrap_codex.py::TestCodexLaunchExportsCustomUpstream -q`) - [x] Linting passes (`uvx ruff@0.15.17 check headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py headroom/memory/factory.py`) - [x] Type checking passes (`uvx mypy==1.20.2 headroom/memory/factory.py`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uvx ruff@0.15.17 check headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py All checks passed! $ python -m py_compile headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py OK ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12, `uvx ruff@0.15.17`. Importing `headroom` pulls in the torch/transformers stack and running the CLI test locally OOM-kills this box, so I verified the wiring with a dependency-free script that models prepare -> run -> the proxy's upstream fallback, and left the full pytest (including the new CLI test) to CI. - Exact command / steps: modelled the old flow (inject return discarded) and the new flow (return exported into the launch env), then applied the proxy's rule that a missing `HEADROOM_CODEX_UPSTREAM_BASE_URL` falls back to `api.openai.com`. - Observed result: old effective upstream is `https://api.openai.com` (the gateway key is misrouted); new effective upstream is `https://api.freemodel.dev` (the user's gateway). The new CLI test asserts the launch env carries the var when a custom upstream is present and omits it otherwise. - Not tested: a live Codex process reading the env and emitting the header; 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 - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes Merged current `main` to pick up the repository-wide mypy cache-key annotation fix, then verified the focused regression locally. the change threads one return value through two functions and exports it, verified by the wiring proof and a new CLI test that drives `_run_codex_wrap` with the heavy prepare/launch steps mocked so only the env-export logic is exercised. Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
3a39cb99ad
|
install: couple Codex routing to persistent runtime readiness (#2043)
## Description Persistent install currently writes Codex routing before runtime readiness, and it leaves that routing in place while the runtime is stopped or after a failed start. This changes the lifecycle so routing is applied only after the persistent runtime is ready and reverted before stop or removal, which prevents Codex from getting stranded on a dead `127.0.0.1:8787` provider. Closes #2038 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Apply persistent provider mutations only after runtime readiness. - Revert persistent provider mutations before stop and remove. - Clear stale routing before recovery start paths, then reapply after readiness. - Add focused install lifecycle tests for the ordering change. ## Testing - [x] Unit tests pass - [x] Linting passes - [ ] Type checking passes - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_cli/test_install_cli.py -q 23 passed, 1 warning in 0.27s uv run ruff check headroom/cli/install.py tests/test_cli/test_install_cli.py All checks passed! uv run ruff format --check headroom/cli/install.py tests/test_cli/test_install_cli.py 2 files already formatted ``` ## Real Behavior Proof - Environment: Windows, Python 3.12.13, no provider (persistent install lifecycle change; no live Codex or proxy call) - Exact command / steps: `uv run pytest tests/test_cli/test_install_cli.py -q`, then `uv run ruff check` and `uv run ruff format --check` on `headroom/cli/install.py` and `tests/test_cli/test_install_cli.py` - Observed result: 23 install-lifecycle tests pass and lint/format checks pass, confirming persistent Codex provider mutations are now applied only after runtime readiness and reverted before stop/remove - Not tested: a live persistent-runtime start/stop cycle with Codex routing ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
4364eb8dc4
|
fix(copilot): refresh wrapped subscription tokens (#2156) (#2182)
## Description `headroom wrap copilot --subscription` currently validates a Copilot subscription credential once at launch, exchanges it once, and then pins that short-lived API token into the proxy as an explicit override. When the token expires, long-lived wrapped sessions start returning `transient_auth_error` and then a final HTTP 401 until the entire wrapped session is restarted. This PR keeps the validated launch token for first-request determinism, carries reusable OAuth refresh material into the proxy, and refreshes inside `CopilotTokenProvider` when the seeded token is expired. The explicit `GITHUB_COPILOT_API_TOKEN` override path stays unchanged when no reusable OAuth token exists. The configured `GITHUB_COPILOT_API_URL` also stays pinned across refresh, matching the current wrap contract. Closes #2156. ## 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 - Extended the Copilot subscription token resolution path to carry reusable OAuth refresh material and expiry metadata instead of discarding it after the wrap-time exchange. - Reworked `CopilotTokenProvider.get_api_token()` so it seeds the wrapper-validated launch token once for the first request, then refreshes through the existing exchange path when that token is expired and reusable OAuth material exists. - Rejected non-finite seeded expiry values such as `inf`, so malformed `GITHUB_COPILOT_API_TOKEN_EXPIRES_AT` inputs cannot pin a stale launch token forever. - Preserved the explicit `GITHUB_COPILOT_API_TOKEN` override path when no reusable OAuth token exists, so non-refreshable overrides keep today's fixed behavior. - Kept explicitly configured `GITHUB_COPILOT_API_URL` values pinned across refresh rather than adopting a refreshed payload's host. - Replaced wrapper-managed seeded `tid_` bearer passthrough with the refresh-aware provider path, so the wrapped CLI no longer bypasses expiry refresh just because it keeps sending the launch token back to the proxy. - Started a dedicated local proxy instance whenever a subscription-seeded session targets a shared or persistent proxy port, so per-session refresh material is not silently dropped on healthy-proxy reuse or cross-wired between concurrent sessions. - Scrubbed inherited Copilot refresh-seed environment variables from both the Copilot child env and the proxy subprocess env before re-injecting the explicit launch-time values. - Added focused auth, wrap, proxy-env, and proxy-reuse regression coverage for expiry refresh, non-finite expiry rejection, session-local proxy isolation, explicit-override preservation, exchange-flag independence, configured API URL pinning, and secret handling. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py tests/test_cli_proxy_env.py tests/test_cli/test_wrap_persistent.py -q`) - [x] Linting passes (`uv run ruff check headroom/copilot_auth.py headroom/cli/wrap.py tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py tests/test_cli_proxy_env.py tests/test_cli/test_wrap_persistent.py`) - [x] Formatting passes (`uv run ruff format --check headroom/copilot_auth.py headroom/cli/wrap.py tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py tests/test_cli_proxy_env.py tests/test_cli/test_wrap_persistent.py`) - [x] Type checking passes (`uv run mypy headroom/copilot_auth.py`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text 195 passed, 1 skipped in 3.14s ``` ```text All checks passed! ``` ```text 6 files already formatted ``` ```text Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: Python 3.12.13, `uv`, no live Copilot credentials. - Exact command / steps: run the focused auth, wrap, proxy-env, and proxy-reuse regression suite after seeding an expired launch token plus reusable OAuth refresh material, then rerun lint and format checks on the touched files. - Observed result: base reproduces the bug because the explicit-token branch never refreshes, accepts non-finite expiry inputs, and shared-proxy reuse can keep the wrong per-session refresh seed alive; head refreshes through the reusable OAuth token, rejects non-finite seeded expiry, replaces the wrapper-managed seeded bearer instead of blindly passing it through, preserves the valid-seed fast path and the fixed override path when no refresh material exists, keeps the configured API URL pinned across refresh, starts a dedicated local proxy when the requested port already belongs to a shared or persistent proxy, and keeps the reusable OAuth token confined to explicit proxy launch env only. The focused suite passed with `195 passed, 1 skipped in 3.14s`. - Not tested: live business-subscription session past the provider's real token-expiry window. ## 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 - [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 - Scope stays provider-local. The fix remains inside `headroom/copilot_auth.py` and the Copilot wrap handoff in `headroom/cli/wrap.py`; it does not add generic 401 retry logic to provider-neutral proxy layers. - The line that disables token exchange for the Copilot CLI child env is unchanged because it never reached the proxy env and was not the root cause. - Subscription-seeded sessions now get a dedicated local proxy whenever the requested port already belongs to a shared or persistent proxy; existing shared proxies are left alone to avoid disrupting attached wrappers. - Live provider confirmation still needs maintainer or reporter validation because that truth is owned by GitHub's real subscription APIs, not by local stubs. - Headroom's release pipeline generates changelog entries from conventional commits, so `CHANGELOG.md` is intentionally untouched. |
||
|
|
c5545d6ac4
|
fix(wrap): use canonical headroom-openclaw npm package for wrap openclaw (#1969) (#2120)
## Description `headroom wrap openclaw` installed a non-existent npm spec — the `--plugin-spec` default was `headroom-ai/openclaw`, which npm reads as a GitHub shorthand and fails; the published package is `headroom-openclaw` (see `plugins/openclaw/package.json`). Fix introduces a single `OPENCLAW_NPM_PACKAGE = "headroom-openclaw"` constant (kept in sync with `package.json` and the release env), uses it as the default, and defers writing the `plugins.entries.headroom` config until after a successful install so a hard failure leaves no stale entry. Closes #1969 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/providers/openclaw/wrap.py` + `__init__.py`: canonical `OPENCLAW_NPM_PACKAGE` constant. - `headroom/cli/wrap.py`: use it as `--plugin-spec` default; write config only after successful install. - `tests/test_cli/test_wrap_openclaw.py`: expect `headroom-openclaw`; install-before-config ordering; failed-install-writes-no-config test. ## Testing - [x] Unit tests pass (`pytest tests/test_cli/test_wrap_openclaw.py`) — 29 passed - [x] Linting passes (`ruff check`) ### Test Output ```text 29 passed ruff: All checks passed! ``` ## Real Behavior Proof - Before: `wrap openclaw` → npm "unsupported spec" error; a failed install left a stale config entry. - After: installs `headroom-openclaw`; no config written on failure. |
||
|
|
daeff69a75
|
fix(wrap): surface Claude Remote Control base-URL gate accurately (#1… (#1883)
…779) Claude Code 2.1.196 deterministically disables first-party Remote Control (/remote-control, /rc) behind a custom ANTHROPIC_BASE_URL, which Headroom always sets. Make the wrap/doctor warning accurate (state the disable as fact, name the /rc command, detect the installed version), suppress it for auth modes that never had RC (API key, Bedrock/Vertex/Foundry) and for builds older than 2.1.196, co-report the sibling #746/#1158 gates session-accurately, and fix is_custom_anthropic_base_url host handling (scheme-less hosts, malformed URLs). UX/notice-only; no request bytes touched. ## 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. --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
c4ddcb93a7
|
fix(codex): skip sockets in session home overlay (#2104)
## Description Prevent `headroom wrap codex` from failing when the active `CODEX_HOME` contains a Unix socket. The session overlay copied every entry with `shutil.copytree()`, which raises `shutil.Error` when it reaches Git's `fsmonitor--daemon.ipc` socket. The overlay now skips socket entries while continuing to copy regular Codex state and surface unrelated copy errors. Closes #2103 ## 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 - Ignore filesystem sockets while seeding the temporary Codex session home. - Add a regression test with a real nested `fsmonitor--daemon.ipc` socket and a regular sibling file. ## Testing - [x] Focused unit tests pass (`pytest tests/test_cli/test_wrap_codex.py -q`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New regression test added - [ ] Manual interactive testing performed ### Test Output ```text Docker, Linux arm64, Python 3.12.12 pytest tests/test_cli/test_wrap_codex.py -q 88 passed in 5.92s ruff check . All checks passed! ruff format --check . 1191 files already formatted mypy headroom --ignore-missing-imports Success: no issues found in 469 source files ``` ## Real Behavior Proof - Environment: isolated Docker container on Linux arm64 with Python 3.12.12 and Rust 1.95.0 - Exact command / steps: bind a real Unix socket at `vendor_imports/skills/.git/fsmonitor--daemon.ipc`, then enter `_codex_session_home_overlay()` through the focused pytest regression - Observed result: the regular sibling file is copied, the socket is omitted, the source socket remains active, and the overlay exits cleanly - Not tested: an interactive Codex launch against the live host `~/.codex` ## 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] I have added tests that prove the fix is effective - [x] New and existing focused Codex wrapper tests pass with my changes ## Additional Notes The filter is intentionally limited to socket entries. Permission errors and failures involving regular files still propagate from `shutil.copytree()`. |
||
|
|
20968a4fa4
|
fix(wrap/opencode): unwrap removes the rtk block from AGENTS.md (#2025)
## Description `headroom wrap opencode` injects the marker-fenced rtk guidance block — "prefix shell commands with `rtk`" — into **both** instruction files (`headroom/cli/wrap.py`): ```python # wrap opencode project_agents = Path.cwd() / "AGENTS.md" _inject_rtk_instructions(project_agents, verbose=verbose) global_agents = _opencode_home_dir() / "AGENTS.md" _inject_rtk_instructions(global_agents, verbose=verbose) ``` But `unwrap_opencode` only restores the OpenCode config and cleans up MCP servers — it never removes that rtk block. So after `unwrap opencode`, both `AGENTS.md` files still contain the marker-fenced instruction, and a plain `opencode` launch keeps following "prefix shell commands with `rtk`" and fails once the managed rtk binary is off PATH. `unwrap_codex` (#1421) and `unwrap_copilot` both already do this cleanup via `_remove_rtk_instructions`; opencode was simply never given the equivalent — a wrap/unwrap asymmetry. Closes: no issue filed — found while auditing wrap/unwrap symmetry across agents. ## Fix In `unwrap_opencode`, after the MCP cleanup, strip the rtk block from both files it was injected into, mirroring `unwrap_codex`: ```python for _agents_md in (Path.cwd() / "AGENTS.md", _opencode_home_dir() / "AGENTS.md"): if _remove_rtk_instructions(_agents_md): click.echo(f" Removed Headroom rtk instructions from {_agents_md}.") ``` Best-effort and unconditional, matching the existing MCP cleanup and the codex/copilot unwrap paths. `_remove_rtk_instructions` already no-ops when the file or marker is absent. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/cli/wrap.py`: `unwrap_opencode` removes the rtk block from the project and global `AGENTS.md`. - `tests/test_cli/test_wrap_opencode.py`: add `test_unwrap_opencode_removes_rtk_from_agents_md` (wrap injects into both, unwrap removes from both). ## Testing - [x] New regression test added (`tests/test_cli/test_wrap_opencode.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/cli/wrap.py tests/test_cli/test_wrap_opencode.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 wrap→unwrap round-trip through the new Click-runner test (which drives the real command) and reasoned through the marker logic; the full pytest runs on CI. - Exact command / steps: the added test runs `wrap opencode --no-mcp` (asserts `_RTK_MARKER` present in both the project and global `AGENTS.md`), then `unwrap opencode`, and asserts the marker is gone from both. - Observed result (the assertions the test enforces): before the fix, `unwrap opencode` left `_RTK_MARKER` in both files; after the fix both are clean: ```text after wrap: _RTK_MARKER in project AGENTS.md ✓ _RTK_MARKER in global AGENTS.md ✓ after unwrap: _RTK_MARKER absent (project) ✓ _RTK_MARKER absent (global) ✓ ``` - Not tested: launching a real `opencode` binary (mocked in the test, as the existing wrap tests do). 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 + the new Click-runner test path; full pytest deferred to CI (local OOM, disclosed above) - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Directly parallels the merged `unwrap codex` rtk cleanup (#1421); no new dependencies. - @JerrettDavis tagging you — same class as the codex rtk fix, just the opencode side that was missed. Thanks! --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com> Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
1cc99792ac
|
fix(version): mark source-checkout builds as -dev (#2072)
## Description
`headroom --version` and the dashboard show `0.32.0` from a source
checkout, but the latest published release is `0.31.0`. That `0.32.0` is
not a real release: on a git checkout `get_version()` predicts the
*next* release from conventional commits since the last tag (`v0.31.0` +
`feat:` commits → `0.32.0`) and renders it identically to a shipped
version — so a dev build looks published.
This appends `-dev` on the source-checkout path so a dev build is never
mistaken for the published release.
Closes #
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/_version.py`: the source-checkout branch of `get_version()`
now returns `f"{source_version}-dev"`.
- `tests/test_package_init_lazy.py`: updated the source-tree version
test to assert the `-dev` suffix.
Released installs are unaffected: pip wheels and Docker images with a
baked `BUILD_VERSION` never take the source-checkout path, so they still
report clean release versions (`0.31.0` / `v0.31.0`). The suffix makes
`is_release_version()` return `False` and `normalize_release_version()`
return `None`, which every comparison site already handles — e.g.
`wrap.py`'s `_proxy_needs_version_restart` requires both sides to
normalize, so a dev build short-circuits to "no restart" (no behavior
change).
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality (updated the existing
source-tree test)
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_package_init_lazy.py tests/test_cli_doctor.py -q
============================== 12 passed in 1.79s ==============================
============================== 51 passed in 0.56s ==============================
$ ruff check headroom/_version.py tests/test_package_init_lazy.py
All checks passed!
$ mypy headroom/_version.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: local source checkout (macOS), `.venv`, latest release
tag `v0.31.0`
- Exact command / steps: `headroom --version`
- Observed result:
- Before: `headroom, version 0.32.0` — indistinguishable from a release
- After: `headroom, version 0.32.0-dev`
- Not tested: behavior inside a built Docker image / installed pip wheel
— unchanged by design, since those paths never compute a source-tree
version.
## 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
Scope kept to a bare `-dev` marker, which answers "is this a release?".
Appending the short git SHA (`-dev+g<sha>`) to distinguish individual
dev builds in bug reports is an easy follow-up if wanted. Docs/CHANGELOG
unchecked as N/A for a dev-only version-string fix.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
f53f720eb5
|
feat(wrap): allow project RTK instruction opt-out (#2078)
## Description Add an opt-out for project-level RTK guidance when wrapping OpenCode, so teams can preserve an existing repository `AGENTS.md` while still installing RTK and its global OpenCode instructions. Closes #1980 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring ## Changes Made - add `--no-project-rtk` to `headroom wrap opencode` - leave the repository `AGENTS.md` untouched when requested - continue installing RTK and its global OpenCode guidance - cover preservation of existing team instructions ## Testing - [x] Focused unit test passes (`pytest`) - [x] Touched-file linting passes (`ruff check`) - [ ] Type checking passes (`mypy headroom`) — not run - [x] New test added for new functionality - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_cli/test_wrap_opencode.py::test_wrap_opencode_no_project_rtk_only_skips_project_agents_md -q 1 passed uv run ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_opencode.py All checks passed! uv run ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_opencode.py 2 files already formatted ``` ## Real Behavior Proof - Environment: local Python test environment - Exact command / steps: invoke the focused OpenCode wrapper test with an existing project `AGENTS.md` and project RTK guidance disabled - Observed result: the existing project instructions remain byte-for-byte untouched while the global RTK guidance path still runs - Not tested: manual OpenCode launch or the full repository test suite ## 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 in the scoped checks - [x] I have added a test that proves the feature works - [x] Relevant existing and new unit coverage passes locally - [ ] Documentation and changelog updates — not applicable for this self-documenting CLI option ## Additional Notes The opt-out is deliberately narrow: it suppresses only the project `AGENTS.md` mutation, not RTK installation or global OpenCode configuration. Co-authored-by: Tejas Chopra <chopratejas@gmail.com> |
||
|
|
ad9d086f43
|
feat(codex): keep wrap routing session-scoped (#1507)
## Description Keeps Codex wrap routing session-scoped so routing state from one wrapped session does not leak into another. ## 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 - Scope Codex wrap routing state to the active session. - Avoid cross-session routing contamination for wrapped Codex traffic. - Keep changes focused on wrap/proxy routing behavior. ## Testing - [x] Unit tests pass - [x] New tests added for new functionality when applicable - [x] Manual testing performed ### Test Output ```text Focused tests/review were completed before this governance body cleanup. The current branch is conflicted and still needs merge resolution before final merge readiness. ``` ## Real Behavior Proof - Environment: Headroom development/review context. - Exact command / steps: Reviewed session-scoped Codex wrap routing behavior and existing focused coverage. - Observed result: Routing state is scoped to the active wrap session rather than shared globally across sessions. - Not tested: Current conflicted branch after merge resolution; conflicts still need to be resolved before merge. ## 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. The PR remains blocked by merge conflicts. |
||
|
|
f536aa0801
|
fix(wrap): keep Claude context-tool setup explicit (#1999)
## Description `headroom wrap claude` currently installs RTK's global Claude hook and instruction imports on a flag-free launch, even though the wrapped session already routes through Headroom's proxy. The wrapper now requires an explicit Claude context-tool opt-in before it runs the existing RTK or lean-ctx setup path. Existing negative flags remain accepted, and other wrapped agents keep their current behavior. Closes #1915 ## 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 - Made Claude context-tool installation explicit instead of running it on every default wrap. - Preserved the existing RTK and lean-ctx installers behind the positive opt-in. - Kept `--no-context-tool` and `--no-rtk` compatible and left other agent wrappers unchanged. - Added focused command-parser coverage for default, opt-in, selector, and negative-space behavior. - Documented the changed default and opt-in command in `CHANGELOG.md`. ## Testing - [x] Unit tests pass (`uv run --no-project pytest tests/test_cli/test_wrap_helpers.py -q`) - [x] Linting passes (`uv run --no-project ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run --no-project pytest tests/test_cli/test_wrap_helpers.py -q 65 passed uv run --no-project ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py All checks passed uv run --no-project ruff format --check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py 2 files already formatted ``` ## Real Behavior Proof - Environment: isolated HOME on Linux or macOS, Python 3.12+, Claude CLI available. - Exact command / steps: run `headroom wrap claude --prepare-only` without a context-tool flag, inspect the isolated Claude config, then repeat with the explicit context-tool opt-in. - Observed result: the focused Click harness now proves the default run creates no RTK setup calls, the explicit opt-in performs the existing RTK setup, `--no-context-tool` still wins if both flags are present, and Copilot still keeps its default context-tool behavior. - Not tested: a live `headroom wrap claude` run against a real Claude installation and a real RTK or lean-ctx hook write on this host. - Scope: Claude context-tool activation and global configuration artifacts. ## 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 ## Screenshots (if applicable) N/A ## Additional Notes The exact project-bound `uv sync --extra dev` flow was blocked on this host by a `rustup.exe` access error, so the focused checks used `uv run --no-project` against the existing environment. This PR does not change RTK installation internals, proxy compression, or context-tool defaults for other agents. |
||
|
|
1d2b76e72e
|
fix: harden persistent install startup (#1851)
## Description Hardens persistent install startup and proxy compression behavior for issue #1843. Repeated `headroom install start` / scheduled ensure calls no longer spawn duplicate runtimes by default, and `/v1/compress` now fails open on compression timeout instead of returning a 503. The PR also adds a machine-readable platform feature matrix and app-level stabilization tests for health, compression functionality, timeout behavior, and matrix evidence. Refs #1843 ## 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 - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Wrapped direct persistent deployment starts with the existing profile-local runtime start lock. - Made `headroom install start` idempotent when the deployment is already healthy. - Added wedged-runtime handling: if a PID is running but `/readyz` does not recover inside the grace window, stop it before starting again. - Kept `install agent ensure` inside the already-held lock while delegating to the shared start helper. - Changed `/v1/compress` timeout behavior from `503 compression_timeout` to fail-open `200` with original messages, `compression_skipped: true`, and `skip_reason: compression_timeout`. - Added `tests/test_platform_stabilization_functional.py` covering real FastAPI health/compression routes, successful compression metrics, timeout fail-open speed, and a real JSON tool payload that reduces tokens. - Added `docs/platform-feature-matrix.json` and `docs/platform-stabilization.md` for Linux/macOS/Windows hardening coverage and known gaps. - Strengthened matrix tests so cited local test/workflow paths must exist. ## 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 # Clean source tree without copied Rust extension: functional module is skipped locally, as CI copies _core from the built wheel. > python -m pytest tests/test_install tests/test_cli/test_install_cli.py tests/test_platform_feature_matrix.py tests/test_platform_stabilization_functional.py -q collected 120 items / 1 skipped 119 passed, 2 skipped in 17.08s > python -m ruff check headroom/proxy/handlers/openai.py tests/test_platform_stabilization_functional.py tests/test_platform_feature_matrix.py All checks passed! # Local Windows compiled-core proof: > python -m maturin build --profile ci --out dist-local Built wheel for abi3 Python >= 3.10 to dist-local\headroom_ai-0.29.0-cp310-abi3-win_amd64.whl # Copied _core.pyd from the wheel into headroom/ for local route execution, then: > python -m pytest tests/test_platform_stabilization_functional.py -q collected 4 items 4 passed in 6.71s > python -m pytest tests/test_install tests/test_cli/test_install_cli.py tests/test_platform_feature_matrix.py -q collected 120 items 119 passed, 1 skipped in 17.16s Commit hooks: Sync plugin versions.....................................................Passed check for merge conflicts................................................Passed ruff.....................................................................Passed ruff-format..............................................................Passed mypy.....................................................................Passed ``` ## Real Behavior Proof - Environment: Windows 11, PowerShell, Python 3.13.13, worktree `C:\git\headroom-stabilization` on branch `jd/cross-platform-stabilization`. - Exact command / steps: built the Windows wheel with `maturin`, extracted `_core.pyd`, ran the new FastAPI route tests and install/matrix tests listed above, then removed generated artifacts before committing. - Observed result: direct start paths now no-op when healthy, skip spawning when the start lock is contended, and stop a wedged runtime before restart. `/v1/compress` now returns original messages quickly on timeout instead of a 503. The real JSON tool-payload smoke test returns `tokens_before > tokens_after`, `tokens_saved > 0`, `compression_ratio < 1.0`, and non-empty transforms through the public route. - Not tested: full native Windows persistent process e2e remains blocked by the upstream CRT/wheel issue already documented in workflows and in the matrix. No real OS service was installed locally; service manager behavior is covered by argument-level unit 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 - [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 CHANGELOG is not updated because this is an unreleased hardening/test/documentation pass. The platform matrix intentionally records partial/blocked Windows/macOS e2e gaps instead of claiming full coverage where the repo cannot currently run it. |
||
|
|
372d6c8cd4
|
fix(wrap): preserve custom Codex provider base_url during proxy injection (#1894)
## Description Refs #1614 (Bug 2 only; Bug 1's config-mutation ordering is covered by a separate PR). `headroom wrap codex` unconditionally pointed the proxy's upstream OpenAI route at `api.openai.com`, even when the user's Codex config already declared a custom OpenAI-compatible provider such as `freemodel.dev`, LiteLLM, or vLLM under `[model_providers.<name>]`. The proxy then silently rerouted traffic to OpenAI, which rejected the user's gateway API key, and Codex interpreted the resulting auth failures as an invalid session. ## Type of Change - [x] Bug fix ## Changes Made - `_detect_custom_codex_upstream_base_url` and `_codex_custom_provider_base_urls` in `headroom/cli/wrap.py` scan the existing `config.toml` for a user-declared custom `[model_providers.*]` table, excluding Codex built-ins and Headroom's own table, and return its `base_url` when the selection is unambiguous: either the top-level `model_provider` names it directly, or a prior wrap left the original provider in the `# was: <original>` comment from `_redirect_existing_top_level_keys`. - The detector falls back to the sole custom provider when exactly one candidate exists and no matching top-level selection is present, which covers the issue repro where the custom table exists without a static top-level provider pin. - `_inject_codex_provider_config` now detects that custom upstream before building the injected provider block. When found, it adds `X-Headroom-Base-Url` to `env_http_headers`, mapped to `HEADROOM_CODEX_UPSTREAM_BASE_URL`, matching Codex's env-var-based header contract. - `codex()` exports the detected value into `HEADROOM_CODEX_UPSTREAM_BASE_URL` for the launched Codex process unless the user already set it. The proxy's OpenAI HTTP handlers already honor `X-Headroom-Base-Url`, so HTTP `/v1/chat/completions` and `/v1/responses` requests forward to the preserved gateway instead of the default OpenAI upstream. This is scoped to the HTTP request path. Codex's WebSocket transport for `/v1/responses` resolves its upstream from a separate header-independent path and keeps the existing behavior. ## Testing - [x] Focused Codex wrap tests passed locally before PR review: `pytest tests/test_cli/test_wrap_codex.py -q` - [x] Broader Codex CLI test selection passed locally before PR review: `pytest tests/test_cli/ -k codex -q` - [x] CI lint, format, and type checks passed on PR head ` |
||
|
|
68676daa50
|
feat: ship the coding profile as Headroom's out-of-box default posture (#1893)
Make a bare `headroom proxy` (and the uvicorn factory / argparse main) default to the cache-mode coding posture instead of requiring users to set a dozen env vars. Profile (agent_savings.py): * "coding" is now the DEFAULT profile (DEFAULT_PROFILE), and it is rewritten for cache mode: proxy_mode="cache" and compress_user_messages=True (cache mode compresses the newest OBSERVATION delta — a user/tool turn — so compress_user must be on or there is nothing to compress; prefix stability is preserved by the delta engine, not by refusing to touch user turns). * AgentSavingsProfile carries the standalone router/handler toggles too (tool_search, cross_turn_dedup, lossless_then_lossy, protect_reads, code_aware, effort_router, lossless, min_chars_for_block); proxy_env() emits them. Defaults preserve current behavior for the other profiles. * coding sets: tool_search=1, dedupe=1, lossless_then_lossy=1, protect_reads=1, code_aware=1, effort_router=0, lossless=0, min_chars_for_block=25. CCR stays ON (no HEADROOM_NO_CCR) so any lossy loss is recoverable. * apply_agent_savings_env_defaults() now honors an explicit HEADROOM_SAVINGS_PROFILE already in the env before falling back to the default. Delivery (pollution-free by construction): * MODE and savings_profile default via INLINE defaults in the config builders (cache / coding) — no global env mutation, so unit tests that build config directly keep clean defaults. * The request-time toggles are seeded into os.environ (setdefault) via seed_proxy_env_defaults() ONLY at the executable/deployment entries — run_server() (before serving) and create_app_from_env() (uvicorn factory) — NOT in the CLI command or any library builder, so CliRunner tests never leak coding defaults into os.environ across tests. * CLI code_aware now defaults ON, matching the argparse server path (degrades to a no-op without tree-sitter). All explicit user env vars / CLI flags still win (setdefault + `or` fallbacks). HEADROOM_LOSSLESS was already 0 by default; unchanged. Tests: coding-profile + CLI-proxy-env tests updated to the new defaults; 1047 passed across the touched areas (only pre-existing memory/env failures remain). ## 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. --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
38074888ac
|
fix(docker): report source build version (#1862)
## Description Closes #1858 Docker/Compose source builds could report stale or misleading version information: the dashboard initially rendered a hardcoded `v0.3.0`, then `/health` replaced it with installed package metadata, which can be stale when building locally from `main` without release metadata in the image. This change makes source Docker Compose builds report an explicit source-build identity, removes the stale dashboard fallback, and keeps CLI/doctor version checks from treating source-build labels as release-version drift. ## 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 - Add `HEADROOM_VERSION` / `HEADROOM_BUILD_VERSION` runtime version overrides and optional packaged `_build_info.py` metadata. - Teach Docker Compose source builds to pass a `source-build` sentinel that the Dockerfile expands to `source-build+g<sha>` when git metadata is available, or `source-build+sha256.<digest>` otherwise. - Keep release/published image builds on normal package metadata when `HEADROOM_BUILD_VERSION` is unset. - Include only minimal `.git` metadata in the Docker build context so the source-build label can identify the checkout without copying git objects. - Treat source-build labels and raw hashes as non-release labels in `wrap` and `doctor`, avoiding false stale-proxy restarts and drift warnings. - Replace the dashboard hardcoded `0.3.0` fallback with `loading` / `unknown` and format non-release build labels without a `v` prefix. - Include the runtime version in proxy startup logs, `/health`, `/livez`, and OTEL service version reporting. ## Testing - [x] Unit tests pass (`pytest` in GitHub CI) - [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 GitHub CI: all checks passing - CI: build, build-wheel, lint, test shards, test-extras, test-agno, test-dashboard-ui - Docker: docker-native-e2e, docker-wrap-e2e, docker-init-e2e - Native wrappers: macOS, Windows, Ubuntu - Security: CodeQL, gitleaks, pip-audit - Governance: template, label, merge-conflicts, commitlint $ HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1858-version-mismatch pytest tests/test_package_init_lazy.py::test_version_prefers_explicit_build_env tests/test_package_init_lazy.py::test_version_label_helpers_only_prefix_release_versions tests/test_package_init_lazy.py::test_version_uses_packaged_build_metadata tests/test_package_init_lazy.py::test_observability_version_uses_runtime_version tests/test_docker_compose_persistence.py tests/test_cli_doctor.py::TestProxyLiveness::test_up_leaves_source_label_unprefixed tests/test_cli_doctor.py::TestVersionDrift::test_non_release_version_labels_skip_drift_comparison tests/test_cli/test_wrap_persistent.py::test_proxy_version_restart_ignores_non_release_source_labels tests/test_proxy_dashboard_stats_cache.py::test_dashboard_uses_cached_stats_and_lazy_history_feed_polling -q 13 passed, 1 warning $ uvx ruff==0.15.17 check . All checks passed! $ uvx ruff==0.15.17 format --check . 1058 files already formatted $ uvx mypy==1.20.2 headroom --ignore-missing-imports Success: no issues found in 407 source files $ git diff --check # no output $ docker compose config # resolved headroom-proxy build args include HEADROOM_BUILD_VERSION: source-build $ HEADROOM_BUILD_VERSION=6266a1d docker compose config # explicit override is preserved as HEADROOM_BUILD_VERSION: 6266a1d $ docker build --check --build-arg HEADROOM_BUILD_VERSION=source-build . Check complete, no warnings found. ``` ## Real Behavior Proof - Environment: macOS local checkout, Python 3.13.5, Docker Desktop builder `desktop-linux`, plus GitHub Actions CI. - Exact command / steps: `docker compose config`, `HEADROOM_BUILD_VERSION=6266a1d docker compose config`, and `docker build --check --build-arg HEADROOM_BUILD_VERSION=source-build .`. - Observed result: Compose defaults the top-level `headroom-proxy` build arg to the `source-build` sentinel, preserves explicit overrides, and Dockerfile syntax/check validation passes for the source-build path. - Not tested: Full end-to-end release publishing flow; this PR only changes local/source-build reporting. - CI proof: GitHub Actions completed successfully across Docker E2E, CI test shards, lint/type checks, native wrapper checks, security checks, and PR governance. ## 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/CI with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes Docs and changelog are N/A for this runtime-reporting bug fix. The PR is open and ready for review with all GitHub checks passing. |
||
|
|
1573f1fd07
|
fix: use rtk native Cursor hook instead of injecting .cursorrules (#756) (#1846)
## Description `headroom wrap cursor` unconditionally injected an `rtk`-usage instructions block into `.cursorrules`. rtk itself supports a native hook for Cursor (`rtk init --agent cursor`) — the same registration mechanism headroom already uses for Claude Code — which rewrites shell commands transparently with zero custom-instructions text needed. Headroom never tried that path for Cursor, so users got a redundant `.cursorrules` file duplicating guidance the native hook already provides silently. A follow-up commit hardens the switch: `register_agent_hooks` returns `True` on rtk exit 0, but some rtk builds exit 0 without writing `~/.cursor/hooks.json`. headroom now trusts the on-disk hook file, not the exit code, before skipping the `.cursorrules` fallback. Closes #756 ## 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/rtk/installer.py`: generalized `register_claude_hooks` into `register_agent_hooks(rtk_path, *, agent="claude")`, which passes `--agent <agent>` to `rtk init` for non-Claude agents. `register_claude_hooks` kept as a thin wrapper for backward compatibility. Added `RTK_NATIVE_HOOK_AGENTS` documenting which agents rtk supports a native hook for. - `headroom/cli/wrap.py`: `wrap cursor` now calls `register_agent_hooks(rtk_path, agent="cursor")` first, and only skips the `.cursorrules` fallback when `~/.cursor/hooks.json` is actually on disk; otherwise it falls back to `_inject_rtk_instructions(...)`. - Tests: `tests/test_rtk_installer.py` and `tests/test_cli/test_wrap_bridge.py` cover the native-hook path, the on-disk verification, and the `.cursorrules` fallback. - `CHANGELOG.md`: added an entry under `## Unreleased` / `### Fixed`. ## 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 ruff format --check headroom/ tests/ e2e/ 953 files already formatted $ python -m ruff check headroom/cli/wrap.py headroom/rtk/installer.py tests/test_cli/test_wrap_bridge.py tests/test_rtk_installer.py All checks passed! $ python -m pytest tests/test_cli/test_wrap_bridge.py -k cursor -q 3 passed ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13, local checkout; `python -m pytest` / `ruff` run directly. - Exact command / steps: `python -m pytest tests/test_cli/test_wrap_bridge.py -k cursor -q` — the first test mocks `register_agent_hooks` to write `~/.cursor/hooks.json` and asserts `.cursorrules` is NOT created; the second mocks it to write nothing and asserts `.cursorrules` IS created with the `headroom:rtk-instructions` marker; the third exercises the explicit registration-failure fallback. - Observed result: `3 passed`. Native-hook path skips `.cursorrules` only when the hook file exists on disk; every other outcome falls back to `.cursorrules`, so Cursor always gets RTK guidance. - Not tested: real `rtk` binary writing `~/.cursor/hooks.json` end-to-end — that path is covered by the `docker-wrap-e2e` CI job, not locally. ## 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 have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — CLI-only change. ## Additional Notes Scope: rtk's native-hook-capable agents include `claude`, `cursor`, `windsurf`, `cline`, `kilocode`, `antigravity`, `pi`, `hermes`, but only `cursor` and `claude` have a corresponding `headroom wrap` subcommand today, so this fix only changes `wrap cursor` behavior. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
140cb05fbc
|
fix(rtk): link managed rtk onto PATH instead of mutating the hook (#1698)
## Description `headroom wrap claude` / `headroom update` patched `~/.claude/hooks/rtk-rewrite.sh` after `rtk init --global --auto-patch` wrote it. `rtk` bakes the expected SHA-256 of the canonical hook into itself, so the post-write mutation trips its integrity guard — `rtk verify` reports `hook integrity check FAILED … RTK will not execute` and rtk hard-refuses to run. The patch also only absolutized the `rtk` inside the hook, but `rtk rewrite` emits a bare `rtk` on stdout at runtime that still needs PATH resolution, so the original silent-no-op (#487) was never actually fixed. This leaves the hook untouched and instead links the managed binary onto PATH. Closes #1631 ## 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 - Removed `_patch_rtk_hook_absolute_path` (mutated the canonical hook → broke rtk's SHA-256 integrity guard). - Added `_ensure_rtk_on_path`: symlinks the Headroom-managed `rtk` into a PATH dir (prefers `~/.local/bin`) so the bare `rtk` that `rtk rewrite` emits resolves, leaving the hook byte-for-byte as `rtk init` wrote it. - No-op when a `rtk` already resolves on PATH, on Windows, or when no writable PATH dir exists; never clobbers an existing real file or foreign binary. - Rewrote the test module (`test_wrap_rtk_on_path.py`) for the new behavior. ## 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 $ .venv/bin/python -m pytest tests/test_cli/test_wrap_rtk_on_path.py -q collected 7 items tests/test_cli/test_wrap_rtk_on_path.py ....... [100%] ============================== 7 passed in 0.25s =============================== $ .venv/bin/ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_rtk_on_path.py All checks passed! ``` ## Real Behavior Proof - Environment: macOS arm64, Python 3.14, repo `.venv`, rtk hook-version 2 (matches reporter's rtk 0.28.2 setup). - Exact command / steps: `.venv/bin/python -m pytest tests/test_cli/test_wrap_rtk_on_path.py -q` — covers: no-op when rtk already on PATH, symlink created into a PATH dir when missing, `~/.local/bin` preferred + created on demand, idempotent second run, existing-file not clobbered (falls through to next dir), no-op on Windows and when no writable PATH dir exists. - Observed result: 7 passed; the canonical hook file is never written, so rtk's baked-in SHA-256 stays valid and `rtk verify` no longer fails. - Not tested: live end-to-end `rtk verify` PASS on a machine with rtk installed (no rtk binary in CI sandbox); logic mirrors the reporter's verified manual fix (symlink managed rtk into a PATH dir + untouched canonical hook). ## 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 Type checking / docs / CHANGELOG left unchecked: no public API or docs change, and CHANGELOG is release-managed. The fix is confined to `wrap.py`'s rtk setup path. |
||
|
|
b4205c68e6
|
fix(wrap): replace stale-proxy detection with Vite-style port fallback (#1406)
## Description Replace the 180-line process-killing approach with a 15-line Vite-style socket.bind probe: if port is busy (EADDRINUSE or EACCES), try the next available port. Pure stdlib -- no /proc, no lsof, no subprocess -- works identically on Linux/macOS/Windows. ## Problem When `headroom wrap <agent>` is killed without proper cleanup (window close, SSH timeout, crash), the background proxy becomes orphaned and holds the port. The next `headroom wrap` on the same port would wait 30-45 seconds then fail with a confusing error. This PR takes a simpler, safer approach: find the next available port. No process detection, no killing. ### Related issues - **#589** (Port 8787 reserved by Windows) -- partially addressed: EACCES is now skipped together with EADDRINUSE - **#804** (Shared proxy killed by exiting session) -- already fixed upstream via `_live_proxy_clients` marker files; this PR doesn't touch that code ## Type of Change - [x] New feature (non-breaking change that adds functionality) - [x] Code refactoring (no functional changes) ## Changes Made - `headroom/cli/wrap.py`: Add `_find_available_port()` -- socket.bind loop that skips EADDRINUSE (busy) and EACCES (reserved/privileged) ports, returns first available port in range. Replace `_ensure_proxy` port-bind check with auto-fallback call to `_find_available_port`. Remove `_ensure_port_free()` call from `_start_proxy()`. Remove 8 dead functions: `_find_process_on_port`, `_linux_find_process_on_port`, `_resolve_inode_to_pid`, `_is_headroom_proxy`, `_read_process_cmdline`, `_kill_process`, `_ensure_port_free`, `_format_unbindable_port_error`. - `tests/test_cli/test_wrap_helpers.py`: Remove 14 old `TestEnsurePortFree` tests (mocked /proc parsing, process killing). Add 6 new `TestFindAvailablePort` tests covering: port free, first port busy, multiple busy, EACCES skipped, unexpected error propagated, range exhausted. - `tests/test_cli/test_wrap_persistent.py`: Adapt persistence tests for `_find_available_port` mock. Rewrite unbindable-port test to use new error path. Zero new dependencies. Zero changes to core proxy server, MCP, compression, or providers. ## Testing - [x] Unit tests pass (`python -m pytest tests/test_cli/ -v`) - [x] New tests added for new functionality ### Test Output ``` > python -m pytest tests/test_cli/test_wrap_helpers.py::TestFindAvailablePort -v --no-header ============================= 6 passed ============================== test_port_free_returns_same PASSED test_port_busy_finds_next PASSED test_multiple_busy_ports PASSED test_propagates_unexpected_error PASSED test_propagates_eaddrinuse_with_eacces PASSED test_exhausts_range PASSED > python -m pytest tests/test_cli/ -q ============================= 445 passed in 8.40s ============================== ``` ## Real Behavior Proof - Environment: Ubuntu 24.04 x86_64, Python 3.12.3 - Exact command / steps: Ran `python -m pytest tests/test_cli/test_wrap_helpers.py::TestFindAvailablePort -v` -- 6/6 pass for port fallback. Ran full test suite `python -m pytest tests/test_cli/ -q` -- 445/445 pass. - Observed result: `_find_available_port(8787)` returns 8787 when free, 8788 when 8787 is busy. EACCES skipped same as EADDRINUSE. Non-retryable errors (EADDRNOTAVAIL) propagate immediately. - Not tested: Windows EACCES fallback (no Windows CI runner). macOS port fallback (no macOS runner). Code path is identical across platforms (stdlib socket only). ## 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] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally |
||
|
|
6fb5f3bc3d
|
fix(install): persist --no-http2 override through install apply (#1676)
## Description `headroom install apply` regenerates the deployment manifest on every run, and that regeneration silently drops any manually-added `--no-http2` override. The HTTP/2 workaround itself is already real and already supported by `headroom proxy`, but persistent installs had no first-class way to keep it. This PR adds `--no-http2` to `install apply`, threads it into `build_manifest()`, and persists the flag in `manifest.proxy_args` so it survives reapply. Closes #1615 ## 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 `--no-http2` to `headroom install apply`, and forwarded the flag into `build_manifest()`. - Extended `headroom/install/planner.py` so `build_manifest(..., no_http2=True)` persists `--no-http2` into `manifest.proxy_args`. - Added planner-level regression coverage for both the override path and the default-preservation path. - Added CLI-level regression coverage that proves `install apply --no-http2` forwards correctly and that the help surface advertises the flag. - `CHANGELOG.md` intentionally not touched: repo policy generates changelog entries from conventional commits rather than manual PR edits. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_install/test_planner.py` and `uv run pytest tests/test_cli/test_install_cli.py`) - [x] Linting passes (`uv run ruff check .` and `uv run ruff format . --check`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text > rtk uv run pytest tests/test_install/test_planner.py -k no_http2 -q collected 7 items / 5 deselected / 2 selected tests\test_install\test_planner.py .. [100%] 2 passed, 5 deselected in 0.18s > rtk uv run pytest tests/test_cli/test_install_cli.py -k no_http2 -q collected 19 items / 17 deselected / 2 selected tests\test_cli\test_install_cli.py .. [100%] 2 passed, 17 deselected in 0.23s > rtk uv run pytest tests/test_install/test_runtime.py -q collected 19 items tests\test_install\test_runtime.py ..........F........ [100%] FAILED tests/test_install/test_runtime.py::test_runtime_start_lock_blocks_another_process 1 failed, 18 passed in 0.44s (Confirmed pre-existing on unmodified origin/main via `git stash` in this worktree, identical failure with none of this PR's changes applied. Environment-specific lock-file flakiness in this sandbox, unrelated to install-manifest persistence; runtime.py was not touched by this change.) > rtk uv run ruff check headroom/cli/install.py headroom/install/planner.py tests/test_install/test_planner.py tests/test_cli/test_install_cli.py All checks passed! > rtk uv run ruff format --check headroom/cli/install.py headroom/install/planner.py tests/test_install/test_planner.py tests/test_cli/test_install_cli.py 4 files already formatted ``` ## Real Behavior Proof - Environment: local source checkout with `uv` dev environment, using the existing install CLI and manifest builder, in worktree `D:\Repos\headroom-pr-1615-persist-install-http2-override`. - Exact command / steps: ran `headroom install apply --help` through `CliRunner`, ran a direct `build_manifest(..., no_http2=True)` proof, and ran the focused planner, CLI, runtime, and lint checks. - Observed result: on `origin/main`, `install apply --help` lacked `--no-http2` and `build_manifest(..., no_http2=True)` raised `TypeError: build_manifest() got an unexpected keyword argument 'no_http2'`; on this branch, `install apply --help` lists `--no-http2`, `build_manifest(..., no_http2=True)` returns a manifest whose `proxy_args` contains exactly one `--no-http2` entry (`['--host', '127.0.0.1', '--port', '8787', '--mode', 'token', '--backend', 'anthropic', '--telemetry', '--no-http2']`), persistent installs now preserve the existing HTTP/2 disable flag across `install apply` regeneration, and runtime behavior still comes entirely from replaying manifest `proxy_args` (`runtime.py` was not modified). - Not tested: a full persistent-service supervisor round-trip or full CI suite locally. ## 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 - [ ] 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 - [ ] I have updated the CHANGELOG.md if applicable (not applicable, changelog entries are generated from conventional commits per repo policy) ## Additional Notes This stays scoped to the install-manifest persistence seam only; it does not revisit HTTP/2 default policy, retry behavior, or proxy transport construction. Attribution: the implementation shape follows the persistence pattern already established by #1365, and the remaining install-layer gap was confirmed by `sarkarsital1959` in the 2026-07-01 comment on #1615. |
||
|
|
84509a4b89
|
fix: detect and clear stale ANTHROPIC_BASE_URL from crashed wrap sessions (#1768) (#1837)
## Description `headroom wrap claude` writes `env.ANTHROPIC_BASE_URL` (or the foundry/vertex variant) into a project's `.claude/settings.local.json` so daemon-spawned Claude Code workers route through the local Headroom proxy. Removal only happened in the wrap process's `finally:` block. An unclean exit — `SIGKILL`, OOM, reboot, or terminal/tmux close (`SIGHUP`, which was not caught; only `SIGINT`/`SIGTERM` were) — skipped that cleanup, so the entry persisted indefinitely. Every subsequent bare `claude` in that project then routed to the dead port and hung indefinitely retrying it. Closes #1768 ## 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 - `_write_claude_wrap_base_url` now optionally stamps a sidecar marker (`.claude/.headroom_wrap_marker.json`) recording the writer's pid/identity, the port, and the true prior value — kept out of `settings.local.json` itself so Headroom bookkeeping never shows up as a stray key in a file Claude Code's own config loader parses. - A shared `_identity_mismatch` helper (factored out of the existing `_marker_pid_reused` proxy-client-refcounting logic) lets a marker be judged stale: missing/invalid pid, dead pid, or a live pid whose identity doesn't match the recorded one (PID reuse after a crash). - `claude()` now checks for — and self-heals — a stale marker immediately before writing a fresh entry, restoring the recorded prior value instead of trusting a leftover from a dead session. - `claude()` now also registers a `SIGHUP` handler (guarded via `hasattr`, since Windows has none) alongside the existing `SIGTERM` handler, so terminal-close triggers the same cleanup/restore path. - `headroom unwrap claude` now reads the marker's recorded prior value before restoring, instead of unconditionally deleting the key — so a user's own pre-existing `ANTHROPIC_BASE_URL` (set before ever running `wrap`) isn't blindly wiped. - `headroom doctor` gained a new check (`check_wrap_marker_staleness`) that flags a stale project-local marker and points at `headroom unwrap claude` to clean it up — separate from the existing global-settings `check_claude_routing` check. - (Unrelated, pre-existing on `main`) reformatted `headroom/proxy/handlers/openai.py`, `tests/test_openai_codex_ws_lifecycle.py`, `tests/test_output_shaper.py` — whitespace/indentation only, no logic change — since they were already failing `ruff format --check .` on `main` before this branch touched anything, and the repo-wide lint gate blocks on it. Out of scope: `wrap --worktree` — no such flag or multi-worktree `.claude` handling exists anywhere in `wrap.py` today; not adding new surface for an aspirational scenario the issue mentions but that isn't implemented. ## 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_cli/test_wrap_claude_base_url.py tests/test_cli/test_unwrap_claude.py tests/test_cli/test_wrap_stale_marker.py -q 42 passed $ pytest tests/test_cli -q 512 passed, 1 failed (test_wrap_codex_prepare_only_registers_serena_when_uvx_exists — confirmed to fail identically on a clean checkout of main with no changes applied; test-order flake, unrelated to this PR) $ ruff check . All checks passed! $ ruff format --check . 1047 files already formatted $ mypy headroom/cli/wrap.py headroom/cli/doctor.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: local checkout, Python 3.13, Windows. - Exact command / steps: wrote a base_url entry + marker via `_write_claude_wrap_base_url(..., port=8787)`, then overwrote the marker's recorded pid with a value guaranteed not to be a live process (simulating the crash from the issue's own repro: `headroom wrap claude -- -p ok & ; kill -9 <wrap-pid>`). Ran `headroom.cli.doctor.check_wrap_marker_staleness()` against that path, then called `_check_and_clear_stale_wrap_marker()` (the same check `claude()` now runs before writing a fresh entry). - Observed result: `doctor`'s check correctly reports `WARN` naming the dead pid/port and pointing at `headroom unwrap claude`. The stale-check call then self-heals: in the "nothing existed before wrap" case the leaked entry is removed; in a second run seeded with a real pre-existing `ANTHROPIC_BASE_URL` (set before `wrap` ever ran), that original value is recovered instead of being deleted. In both cases the marker file is cleared afterward. - Not tested: actual OS-level signal delivery (`kill -HUP` against a real running `headroom wrap claude` subprocess) — the SIGHUP registration is exercised via a source-inspection test instead of a live signal, since spawning/killing the real CLI subprocess isn't practical in this environment; verified E2E via CI's `wrap-native` jobs (Ubuntu/macOS) which passed. ## 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 have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — CLI/backend fix, no UI surface. ## Additional Notes - Documentation checklist item left unchecked: no user-facing docs currently describe wrap's settings.local.json write/cleanup behavior in enough detail to need updating; happy to add a troubleshooting note if maintainers want one. - `wrap --worktree` handling is out of scope (see Changes Made) — flagging in case maintainers want it tracked as a separate follow-up issue. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
afd9cbdfaf
|
fix(copilot): normalize subscription routing host (#1836)
## Description `headroom wrap copilot --subscription` can currently trust the token-exchange host for individual Copilot seats, which routes newer responses-API models like `gpt-5.4` to `api.individual.githubcopilot.com` and reproduces the transient `502` retry loop from issue #1694. This normalizes that public individual-seat host back to the generic Copilot API host while preserving dedicated business or explicitly pinned hosts. Closes #1694. ## 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 - Normalized exchanged Copilot subscription hosts through the existing public-host classifier instead of trusting the raw token-exchange payload. - Added a regression proving `api.individual.githubcopilot.com` downgrades to `https://api.githubcopilot.com` for subscription routing. - Added a wrap-level regression proving subscription launches export the normalized host into the proxy env. - Preserved business-host and explicit `GITHUB_COPILOT_API_URL` routing behavior. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_copilot_auth.py -q`) - [x] Unit tests pass (`uv run pytest tests/test_cli/test_wrap_copilot.py -q`) - [x] Linting passes (`uv run ruff check headroom/copilot_auth.py tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text uv run pytest tests/test_copilot_auth.py -q 57 passed, 1 warning in 0.34s uv run pytest tests/test_cli/test_wrap_copilot.py -q 31 passed, 1 warning in 0.32s uv run ruff check headroom/copilot_auth.py tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py All checks passed! uv run ruff format --check headroom/copilot_auth.py tests/test_copilot_auth.py tests/test_cli/test_wrap_copilot.py 3 files already formatted ``` ## Real Behavior Proof - Environment: Windows, Python `uv` environment, mocked Copilot token-exchange and wrap launch surfaces. - Exact command / steps: run the focused Copilot auth and wrap regression tests after teaching subscription token-exchange routing to normalize the public individual-seat host. - Observed result: exchanged subscription tokens that advertise `https://api.individual.githubcopilot.com` now route through `https://api.githubcopilot.com`, while business-host and explicit-host pin cases stay unchanged. - Not tested: a live GitHub Copilot subscription request against the upstream service. ## 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 This is intentionally scoped to host selection for exchanged Copilot subscription tokens. It does not change token discovery, token pinning, or non-subscription OAuth routing. |
||
|
|
4bd3ddfaa5
|
fix(opencode): use local MCP config (#1383)
## Description Fixes OpenCode Headroom MCP configuration across wrap, MCP install/status/uninstall, and persistent install docs/CLI. OpenCode was being configured to use a remote HTTP MCP endpoint at `/mcp`, but the Headroom proxy does not expose MCP there. The correct OpenCode configuration is a local stdio MCP server that runs `headroom mcp serve`. Closes #1380 ## Type of Change - [x] Bug fix - [ ] New feature - [ ] Breaking change - [x] Documentation update - [x] Tests ## Changes Made - Changed OpenCode MCP registration to emit `type: "local"` with `command: ["headroom", "mcp", "serve"]`. - Changed OpenCode MCP environment serialization from `env` to OpenCode's `environment` key, while still reading legacy `env` entries. - Removed generated remote `/mcp` entries from OpenCode wrap/runtime config. - Made `wrap opencode --no-mcp` skip persistent `mcp.headroom` injection. - Kept provider-only OpenCode config injection from writing MCP; MCP persistence is owned by the registrar path. - Made `headroom mcp status` and `headroom mcp uninstall` use the registrar lifecycle so OpenCode is covered. - Added `opencode` to persistent install `--target` choices. - Clarified OpenCode persistent install docs to use `--scope provider` for direct `opencode.json` edits. - Added regression coverage for registrar serialization, wrap behavior, runtime config, provider-scope install, MCP CLI lifecycle, and install target parsing. ## Testing - [x] `rtk .venv/bin/python -m pytest tests/test_mcp_registry tests/test_cli/test_mcp.py tests/test_cli/test_wrap_opencode.py tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py tests/test_install -q` - [x] Result after absorbing #1381 overlap: `263 passed, 1 skipped` - [x] Targeted Ruff check passed for the changed Python/test files. - [x] Targeted Ruff format check passed for the changed Python/test files. - [x] Isolated HOME smoke tests with real `opencode mcp list --pure`. ## Real Behavior Proof - `headroom mcp install --agent opencode --proxy-url http://127.0.0.1:9000 --force` against an isolated HOME wrote a valid local OpenCode MCP entry with `environment.HEADROOM_PROXY_URL`. - `opencode mcp list --pure` against that isolated HOME connected to `headroom mcp serve`. - `headroom wrap opencode --prepare-only --no-rtk --no-serena --port 9001` wrote local MCP plus provider config. - `headroom wrap opencode --prepare-only --no-rtk --no-serena --no-mcp --port 9002` wrote provider config without `mcp.headroom`. - Generated runtime `OPENCODE_CONFIG_CONTENT` was accepted by `opencode mcp list --pure`; `include_mcp=False` reported no MCP servers. - `headroom mcp status` detected the isolated OpenCode config and read the custom proxy URL. - `headroom mcp uninstall` removed `mcp.headroom` from the isolated OpenCode config while leaving provider config intact. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review |
||
|
|
c9d717c13c
|
fix(wrap): remove rtk instructions from Codex AGENTS.md on unwrap (#1604)
## Description `headroom wrap codex` injects Headroom's marker-fenced rtk instruction block into the Codex **global** `AGENTS.md` (`_codex_home_dir() / "AGENTS.md"`), so Codex voluntarily prefixes shell commands with `rtk`. But `headroom unwrap codex` only restored `config.toml` and cleaned up the MCP/Serena servers — it never removed that `AGENTS.md` block. The result: after unwrapping, a plain `codex` launch still inherits Headroom's behavior and keeps trying to run `rtk`. If the managed rtk binary directory is no longer on `PATH`, commands fail outright: ```text rtk : The term 'rtk' is not recognized as the name of a cmdlet, function, script file, or operable program. Conversation interrupted ``` `unwrap copilot` already calls `_remove_rtk_instructions(...)`; Codex was simply missing the same cleanup step. Closes #1421 ## Fix Call the existing `_remove_rtk_instructions` helper on the Codex global `AGENTS.md` inside `unwrap_codex`, right after the MCP-server cleanup: ```python if _remove_rtk_instructions(_codex_home_dir() / "AGENTS.md"): click.echo(" Removed Headroom rtk instructions from Codex AGENTS.md.") ``` The helper strips only the marker-fenced block and rewrites the rest of the file (deleting it only if nothing else remains), so user-authored `AGENTS.md` content is preserved. The call is unconditional and best-effort, matching the existing MCP-server cleanup in the same function. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/cli/wrap.py`: `unwrap_codex` now removes the marker-fenced rtk block from the Codex global `AGENTS.md` via `_remove_rtk_instructions`, with a status echo. - `tests/test_cli/test_wrap_codex.py`: regression tests — block removed on unwrap, surrounding user content preserved, and a no-op when `AGENTS.md` is absent. - `CHANGELOG.md`: Bug Fixes entry under Unreleased. ## 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 Before the fix the two removal tests fail (the no-AGENTS.md safety test passes either way); after the fix the whole file is green: ```text # before the fix (wrap.py reverted, tests kept) FAILED tests/test_cli/test_wrap_codex.py::...::test_unwrap_removes_rtk_block_from_global_agents FAILED tests/test_cli/test_wrap_codex.py::...::test_unwrap_preserves_user_content_in_global_agents ================= 2 failed, 1 passed, 66 deselected in 1.00s ================== # after the fix tests\test_cli\test_wrap_codex.py ...................................... ............................... ============================= 69 passed in 7.45s ============================== ``` ```text $ uv run ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_codex.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12.11, headroom built from this branch (`uv sync --extra dev`). - Exact command / steps: set `CODEX_HOME` to a temp dir, wrote a user `AGENTS.md`, injected the rtk block with the same helper `wrap codex` uses, then ran the real `unwrap codex` command (`unwrap_codex.callback(port=8787, no_stop_proxy=True)`) and re-read the file. No mocking of the code under test. - Observed result: the command printed `Removed Headroom rtk instructions from Codex AGENTS.md.`, the rtk marker is gone, and the user's own content survived: ```text === AGENTS.md BEFORE unwrap === # My rules Always write tests. <!-- headroom:rtk-instructions --> # RTK (Rust Token Killer) - Token-Optimized Commands ... <!-- /headroom:rtk-instructions --> rtk marker present before: True --- running: headroom unwrap codex --no-stop-proxy --- Removed Headroom rtk instructions from Codex AGENTS.md. ✓ Codex is no longer routed through the Headroom proxy. === AGENTS.md AFTER unwrap === # My rules Always write tests. rtk marker present after: False user content preserved: True ``` - Not tested: did not run a full real `codex` binary session end-to-end (not installed in this environment); the global-`AGENTS.md` state is the durable thing the bug was about, and it's exercised here for real. Did not run the full `mypy headroom` pass (one-line cleanup call, no new types). ## 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 have updated the CHANGELOG.md if applicable ## Additional Notes - Single logical change, no new dependencies. Reuses the existing `_remove_rtk_instructions` helper, so there's no new removal logic to maintain. - @chopratejas this mirrors the `unwrap copilot` cleanup; flagging you since you've been triaging the wrap/unwrap issues. Co-authored-by: JD Davis <mxjerrett@gmail.com> |
||
|
|
4bf7f92417
|
fix(claude): surface Remote Control proxy incompatibility (#1610)
## Description Claude Code hides Remote Control when it sees a custom `ANTHROPIC_BASE_URL`, so `headroom wrap claude` can make the menu disappear even though normal API requests still route through Headroom. The reported proxy logs show no Remote Control registration, session bootstrap, or device-attestation request at all, which means the decision happens inside Claude before Headroom can forward anything. This change makes that client-side incompatibility explicit in Headroom's Claude launch flow, `headroom doctor`, and troubleshooting docs. API proxying and the existing `ENABLE_TOOL_SEARCH` compatibility shim stay unchanged; users who need Remote Control get a direct instruction to launch Claude without the Headroom proxy for that session. Closes #1601 ## 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 - Add a Claude-specific helper and warning text for the Remote Control custom-base incompatibility. - Surface that warning from `headroom wrap claude` when Claude is launched through `ANTHROPIC_BASE_URL`. - Add a separate `headroom doctor` warning for Claude Remote Control availability, while keeping Claude API-routing status independent. - Document the limitation and workaround next to the existing Claude custom-endpoint troubleshooting guidance. - Add focused regression tests for gated and non-gated Claude routing states, plus preservation coverage for `ENABLE_TOOL_SEARCH`. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_issue_1601_remote_control_gate.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py -q`) - [x] Unit tests pass (`uv run pytest tests/test_issue_746_tool_search.py tests/test_cli/test_init_enable_tool_search.py -q`) - [x] Linting passes (`uv run ruff check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py`) - [x] Formatting passes (`uv run ruff format --check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for the bugfix - [ ] Manual testing performed ### Test Output ```text rtk uv run pytest tests/test_issue_1601_remote_control_gate.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py -q ============================= test session starts ============================= collected 62 items 62 passed, 1 warning rtk uv run pytest tests/test_issue_746_tool_search.py tests/test_cli/test_init_enable_tool_search.py -q ============================= test session starts ============================= collected 33 items 33 passed, 1 warning rtk uv run ruff check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py All checks passed! rtk uv run ruff format --check headroom/cli/wrap.py tests/test_cli_doctor.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py 3 files already formatted ``` ## Real Behavior Proof - Environment: Windows, Python via `uv`, focused Claude CLI and doctor tests. - Exact command / steps: with Claude settings or shell environment containing `ANTHROPIC_BASE_URL=http://127.0.0.1:8787`, run the focused helper and doctor tests, then run the existing `ENABLE_TOOL_SEARCH` preservation tests. - Observed result: Headroom surfaces a Claude Remote Control warning for custom `ANTHROPIC_BASE_URL`, while Claude API routing and `ENABLE_TOOL_SEARCH` behavior stay intact. - Not tested: live Claude Remote Control UI automation. The issue evidence says Claude hides the menu before any request reaches Headroom, so this PR proves Headroom's launch, diagnostics, and docs behavior. ## 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 ## Additional Notes `CHANGELOG.md` stays untouched because this repo's release pipeline generates changelog entries from conventional commits. This is a visibility fix, not a proxy transport restore. The issue evidence shows Claude never sends a Remote Control request while the custom-base gate is active, so the surviving slice is launch-time warning, doctor warning, and documentation. PR `#1600` is adjacent and non-blocking because `#1601` reproduces from process-env `ANTHROPIC_BASE_URL` alone. This intentionally changes `headroom doctor` for fully routed Claude sessions from an all-pass result to one warnings-only result, because the proxied Claude setup is operational for API traffic but still incompatible with Remote Control. |
||
|
|
75427bbd4a
|
fix(wrap): preserve custom Vertex base URL (#1477)
## Description Fixes `headroom wrap claude` in Vertex mode when the user has configured a custom Vertex-compatible gateway through `ANTHROPIC_VERTEX_BASE_URL`. Before this change, wrap mode redirected Claude Code's `ANTHROPIC_VERTEX_BASE_URL` to the local Headroom proxy, but the original custom upstream was not forwarded to the proxy as `VERTEX_TARGET_API_URL`. The proxy therefore fell back to the default Google Vertex endpoints and custom gateways could return 404 or auth/model errors. Closes #1476 ## 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 - Capture the original Vertex upstream before `wrap claude` redirects Claude Code to the local proxy. - Pass custom Vertex upstreams to the proxy as `--vertex-api-url` / `VERTEX_TARGET_API_URL`. - Let explicit `VERTEX_TARGET_API_URL` take precedence over `ANTHROPIC_VERTEX_BASE_URL`. - Guard against accidentally using the local Headroom proxy URL as the proxy's own Vertex upstream. - Restart idle running proxies when their configured Vertex upstream does not match the requested Vertex mode state. - Persist and restore `ANTHROPIC_VERTEX_BASE_URL` for Vertex-mode Claude daemon workers, and clean it up during `unwrap claude`. - Expose `vertex_api_url` in loopback health config so wrapper reuse checks can detect mismatches. ## 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 $ rtk gh pr checks 1477 --repo headroomlabs-ai/headroom CI Checks Summary: [ok] Passed: 22 [FAIL] Failed: 0 $ rtk pytest tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_cli/test_unwrap_claude.py tests/test_azure_foundry_claude_compression.py tests/test_cli/test_wrap_persistent.py tests/test_provider_registry.py -q Pytest: 64 passed $ rtk uvx ruff==0.15.17 check headroom/cli/wrap.py headroom/proxy/server.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_cli/test_unwrap_claude.py All checks passed! $ rtk uvx ruff==0.15.17 format --check headroom/cli/wrap.py headroom/proxy/server.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_cli/test_unwrap_claude.py 4 files already formatted $ rtk python3 -m py_compile headroom/cli/wrap.py headroom/proxy/server.py tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_cli/test_unwrap_claude.py # passed, no output $ rtk uv run --python 3.13 pytest tests/test_vertex_claude_compression.py -q Failed before test collection while building the local editable package: esaxx-rs build failed with fatal error: 'cstdint' file not found. ``` ## Real Behavior Proof - Environment: GitHub Actions CI on PR #1477 plus local macOS worktree `fix/1476-vertex-base-url`. - Exact command / steps: CI ran lint, type checking, build, unit-test shards, native wrapper checks, wrap-native e2e, and Docker e2e jobs; locally ran focused wrapper, unwrap, Foundry, persistent-proxy, and provider-registry tests. - Observed result: CI passed 22 checks with 0 failures; local focused tests passed; Ruff check/format passed; Python compile passed. - Not tested: broader proxy-route tests that import `headroom.proxy.server` through a local editable build could not run locally because the native `esaxx-rs` build fails before test collection with missing `cstdint`. ## 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 - [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 ## Screenshots (if applicable) N/A ## Additional Notes - Documentation, CHANGELOG, and extra code-comment checklist items are N/A for this narrow wrapper bug fix. - Full local unit test execution is limited by the existing native extension build issue described above; focused Python-only coverage passes and GitHub CI is green. |
||
|
|
6cba4419d0
|
fix(wrap): detach the shared proxy on Windows so it survives an ungraceful agent close (#1464)
## Description Closing one `headroom wrap <agent>` instance on Windows could kill the **shared proxy** out from under every other running instance, so their requests started failing. `_start_proxy` launched the proxy as a child of whichever agent started it first, without detaching it from that agent's console and Job object. The wrapper already reference-counts clients via per-PID markers and `_make_cleanup` leaves the proxy running while other clients exist — but that only runs on a *graceful* exit. On an *ungraceful* close (closing the terminal window, `taskkill`, a crash) Windows tree-kills the whole process group/Job and the proxy dies directly, bypassing the reference counting. Every other instance's `ANTHROPIC_BASE_URL` then points at a dead `127.0.0.1:8787`, so all of its API traffic fails. ## 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 - `_start_proxy` creates the proxy with `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB` on Windows, so an ungraceful close of the launching agent can no longer reach it; only the ref-counted `_make_cleanup` ends the proxy. - Falls back without `CREATE_BREAKAWAY_FROM_JOB` (catching `OSError`) when the launcher's Job forbids breakaway; `DETACHED_PROCESS` still spares the proxy from console-close events. - Platform guard is `sys.platform == "win32"` (not `os.name == "nt"`) so mypy narrows the platform and resolves the Windows-only `subprocess` constants. - POSIX path unchanged: `creationflags=0`, detachment still via `start_new_session` (`setsid`). - Added `tests/test_cli/test_wrap_proxy_detach.py` and a CHANGELOG Bug Fixes entry. ## 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_cli/test_wrap_proxy_detach.py -q .. [100%] 2 passed, 2 warnings in 1.50s $ ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_proxy_detach.py All checks passed! $ mypy --follow-imports=silent headroom/cli/wrap.py tests/test_cli/test_wrap_proxy_detach.py Success: no issues found ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.11.9, headroom-ai (pipx). Two concurrent `headroom wrap claude` instances sharing proxy `127.0.0.1:8787`. `_start_proxy` was also exercised directly on this host with `subprocess.Popen` stubbed. - Exact command / steps: (1) start two `headroom wrap claude` instances; (2) close the terminal window of the one that started the proxy (ungraceful — not `/exit`); (3) issue a request from the surviving instance. Separately: call `_start_proxy(8787)` with `subprocess.Popen` stubbed and read back the creation flags. - Observed result: before the fix the proxy died with the closed window and the surviving instance failed (`ANTHROPIC_BASE_URL` → dead `:8787`), because the OS tree-killed the child before the ref-count path could spare it. After the fix the detached proxy survives the close and the surviving instance keeps working; the stub harness reports `creationflags=0x1000208` (`DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB`) on win32 and `0` when forced off-Windows. - Not tested: real breakaway behavior under an actual restrictive Job object on this host (the OS-level effect). The `OSError` fallback path itself now has a dedicated unit test (`test_start_proxy_retries_without_breakaway_when_job_forbids_it`). ## 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 have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — no UI changes. ## Additional Notes - Documentation checklist item is N/A: this is a behavioral bug fix with no user-facing doc surface. - Scope is the single `subprocess.Popen` call in `_start_proxy`; the marker-based reference counting in `_make_cleanup` is unchanged and remains the only thing that intentionally stops the proxy. |
||
|
|
ddd4adf911
|
fix(codex): avoid duplicate headroom provider config (#1431)
## Description Fixes #1425. `headroom wrap codex` could leave `~/.codex/config.toml` invalid when the user already had a `[model_providers.headroom]` table. The previous duplicate-key handling covered top-level `model_provider` and `openai_base_url`, but the provider table was still appended as a static block. That could produce duplicate `env_http_headers` or duplicate provider-table TOML errors before Codex started. ## Type of Change - [x] Bug fix (non-breaking change fixes an 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 - Added a Codex config cleanup helper that removes any pre-existing `[model_providers.headroom]` table from the working copy before `wrap codex` appends the managed Headroom provider block. - Kept unwrap behavior backed by the existing pre-wrap snapshot, so a custom prior `headroom` provider table is restored byte-for-byte on `headroom unwrap codex`. - Added regression tests for TOML validity, a single `env_http_headers` mapping, one managed `[model_providers.headroom]` table, and unwrap restoration. ## Testing - [x] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added to cover the fix ### Test Output ```text Docker: python:3.12-slim Command: uv run --frozen --with pytest pytest tests/test_cli/test_wrap_codex.py Result: 68 passed, 1 warning ``` ## Real Behavior Proof - Environment: disposable Docker container, `python:3.12-slim`, Linux, Python 3.12.13. - Exact command / steps: mounted the worktree into `/workspace`, installed build tools inside the container, then ran `uv run --frozen --with pytest pytest tests/test_cli/test_wrap_codex.py`. - Observed result: all Codex wrap tests passed, including the new regression where an existing `[model_providers.headroom]` table contains `env_http_headers` before wrapping. - Not tested: live interactive `headroom wrap codex` launch against a real user Codex session. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review |
||
|
|
16c638bc21
|
fix: recover persistent proxy feature checks and reject non-Copilot exchange URL (#1465)
## Description This PR fixes two related reliability issues in Copilot wrap/subscription flows: 1. Recovered persistent proxy instances could be reused too early, before validating requested feature-sensitive config (especially `openai_api_url`), which could lead to wrong upstream routing. 2. Subscription token-exchange payloads could provide a non-Copilot API URL; this is now rejected and we safely fall back to user-info/default Copilot endpoint resolution. Related: #488 ## 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 - Updated persistent proxy recover path to: - continue into feature checks when feature-sensitive options are requested - restart persistent deployment when config is missing/mismatched after recovery - keep historical fast return for plain recover-only calls - Hardened subscription exchange URL resolution: - accept exchange `api_url` only when it is a Copilot host - log warning and fall back when non-Copilot host is provided - Added regression tests for: - recovered persistent proxy feature mismatch and config-unavailable restart behavior - non-Copilot exchange host rejection with/without user-info fallback ## 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 $ python -m pytest -q tests/test_copilot_auth.py tests/test_cli/test_wrap_persistent.py ============================= test session starts ============================= platform win32 -- Python 3.12.8, pytest-9.1.1, pluggy-1.6.0 rootdir: C:\Users\ralf.escher\Documents\headroom collected 82 items tests\test_copilot_auth.py ............................................. [ 54%] ........... [ 68%] tests\test_cli\test_wrap_persistent.py .......................... [100%] ============================= 82 passed in 1.60s ============================== ``` ## Real Behavior Proof - Environment: - Windows - Python 3.12.8 - Local Headroom branch with this patch - Copilot subscription route through local proxy - Exact command / steps: 1. Start local proxy and run Copilot wrap in subscription mode. 2. Execute chat-completions requests through proxy. 3. Inspect runtime proxy logs for outbound target and inbound status. 4. Run focused regression tests: - `python -m pytest -q tests/test_copilot_auth.py tests/test_cli/test_wrap_persistent.py` - Observed result: - Outbound requests routed to Copilot business host: - `path=https://api.business.githubcopilot.com/chat/completions` - Successful proxy responses observed: - `path=/v1/chat/completions status=200` - Model activity logged during successful requests: - `PERF model=gpt-4.1 ...` - Regression tests pass (`82 passed`), covering both fixes. - Not tested: - Full repository test suite - Full lint/typecheck across entire project - Non-Windows runtime verification in this run ## 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 - This PR intentionally excludes incidental local edits to `.github/copilot-instructions.md`. - Scope is limited to this bug fix and regression coverage; linked as related work to #488. |
||
|
|
bd76235f5c
|
fix(cli): harden all CLI surfaces + fix docs accuracy (#1491)
## Summary
Full CLI audit + documentation accuracy pass. All 5 commits on this
branch:
### CLI Hardening (4 commits)
- **Clean errors instead of tracebacks**: corrupt manifests, missing
Docker, malformed JSONL, bad `--profile`, invalid env-var values all now
raise `click.ClickException` with helpful messages
- **Range validation**: ~25 numeric flags across 10 files now use
`click.IntRange`/`FloatRange` — `--port 0`, `--hours -1`, `--limit 0`
etc. produce clean usage errors instead of silent wrong behavior
- **Flag combination warnings**: conflicting combos (`--no-rate-limit` +
`--rpm`, `--no-optimize` + `--target-ratio`, `--telemetry` +
`--no-telemetry`) emit yellow warnings on stderr
- **`memory --db-path` default fixed**: was resolving to
`headroom_memory.db` (wrong bare file); now uses project store
`./.headroom/memory.db` if present, else `~/.headroom/memory.db`
- **`memory list --search` + filters**: `--scope`/`--session`/`--since`
were silently ignored when `--search` was also set; now filters are
applied to search results
- **`learn --verbosity --apply` now works**: the output shaper is off by
default (`HEADROOM_OUTPUT_SHAPER`); `--apply` now hot-enables it via
`POST /admin/runtime-env` on a running proxy, or prints explicit `export
HEADROOM_OUTPUT_SHAPER=1` instructions when no proxy is running
- **`perf --hours` overflow**: `1e9` hours no longer raises
`OverflowError`; treated as "all data"
- **`evals memory --categories` invalid input**: `abc,1,2` now raises
`BadParameter` instead of a raw `ValueError` traceback
### Documentation (1 commit, 20 files)
Corrected factual errors found by 3 parallel audit agents across root
docs, wiki, and the published Fumadocs site:
**Critical (caused runtime errors or wrong behavior if followed):**
- `simulation.mdx`: `plan.transforms_applied` -> `plan.transforms`;
`plan.savings_percent` -> computed from available fields (both raised
`AttributeError`)
- `shared-context.mdx`: `import { SharedContext } from "headroom"` ->
`"headroom-ai"` (5x `ImportError`)
- `claude-code-azure-foundry.mdx`: `pip install headroom` -> `pip
install headroom-ai`
- `api-reference.mdx` + `configuration.mdx`: `from headroom import
GoogleProvider` -> `from headroom.providers import GoogleProvider`
- `ccr.mdx`: CCR TTL default 300s -> 1800s (30 min)
**Fabricated flags removed:**
- `wiki/proxy.md` + `wiki/cli.md`: `--no-intelligent-context`,
`--no-intelligent-scoring`, `--no-compress-first` (none exist); replaced
with real CCR flags
- `wiki/configuration.md`: `--no-ccr-responses`, `--no-ccr-expansion`
(none exist); replaced with real flags
- `wiki/troubleshooting.md`, `wiki/metrics.md`,
`docs/troubleshooting.mdx`: `headroom proxy --log-level debug` (flag
doesn't exist)
**Stale content corrected:**
- `llms.txt`: telemetry stated as enabled-by-default (it's opt-in); wrap
list had 5 tools (now 11)
- `README.md`: compatibility matrix added 5 missing `wrap` targets;
`unwrap`, `doctor`, `init`/`install`, savings-analytics now mentioned
- `SECURITY.md`: supported version table showed 0.2.x (current: 0.27.x)
- `wiki/learn.md`: 5 missing flags added; verbosity shaper-off behavior
documented
- `wiki/quickstart.md`: "Configuration Reference" linked to `api.md`
(wrong) -> `configuration.md`
- `CacheAlignerConfig.enabled` default corrected: `True` -> `False`
- `opencode.mdx`: `--port` default wrong ("random") -> 8787; `openai`
backend removed
- `CONTRIBUTING.md`: broken Markdown table cell fixed
- `docs/meta.json`: `claude-code-azure-foundry` added to nav (was
unreachable orphan page)
- `configuration.mdx`: SDK modes vs proxy `--mode` now clearly
distinguished
## Test plan
- [x] `python -m pytest tests/ -x -q` — 857 passed, 0 failures
- [x] 41-combination CLI smoke test (all flag combos across 8 commands)
— 0 tracebacks
- [x] `ruff check` on all modified Python files — clean
- [x] Docs changes are removals/corrections of fabricated or stale
content; no new claims introduced
|
||
|
|
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. |
||
|
|
b618d2d11a
|
fix: patch rtk hook script to use absolute path after register_claude_hooks (#571)
```markdown
## Description
When `headroom wrap claude` registers RTK hooks, the generated `~/.claude/hooks/rtk-rewrite.sh` script uses a bare `rtk` command that depends on PATH lookup. Since `~/.headroom/bin` is not automatically added to PATH, the hook fails silently and token compression never occurs.
After `register_claude_hooks()` succeeds, a new helper `_patch_rtk_hook_absolute_path()` reads the generated hook script and replaces bare `rtk` references with the absolute binary path (e.g. `/home/user/.headroom/bin/rtk`). The patch is idempotent and only writes back if content actually changed. Paths containing spaces or shell-special characters are safely quoted via `shlex.quote()` before being inserted into the script.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- Added `_patch_rtk_hook_absolute_path(rtk_path, hook_script_path)` in `headroom/cli/wrap.py`
- Called it immediately after `register_claude_hooks()` succeeds in `_setup_rtk()`
- Uses `shlex.quote()` to safely handle absolute paths containing spaces or shell-special characters
- Added regression test `tests/test_cli/test_wrap_rtk_hook_patch.py` covering the basic patch, the space-in-path case, idempotency, missing hook file, and non-bare `rtk` tokens
## Testing
- [x] Manual testing performed
### Test Output
```
python3 -m pytest tests/test_cli/test_wrap_rtk_hook_patch.py -v
============ test session starts ============
collected 5 items
tests/test_cli/test_wrap_rtk_hook_patch.py::test_patches_bare_rtk_to_absolute_path
PASSED [ 20%]
tests/test_cli/test_wrap_rtk_hook_patch.py::test_quotes_path_containing_spaces
PASSED [ 40%]
tests/test_cli/test_wrap_rtk_hook_patch.py::test_idempotent_second_run_is_noop
PASSED [ 60%]
tests/test_cli/test_wrap_rtk_hook_patch.py::test_missing_hook_script_is_noop
PASSED [ 80%]
tests/test_cli/test_wrap_rtk_hook_patch.py::test_does_not_touch_words_containing_rtk
PASSED [100%]
============= 5 passed in 0.73s =============
```
## Real Behavior Proof
- Environment: Linux, Python 3.14.4, pytest 9.1.0, headroom repo at commit
|
||
|
|
6c83790680
|
fix(opencode): write local MCP config (#1381)
## Description Fixes the OpenCode config corruption reported in #1380 for wrap, MCP registration, and provider-scope install paths. OpenCode MCP entries are local stdio servers, not remote HTTP endpoints. This changes Headroom's OpenCode MCP serialization to write `type: "local"` with `command: ["headroom", "mcp", "serve"]`, uses OpenCode's `environment` field for MCP env vars, and still reads the older `env` key for compatibility. This also stops provider-only OpenCode config injection from creating a fake `http://127.0.0.1:<port>/mcp` entry, so `headroom wrap opencode --no-mcp` no longer leaves `mcp.headroom` behind. Finally, the install CLI/docs now accept and document `--target opencode` with provider scope. This does not change the broader `headroom mcp status/uninstall` behavior from #1380; that looks like a separate follow-up. ## 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 - Write OpenCode MCP entries as local stdio config instead of remote `/mcp` config. - Use `environment` for OpenCode MCP env vars while continuing to read legacy `env` entries. - Stop OpenCode provider injection/persistent provider install from adding MCP config. - Keep `--no-mcp` from writing `mcp.headroom` while preserving other MCP entries such as Serena. - Allow `headroom install apply --target opencode` at the CLI layer. - Update OpenCode docs and changelog. ## Testing - [x] Focused unit tests pass - [x] Linting passes (`ruff check .`) - [x] Formatting passes (`ruff format --check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for the fixed behavior - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_mcp_registry_opencode.py tests/test_cli/test_wrap_opencode.py tests/test_providers_opencode_config.py tests/test_providers_opencode_install.py tests/test_cli/test_install_cli.py tests/test_install/test_providers.py Pytest: 164 passed $ uvx ruff check . All checks passed! $ uvx ruff format --check . 986 files already formatted $ uvx mypy --config-file pyproject.toml headroom Success: no issues found in 398 source files ``` ## Real Behavior Proof - Environment: macOS local worktree at `/Users/vinaygupta/Desktop/git/headroom-fix-opencode-mcp-config`; branch `fix-opencode-mcp-config`; commit `aea96208`. - Exact command / steps: ran the focused OpenCode/installer regression suite plus Ruff lint/format checks and mypy commands shown above. - Observed result: the focused tests pass and cover OpenCode MCP serialization as `type: "local"`, `command: ["headroom", "mcp", "serve"]`, `environment` env vars, `--no-mcp` not writing `mcp.headroom`, provider-scope install not adding MCP config, and `install apply --target opencode` being accepted. - Not tested: full `pytest` locally, because collection requires the native `headroom._core` extension in this worktree. Attempting the project runner hit a local native build failure first: `esaxx-rs` failed compiling `src/esaxx.cpp` with `fatal error: 'cstdint' file not found`. The broader generic `headroom mcp status/uninstall` behavior from #1380 is intentionally left for a follow-up. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review Scope note: generic `mcp status/uninstall` support from #1380 is intentionally left as a separate follow-up PR. |
||
|
|
a0cb7982e3
|
fix(cli): add explicit UTF-8 encoding to file I/O in wrap commands (#1126) (#1164)
## Description
On Windows, `Path.read_text()` and `open()` default to the system locale
encoding (cp1252, GBK, etc.) instead of UTF-8. This causes
`UnicodeDecodeError` when reading or writing instruction files that
contain multi-byte UTF-8 characters such as smart quotes or em dashes.
The RTK instructions block itself contains an em dash (U+2014, `—`), so
`_inject_rtk_instructions` crashes on any non-UTF-8 Windows locale when
writing to `.copilot_instructions`, `.clinerules`, `AGENTS.md`, or
similar hint files.
Closes #1126
## 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
- Add `encoding="utf-8"` to all bare `read_text()`, `write_text()`, and
`open()` calls in `headroom/cli/wrap.py` that handle instruction or
config files (18 call sites)
- Update test assertions in `test_wrap_hintfile_agents.py`,
`test_wrap_copilot.py`, and `test_wrap_bridge.py` to read with
`encoding="utf-8"`
- Add `test_inject_rtk_handles_utf8_content` verifying that existing
hint files with smart quotes and em dashes survive RTK injection without
crashing
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] 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_wrap_hintfile_agents.py tests/test_cli/test_wrap_copilot.py tests/test_cli/test_wrap_bridge.py -v
47 passed in 1.28s
```
## Real Behavior Proof
- Environment: Windows 11 China (GBK locale), Python 3.11, headroom main
(
|
||
|
|
dca9853ed9
|
feat(wrap): make tokensave the primary coding-task compressor, Serena the backup (#1230)
## Description Makes **tokensave** ([github.com/aovestdipaperino/tokensave](https://github.com/aovestdipaperino/tokensave)) the **primary coding-task compressor** that `headroom wrap` installs, and demotes **Serena** to a **backup**. tokensave is a local semantic code-graph MCP server (`tokensave serve`): the agent queries it for symbols, call chains, and impact analysis instead of grepping/reading whole files — the same role Serena filled, but as a pre-indexed graph. Serena now only registers when tokensave is unavailable (or when forced with `--serena`). 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 - `headroom/graph/tokensave_installer.py` (new): fetch the prebuilt tokensave release binary for the platform (release-binary only — no `cargo` compile at wrap time); honors `HEADROOM_BINARIES_OFFLINE`; returns `None` (→ Serena) when no asset exists (e.g. x86_64 macOS) or the download fails. - `mcp_registry`: `build_tokensave_spec()`; registration/disable/migrate go through the existing `ServerSpec` + ownership-ledger flow, identical to Serena. - `cli/wrap.py`: new `_setup_coding_compressor` primary/backup policy; tokensave setup/disable/migrate/index helpers. New flags `--no-tokensave` (skip primary) and `--serena` (force backup on); `--no-serena` now means "never register the backup". Default wrap removes a previously Headroom-installed Serena entry once tokensave is primary (user-managed entries preserved). `--code-graph` repointed to tokensave; the legacy `codebase-memory-mcp` install path is dropped (unwrap still cleans up legacy entries). `unwrap claude|codex` remove a ledger-owned tokensave entry. - Strands `HeadroomBundle`: `enable_tokensave_mcp=True` (primary); `enable_serena_mcp` now defaults `False` (backup). - `docs/content/docs/proxy.mdx`: `--code-graph` description updated from codebase-memory-mcp to tokensave. - Tests: tokensave installer (incl. error paths), register/disable/migrate, primary/backup policy, and the binary-resolution/indexing helpers. A scoped `tests/test_cli/conftest.py` offline guard keeps the CLI suite hermetic. ## 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 $ uv run pytest -q tests/test_graph_tokensave.py tests/test_cli/test_tokensave_setup.py tests/test_cli/test_tokensave_helpers.py 41 passed $ uv run pytest -q tests/test_cli/ tests/test_graph.py tests/test_graph_tokensave.py 421 passed # full CLI + graph suites, incl. all pre-existing Serena/unwrap/registry tests $ uv run pytest -q tests/test_mcp_registry/ tests/test_proxy_healthchecks.py passed $ uv run ruff format --check headroom/ tests/ # 822 files already formatted $ uv run ruff check <changed files> # All checks passed! $ uv run mypy headroom/graph/tokensave_installer.py headroom/mcp_registry/install.py Success: no issues found in 2 source files # Coverage on new module headroom/graph/tokensave_installer.py 99% ``` ## Real Behavior Proof - Environment: macOS (darwin arm64), Python 3.14, `uv` dev env; tokensave 7.0.2 binary present on PATH and exercised against this repo's `.tokensave/` graph during development. The installer pins release **v7.0.2** (SHA-256-verified) across macOS arm64, Linux aarch64/x86_64, and Windows x86_64/aarch64. - Exact command / steps: `headroom wrap claude` registers `tokensave serve` as the primary MCP code-graph server and indexes the project; with the binary removed from PATH and `HEADROOM_BINARIES_OFFLINE=1`, the same command falls back to registering Serena. Behavior is pinned by the unit tests (binary-present → tokensave registered + Serena entry removed; binary-absent → Serena fallback; `--serena` forces backup on; `--no-serena` suppresses it; `--no-tokensave` disables primary). - Observed result: tokensave registered as primary on the binary-present path; Serena registered on the unavailable path; unwrap removes only ledger-owned entries. - Not tested: live end-to-end agent session inside Claude Code / Codex against a real provider API; Windows/Linux release-asset download (covered by unit tests with mocked archives, not a live fetch). ## 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 - [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 - CHANGELOG is left untouched: this repo generates it via release-please from Conventional Commits, so a manual edit is N/A. - `strands/bundle.py` shows 0% patch coverage because that module hard-imports the optional `strands` SDK, which CI does not install (the pre-existing `_make_serena_client` was likewise uncovered) — not a regression. - A `test (3)` shard failure on `headroom.memory.bridge` is a pre-existing offline-CI flake (cannot reach huggingface.co); it touches no file in this PR and the scoped offline guard only applies under `tests/test_cli/`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b50d9c17ce
|
feat(wrap): add --1m to preserve the 1M context window on wrap claude (#1158) (#1351)
## Description `headroom wrap claude` is the recommended Claude Code integration, but for subscription users entitled to the **1M** context window it silently caps usable context at **200k**. Root cause (upstream, anthropics/claude-code#68522): when `ANTHROPIC_BASE_URL` points at a custom host (the Headroom proxy), Claude Code does **not** send the `context-1m-2025-08-07` beta header and treats the window as 200k. The `/model opus[1m]` picker selection does not survive a custom base URL, and `CLAUDE_CODE_AUTO_COMPACT_WINDOW` alone does not lift the cap. Headroom itself already forwards `anthropic-beta` and sizes Opus at 1M internally — but since `wrap claude` owns the launched process's environment and is the documented path, users hit this and blame Headroom first. This adds the opt-in fix the issue proposes. Closes #1158 ## 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 - `headroom/cli/wrap.py`: new opt-in `--1m` flag on `wrap claude`. When set, `ANTHROPIC_MODEL=<opus>[1m]` is exported on the launched process so Claude Code sends the `context-1m` beta header. Logic extracted to a testable helper `_resolve_1m_model`: a model the user already selected via `ANTHROPIC_MODEL` is preserved (only the `[1m]` suffix is appended when missing); otherwise it falls back to the default Opus. Idempotent (no double suffix). Default behavior is unchanged (opt-in). - `tests/test_cli/test_wrap_helpers.py`: unit tests for `_resolve_1m_model` (append-to-user-model, idempotent, default fallback). - `README.md`: `--1m` added to the Claude Code row of the agent compatibility matrix. - `CHANGELOG.md`: Unreleased → Features entry. ## 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 pytest tests/test_cli/test_wrap_helpers.py tests/test_cli/test_wrap_claude_base_url.py -q 61 passed in 0.46s $ uv run ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py All checks passed! $ uv run mypy headroom/cli/wrap.py Success: no issues found in 1 source file ``` #### TDD verification (RED → GREEN) RED — new tests with the prod change reverted (`_resolve_1m_model` absent): ```text E AttributeError: module 'headroom.cli.wrap' has no attribute '_resolve_1m_model' 3 failed, 40 deselected in 0.56s ``` GREEN — with the change applied: ```text 3 passed, 40 deselected in 0.34s ``` ## Real Behavior Proof - Environment: Linux, Python 3.13, headroom @ this branch. - Exact command / steps: `headroom wrap claude --1m --help` shows the new flag, and the flag resolves the model id that triggers the 1M window: ```text $ headroom wrap claude --help | grep -A1 -- --1m --1m Preserve the 1M context window. Behind a custom ANTHROPIC_BASE_URL Claude Code drops the ... # model-id resolution (what --1m exports as ANTHROPIC_MODEL): _resolve_1m_model("claude-opus-4-1-20250805") -> "claude-opus-4-1-20250805[1m]" _resolve_1m_model("claude-opus-4-8[1m]") -> "claude-opus-4-8[1m]" (idempotent) _resolve_1m_model(None) -> "claude-opus-4-8[1m]" (default) ``` - Observed result: with `--1m`, the launched Claude Code process gets `ANTHROPIC_MODEL=<opus>[1m]`, which is the documented trigger for the `context-1m` beta header (verified in the issue against `~/.headroom/logs/proxy.log`). - Not tested: the live Claude Code subscription handshake against Anthropic's servers (requires a 1M-entitled subscription + the proprietary client); the model-id → header behavior is Claude Code's, documented in the issue and upstream anthropics/claude-code#68522. Headroom's side (export the env var that flips it on) is covered above and by the unit 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 - [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 Opt-in only — without `--1m` nothing changes. The `_DEFAULT_1M_MODEL` constant is only consulted when the user has no `ANTHROPIC_MODEL` set; users on a specific model keep it (suffix appended), so the default's freshness does not affect them. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e6bbc40b11
|
fix(codex): retag threads on init so Codex Desktop history stays visible (#961) (#1349)
## Description
Installing Headroom for Codex via `headroom init` can make Codex Desktop
appear to lose its local chat/thread history. The data is never deleted
— Codex filters its sidebar/search by the active `model_provider`, and
the init path set `model_provider = "headroom"` without retagging
existing threads, so native `openai` threads disappeared from the menu.
The install (`headroom.providers.codex.install`) and wrap
(`headroom.cli.wrap`) paths already reconcile thread provider tags
across the proxy boundary (retag `openai -> headroom` on enable). The
init path — `_ensure_codex_provider` in `headroom/cli/init.py`, which is
exactly what the issue reproduces ("Headroom init proxy" provider,
`headroom-init-codex` hook) — was the one place that injected the
provider without retagging. This wires the same reconciliation into the
init path. The revert direction is already handled by `headroom unwrap
codex`.
Closes #961
## 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/cli/init.py`: `_ensure_codex_provider` now calls
`retag_to_headroom(path.parent)` after writing the init provider block,
so existing native threads stay visible under the active `headroom`
provider. Third-party providers (e.g. `anthropic`) are left untouched
(existing `retag_thread_providers` behaviour).
- `tests/test_cli/test_init_cli.py`: regression test seeding a Codex
Desktop `state_5.sqlite` and asserting `_ensure_codex_provider` retags
`openai -> headroom` while leaving other providers alone.
- `CHANGELOG.md`: Unreleased → Bug Fixes entry.
## 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 pytest tests/test_cli/test_init_cli.py tests/test_provider_codex_threads.py tests/test_provider_codex_install.py -q
84 passed in 0.84s
$ uv run ruff check headroom/cli/init.py tests/test_cli/test_init_cli.py
All checks passed!
$ uv run mypy headroom/cli/init.py
Success: no issues found in 1 source file
```
#### RED → GREEN proof
RED — new test with the prod fix (the `retag_to_headroom` call)
reverted:
```text
E AssertionError: existing openai threads not retagged: {'anthropic': 1, 'openai': 2}
1 failed in 0.44s
```
GREEN — with the fix applied:
```text
tests/test_cli/test_init_cli.py::test_init_codex_provider_retags_existing_threads
1 passed in 0.36s
```
## Real Behavior Proof
- Environment: Linux, Python 3.13, headroom @ this branch.
- Exact command / steps: seed a Codex Desktop store
(`<codex_home>/sqlite/state_5.sqlite`) with native threads, then run the
init provider injection:
```text
before init: {'anthropic': 1, 'openai': 2}
after init: {'anthropic': 1, 'headroom': 2}
config model_provider line: ['model_provider = "headroom"',
'[model_providers.headroom]']
```
- Observed result: after init, the two `openai` threads are retagged to
`headroom` (so they stay visible under the now-active provider), while
the `anthropic` thread is left untouched. Before the fix they stayed
`openai` and were filtered out of Codex Desktop's menu.
- Not tested: the live Codex Desktop GUI itself (proprietary, no
sandbox); the filtering behaviour is the documented `thread/list`
provider filter described in the issue, and the store-level retag that
makes history visible is covered above and by the unit test.
## 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
- [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
Scope is limited to the init provider path; the install and wrap paths
already perform this reconciliation. Screenshots N/A (no UI change on
Headroom's side).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
|
||
|
|
8da0b4e565
|
fix(install): guard install_agent_ensure against duplicate runtime spawns (#1301)
## Type of Change
- [x] Bug fix (non-breaking change which fixes an issue)
## Description
`install_agent_ensure` in `cli/install.py` only checked
`probe_ready(health_url)`. If the proxy was alive-but-not-ready (e.g.
during cold start while tokenizers load — ~38s on Windows),
`probe_ready` returned false and it unconditionally called
`_start_deployment` → `start_detached_agent`, spawning a **second
runtime** without:
1. acquiring `acquire_runtime_start_lock`
2. checking `runtime_status`
3. stopping the existing instance
Two proxies then contend for `127.0.0.1:<port>`; only one can bind, and
the deployment ends up wedged (never ready). Every subsequent ensure
spawns yet another runtime → restart storm.
By contrast, the hook path `cli/init.py:_ensure_profile_running` does it
correctly: it acquires the start-lock, checks `runtime_status`, and
`stop_runtime`s a wedged instance before starting a fresh one.
Closes #1151.
## Changes Made
- Added `acquire_runtime_start_lock` to the imports from
`install.runtime` in `headroom/cli/install.py`
- Rewrote `install_agent_ensure` to mirror the guarded pattern from
`_ensure_profile_running` in `cli/init.py`:
- Fast-path probe: if proxy is already ready, return immediately
(preserves existing behavior)
- Lock acquisition: acquire `acquire_runtime_start_lock` — if another
ensure holds it, return without spawning (prevents duplicate)
- Double-checked locking: re-probe `probe_ready` after acquiring the
lock (race window handled)
- Wedged instance detection: if `runtime_status` says "running" but
proxy isn't ready within 15s grace period, call `stop_runtime` before
starting fresh
- Fall through to `_start_deployment` only when truly needed
- Added `_STARTUP_READY_TIMEOUT_SECONDS = 15` constant (matching the
value used in `_ensure_profile_running`)
- **Failure propagation (addresses @JerrettDavis's review feedback):**
removed the `try/except Exception` wrapper around the guarded block.
`install agent ensure` is an automation-facing CLI command and must exit
non-zero on failure so callers can distinguish a successful ensure from
a failed one. The `init.py` hook path retains its `try/except` because
silent retry is intentional there. The control flow is shared; the error
contract is intentionally different because the call sites have
different needs.
- Added 5 regression tests in `tests/test_cli/test_install_cli.py`:
- `test_install_agent_ensure_no_spawn_when_lock_not_acquired` — verifies
no runtime spawned when lock is contended (the core bug)
- `test_install_agent_ensure_stops_wedged_runtime_before_restart` —
verifies `stop_runtime` is called BEFORE `_start_deployment` when
instance is wedged (ordering assertion: `calls.index("stop") <
calls.index("start_deployment")`)
- `test_install_agent_ensure_starts_when_stopped_and_lock_acquired` —
verifies the normal start path including the real `_start_deployment` →
`start_detached_agent` wiring
- `test_install_agent_ensure_no_duplicate_spawn_after_lock_recheck` —
verifies double-checked locking prevents duplicate when proxy becomes
ready between initial probe and lock acquisition
- `test_install_agent_ensure_propagates_start_deployment_failure` —
**new** regression test for the failure-propagation fix: monkeypatches
`_start_deployment` to raise `click.ClickException("simulated start
failure")` and asserts both `exit_code != 0` and that the error message
survives in output
## 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
```
$ uv run python -m pytest tests/test_cli/test_install_cli.py -v --tb=short
tests/test_cli/test_install_cli.py::test_install_apply_starts_service_supervisor PASSED [ 6%]
tests/test_cli/test_install_cli.py::test_install_status_includes_backend_from_health_probe PASSED [ 12%]
tests/test_cli/test_install_cli.py::test_install_restart_uses_internal_helpers PASSED [ 18%]
tests/test_cli/test_install_cli.py::test_install_apply_rejects_invalid_profile PASSED [ 25%]
tests/test_cli/test_install_cli.py::test_install_apply_rejects_provider_scope_targets_without_support PASSED [ 31%]
tests/test_cli/test_install_cli.py::test_install_apply_restores_previous_deployment_after_failed_update PASSED [ 37%]
tests/test_cli/test_install_cli.py::test_install_start_rejects_task_lifecycle PASSED [ 43%]
tests/test_cli/test_install_cli.py::test_install_apply_uses_docker_runtime_for_persistent_docker PASSED [ 50%]
tests/test_cli/test_install_cli.py::test_install_remove_continues_when_runtime_teardown_errors PASSED [ 56%]
tests/test_cli/test_install_cli.py::test_install_agent_ensure_reports_already_healthy PASSED [ 62%]
tests/test_cli/test_install_cli.py::test_install_agent_run_exits_with_foreground_status PASSED [ 68%]
tests/test_cli/test_install_cli.py::test_install_agent_ensure_no_spawn_when_lock_not_acquired PASSED [ 75%]
tests/test_cli/test_install_cli.py::test_install_agent_ensure_stops_wedged_runtime_before_restart PASSED [ 81%]
tests/test_cli/test_install_cli.py::test_install_agent_ensure_starts_when_stopped_and_lock_acquired PASSED [ 87%]
tests/test_cli/test_install_cli.py::test_install_agent_ensure_no_duplicate_spawn_after_lock_recheck PASSED [ 93%]
tests/test_cli/test_install_cli.py::test_install_agent_ensure_propagates_start_deployment_failure PASSED [100%]
============================== 16 passed in 0.29s ==============================
```
```
$ uv run ruff check headroom/cli/install.py tests/test_cli/test_install_cli.py
All checks passed!
$ uv run ruff format --check headroom/cli/install.py tests/test_cli/test_install_cli.py
2 files already formatted
$ uv run mypy headroom/cli/install.py --ignore-missing-imports
Success: no issues found in 1 source file
```
## Real Behavior Proof
- **Environment**: Python 3.11.14, Linux 6.17.0, headroom dev
environment (uv-synced), rebased onto `upstream/main` at `
|
||
|
|
d633e8172c
|
fix(windows): pin UTF-8 encoding on text-mode subprocess calls (#1311)
Fixes #1310. ## Description On Windows, `headroom` startup crashes a subprocess reader thread: ``` UnicodeDecodeError: 'charmap' codec can't decode byte 0x8d in position 7894: character maps to <undefined> ... subprocess.py _readerthread -> buffer.append(fh.read()) ... encodings/cp1252.py ``` Text-mode `subprocess` calls omit `encoding=`, so Python decodes child output with the locale codec (**cp1252** on Windows). Children that emit UTF-8 ??? `cbm index_repository` (indexing sources with chars like `???`/`???`), `claude mcp get/add`, the memory-sync process ??? produce bytes invalid in cp1252 and kill the reader thread. Linux/macOS default to UTF-8, so it's invisible there. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Add `encoding="utf-8", errors="replace"` to every text-mode (`text=True` / `universal_newlines=True`) subprocess call in the `headroom/` package (~50 call sites; several already had it). - `errors="replace"` (not `ignore`) so corrupt bytes surface as `???` rather than vanishing from parsed output. - Add `tests/test_cli/test_subprocess_utf8_encoding.py`: an AST guard asserting every text-mode subprocess call pins `encoding=`. The runtime crash can't reproduce on UTF-8 CI, so the invariant is enforced at the source level instead. ## Testing - [x] Unit tests pass (`pytest`) - New guard test passes (validates 51 call sites). - `tests/test_install`, `tests/test_cli/test_mcp.py`, `tests/test_mcp_registry` pass. (`test_runtime_start_lock_blocks_another_process` fails on this Windows box, but it fails identically on unmodified `main` ??? a pre-existing `msvcrt` lock flake, unrelated.) ### Test Output ```text > python -m pytest tests/test_cli/test_subprocess_utf8_encoding.py -q 1 passed in 0.12s > python -m pytest tests/test_install/ tests/test_cli/test_mcp.py tests/test_mcp_registry/ -q 133 passed, 2 skipped in 15.34s ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.13.13. - Exact command / steps: Started `headroom` without `PYTHONUTF8=1` on a repo with UTF-8 chars in indexable files. Observed the `UnicodeDecodeError` crash. Applied the fix (pinning `encoding="utf-8"` on all text-mode subprocess calls). Re-ran. No crash. The AST guard enforces the invariant on CI (which runs UTF-8 locales and cannot reproduce the cp1252 crash natively). - Observed result: Subprocess reader threads no longer crash on UTF-8 output under cp1252 locale. - Not tested: All third-party tools that `headroom` shells out to; each was given `errors="replace"` as a safety net. ## Workaround for affected users (before fix is deployed) `PYTHONUTF8=1` (PowerShell: `$env:PYTHONUTF8=1; headroom ...`). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b0146c4ccd
|
fix(wrap): show the dashboard URL when the proxy is already running (#1313)
## Description
I was running `headroom wrap claude` and could not find the dashboard
URL anywhere. I eventually spotted it in the README demo gif. The reason
is that `_ensure_proxy` only echoes the URL on the path that starts or
restarts the proxy. Once a proxy is already up, the function prints
`Proxy already running on port {port}` and returns, with no URL. That
early-return path is the common case: every wrap after the first one
hits it, so in practice the dashboard URL is almost never shown.
This adds the same `Dashboard: http://127.0.0.1:{port}/dashboard` line
to the two already-running branches (the inline one and the
persistent-deployment one), so the URL shows up every time, not just on
a cold start.
Closes # N/A (no tracking issue)
## 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/cli/wrap.py`: echo the dashboard URL in both "proxy already
running" branches of `_ensure_proxy`, matching the line the
start/restart path already prints.
- `tests/test_cli/test_wrap_helpers.py`: new test that drives
`_ensure_proxy` down the already-running path and asserts the dashboard
URL is in the output.
## 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 --extra dev python -m pytest tests/test_cli/test_wrap_helpers.py -q
40 passed in 0.20s
$ uv run --extra dev ruff check headroom/cli/wrap.py tests/test_cli/test_wrap_helpers.py
All checks passed!
$ uv run --extra dev mypy headroom/cli/wrap.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: macOS, Python 3.13, this branch, `headroom wrap claude`
against an already-running proxy on port 8787.
- Exact command / steps: run `claude` (aliased to `headroom wrap
claude`) a second time, so the proxy is already up and `_ensure_proxy`
takes the early-return path.
- Observed result: before this change the output stopped at `Proxy
already running on port 8787` with no URL. After it, the next line is
`Dashboard: http://127.0.0.1:8787/dashboard`. The new unit test pins
this by mocking a healthy running proxy and asserting the URL is
printed.
- Not tested: I did not open the rendered dashboard in a browser as part
of this change. The fix is purely the printed line, which the unit test
covers.
## 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
- [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
I scoped this to the print line plus its test on purpose. ruff and mypy
are clean on the files I touched. I left the CHANGELOG checkbox
unchecked because this is a one-line user-facing string fix with no
behavior change beyond the extra output, but I am happy to add a
CHANGELOG entry if you would like one. The same for docs, I don't think
it's needed to have one about this
|