## Description
`start_detached_agent()` opens the agent log file and hands it to
`subprocess.Popen` as `stdout`/`stderr`, then returns the process
**without
closing the parent's copy of the file descriptor**. The child inherits
the fd
and writes to it, but the parent keeps its own copy open forever.
The result: every `headroom install start` leaks one file descriptor in
the
parent, and the leaked handle pins the log file open so it can't be
rotated.
On a tight `ulimit` or inside a container, repeated starts can walk
straight
into the fd limit.
```python
# headroom/install/runtime.py — before
log_file = open(log_file_path, "a", encoding="utf-8", errors="replace")
kwargs = {"stdout": log_file, "stderr": log_file, ...}
return subprocess.Popen(command, **kwargs) # parent's log_file never closed
```
The fix closes the parent's copy in a `try/finally` right after `Popen`
returns:
```python
try:
proc = subprocess.Popen(command, **kwargs)
finally:
# The child has inherited the log file descriptor, so the parent's
# copy is dead weight. Closing it (even when Popen raises) avoids
# leaking one fd per `headroom install start` and lets the log file
# be rotated.
log_file.close()
return proc
```
The `finally` is deliberate: it also covers the case where `Popen`
itself
raises (bad executable, fork failure), which would otherwise leak the
just-opened handle. This matches the `with open(...)` pattern already
used by
`run_foreground()` a few lines above.
Closes#1554
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `headroom/install/runtime.py`: close the parent's log file descriptor
in a `try/finally` after `subprocess.Popen` in `start_detached_agent()`,
so it is released on the normal path and when `Popen` raises.
- `tests/test_install/test_runtime.py`: add two regression tests — one
for a normal start, one for `Popen` raising — asserting the parent's log
handle is closed afterwards.
- `CHANGELOG.md`: Bug Fixes entry under Unreleased.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
Before the fix (`runtime.py` reverted to its parent commit, new tests
kept) —
the assertion inspects the *actual* log file handle and finds it still
open:
```text
E AssertionError: assert False is True
E + where False = <_io.TextIOWrapper name='...\deploy\demo\runner.log' mode='a' encoding='utf-8'>.closed
FAILED tests/test_install/test_runtime.py::test_start_detached_agent_closes_parent_log_fd
FAILED tests/test_install/test_runtime.py::test_start_detached_agent_closes_log_fd_when_popen_raises
============================== 2 failed in 0.43s ==============================
```
After the fix:
```text
tests\test_install\test_runtime.py ................
====================== 16 passed, 1 deselected in 0.35s =======================
```
(The one deselected test,
`test_runtime_start_lock_blocks_another_process`, is a
pre-existing failure on my Windows box — it fails identically on a clean
checkout of `main` and is unrelated to this change.)
## Real Behavior Proof
- Environment: Windows 11, Python 3.12.11, headroom built from this
branch (`uv sync --extra dev`).
- Exact command / steps: ran the two regression tests, which drive the
real `start_detached_agent` code path — real `open()`, real
`Popen(stdout=...)`, real (or missing) `close()`. Only
`subprocess.Popen` is stubbed so the test never launches an actual
detached agent; the fd-lifecycle bug lives entirely in how the parent
handles its own handle, and that runs for real.
- Observed result: the log file handle the parent passed to `Popen` is
`.closed == False` before the fix and `.closed == True` after — for both
the normal path and the `Popen`-raises path (output above).
- Not tested: I intentionally did not spin up many real detached agents
to watch the OS fd table grow — on Windows that means flashing console
windows and isn't a clean signal anyway. The handle-state assertion on
the real file object is the deterministic equivalent. Did not run the
full `mypy headroom` pass (one-line lifecycle change, no new types).
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- Single logical change, no new dependencies, default behavior otherwise
unchanged.
- Rebased/merged latest `main` to clear a `CHANGELOG.md` conflict.
- @chopratejas this is a sibling of #1555 (the `wrap.py` readiness-loop
handle leak you've got filed). I scoped this PR to #1554 only; happy to
follow up on #1555 separately if you'd like.
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
## Description
`headroom install status` crashed with `OSError: [WinError 87] The
parameter is incorrect` on Windows and, worse, tore down the live proxy
it was only meant to inspect. `runtime_status()` probed liveness with a
bare `os.kill(pid, 0)` guarded only by `except OSError`. Against a
detached Windows agent (`DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP`),
that call raises WinError 87, which CPython surfaces as a `SystemError`
— not an `OSError` — so it escaped the handler, crashed status, and left
the deployment dead (PID file removed, port 8787 freed). This mirrors
the `os.kill`/`SystemError` fix PR #1315 applied to `cli/wrap.py`.
Closes#1544
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added a shared Windows-safe `headroom._subprocess.pid_alive()` helper:
rejects non-positive PIDs, prefers `psutil.pid_exists()`, and treats
`SystemError` (WinError 87) as "not alive".
- `install/runtime.py` `runtime_status()` now delegates to `pid_alive()`
instead of an unguarded `os.kill(pid, 0)`.
- `install/runtime.py` `stop_runtime()` now also catches `SystemError`
to avoid the same crash class on shutdown.
- `cli/wrap.py` `_pid_alive()` now delegates to the shared helper, so
the marker-cleanup path and the install/runtime status path share one
liveness probe (the shared helper the issue asked for).
- Added regression tests for the helper and `runtime_status`.
## 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
$ ruff check .
All checks passed!
$ ruff format --check headroom/_subprocess.py headroom/install/runtime.py headroom/cli/wrap.py tests/test_install/test_runtime.py tests/test_pid_alive.py
5 files already formatted
$ mypy headroom --ignore-missing-imports
(exit 0)
$ pytest tests/test_pid_alive.py tests/test_install/test_runtime.py tests/test_cli/test_wrap_helpers.py tests/test_cli/test_wrap_persistent.py \
--deselect "tests/test_install/test_runtime.py::test_runtime_start_lock_blocks_another_process"
89 passed
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.13, ruff 0.15.17 / mypy 1.20.2 /
pytest 9.1.0, psutil 7.2.2, branch `fix/1544-windows-pid-liveness`.
- Exact command / steps: ran the four checks above; the new
`tests/test_pid_alive.py` injects a `SystemError` (simulated WinError
87) and a stubbed `psutil` to drive both code paths, and
`test_runtime_status_*` exercise `runtime_status()` end to end with a
PID file present.
- Observed result: `runtime_status` returns `"running"` for a live PID
without sending any signal (asserted), returns `"stopped"` instead of
crashing when the probe raises `SystemError`, and the helper only ever
passes signal `0`. All 89 targeted tests pass; ruff/format/mypy clean.
- Not tested: the full `headroom install apply --preset persistent-task`
detached-agent reproduction against a live proxy was not run end to end;
it is instead covered by the deterministic `SystemError`/WinError-87
injection regression tests.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
- One pre-existing test,
`test_runtime_start_lock_blocks_another_process`, fails on my local
Windows checkout **before** these changes too (it asserts cross-process
file-lock blocking and depends on `HOME` semantics that differ on
Windows). It is unrelated to this fix and is deselected above; it passes
on the Linux CI runners.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Description
`headroom install apply --preset persistent-docker` pulls the image,
starts the container, then fails after ~45s with "Deployment 'default'
did not become ready after start." The rollback removes the container
and manifest, leaving nothing running and no logs.
Root cause: the published image already bakes the proxy invocation into
its ENTRYPOINT (`Dockerfile`: `ENTRYPOINT ["headroom", "proxy"]`), but
`build_runtime_command()` in `headroom/install/runtime.py` re-added
`headroom proxy` after the image name. Docker concatenates ENTRYPOINT +
args, so the container ran `headroom proxy headroom proxy --host 0.0.0.0
...` and Click aborted with `Got unexpected extra arguments (headroom
proxy)`.
The runtime command now appends only the proxy flags after the image
name, substituting the all-interface container bind host for the host
pair carried in `proxy_args`.
Closes#833
## 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/runtime.py`: drop the duplicated `headroom proxy`
from the docker `build_runtime_command` output; append only `--host
<bind> *proxy_args[2:]`. Extracted `CONTAINER_BIND_HOST` and
`_PROXY_ARGS_HOST_PAIR_LEN` named constants.
- `tests/test_install/test_runtime.py`: new regression test asserting
the args appended after the image name never re-add the `headroom proxy`
ENTRYPOINT.
- `CHANGELOG.md`: Unreleased → Bug Fixes entry.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ uv run pytest tests/test_install/ -q
91 passed, 1 skipped in 5.48s
$ uv run ruff check headroom/install/runtime.py tests/test_install/test_runtime.py
All checks passed!
$ uv run mypy headroom/install/runtime.py
Success: no issues found in 1 source file
```
#### RED → GREEN proof
RED — new test with the prod fix reverted (test kept):
```text
E AssertionError: container args re-add the ENTRYPOINT — got
['headroom', 'proxy', '--host', '0.0.0.0', '--port', '8787', '--backend', 'anthropic']
FAILED tests/test_install/test_runtime.py::test_build_runtime_command_for_docker_does_not_duplicate_entrypoint
1 failed in 0.17s
```
GREEN — with the fix applied:
```text
tests/test_install/test_runtime.py::test_build_runtime_command_for_docker_does_not_duplicate_entrypoint
1 passed in 0.11s
```
## Real Behavior Proof
- Environment: Linux, Python 3.13, headroom @ this branch.
- Exact command / steps: reproduce the exact concatenation Docker
performs (ENTRYPOINT `headroom proxy` + the buggy CMD `headroom proxy
--host 0.0.0.0 --port 8787`):
```text
$ headroom proxy headroom proxy --host 0.0.0.0 --port 8787
Usage: headroom proxy [OPTIONS]
Try 'headroom proxy --help' for help.
Error: Got unexpected extra arguments (headroom proxy)
```
This is the exact error from the issue. After the fix,
`build_runtime_command` appends only the flags after the image name:
```text
args after image: ['--host', '0.0.0.0', '--port', '8787', '--backend',
'anthropic']
```
so the container runs `headroom proxy --host 0.0.0.0 --port 8787
--backend anthropic` (ENTRYPOINT + flags) and Click accepts it.
- Observed result: pre-fix Click aborts with the unexpected-arguments
error (container crash-loops); post-fix the command line is valid.
- Not tested: pulling and running the real `ghcr.io` image end-to-end
(requires the published image + Docker host); the failure is fully
determined by the generated argv, which is covered above and by the unit
test.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
Scope is limited to the docker runtime command construction. The Python
(`runtime_kind=python`) path was already correct and is unchanged.
Screenshots N/A (CLI-only change).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
## Description
Fixes `headroom install restart` and `headroom install start` for macOS
launchd `persistent-service` deployments — both currently leave the
proxy **stopped**.
`restart = stop + start`, but the two halves used incompatible
`launchctl` verbs: `stop` runs `launchctl bootout` (which
**unregisters** the job from the domain), while `start` only ran
`launchctl kickstart -k` (which requires the job to **still be
registered**). After `bootout` removes the job, `kickstart` can never
find it again (`exit 113`), and nothing ever called `launchctl
bootstrap` — so neither a post-`bootout` restart nor a cold `start`
could (re)register it. `stop` also used `check=True`, so booting out an
already-absent job (`exit 3`) raised and aborted `restart` before it
could start again.
Closes#1289
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `start_supervisor` (darwin): try `launchctl kickstart -k` first (fast
path when the job is already bootstrapped, e.g. right after `install
apply` or on a running service); on failure, `launchctl bootstrap` the
plist fresh — which also starts it via `RunAtLoad`.
- Retry the `bootstrap` for ~15s. launchd returns EIO (`Bootstrap
failed: 5: Input/output error`) from `bootstrap` for several seconds
after a `bootout` while it releases the label; on exhaustion a
`click.ClickException` surfaces the last launchctl error instead of a
raw traceback. Tunables: `_MACOS_BOOTSTRAP_RETRIES` /
`_MACOS_BOOTSTRAP_RETRY_DELAY`.
- `stop_supervisor` (darwin): run `bootout` with `check=False` so an
already-absent job (`exit 3`) is treated as already-stopped rather than
aborting `restart`.
- Tests: 5 new cases in `tests/test_install/test_supervisors.py` (warm
`kickstart` success, `bootstrap` fallback when not registered, EIO
retry, raise-after-exhaustion, tolerant stop); `time.sleep` is
monkeypatched so they stay fast.
- `CHANGELOG.md`: entry under Unreleased → Bug Fixes.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_install/
77 passed, 1 skipped, 1 warning in 5.35s
$ pytest tests/test_install/test_supervisors.py -q
19 passed, 1 warning in 0.10s
$ ruff check headroom/install/supervisors.py tests/test_install/test_supervisors.py
All checks passed!
$ ruff format --check headroom/install/supervisors.py tests/test_install/test_supervisors.py
2 files already formatted
$ mypy --python-version 3.10 headroom/install/supervisors.py
Success: no issues found in 1 source file
```
## Real Behavior Proof
- Environment: macOS 26.5.1 (arm64), launchd 7.0.0, headroom installed
via pipx; profile `default`, preset `persistent-service`, scope `user`,
port 8787.
- Exact command / steps: patched the installed `supervisors.py` to this
exact code, then exercised the live deployment — `headroom install
restart --profile default` (warm restart), `headroom install stop
--profile default`, then `headroom install start --profile default`
(cold start, post-bootout); health checked via `curl
http://127.0.0.1:8787/readyz` and `headroom install status` after each.
- Observed result: every transition lands healthy with no traceback
(before this PR they failed). `install restart` on a running service →
healthy (was: `bootout` exit 3 → abort, proxy down); `install start`
cold/post-bootout → healthy in ~8s (was: exit 113 / EIO); `install stop`
→ down; `install start` from stopped → healthy; 3× rapid `install
restart` → all healthy. The EIO settle window was measured directly:
`bootstrap` failed with error 5 for ~5s (10 attempts) then succeeded on
attempt 11 — which is what the retry loop rides out.
- Not tested: system-scope (`/Library/LaunchDaemons`) deployments and
the Linux/Windows branches were not exercised on hardware (unchanged by
this PR); covered by unit tests 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 lifecycle change.
## Additional Notes
- Docs checkbox left unchecked: no user-facing docs describe the launchd
lifecycle internals; happy to add a note if you point me at the right
place.
- **Tradeoff:** because the correct post-`bootout` recovery has to wait
out launchd's ~5s EIO window, `restart` and cold `start` take several
seconds. The `kickstart`-first fast path keeps the common
already-bootstrapped case instant; only the post-`bootout` path pays the
settle. Open to a different shape if you'd prefer (e.g. having `restart`
avoid the full `bootout`).
- CI-only checks (commitlint, pre-commit `ci-precheck`) were not run
locally; the commit header follows conventional commits (`fix(install):
…`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
## Description
The CI lint job (`ruff check .` → `ruff format --check .` → `mypy
headroom`) was red on `main`
and therefore on every open PR, for two unrelated reasons that the early
ruff failure was masking:
1. **ruff**: the lint job installs `ruff` unpinned, and ruff 0.15.17
began enforcing import-block
sorting (`I001`) and formatting that older ruff accepted → `ruff check
.` / `ruff format --check .`
fail on files nobody touched.
2. **mypy**: `headroom/providers/opencode/config.py` had two `return
json.loads(...)` statements in
a function declared `-> dict[str, Any]`; `json.loads` is typed `Any`, so
`mypy headroom` fails
with `no-any-return` (reproduced on mypy 1.20.2 — not a version-specific
quirk).
This restores a green lint baseline and pins both linters so a future
release can't silently break
CI again.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `.github/workflows/ci.yml`: pin `ruff==0.15.17` and `mypy==1.20.2` in
the lint job.
- Applied `ruff check --fix .` (6 `I001` import-sort fixes) and `ruff
format .` (10 files) across
the repo — import ordering and whitespace only, no behavior change.
- `headroom/providers/opencode/config.py`: narrow both
`_parse_json_loose` return sites with an
`isinstance(parsed, dict)` guard, so the `dict[str, Any]` annotation is
true at runtime
(non-dict JSON falls back to `{}`) and mypy's `no-any-return` is
resolved.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`, `ruff format --check .`, `mypy
headroom --ignore-missing-imports`)
### Test Output
```text
$ python -m ruff check .
All checks passed!
$ python -m ruff format --check .
913 files already formatted
$ python -m mypy headroom/providers/opencode/config.py --ignore-missing-imports
Success: no issues found in 1 source file
$ python -m pytest tests/test_providers_opencode_config.py -q
37 passed
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.13, ruff 0.15.17, mypy 1.20.2,
branch ci/fix-ruff-lint off
headroomlabs-ai/main
- Exact command / steps: reproduced the red lint (latest ruff: 6 `I001`
+ 10 unformatted files;
the mypy failure was read from the #1295 CI lint log —
`config.py:125,133 no-any-return`, and
re-confirmed locally on mypy 1.20.2). Applied the ruff auto-fix/format,
added the dict guard,
pinned both linters, and re-ran each lint step.
- Observed result: `ruff check .` → "All checks passed!"; `ruff format
--check .` → "913 files
already formatted"; `mypy` on the fixed file → "Success: no issues
found"; full `mypy headroom`
reports only Unix `fcntl` attributes that don't exist on this Windows
box (present on the Linux
CI runner, where the prior run showed exactly the two now-fixed errors).
37 opencode-config
tests pass.
- Not tested: did not run the full OS/Python test matrix — the change is
formatting + two CI
dependency pins + a two-line type-narrowing guard, with no runtime
behavior change for dict JSON.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
This PR implements transparent `headroom wrap opencode` support without
asking users to edit OpenCode provider URLs, choose an extra CLI flag,
or maintain a static provider list.
The wrapper now lives at the runtime transport boundary: OpenCode keeps
its user/provider config, while Headroom intercepts outbound provider
traffic in-process and routes it through the local Headroom proxy.
## What changed
### Transparent OpenCode wrapping
- `headroom wrap opencode` injects the `headroom-opencode` plugin
through `OPENCODE_CONFIG_CONTENT`.
- Existing OpenCode provider URLs are preserved. We do not rewrite user
config URLs to point at Headroom.
- Existing `OPENAI_BASE_URL` and `ANTHROPIC_BASE_URL` env vars are
preserved.
- Local OpenCode traffic, localhost traffic, and Headroom proxy traffic
bypass the shim to avoid loops.
### Runtime transport interception
- Added an OpenCode plugin transport shim that wraps:
- `globalThis.fetch`
- `http.request` / `http.get`
- `https.request` / `https.get`
- External provider calls are routed to the local Headroom proxy.
- The original upstream origin is passed through `x-headroom-base-url`,
so the proxy can forward to the real provider without changing OpenCode
config.
- External `http2.connect` is blocked loudly instead of allowing direct
provider traffic to leak outside Headroom.
### Live provider additions
Provider coverage is no longer based on a static config scan. Because
routing happens at outbound request time, providers added mid-session
are routed through Headroom automatically as long as they use the
covered Node transport paths.
### Subagent and child-process coverage
- The parent OpenCode plugin sets a packaged Node preload shim through
`NODE_OPTIONS=--import=.../hook-shim/handler.js`.
- The transport shim patches `child_process.spawn`, `exec`, `execFile`,
and `fork` so child Node processes receive the Headroom preload even
when OpenCode passes a custom `env`.
- The child-process shim fails closed if it loads without
`HEADROOM_OPENCODE_TRANSPORT_PROXY_URL`.
- This closes the subagent leak path where a child Node process could
otherwise start without Headroom transport interception.
## Why this goes beyond PR #1089
PR #1089 improves OpenCode provider registration, but it still focuses
on provider config shape. This PR moves the enforcement boundary to
runtime transport interception.
This PR goes further because:
- No provider URL rewriting is required.
- New providers added mid-session are covered automatically.
- Subagents and child Node processes inherit the Headroom transport
shim.
- Direct external HTTP/2 paths fail loudly instead of leaking.
- The wrap remains transparent to the user's OpenCode provider config.
- The wrapper is fail-closed for unsupported child-process preload
state.
## Additional robustness fixes
While validating the change in Docker, the full Python suite exposed
unrelated Linux/container robustness issues. These are fixed in this PR
so the suite is green:
- Binary cache handling now treats cache paths under a non-writable
existing parent as unavailable, including when tests run as root in
Docker.
- `release_version.py` honors `MANUAL_VER` before git calls so direct
script execution works outside a `.git` checkout.
- Test logger isolation now resets relevant Headroom child loggers so
proxy logging setup cannot poison later `caplog` tests.
- The scanner missing-path test now uses a guaranteed missing `tmp_path`
child instead of relying on `/nonexistent/path`.
## Validation
All implementation validation was run inside Docker.
- Full Python suite from a fresh Docker copy: `6605 passed, 523
skipped`.
- Ruff on changed Python/OpenCode paths: passed.
- OpenCode plugin typecheck: passed.
- OpenCode plugin tests: `9 passed`.
- OpenCode plugin build: passed.
- Hook shim preload smoke test: passed.
## Notes
This PR intentionally does not add a CLI option. `headroom wrap
opencode` means full wrap. Either Headroom wraps OpenCode transparently,
or the path fails loudly instead of silently leaking provider traffic.
---------
Co-authored-by: Rudimar Ronsoni <6081613+rudironsoni@users.noreply.github.com>
## 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.
## Description
Claude Code disables on-demand tool loading (Tool Search) when
`ANTHROPIC_BASE_URL` is a custom host and `ENABLE_TOOL_SEARCH` is unset,
materializing all MCP/system tool schemas into its context window
(#746). With many MCP servers this overflows the window — breaking
sub-agent spawns ("prompt too long, ~214k > 200k") and forcing constant
compaction. `headroom wrap claude` already sets it; `init`/install did
not. Refs #746.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- Keep tool deferral on at both entry points, sharing one
`TOOL_SEARCH_ENV` / `TOOL_SEARCH_DEFAULT` constant from the Claude
provider package (`providers/claude/runtime.py`) so the key/default
can't drift:
- `init` (`_ensure_claude_hooks`): sets `ENABLE_TOOL_SEARCH=true` via
`setdefault`, respecting a pre-existing user-provided value.
- `install` (`build_install_env`): always writes
`ENABLE_TOOL_SEARCH=true`. This is the headroom-managed install env
(recorded and reverted on uninstall), so it is authoritative rather than
deferring to an existing value.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ pytest tests/test_cli/test_init_enable_tool_search.py -q
3 passed in 0.63s
```
## Real Behavior Proof
- Environment: Python 3.14, headroom proxy on :8799, ~19 MCP servers
connected
- Exact command / steps: launched `claude` through the proxy with vs
without `ENABLE_TOOL_SEARCH=true`, asked each to spawn 5 parallel
sub-agents
- Observed result: without it, all 5 sub-agents fail ("prompt too long,
~214k > 200k"); with `ENABLE_TOOL_SEARCH=true` all 5 succeed and traffic
compresses
- Not tested: non-Claude-Code agents
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Description
Codex's subscription usage window (primary/secondary rate-limit gauges)
stopped populating for ChatGPT-OAuth sessions. This PR restores it by
polling Codex's dedicated usage endpoint instead of relying on response
headers that are no longer sent.
### Why the previous approach no longer works
The existing code populates `CodexRateLimitState` from `x-codex-*`
rate-limit headers captured on the `/v1/responses` WebSocket handshake
(`update_from_headers` at WS accept). That worked when OpenAI returned
`x-codex-primary-used-percent`, `x-codex-primary-window-minutes`, etc.
on the handshake response.
OpenAI has since stopped sending those headers on the ChatGPT WebSocket
handshake. I confirmed this by faithfully replaying a real Plus-account
handshake (both `prewarm` and regular `request_kind`): no `x-codex-*`
headers come back on either. This matches OpenAI's own move to a
dedicated usage endpoint (`GET /backend-api/codex/usage` in CodexApi
mode) and reports such as openai/codex#14728. So `update_from_headers`
now runs on every accept but finds nothing to parse, and the window
silently stays empty.
The headers aren't coming back, so there is nothing to fix in the
parsing path. The data now lives behind a request we have to make
ourselves.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
## Changes Made
- `subscription/codex_rate_limits.py`:
- `parse_codex_usage_payload()` / `update_from_usage_payload()` — map
the `GET /backend-api/wham/usage` JSON (`rate_limit.primary_window` /
`secondary_window` with `used_percent`, `limit_window_seconds`,
`reset_at`; `credits`; `rate_limit_reached_type`) into the existing
`CodexRateLimitState`. `limit_window_seconds` is converted to
window-minutes with the same round-up codex-rs uses (`(secs + 59) //
60`).
- `maybe_schedule_usage_poll()` — fire-and-forget, throttled to one
request per 60s, scoped to ChatGPT sessions (requires both a Bearer
token and `ChatGPT-Account-Id`; API-key traffic is skipped). Uses an
in-flight guard so concurrent accepts don't stack polls. Endpoint is
overridable via `HEADROOM_CODEX_USAGE_URL`.
- `proxy/handlers/openai.py`:
- At the Codex WS accept site, after the now-usually-empty
`update_from_headers` block, schedule the usage poll. Wrapped in
`contextlib.suppress` and fully non-blocking so it can never delay or
fail the WebSocket accept.
The old header-capture path is intentionally left in place as a no-cost
fallback in case OpenAI restores the headers.
## 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 (live `/wham/usage` replay against a Plus
account returned HTTP 200 with the expected schema; payload fixture in
tests mirrors that real shape)
## Test Output
```
$ uv run pytest tests/test_codex_rate_limits.py -q
........................................ [100%]
41 passed in 0.16s
$ uv run ruff check headroom/subscription/codex_rate_limits.py headroom/proxy/handlers/openai.py tests/test_codex_rate_limits.py
All checks passed!
$ uv run mypy headroom/subscription/codex_rate_limits.py
Success: no issues found in 1 source file
```
## Additional Notes
- New tests cover: full-payload mapping, window-minutes round-up,
credits balance kept only when `has_credits`, promo object vs string,
empty payload returns `None`, missing `used_percent` skipped,
header-gating (requires Bearer + account-id), poll throttling, and
no-event-loop safety.
- Scoping to `ChatGPT-Account-Id` keeps the poll off API-key traffic,
and the 60s throttle plus in-flight guard bound it to at most one
lightweight GET per minute per running proxy.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Description
Prevent `headroom init` hooks from spawning duplicate persistent-task
runners while a proxy is still starting.
Fixes#615
## 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)
## Problem
`_ensure_profile_running()` checked readiness for only one second and
then launched `start_detached_agent()` whenever the proxy was not ready
yet. When Claude/Codex hooks fired close together, each hook could race
through that path and spawn another detached persistent-task runner.
## Changes Made
- Add a profile-local, nonblocking runtime start lock around init hook
startup.
- Re-check readiness after acquiring the lock so late-arriving hooks do
not start a duplicate runner.
- If a runtime is already alive, wait up to 15 seconds for readiness
before stopping and restarting it.
- Add regression tests for lock contention, slow startup, and
cross-process lock behavior.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
## Test Output
```
UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy pytest tests/test_cli/test_init_cli.py tests/test_cli/test_install_cli.py tests/test_cli/test_wrap_persistent.py tests/test_install/test_runtime.py
# 89 passed in 0.61s
UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy ruff check .
# All checks passed!
UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy ruff format --check .
# 775 files already formatted
UV_SKIP_WHEEL_FILENAME_CHECK=1 uv run --frozen --extra dev --extra proxy mypy headroom --ignore-missing-imports
# Success: no issues found in 346 source files
```
Manual sandbox check:
```
# before this change: 3 ensure calls spawned 3 detached starts
# after this change: 3 ensure calls spawned 1 detached start while the runtime was still starting
```
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Additional Notes
Docs and CHANGELOG were left unchanged because this is a small runtime
bug fix with no user-facing CLI/API change.
Three files modified in the previous commit (4071d57) needed ruff
format reformatting per CI's `ruff format --check .` step.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Restore build_provider_section() to headroom/providers/codex/install.py
without requires_openai_auth (was removed entirely; pre-existing test
test_provider_codex_install.py imports it and would fail to collect)
- Flip test_codex_provider_section_preserves_openai_oauth to assert
requires_openai_auth is ABSENT, not present (old behavior was wrong)
- Fix test_provider_codex_runtime.py:337 same way — init config must
NOT contain requires_openai_auth
- Fix Ruff B023 lint error in test_providers.py:492 — capture loop
variable config_path in lambda default arg (_p=config_path)
- Fix e2e/init/run.py _verify_codex_local and _verify_codex_global to
assert requires_openai_auth is absent, not present
- Fix e2e/wrap/run.py verify_codex_wrap same way
All unit tests pass locally (82 affected tests green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per issue #393, env_key = "OPENAI_API_KEY" breaks ChatGPT subscription
users who don't have OPENAI_API_KEY set. Remove it from wrap, init, and
persistent install entry points. The openai_base_url top-level injection
handles subscription routing without requiring env_key.
Bug 3 fix is now consistent across all three Codex entry points.
Subscription (ChatGPT plan) users will always have their traffic routed
through headroom regardless of whether they reached Codex config via
`headroom wrap codex`, `headroom init codex`, or the persistent-install
provider scope — all three now write `openai_base_url` at the TOML
top-level (outside any `[model_providers.*]` block) so Codex's built-in
openai provider is intercepted even when subscription auth bypasses the
`model_provider = "headroom"` selection.
Changes:
- headroom/cli/init.py: add `openai_base_url` line to `_ensure_codex_provider`
block; add `_strip_codex_init_block` helper with orphan-key cleanup
(mirrors `_strip_codex_headroom_blocks` in wrap.py)
- headroom/providers/codex/install.py: add `openai_base_url` line to
`apply_provider_scope` section; add orphan-cleanup regexes and apply
them in `revert_provider_scope` to handle crash-recovery scenarios
- tests/test_install/test_providers.py: add
`test_apply_provider_scope_writes_openai_base_url`,
`test_persistent_install_strip_removes_openai_base_url`
- tests/test_cli/test_init_cli.py: add
`test_init_codex_writes_openai_base_url`,
`test_init_codex_strip_removes_openai_base_url`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug 3 (#406) has two halves:
1. Strip requires_openai_auth from all three headroom provider block
emission sites — done in 3ca48d3. This prevented custom-provider traffic
from triggering OpenAI OAuth login prompts.
2. Inject openai_base_url at the top level of ~/.codex/config.toml — this
commit. Without this key, Codex subscription (ChatGPT plan) users bypass
headroom entirely: Codex detects subscription auth and routes through the
built-in openai provider using chatgpt.com/backend-api/codex as the base
URL, ignoring both OPENAI_BASE_URL env var and model_provider = "headroom".
Setting openai_base_url in config.toml overrides that default so both
API-key and subscription traffic flow through the proxy.
Changes:
- headroom/cli/wrap.py: add openai_base_url = "http://127.0.0.1:{port}/v1"
to the top-level marker block in _inject_codex_provider_config; add orphan
cleanup regex for openai_base_url in _strip_codex_headroom_blocks (handles
crash/migration residue).
- tests/test_install/test_providers.py: invert
test_inject_codex_provider_config_does_not_write_openai_base_url →
test_inject_codex_provider_config_writes_openai_base_url (asserts exact
value "http://127.0.0.1:8787/v1" so port drift causes failure); add
test_unwrap_removes_top_level_openai_base_url covering both the
backup-restore path and the _strip_codex_headroom_blocks orphan-cleanup path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add two deterministic, no-network regression tests to prevent bug 3 from
silently re-appearing:
- test_headroom_provider_block_never_sets_requires_openai_auth: calls
apply_provider_scope() directly with multiple ports and asserts the
rendered TOML never contains requires_openai_auth anywhere in the
headroom provider block.
- test_inject_codex_provider_config_does_not_write_openai_base_url:
calls _inject_codex_provider_config(8787) against a tmp_path-based
home dir (via monkeypatched HOME/USERPROFILE) and asserts openai_base_url
is absent at the top level, and requires_openai_auth is absent in the
injected provider block.
Both tests fail loudly with descriptive messages if either field
re-appears after a future change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove `requires_openai_auth = true` from all three sites that emit the
`[model_providers.headroom]` block: `headroom/providers/codex/install.py`
(persistent install), `headroom/cli/wrap.py` (_inject_codex_provider_config),
and `headroom/cli/init.py` (_ensure_codex_provider).
The field belongs only on the built-in `openai` provider where codex
hardcodes it. Setting it on a custom local-proxy provider forces codex
to demand OpenAI OAuth login for every headroom-routed request.
Top-level `openai_base_url` injection was audited — it was never written
to config.toml by the current codebase, only referenced in comments and
env-var routing logic. No change needed there.
Update test_apply_codex_provider_scope_replaces_existing_managed_block to
assert the replacement block no longer carries requires_openai_auth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Preserve Codex OAuth-safe provider config across init, wrap, and
persistent install paths, and strengthen coverage so Codex requests
are proven to reach Headroom and the mock upstream.
The wrap e2e now sends a real chat-completions probe and checks
Headroom /stats. Runtime tests cover temporary launch env, install
env, init config, provider-scope config delivery, and the Python
3.11 ws bootstrap path.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Phase B step 1 of the live-zone-only realignment. Removes ~10K LOC of
"drop messages from history" machinery that became unreachable after
PR-A1 made `/v1/messages` a passthrough on the proxy. Live-zone-only
compression (PR-B2..B7) operates on content blocks within messages;
message-list mutation no longer happens in the pipeline.
Python deletes:
- headroom/transforms/intelligent_context.py (1077 LOC)
- headroom/transforms/rolling_window.py (395 LOC)
- headroom/transforms/progressive_summarizer.py (508 LOC)
- headroom/transforms/scoring.py (459 LOC)
- headroom/transforms/tool_crusher.py (338 LOC)
- 5 corresponding tests/test_transforms/* and tests/test_proxy_intelligent_context.py
Rust deletes:
- crates/headroom-core/src/context/* (manager, config, workspace,
candidate, ccr_drop, strategy/, mod) + safety.rs replaced
- crates/headroom-core/src/scoring/* (mod, score, scorer, traits, weights)
- MessageScorerComparator from crates/headroom-parity (PR #338/#343
becomes deletable; sunk cost stays sunk)
- 13 message_scorer fixtures + record_message_scorer.py
Rust adds (move + rewrite):
- crates/headroom-core/src/transforms/safety.rs — `tool_pair_indices`
preserves the OpenAI/Anthropic tool_use ↔ tool_result pairing rule
the live-zone dispatcher (PR-B2) needs. No IcmConfig dependency.
Surface refactors:
- HeadroomConfig: drop `tool_crusher`, `rolling_window`,
`intelligent_context` fields; hoist `output_buffer_tokens` to top
level (used by client.py).
- ProxyConfig: drop `intelligent_context*` fields.
- `headroom wrap` proxy server: retire IntelligentContextManager
and RollingWindow imports + branch; pipeline is CacheAligner →
ContentRouter (smart_routing) or CacheAligner → SmartCrusher
(legacy).
- CLI: drop `--no-intelligent-context`, `--no-intelligent-scoring`,
`--no-compress-first` flags.
- LangChain memory integration: rename `_apply_rolling_window` →
`_apply_compression`, drop RollingWindowConfig dep. Threshold is
now advisory — B6 will rework the contract.
- TransformPipeline.create_pipeline now takes only cache_aligner_config.
- headroom/__init__.py + headroom/transforms/__init__.py: strip
exports of deleted symbols.
Bug fixes uncovered by full pytest sweep:
- providers/copilot/wrap.py: `environ or os.environ` collapsed
empty-dict to falsy → callers passing `environ={}` accidentally
pulled from os.environ. Use `environ if environ is not None else
os.environ`.
Test correctness fixes:
- _DummyAnthropicHandler._retry_request gains **_kwargs to match
the real handler signature post-A8.
- test_ws_http_fallback extracts JSON from `content=` (post-A3
byte-faithful) rather than the obsolete `json=` kwarg.
- test_ccr_response_handler_extra fixture joins SSE events with
`\n\n` per spec (post-A8 byte-buffer parser requirement).
- test_proxy_responses_phase_preservation: capture via direct
handler attached to the named logger, so the assertion is
order-independent (proxy `_setup_file_logging` flips
`headroom.propagate=False` once any earlier test triggers it).
- conftest.py autouse fixture resets `headroom.propagate=True`
before each test as a defensive measure for the same pollution.
- test_wrap_copilot_translated_backend_still_requires_byok:
monkeypatch.delenv every provider key so the BYOK error
actually fires.
- test_native_installers: skip when system bash < 4.3 (macOS ships 3.2).
- TestGeminiEmbedContent / TestGeminiBatchEmbedContents:
pytest.mark.skip — proxy currently has no :embedContent route;
feature gap, not regression.
Acceptance:
- cargo build --workspace + cargo clippy + cargo fmt --check: green.
- cargo test --workspace --exclude headroom-py: 777 passed.
- pytest: 4892 passed, 240 skipped, 0 failed.
- git grep returns only intentional comments referencing the deletion.
Per-PR-B1 plan: REALIGNMENT/04-phase-B-live-zone.md.
`.gitattributes` declares `*.py text eol=lf` and `*.sh text eol=lf`, but
74 files (73 .py, 1 .sh) are stored in the index with CRLF line endings,
violating that contract. Every macOS/Linux clone reports these files as
"modified" on fresh checkout because git's diff engine sees the stored
bytes don't match the attribute contract, even though the working tree
and index match byte-for-byte.
Running `git add --renormalize .` rewrites each affected blob so the
stored form matches the attribute declaration. No semantic changes —
every affected file's diff is "N insertions, N deletions" with inserts
and deletes being the same lines modulo line endings.
Follow-up commit adds `.git-blame-ignore-revs` so `git blame` / GitHub
blame skip this mechanical commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add focused regression coverage for install, runtime, provider, state, health, supervisor, and persistent wrap flows so the new persistent deployment surfaces are exercised more thoroughly in CI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align Docker-native wrapper help and runtime behavior with the Python install contract, including persistent deployment metadata, baked install-image defaults, and explicit unsupported wrap targets.
Harden the Python persistent-install path with profile validation, safer provider-scope handling, Windows environment restoration, runtime parity improvements, and rollback-safe apply/update behavior.
Update README, Docker install docs, CI, and focused regressions to cover the Windows BOM failure, wrapper parity, compose coverage, and Docker-native wrap behavior.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Tighten Docker-native bash and PowerShell wrapper validation for wrap and proxy flows, pin the bash wrapper to the install-time interpreter, clean up failed persistent container starts, and extend docs, CI, e2e, and native installer coverage for persistent Docker installs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>