fix(docker): persist headroom workspace in compose (#1839)
## Description
Pin the top-level Docker Compose proxy service to Headroom's canonical
writable workspace under the existing `headroom_workspace` named volume.
Closes #1835
The dashboard's durable savings/history data is loaded from
`proxy_savings.json` via `HEADROOM_WORKSPACE_DIR`; logs, session stats,
TOIN, config, and default workspace state are also derived from that
root. The top-level compose file already mounted
`/home/nonroot/.headroom`, but it relied on image/user home resolution
instead of exporting the canonical workspace env. This makes the
official compose contract explicit and matches the Docker-native
compose/runtime path behavior.
## 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
- Set `HOME=/home/nonroot` for the top-level compose proxy service.
- Set `HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom` and
`HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config` so dashboard
savings/history, logs, config, memory state, session stats, and TOIN
resolve into the persisted named volume.
- Added a regression test that locks the top-level compose persistence
wiring.
## Testing
- [x] Unit tests pass (`pytest`) — focused local tests and full CI test
matrix passed
- [x] Linting passes (`ruff check .`) — local Ruff and CI lint passed
- [x] Type checking passes (`mypy headroom`) — local mypy and CI lint
passed
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ rtk pytest tests/test_docker_compose_persistence.py
Pytest: 1 passed
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py
Pytest: 76 passed
$ rtk uvx ruff check tests/test_docker_compose_persistence.py
All checks passed!
$ rtk docker compose config
services:
headroom-proxy:
environment:
HEADROOM_CONFIG_DIR: /home/nonroot/.headroom/config
HEADROOM_HOST: 0.0.0.0
HEADROOM_WORKSPACE_DIR: /home/nonroot/.headroom
HOME: /home/nonroot
volumes:
- type: volume
source: headroom_workspace
target: /home/nonroot/.headroom
```
Attempted broader proxy stats-history coverage, but this local checkout
does not have the native extension built:
```text
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py tests/test_proxy_savings_history.py::test_stats_history_persists_across_restarts_and_stats_stays_compatible
ModuleNotFoundError: No module named 'headroom._core'
```
Attempted project-managed Ruff, but `uv run` tried to build the editable
package first and hit the known local native build issue before Ruff
could execute:
```text
$ rtk uv run ruff check tests/test_docker_compose_persistence.py
error: failed to run custom build command for `esaxx-rs v0.1.10`
fatal error: 'cstdint' file not found
```
## Real Behavior Proof
- Environment: local clean clone at current upstream `main`, branch
`fix/1835-docker-compose-persistence`.
- Exact command / steps: `rtk docker compose config` from the repo root.
- Observed result: Compose renders `HOME`, `HEADROOM_WORKSPACE_DIR`, and
`HEADROOM_CONFIG_DIR` under `/home/nonroot/.headroom`, and the
`headroom_workspace` named volume targets that same path.
- Not tested: full Docker image build or live `docker compose up`
restart cycle; full pytest/mypy not run locally because this checkout
lacks the built `headroom._core` extension.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- All non-skipped GitHub Actions checks are green after the rebase onto
`main`; skipped jobs are path-gated.
- The dashboard's recent request table is still an in-memory tail and is
expected to be empty after a proxy restart. This PR targets durable
dashboard savings/history and other workspace-backed files.
- `HEADROOM_LOG_FILE=/home/nonroot/.headroom/requests.jsonl` remains an
optional operator setting; persisted request JSONL is not replayed into
the dashboard after restart.
- The docs/CHANGELOG checklist items are N/A for this narrow compose
configuration fix.
2026-07-06 10:35:06 -05:00
|
|
|
"""Regression checks for Docker Compose persistence wiring."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## 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 (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
import yaml
|
|
|
|
|
|
fix(docker): persist headroom workspace in compose (#1839)
## Description
Pin the top-level Docker Compose proxy service to Headroom's canonical
writable workspace under the existing `headroom_workspace` named volume.
Closes #1835
The dashboard's durable savings/history data is loaded from
`proxy_savings.json` via `HEADROOM_WORKSPACE_DIR`; logs, session stats,
TOIN, config, and default workspace state are also derived from that
root. The top-level compose file already mounted
`/home/nonroot/.headroom`, but it relied on image/user home resolution
instead of exporting the canonical workspace env. This makes the
official compose contract explicit and matches the Docker-native
compose/runtime path behavior.
## 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
- Set `HOME=/home/nonroot` for the top-level compose proxy service.
- Set `HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom` and
`HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config` so dashboard
savings/history, logs, config, memory state, session stats, and TOIN
resolve into the persisted named volume.
- Added a regression test that locks the top-level compose persistence
wiring.
## Testing
- [x] Unit tests pass (`pytest`) — focused local tests and full CI test
matrix passed
- [x] Linting passes (`ruff check .`) — local Ruff and CI lint passed
- [x] Type checking passes (`mypy headroom`) — local mypy and CI lint
passed
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ rtk pytest tests/test_docker_compose_persistence.py
Pytest: 1 passed
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py
Pytest: 76 passed
$ rtk uvx ruff check tests/test_docker_compose_persistence.py
All checks passed!
$ rtk docker compose config
services:
headroom-proxy:
environment:
HEADROOM_CONFIG_DIR: /home/nonroot/.headroom/config
HEADROOM_HOST: 0.0.0.0
HEADROOM_WORKSPACE_DIR: /home/nonroot/.headroom
HOME: /home/nonroot
volumes:
- type: volume
source: headroom_workspace
target: /home/nonroot/.headroom
```
Attempted broader proxy stats-history coverage, but this local checkout
does not have the native extension built:
```text
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py tests/test_proxy_savings_history.py::test_stats_history_persists_across_restarts_and_stats_stays_compatible
ModuleNotFoundError: No module named 'headroom._core'
```
Attempted project-managed Ruff, but `uv run` tried to build the editable
package first and hit the known local native build issue before Ruff
could execute:
```text
$ rtk uv run ruff check tests/test_docker_compose_persistence.py
error: failed to run custom build command for `esaxx-rs v0.1.10`
fatal error: 'cstdint' file not found
```
## Real Behavior Proof
- Environment: local clean clone at current upstream `main`, branch
`fix/1835-docker-compose-persistence`.
- Exact command / steps: `rtk docker compose config` from the repo root.
- Observed result: Compose renders `HOME`, `HEADROOM_WORKSPACE_DIR`, and
`HEADROOM_CONFIG_DIR` under `/home/nonroot/.headroom`, and the
`headroom_workspace` named volume targets that same path.
- Not tested: full Docker image build or live `docker compose up`
restart cycle; full pytest/mypy not run locally because this checkout
lacks the built `headroom._core` extension.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- All non-skipped GitHub Actions checks are green after the rebase onto
`main`; skipped jobs are path-gated.
- The dashboard's recent request table is still an in-memory tail and is
expected to be empty after a proxy restart. This PR targets durable
dashboard savings/history and other workspace-backed files.
- `HEADROOM_LOG_FILE=/home/nonroot/.headroom/requests.jsonl` remains an
optional operator setting; persisted request JSONL is not replayed into
the dashboard after restart.
- The docs/CHANGELOG checklist items are N/A for this narrow compose
configuration fix.
2026-07-06 10:35:06 -05:00
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
|
|
|
|
|
fix(docker): publish compose ports on loopback only (#3061)
## Description
`docker compose up -d` published every service on `0.0.0.0`, and none of
the three authenticates an inbound caller by default:
| port | service | default auth |
|---|---|---|
| 8787 | proxy | `/v1/*` data plane open unless `HEADROOM_PROXY_TOKEN`
is set |
| 6333/6334 | Qdrant | **none at all** — holds embeddings derived from
prompts |
| 7474/7687 | Neo4j | `NEO4J_AUTH` falls back to `neo4j/devpassword`,
published in this file |
So the shipped default handed any peer on the surrounding network a
relay through the proxy plus direct read/write on the vector and graph
stores built from the operator's own prompt content. The proxy already
warns about exactly this shape at `headroom/proxy/server.py:3289` — the
compose file just never took its own advice.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
## Changes Made
- Pinned all five published ports to `127.0.0.1`.
- Documented in the file header how to expose the proxy deliberately,
pairing the port override with `HEADROOM_PROXY_TOKEN` rather than
leaving that implicit.
- Added a commented `HEADROOM_PROXY_TOKEN` entry to the proxy service
environment.
- Added a regression test asserting every published port names a
loopback host IP.
## Testing
- [x] Unit tests pass
- [x] Linting passes (ruff check + format)
- [ ] Type checking passes — N/A (YAML + test only)
- [x] New tests added for new functionality
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_docker_compose_persistence.py -q
3 passed in 0.14s
$ docker compose -f docker-compose.yml config # validates
headroom-proxy host_ip=127.0.0.1 published=8787 -> 8787
neo4j host_ip=127.0.0.1 published=7474 -> 7474
neo4j host_ip=127.0.0.1 published=7687 -> 7687
qdrant host_ip=127.0.0.1 published=6333 -> 6333
qdrant host_ip=127.0.0.1 published=6334 -> 6334
```
Against the parent commit:
```text
FAILED test_top_level_compose_publishes_only_to_loopback
E AssertionError: headroom-proxy: port '8787:8787' publishes on all interfaces
```
## Real Behavior Proof
- Environment: macOS 15 (darwin 25.4.0), Docker Compose v2 available
locally.
- Exact command / steps: `docker compose -f docker-compose.yml config
--format json` before and after, comparing the resolved `host_ip` on
every published port.
- Observed result: before, no port carried a `host_ip` (Docker binds
`0.0.0.0`); after, all five resolve to `host_ip=127.0.0.1`. The compose
file still validates.
- Not tested: bringing the stack up and probing the ports from a second
machine on the LAN — the assertion is made against Docker's own resolved
configuration rather than a live two-host network.
## Runtime Rollout Safety
- Rollout-managed feature(s): none.
- Minimum rollout channel: N/A.
- Stable/default behavior changed: yes — the compose stack is no longer
reachable from other machines by default.
- Kill switch / disable path: override `ports:` in a
`docker-compose.override.yml`; the header documents this and pairs it
with `HEADROOM_PROXY_TOKEN`.
- Unsafe override required: none.
- Qualification impact: none.
- Rollback path: revert this commit.
## 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 (the
compose header)
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective
- [x] New and existing unit tests pass locally with my changes
## Additional Notes
**This is a deliberate breaking change for one workflow**: anyone
reaching the compose proxy from another machine will need to override
`ports:`. That is exactly the configuration that was unsafe, so it
should break loudly rather than silently. `http://localhost:8787` from
the host is unchanged, the container still listens on `0.0.0.0`
internally, and service-to-service traffic on the compose network is
unaffected.
Scope note: I fixed all three services rather than only the proxy.
Closing 8787 while leaving an unauthenticated Qdrant and a
default-password Neo4j published on `0.0.0.0` would not have improved
the security posture.
Co-authored-by: Tejas Chopra <tejas@Tejass-MacBook-Pro.local>
2026-08-16 19:05:29 -07:00
|
|
|
def test_top_level_compose_publishes_only_to_loopback() -> None:
|
|
|
|
|
"""Every published port must name an explicit loopback host IP.
|
|
|
|
|
|
|
|
|
|
None of the three services authenticates by default: the proxy's /v1/*
|
|
|
|
|
data plane is open without HEADROOM_PROXY_TOKEN, Qdrant has no API key,
|
|
|
|
|
and NEO4J_AUTH falls back to a password published in the compose file. A
|
|
|
|
|
bare "8787:8787" binds 0.0.0.0 on the host, so `docker compose up -d` on a
|
|
|
|
|
shared network would expose all three.
|
|
|
|
|
"""
|
|
|
|
|
compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8"))
|
|
|
|
|
|
|
|
|
|
published = [
|
|
|
|
|
(service, port)
|
|
|
|
|
for service, spec in compose["services"].items()
|
|
|
|
|
for port in spec.get("ports", [])
|
|
|
|
|
]
|
|
|
|
|
assert published, "expected the compose file to publish at least one port"
|
|
|
|
|
|
|
|
|
|
for service, port in published:
|
|
|
|
|
# Short syntax is "HOST_IP:HOST_PORT:CONTAINER_PORT"; anything with
|
|
|
|
|
# fewer than three segments is published on every interface.
|
|
|
|
|
assert isinstance(port, str), f"{service}: expected short-syntax port, got {port!r}"
|
|
|
|
|
segments = port.split(":")
|
|
|
|
|
assert len(segments) == 3, f"{service}: port {port!r} publishes on all interfaces"
|
|
|
|
|
assert segments[0] in {"127.0.0.1", "::1"}, (
|
|
|
|
|
f"{service}: port {port!r} publishes on {segments[0]}, not loopback"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
fix(docker): persist headroom workspace in compose (#1839)
## Description
Pin the top-level Docker Compose proxy service to Headroom's canonical
writable workspace under the existing `headroom_workspace` named volume.
Closes #1835
The dashboard's durable savings/history data is loaded from
`proxy_savings.json` via `HEADROOM_WORKSPACE_DIR`; logs, session stats,
TOIN, config, and default workspace state are also derived from that
root. The top-level compose file already mounted
`/home/nonroot/.headroom`, but it relied on image/user home resolution
instead of exporting the canonical workspace env. This makes the
official compose contract explicit and matches the Docker-native
compose/runtime path behavior.
## 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
- Set `HOME=/home/nonroot` for the top-level compose proxy service.
- Set `HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom` and
`HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config` so dashboard
savings/history, logs, config, memory state, session stats, and TOIN
resolve into the persisted named volume.
- Added a regression test that locks the top-level compose persistence
wiring.
## Testing
- [x] Unit tests pass (`pytest`) — focused local tests and full CI test
matrix passed
- [x] Linting passes (`ruff check .`) — local Ruff and CI lint passed
- [x] Type checking passes (`mypy headroom`) — local mypy and CI lint
passed
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ rtk pytest tests/test_docker_compose_persistence.py
Pytest: 1 passed
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py
Pytest: 76 passed
$ rtk uvx ruff check tests/test_docker_compose_persistence.py
All checks passed!
$ rtk docker compose config
services:
headroom-proxy:
environment:
HEADROOM_CONFIG_DIR: /home/nonroot/.headroom/config
HEADROOM_HOST: 0.0.0.0
HEADROOM_WORKSPACE_DIR: /home/nonroot/.headroom
HOME: /home/nonroot
volumes:
- type: volume
source: headroom_workspace
target: /home/nonroot/.headroom
```
Attempted broader proxy stats-history coverage, but this local checkout
does not have the native extension built:
```text
$ rtk pytest tests/test_docker_compose_persistence.py tests/test_paths.py tests/test_proxy_savings_history.py::test_stats_history_persists_across_restarts_and_stats_stays_compatible
ModuleNotFoundError: No module named 'headroom._core'
```
Attempted project-managed Ruff, but `uv run` tried to build the editable
package first and hit the known local native build issue before Ruff
could execute:
```text
$ rtk uv run ruff check tests/test_docker_compose_persistence.py
error: failed to run custom build command for `esaxx-rs v0.1.10`
fatal error: 'cstdint' file not found
```
## Real Behavior Proof
- Environment: local clean clone at current upstream `main`, branch
`fix/1835-docker-compose-persistence`.
- Exact command / steps: `rtk docker compose config` from the repo root.
- Observed result: Compose renders `HOME`, `HEADROOM_WORKSPACE_DIR`, and
`HEADROOM_CONFIG_DIR` under `/home/nonroot/.headroom`, and the
`headroom_workspace` named volume targets that same path.
- Not tested: full Docker image build or live `docker compose up`
restart cycle; full pytest/mypy not run locally because this checkout
lacks the built `headroom._core` extension.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- All non-skipped GitHub Actions checks are green after the rebase onto
`main`; skipped jobs are path-gated.
- The dashboard's recent request table is still an in-memory tail and is
expected to be empty after a proxy restart. This PR targets durable
dashboard savings/history and other workspace-backed files.
- `HEADROOM_LOG_FILE=/home/nonroot/.headroom/requests.jsonl` remains an
optional operator setting; persisted request JSONL is not replayed into
the dashboard after restart.
- The docs/CHANGELOG checklist items are N/A for this narrow compose
configuration fix.
2026-07-06 10:35:06 -05:00
|
|
|
def test_top_level_compose_pins_headroom_state_to_named_volume() -> None:
|
|
|
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert "- headroom_workspace:/home/nonroot/.headroom" in compose
|
|
|
|
|
assert "- HOME=/home/nonroot" in compose
|
|
|
|
|
assert "- HEADROOM_WORKSPACE_DIR=/home/nonroot/.headroom" in compose
|
|
|
|
|
assert "- HEADROOM_CONFIG_DIR=/home/nonroot/.headroom/config" in compose
|
fix(docker): report source build version (#1862)
## Description
Closes #1858
Docker/Compose source builds could report stale or misleading version
information: the dashboard initially rendered a hardcoded `v0.3.0`, then
`/health` replaced it with installed package metadata, which can be
stale when building locally from `main` without release metadata in the
image.
This change makes source Docker Compose builds report an explicit
source-build identity, removes the stale dashboard fallback, and keeps
CLI/doctor version checks from treating source-build labels as
release-version drift.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Add `HEADROOM_VERSION` / `HEADROOM_BUILD_VERSION` runtime version
overrides and optional packaged `_build_info.py` metadata.
- Teach Docker Compose source builds to pass a `source-build` sentinel
that the Dockerfile expands to `source-build+g<sha>` when git metadata
is available, or `source-build+sha256.<digest>` otherwise.
- Keep release/published image builds on normal package metadata when
`HEADROOM_BUILD_VERSION` is unset.
- Include only minimal `.git` metadata in the Docker build context so
the source-build label can identify the checkout without copying git
objects.
- Treat source-build labels and raw hashes as non-release labels in
`wrap` and `doctor`, avoiding false stale-proxy restarts and drift
warnings.
- Replace the dashboard hardcoded `0.3.0` fallback with `loading` /
`unknown` and format non-release build labels without a `v` prefix.
- Include the runtime version in proxy startup logs, `/health`,
`/livez`, and OTEL service version reporting.
## Testing
- [x] Unit tests pass (`pytest` in GitHub CI)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
GitHub CI: all checks passing
- CI: build, build-wheel, lint, test shards, test-extras, test-agno, test-dashboard-ui
- Docker: docker-native-e2e, docker-wrap-e2e, docker-init-e2e
- Native wrappers: macOS, Windows, Ubuntu
- Security: CodeQL, gitleaks, pip-audit
- Governance: template, label, merge-conflicts, commitlint
$ HEADROOM_REQUIRE_RUST_CORE=false PYTHONPATH=/Users/vinaygupta/Desktop/git/headroom-fix-1858-version-mismatch pytest tests/test_package_init_lazy.py::test_version_prefers_explicit_build_env tests/test_package_init_lazy.py::test_version_label_helpers_only_prefix_release_versions tests/test_package_init_lazy.py::test_version_uses_packaged_build_metadata tests/test_package_init_lazy.py::test_observability_version_uses_runtime_version tests/test_docker_compose_persistence.py tests/test_cli_doctor.py::TestProxyLiveness::test_up_leaves_source_label_unprefixed tests/test_cli_doctor.py::TestVersionDrift::test_non_release_version_labels_skip_drift_comparison tests/test_cli/test_wrap_persistent.py::test_proxy_version_restart_ignores_non_release_source_labels tests/test_proxy_dashboard_stats_cache.py::test_dashboard_uses_cached_stats_and_lazy_history_feed_polling -q
13 passed, 1 warning
$ uvx ruff==0.15.17 check .
All checks passed!
$ uvx ruff==0.15.17 format --check .
1058 files already formatted
$ uvx mypy==1.20.2 headroom --ignore-missing-imports
Success: no issues found in 407 source files
$ git diff --check
# no output
$ docker compose config
# resolved headroom-proxy build args include HEADROOM_BUILD_VERSION: source-build
$ HEADROOM_BUILD_VERSION=6266a1d docker compose config
# explicit override is preserved as HEADROOM_BUILD_VERSION: 6266a1d
$ docker build --check --build-arg HEADROOM_BUILD_VERSION=source-build .
Check complete, no warnings found.
```
## Real Behavior Proof
- Environment: macOS local checkout, Python 3.13.5, Docker Desktop
builder `desktop-linux`, plus GitHub Actions CI.
- Exact command / steps: `docker compose config`,
`HEADROOM_BUILD_VERSION=6266a1d docker compose config`, and `docker
build --check --build-arg HEADROOM_BUILD_VERSION=source-build .`.
- Observed result: Compose defaults the top-level `headroom-proxy` build
arg to the `source-build` sentinel, preserves explicit overrides, and
Dockerfile syntax/check validation passes for the source-build path.
- Not tested: Full end-to-end release publishing flow; this PR only
changes local/source-build reporting.
- CI proof: GitHub Actions completed successfully across Docker E2E, CI
test shards, lint/type checks, native wrapper checks, security checks,
and PR governance.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally/CI with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
Docs and changelog are N/A for this runtime-reporting bug fix. The PR is
open and ready for review with all GitHub checks passing.
2026-07-08 13:32:04 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_top_level_compose_marks_source_build_version() -> None:
|
|
|
|
|
compose = (ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
|
|
|
|
dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
|
|
|
|
|
dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert "HEADROOM_BUILD_VERSION: ${HEADROOM_BUILD_VERSION:-source-build}" in compose
|
|
|
|
|
assert 'ARG HEADROOM_BUILD_VERSION=""' in dockerfile
|
|
|
|
|
assert "ARG PYTHON_SITE_PACKAGES" in dockerfile
|
|
|
|
|
assert "if not build_version:" in dockerfile
|
|
|
|
|
assert "source-build+g{revision}" in dockerfile
|
|
|
|
|
assert "source-build+sha256." in dockerfile
|
|
|
|
|
assert "_build_info.py" in dockerfile
|
|
|
|
|
assert "import headroom._version" not in dockerfile
|
|
|
|
|
assert ".git/*" in dockerignore
|
|
|
|
|
assert "!.git/HEAD" in dockerignore
|
|
|
|
|
assert "!.git/refs/**" in dockerignore
|