feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.
Layout
Cargo.toml (workspace) + rust-toolchain.toml
crates/headroom-core — transform library, stub only
crates/headroom-proxy — axum binary, /healthz only
crates/headroom-py — PyO3 cdylib, exposes headroom._core.hello()
crates/headroom-parity — Rust-vs-Python oracle harness + parity-run CLI
Tooling
Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
.github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
deny.toml for cargo-deny
Parity corpus
tests/parity/recorder.py + scripts/record_fixtures.py
125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
log_compressor, diff_compressor, cache_aligner)
Docs
RUST_DEV.md — developer setup and workspace reference
docs/spec/022-rust-migration.md — migration plan and stage breakdown
.gitignore: whitelist scripts/record_fixtures.py; ignore target/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00
|
|
|
# Headroom Rust build targets. `just` is not installed on dev boxes; this
|
|
|
|
|
# Makefile is the source of truth and is mirrored by .github/workflows/rust.yml.
|
|
|
|
|
|
|
|
|
|
SHELL := /bin/bash
|
|
|
|
|
CARGO ?= cargo
|
|
|
|
|
MATURIN ?= maturin
|
|
|
|
|
PYTHON ?= python3
|
|
|
|
|
FIXTURES ?= tests/parity/fixtures
|
|
|
|
|
|
fix: A0 — fail-loud rust core deployment smoke test
Production incident (Finding #2 of HEADROOM_PROXY_LOG_FINDINGS_2026_05_03.md):
on this customer's deployment the Rust extension `headroom._core` was
never installed into the runtime Docker image. Diff compression failed
54 times in a single day; "Optimization failed: ModuleNotFoundError" hit
379 times. The failure rate climbed every day and reached ~223/day on
2026-05-03 — effectively 100% of requests on the Rust path. Every Rust
PR we'd merged (MessageScorer, ICM, DiffCompressor, etc.) was providing
zero customer value because the module wasn't loadable at all.
Root cause: the Dockerfile builder stage installed Python deps and the
in-tree `headroom-ai` package but never ran `maturin build` for the
`headroom-py` crate, so the runtime image shipped without `_core.so`.
The Python proxy continued to start because the extension's absence is
caught and routed through Python-only fallbacks that either silently
no-op or raise per-request.
This change makes that mode impossible by default:
* `headroom.proxy.server._check_rust_core()` runs as the first step of
the FastAPI lifespan. If the import fails it prints a structured
diagnostic, logs `event=rust_core_missing`, and calls `sys.exit(78)`
(sysexits.h `EX_CONFIG`). Process supervisors (systemd / k8s /
docker) treat this as a deliberate config error and stop restart
loops.
* `HEADROOM_REQUIRE_RUST_CORE=false` is the explicit opt-out for
Python-only `pip install -e .` developer flows; lifespan logs
`event=rust_core_disabled` and continues. Any other value (including
unset) keeps the fail-loud default.
* `/health` now surfaces `rust_core: "loaded" | "disabled" | "missing"`
(plus `rust_core_error` when non-loaded) so operators can alert on
the degraded state rather than discovering it via a customer ticket.
* `scripts/build_rust_extension.sh` is the single dev-time path: build
→ install → import-verify with the same `hello()` marker the lifespan
checks. Failures are loud at every step.
* `Makefile` exposes the script as `make verify-rust-core`.
* `Dockerfile` now installs `rustup` + `maturin`, builds the wheel from
`crates/headroom-py`, force-installs it into site-packages, and runs
the same `hello()` import-verify in the build image so a broken build
fails the docker-build, not the next runtime restart.
Tests:
* `tests/test_rust_core_smoke.py` pins all four contracts:
- `_core.hello()` returns `"headroom-core"`
- missing extension + default env → `SystemExit(78)`
- missing extension + opt-out env → lifespan starts, `/health`
returns `rust_core: "disabled"` with the underlying error
- present extension + default env → `("loaded", None)`
Per-finding-#2: ~/Desktop/HEADROOM_PROXY_LOG_FINDINGS_2026_05_03.md.
2026-05-02 17:51:24 -07:00
|
|
|
.PHONY: help test test-parity bench build-proxy build-wheel fmt fmt-check lint clippy clean ci-precheck ci-precheck-rust ci-precheck-python ci-precheck-commitlint install-git-hooks verify-rust-core
|
feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.
Layout
Cargo.toml (workspace) + rust-toolchain.toml
crates/headroom-core — transform library, stub only
crates/headroom-proxy — axum binary, /healthz only
crates/headroom-py — PyO3 cdylib, exposes headroom._core.hello()
crates/headroom-parity — Rust-vs-Python oracle harness + parity-run CLI
Tooling
Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
.github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
deny.toml for cargo-deny
Parity corpus
tests/parity/recorder.py + scripts/record_fixtures.py
125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
log_compressor, diff_compressor, cache_aligner)
Docs
RUST_DEV.md — developer setup and workspace reference
docs/spec/022-rust-migration.md — migration plan and stage breakdown
.gitignore: whitelist scripts/record_fixtures.py; ignore target/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00
|
|
|
|
|
|
|
|
help:
|
|
|
|
|
@echo "Headroom Rust targets:"
|
ci: fix smart_crusher branch CI failures + add make ci-precheck pre-push gate
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
2026-04-27 11:13:47 -07:00
|
|
|
@echo " make test - cargo test --workspace"
|
2026-07-26 11:00:14 -07:00
|
|
|
@echo " make test-parity - parity-run against recorded fixtures"
|
ci: fix smart_crusher branch CI failures + add make ci-precheck pre-push gate
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
2026-04-27 11:13:47 -07:00
|
|
|
@echo " make bench - cargo bench --workspace"
|
|
|
|
|
@echo " make build-proxy - release build + strip headroom-proxy, print size"
|
|
|
|
|
@echo " make build-wheel - release wheel for headroom-py"
|
fix: A0 — fail-loud rust core deployment smoke test
Production incident (Finding #2 of HEADROOM_PROXY_LOG_FINDINGS_2026_05_03.md):
on this customer's deployment the Rust extension `headroom._core` was
never installed into the runtime Docker image. Diff compression failed
54 times in a single day; "Optimization failed: ModuleNotFoundError" hit
379 times. The failure rate climbed every day and reached ~223/day on
2026-05-03 — effectively 100% of requests on the Rust path. Every Rust
PR we'd merged (MessageScorer, ICM, DiffCompressor, etc.) was providing
zero customer value because the module wasn't loadable at all.
Root cause: the Dockerfile builder stage installed Python deps and the
in-tree `headroom-ai` package but never ran `maturin build` for the
`headroom-py` crate, so the runtime image shipped without `_core.so`.
The Python proxy continued to start because the extension's absence is
caught and routed through Python-only fallbacks that either silently
no-op or raise per-request.
This change makes that mode impossible by default:
* `headroom.proxy.server._check_rust_core()` runs as the first step of
the FastAPI lifespan. If the import fails it prints a structured
diagnostic, logs `event=rust_core_missing`, and calls `sys.exit(78)`
(sysexits.h `EX_CONFIG`). Process supervisors (systemd / k8s /
docker) treat this as a deliberate config error and stop restart
loops.
* `HEADROOM_REQUIRE_RUST_CORE=false` is the explicit opt-out for
Python-only `pip install -e .` developer flows; lifespan logs
`event=rust_core_disabled` and continues. Any other value (including
unset) keeps the fail-loud default.
* `/health` now surfaces `rust_core: "loaded" | "disabled" | "missing"`
(plus `rust_core_error` when non-loaded) so operators can alert on
the degraded state rather than discovering it via a customer ticket.
* `scripts/build_rust_extension.sh` is the single dev-time path: build
→ install → import-verify with the same `hello()` marker the lifespan
checks. Failures are loud at every step.
* `Makefile` exposes the script as `make verify-rust-core`.
* `Dockerfile` now installs `rustup` + `maturin`, builds the wheel from
`crates/headroom-py`, force-installs it into site-packages, and runs
the same `hello()` import-verify in the build image so a broken build
fails the docker-build, not the next runtime restart.
Tests:
* `tests/test_rust_core_smoke.py` pins all four contracts:
- `_core.hello()` returns `"headroom-core"`
- missing extension + default env → `SystemExit(78)`
- missing extension + opt-out env → lifespan starts, `/health`
returns `rust_core: "disabled"` with the underlying error
- present extension + default env → `("loaded", None)`
Per-finding-#2: ~/Desktop/HEADROOM_PROXY_LOG_FINDINGS_2026_05_03.md.
2026-05-02 17:51:24 -07:00
|
|
|
@echo " make verify-rust-core - build + install + import-verify headroom._core"
|
ci: fix smart_crusher branch CI failures + add make ci-precheck pre-push gate
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
2026-04-27 11:13:47 -07:00
|
|
|
@echo " make fmt - cargo fmt --all"
|
|
|
|
|
@echo " make fmt-check - cargo fmt --all -- --check"
|
|
|
|
|
@echo " make lint - cargo clippy --workspace -- -D warnings"
|
|
|
|
|
@echo " make clean - cargo clean"
|
|
|
|
|
@echo ""
|
feat: headroom wrap opencode / unwrap opencode CLI (#1105)
## 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>
2026-06-22 18:07:12 +02:00
|
|
|
@echo "E2e targets:"
|
|
|
|
|
@echo " make build-e2e-wrap - build the wrap-e2e Docker image"
|
|
|
|
|
@echo " make run-e2e-wrap - build + run the wrap-e2e Docker container"
|
|
|
|
|
@echo ""
|
ci: fix smart_crusher branch CI failures + add make ci-precheck pre-push gate
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
2026-04-27 11:13:47 -07:00
|
|
|
@echo "Pre-push verification (run BEFORE git push to catch CI failures locally):"
|
|
|
|
|
@echo " make ci-precheck - run all CI gates (rust + python + commitlint)"
|
|
|
|
|
@echo " make ci-precheck-rust - cargo fmt --check + clippy + test"
|
|
|
|
|
@echo " make ci-precheck-python - smart_crusher-affected python tests"
|
|
|
|
|
@echo " make ci-precheck-commitlint - lint commits since origin/main"
|
2026-06-12 17:11:39 -05:00
|
|
|
@echo " make install-git-hooks - install pre-commit, commit-msg, and pre-push hooks"
|
feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.
Layout
Cargo.toml (workspace) + rust-toolchain.toml
crates/headroom-core — transform library, stub only
crates/headroom-proxy — axum binary, /healthz only
crates/headroom-py — PyO3 cdylib, exposes headroom._core.hello()
crates/headroom-parity — Rust-vs-Python oracle harness + parity-run CLI
Tooling
Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
.github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
deny.toml for cargo-deny
Parity corpus
tests/parity/recorder.py + scripts/record_fixtures.py
125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
log_compressor, diff_compressor, cache_aligner)
Docs
RUST_DEV.md — developer setup and workspace reference
docs/spec/022-rust-migration.md — migration plan and stage breakdown
.gitignore: whitelist scripts/record_fixtures.py; ignore target/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00
|
|
|
|
|
|
|
|
test:
|
|
|
|
|
$(CARGO) test --workspace
|
|
|
|
|
|
2026-07-26 11:00:14 -07:00
|
|
|
# headroom-parity has no pyo3 dependency — its comparators call headroom-core
|
|
|
|
|
# directly, so this target needs neither a venv nor a built extension module.
|
|
|
|
|
# (See crates/headroom-parity/Cargo.toml: "Phase 0 does not invoke Python from
|
|
|
|
|
# Rust.") Dropping the `maturin develop` step keeps the harness runnable from a
|
|
|
|
|
# bare checkout and takes the Python toolchain off the CI parity job.
|
feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.
Layout
Cargo.toml (workspace) + rust-toolchain.toml
crates/headroom-core — transform library, stub only
crates/headroom-proxy — axum binary, /healthz only
crates/headroom-py — PyO3 cdylib, exposes headroom._core.hello()
crates/headroom-parity — Rust-vs-Python oracle harness + parity-run CLI
Tooling
Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
.github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
deny.toml for cargo-deny
Parity corpus
tests/parity/recorder.py + scripts/record_fixtures.py
125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
log_compressor, diff_compressor, cache_aligner)
Docs
RUST_DEV.md — developer setup and workspace reference
docs/spec/022-rust-migration.md — migration plan and stage breakdown
.gitignore: whitelist scripts/record_fixtures.py; ignore target/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00
|
|
|
test-parity:
|
|
|
|
|
$(CARGO) run -p headroom-parity -- run --fixtures $(FIXTURES)
|
|
|
|
|
|
|
|
|
|
bench:
|
|
|
|
|
$(CARGO) bench --workspace
|
|
|
|
|
|
|
|
|
|
build-proxy:
|
|
|
|
|
$(CARGO) build --release -p headroom-proxy
|
|
|
|
|
@BIN=target/release/headroom-proxy; \
|
|
|
|
|
if command -v strip >/dev/null 2>&1; then strip "$$BIN" || true; fi; \
|
|
|
|
|
SIZE=$$(wc -c < "$$BIN"); \
|
|
|
|
|
printf 'headroom-proxy: %s bytes (%.1f MiB)\n' "$$SIZE" "$$(echo "$$SIZE / 1048576" | bc -l)"
|
|
|
|
|
|
|
|
|
|
build-wheel:
|
2026-04-25 11:56:44 -07:00
|
|
|
$(MATURIN) build --release -m crates/headroom-py/Cargo.toml
|
feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.
Layout
Cargo.toml (workspace) + rust-toolchain.toml
crates/headroom-core — transform library, stub only
crates/headroom-proxy — axum binary, /healthz only
crates/headroom-py — PyO3 cdylib, exposes headroom._core.hello()
crates/headroom-parity — Rust-vs-Python oracle harness + parity-run CLI
Tooling
Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
.github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
deny.toml for cargo-deny
Parity corpus
tests/parity/recorder.py + scripts/record_fixtures.py
125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
log_compressor, diff_compressor, cache_aligner)
Docs
RUST_DEV.md — developer setup and workspace reference
docs/spec/022-rust-migration.md — migration plan and stage breakdown
.gitignore: whitelist scripts/record_fixtures.py; ignore target/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00
|
|
|
|
fix: A0 — fail-loud rust core deployment smoke test
Production incident (Finding #2 of HEADROOM_PROXY_LOG_FINDINGS_2026_05_03.md):
on this customer's deployment the Rust extension `headroom._core` was
never installed into the runtime Docker image. Diff compression failed
54 times in a single day; "Optimization failed: ModuleNotFoundError" hit
379 times. The failure rate climbed every day and reached ~223/day on
2026-05-03 — effectively 100% of requests on the Rust path. Every Rust
PR we'd merged (MessageScorer, ICM, DiffCompressor, etc.) was providing
zero customer value because the module wasn't loadable at all.
Root cause: the Dockerfile builder stage installed Python deps and the
in-tree `headroom-ai` package but never ran `maturin build` for the
`headroom-py` crate, so the runtime image shipped without `_core.so`.
The Python proxy continued to start because the extension's absence is
caught and routed through Python-only fallbacks that either silently
no-op or raise per-request.
This change makes that mode impossible by default:
* `headroom.proxy.server._check_rust_core()` runs as the first step of
the FastAPI lifespan. If the import fails it prints a structured
diagnostic, logs `event=rust_core_missing`, and calls `sys.exit(78)`
(sysexits.h `EX_CONFIG`). Process supervisors (systemd / k8s /
docker) treat this as a deliberate config error and stop restart
loops.
* `HEADROOM_REQUIRE_RUST_CORE=false` is the explicit opt-out for
Python-only `pip install -e .` developer flows; lifespan logs
`event=rust_core_disabled` and continues. Any other value (including
unset) keeps the fail-loud default.
* `/health` now surfaces `rust_core: "loaded" | "disabled" | "missing"`
(plus `rust_core_error` when non-loaded) so operators can alert on
the degraded state rather than discovering it via a customer ticket.
* `scripts/build_rust_extension.sh` is the single dev-time path: build
→ install → import-verify with the same `hello()` marker the lifespan
checks. Failures are loud at every step.
* `Makefile` exposes the script as `make verify-rust-core`.
* `Dockerfile` now installs `rustup` + `maturin`, builds the wheel from
`crates/headroom-py`, force-installs it into site-packages, and runs
the same `hello()` import-verify in the build image so a broken build
fails the docker-build, not the next runtime restart.
Tests:
* `tests/test_rust_core_smoke.py` pins all four contracts:
- `_core.hello()` returns `"headroom-core"`
- missing extension + default env → `SystemExit(78)`
- missing extension + opt-out env → lifespan starts, `/health`
returns `rust_core: "disabled"` with the underlying error
- present extension + default env → `("loaded", None)`
Per-finding-#2: ~/Desktop/HEADROOM_PROXY_LOG_FINDINGS_2026_05_03.md.
2026-05-02 17:51:24 -07:00
|
|
|
# Hotfix-A0: maturin-develop + symlink + import-verify in one shot. Run this
|
|
|
|
|
# any time you suspect the proxy is silently falling back to Python-only
|
|
|
|
|
# mode (Finding #2 in HEADROOM_PROXY_LOG_FINDINGS_2026_05_03.md). The
|
|
|
|
|
# proxy itself runs the same check at lifespan startup; this target
|
|
|
|
|
# exposes it as a developer-facing one-liner.
|
|
|
|
|
verify-rust-core:
|
|
|
|
|
@if [ -z "$$VIRTUAL_ENV" ]; then \
|
|
|
|
|
echo "error: activate a venv first (e.g. source .venv/bin/activate)"; \
|
|
|
|
|
exit 1; \
|
|
|
|
|
fi
|
|
|
|
|
bash scripts/build_rust_extension.sh
|
|
|
|
|
|
feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.
Layout
Cargo.toml (workspace) + rust-toolchain.toml
crates/headroom-core — transform library, stub only
crates/headroom-proxy — axum binary, /healthz only
crates/headroom-py — PyO3 cdylib, exposes headroom._core.hello()
crates/headroom-parity — Rust-vs-Python oracle harness + parity-run CLI
Tooling
Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
.github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
deny.toml for cargo-deny
Parity corpus
tests/parity/recorder.py + scripts/record_fixtures.py
125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
log_compressor, diff_compressor, cache_aligner)
Docs
RUST_DEV.md — developer setup and workspace reference
docs/spec/022-rust-migration.md — migration plan and stage breakdown
.gitignore: whitelist scripts/record_fixtures.py; ignore target/
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00
|
|
|
fmt:
|
|
|
|
|
$(CARGO) fmt --all
|
|
|
|
|
|
|
|
|
|
fmt-check:
|
|
|
|
|
$(CARGO) fmt --all -- --check
|
|
|
|
|
|
|
|
|
|
clippy lint:
|
|
|
|
|
$(CARGO) clippy --workspace -- -D warnings
|
|
|
|
|
|
|
|
|
|
clean:
|
|
|
|
|
$(CARGO) clean
|
ci: fix smart_crusher branch CI failures + add make ci-precheck pre-push gate
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
2026-04-27 11:13:47 -07:00
|
|
|
|
|
|
|
|
# ─── Pre-push CI gate ──────────────────────────────────────────────────────
|
|
|
|
|
#
|
|
|
|
|
# These targets run the same checks GitHub Actions runs, locally. The intent
|
|
|
|
|
# is: if `make ci-precheck` is green, `git push` will not turn red. The
|
|
|
|
|
# 2026-04-27 push surfaced five CI breaks (cargo fmt drift, x86_64-apple-
|
|
|
|
|
# darwin wheel, headroom._core not built in test-extras + smoke-test,
|
|
|
|
|
# commitlint footer-leading-blank). The first three are caught by the gates
|
|
|
|
|
# below; the last two are caught by the workflow fixes themselves.
|
|
|
|
|
#
|
|
|
|
|
# Run before EVERY `git push`. Install the git hook (one-time) with:
|
|
|
|
|
# make install-git-hooks
|
|
|
|
|
|
|
|
|
|
ci-precheck: ci-precheck-rust ci-precheck-python ci-precheck-commitlint
|
|
|
|
|
@echo ""
|
|
|
|
|
@echo "✅ ci-precheck PASSED — safe to push."
|
|
|
|
|
|
|
|
|
|
ci-precheck-rust:
|
|
|
|
|
@echo "── ci-precheck-rust ────────────────────────────────────────────"
|
|
|
|
|
$(CARGO) fmt --all -- --check
|
|
|
|
|
$(CARGO) clippy --workspace -- -D warnings
|
|
|
|
|
$(CARGO) test --workspace
|
|
|
|
|
|
|
|
|
|
# Mirrors the smart_crusher-affected test files we expect green on every
|
|
|
|
|
# push. Builds the Rust extension first because most of these tests
|
|
|
|
|
# instantiate `SmartCrusher`, which hard-imports `headroom._core`.
|
|
|
|
|
ci-precheck-python:
|
|
|
|
|
@echo "── ci-precheck-python ─────────────────────────────────────────"
|
|
|
|
|
@if [ -z "$$VIRTUAL_ENV" ]; then \
|
|
|
|
|
echo "error: activate a venv first (e.g. source .venv/bin/activate)"; \
|
|
|
|
|
exit 1; \
|
|
|
|
|
fi
|
|
|
|
|
bash scripts/build_rust_extension.sh
|
|
|
|
|
$(PYTHON) -m pytest -q \
|
|
|
|
|
tests/test_transforms/test_smart_crusher_bugs.py \
|
|
|
|
|
tests/test_transforms/test_smart_crusher_rust_parity.py \
|
|
|
|
|
tests/test_transforms/test_diff_compressor.py \
|
|
|
|
|
tests/test_transforms/test_diff_compressor_rust_parity.py \
|
|
|
|
|
tests/test_relevance.py \
|
|
|
|
|
tests/test_relevance_extra.py \
|
|
|
|
|
tests/test_ccr.py \
|
|
|
|
|
tests/test_acceptance.py \
|
|
|
|
|
tests/test_critical_fixes.py \
|
|
|
|
|
tests/test_quality_retention.py \
|
|
|
|
|
tests/test_toin_integration.py
|
|
|
|
|
|
|
|
|
|
# Lint commits since `origin/main`. Requires npx (Node 18+) on PATH.
|
|
|
|
|
ci-precheck-commitlint:
|
|
|
|
|
@echo "── ci-precheck-commitlint ─────────────────────────────────────"
|
|
|
|
|
@if ! command -v npx >/dev/null 2>&1; then \
|
2026-06-12 17:11:39 -05:00
|
|
|
echo "error: npx not on PATH (install Node 18+ to enable commitlint checks)"; \
|
|
|
|
|
exit 1; \
|
ci: fix smart_crusher branch CI failures + add make ci-precheck pre-push gate
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
2026-04-27 11:13:47 -07:00
|
|
|
fi
|
|
|
|
|
@if ! git rev-parse --verify origin/main >/dev/null 2>&1; then \
|
2026-06-12 17:11:39 -05:00
|
|
|
echo "error: origin/main not fetched (run 'git fetch origin main')"; \
|
|
|
|
|
exit 1; \
|
ci: fix smart_crusher branch CI failures + add make ci-precheck pre-push gate
Five gates broke on the 2026-04-27 push of the smart_crusher branch.
Each is fixed below; the second half adds a `make ci-precheck` target
(plus an installable git pre-push hook) so the same dance never happens
again.
Failures fixed:
1. cargo fmt — 22 files had formatting drift introduced over the
stage 3c.1 work. `cargo fmt --all` reformatted them; no semantic
changes. `cargo test --workspace` still green (388 + supporting).
2. wheels job (macOS x86_64) — `fastembed -> ort -> ort-sys` does not
publish prebuilt ONNX Runtime binaries for `x86_64-apple-darwin`.
Removed that target from `.github/workflows/rust.yml`'s wheels
matrix. Apple Silicon (`aarch64-apple-darwin`) covers macOS
distribution; Intel macOS users can build from source. The matrix
now has 2 targets: linux x86_64 + macOS aarch64.
3. test-extras (relevance.py) — `tests/test_relevance.py::TestSmartCrusherIntegration`
constructs a `SmartCrusher`, which hard-imports `headroom._core`
since the python implementation was retired in stage 3c.1b. The
test-extras job didn't build the rust extension. Added the same
`maturin build + symlink` block the main `test` job uses.
4. smoke-test (eval.yml) — same root cause:
`compression_only.evaluate_ccr_lossless` instantiates a SmartCrusher.
Same fix: build the rust extension before the smoke test runs.
5. commitlint — three rules tripped:
- `subject-case` rejects PascalCase identifiers in subjects, but
the project deliberately names classes (SmartCrusher, HfTokenizer,
ContentRouter, DiffCompressor) in commit subjects. Disabled.
- `footer-leading-blank` is a warning that the wagoid action turns
into a CI failure; lines like `Module: foo.rs` in our bodies
match the conventional footer pattern and trip it. Disabled.
- `type-enum` doesn't include `parity`, but the project ships
parity-test infrastructure as its own concern (separate from
`test:`); added `parity` to the allowed types.
Pre-push verification — the prevention half:
`make ci-precheck` runs all of the above CI gates locally:
- `ci-precheck-rust`: cargo fmt --check + clippy + test --workspace.
- `ci-precheck-python`: builds the rust extension via maturin, then
runs the smart_crusher-affected python test files (185 tests across
test_transforms/, test_relevance*, test_ccr, test_acceptance,
test_critical_fixes, test_quality_retention).
- `ci-precheck-commitlint`: `npx commitlint --from origin/main --to
HEAD` against the same config CI uses. Skipped silently if npx is
not on PATH (install Node 18+ to enable).
`make install-git-hooks` (or `scripts/install-git-hooks.sh`) installs
a git pre-push hook that runs `make ci-precheck` automatically.
Bypass with `--no-verify` only when truly needed.
When new CI gates land in `.github/workflows/`, mirror them into a
`make ci-precheck-*` target. The Makefile is the local mirror of the
CI configuration; keeping them in sync is a load-bearing invariant.
Verification: `make ci-precheck` runs green on this commit.
2026-04-27 11:13:47 -07:00
|
|
|
fi
|
|
|
|
|
npx --yes --package=@commitlint/cli --package=@commitlint/config-conventional -- \
|
|
|
|
|
commitlint --from origin/main --to HEAD --config .commitlintrc.json
|
|
|
|
|
|
|
|
|
|
install-git-hooks:
|
|
|
|
|
@scripts/install-git-hooks.sh
|
feat: headroom wrap opencode / unwrap opencode CLI (#1105)
## 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>
2026-06-22 18:07:12 +02:00
|
|
|
|
|
|
|
|
# ─── E2e Docker targets ────────────────────────────────────────────────────
|
|
|
|
|
#
|
|
|
|
|
# The wrap-e2e Dockerfile uses manylinux_2_28_x86_64 as its builder stage,
|
|
|
|
|
# which only ships amd64 binaries. Pass --platform linux/amd64 explicitly
|
|
|
|
|
# so the build works on Apple Silicon (requires QEMU emulation). On native
|
|
|
|
|
# x86_64 hosts the flag is harmless and matches CI behaviour.
|
|
|
|
|
|
|
|
|
|
build-e2e-wrap:
|
|
|
|
|
docker build --platform linux/amd64 -f e2e/wrap/Dockerfile -t headroom-wrap-e2e .
|
|
|
|
|
|
|
|
|
|
run-e2e-wrap: build-e2e-wrap
|
|
|
|
|
docker run --rm headroom-wrap-e2e
|