headroom/tests/test_install/test_planner.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

368 lines
13 KiB
Python
Raw Permalink Normal View History

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
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
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",
)
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
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
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"
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",
)
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
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",
}
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"