mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
## Description
`headroom wrap` starts the proxy via `_start_proxy`, which builds `cmd =
[sys.executable, "-m", "headroom.cli", "proxy", ...]`. A `python -m
<module>` invocation prepends the launch cwd to `sys.path`. So when
`wrap` is run from a directory that contains a `headroom/` folder (most
commonly a clone of this very repo, whose package lives at
`<repo-root>/headroom/`), that raw source tree shadows the installed
wheel in site-packages. The source tree has no compiled `headroom._core`
(the maturin extension only exists in the built wheel), so the proxy
dies with:
```text
Error: Proxy dependencies not installed. Run: pip install headroom-ai[proxy]
Details: No module named 'headroom._core'
```
`wrap` then falls back to launching the client unwrapped, and the "not
installed" hint is misleading: the dependency is installed, it is being
shadowed by cwd.
The fix sets `PYTHONSAFEPATH=1` in the proxy subprocess env. That
disables the cwd/script-dir prepend to `sys.path` (Python 3.11+, and a
harmless no-op on 3.10, so it never breaks the supported floor), which
is exactly what the issue reporter confirmed resolves it:
```console
$ PYTHONSAFEPATH=1 python -c "import headroom._core; print('OK')" # -> OK
```
The proxy is still launched as `-m headroom.cli`, so nothing about the
invocation changes except that it now always resolves the installed
package.
Fixes #2793
## 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` (`_start_proxy`): set
`proxy_env["PYTHONSAFEPATH"] = "1"` alongside the existing
`PYTHONIOENCODING`, with a comment explaining the cwd-shadow failure
mode.
- `tests/test_cli/test_wrap_claude_vertex_proxy_env.py`: added
`test_start_proxy_sets_pythonsafepath_to_avoid_cwd_shadow`, which drives
`_start_proxy` with a faked `subprocess.Popen` and asserts the
subprocess env carries `PYTHONSAFEPATH=1` while still launching `-m
headroom.cli proxy`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
# Fail-before (source fix stashed, new test kept):
tests/test_cli/test_wrap_claude_vertex_proxy_env.py::test_start_proxy_sets_pythonsafepath_to_avoid_cwd_shadow FAILED
assert captured["kwargs"]["env"]["PYTHONSAFEPATH"] == "1"
KeyError: 'PYTHONSAFEPATH'
# Pass-after (fix applied):
tests/test_cli/test_wrap_claude_vertex_proxy_env.py 18 passed
# Broader wrap suites:
tests/test_cli/test_wrap_claude_vertex_proxy_env.py tests/test_cli_proxy_env.py tests/test_cli/test_wrap_persistent.py
121 passed, 1 skipped
# uvx ruff@0.15.17 check -> All checks passed!
# uvx mypy@1.20.2 headroom/cli/wrap.py -> Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12.11, project venv, pytest 9.1.1,
ruff 0.15.17 and mypy 1.20.2 via uvx.
- Exact command / steps: confirmed `_start_proxy` builds
`[sys.executable, "-m", "headroom.cli", "proxy", ...]` and constructs
the subprocess env as `proxy_env`, reproduced the shadowing behaviour in
the reporter's terms (`python -m` prepends cwd; a cwd `headroom/`
without `_core` shadows the wheel), fail-before with `git stash push
headroom/cli/wrap.py` and `python -m pytest ... -k pythonsafepath` (the
env lacks the key), then pass-after with `git stash pop` and rerunning
the file (18 passed) plus the broader wrap suites (121 passed, 1
skipped).
- Observed result: the proxy subprocess env now carries
`PYTHONSAFEPATH=1`, which disables the cwd prepend, so `import
headroom._core` resolves the installed wheel instead of a shadowing
local `headroom/` source tree. The proxy command is unchanged otherwise.
- Not tested: an end-to-end `cd <repo-checkout> && headroom wrap claude`
against a real installed wheel (this environment is a source checkout
without a separate installed wheel to shadow). The behaviour is verified
through the spawn env the subprocess inherits, and `PYTHONSAFEPATH` is
the documented, reporter-confirmed switch for this exact failure mode.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md`: it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)
## Additional Notes
Scoped to the proxy launch, which is the reported, high-impact path (its
failure makes `wrap` fall back to unwrapped). `wrap` spawns one other
`python -m headroom.*` subprocess (the memory-sync helper in the Claude
flow) that shares the same root cause; it is a lower-severity,
unreported path and is left for a follow-up rather than widening this
diff. The misleading "pip install headroom-ai[proxy]" message the
reporter also flagged is a separate error-text concern and is likewise
out of scope here.
|
||
|---|---|---|
| .. | ||
| conftest.py | ||
| test_copilot_auth.py | ||
| test_init_cli.py | ||
| test_init_enable_tool_search.py | ||
| test_install_cli.py | ||
| test_main_help_version.py | ||
| test_mcp.py | ||
| test_mcp_status.py | ||
| test_recover_codex.py | ||
| test_serena_disable.py | ||
| test_serena_migrate.py | ||
| test_subprocess_utf8_encoding.py | ||
| test_unwrap_claude.py | ||
| test_wrap_aider.py | ||
| test_wrap_bridge.py | ||
| test_wrap_claude_base_url.py | ||
| test_wrap_claude_finally_unbound.py | ||
| test_wrap_claude_vertex_proxy_env.py | ||
| test_wrap_codex.py | ||
| test_wrap_copilot.py | ||
| test_wrap_dead_marker_selfheal.py | ||
| test_wrap_encoding.py | ||
| test_wrap_goose.py | ||
| test_wrap_grok.py | ||
| test_wrap_helpers.py | ||
| test_wrap_kimi.py | ||
| test_wrap_omp.py | ||
| test_wrap_openclaude.py | ||
| test_wrap_openclaw.py | ||
| test_wrap_opencode.py | ||
| test_wrap_openhands.py | ||
| test_wrap_persistent.py | ||
| test_wrap_proxy_detach.py | ||
| test_wrap_serena_boost.py | ||
| test_wrap_stale_marker.py | ||
| test_wrap_vibe.py | ||
| test_wrap_vscode.py | ||
| test_wrap_vscode_claude.py | ||
| test_wrap_zcode.py | ||