2026-04-11 13:47:05 -05:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
fix(install): only validate requested targets on the manual path (#1659)
## Description
`resolve_targets()` runs the provider-scope "unsupported targets"
validation
**before** it dispatches on `provider_mode`:
```python
if scope == ConfigScope.PROVIDER.value:
unsupported = [t for t in requested if t and t not in valid]
if unsupported:
raise click.ClickException("Provider scope supports only ...; unsupported targets: ...")
if provider_mode == ALL: return [t.value for t in valid_targets] # ignores `requested`
if provider_mode == AUTO: ... # ignores `requested`
# manual: filters `requested`
```
But `all` and `auto` never consult the requested target list — only the
manual
path does. So an unsupported entry that those modes would simply ignore
instead
makes the call raise. Concretely:
```
headroom install apply --scope provider --providers all --target cursor
→ ClickException: Provider scope supports only claude, codex, openclaw, and
opencode; unsupported targets: cursor
```
...when it should just return the full provider set. (`--target` is a
click
`Choice` that accepts all 7 targets regardless of scope/mode, so this is
reachable from the CLI.) The user-scope equivalent,
`resolve_targets("all",
["cursor"])`, happily ignores `cursor` and returns all user targets — so
provider
scope is inconsistent with user scope for identical, ignored input.
Closes: no issue filed — found while auditing `install` target
resolution.
## Fix
Move the provider-scope validation so it runs only on the manual path
(the only
mode that reads `requested`). `all` returns the full provider set and
`auto`
returns detected/default targets, neither raising on an ignored
requested list.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/install/planner.py`: move the provider-scope "unsupported
targets" check below the `all`/`auto` dispatch, into the manual path.
- `tests/test_install/test_planner.py`: regression tests — `all` and
`auto` ignore an unsupported requested target under provider scope; the
manual path still rejects it.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.
## Testing
- [x] New tests added for the fixed behavior
(`tests/test_install/test_planner.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (see Real Behavior Proof for the
local-OOM reason).
```text
$ uv run ruff check headroom/install/planner.py tests/test_install/test_planner.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12.11, headroom built from this
branch. Importing `headroom` loads the torch/transformers stack and a
full `pytest` gets OOM-killed on this box, so I verified the control
flow with a dependency-free script (only stdlib) and left the full
pytest to CI.
- Exact command / steps: replicated `resolve_targets`'s control flow
(with the fix, `click.ClickException` stubbed, and stand-in target
lists) in a standalone script — no `headroom` import — and exercised
`all`/`auto`/`manual` under provider scope with an unsupported `cursor`
entry, plus the user-scope and manual-dedup regressions.
- Observed result: `all`/`auto` return the provider targets without
raising, `manual` still raises on the unsupported target, and the
pre-existing manual-dedup and user-scope behavior is unchanged:
```text
OK: all + [cursor] + provider -> provider set (no raise)
OK: auto + [cursor] + provider -> [claude, codex] (no raise)
OK: manual + [cursor] + provider -> raises (preserved)
OK: manual dedupe/filter + user-scope all unchanged
PLANNER LOGIC VERIFIED
```
- Not tested: a full `headroom install apply` end-to-end (would require
the heavy stack and a real deployment); the change is confined to pure
target-list resolution. Full local `pytest` deferred to CI (OOM, per
above).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- No new dependencies; a control-flow move plus tests.
---------
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-13 03:00:31 +05:30
|
|
|
import click
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-04-11 18:24:15 -05:00
|
|
|
from headroom.install.models import ConfigScope, InstallPreset, ProviderSelectionMode, ToolTarget
|
fix(install): only validate requested targets on the manual path (#1659)
## Description
`resolve_targets()` runs the provider-scope "unsupported targets"
validation
**before** it dispatches on `provider_mode`:
```python
if scope == ConfigScope.PROVIDER.value:
unsupported = [t for t in requested if t and t not in valid]
if unsupported:
raise click.ClickException("Provider scope supports only ...; unsupported targets: ...")
if provider_mode == ALL: return [t.value for t in valid_targets] # ignores `requested`
if provider_mode == AUTO: ... # ignores `requested`
# manual: filters `requested`
```
But `all` and `auto` never consult the requested target list — only the
manual
path does. So an unsupported entry that those modes would simply ignore
instead
makes the call raise. Concretely:
```
headroom install apply --scope provider --providers all --target cursor
→ ClickException: Provider scope supports only claude, codex, openclaw, and
opencode; unsupported targets: cursor
```
...when it should just return the full provider set. (`--target` is a
click
`Choice` that accepts all 7 targets regardless of scope/mode, so this is
reachable from the CLI.) The user-scope equivalent,
`resolve_targets("all",
["cursor"])`, happily ignores `cursor` and returns all user targets — so
provider
scope is inconsistent with user scope for identical, ignored input.
Closes: no issue filed — found while auditing `install` target
resolution.
## Fix
Move the provider-scope validation so it runs only on the manual path
(the only
mode that reads `requested`). `all` returns the full provider set and
`auto`
returns detected/default targets, neither raising on an ignored
requested list.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/install/planner.py`: move the provider-scope "unsupported
targets" check below the `all`/`auto` dispatch, into the manual path.
- `tests/test_install/test_planner.py`: regression tests — `all` and
`auto` ignore an unsupported requested target under provider scope; the
manual path still rejects it.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.
## Testing
- [x] New tests added for the fixed behavior
(`tests/test_install/test_planner.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (see Real Behavior Proof for the
local-OOM reason).
```text
$ uv run ruff check headroom/install/planner.py tests/test_install/test_planner.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12.11, headroom built from this
branch. Importing `headroom` loads the torch/transformers stack and a
full `pytest` gets OOM-killed on this box, so I verified the control
flow with a dependency-free script (only stdlib) and left the full
pytest to CI.
- Exact command / steps: replicated `resolve_targets`'s control flow
(with the fix, `click.ClickException` stubbed, and stand-in target
lists) in a standalone script — no `headroom` import — and exercised
`all`/`auto`/`manual` under provider scope with an unsupported `cursor`
entry, plus the user-scope and manual-dedup regressions.
- Observed result: `all`/`auto` return the provider targets without
raising, `manual` still raises on the unsupported target, and the
pre-existing manual-dedup and user-scope behavior is unchanged:
```text
OK: all + [cursor] + provider -> provider set (no raise)
OK: auto + [cursor] + provider -> [claude, codex] (no raise)
OK: manual + [cursor] + provider -> raises (preserved)
OK: manual dedupe/filter + user-scope all unchanged
PLANNER LOGIC VERIFIED
```
- Not tested: a full `headroom install apply` end-to-end (would require
the heavy stack and a real deployment); the change is confined to pure
target-list resolution. Full local `pytest` deferred to CI (OOM, per
above).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- No new dependencies; a control-flow move plus tests.
---------
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-13 03:00:31 +05:30
|
|
|
from headroom.install.planner import PROVIDER_SCOPE_TARGETS, build_manifest, resolve_targets
|
2026-04-11 13:47:05 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_targets_auto_falls_back_when_detection_empty(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setattr("headroom.install.planner.detect_targets", lambda: [])
|
|
|
|
|
|
|
|
|
|
targets = resolve_targets(ProviderSelectionMode.AUTO.value, [])
|
|
|
|
|
|
|
|
|
|
assert targets == [
|
|
|
|
|
ToolTarget.CLAUDE.value,
|
|
|
|
|
ToolTarget.CODEX.value,
|
|
|
|
|
ToolTarget.COPILOT.value,
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_for_persistent_docker_sets_expected_defaults() -> None:
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
profile="default",
|
|
|
|
|
preset=InstallPreset.PERSISTENT_DOCKER.value,
|
|
|
|
|
runtime_kind="docker",
|
|
|
|
|
scope="user",
|
|
|
|
|
provider_mode="manual",
|
|
|
|
|
targets=["claude", "copilot"],
|
|
|
|
|
port=8787,
|
|
|
|
|
backend="anthropic",
|
|
|
|
|
anyllm_provider=None,
|
|
|
|
|
region=None,
|
|
|
|
|
proxy_mode="token",
|
|
|
|
|
memory_enabled=True,
|
|
|
|
|
telemetry_enabled=False,
|
fix(install): default docker image to headroomlabs-ai GHCR registry (#1867) (#2039)
## Description
The GitHub repository was transferred from `chopratejas/headroom` to
`headroomlabs-ai/headroom`. GitHub 301-redirects transferred repos for
web and git operations, but **GitHub Container Registry (GHCR) does
not** — the old package `ghcr.io/chopratejas/headroom` is now orphaned
and frozen (its `latest` tag stopped advancing at `0.27.0`), while CI
publishes new images to `ghcr.io/headroomlabs-ai/headroom` (the workflow
derives the path from `${{ github.repository }}`).
Headroom's install tooling still defaulted to the dead path, so
`headroom install apply --preset persistent-docker`, `headroom init`,
and the standalone install scripts all pulled a stale `0.27.0` image
instead of the current release.
This changes every docker-image **default** to
`ghcr.io/headroomlabs-ai/headroom:latest`.
Closes #1867
## 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/install/models.py` — `InstallManifest.image` default.
- `headroom/cli/install.py` — `--image` Click option default.
- `headroom/cli/init.py` — two `InstallManifest(...)` image args.
- `scripts/install.sh` / `scripts/install.ps1` — `IMAGE_DEFAULT` /
`$ImageDefault` plus the `--image` help-text default.
- `docker/docker-compose.native.yml` — image default in both services.
- `tests/test_install/test_planner.py` (4) and
`tests/test_install/test_runtime.py` (5) — updated the assertions that
pinned the old image (including `assert "<image>" in command`), so they
now verify the corrected registry threads through the planner and docker
runtime command.
Scope note: `github.com/chopratejas/...` links and the plugin
marketplace slug are intentionally **not** changed — GitHub redirects
those, so they still work. Only the genuinely-dead GHCR image references
are touched. No new dependency, no new abstraction.
## 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_install/ -q
99 passed, 1 skipped in 48.34s
$ uv run ruff check headroom/install/models.py headroom/cli/install.py headroom/cli/init.py
All checks passed!
$ grep -rn "ghcr.io/chopratejas/headroom" headroom/ scripts/ docker/ tests/
# (no source matches — every default now points at headroomlabs-ai)
```
The install-test assertions pinned the old image, so they fail against
the old defaults and pass after the fix — they are the regression guard.
## Real Behavior Proof
- **Environment:** Windows 11, Python 3.13.5, headroom installed from
this branch (editable, via `uv`).
- **Exact command / steps:** The dead-registry claim is verifiable at
the registry level, independent of a release:
```text
docker pull ghcr.io/chopratejas/headroom:latest # old default → 0.27.0
(frozen / orphaned)
docker pull ghcr.io/headroomlabs-ai/headroom:latest # new default →
current release
```
And the install pipeline now emits the correct image (covered by
`tests/test_install/test_runtime.py`, which asserts the resolved docker
command contains `ghcr.io/headroomlabs-ai/headroom:latest`).
- **Observed result:** `grep` confirms no `ghcr.io/chopratejas/headroom`
default remains in code, scripts, compose, or tests;
`tests/test_install/` is green (99 passed) with the corrected image
asserted end-to-end through planner → runtime command.
- **Not tested:** A live `docker pull` of both tags on this specific
machine (no local Docker daemon guaranteed) — the registry difference is
reproducible by anyone running the two `docker pull` commands above; and
an end-to-end `headroom install apply --preset persistent-docker`
against a real Docker host.
## 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
## Additional Notes
- Documentation checklist item is N/A — no user-facing docs surface
beyond the CHANGELOG entry.
- `mypy` left unchecked — not run as part of this verification; the
change is a string-default swap with no type-level surface.
- The `--image` help text and `docker-compose.native.yml` were included
so every user-facing default is consistent; only the dead GHCR image
string was changed.
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 23:31:28 +05:30
|
|
|
image="ghcr.io/headroomlabs-ai/headroom:latest",
|
2026-04-11 13:47:05 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert manifest.supervisor_kind == "none"
|
|
|
|
|
assert manifest.runtime_kind == "docker"
|
|
|
|
|
assert manifest.health_url == "http://127.0.0.1:8787/readyz"
|
|
|
|
|
assert manifest.base_env["HEADROOM_PORT"] == "8787"
|
|
|
|
|
assert manifest.base_env["HEADROOM_TELEMETRY"] == "off"
|
fix(telemetry): switch anonymous telemetry to opt-in (off by default) (#1223)
## Description
Anonymous usage telemetry was **on by default** (opt-out). This flips it
to **opt-in**: nothing is collected or shipped unless the user
explicitly turns it on. Small change, but it makes "no data leaves the
proxy by default" the actual default rather than something users have to
discover and disable.
Closes # <!-- N/A: no tracking issue -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality) — adds
`--telemetry` opt-in flag
- [x] Breaking change (fix or feature that would cause existing
functionality to change) — telemetry no longer runs unless opted in
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `is_telemetry_enabled()` is now **fail-closed**: only explicit
on-values (`on`/`true`/`1`/`yes`/`enable`/`enabled`) enable telemetry;
unset, empty, or unrecognized values stay disabled. This single
predicate gates both the Supabase beacon and the local `/v1/telemetry`
collector.
- Added `--telemetry` opt-in flag to `headroom proxy` and `headroom
install apply`; kept `--no-telemetry` and `HEADROOM_TELEMETRY=off` for
back-compat. If both are passed, opt-out wins.
- Install manifests now write `HEADROOM_TELEMETRY` explicitly
(`on`/`off`) plus the matching flag, so generated systemd/docker/launchd
deployments are unambiguous and don't rely on the runtime default.
- Startup banner and proxy log show `DISABLED` by default and surface
how to opt in.
- Updated tests for opt-in defaults; added coverage for the default-off
banner, the `--telemetry` flag, and the explicit-on manifest path.
- Updated docs
(proxy/configuration/installation/benchmarks/community-savings mdx, spec
011/015, wiki proxy/cli/benchmarks/metrics) and `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`) — targeted telemetry + install-planner
suites
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_telemetry_warning.py tests/test_telemetry.py tests/test_install/test_planner.py -q
81 passed
$ ruff check <changed source + test files>
All checks passed!
$ mypy headroom/telemetry/beacon.py headroom/cli/proxy.py headroom/cli/install.py \
headroom/install/planner.py headroom/proxy/server.py
Success: no issues found in 5 source files
```
## Real Behavior Proof
- **Environment:** macOS (darwin 25.4.0), project `.venv`, `headroom`
CLI.
- **Exact command / steps:**
```
$ python -c "import os; from headroom.telemetry.beacon import
is_telemetry_enabled; \
os.environ.pop('HEADROOM_TELEMETRY', None); print('unset ->',
is_telemetry_enabled()); \
[ (os.environ.__setitem__('HEADROOM_TELEMETRY', v), print(repr(v), '->',
is_telemetry_enabled())) \
for v in ['on','TRUE','1','yes','off','0','garbage',''] ]"
$ headroom proxy --help | grep -i telemetry
$ headroom install apply --help | grep -i telemetry
```
- **Observed result:**
```
unset -> False # off by default
'on' -> True 'TRUE' -> True '1' -> True 'yes' -> True
'off' -> False '0' -> False
'garbage' -> False '' -> False # fail-closed
proxy: --telemetry Opt in to anonymous usage telemetry — off by default
(env: HEADROOM_TELEMETRY=on)
--no-telemetry Force anonymous usage telemetry off (already the default;
env: HEADROOM_TELEMETRY=off)
install: --telemetry Opt in to anonymous telemetry in the runtime (off
by default).
--no-telemetry Force anonymous telemetry off in the runtime (already the
default).
```
- **Not tested:** live Supabase beacon network round-trip (no opt-in
network call was made); full `pytest` suite was not run — only the
telemetry + install-planner targeted suites.
## 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
- "Breaking change" is checked because the default behavior changes
(telemetry stops running unless opted in). It is **not** an API break —
`--no-telemetry` and `HEADROOM_TELEMETRY=off` still work, so existing
opt-out configs are unaffected.
- No tracking issue, so `Closes #` is left N/A.
2026-06-20 21:26:04 -07:00
|
|
|
assert "--no-telemetry" in manifest.proxy_args
|
2026-04-11 13:47:05 -05:00
|
|
|
assert manifest.tool_envs["claude"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
|
|
|
|
|
assert manifest.tool_envs["copilot"]["COPILOT_PROVIDER_TYPE"] == "anthropic"
|
|
|
|
|
assert "--memory" in manifest.proxy_args
|
fix(install): stop baking the host memory DB path into a container deployment (#2845)
## Description
`headroom deploy --memory` on the `persistent-docker` preset can never
become ready. The planner resolves the memory DB path against the
**host** home and appends it verbatim to `proxy_args`:
```python
# headroom/install/planner.py
proxy_args.extend(["--memory", "--memory-db-path", str(_paths.memory_db_path())])
# -> --memory-db-path /home/<user>/.headroom/memory.db
```
The docker runtime passes everything after the leading `--host` pair
through unchanged, and the container's `HOME` is `/tmp/headroom-home`
with the host's `~/.headroom` bind-mounted at
`/tmp/headroom-home/.headroom`. The host path
`/home/<user>/.headroom/memory.db` does not exist inside the container,
so SQLite cannot open the DB:
```text
Memory: backend initialization failed (startup continues): unable to open database file
```
`/health` then reports `memory.ready = false`, `/readyz` stays 503 for
the full `wait_ready` window, and `_start_deployment` times out and
rolls back, so the failure presents as "did not become ready" rather
than a path bug. The same applies on macOS with `/Users/<user>/...`.
The fix omits `--memory-db-path` for a container (docker) runtime. When
the flag is absent the proxy resolves the DB under its own cwd
(`.headroom/memory.db`), and the container's workdir is
`/tmp/headroom-home` (the bind mount), so the DB lands in exactly the
same host file the explicit path intended. The host (python) runtime
still passes the resolved host path, which is correct there.
Fixes #2803
## 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/install/planner.py` (`build_manifest`): append `--memory`
always, but add `--memory-db-path <host path>` only when `runtime_kind
!= RuntimeKind.DOCKER.value`. Imported `RuntimeKind` from `.models`.
- `tests/test_install/test_planner.py`: extended
`test_build_manifest_for_persistent_docker_sets_expected_defaults` to
assert `--memory-db-path` is absent for the docker runtime, and added
`test_build_manifest_python_runtime_keeps_explicit_memory_db_path`
asserting it is still present for the python runtime.
## 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, updated tests kept):
tests/test_install/test_planner.py::test_build_manifest_for_persistent_docker_sets_expected_defaults FAILED
assert "--memory-db-path" not in manifest.proxy_args
AssertionError: assert '--memory-db-path' not in ['--host', '127.0.0.1', ...]
# Pass-after (fix applied):
tests/test_install/test_planner.py 19 passed
# Broader install suites:
tests/test_install/ 141 passed, 1 skipped, 2 unrelated pre-existing/flaky failures
# - test_native_installers.py::test_powershell_native_installer_supports_persistent_docker_lifecycle
# runs scripts/install.ps1 and fails identically on clean main (environment-specific).
# - test_runtime.py::test_runtime_status_survives_winerror87_systemerror passes in isolation
# and in its own file; it only failed under cross-file ordering in the broad run, and is
# untouched by this diff (planner.py only).
# uvx ruff@0.15.17 check -> All checks passed!
# uvx mypy@1.20.2 headroom/install/planner.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: traced the path from `planner.py`
(`--memory-db-path str(_paths.memory_db_path())`, host home) through
`runtime.py` (`build_runtime_command` passes
`proxy_args[_PROXY_ARGS_HOST_PAIR_LEN:]` through, container HOME
`/tmp/headroom-home`, `~/.headroom` bind-mounted) and confirmed via
`server.py` that an empty `memory_db_path` resolves to
`Path.cwd()/.headroom/memory.db` (the container workdir, hence the
mount). Fail-before with `git stash push headroom/install/planner.py`
and `python -m pytest tests/test_install/test_planner.py -k
persistent_docker` (host path present in proxy_args), pass-after with
`git stash pop` and rerunning (19 passed).
- Observed result: for the docker runtime, `manifest.proxy_args` now
carries `--memory` without `--memory-db-path`, so the container resolves
the DB to `/tmp/headroom-home/.headroom/memory.db` (the bind mount to
host `~/.headroom/memory.db`) and can open it, instead of receiving a
nonexistent host path. The python runtime still carries the explicit
host path.
- Not tested: a live `headroom deploy --memory` against a running Docker
daemon (no container runtime in this environment). The manifest
construction is verified directly, and the container-side resolution it
relies on is existing server behavior (`empty memory_db_path ->
cwd/.headroom/memory.db`) confirmed by reading `server.py`.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md`: it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)
## Additional Notes
The DB persistence location is unchanged: both the old host path and the
new container-cwd resolution point at the host's `~/.headroom/memory.db`
(directly on the host, or through the bind mount inside the container),
so existing memory DBs are picked up either way. This is the memory-path
half of the persistent-docker issues; the separate rootless-Podman
`--user` bind-mount problem (#2804) is left for its own fix.
2026-08-08 11:45:41 +05:30
|
|
|
# A container runtime must NOT carry the host memory DB path: it does not
|
|
|
|
|
# exist inside the container and would keep /readyz at 503 (#2803). The proxy
|
|
|
|
|
# resolves the DB under its own cwd, which is the bind-mounted ~/.headroom.
|
|
|
|
|
assert "--memory-db-path" not in manifest.proxy_args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_python_runtime_keeps_explicit_memory_db_path() -> None:
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
profile="default",
|
|
|
|
|
preset=InstallPreset.PERSISTENT_SERVICE.value,
|
|
|
|
|
runtime_kind="python",
|
|
|
|
|
scope="user",
|
|
|
|
|
provider_mode="manual",
|
|
|
|
|
targets=["claude"],
|
|
|
|
|
port=8787,
|
|
|
|
|
backend="anthropic",
|
|
|
|
|
anyllm_provider=None,
|
|
|
|
|
region=None,
|
|
|
|
|
proxy_mode="token",
|
|
|
|
|
memory_enabled=True,
|
|
|
|
|
telemetry_enabled=False,
|
|
|
|
|
image="ghcr.io/headroomlabs-ai/headroom:latest",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# On the host the resolved path is correct, so it is still passed explicitly.
|
|
|
|
|
assert "--memory" in manifest.proxy_args
|
|
|
|
|
assert "--memory-db-path" in manifest.proxy_args
|
2026-04-11 18:24:15 -05:00
|
|
|
|
|
|
|
|
|
fix(install): consolidate Windows fallback and cleanup safety (#2980)
## Description
Consolidates two fully reviewed installation-safety fixes whose original
PRs can no longer merge under current branch protection: Windows
persistent-service deployments need a supported Task Scheduler fallback,
and legacy context-tool cleanup must never delete user-owned
RTK/lean-ctx artifacts.
Closes #2552
Closes #2817
Supersedes #2600 and #2828 while preserving their authors' commits and
review-driven corrections.
## 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
- Convert Windows `persistent-service` plans to the supported
`persistent-task` supervisor and make the fallback explicit in CLI
output.
- Restrict context-tool cleanup to artifacts proven to live under
Headroom's managed directory.
- Recognize wrapped, relative, and platform-specific managed commands
without accepting prefixed/path-boundary lookalikes.
- Scope cleanup completion state correctly across projects and alternate
agent homes.
- Stamp cleanup complete only after all managed remnants are settled.
- Preserve the original focused regression suites and behavior-proof
artifact.
## 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 -q tests/test_install/test_planner.py tests/test_install/test_supervisors.py tests/test_cli/test_install_cli.py tests/test_context_tool_cleanup.py tests/test_cli/test_unwrap_claude.py
135 passed in 0.45s
$ uv run ruff check <changed Python and test files>
All checks passed!
$ uv run ruff format --check <changed Python and test files>
8 files already formatted
```
## Real Behavior Proof
- Environment: macOS arm64 for consolidated current-main validation; the
Windows fallback source PR was independently validated on Windows and
includes its captured verification artifact.
- Exact command / steps: run the planner, supervisor, install CLI,
cleanup provenance, and unwrap suites on the rebased combined branch.
- Observed result: 135/135 focused tests pass. Windows service requests
resolve to `persistent-task`; cleanup rejects user-owned and path-prefix
lookalikes while removing managed artifacts.
- Not tested: a fresh privileged Windows host deployment in this local
pass; #2600's accepted review contains the Windows-specific proof.
## Runtime Rollout Safety
- Rollout-managed feature(s): Install supervisor selection and one-time
legacy cleanup.
- Minimum rollout channel: Stable/default; both prevent currently
destructive or nonfunctional install paths.
- Stable/default behavior changed: Windows service requests use Task
Scheduler; cleanup requires managed provenance.
- Kill switch / disable path: Select `persistent-task` explicitly;
cleanup remains bounded by its completion stamp and provenance checks.
- Unsafe override required: No.
- Qualification impact: Windows native install and wrap/unwrap cleanup
suites.
- Rollback path: Revert this PR, restoring the two pre-fix behaviors.
## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)
## Screenshots (if applicable)
The Windows verification artifact from #2600 is retained at
`.github/pr-images/issue-2552-windows-fallback-verification.png`.
## Additional Notes
This is intentionally an installation-safety batch rather than two
replacement PRs. Original commit authorship is preserved, and the
combined diff was applied cleanly to current `main` after #2832 and
#1628 landed.
---------
Co-authored-by: Inference1 <68734681+Inference1@users.noreply.github.com>
Co-authored-by: Dennis Alexis Valin Dittrich <dd+github@dr-dittrich.de>
2026-08-13 15:05:45 -05:00
|
|
|
def test_build_manifest_falls_back_from_windows_service_to_task(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setattr("headroom.install.planner.sys.platform", "win32")
|
|
|
|
|
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
profile="default",
|
|
|
|
|
preset=InstallPreset.PERSISTENT_SERVICE.value,
|
|
|
|
|
runtime_kind="python",
|
|
|
|
|
scope="user",
|
|
|
|
|
provider_mode="manual",
|
|
|
|
|
targets=["claude"],
|
|
|
|
|
port=8787,
|
|
|
|
|
backend="anthropic",
|
|
|
|
|
anyllm_provider=None,
|
|
|
|
|
region=None,
|
|
|
|
|
proxy_mode="token",
|
|
|
|
|
memory_enabled=False,
|
|
|
|
|
telemetry_enabled=False,
|
|
|
|
|
image="ghcr.io/headroomlabs-ai/headroom:latest",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert manifest.preset == InstallPreset.PERSISTENT_TASK.value
|
|
|
|
|
assert manifest.supervisor_kind == "task"
|
|
|
|
|
|
|
|
|
|
|
2026-04-21 22:35:24 -05:00
|
|
|
def test_build_manifest_uses_provider_slice_env_builders_for_all_supported_targets() -> None:
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
profile="default",
|
|
|
|
|
preset=InstallPreset.PERSISTENT_SERVICE.value,
|
|
|
|
|
runtime_kind="python",
|
|
|
|
|
scope="user",
|
|
|
|
|
provider_mode="manual",
|
|
|
|
|
targets=["claude", "copilot", "codex", "aider", "cursor"],
|
|
|
|
|
port=9999,
|
|
|
|
|
backend="anyllm",
|
|
|
|
|
anyllm_provider="groq",
|
|
|
|
|
region=None,
|
|
|
|
|
proxy_mode="token",
|
|
|
|
|
memory_enabled=False,
|
|
|
|
|
telemetry_enabled=True,
|
fix(install): default docker image to headroomlabs-ai GHCR registry (#1867) (#2039)
## Description
The GitHub repository was transferred from `chopratejas/headroom` to
`headroomlabs-ai/headroom`. GitHub 301-redirects transferred repos for
web and git operations, but **GitHub Container Registry (GHCR) does
not** — the old package `ghcr.io/chopratejas/headroom` is now orphaned
and frozen (its `latest` tag stopped advancing at `0.27.0`), while CI
publishes new images to `ghcr.io/headroomlabs-ai/headroom` (the workflow
derives the path from `${{ github.repository }}`).
Headroom's install tooling still defaulted to the dead path, so
`headroom install apply --preset persistent-docker`, `headroom init`,
and the standalone install scripts all pulled a stale `0.27.0` image
instead of the current release.
This changes every docker-image **default** to
`ghcr.io/headroomlabs-ai/headroom:latest`.
Closes #1867
## 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/install/models.py` — `InstallManifest.image` default.
- `headroom/cli/install.py` — `--image` Click option default.
- `headroom/cli/init.py` — two `InstallManifest(...)` image args.
- `scripts/install.sh` / `scripts/install.ps1` — `IMAGE_DEFAULT` /
`$ImageDefault` plus the `--image` help-text default.
- `docker/docker-compose.native.yml` — image default in both services.
- `tests/test_install/test_planner.py` (4) and
`tests/test_install/test_runtime.py` (5) — updated the assertions that
pinned the old image (including `assert "<image>" in command`), so they
now verify the corrected registry threads through the planner and docker
runtime command.
Scope note: `github.com/chopratejas/...` links and the plugin
marketplace slug are intentionally **not** changed — GitHub redirects
those, so they still work. Only the genuinely-dead GHCR image references
are touched. No new dependency, no new abstraction.
## 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_install/ -q
99 passed, 1 skipped in 48.34s
$ uv run ruff check headroom/install/models.py headroom/cli/install.py headroom/cli/init.py
All checks passed!
$ grep -rn "ghcr.io/chopratejas/headroom" headroom/ scripts/ docker/ tests/
# (no source matches — every default now points at headroomlabs-ai)
```
The install-test assertions pinned the old image, so they fail against
the old defaults and pass after the fix — they are the regression guard.
## Real Behavior Proof
- **Environment:** Windows 11, Python 3.13.5, headroom installed from
this branch (editable, via `uv`).
- **Exact command / steps:** The dead-registry claim is verifiable at
the registry level, independent of a release:
```text
docker pull ghcr.io/chopratejas/headroom:latest # old default → 0.27.0
(frozen / orphaned)
docker pull ghcr.io/headroomlabs-ai/headroom:latest # new default →
current release
```
And the install pipeline now emits the correct image (covered by
`tests/test_install/test_runtime.py`, which asserts the resolved docker
command contains `ghcr.io/headroomlabs-ai/headroom:latest`).
- **Observed result:** `grep` confirms no `ghcr.io/chopratejas/headroom`
default remains in code, scripts, compose, or tests;
`tests/test_install/` is green (99 passed) with the corrected image
asserted end-to-end through planner → runtime command.
- **Not tested:** A live `docker pull` of both tags on this specific
machine (no local Docker daemon guaranteed) — the registry difference is
reproducible by anyone running the two `docker pull` commands above; and
an end-to-end `headroom install apply --preset persistent-docker`
against a real Docker host.
## 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
## Additional Notes
- Documentation checklist item is N/A — no user-facing docs surface
beyond the CHANGELOG entry.
- `mypy` left unchecked — not run as part of this verification; the
change is a string-default swap with no type-level surface.
- The `--image` help text and `docker-compose.native.yml` were included
so every user-facing default is consistent; only the dead GHCR image
string was changed.
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 23:31:28 +05:30
|
|
|
image="ghcr.io/headroomlabs-ai/headroom:latest",
|
2026-04-21 22:35:24 -05:00
|
|
|
)
|
|
|
|
|
|
fix(telemetry): switch anonymous telemetry to opt-in (off by default) (#1223)
## Description
Anonymous usage telemetry was **on by default** (opt-out). This flips it
to **opt-in**: nothing is collected or shipped unless the user
explicitly turns it on. Small change, but it makes "no data leaves the
proxy by default" the actual default rather than something users have to
discover and disable.
Closes # <!-- N/A: no tracking issue -->
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality) — adds
`--telemetry` opt-in flag
- [x] Breaking change (fix or feature that would cause existing
functionality to change) — telemetry no longer runs unless opted in
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `is_telemetry_enabled()` is now **fail-closed**: only explicit
on-values (`on`/`true`/`1`/`yes`/`enable`/`enabled`) enable telemetry;
unset, empty, or unrecognized values stay disabled. This single
predicate gates both the Supabase beacon and the local `/v1/telemetry`
collector.
- Added `--telemetry` opt-in flag to `headroom proxy` and `headroom
install apply`; kept `--no-telemetry` and `HEADROOM_TELEMETRY=off` for
back-compat. If both are passed, opt-out wins.
- Install manifests now write `HEADROOM_TELEMETRY` explicitly
(`on`/`off`) plus the matching flag, so generated systemd/docker/launchd
deployments are unambiguous and don't rely on the runtime default.
- Startup banner and proxy log show `DISABLED` by default and surface
how to opt in.
- Updated tests for opt-in defaults; added coverage for the default-off
banner, the `--telemetry` flag, and the explicit-on manifest path.
- Updated docs
(proxy/configuration/installation/benchmarks/community-savings mdx, spec
011/015, wiki proxy/cli/benchmarks/metrics) and `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`) — targeted telemetry + install-planner
suites
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_telemetry_warning.py tests/test_telemetry.py tests/test_install/test_planner.py -q
81 passed
$ ruff check <changed source + test files>
All checks passed!
$ mypy headroom/telemetry/beacon.py headroom/cli/proxy.py headroom/cli/install.py \
headroom/install/planner.py headroom/proxy/server.py
Success: no issues found in 5 source files
```
## Real Behavior Proof
- **Environment:** macOS (darwin 25.4.0), project `.venv`, `headroom`
CLI.
- **Exact command / steps:**
```
$ python -c "import os; from headroom.telemetry.beacon import
is_telemetry_enabled; \
os.environ.pop('HEADROOM_TELEMETRY', None); print('unset ->',
is_telemetry_enabled()); \
[ (os.environ.__setitem__('HEADROOM_TELEMETRY', v), print(repr(v), '->',
is_telemetry_enabled())) \
for v in ['on','TRUE','1','yes','off','0','garbage',''] ]"
$ headroom proxy --help | grep -i telemetry
$ headroom install apply --help | grep -i telemetry
```
- **Observed result:**
```
unset -> False # off by default
'on' -> True 'TRUE' -> True '1' -> True 'yes' -> True
'off' -> False '0' -> False
'garbage' -> False '' -> False # fail-closed
proxy: --telemetry Opt in to anonymous usage telemetry — off by default
(env: HEADROOM_TELEMETRY=on)
--no-telemetry Force anonymous usage telemetry off (already the default;
env: HEADROOM_TELEMETRY=off)
install: --telemetry Opt in to anonymous telemetry in the runtime (off
by default).
--no-telemetry Force anonymous telemetry off in the runtime (already the
default).
```
- **Not tested:** live Supabase beacon network round-trip (no opt-in
network call was made); full `pytest` suite was not run — only the
telemetry + install-planner targeted suites.
## 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
- "Breaking change" is checked because the default behavior changes
(telemetry stops running unless opted in). It is **not** an API break —
`--no-telemetry` and `HEADROOM_TELEMETRY=off` still work, so existing
opt-out configs are unaffected.
- No tracking issue, so `Closes #` is left N/A.
2026-06-20 21:26:04 -07:00
|
|
|
# telemetry_enabled=True must write the explicit opt-in value + flag.
|
|
|
|
|
assert manifest.base_env["HEADROOM_TELEMETRY"] == "on"
|
|
|
|
|
assert "--telemetry" in manifest.proxy_args
|
2026-04-21 22:35:24 -05:00
|
|
|
assert manifest.tool_envs["claude"]["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:9999"
|
|
|
|
|
assert manifest.tool_envs["codex"]["OPENAI_BASE_URL"] == "http://127.0.0.1:9999/v1"
|
|
|
|
|
assert manifest.tool_envs["aider"] == {
|
|
|
|
|
"OPENAI_API_BASE": "http://127.0.0.1:9999/v1",
|
|
|
|
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9999",
|
|
|
|
|
}
|
|
|
|
|
assert manifest.tool_envs["cursor"] == {
|
|
|
|
|
"OPENAI_BASE_URL": "http://127.0.0.1:9999/v1",
|
|
|
|
|
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9999",
|
|
|
|
|
}
|
|
|
|
|
assert manifest.tool_envs["copilot"] == {
|
|
|
|
|
"COPILOT_PROVIDER_TYPE": "openai",
|
|
|
|
|
"COPILOT_PROVIDER_BASE_URL": "http://127.0.0.1:9999/v1",
|
|
|
|
|
"COPILOT_PROVIDER_WIRE_API": "completions",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-11 18:24:15 -05:00
|
|
|
def test_resolve_targets_provider_scope_auto_excludes_copilot(monkeypatch) -> None:
|
|
|
|
|
monkeypatch.setattr("headroom.install.planner.detect_targets", lambda: [])
|
|
|
|
|
|
|
|
|
|
targets = resolve_targets(
|
|
|
|
|
ProviderSelectionMode.AUTO.value,
|
|
|
|
|
[],
|
|
|
|
|
scope=ConfigScope.PROVIDER.value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert targets == [ToolTarget.CLAUDE.value, ToolTarget.CODEX.value]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_targets_manual_dedupes_and_filters_invalid() -> None:
|
|
|
|
|
targets = resolve_targets(
|
|
|
|
|
ProviderSelectionMode.MANUAL.value,
|
|
|
|
|
["claude", "copilot", "claude", "invalid"],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert targets == [ToolTarget.CLAUDE.value, ToolTarget.COPILOT.value]
|
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.
2026-07-07 12:37:23 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_omits_no_http2_by_default() -> None:
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
profile="default",
|
|
|
|
|
preset=InstallPreset.PERSISTENT_SERVICE.value,
|
|
|
|
|
runtime_kind="python",
|
|
|
|
|
scope="user",
|
|
|
|
|
provider_mode="manual",
|
|
|
|
|
targets=["claude"],
|
|
|
|
|
port=8787,
|
|
|
|
|
backend="anthropic",
|
|
|
|
|
anyllm_provider=None,
|
|
|
|
|
region=None,
|
|
|
|
|
proxy_mode="token",
|
|
|
|
|
memory_enabled=False,
|
|
|
|
|
telemetry_enabled=True,
|
fix(install): default docker image to headroomlabs-ai GHCR registry (#1867) (#2039)
## Description
The GitHub repository was transferred from `chopratejas/headroom` to
`headroomlabs-ai/headroom`. GitHub 301-redirects transferred repos for
web and git operations, but **GitHub Container Registry (GHCR) does
not** — the old package `ghcr.io/chopratejas/headroom` is now orphaned
and frozen (its `latest` tag stopped advancing at `0.27.0`), while CI
publishes new images to `ghcr.io/headroomlabs-ai/headroom` (the workflow
derives the path from `${{ github.repository }}`).
Headroom's install tooling still defaulted to the dead path, so
`headroom install apply --preset persistent-docker`, `headroom init`,
and the standalone install scripts all pulled a stale `0.27.0` image
instead of the current release.
This changes every docker-image **default** to
`ghcr.io/headroomlabs-ai/headroom:latest`.
Closes #1867
## 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/install/models.py` — `InstallManifest.image` default.
- `headroom/cli/install.py` — `--image` Click option default.
- `headroom/cli/init.py` — two `InstallManifest(...)` image args.
- `scripts/install.sh` / `scripts/install.ps1` — `IMAGE_DEFAULT` /
`$ImageDefault` plus the `--image` help-text default.
- `docker/docker-compose.native.yml` — image default in both services.
- `tests/test_install/test_planner.py` (4) and
`tests/test_install/test_runtime.py` (5) — updated the assertions that
pinned the old image (including `assert "<image>" in command`), so they
now verify the corrected registry threads through the planner and docker
runtime command.
Scope note: `github.com/chopratejas/...` links and the plugin
marketplace slug are intentionally **not** changed — GitHub redirects
those, so they still work. Only the genuinely-dead GHCR image references
are touched. No new dependency, no new abstraction.
## 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_install/ -q
99 passed, 1 skipped in 48.34s
$ uv run ruff check headroom/install/models.py headroom/cli/install.py headroom/cli/init.py
All checks passed!
$ grep -rn "ghcr.io/chopratejas/headroom" headroom/ scripts/ docker/ tests/
# (no source matches — every default now points at headroomlabs-ai)
```
The install-test assertions pinned the old image, so they fail against
the old defaults and pass after the fix — they are the regression guard.
## Real Behavior Proof
- **Environment:** Windows 11, Python 3.13.5, headroom installed from
this branch (editable, via `uv`).
- **Exact command / steps:** The dead-registry claim is verifiable at
the registry level, independent of a release:
```text
docker pull ghcr.io/chopratejas/headroom:latest # old default → 0.27.0
(frozen / orphaned)
docker pull ghcr.io/headroomlabs-ai/headroom:latest # new default →
current release
```
And the install pipeline now emits the correct image (covered by
`tests/test_install/test_runtime.py`, which asserts the resolved docker
command contains `ghcr.io/headroomlabs-ai/headroom:latest`).
- **Observed result:** `grep` confirms no `ghcr.io/chopratejas/headroom`
default remains in code, scripts, compose, or tests;
`tests/test_install/` is green (99 passed) with the corrected image
asserted end-to-end through planner → runtime command.
- **Not tested:** A live `docker pull` of both tags on this specific
machine (no local Docker daemon guaranteed) — the registry difference is
reproducible by anyone running the two `docker pull` commands above; and
an end-to-end `headroom install apply --preset persistent-docker`
against a real Docker host.
## 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
## Additional Notes
- Documentation checklist item is N/A — no user-facing docs surface
beyond the CHANGELOG entry.
- `mypy` left unchecked — not run as part of this verification; the
change is a string-default swap with no type-level surface.
- The `--image` help text and `docker-compose.native.yml` were included
so every user-facing default is consistent; only the dead GHCR image
string was changed.
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 23:31:28 +05:30
|
|
|
image="ghcr.io/headroomlabs-ai/headroom:latest",
|
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.
2026-07-07 12:37:23 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert "--no-http2" not in manifest.proxy_args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_persists_no_http2_override() -> None:
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
profile="default",
|
|
|
|
|
preset=InstallPreset.PERSISTENT_SERVICE.value,
|
|
|
|
|
runtime_kind="python",
|
|
|
|
|
scope="user",
|
|
|
|
|
provider_mode="manual",
|
|
|
|
|
targets=["claude"],
|
|
|
|
|
port=8787,
|
|
|
|
|
backend="anthropic",
|
|
|
|
|
anyllm_provider=None,
|
|
|
|
|
region=None,
|
|
|
|
|
proxy_mode="token",
|
|
|
|
|
memory_enabled=False,
|
|
|
|
|
telemetry_enabled=True,
|
fix(install): default docker image to headroomlabs-ai GHCR registry (#1867) (#2039)
## Description
The GitHub repository was transferred from `chopratejas/headroom` to
`headroomlabs-ai/headroom`. GitHub 301-redirects transferred repos for
web and git operations, but **GitHub Container Registry (GHCR) does
not** — the old package `ghcr.io/chopratejas/headroom` is now orphaned
and frozen (its `latest` tag stopped advancing at `0.27.0`), while CI
publishes new images to `ghcr.io/headroomlabs-ai/headroom` (the workflow
derives the path from `${{ github.repository }}`).
Headroom's install tooling still defaulted to the dead path, so
`headroom install apply --preset persistent-docker`, `headroom init`,
and the standalone install scripts all pulled a stale `0.27.0` image
instead of the current release.
This changes every docker-image **default** to
`ghcr.io/headroomlabs-ai/headroom:latest`.
Closes #1867
## 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/install/models.py` — `InstallManifest.image` default.
- `headroom/cli/install.py` — `--image` Click option default.
- `headroom/cli/init.py` — two `InstallManifest(...)` image args.
- `scripts/install.sh` / `scripts/install.ps1` — `IMAGE_DEFAULT` /
`$ImageDefault` plus the `--image` help-text default.
- `docker/docker-compose.native.yml` — image default in both services.
- `tests/test_install/test_planner.py` (4) and
`tests/test_install/test_runtime.py` (5) — updated the assertions that
pinned the old image (including `assert "<image>" in command`), so they
now verify the corrected registry threads through the planner and docker
runtime command.
Scope note: `github.com/chopratejas/...` links and the plugin
marketplace slug are intentionally **not** changed — GitHub redirects
those, so they still work. Only the genuinely-dead GHCR image references
are touched. No new dependency, no new abstraction.
## 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_install/ -q
99 passed, 1 skipped in 48.34s
$ uv run ruff check headroom/install/models.py headroom/cli/install.py headroom/cli/init.py
All checks passed!
$ grep -rn "ghcr.io/chopratejas/headroom" headroom/ scripts/ docker/ tests/
# (no source matches — every default now points at headroomlabs-ai)
```
The install-test assertions pinned the old image, so they fail against
the old defaults and pass after the fix — they are the regression guard.
## Real Behavior Proof
- **Environment:** Windows 11, Python 3.13.5, headroom installed from
this branch (editable, via `uv`).
- **Exact command / steps:** The dead-registry claim is verifiable at
the registry level, independent of a release:
```text
docker pull ghcr.io/chopratejas/headroom:latest # old default → 0.27.0
(frozen / orphaned)
docker pull ghcr.io/headroomlabs-ai/headroom:latest # new default →
current release
```
And the install pipeline now emits the correct image (covered by
`tests/test_install/test_runtime.py`, which asserts the resolved docker
command contains `ghcr.io/headroomlabs-ai/headroom:latest`).
- **Observed result:** `grep` confirms no `ghcr.io/chopratejas/headroom`
default remains in code, scripts, compose, or tests;
`tests/test_install/` is green (99 passed) with the corrected image
asserted end-to-end through planner → runtime command.
- **Not tested:** A live `docker pull` of both tags on this specific
machine (no local Docker daemon guaranteed) — the registry difference is
reproducible by anyone running the two `docker pull` commands above; and
an end-to-end `headroom install apply --preset persistent-docker`
against a real Docker host.
## 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
## Additional Notes
- Documentation checklist item is N/A — no user-facing docs surface
beyond the CHANGELOG entry.
- `mypy` left unchecked — not run as part of this verification; the
change is a string-default swap with no type-level surface.
- The `--image` help text and `docker-compose.native.yml` were included
so every user-facing default is consistent; only the dead GHCR image
string was changed.
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-07-13 23:31:28 +05:30
|
|
|
image="ghcr.io/headroomlabs-ai/headroom:latest",
|
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.
2026-07-07 12:37:23 -04:00
|
|
|
no_http2=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert manifest.proxy_args.count("--no-http2") == 1
|
|
|
|
|
assert "HEADROOM_HTTP2" not in manifest.base_env
|
fix(install): only validate requested targets on the manual path (#1659)
## Description
`resolve_targets()` runs the provider-scope "unsupported targets"
validation
**before** it dispatches on `provider_mode`:
```python
if scope == ConfigScope.PROVIDER.value:
unsupported = [t for t in requested if t and t not in valid]
if unsupported:
raise click.ClickException("Provider scope supports only ...; unsupported targets: ...")
if provider_mode == ALL: return [t.value for t in valid_targets] # ignores `requested`
if provider_mode == AUTO: ... # ignores `requested`
# manual: filters `requested`
```
But `all` and `auto` never consult the requested target list — only the
manual
path does. So an unsupported entry that those modes would simply ignore
instead
makes the call raise. Concretely:
```
headroom install apply --scope provider --providers all --target cursor
→ ClickException: Provider scope supports only claude, codex, openclaw, and
opencode; unsupported targets: cursor
```
...when it should just return the full provider set. (`--target` is a
click
`Choice` that accepts all 7 targets regardless of scope/mode, so this is
reachable from the CLI.) The user-scope equivalent,
`resolve_targets("all",
["cursor"])`, happily ignores `cursor` and returns all user targets — so
provider
scope is inconsistent with user scope for identical, ignored input.
Closes: no issue filed — found while auditing `install` target
resolution.
## Fix
Move the provider-scope validation so it runs only on the manual path
(the only
mode that reads `requested`). `all` returns the full provider set and
`auto`
returns detected/default targets, neither raising on an ignored
requested list.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/install/planner.py`: move the provider-scope "unsupported
targets" check below the `all`/`auto` dispatch, into the manual path.
- `tests/test_install/test_planner.py`: regression tests — `all` and
`auto` ignore an unsupported requested target under provider scope; the
manual path still rejects it.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.
## Testing
- [x] New tests added for the fixed behavior
(`tests/test_install/test_planner.py`)
- [x] Linting passes (`ruff check`) and formatting is clean (`ruff
format --check`)
- [ ] Full `pytest` deferred to CI (see Real Behavior Proof for the
local-OOM reason).
```text
$ uv run ruff check headroom/install/planner.py tests/test_install/test_planner.py
All checks passed!
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.12.11, headroom built from this
branch. Importing `headroom` loads the torch/transformers stack and a
full `pytest` gets OOM-killed on this box, so I verified the control
flow with a dependency-free script (only stdlib) and left the full
pytest to CI.
- Exact command / steps: replicated `resolve_targets`'s control flow
(with the fix, `click.ClickException` stubbed, and stand-in target
lists) in a standalone script — no `headroom` import — and exercised
`all`/`auto`/`manual` under provider scope with an unsupported `cursor`
entry, plus the user-scope and manual-dedup regressions.
- Observed result: `all`/`auto` return the provider targets without
raising, `manual` still raises on the unsupported target, and the
pre-existing manual-dedup and user-scope behavior is unchanged:
```text
OK: all + [cursor] + provider -> provider set (no raise)
OK: auto + [cursor] + provider -> [claude, codex] (no raise)
OK: manual + [cursor] + provider -> raises (preserved)
OK: manual dedupe/filter + user-scope all unchanged
PLANNER LOGIC VERIFIED
```
- Not tested: a full `headroom install apply` end-to-end (would require
the heavy stack and a real deployment); the change is confined to pure
target-list resolution. Full local `pytest` deferred to CI (OOM, per
above).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes — ran
lint + a standalone logic check; full pytest deferred to CI (local OOM,
disclosed above)
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- No new dependencies; a control-flow move plus tests.
---------
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-13 03:00:31 +05:30
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_targets_provider_scope_all_ignores_unsupported_requested() -> None:
|
|
|
|
|
"""`all` mode never consults the requested list, so an unsupported entry
|
|
|
|
|
like `cursor` must not make it raise — it should return the full provider
|
|
|
|
|
target set (regression: this used to raise a ClickException)."""
|
|
|
|
|
targets = resolve_targets(
|
|
|
|
|
ProviderSelectionMode.ALL.value,
|
|
|
|
|
["cursor"],
|
|
|
|
|
scope=ConfigScope.PROVIDER.value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert targets == [t.value for t in PROVIDER_SCOPE_TARGETS]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_targets_provider_scope_auto_ignores_unsupported_requested(monkeypatch) -> None:
|
|
|
|
|
"""`auto` mode also ignores the requested list, so an unsupported entry
|
|
|
|
|
must not raise."""
|
|
|
|
|
monkeypatch.setattr("headroom.install.planner.detect_targets", lambda: [])
|
|
|
|
|
|
|
|
|
|
targets = resolve_targets(
|
|
|
|
|
ProviderSelectionMode.AUTO.value,
|
|
|
|
|
["cursor"],
|
|
|
|
|
scope=ConfigScope.PROVIDER.value,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert targets == [ToolTarget.CLAUDE.value, ToolTarget.CODEX.value]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_targets_provider_scope_manual_rejects_unsupported() -> None:
|
|
|
|
|
"""The manual path DOES consult the requested list, so an unsupported
|
|
|
|
|
target under provider scope must still be rejected."""
|
|
|
|
|
with pytest.raises(click.ClickException, match="cursor"):
|
|
|
|
|
resolve_targets(
|
|
|
|
|
ProviderSelectionMode.MANUAL.value,
|
|
|
|
|
["cursor"],
|
|
|
|
|
scope=ConfigScope.PROVIDER.value,
|
|
|
|
|
)
|
feat(install): add apply flag parity, --env passthrough, and EIO retry (#2152)
## Description
Three related gaps in `headroom install apply` and its supervisor
lifecycle, found operating a real persistent deployment on this fork:
1. `install apply` only exposed a fixed subset of `headroom proxy`'s
flags (`--backend`, `--region`, `--mode`, `--port`, `--memory`,
`--telemetry`, `--no-http2`). Deployments that need code-aware
compression, tool-result interception, per-tool lossy-compression
protection, or a named AWS profile for Bedrock had no native way to
configure them through `install apply` — the generated `manifest.json`
would have to be hand-edited after the fact, which silently reverts on
the next `install apply` and isn't tracked anywhere.
2. Supervised runners (macOS launchd, Linux systemd/cron, Windows
services/tasks) all start their runner scripts with a bare environment
and do not inherit the interactive shell's exports. In particular, a
custom `HEADROOM_WORKSPACE_DIR` never reached the supervised process, so
`headroom install agent run` looked for its manifest in the wrong
location and failed outright with "No deployment profile named 'default'
is installed" even though `install apply` itself had succeeded moments
earlier.
3. `install_supervisor`'s macOS branch does an unconditional `launchctl
bootout` followed by a bare `bootstrap` with no retry, unlike
`start_supervisor` (already fixed by #1290), which rides out the ~15s
EIO (error 5) window launchd exhibits for several seconds after a
bootout. This left `install apply`'s own reinstall path exposed to the
same race #1290 fixed elsewhere — requiring the exact manual recovery
(bootout + remove the plist + reapply) #1290 was meant to eliminate.
Closes #
## Type of Change
- [x] 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/install.py`: `install apply` gains
`--code-aware/--no-code-aware`, `--intercept-tool-results`,
`--protect-tool-results <tool1,tool2>`, and `--bedrock-profile
<profile>`, mirroring the equivalent flags already on `headroom proxy`
(same names, same help text style). Also gains `--env KEY=VALUE`
(repeatable).
- `headroom/install/planner.py`: `build_manifest()` threads all five new
parameters into `proxy_args`/`base_env`, following the exact pattern
already used for `--region`/`--no-http2`. `--env` entries are merged
into `base_env` last, so they can override auto-derived defaults.
- `headroom/install/supervisors.py`:
- `_render_unix_runner`/`_render_windows_runner` emit `export`/`$env:`
lines for `base_env` before the `exec`, so
`run-headroom.sh`/`ensure-headroom.sh` (and Windows equivalents) carry
the environment forward to both the outer `install agent run` process
and the proxy subprocess it spawns. The Docker runtime path already
threaded `base_env` into `docker run --env`; this closes the same gap
for the process-based runtime.
- New `_bootstrap_with_retry()` helper extracted from
`start_supervisor`'s existing retry loop (from #1290), now shared by
both `start_supervisor` and `install_supervisor`.
- `tests/test_install/test_planner.py`: new tests for all five flags
(default-omitted and persisted cases), following the existing
`--no-http2` test pattern.
- `tests/test_install/test_supervisors.py`: new tests for `--env`
propagation into rendered runner scripts, and for `install_supervisor`'s
retry-until-success and raise-after-exhausted-retries paths (mirroring
the existing `start_supervisor` coverage). Also fixes a pre-existing
test's mock that returned `None` from a `subprocess.run` stub — this
only worked before because the old bare `bootstrap` call site never
inspected the return value; the new `_bootstrap_with_retry()` call does.
- `CHANGELOG.md`: added `### Features` and `### Fixed` entries under
`Unreleased`.
## 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_install/ tests/test_cli/test_wrap_persistent.py tests/test_cli/test_init_cli.py -q
============================= test session starts ==============================
platform darwin -- Python 3.13.13, pytest-9.0.3, pluggy-1.6.0
collected 215 items
tests/test_install/test_health.py ... [ 1%]
tests/test_install/test_native_installers.py ss [ 2%]
tests/test_install/test_paths.py ... [ 3%]
tests/test_install/test_planner.py .................. [ 12%]
tests/test_install/test_providers.py ................................... [ 28%]
...... [ 31%]
tests/test_install/test_runtime.py .................... [ 40%]
tests/test_install/test_state.py ..... [ 42%]
tests/test_install/test_supervisors.py ......................... [ 54%]
tests/test_cli/test_wrap_persistent.py ............................ [ 67%]
tests/test_cli/test_init_cli.py ........................................ [ 86%]
.............................. [100%]
======================== 213 passed, 2 skipped in 0.57s ========================
$ uv run ruff check headroom/cli/install.py headroom/install/planner.py headroom/install/supervisors.py tests/test_install/
All checks passed!
$ uv run mypy headroom/cli/install.py headroom/install/planner.py headroom/install/supervisors.py
Success: no issues found in 3 source files
```
## Real Behavior Proof
- Environment: personal fork deployed as a real proxy (macOS launchd
service via `headroom install apply`), profile `default`, backend
`bedrock` with a named AWS SSO profile.
- Exact command / steps: (flags 1 & 2) ran `headroom install apply
--backend bedrock --mode token --code-aware --protect-tool-results Bash
--bedrock-profile sso-bedrock --env
HEADROOM_WORKSPACE_DIR=/Users/<redacted>/.headroom-workspace --env
AWS_PROFILE=sso-bedrock --env AWS_REGION=eu-west-1`, then inspected the
generated `manifest.json`, the rendered `run-headroom.sh`, and the
running launchd job.
- Observed result: before this PR, none of `--code-aware`,
`--protect-tool-results`, `--bedrock-profile`, or `--env` were accepted
flags on `install apply` at all (`Error: No such option`). Reproduced
the `--env` gap specifically by running the exact command a launchd job
invokes with a stripped environment (no `HEADROOM_WORKSPACE_DIR`, no
`AWS_PROFILE`) — it failed to find the manifest; with the interactive
shell's env forwarded manually, it started fine. The generated plist had
no `EnvironmentVariables` key and `run-headroom.sh` was a bare `exec`,
confirming this wasn't a config mistake but a real gap between `install
apply`'s flag surface and what a supervisor actually runs with. After
this PR, `install apply` with all the flags above produces a launchd job
that starts clean, reports healthy, and successfully proxies a real
request to Bedrock (200, not just a green health check) using the named
AWS profile with no `AWS_PROFILE` env var needed elsewhere.
- Exact command / steps: (EIO retry, flag 3) triggered the same EIO race
#1290 documents by running `headroom install apply` twice in quick
succession against the same profile (the second run's
`install_supervisor` bootout+bootstrap lands inside the first run's
launchd settle window).
- Observed result: before this PR, the second `install apply`
occasionally failed outright with `CalledProcessError` from the bare
`subprocess.run(..., check=True)` bootstrap call, requiring the manual
bootout+`rm` plist+reapply recovery. After this PR (with
`_bootstrap_with_retry` in place), the same back-to-back sequence
completes successfully every time observed, riding out the EIO window
instead of failing.
- Not tested: Linux systemd/cron and Windows service/task supervisor
paths for the `--env` propagation — verified via the new unit tests
(which cover the runner-script rendering directly) but not against a
live Linux or Windows machine, since this deployment is macOS-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] 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/install logic change, no UI surface.
## Additional Notes
- "I have made corresponding changes to the documentation" is unchecked:
no file in `docs/`, `README.md`, or `CONTRIBUTING.md` documents `install
apply`'s flag surface in detail (it's discoverable via `--help`), so
there is no existing section to update for the new flags.
- Re-derivation note: this PR's `install_supervisor` EIO-retry fix and
its `_bootstrap_with_retry` extraction are written directly against
current `upstream/main`'s post-#1290 shape of `start_supervisor` (inline
retry loop with
`_MACOS_BOOTSTRAP_RETRIES`/`_MACOS_BOOTSTRAP_RETRY_DELAY`), not
cherry-picked from an older fork commit that predated #1290 — the diff
here is intentionally different from what a naive cherry-pick would have
produced.
- No linked issue number: found via operating a real persistent
deployment on a personal fork, not filed as a `headroomlabs-ai/headroom`
issue first. Checked `gh pr list --search` for "install apply flags/env"
and "bootstrap EIO"/"install_supervisor bootstrap retry" — no open or
merged coverage found beyond #1290 (which fixes `start_supervisor` only,
a different call site from the one this PR fixes).
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 20:10:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _base_manifest_kwargs(**overrides):
|
|
|
|
|
kwargs = {
|
|
|
|
|
"profile": "default",
|
|
|
|
|
"preset": InstallPreset.PERSISTENT_SERVICE.value,
|
|
|
|
|
"runtime_kind": "python",
|
|
|
|
|
"scope": "user",
|
|
|
|
|
"provider_mode": "manual",
|
|
|
|
|
"targets": ["claude"],
|
|
|
|
|
"port": 8787,
|
|
|
|
|
"backend": "bedrock",
|
|
|
|
|
"anyllm_provider": None,
|
|
|
|
|
"region": "eu-west-1",
|
|
|
|
|
"proxy_mode": "token",
|
|
|
|
|
"memory_enabled": False,
|
|
|
|
|
"telemetry_enabled": False,
|
|
|
|
|
"image": "ghcr.io/chopratejas/headroom:latest",
|
|
|
|
|
}
|
|
|
|
|
kwargs.update(overrides)
|
|
|
|
|
return kwargs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_omits_new_bedrock_flags_by_default() -> None:
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs())
|
|
|
|
|
|
|
|
|
|
assert "--code-aware" not in manifest.proxy_args
|
|
|
|
|
assert "--no-code-aware" not in manifest.proxy_args
|
|
|
|
|
assert "--intercept-tool-results" not in manifest.proxy_args
|
|
|
|
|
assert "--protect-tool-results" not in manifest.proxy_args
|
|
|
|
|
assert "--bedrock-profile" not in manifest.proxy_args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_persists_code_aware_true() -> None:
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs(code_aware=True))
|
|
|
|
|
|
|
|
|
|
assert "--code-aware" in manifest.proxy_args
|
|
|
|
|
assert "--no-code-aware" not in manifest.proxy_args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_persists_code_aware_false() -> None:
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs(code_aware=False))
|
|
|
|
|
|
|
|
|
|
assert "--no-code-aware" in manifest.proxy_args
|
|
|
|
|
assert "--code-aware" not in manifest.proxy_args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_persists_intercept_tool_results() -> None:
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs(intercept_tool_results=True))
|
|
|
|
|
|
|
|
|
|
assert "--intercept-tool-results" in manifest.proxy_args
|
feat: add deterministic runtime rollout controls (#1490)
## Description
Establish one centrally resolved, observable, deterministic, versioned
runtime rollout-control mechanism for Headroom. Runtime rollout controls
which behaviors an already-built artifact may expose; it does not select
or qualify a Headroom release/version.
## Type of Change
- [x] New feature (non-breaking change that adds functionality)
- [x] Bug fix (non-breaking change that fixes rollout enforcement
regressions)
- [x] Documentation update
- [x] Code refactoring (no functional changes)
## Changes Made
- Added `RolloutChannel`, `HEADROOM_ROLLOUT_CHANNEL`,
`--rollout-channel`, and a versioned immutable `RolloutSnapshot` shared
by Python configuration boundaries.
- Added schema/policy versions, canonical registry and snapshot SHA-256
identities, per-feature decision reasons, disable precedence, unsafe
qualification poisoning, strict CLI validation, and fail-closed
environment handling.
- Added `headroom rollout status --json`, Python `/stats.rollout`, and
Rust `/rollout/status` runtime provenance.
- Added equivalent Rust snapshot semantics and shared Python/Rust policy
vectors while retaining language-specific feature registries.
- Enforced rollout policy at alternate Python server composition roots
so `HEADROOM_READ_MATURATION=1` cannot bypass its beta gate.
- Preserved typed rollout snapshots across multi-worker serialization
with schema, policy, registry, snapshot-digest, type, and feature-name
validation.
- Made loopback runtime output-shaper updates replace the immutable
snapshot atomically for request readers, retain explicit request/disable
provenance, preserve channel and kill-switch precedence, invalidate
cached stats, and return the effective rollout decision.
- Made `headroom learn --verbosity --apply` report a channel-blocked
update instead of claiming the shaper is live.
- Made explicit CLI feature flags fail loudly when their current channel
blocks them.
- Made persistent interceptor installation select canary automatically,
or reject an explicitly insufficient channel unless the break-glass
override is set.
- Updated architecture, proxy, rollout, learn, and output-shaper
documentation with required channels and hot-reload semantics.
## Testing
- [x] Unit tests pass
- [x] Linting passes (`ruff check .` and `ruff format --check .`)
- [x] Type checking passes (`mypy headroom --ignore-missing-imports`)
- [x] New regression tests added for every corrected behavior
- [x] Rust tests and production-target Clippy pass
- [x] Documentation build passes
### Test Output
```text
Focused rollout coverage suite
57 passed; headroom.rollout + rollout CLI: 98% coverage
Affected proxy/rollout/transform/governance suites
222 passed; 0 failed
Final changed regression suites
100 passed; 0 failed
Cross-module hot-reload isolation regression
6 passed; 0 failed
cargo test -p headroom-core -p headroom-proxy --quiet
headroom-core: 924 passed; 1 ignored
headroom-proxy and integration suites: all passed
cargo clippy -p headroom-core -p headroom-proxy --lib --bins -- -D warnings
cargo fmt --all -- --check
ruff check .
ruff format --check .
mypy headroom --ignore-missing-imports
git diff --check
All passed
cd docs && npm run build
Compiled successfully; 164 static pages generated
```
The unsharded Windows-only CI selection exposed unrelated baseline
failures, principally the existing `sqlite:///C:\\...` URL parser
producing an invalid `\\C:\\...` path. At commit `8e793a80`, all 52
completed GitHub checks passed; the only other conclusions are expected
skips and superseded governance jobs.
## Real Behavior Proof
- **Environment:** Windows checkout on Python 3.13.3 and the current
Rust workspace, based on upstream `main` at `93f2d7a2`.
- **Exact command / steps:** Exercised canary and beta feature requests
through CLI status, Python `/stats.rollout`, Rust `/rollout/status`,
multi-worker payload round trips, loopback `/admin/runtime-env`, real
proxy request shaping before/after hot reload, installer manifest
generation, and shared Python/Rust policy vectors.
- **Observed result:** Stable blocks unstable requests; disable wins
over explicit/default/legacy/unsafe paths; unsafe state reports
`qualification_eligible=false`; worker handoff rejects tampering;
running output shaping changes only when the effective beta policy
permits it; explicit blocked flags fail with actionable diagnostics.
- **Not tested:** Live production traffic requiring provider
credentials, or future artifact qualification/promotion automation
(intentionally out of scope).
## Runtime Rollout Safety
- **Rollout-managed features:** Python `tool_result_interceptors`,
`proxy_output_shaper`, `read_maturation`; Rust `native_bedrock`,
`openai_responses_streaming`, `canary_probe`.
- **Minimum rollout channel:** Registry-defined per feature; process
default is `stable`.
- **Stable/default behavior changed:** No unstable feature becomes
enabled by default. Explicit blocked CLI flags now fail instead of
silently doing nothing.
- **Kill switch / disable path:**
`HEADROOM_DISABLE_FEATURES=<comma-separated feature names>`; explicit
disable has highest precedence, including over the unsafe override.
- **Unsafe override required:** No.
`HEADROOM_UNSAFE_ALLOW_UNSTABLE_FEATURES=1` is break-glass only and
makes qualification evidence ineligible.
- **Qualification impact:** Adds machine-readable policy/snapshot
identities and eligibility; does not implement qualification itself.
- **Rollback path:** Set the named disable list for operational
rollback, lower the channel, or revert this PR.
## Review Readiness
- [x] I have performed a full diff 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 hard-to-understand areas
- [x] I have made corresponding documentation changes
- [x] My changes generate no new warnings
- [x] I added tests that reproduce and prevent every regression fixed
during review
- [x] New and existing affected tests pass locally
- [x] I did **not** edit `CHANGELOG.md`; release-please generates it
from the Conventional Commit PR title
## Additional Notes
Out of scope: artifact candidates, benchmark orchestration,
qualification manifests/gates, promotion automation, release branches,
publication guards, and release-risk classification. Those workflows can
consume the rollout registry digest, runtime snapshot digest, decision
reasons, and qualification eligibility through supported black-box
interfaces.
---------
Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
Co-authored-by: JD Davis <jd@JDH-AIR-00.local>
2026-08-12 23:16:54 -05:00
|
|
|
assert manifest.base_env["HEADROOM_ROLLOUT_CHANNEL"] == "canary"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_rejects_interceptor_below_required_rollout_channel() -> None:
|
|
|
|
|
with pytest.raises(click.ClickException, match="requires HEADROOM_ROLLOUT_CHANNEL=canary"):
|
|
|
|
|
build_manifest(
|
|
|
|
|
**_base_manifest_kwargs(
|
|
|
|
|
intercept_tool_results=True,
|
|
|
|
|
extra_env={"HEADROOM_ROLLOUT_CHANNEL": "stable"},
|
|
|
|
|
)
|
|
|
|
|
)
|
feat(install): add apply flag parity, --env passthrough, and EIO retry (#2152)
## Description
Three related gaps in `headroom install apply` and its supervisor
lifecycle, found operating a real persistent deployment on this fork:
1. `install apply` only exposed a fixed subset of `headroom proxy`'s
flags (`--backend`, `--region`, `--mode`, `--port`, `--memory`,
`--telemetry`, `--no-http2`). Deployments that need code-aware
compression, tool-result interception, per-tool lossy-compression
protection, or a named AWS profile for Bedrock had no native way to
configure them through `install apply` — the generated `manifest.json`
would have to be hand-edited after the fact, which silently reverts on
the next `install apply` and isn't tracked anywhere.
2. Supervised runners (macOS launchd, Linux systemd/cron, Windows
services/tasks) all start their runner scripts with a bare environment
and do not inherit the interactive shell's exports. In particular, a
custom `HEADROOM_WORKSPACE_DIR` never reached the supervised process, so
`headroom install agent run` looked for its manifest in the wrong
location and failed outright with "No deployment profile named 'default'
is installed" even though `install apply` itself had succeeded moments
earlier.
3. `install_supervisor`'s macOS branch does an unconditional `launchctl
bootout` followed by a bare `bootstrap` with no retry, unlike
`start_supervisor` (already fixed by #1290), which rides out the ~15s
EIO (error 5) window launchd exhibits for several seconds after a
bootout. This left `install apply`'s own reinstall path exposed to the
same race #1290 fixed elsewhere — requiring the exact manual recovery
(bootout + remove the plist + reapply) #1290 was meant to eliminate.
Closes #
## Type of Change
- [x] 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/install.py`: `install apply` gains
`--code-aware/--no-code-aware`, `--intercept-tool-results`,
`--protect-tool-results <tool1,tool2>`, and `--bedrock-profile
<profile>`, mirroring the equivalent flags already on `headroom proxy`
(same names, same help text style). Also gains `--env KEY=VALUE`
(repeatable).
- `headroom/install/planner.py`: `build_manifest()` threads all five new
parameters into `proxy_args`/`base_env`, following the exact pattern
already used for `--region`/`--no-http2`. `--env` entries are merged
into `base_env` last, so they can override auto-derived defaults.
- `headroom/install/supervisors.py`:
- `_render_unix_runner`/`_render_windows_runner` emit `export`/`$env:`
lines for `base_env` before the `exec`, so
`run-headroom.sh`/`ensure-headroom.sh` (and Windows equivalents) carry
the environment forward to both the outer `install agent run` process
and the proxy subprocess it spawns. The Docker runtime path already
threaded `base_env` into `docker run --env`; this closes the same gap
for the process-based runtime.
- New `_bootstrap_with_retry()` helper extracted from
`start_supervisor`'s existing retry loop (from #1290), now shared by
both `start_supervisor` and `install_supervisor`.
- `tests/test_install/test_planner.py`: new tests for all five flags
(default-omitted and persisted cases), following the existing
`--no-http2` test pattern.
- `tests/test_install/test_supervisors.py`: new tests for `--env`
propagation into rendered runner scripts, and for `install_supervisor`'s
retry-until-success and raise-after-exhausted-retries paths (mirroring
the existing `start_supervisor` coverage). Also fixes a pre-existing
test's mock that returned `None` from a `subprocess.run` stub — this
only worked before because the old bare `bootstrap` call site never
inspected the return value; the new `_bootstrap_with_retry()` call does.
- `CHANGELOG.md`: added `### Features` and `### Fixed` entries under
`Unreleased`.
## 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_install/ tests/test_cli/test_wrap_persistent.py tests/test_cli/test_init_cli.py -q
============================= test session starts ==============================
platform darwin -- Python 3.13.13, pytest-9.0.3, pluggy-1.6.0
collected 215 items
tests/test_install/test_health.py ... [ 1%]
tests/test_install/test_native_installers.py ss [ 2%]
tests/test_install/test_paths.py ... [ 3%]
tests/test_install/test_planner.py .................. [ 12%]
tests/test_install/test_providers.py ................................... [ 28%]
...... [ 31%]
tests/test_install/test_runtime.py .................... [ 40%]
tests/test_install/test_state.py ..... [ 42%]
tests/test_install/test_supervisors.py ......................... [ 54%]
tests/test_cli/test_wrap_persistent.py ............................ [ 67%]
tests/test_cli/test_init_cli.py ........................................ [ 86%]
.............................. [100%]
======================== 213 passed, 2 skipped in 0.57s ========================
$ uv run ruff check headroom/cli/install.py headroom/install/planner.py headroom/install/supervisors.py tests/test_install/
All checks passed!
$ uv run mypy headroom/cli/install.py headroom/install/planner.py headroom/install/supervisors.py
Success: no issues found in 3 source files
```
## Real Behavior Proof
- Environment: personal fork deployed as a real proxy (macOS launchd
service via `headroom install apply`), profile `default`, backend
`bedrock` with a named AWS SSO profile.
- Exact command / steps: (flags 1 & 2) ran `headroom install apply
--backend bedrock --mode token --code-aware --protect-tool-results Bash
--bedrock-profile sso-bedrock --env
HEADROOM_WORKSPACE_DIR=/Users/<redacted>/.headroom-workspace --env
AWS_PROFILE=sso-bedrock --env AWS_REGION=eu-west-1`, then inspected the
generated `manifest.json`, the rendered `run-headroom.sh`, and the
running launchd job.
- Observed result: before this PR, none of `--code-aware`,
`--protect-tool-results`, `--bedrock-profile`, or `--env` were accepted
flags on `install apply` at all (`Error: No such option`). Reproduced
the `--env` gap specifically by running the exact command a launchd job
invokes with a stripped environment (no `HEADROOM_WORKSPACE_DIR`, no
`AWS_PROFILE`) — it failed to find the manifest; with the interactive
shell's env forwarded manually, it started fine. The generated plist had
no `EnvironmentVariables` key and `run-headroom.sh` was a bare `exec`,
confirming this wasn't a config mistake but a real gap between `install
apply`'s flag surface and what a supervisor actually runs with. After
this PR, `install apply` with all the flags above produces a launchd job
that starts clean, reports healthy, and successfully proxies a real
request to Bedrock (200, not just a green health check) using the named
AWS profile with no `AWS_PROFILE` env var needed elsewhere.
- Exact command / steps: (EIO retry, flag 3) triggered the same EIO race
#1290 documents by running `headroom install apply` twice in quick
succession against the same profile (the second run's
`install_supervisor` bootout+bootstrap lands inside the first run's
launchd settle window).
- Observed result: before this PR, the second `install apply`
occasionally failed outright with `CalledProcessError` from the bare
`subprocess.run(..., check=True)` bootstrap call, requiring the manual
bootout+`rm` plist+reapply recovery. After this PR (with
`_bootstrap_with_retry` in place), the same back-to-back sequence
completes successfully every time observed, riding out the EIO window
instead of failing.
- Not tested: Linux systemd/cron and Windows service/task supervisor
paths for the `--env` propagation — verified via the new unit tests
(which cover the runner-script rendering directly) but not against a
live Linux or Windows machine, since this deployment is macOS-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] 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/install logic change, no UI surface.
## Additional Notes
- "I have made corresponding changes to the documentation" is unchecked:
no file in `docs/`, `README.md`, or `CONTRIBUTING.md` documents `install
apply`'s flag surface in detail (it's discoverable via `--help`), so
there is no existing section to update for the new flags.
- Re-derivation note: this PR's `install_supervisor` EIO-retry fix and
its `_bootstrap_with_retry` extraction are written directly against
current `upstream/main`'s post-#1290 shape of `start_supervisor` (inline
retry loop with
`_MACOS_BOOTSTRAP_RETRIES`/`_MACOS_BOOTSTRAP_RETRY_DELAY`), not
cherry-picked from an older fork commit that predated #1290 — the diff
here is intentionally different from what a naive cherry-pick would have
produced.
- No linked issue number: found via operating a real persistent
deployment on a personal fork, not filed as a `headroomlabs-ai/headroom`
issue first. Checked `gh pr list --search` for "install apply flags/env"
and "bootstrap EIO"/"install_supervisor bootstrap retry" — no open or
merged coverage found beyond #1290 (which fixes `start_supervisor` only,
a different call site from the one this PR fixes).
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 20:10:39 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_persists_protect_tool_results() -> None:
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs(protect_tool_results="Bash,WebFetch"))
|
|
|
|
|
|
|
|
|
|
idx = manifest.proxy_args.index("--protect-tool-results")
|
|
|
|
|
assert manifest.proxy_args[idx + 1] == "Bash,WebFetch"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_persists_bedrock_profile() -> None:
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs(bedrock_profile="sso-bedrock"))
|
|
|
|
|
|
|
|
|
|
idx = manifest.proxy_args.index("--bedrock-profile")
|
|
|
|
|
assert manifest.proxy_args[idx + 1] == "sso-bedrock"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_merges_extra_env_into_base_env() -> None:
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
**_base_manifest_kwargs(extra_env={"HEADROOM_WORKSPACE_DIR": "/custom/workspace"})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert manifest.base_env["HEADROOM_WORKSPACE_DIR"] == "/custom/workspace"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_extra_env_overrides_derived_defaults() -> None:
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs(extra_env={"HEADROOM_TELEMETRY": "on"}))
|
|
|
|
|
|
|
|
|
|
# telemetry_enabled=False in _base_manifest_kwargs would normally set "off";
|
|
|
|
|
# an explicit --env must win.
|
|
|
|
|
assert manifest.base_env["HEADROOM_TELEMETRY"] == "on"
|
fix(wrap): set xAI upstream for grok-build proxy (#2772)
## Description
`headroom wrap grok-build` injected the client hop into
`~/.grok/config.toml` but started the local proxy **without** setting
the OpenAI-compatible upstream to xAI. The proxy defaulted to
`api.openai.com`, so Grok session auth returned **401** on every chat
completion even though compression still ran.
`wrap grok` already passes `openai_api_url` → xAI. This PR aligns `wrap
grok-build` and the Grok-only persistent `install` path on the shared
`DEFAULT_API_URL` (`https://api.x.ai`).
Closes # (none — discovered in live Grok Build pilot)
## Type of Change
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which 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
- Pass `openai_api_url=_GROK_DEFAULT_API_URL` into
`_run_proxy_only_watcher` from `wrap grok-build`
- Use shared `DEFAULT_API_URL` from `wrap grok` (no hard-coded string
drift)
- Print proxy upstream in Grok Build setup lines
- Persistent install: when targets are Grok-only, set
`OPENAI_TARGET_API_URL` + `--openai-api-url` (skip when
Codex/Copilot/Aider/OpenCode share the proxy; explicit env still wins)
- Regression tests for wrap kwargs, setup lines, and install planner
## Testing
- [x] Unit tests pass (`pytest` targeted suite)
- [ ] Linting passes (`ruff check .`) — not run in this environment (no
native editable build)
- [ ] Type checking passes (`mypy headroom`) — not run
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ PYTHONPATH=$PWD python -m pytest \
tests/test_cli/test_wrap_bridge.py::test_wrap_grok_build_passes_xai_openai_api_url \
tests/test_cli/test_wrap_bridge.py::test_wrap_grok_build_uses_actual_proxy_port \
tests/test_install/test_planner.py::test_build_manifest_grok_build_only_sets_xai_upstream \
tests/test_install/test_planner.py::test_build_manifest_grok_with_codex_does_not_force_xai \
tests/test_install/test_planner.py::test_build_manifest_extra_env_wins_over_grok_xai_default \
tests/test_provider_grok_build.py::test_grok_build_setup_lines_include_proxy_url -q
......
6 passed in 0.33s
```
## Real Behavior Proof
- Environment: macOS (darwin), Headroom 0.33.0 via `uv tool install
"headroom-ai[proxy,mcp,code]==0.33.0"`, Grok Build CLI, models
`grok-build` and `grok-4.5`, proxy on `127.0.0.1:8787`, upstream must be
xAI
- Exact command / steps: (1) Before: stock `headroom wrap grok-build`
then `grok -m grok-build` one-shot prompt. (2) After: same wrap path
with this branch (`openai_api_url=DEFAULT_API_URL` into
`_run_proxy_only_watcher`) then `grok -m grok-build -p
'…HEADROOM_XAI_OK…'`. Also exercised `grok-4.5` via `[model."grok-4.5"]
base_url` → same proxy.
- Observed result: Before — proxy log outbound `api.openai.com` → HTTP
401; client failed while local compression still ran. After — setup line
prints Proxy upstream `https://api.x.ai`; proxy log `POST
https://api.x.ai/v1/chat/completions` (and `/v1/responses` for grok-4.5)
→ status=200; dashboard shows 0 failed requests and accumulating token
savings on live traffic.
- Not tested: full `uv run` editable/maturin native build on this host;
multi-tool install matrix beyond planner unit tests; Windows; ruff/mypy
full tree
## 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 commented my code, particularly in hard-to-understand areas
- [ ] I made corresponding changes to the documentation (CLI help text /
setup lines only)
- [x] My changes generate no new warnings
- [x] I 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` — generated by release-please
from Conventional Commit PR title (a CI guard enforces this)
## Additional Notes
- Intentional non-goal: changing default model, savings %, or Grok Build
context-tool defaults
- Mixed-target install (e.g. `grok_build` + `codex`) does **not** force
xAI — operator must set upstream explicitly if they share one proxy
- Related live routing: manual `[model."grok-4.5"] base_url` through the
same proxy works once upstream is xAI (`/v1/responses`)
---------
Co-authored-by: Grok 4.5 <noreply@x.ai>
Co-authored-by: Nestor G Pestelos Jr <ngpestelos@me.com>
Co-authored-by: JD Davis <jd@jds-macbook-air.tail2a279.ts.net>
2026-08-17 06:04:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_grok_build_only_sets_xai_upstream() -> None:
|
|
|
|
|
"""Persistent install for Grok Build alone must route proxy upstream to xAI."""
|
|
|
|
|
from headroom.providers.grok import DEFAULT_API_URL
|
|
|
|
|
|
|
|
|
|
manifest = build_manifest(**_base_manifest_kwargs(targets=["grok_build"], backend="openai"))
|
|
|
|
|
|
|
|
|
|
assert manifest.base_env.get("OPENAI_TARGET_API_URL") == DEFAULT_API_URL
|
|
|
|
|
idx = manifest.proxy_args.index("--openai-api-url")
|
|
|
|
|
assert manifest.proxy_args[idx + 1] == DEFAULT_API_URL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_grok_with_codex_does_not_force_xai() -> None:
|
|
|
|
|
"""Do not override OpenAI upstream when OpenAI-native tools share the proxy."""
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
**_base_manifest_kwargs(targets=["grok_build", "codex"], backend="openai")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert "OPENAI_TARGET_API_URL" not in manifest.base_env
|
|
|
|
|
assert "--openai-api-url" not in manifest.proxy_args
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_manifest_extra_env_wins_over_grok_xai_default() -> None:
|
|
|
|
|
manifest = build_manifest(
|
|
|
|
|
**_base_manifest_kwargs(
|
|
|
|
|
targets=["grok_build"],
|
|
|
|
|
backend="openai",
|
|
|
|
|
extra_env={"OPENAI_TARGET_API_URL": "https://gateway.example/v1"},
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert manifest.base_env["OPENAI_TARGET_API_URL"] == "https://gateway.example/v1"
|
|
|
|
|
idx = manifest.proxy_args.index("--openai-api-url")
|
|
|
|
|
assert manifest.proxy_args[idx + 1] == "https://gateway.example/v1"
|