headroom/pyproject.toml

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

456 lines
16 KiB
TOML
Raw Normal View History

[build-system]
refactor: single-wheel maturin build backend (fixes #355) Eliminates the dual-package architecture that was the root cause of #355. `pip install headroom-ai` now produces ONE wheel containing both the Python source (headroom/*.py) and the compiled Rust extension (headroom/_core.so). No more separate `headroom-core-py` package, no more chicken-and-egg with PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action plumbing in CI. This is the canonical pattern used by cryptography, polars, ruff, pydantic-core, and other Rust-as-core Python packages. Honors the "Rust as core engine" direction. ## What changed - pyproject.toml: `[build-system]` swapped from hatchling to maturin. `[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at `crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."` picks up the root `headroom/` package directly (dashboard HTML templates and other non-Python files included automatically). - crates/headroom-py/pyproject.toml: deleted. The crate is no longer a separate published package; its Cargo.toml stays as the cdylib build target invoked via `[tool.maturin] manifest-path`. - crates/headroom-py/python/: deleted (placeholder layout for the old separate package). ## CI updates - ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust toolchain set up before `pip install -e .` (which now invokes maturin via build-system). Removed the "build wheel + symlink .so" dance. `build` job swapped from `python -m build` (hatch) to `maturin build` + `maturin sdist`. - release.yml: collapsed dual-package matrix into one. New `build-wheels` matrix produces cross-platform wheels for cp310/11/12/13 × {linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New `collect-dist` aggregator merges artifacts. publish-pypi consumes the merged dist. - init-native-e2e.yml: dropped windows-latest from the matrix — upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting MSVC C runtime libraries, so the Rust extension cannot build for win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS. - headroom-e2e-setup: composite action now sets up Rust toolchain + Swatinem/rust-cache before `pip install -e .[proxy]`. - eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before install. rust.yml's wheels job builds from root pyproject.toml (no more `-m crates/headroom-py/Cargo.toml`). - e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false` from wrap-e2e — the image now ships the full Rust core. - Dockerfile (main): simplified — no more Layer 2/3 dance with `headroom-core-py` install + symlink. Single `uv pip install` builds + installs everything. - .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin added so `uv sync` builds the extension inside the devcontainer. ## Lockfile + script - uv.lock: regenerated. No `headroom-core-py` entries remain. - scripts/build_rust_extension.sh: simplified from a symlink-into-tree workaround to a thin wrapper around `pip install -e .`. The maturin build-backend handles placement automatically. ## Local validation (all green on macOS aarch64) 1. Clean venv `pip install -e .` → `from headroom._core import …` works. 2. `maturin build --release` → 13.8 MB wheel, 336 files including `headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and `headroom/dashboard/templates/dashboard.html`. 3. `pip install <wheel>` in fresh venv → import works. 4. Wheel contents verified via `unzip -l`. 5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed. 6. `pytest tests/test_relevance.py` — 30 passed. 7. `cargo build --workspace` + `cargo test --workspace` — all green. 8. `make ci-precheck` — 176 Python tests + Rust + commitlint green. ## Migration notes Users on `pip install headroom-ai` get the Rust core automatically (linux + macos wheels). sdist installs require rust toolchain available locally — pip will build via maturin. Closes #355 Supersedes #357 (workarounds-based fix abandoned in favor of architectural fix)
2026-05-03 13:16:41 -07:00
requires = ["maturin>=1.5,<2.0"]
build-backend = "maturin"
[project]
name = "headroom-ai"
chore: release main (#1274) :robot: I have created a release *beep* *boop* --- <details><summary>0.27.0</summary> ## [0.27.0](https://github.com/chopratejas/headroom/compare/v0.26.0...v0.27.0) (2026-06-22) ### Features * **cli:** add headroom doctor setup diagnostics ([#926](https://github.com/chopratejas/headroom/issues/926)) ([e45cf4e](https://github.com/chopratejas/headroom/commit/e45cf4e0618b4de02608f68c502ac4cf1270eb84)) * **cli:** add headroom update command and release banner ([#1088](https://github.com/chopratejas/headroom/issues/1088)) ([26be2c3](https://github.com/chopratejas/headroom/commit/26be2c39cb8a3c23edc08516f01cf91fad33c117)) * compression extraction — Rust knob exposure, CCR hardening, traffic audits ([#818](https://github.com/chopratejas/headroom/issues/818)) ([b7be381](https://github.com/chopratejas/headroom/commit/b7be3814f1d38375bc27901272bbe919e6b35940)) * measure and surface token throughput (tokens/sec) through the proxy ([#983](https://github.com/chopratejas/headroom/issues/983)) ([0d89c67](https://github.com/chopratejas/headroom/commit/0d89c674cd3522c0a46e3df9b98426e59b337b10)) * output-token reduction — verbosity shaper, per-user learning, counterfactual savings ([#965](https://github.com/chopratejas/headroom/issues/965)) ([a99dc61](https://github.com/chopratejas/headroom/commit/a99dc61424df4c7b22c37986fb8dfc648f3ac3b8)) * **policy:** decay P_alive from idle time near cache TTL ([#856](https://github.com/chopratejas/headroom/issues/856) P3b) ([#1028](https://github.com/chopratejas/headroom/issues/1028)) ([fe4f9ee](https://github.com/chopratejas/headroom/commit/fe4f9ee478f50a84190a2d44de2b9fbf24272acf)) * **providers:** add Cortex Code (Snowflake CoCo) as a supported agent ([#1190](https://github.com/chopratejas/headroom/issues/1190)) ([d9d0bf4](https://github.com/chopratejas/headroom/commit/d9d0bf4b79f57ce760f4ac236afe19721727d936)) * **proxy:** cc-switch reconciler — keep Headroom in the request path alongside cc-switch ([#1030](https://github.com/chopratejas/headroom/issues/1030)) ([e8fc8a0](https://github.com/chopratejas/headroom/commit/e8fc8a0d18a551bad572ec21aa92a424748683a5)) * **proxy:** hot-reload live env knobs so a reused proxy picks them up without a restart ([#1090](https://github.com/chopratejas/headroom/issues/1090)) ([6904d47](https://github.com/chopratejas/headroom/commit/6904d47a01e7be496e21d8ebcf34739db5c3b7dd)) * **proxy:** make COMPRESSION_TIMEOUT_SECONDS configurable via env ([#946](https://github.com/chopratejas/headroom/issues/946)) ([#991](https://github.com/chopratejas/headroom/issues/991)) ([addebdb](https://github.com/chopratejas/headroom/commit/addebdb29c3b4a877ed46553d9b0c0a128d62cef)) * **transforms:** tabular + spreadsheet (.xlsx/.xls) compression ([#1128](https://github.com/chopratejas/headroom/issues/1128)) ([d789a7c](https://github.com/chopratejas/headroom/commit/d789a7c528ceee1f4ba648a1002f2e6b6f620854)) * **vertex:** turnkey Claude Code + Vertex compression (+ fixes from the Vertex review) ([#1113](https://github.com/chopratejas/headroom/issues/1113)) ([0e05915](https://github.com/chopratejas/headroom/commit/0e0591506c3f120b96cdc98054114d9ec1771f67)) ### Bug Fixes * **ccr:** accept 12-char SmartCrusher hashes in tool injection ([#1095](https://github.com/chopratejas/headroom/issues/1095)) ([#1141](https://github.com/chopratejas/headroom/issues/1141)) ([9f7f3ad](https://github.com/chopratejas/headroom/commit/9f7f3adfea03710d5e67c4c630b3c8061ff6d161)) * **ccr:** return stored content when headroom_retrieve query matches nothing ([#1213](https://github.com/chopratejas/headroom/issues/1213)) ([#1236](https://github.com/chopratejas/headroom/issues/1236)) ([08fb845](https://github.com/chopratejas/headroom/commit/08fb845fe37478af2c2f55c402df77d7a448fc86)) * **content-router:** honor target_ratio in compression cache + add proxy --target-ratio flag ([#1108](https://github.com/chopratejas/headroom/issues/1108)) ([8894ee0](https://github.com/chopratejas/headroom/commit/8894ee0c18e6dfe858cf0034ec424fd0768a1334)) * **dashboard:** light-mode backgrounds + aligned savings tables ([#1064](https://github.com/chopratejas/headroom/issues/1064)) ([5eae32b](https://github.com/chopratejas/headroom/commit/5eae32ba47fd2e6479cbc1cef1ef4f2fb992fe15)) * **deps:** make litellm optional on Python 3.14 ([#956](https://github.com/chopratejas/headroom/issues/956)) ([#993](https://github.com/chopratejas/headroom/issues/993)) ([b2f04e4](https://github.com/chopratejas/headroom/commit/b2f04e4ef714fb6f2776ed95ee9157c34333e6c3)) * **e2e:** align Codex wrap e2e with global-only RTK guidance ([#1240](https://github.com/chopratejas/headroom/issues/1240)) ([#1254](https://github.com/chopratejas/headroom/issues/1254)) ([bc12ace](https://github.com/chopratejas/headroom/commit/bc12acef5998f264f22ca6d36b17337791a62e6f)) * **init:** set ENABLE_TOOL_SEARCH=true so Claude Code keeps deferring tools ([#746](https://github.com/chopratejas/headroom/issues/746)) ([#995](https://github.com/chopratejas/headroom/issues/995)) ([500ec2b](https://github.com/chopratejas/headroom/commit/500ec2b7faebfd24c9ea404ae1dece40b3b14b84)) * **kompress:** never block the request path on the cold-cache model download ([#1161](https://github.com/chopratejas/headroom/issues/1161)) ([3fc2a78](https://github.com/chopratejas/headroom/commit/3fc2a78a5e20f159f7c5f198de6b91788dc64287)) * **memory:** use ONNX embedder for `wrap --memory` sync ([#1092](https://github.com/chopratejas/headroom/issues/1092)) ([#1262](https://github.com/chopratejas/headroom/issues/1262)) ([4f9feda](https://github.com/chopratejas/headroom/commit/4f9fedaa7a02e41114b5d5f4606f95f903e17b2a)) * **openclaw:** wrap plugin export as {register} object for OpenClaw 2026.x compatibility ([#1218](https://github.com/chopratejas/headroom/issues/1218)) ([2e6c442](https://github.com/chopratejas/headroom/commit/2e6c442dc87f0853313b18ab1a7c80e991058bf7)) * **providers:** update DeepSeek V3 context limit from 128K to 1M ([#1038](https://github.com/chopratejas/headroom/issues/1038)) ([#1137](https://github.com/chopratejas/headroom/issues/1137)) ([bcabc5c](https://github.com/chopratejas/headroom/commit/bcabc5cb11c7c411ed29dac1fcc3771833ac8524)) * **proxy:** allow disabling periodic TOIN stats logging ([#1265](https://github.com/chopratejas/headroom/issues/1265)) ([b5f63d8](https://github.com/chopratejas/headroom/commit/b5f63d8fa9f81f39eab854f29a2fdc39878566df)) * **proxy:** honor HEADROOM_EXCLUDE_TOOLS for Codex /v1/responses tool outputs ([#940](https://github.com/chopratejas/headroom/issues/940)) ([#1053](https://github.com/chopratejas/headroom/issues/1053)) ([f03e77b](https://github.com/chopratejas/headroom/commit/f03e77bec05494aebb4de188eddf2b57f99f6997)) * **proxy:** preserve byte-faithful Anthropic tool forwarding ([#1222](https://github.com/chopratejas/headroom/issues/1222)) ([1f18d59](https://github.com/chopratejas/headroom/commit/1f18d5980972fc7b2091ca0be5318d06c4edfa79)) * **proxy:** route Codex OAuth image requests ([#1215](https://github.com/chopratejas/headroom/issues/1215)) ([381d771](https://github.com/chopratejas/headroom/commit/381d771e4618585e5756e20c090354ccad09183f)) * **proxy:** scope CORS to loopback + gate operator/content endpoints ([#1226](https://github.com/chopratejas/headroom/issues/1226)) ([bd55a42](https://github.com/chopratejas/headroom/commit/bd55a426bc3ec6cd3e0ad46cd3182209afb84937)) * **proxy:** stamp X-Client: codex on Responses endpoint for unidentified callers ([#1036](https://github.com/chopratejas/headroom/issues/1036)) ([b0cd032](https://github.com/chopratejas/headroom/commit/b0cd0329c75c8556c51c1c96dc19f2ab6a23677d)) * **proxy:** treat NODE_EXTRA_CA_CERTS as additive, not replacement ([#998](https://github.com/chopratejas/headroom/issues/998)) ([#1031](https://github.com/chopratejas/headroom/issues/1031)) ([c987283](https://github.com/chopratejas/headroom/commit/c98728363a1079f39bb19da2955cc859b35900a8)) * **telemetry:** switch anonymous telemetry to opt-in (off by default) ([#1223](https://github.com/chopratejas/headroom/issues/1223)) ([b998697](https://github.com/chopratejas/headroom/commit/b99869778bb3ebe223015bdd051e3b9746c8a22c)) * **tokenizers:** bound tiktoken vocab load so a stalled download cannot hang requests ([#956](https://github.com/chopratejas/headroom/issues/956)) ([#994](https://github.com/chopratejas/headroom/issues/994)) ([7e86baf](https://github.com/chopratejas/headroom/commit/7e86bafb9004e40716a04e22398d24157928ca67)) * **unwrap:** remove ANTHROPIC_BASE_URL + ENABLE_TOOL_SEARCH and init hooks on unwrap ([#992](https://github.com/chopratejas/headroom/issues/992)) ([5b84691](https://github.com/chopratejas/headroom/commit/5b846917701e346739346c99c48d5ab6e226e17d)) * **wrap:** keep Codex RTK guidance global ([#1240](https://github.com/chopratejas/headroom/issues/1240)) ([7c26a54](https://github.com/chopratejas/headroom/commit/7c26a54d53aa06a3d75e1111b285c2593155c43e)) * **wrap:** percent-encode non-ASCII cwd names in X-Headroom-Project header ([#1071](https://github.com/chopratejas/headroom/issues/1071)) ([9f712cc](https://github.com/chopratejas/headroom/commit/9f712ccbd7ec27b74f6ac7f20b7d2a9743dba1d8)) * **wrap:** write env.ANTHROPIC_BASE_URL to settings.json so daemon-spawned conversations inherit proxy ([#951](https://github.com/chopratejas/headroom/issues/951)) ([#1078](https://github.com/chopratejas/headroom/issues/1078)) ([a554c3a](https://github.com/chopratejas/headroom/commit/a554c3a0e6c5c57a7c745d8648024362d9d502a4)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-21 22:28:55 -07:00
version = "0.27.0"
description = "The Context Optimization Layer for LLM Applications - Cut costs by 50-90%"
readme = "README.md"
license = "Apache-2.0"
requires-python = ">=3.10"
authors = [
{ name = "Headroom Contributors" }
]
maintainers = [
{ name = "Headroom Contributors" }
]
keywords = [
"llm",
"openai",
"anthropic",
"claude",
"gpt",
"context",
"token",
"optimization",
"compression",
"caching",
"proxy",
"ai",
"machine-learning",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Apache Software License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Software Development :: Libraries :: Python Modules",
"Typing :: Typed",
]
dependencies = [
# Core: lightweight compression (SmartCrusher, ContentRouter, CCR, TOIN)
"tiktoken>=0.5.0", # Tokenizer for all compressors
"pydantic>=2.0.0", # Config and data models
fix(deps): make litellm optional on Python 3.14 (#956) (#993) ## Description `litellm` is a hard dependency and its metadata caps `Requires-Python >=3.10,<3.14`, so `pip install headroom-ai` is unsatisfiable on Python 3.14. But litellm is only used for model registry / pricing / non-core providers — all lazily imported behind `ImportError` guards — never on the core compression or Anthropic proxy path. Refs #956 (install half). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Add a `python_version < '3.14'` marker to both litellm declarations (core deps + dev extra); installs unchanged on <=3.13, skipped on 3.14 (matches the existing rapidocr/tomli marker pattern). ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_litellm_optional.py -q 2 passed in 0.10s $ python3.14 -m pip install dist/headroom_ai-0.25.0-cp310-abi3-linux_x86_64.whl Successfully installed headroom-ai-0.25.0 ... # litellm NOT installed $ python3.14 -c "import importlib.util as u; print(u.find_spec('litellm') is not None)" False ``` ## Real Behavior Proof - Environment: fresh venv on CPython 3.14.5, Linux - Exact command / steps: built the abi3 wheel, `pip install` it on Python 3.14, then `import headroom` + start the proxy + send a compressible request - Observed result: install exits 0 with litellm skipped; `import headroom` works; the proxy compresses (29913 -> 27626 tokens). Stock 0.25.0 cannot install on 3.14 at all. - Not tested: litellm-backed features on 3.14 (intentionally unavailable there until litellm supports 3.14) ## 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>
2026-06-16 22:11:12 -04:00
# litellm's own metadata pins requires-python <3.14, and headroom only uses it for
# model registry / pricing / non-core providers — all lazily imported and
# ImportError-guarded. Marking it 3.14-optional lets headroom install on Python 3.14
# (core compression + the Anthropic proxy path never import litellm). See GH #956.
"litellm>=1.86.2,<2.0; python_version < '3.14'", # model registry, pricing, providers (lazy)
"click>=8.1.0", # CLI framework
"rich>=13.0.0", # Rich terminal output
"opentelemetry-api>=1.24.0", # Safe no-op OTEL API for instrumentation
fix: bundle ast-grep/difftastic/scc + generic tool_result interceptor framework What this does, in plain terms: Headroom's proxy now ships with three CLI tools (ast-grep, difftastic, scc) that it can use to shrink tool_result payloads before they reach the model. The goal is simple: when Claude Code (or Codex, Aider, etc.) asks the model to reason about a big file or diff, we swap the verbose output for a compact, same-meaning version. Fewer tokens per turn, same answers, lower bill. Today a single interceptor is wired: ast-grep on Read. When an agent reads a large code file, the proxy replaces the file body with an outline of its top-level functions/classes plus docstrings. In live tests that cut prompt tokens 74–76% on both OpenAI and Anthropic, same answer either way. How it works: - `pip install headroom-ai` now installs ast-grep via a PyPI wheel (core dep). difftastic and scc are fetched once at proxy startup from pinned upstream GitHub releases and cached per-user. - A generic registry (`headroom/proxy/interceptors/`) lets us add more tool-aware rewrites in one file each: declare `matches()` and `transform()`, call `register()`, done. No proxy or metrics plumbing per tool. - Safety rails built in: pass-through when a Read specifies a line range; second Read of the same file in a conversation returns full content (progressive disclosure); any failing interceptor logs and skips, never crashes a request. Opt-in for now: - Off by default while this ships. Turn on with `headroom proxy --intercept-tool-results` or `HEADROOM_INTERCEPT_ENABLED=1`, so we can measure before flipping defaults. What users see after turning it on: - First `headroom wrap claude` boot is ~5s longer (binaries fetched). Every subsequent run is cache-only. - Existing `transforms_applied` field in metrics gets entries like `interceptor:ast-grep`, so savings show up in current dashboards and HTML reports with no UI change. Other housekeeping in this PR: - uv.lock moved to .gitignore — regenerated locally per environment. - 35 unit + integration tests, ruff + mypy clean. - Dead-code audit done: removed `binaries.run()`, `needs_filesystem` plumbing, unused `_kind` tuple elements, unused `tool_output` parameter, and the never-set HEADROOM_SKIP_TOOLS_BOOTSTRAP env.
2026-04-20 13:41:29 -07:00
"ast-grep-cli>=0.30.0", # AST-aware code slicing (CodeCompressor); binary wheel
"tomli>=2.0.0; python_version < '3.11'", # tomllib backport for helper scripts
]
[project.optional-dependencies]
# Proxy server (most common install: pip install headroom-ai[proxy])
proxy = [
"fastapi>=0.100.0",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"uvicorn>=0.23.0,<1.0",
"httpx[http2]>=0.24.0",
"openai>=2.14.0", # OpenAI API format support
"mcp>=1.0.0", # MCP server (headroom_compress, retrieve, stats)
"magika>=0.6.0", # ML content detection for ContentRouter
"zstandard>=0.20.0", # Decompress zstd request bodies (Codex, etc.)
"websockets>=13.0", # WebSocket proxy for /v1/responses (Codex gpt-5.4+)
"onnxruntime>=1.16.0", # Kompress ONNX INT8 text compression (no torch needed)
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"transformers>=4.30.0,<6.0", # Tokenizer only (for Kompress)
"watchdog>=4.0.0", # File watcher for live code graph reindexing (--code-graph)
"sqlite-vec>=0.1.6", # Vector index for memory (--memory). Lightweight, no torch.
]
fix(deps): move gunicorn to [proxy-prod] extra, add Windows guard (#537) * fix(deps): add missing runtime deps to [code] and [proxy] extras - Add gunicorn>=21.0.0 to the [proxy] extra The proxy docs (docs/content/docs/proxy.mdx and wiki/proxy.md) show gunicorn as the recommended production deployment server: pip install gunicorn gunicorn headroom.proxy.server:app --worker-class uvicorn.workers.UvicornWorker Users installing headroom-ai[proxy] for production get uvicorn (already declared) but had to discover and install gunicorn manually. Adding it to [proxy] removes that friction. Investigation notes: - [code] only needs tree-sitter-language-pack (already declared). code_compressor.py has zero numpy imports. The kompress fallback inside code_compressor.py is guarded by ImportError and requires [ml]. - numpy is correctly declared in [relevance] (numpy>=1.24.0) and pulled transitively by sentence-transformers in [memory]. It is NOT needed under [code]. - tree-sitter is a transitive dep of tree-sitter-language-pack (requires tree-sitter>=0.25.2) so it does not need an explicit entry. * docs(changelog): add entry for gunicorn proxy dep fix style(tests): ruff format test_provider_proxy_routes.py (blank lines after docstrings) * fix(deps): move gunicorn to [proxy-prod] extra, add Windows guard - Remove gunicorn from [proxy] so dev, CI, and Windows users are not forced to install a Unix-only package that does nothing on Windows - Add new [proxy-prod] extra that includes [proxy] + gunicorn with a sys_platform != 'win32' environment marker - Production users: pip install 'headroom-ai[proxy,proxy-prod]' - Update CHANGELOG to reflect the new extra name * fix(devcontainer): bump uv floor to >=0.11.0 for lockfile compatibility uv 0.6.17 (previously pinned) cannot parse lockfiles generated by uv >= 0.11.x. The validate CI job (triggered by pyproject.toml changes) was failing with 'Failed to parse uv.lock'. Loosening the pin to >=0.11.0 picks up the matching format parser while keeping the Docker layer cacheable with a range rather than an exact pin. * fix(devcontainer): skip gitpython wheel filename check in uv sync gitpython 3.1.47 on PyPI has wheel gitpython-3.1.46-py3-none-any.whl (wrong filename). uv >=0.11.19 strict filename validation rejects this lockfile entry. UV_SKIP_WHEEL_FILENAME_CHECK=1 bypasses the check until the upstream lockfile is regenerated with a corrected entry. * fix(deps): correct gitpython version in uv.lock to match actual wheel gitpython 3.1.47 on PyPI was uploaded with sdist/wheel files named gitpython-3.1.46.*. The version field in uv.lock said 3.1.47 but all download URLs reference 3.1.46 files, causing uv >=0.11.19 to refuse to parse the lockfile with a version-mismatch error. Change the version field to 3.1.46 so the entry is internally consistent. Also revert the now-unnecessary UV_SKIP_WHEEL_FILENAME_CHECK workaround from post-create.sh. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-08 02:49:14 -04:00
# Production ASGI/WSGI server — Unix-only (gunicorn does not support Windows).
# Kept separate from [proxy] so that dev, CI, and Windows users are not forced
# to install a non-functional package. Production deployments should use:
# pip install headroom-ai[proxy,proxy-prod]
proxy-prod = [
"headroom-ai[proxy]",
"gunicorn>=21.0.0; sys_platform != 'win32'",
]
# AST-based code compression (tree-sitter)
fix(code): pin tree-sitter-language-pack <1.0 so code compression works (#1234) ## Description The `[code]` extra requires `tree-sitter-language-pack>=0.10.0` with no upper bound, so it now resolves to the 1.x line. tree-sitter-language-pack 1.0 (2026-03-21) is a breaking rewrite whose `get_language()` / `get_parser()` return the pack's own binding types instead of standalone `tree_sitter.Language` / `tree_sitter.Parser`. As a result `headroom/transforms/code_compressor.py::_get_parser()` raises, the exception is caught upstream, and AST code compression silently falls back to passthrough (0% reduction, no error surfaced) on a fresh `pip install headroom-ai[code]`. This caps the dependency below the breaking rewrite and pins the matching tree-sitter range, which is the line the existing code is written against. Closes #1232 ## 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 - Pin `tree-sitter-language-pack>=0.10.0,<1.0` in the `[code]` extra (was `>=0.10.0`). - Add an explicit `tree-sitter>=0.25.2,<0.26` pin to document the supported range (0.13.0 already requires `tree-sitter>=0.25.2`). - Add an inline comment explaining why the `<1.0` cap is required, to prevent a future re-bump. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed I did not run the full pytest / ruff / mypy suite for this change (it is a dependency-constraint pin); I verified the actual runtime behavior the pin restores. See Real Behavior Proof. ### Test Output ```text # BEFORE (resolved tree-sitter-language-pack 1.9.1): code compression no-ops CodeAwareCompressor().compress(<real .py>) -> compression_ratio = 1.0 (0% on every file sampled) # AFTER (tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2), headroom code unchanged: is_tree_sitter_available(): True # 60 varied real Python files (headroom, litellm, pydantic, openai), default CodeCompressorConfig: compressed OK (valid + reduced): 31 (52%) rejected for invalid syntax: 17 (28%) -> returns original, never serves broken code no reduction / too small: 12 (20%) reduction when it worked: min 4.4% median 37.2% max 88.8% # All compressed outputs re-parsed clean with ast.parse(). ``` ## Real Behavior Proof - Environment: Python 3.12, headroom-ai 0.26.0. Before: tree-sitter-language-pack 1.9.1 (what `[code]` resolves today). After: tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2 (what this pin resolves). - Exact command / steps: `pip install "headroom-ai[code]"`; then run `CodeAwareCompressor(CodeCompressorConfig()).compress(src)` over a sample of real `.py` files and re-tokenize before/after with tiktoken (cl100k_base), re-parsing each output with `ast.parse`. - Observed result: with the unpinned (1.x) resolution, every sampled file returned `compression_ratio == 1.0` (0%, silent passthrough). With the pinned (0.x) resolution and no code changes, `is_tree_sitter_available()` is True and 31/60 files compressed validly at a ~37% median (up to ~89%); all compressed outputs re-parsed clean. - Not tested: the full pytest / ruff / mypy suite; per-language rates for JS/TS/Go/Rust/Java/C/C++ (they share the same `_get_parser()` path, so the fix applies, but I measured Python specifically); the ~28% invalid-syntax rejections are a separate pre-existing robustness issue tracked in #1233, not addressed here. ## 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 - [ ] 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 (dependency-constraint change). ## Additional Notes - This is the minimal fix to restore functionality. The proper longer-term fix is to migrate `_get_parser()` and the AST walker to the tree-sitter-language-pack 1.x API, after which the `<1.0` cap can be lifted; happy to follow up with that if preferred. - Unchecked checklist items, with rationale: no docs change needed (constraint-only); no new tests added (a corpus-based compress-and-reparse regression test would be valuable but belongs with the robustness work in #1233); I did not run the full local unit-test suite for a dependency pin; CHANGELOG appears to be release-please managed, so I left it untouched. - I am not a maintainer; this came out of an independent evaluation of the `[code]` path. Pinning `<1.0` parks the project on the now-superseded 0.x pack, which is the tradeoff for a one-line fix today. Co-authored-by: mitralone <5514599+mitralone@users.noreply.github.com>
2026-06-21 20:06:59 +03:00
# NOTE: cap below 1.0. tree-sitter-language-pack 1.x is a breaking rewrite whose
# get_language()/get_parser() return the pack's own binding types instead of
# standalone tree_sitter.Language/Parser, so _get_parser() in
# transforms/code_compressor.py fails and code compression silently no-ops.
# The 0.x line (>=0.10,<1.0) returns standalone tree_sitter objects as expected.
code = [
fix(code): pin tree-sitter-language-pack <1.0 so code compression works (#1234) ## Description The `[code]` extra requires `tree-sitter-language-pack>=0.10.0` with no upper bound, so it now resolves to the 1.x line. tree-sitter-language-pack 1.0 (2026-03-21) is a breaking rewrite whose `get_language()` / `get_parser()` return the pack's own binding types instead of standalone `tree_sitter.Language` / `tree_sitter.Parser`. As a result `headroom/transforms/code_compressor.py::_get_parser()` raises, the exception is caught upstream, and AST code compression silently falls back to passthrough (0% reduction, no error surfaced) on a fresh `pip install headroom-ai[code]`. This caps the dependency below the breaking rewrite and pins the matching tree-sitter range, which is the line the existing code is written against. Closes #1232 ## 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 - Pin `tree-sitter-language-pack>=0.10.0,<1.0` in the `[code]` extra (was `>=0.10.0`). - Add an explicit `tree-sitter>=0.25.2,<0.26` pin to document the supported range (0.13.0 already requires `tree-sitter>=0.25.2`). - Add an inline comment explaining why the `<1.0` cap is required, to prevent a future re-bump. ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [x] Manual testing performed I did not run the full pytest / ruff / mypy suite for this change (it is a dependency-constraint pin); I verified the actual runtime behavior the pin restores. See Real Behavior Proof. ### Test Output ```text # BEFORE (resolved tree-sitter-language-pack 1.9.1): code compression no-ops CodeAwareCompressor().compress(<real .py>) -> compression_ratio = 1.0 (0% on every file sampled) # AFTER (tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2), headroom code unchanged: is_tree_sitter_available(): True # 60 varied real Python files (headroom, litellm, pydantic, openai), default CodeCompressorConfig: compressed OK (valid + reduced): 31 (52%) rejected for invalid syntax: 17 (28%) -> returns original, never serves broken code no reduction / too small: 12 (20%) reduction when it worked: min 4.4% median 37.2% max 88.8% # All compressed outputs re-parsed clean with ast.parse(). ``` ## Real Behavior Proof - Environment: Python 3.12, headroom-ai 0.26.0. Before: tree-sitter-language-pack 1.9.1 (what `[code]` resolves today). After: tree-sitter-language-pack 0.13.0 + tree-sitter 0.25.2 (what this pin resolves). - Exact command / steps: `pip install "headroom-ai[code]"`; then run `CodeAwareCompressor(CodeCompressorConfig()).compress(src)` over a sample of real `.py` files and re-tokenize before/after with tiktoken (cl100k_base), re-parsing each output with `ast.parse`. - Observed result: with the unpinned (1.x) resolution, every sampled file returned `compression_ratio == 1.0` (0%, silent passthrough). With the pinned (0.x) resolution and no code changes, `is_tree_sitter_available()` is True and 31/60 files compressed validly at a ~37% median (up to ~89%); all compressed outputs re-parsed clean. - Not tested: the full pytest / ruff / mypy suite; per-language rates for JS/TS/Go/Rust/Java/C/C++ (they share the same `_get_parser()` path, so the fix applies, but I measured Python specifically); the ~28% invalid-syntax rejections are a separate pre-existing robustness issue tracked in #1233, not addressed here. ## 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 - [ ] 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 (dependency-constraint change). ## Additional Notes - This is the minimal fix to restore functionality. The proper longer-term fix is to migrate `_get_parser()` and the AST walker to the tree-sitter-language-pack 1.x API, after which the `<1.0` cap can be lifted; happy to follow up with that if preferred. - Unchecked checklist items, with rationale: no docs change needed (constraint-only); no new tests added (a corpus-based compress-and-reparse regression test would be valuable but belongs with the robustness work in #1233); I did not run the full local unit-test suite for a dependency pin; CHANGELOG appears to be release-please managed, so I left it untouched. - I am not a maintainer; this came out of an independent evaluation of the `[code]` path. Pinning `<1.0` parks the project on the now-superseded 0.x pack, which is the tradeoff for a one-line fix today. Co-authored-by: mitralone <5514599+mitralone@users.noreply.github.com>
2026-06-21 20:06:59 +03:00
"tree-sitter-language-pack>=0.10.0,<1.0",
"tree-sitter>=0.25.2,<0.26",
]
fix(cli): proxy/perf/wrap UX cleanup + perf --hours correctness Address user-reported UX gaps across the CLI surface: - code-aware: add --code-aware/--no-code-aware (+ HEADROOM_CODE_AWARE_ENABLED env) to the Click CLI. PR #411 had added these only to the orphaned argparse main; the user-facing CLI couldn't reach the flag. Banner status text "remove --no-code-aware to enable" referenced a flag that didn't exist — fix to point at the actual flag/env. Surface code-aware in the click banner and add print_banner=False plumbing to run_server so the click path doesn't print two banners back-to-back. - --mode: hide alias clutter via metavar=[token|cache] and rewrite help to lead with the two real modes. Legacy aliases (token_mode/token_savings/...) still validate. - perf --hours: was documented but ignored. Records are now actually filtered, the report shows the actual time-range covered, and the count of records filtered out (so users can tell when raising --hours helps). - perf TOIN: replace the hash-keyed pattern dump with a strategy-distribution view + recommendation-eligibility from the live store — actionable signal rather than opaque rows. - code-graph: clarify in --help that it indexes cwd / project root. - wrap: spell out supported tools, wrap-vs-proxy distinction, and that `headroom wrap opencode` isn't a thing (use `proxy` directly for opencode; openclaw is not opencode). - mcp: note that mcp__headroom__headroom_retrieve is correct MCP namespacing, not a doubled-prefix bug. Renaming would break the proxy's tool injection. - LLMLingua cleanup: remove [llmlingua] extra from pyproject (no live code uses it). Delete wiki/llmlingua.md and clean retired flag/class references in 6 other wiki pages. Point at [ml] (Kompress) where ML compression is documented. - init -g openclaw: strip mcpServers from existing plugin entries before re-writing — newer openclaw schemas reject it, leaving stale entries from older installs unhealable. Pinned with regression test. Tests: mock_run_server signatures in two existing tests accept **kwargs (needed for the new print_banner plumbing). New test for the openclaw mcpServers strip. Full suite: 4847 passed, 262 skipped, 0 failed.
2026-05-07 16:43:35 -07:00
# ML-based compression with Kompress (ModernBERT).
# (The legacy [llmlingua] extra was removed in 0.9.x — no live code path used it.
# Use [ml] for the supported ML compression dependencies.)
ml = [
fix(deps): remediate dependency CVEs and publish SBOM (#1509) ## Description Supply-chain hardening: takes the **shipped** dependency surface from **26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with no known vulnerabilities (verified with Anchore syft + grype). Also publishes a checked-in SBOM package (`sbom/`) so any user — especially pilots running their own security review — can verify what's inside and that we track it. This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7 low at time of writing). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **Rust** - `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both required by the 0.25+ API). - `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j). **Python** - `torch` → 2.12.1, `mem0ai` → 2.x. - Floor-pinned transitive CVE deps via `[tool.uv] constraint-dependencies`: `pygments>=2.20.0`, `pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`. - **Removed `benchmark` from the `[all]` aggregate** so the default install is CVE-free. `lm-eval` is invoked as an external subprocess (`python -m lm_eval`) and never imported, so it is not a true runtime dep — it remains available via the opt-in `[benchmark]` extra. See [Accepted Risks](#additional-notes). **npm (build/test tooling — never shipped in the wheel/container/published SDK)** - `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw` (GHSA-g7r4-m6w7-qqqr). - `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf), `postcss` override to force Next.js's bundled copy ≥8.5.10 (GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a **Critical** vitest/vite. **CI** - Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0` (GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`. **SBOM** - New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan evidence, 330-package license inventory, and a regeneration guide. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no Python source changed (deps/config only) - [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is unaffected - [x] Type checking passes — `cargo check --workspace` (0 errors) - [ ] New tests added — N/A (dependency bumps; covered by existing suites) - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text # headroom-ai[all] product surface — the number that matters $ grype sbom:sbom/headroom-sbom-all-extra.cdx.json No vulnerabilities found # full repo scan (universal lock incl. opt-in [benchmark] + dev) $ grype sbom:sbom/headroom-sbom.cdx.json NAME INSTALLED TYPE VULNERABILITY SEVERITY sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted # pyo3 0.29 migration — extension builds + imports + runs $ cargo check --workspace Finished `dev` profile [unoptimized + debuginfo] target(s) $ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..." extension OK — detach + from_py_object paths exercised # lru 0.18 — eviction path $ cargo test -p headroom-proxy --lib drift 14 passed, 213 filtered out # per-ecosystem npm audits $ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities $ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities $ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust 1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3. - Exact command / steps: (1) `uv export --extra all --no-dev --no-emit-project | syft → grype` for the product surface; (2) `cargo check --workspace` + `maturin develop` + extension import/compress smoke test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt --check` + `cargo clippy -p headroom-py`; (5) `npm audit` in sdk/openclaw/docs + `bun audit` in docs. - Observed result: `headroom-ai[all]` resolution scans clean — "No vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2 documented accepted CVEs; pyo3 0.29 extension imports and runs (detach + from_py_object paths exercised); drift tests 14/14 pass; cargo fmt + clippy clean; all npm/bun audits report 0. - Not tested: full `pytest` suite (no Python source changed); release-profile wheel build (used dev-profile `maturin develop` for the import proof — the extension is semantically identical). ## 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 (`sbom/README.md`) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective — N/A (dependency bumps; existing suites + scans cover it) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (Release Please auto-generates from the conventional commit) ## Additional Notes **Accepted risks (the 2 residual CVEs).** Both originate solely from the EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]` extra**, which Headroom invokes as a subprocess (never imports): - `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package abandoned (last release 2021), **no upstream fix exists**. - `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`; affects ≤3.9.4 (current latest), **no patched release**. Neither is in `[all]`, the published wheel, or the container. They are documented in `sbom/README.md` and will be picked up automatically once upstream ships fixes. **Release/CHANGELOG:** N/A items above are because this is a dependency/security PR with no Python source changes; CHANGELOG is Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
"torch>=2.12.1",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"transformers>=4.30.0,<6.0",
fix(proxy): Strands MCP bundle + backend path fixes + Codex fail-closed protection Three logically-related sets of proxy changes ship in this branch: 1. Strands integration on the Bedrock path (HeadroomBundle + 4 OpenAI handler fixes + LiteLLM cache stats + dep pin) 2. /stats MCP aggregation (cross-process events log → proxy summary) 3. Codex compression-failure fail-closed (WS + HTTP /v1/responses) == 1. Strands integration on the Bedrock path == * HeadroomBundle (headroom/integrations/strands/bundle.py): single-helper MCP wiring for a Strands Agent — Headroom MCP server (headroom_compress / headroom_retrieve / headroom_stats) plus optional Serena MCP and optional in-process compression hook. Constructor builds unstarted MCPClient instances per server; Strands' Agent owns the subprocess lifecycle. Default config: MCP enabled, Serena enabled, hook OFF (proxy is the single source of truth for compression). User-side integration is two lines in any Strands app. * headroom/proxy/handlers/openai.py — backend path now: - calls PrefixCacheTracker.update_from_response (was direct-OpenAI only) - intercepts CCR headroom_retrieve tool_calls server-side, mirroring the Anthropic handler pattern; NO silent fallback, re-raises on CCR errors (per feedback_no_silent_fallbacks) - works for both non-streaming and streaming paths * headroom/proxy/handlers/streaming.py: _stream_openai_via_backend now accepts prefix_tracker + optimized_messages, parses cache stats from the SSE final-usage frame (cache_creation_input_tokens added to the state machine), records CCR retrieve feedback via a new _record_ccr_feedback_from_openai_sse helper. Streaming CCR intercept is intentionally out of scope (mirrors Anthropic streaming behaviour). * headroom/backends/litellm.py: send_openai_message response usage block now carries cache_read_input_tokens / cache_creation_input_tokens (Anthropic/Bedrock dialect) and prompt_tokens_details.cached_tokens (OpenAI dialect). Backwards-compatible — cold-start callers see the same 3-key shape; cache keys appear only when the underlying provider returns them. Pinned by test_no_cache_fields_means_no_cache_keys. * headroom/proxy/auth_mode.py: ("strands-agents/", "strands") added to CLIENT_UA_MAP. Production callers should also set X-Client: strands since the default openai-python UA carries no Strands signal. * pyproject.toml: huggingface-hub>=1.5.0,<2.0 pinned in [ml] so a sibling install (e.g. strands-agents) can't drag the version below the floor transformers 5.x requires (otherwise Kompress silently goes "unavailable"). == 2. /stats MCP aggregation == * headroom/proxy/cost.py: _aggregate_mcp_events() reads the cross-process shared events file the Headroom MCP server already writes to and surfaces summary.mcp with three new keys: - compressions (count of headroom_compress invocations) - tokens_removed (sum of input - output across those) - retrievals (count of headroom_retrieve — the load-bearing over-compression alarm; if it grows linearly with turn count, lossy compressors are dropping info the model actually needs) Defensive on every axis — missing MCP SDK, missing file, malformed events, read errors — never blocks /stats. * examples/strands_bundle_demo.py: stats panel prints the new fields so the demo shows the full proxy-HTTP + MCP-tool story in one view. == 3. Codex compression-failure fail-closed protection == Reported by Camille (2026-05-21): Codex threads were locking with "ran out of room in the model's context window" after Headroom's compression timed out on an oversized response.create frame and forwarded the original ~1.7 MB frame to the upstream, which then rejected it. Codex's auto-compact heuristic gates on the upstream- reported total_usage_tokens (which Headroom had been shrinking on earlier turns), so its compaction never fired and the thread locked. Validated against open Codex issues (CLI + Desktop share codex-rs/core): * #16068 — confirms compaction gates on total_usage_tokens, estimated_token_count is computed but only logged * #19806 — confirms image token estimator unbounded, contributes to the same ContextManager.get_total_token_usage → auto-compaction chain * headroom/proxy/helpers.py: decide_compression_failure_action() with a unit-tested decision matrix: - asyncio.TimeoutError → refuse, always - non-timeout failure + frame > 256 KiB (configurable) → refuse - non-timeout failure + small frame → forward (legacy) Operator escape hatches: - HEADROOM_WS_FAIL_OPEN_ON_COMPRESSION_FAILURE=1 restores legacy - HEADROOM_WS_COMPRESSION_FAIL_THRESHOLD_BYTES tunes the threshold * headroom/proxy/handlers/openai.py (WS /v1/responses): consults the helper after compression failure. On refuse: close client websocket code 1009 with "headroom: compression <reason> — please compact context and retry" reason; set termination_cause for the outer lifecycle finally; return. * headroom/proxy/handlers/openai.py (HTTP /v1/responses): same helper. On refuse: raise HTTPException(413) with a structured error body so FastAPI's HTTPException handler emits a clean 413. The existing `except HTTPException: raise` guard in this handler already ensures the 413 propagates without being swallowed by the 502 catch-all. Anthropic /v1/messages NOT changed in this branch: no equivalent bug report on Anthropic-protocol clients, Claude Code (Anthropic-owned) handles context overflow via its own cache_control/ephemeral primitives, and Cursor/Aider don't maintain the local-Y estimate the Codex bug requires. Deferred until a real report lands; the patch is a one-liner reusing the same helper. == Tests + verification == * tests/test_backends/test_litellm_cache_stats.py — 3 tests pinning cache-stat surfacing across Anthropic/OpenAI dialects + backwards- compat for no-cache responses. * tests/test_proxy/test_openai_backend_path.py — 5 tests (Bedrock cache fields, OpenAI fallback shape, CCR intercept with provider="openai", CCR re-raise on exception, streaming signature contract). * tests/test_proxy/test_mcp_stats_aggregation.py — 5 tests pinning the aggregator across compress+retrieve mixes, empty events, unknown event types, missing token fields, and read failures. * tests/test_proxy/test_compression_failure_action.py — 12 tests pinning the fail-closed decision matrix (timeout always refuses, small transient passes through, oversize refuses, env override variants, custom threshold, invalid threshold falls back, 0/negative ignored). * examples/strands_bedrock_demo.py — model_id bumped from deprecated Claude 3 Haiku to Sonnet 4.5 (the deprecated model now errors on account access). * examples/strands_via_proxy_demo.py — proxy + Bedrock cache + streaming smoke test. * examples/strands_mcp_dispatch_test.py — pure MCP round-trip probe. * examples/strands_bundle_demo.py — full Strands + HeadroomBundle E2E demo (this is the shape a real Strands user copies into their app). Full pytest: 5327 passed, 178 skipped. The previously-failing test_core_operations.py::TestAddBatch::test_add_batch_basic passes now that the huggingface-hub pin in pyproject.toml unblocks transformers imports. E2E verified live against AWS Bedrock (Sonnet 4.5): * cache_write=10,438 on turn A → cache_read=10,438 on turn B * streaming SSE final usage frame carries cache_read_input_tokens * 78.7% reduction on a 50 KB JSON tool_result via SmartCrusher ( dispatched per-content-type by ContentRouter) * Strands Agent + HeadroomBundle: model autonomously called headroom_compress + headroom_retrieve via MCP; CompressionStore round-trip succeeded; final answer correct.
2026-05-21 11:00:14 -07:00
# transformers >= 5.x requires huggingface-hub >= 1.5.0,<2.0; pinning
# the floor here prevents Kompress from silently falling back to
# "unavailable" when a sibling install (e.g. `pip install
# strands-agents`) drags huggingface-hub backwards.
"huggingface-hub>=1.5.0,<2.0",
]
# Memory system (hierarchical memory with vector search)
memory = [
"hnswlib>=0.8.0",
"sqlite-vec>=0.1.6",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"sentence-transformers>=2.2.0,<6.0",
]
# Qdrant + Neo4j memory backend helpers
memory-stack = [
fix(deps): remediate dependency CVEs and publish SBOM (#1509) ## Description Supply-chain hardening: takes the **shipped** dependency surface from **26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with no known vulnerabilities (verified with Anchore syft + grype). Also publishes a checked-in SBOM package (`sbom/`) so any user — especially pilots running their own security review — can verify what's inside and that we track it. This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7 low at time of writing). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **Rust** - `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both required by the 0.25+ API). - `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j). **Python** - `torch` → 2.12.1, `mem0ai` → 2.x. - Floor-pinned transitive CVE deps via `[tool.uv] constraint-dependencies`: `pygments>=2.20.0`, `pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`. - **Removed `benchmark` from the `[all]` aggregate** so the default install is CVE-free. `lm-eval` is invoked as an external subprocess (`python -m lm_eval`) and never imported, so it is not a true runtime dep — it remains available via the opt-in `[benchmark]` extra. See [Accepted Risks](#additional-notes). **npm (build/test tooling — never shipped in the wheel/container/published SDK)** - `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw` (GHSA-g7r4-m6w7-qqqr). - `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf), `postcss` override to force Next.js's bundled copy ≥8.5.10 (GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a **Critical** vitest/vite. **CI** - Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0` (GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`. **SBOM** - New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan evidence, 330-package license inventory, and a regeneration guide. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no Python source changed (deps/config only) - [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is unaffected - [x] Type checking passes — `cargo check --workspace` (0 errors) - [ ] New tests added — N/A (dependency bumps; covered by existing suites) - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text # headroom-ai[all] product surface — the number that matters $ grype sbom:sbom/headroom-sbom-all-extra.cdx.json No vulnerabilities found # full repo scan (universal lock incl. opt-in [benchmark] + dev) $ grype sbom:sbom/headroom-sbom.cdx.json NAME INSTALLED TYPE VULNERABILITY SEVERITY sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted # pyo3 0.29 migration — extension builds + imports + runs $ cargo check --workspace Finished `dev` profile [unoptimized + debuginfo] target(s) $ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..." extension OK — detach + from_py_object paths exercised # lru 0.18 — eviction path $ cargo test -p headroom-proxy --lib drift 14 passed, 213 filtered out # per-ecosystem npm audits $ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities $ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities $ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust 1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3. - Exact command / steps: (1) `uv export --extra all --no-dev --no-emit-project | syft → grype` for the product surface; (2) `cargo check --workspace` + `maturin develop` + extension import/compress smoke test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt --check` + `cargo clippy -p headroom-py`; (5) `npm audit` in sdk/openclaw/docs + `bun audit` in docs. - Observed result: `headroom-ai[all]` resolution scans clean — "No vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2 documented accepted CVEs; pyo3 0.29 extension imports and runs (detach + from_py_object paths exercised); drift tests 14/14 pass; cargo fmt + clippy clean; all npm/bun audits report 0. - Not tested: full `pytest` suite (no Python source changed); release-profile wheel build (used dev-profile `maturin develop` for the import proof — the extension is semantically identical). ## 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 (`sbom/README.md`) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective — N/A (dependency bumps; existing suites + scans cover it) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (Release Please auto-generates from the conventional commit) ## Additional Notes **Accepted risks (the 2 residual CVEs).** Both originate solely from the EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]` extra**, which Headroom invokes as a subprocess (never imports): - `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package abandoned (last release 2021), **no upstream fix exists**. - `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`; affects ≤3.9.4 (current latest), **no patched release**. Neither is in `[all]`, the published wheel, or the container. They are documented in `sbom/README.md` and will be picked up automatically once upstream ships fixes. **Release/CHANGELOG:** N/A items above are because this is a dependency/security PR with no Python source changes; CHANGELOG is Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
"mem0ai>=2.0.0,<3.0",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"qdrant-client>=1.9.0,<2.0",
"neo4j>=5.20.0,<7.0",
]
feat(memory): add opt-in Apple-GPU (MPS) embedding runtime (#766) ## Description On Apple-Silicon Macs — especially fanless models like the MacBook Air (M5) — running the proxy with memory context injection can pin the CPU while embedding. The embedding work runs an uncapped session on the CPU, saturating multiple cores, which starves the proxy's asyncio loop and leads to request timeouts. This PR adds an **opt-in** runtime that offloads the memory embedder to the Apple GPU (MPS). Setting `HEADROOM_EMBEDDER_RUNTIME=pytorch_mps` routes embedding through the torch `sentence-transformers` backend on MPS instead of the default ONNX CPU embedder, moving the work off the CPU and keeping the proxy responsive. The default behavior is unchanged — the feature is strictly opt-in, env-var only, and falls through to the existing default embedder selection (with a warning) whenever MPS or the torch dependencies are unavailable. Fixes: N/A — no tracking issue (surfaced while running codex auto-review through the proxy on a fanless MacBook Air (M5)). ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Runtime selection** (`headroom/proxy/memory_handler.py`): read `HEADROOM_EMBEDDER_RUNTIME`; when set to `pytorch_mps` **and** MPS is actually available, route the memory embedder to the torch `sentence-transformers` backend (Apple GPU). - If MPS is unavailable or torch/sentence-transformers is not installed, log a warning and fall through to the existing default embedder selection (ONNX when available, else the pre-existing local sentence-transformers fallback) — no crash. The default (env var unset) is unchanged. Env-var only - **MPS serialization** (`headroom/memory/adapters/embedders.py`): `LocalEmbedder` now funnels every `encode()` through a dedicated single-worker `ThreadPoolExecutor` when the resolved device is MPS. torch-MPS is not thread-safe, and the existing `run_in_executor(None, ...)` dispatch would otherwise let concurrent proxy requests call MPS from multiple threads. CPU/CUDA keep the shared default executor (behavior unchanged). `close()` also drops the cached model so re-use after close re-initializes cleanly. - **Packaging** (`pyproject.toml`): new `pytorch-mps` extra (`torch` + `sentence-transformers`), **platform-gated to macOS** (`; sys_platform == 'darwin'`) since MPS is Apple-Silicon-only. Deliberately left out of `[all]` (its deps already arrive via `[ml]`/`[memory]`). - **Tests** (`tests/test_memory/test_embedder_mps_serialization.py`): regression coverage for the serialized executor, concurrency safety (no SIGABRT), CPU-path default behavior, and close/re-use re-initialization. - **Docs**: `wiki/{configuration,memory,macos-deployment}.md`, `docs/content/docs/{configuration,installation,memory}.mdx`, `README.md`, `CHANGELOG.md`. ## 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 (CPU-offload + concurrency profiling on Apple Silicon) ## Test Output ``` $ pytest -v tests/test_memory/test_embedder_mps_serialization.py collected 4 items tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor PASSED [ 25%] tests/test_memory/test_embedder_mps_serialization.py::test_mps_creates_single_worker_executor PASSED [ 50%] tests/test_memory/test_embedder_mps_serialization.py::test_mps_concurrent_embeds_do_not_crash PASSED [ 75%] tests/test_memory/test_embedder_mps_serialization.py::test_mps_reembed_after_close_recreates_executor PASSED [100%] ============================== 4 passed in 6.92s =============================== $ ruff check headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py tests/test_memory/test_embedder_mps_serialization.py All checks passed! $ mypy headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py Success: no issues found in 2 source files $ pytest -q tests/test_memory/ tests/test_memory_handler_concurrent_init.py tests/test_memory_handler_native_ops.py 553 passed, 1 skipped in 13.51s ``` ## 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 **Why MPS (and not CoreML or a thread cap):** measured on an Apple-Silicon Mac, the default uncapped CPU embedding session saturates the cores; the same model on MPS runs at a fraction of the CPU (≈8x lower sustained CPU utilization in profiling) while producing **byte-identical embeddings** (cosine distance ≈ 0 across runtimes), so relevance/ranking is unchanged. A CoreML execution-provider path was evaluated and rejected: the default optimized ONNX model uses fused ops that fall back to CPU under CoreML (no offload), and a full-precision re-export was impractical (very low throughput + multi-GB memory). MPS via `sentence-transformers` was the only practical GPU offload. **Why serialization is mandatory:** torch-MPS is not thread-safe — concurrent encode calls from a multi-worker executor abort with `-[IOGPUMetalCommandBuffer validate]: failed assertion 'commit an already committed command buffer'` (reproduced deterministically; a single-worker executor resolves it). Under concurrent load the serialized single-GPU-stream throughput meets or exceeds the parallel CPU path while using a fraction of the cores. **Scope / boundary:** this targets the Python **memory** embedder, which is live on the proxy request path (memory context injection). The Rust-backed SmartCrusher compression path is unaffected and remains non-configurable from Python by design. **Safety:** default behavior is unchanged (ONNX, no torch). The feature is opt-in, env-var only, macOS-gated at the packaging layer, and degrades gracefully (warn + the existing default embedder selection) when MPS or the dependencies are unavailable.
2026-06-12 02:59:20 +09:00
# Apple-Silicon GPU (MPS) offload for the memory embedder. Opt in at runtime with
# HEADROOM_EMBEDDER_RUNTIME=pytorch_mps. macOS-only; intentionally excluded from [all].
pytorch-mps = [
fix(deps): remediate dependency CVEs and publish SBOM (#1509) ## Description Supply-chain hardening: takes the **shipped** dependency surface from **26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with no known vulnerabilities (verified with Anchore syft + grype). Also publishes a checked-in SBOM package (`sbom/`) so any user — especially pilots running their own security review — can verify what's inside and that we track it. This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7 low at time of writing). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **Rust** - `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both required by the 0.25+ API). - `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j). **Python** - `torch` → 2.12.1, `mem0ai` → 2.x. - Floor-pinned transitive CVE deps via `[tool.uv] constraint-dependencies`: `pygments>=2.20.0`, `pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`. - **Removed `benchmark` from the `[all]` aggregate** so the default install is CVE-free. `lm-eval` is invoked as an external subprocess (`python -m lm_eval`) and never imported, so it is not a true runtime dep — it remains available via the opt-in `[benchmark]` extra. See [Accepted Risks](#additional-notes). **npm (build/test tooling — never shipped in the wheel/container/published SDK)** - `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw` (GHSA-g7r4-m6w7-qqqr). - `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf), `postcss` override to force Next.js's bundled copy ≥8.5.10 (GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a **Critical** vitest/vite. **CI** - Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0` (GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`. **SBOM** - New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan evidence, 330-package license inventory, and a regeneration guide. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no Python source changed (deps/config only) - [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is unaffected - [x] Type checking passes — `cargo check --workspace` (0 errors) - [ ] New tests added — N/A (dependency bumps; covered by existing suites) - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text # headroom-ai[all] product surface — the number that matters $ grype sbom:sbom/headroom-sbom-all-extra.cdx.json No vulnerabilities found # full repo scan (universal lock incl. opt-in [benchmark] + dev) $ grype sbom:sbom/headroom-sbom.cdx.json NAME INSTALLED TYPE VULNERABILITY SEVERITY sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted # pyo3 0.29 migration — extension builds + imports + runs $ cargo check --workspace Finished `dev` profile [unoptimized + debuginfo] target(s) $ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..." extension OK — detach + from_py_object paths exercised # lru 0.18 — eviction path $ cargo test -p headroom-proxy --lib drift 14 passed, 213 filtered out # per-ecosystem npm audits $ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities $ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities $ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust 1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3. - Exact command / steps: (1) `uv export --extra all --no-dev --no-emit-project | syft → grype` for the product surface; (2) `cargo check --workspace` + `maturin develop` + extension import/compress smoke test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt --check` + `cargo clippy -p headroom-py`; (5) `npm audit` in sdk/openclaw/docs + `bun audit` in docs. - Observed result: `headroom-ai[all]` resolution scans clean — "No vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2 documented accepted CVEs; pyo3 0.29 extension imports and runs (detach + from_py_object paths exercised); drift tests 14/14 pass; cargo fmt + clippy clean; all npm/bun audits report 0. - Not tested: full `pytest` suite (no Python source changed); release-profile wheel build (used dev-profile `maturin develop` for the import proof — the extension is semantically identical). ## 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 (`sbom/README.md`) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective — N/A (dependency bumps; existing suites + scans cover it) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (Release Please auto-generates from the conventional commit) ## Additional Notes **Accepted risks (the 2 residual CVEs).** Both originate solely from the EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]` extra**, which Headroom invokes as a subprocess (never imports): - `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package abandoned (last release 2021), **no upstream fix exists**. - `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`; affects ≤3.9.4 (current latest), **no patched release**. Neither is in `[all]`, the published wheel, or the container. They are documented in `sbom/README.md` and will be picked up automatically once upstream ships fixes. **Release/CHANGELOG:** N/A items above are because this is a dependency/security PR with no Python source changes; CHANGELOG is Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
"torch>=2.12.1; sys_platform == 'darwin'",
feat(memory): add opt-in Apple-GPU (MPS) embedding runtime (#766) ## Description On Apple-Silicon Macs — especially fanless models like the MacBook Air (M5) — running the proxy with memory context injection can pin the CPU while embedding. The embedding work runs an uncapped session on the CPU, saturating multiple cores, which starves the proxy's asyncio loop and leads to request timeouts. This PR adds an **opt-in** runtime that offloads the memory embedder to the Apple GPU (MPS). Setting `HEADROOM_EMBEDDER_RUNTIME=pytorch_mps` routes embedding through the torch `sentence-transformers` backend on MPS instead of the default ONNX CPU embedder, moving the work off the CPU and keeping the proxy responsive. The default behavior is unchanged — the feature is strictly opt-in, env-var only, and falls through to the existing default embedder selection (with a warning) whenever MPS or the torch dependencies are unavailable. Fixes: N/A — no tracking issue (surfaced while running codex auto-review through the proxy on a fanless MacBook Air (M5)). ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [x] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Runtime selection** (`headroom/proxy/memory_handler.py`): read `HEADROOM_EMBEDDER_RUNTIME`; when set to `pytorch_mps` **and** MPS is actually available, route the memory embedder to the torch `sentence-transformers` backend (Apple GPU). - If MPS is unavailable or torch/sentence-transformers is not installed, log a warning and fall through to the existing default embedder selection (ONNX when available, else the pre-existing local sentence-transformers fallback) — no crash. The default (env var unset) is unchanged. Env-var only - **MPS serialization** (`headroom/memory/adapters/embedders.py`): `LocalEmbedder` now funnels every `encode()` through a dedicated single-worker `ThreadPoolExecutor` when the resolved device is MPS. torch-MPS is not thread-safe, and the existing `run_in_executor(None, ...)` dispatch would otherwise let concurrent proxy requests call MPS from multiple threads. CPU/CUDA keep the shared default executor (behavior unchanged). `close()` also drops the cached model so re-use after close re-initializes cleanly. - **Packaging** (`pyproject.toml`): new `pytorch-mps` extra (`torch` + `sentence-transformers`), **platform-gated to macOS** (`; sys_platform == 'darwin'`) since MPS is Apple-Silicon-only. Deliberately left out of `[all]` (its deps already arrive via `[ml]`/`[memory]`). - **Tests** (`tests/test_memory/test_embedder_mps_serialization.py`): regression coverage for the serialized executor, concurrency safety (no SIGABRT), CPU-path default behavior, and close/re-use re-initialization. - **Docs**: `wiki/{configuration,memory,macos-deployment}.md`, `docs/content/docs/{configuration,installation,memory}.mdx`, `README.md`, `CHANGELOG.md`. ## 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 (CPU-offload + concurrency profiling on Apple Silicon) ## Test Output ``` $ pytest -v tests/test_memory/test_embedder_mps_serialization.py collected 4 items tests/test_memory/test_embedder_mps_serialization.py::test_cpu_uses_shared_executor PASSED [ 25%] tests/test_memory/test_embedder_mps_serialization.py::test_mps_creates_single_worker_executor PASSED [ 50%] tests/test_memory/test_embedder_mps_serialization.py::test_mps_concurrent_embeds_do_not_crash PASSED [ 75%] tests/test_memory/test_embedder_mps_serialization.py::test_mps_reembed_after_close_recreates_executor PASSED [100%] ============================== 4 passed in 6.92s =============================== $ ruff check headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py tests/test_memory/test_embedder_mps_serialization.py All checks passed! $ mypy headroom/memory/adapters/embedders.py headroom/proxy/memory_handler.py Success: no issues found in 2 source files $ pytest -q tests/test_memory/ tests/test_memory_handler_concurrent_init.py tests/test_memory_handler_native_ops.py 553 passed, 1 skipped in 13.51s ``` ## 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 **Why MPS (and not CoreML or a thread cap):** measured on an Apple-Silicon Mac, the default uncapped CPU embedding session saturates the cores; the same model on MPS runs at a fraction of the CPU (≈8x lower sustained CPU utilization in profiling) while producing **byte-identical embeddings** (cosine distance ≈ 0 across runtimes), so relevance/ranking is unchanged. A CoreML execution-provider path was evaluated and rejected: the default optimized ONNX model uses fused ops that fall back to CPU under CoreML (no offload), and a full-precision re-export was impractical (very low throughput + multi-GB memory). MPS via `sentence-transformers` was the only practical GPU offload. **Why serialization is mandatory:** torch-MPS is not thread-safe — concurrent encode calls from a multi-worker executor abort with `-[IOGPUMetalCommandBuffer validate]: failed assertion 'commit an already committed command buffer'` (reproduced deterministically; a single-worker executor resolves it). Under concurrent load the serialized single-GPU-stream throughput meets or exceeds the parallel CPU path while using a fraction of the cores. **Scope / boundary:** this targets the Python **memory** embedder, which is live on the proxy request path (memory context injection). The Rust-backed SmartCrusher compression path is unaffected and remains non-configurable from Python by design. **Safety:** default behavior is unchanged (ONNX, no torch). The feature is opt-in, env-var only, macOS-gated at the packaging layer, and degrades gracefully (warn + the existing default embedder selection) when MPS or the dependencies are unavailable.
2026-06-12 02:59:20 +09:00
"sentence-transformers>=2.2.0; sys_platform == 'darwin'",
]
# Semantic relevance scoring with embeddings.
# Uses `fastembed` (BAAI/bge-small-en-v1.5 by default — 33M params,
# 384 dims, ~30 MB int8-quantized ONNX). Same library + model used by
# the Rust SmartCrusher (`fastembed` crate), giving byte-equal embeddings
# across the language boundary. Replaced sentence-transformers in
# Stage 3c.1 — fastembed is faster (~2-3x), smaller (no torch
# dependency), and outranks all-MiniLM-L6-v2 on MTEB by ~6 points.
relevance = [
"fastembed>=0.4.0",
"numpy>=1.24.0",
]
# Image compression (ML-based routing + OCR)
fix: PR #372 — restore [image] extra on Python 3.13 via rapidocr 3.x adapter Root cause: `headroom-ai[all]==0.20.16` fails to install on Python 3.13 because `rapidocr-onnxruntime` 1.4.0–1.4.4 wheels declare `requires-python: <3.13,>=3.6`. After 1.4.x the rapidocr ecosystem split: `rapidocr-onnxruntime` (bundled-ORT, capped at <3.13) vs `rapidocr` 3.x (engine-agnostic core, supports 3.13+, returns RapidOCROutput dataclass instead of v1's tuple). Fix: 1. pyproject.toml — environment-marker hybrid in [image]: - rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13' - rapidocr>=3.0,<4; python_version>='3.13' - onnxruntime>=1.7,<2; python_version>='3.13' ORT remains the engine on every Python version; bundle and speed unchanged, just split into two packages on 3.13+. 2. headroom/image/compressor.py — runtime adapter: _resolve_rapidocr() tries v1 first, falls back to v3 when v1 is missing, returns (None, None) when neither installed. Cached at module scope. Detection at runtime (not Python-version-based) so users can install either package on any Python version. _ocr_extract branches on resolved api_version: - v1: (list[(box, text, score)], elapsed) tuple — unchanged - v3: RapidOCROutput dataclass with .txts / .scores / .boxes attrs (each may be None when nothing detected) Defensive None-handling, length-mismatch detection, structured log events for both branches. Smoke test (real install verified before commit): pip install rapidocr onnxruntime pillow → result type: RapidOCROutput → fields: txts (None when empty), scores (None when empty), boxes Confirms the v3 None-coercion is necessary. Tests: 11 new unit tests in tests/test_image_ocr_api_compat.py covering: - Resolver: v1 preferred, v3 fallback, both missing - v1 path: tuple parses, low-confidence None, empty result None - v3 path: dataclass parses, low-confidence None, None attrs handled, mismatched lengths logged + None - Backend missing: returns None gracefully All 11 pass; `make ci-precheck` PASSED. Closes #372.
2026-05-04 08:20:01 -07:00
#
# OCR backend uses ONNX Runtime regardless of Python version. The
# rapidocr ecosystem split into two flavors after 1.4.x:
# * rapidocr-onnxruntime 1.4.x — bundled-ORT package, capped at
# Python <3.13 by its requires-python metadata. Drop-in for our
# existing v1 tuple-shaped API call.
# * rapidocr 3.x — engine-agnostic core, supports Python 3.13+.
# Returns a RapidOCROutput dataclass (txts, scores, boxes, ...).
# Needs `onnxruntime` installed separately to use the ORT backend.
#
# `headroom/image/compressor.py` adapts both API shapes at runtime via
# a try/except cascade. See issue #372 for context.
image = [
"pillow>=10.0.0",
"sentencepiece>=0.1.99", # Required by SigLIP tokenizer (SiglipTokenizer)
fix: PR #372 — restore [image] extra on Python 3.13 via rapidocr 3.x adapter Root cause: `headroom-ai[all]==0.20.16` fails to install on Python 3.13 because `rapidocr-onnxruntime` 1.4.0–1.4.4 wheels declare `requires-python: <3.13,>=3.6`. After 1.4.x the rapidocr ecosystem split: `rapidocr-onnxruntime` (bundled-ORT, capped at <3.13) vs `rapidocr` 3.x (engine-agnostic core, supports 3.13+, returns RapidOCROutput dataclass instead of v1's tuple). Fix: 1. pyproject.toml — environment-marker hybrid in [image]: - rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13' - rapidocr>=3.0,<4; python_version>='3.13' - onnxruntime>=1.7,<2; python_version>='3.13' ORT remains the engine on every Python version; bundle and speed unchanged, just split into two packages on 3.13+. 2. headroom/image/compressor.py — runtime adapter: _resolve_rapidocr() tries v1 first, falls back to v3 when v1 is missing, returns (None, None) when neither installed. Cached at module scope. Detection at runtime (not Python-version-based) so users can install either package on any Python version. _ocr_extract branches on resolved api_version: - v1: (list[(box, text, score)], elapsed) tuple — unchanged - v3: RapidOCROutput dataclass with .txts / .scores / .boxes attrs (each may be None when nothing detected) Defensive None-handling, length-mismatch detection, structured log events for both branches. Smoke test (real install verified before commit): pip install rapidocr onnxruntime pillow → result type: RapidOCROutput → fields: txts (None when empty), scores (None when empty), boxes Confirms the v3 None-coercion is necessary. Tests: 11 new unit tests in tests/test_image_ocr_api_compat.py covering: - Resolver: v1 preferred, v3 fallback, both missing - v1 path: tuple parses, low-confidence None, empty result None - v3 path: dataclass parses, low-confidence None, None attrs handled, mismatched lengths logged + None - Backend missing: returns None gracefully All 11 pass; `make ci-precheck` PASSED. Closes #372.
2026-05-04 08:20:01 -07:00
# Python 3.63.12: keep the proven ORT-bundled package directly.
# ~15 MB ONNX models auto-downloaded on first use.
"rapidocr-onnxruntime>=1.4.0,<2; python_version<'3.13'",
# Python 3.13+: rapidocr-onnxruntime is unavailable (its wheels
# declare requires-python<3.13). Use the successor `rapidocr` 3.x
# core + `onnxruntime` engine; same ORT backend, just split into
# two packages. Total install size and inference speed unchanged.
"rapidocr>=3.0,<4; python_version>='3.13'",
"onnxruntime>=1.7,<2; python_version>='3.13'",
]
# Report generation
reports = [
"jinja2>=3.0.0",
]
feat(transforms): tabular + spreadsheet (.xlsx/.xls) compression (#1128) ## Description Adds a content-type-aware path for **tabular data** — CSV/TSV, markdown tables, fixed-width text, and binary `.xlsx`/`.xls` spreadsheets — by routing them through the existing, battle-tested `SmartCrusher` instead of letting them fall through to `PLAIN_TEXT → Kompress`. The pipeline already compressed tables losslessly when handed a JSON array of records. This wires up the missing front door: detect tabular text (and ingest binary spreadsheets), convert to JSON records, and reuse `SmartCrusher.crush()`. No new compression algorithm. Closes # ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Detection** (`content_detector.py`): new `ContentType.TABULAR` + `_try_detect_tabular()` for CSV/TSV, markdown tables, and fixed-width columns. Ordered after search/log (which also look "delimited") and before code, with a prose-rejection guard so it never steals `file:line:content` search output, `key: value` logs, or sentences with incidental commas. Rust backend returns `plain_text` for unknown types and the router already falls back to the Python detector, so **no Rust change**. - **Bridge** (`tabular_ingest.py`): stdlib parsers + `to_records()` + a `TabularCompressor` that parses → JSON records → `SmartCrusher` (lossless `csv-schema` first; lossy row-drop with reversible `<<ccr:HASH>>` markers stays SmartCrusher's built-in fallback). Only adopts a result when it actually saves bytes. - **Spreadsheets** (`spreadsheet_ingest.py`): `.xlsx`/`.xls` → per-sheet CSV text at the SDK boundary. Optional deps (`pip install headroom-ai[spreadsheet]`) fail loudly with an install hint, never silently degrade. - **Routing** (`content_router.py`): `CompressionStrategy.TABULAR`, `enable_tabular_compressor` flag, lazy getter, apply branch, strategy maps, Kompress fallback eligibility. - **SDK** (`compress.py`): `compress_spreadsheet(path, ...)` helper (one message per sheet). - **Packaging** (`pyproject.toml`): new `[spreadsheet]` extra; `openpyxl` added to `[dev]` so the xlsx path is exercised in CI. - **Docs/demo**: `examples/tabular_compression_demo.py` + README entry. ### Design note: lossless-only Compact, all-unique tables with no query yield ~0 savings — this is correct, not a bug. SmartCrusher returns `skip:unique_entities_no_signal` and won't drop unique rows without a duplicate/relevance signal. Real wins come from verbose/redundant tables and query-driven selection. A pressure-driven lossy row sampler was considered and intentionally not added. ## 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 $ python -m pytest tests/test_transforms_tabular.py -q collected 20 items tests/test_transforms_tabular.py .................... [100%] ============================== 20 passed in 7.15s ============================== $ ruff check headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py All checks passed! $ mypy headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py Success: no issues found in 2 source files ``` `tests/test_transforms_tabular.py` (20 tests): detection true positives + no-misroute negatives (search/log/JSON/prose), parser units (incl. fixed-width), the CSV→SmartCrusher bridge, router routing + disable flag, and `.xlsx` ingestion (skipif openpyxl missing) + error paths. `spreadsheet_ingest` 100% / `tabular_ingest` 90% line coverage. ## Real Behavior Proof - **Environment:** local checkout of `feat/tabular-compression`, Python 3.x, `pip install -e ".[dev]"`. - **Exact command / steps:** `python examples/tabular_compression_demo.py` (no API key required). - **Observed result:** ```text === Raw tabular text (ContentRouter, char-level) === compact unique CSV strat=tabular chars 1306 -> 1072 ( 17.9% saved) redundant CSV strat=tabular chars 2661 -> 1350 ( 49.3% saved) verbose markdown strat=tabular chars 2019 -> 1580 ( 21.7% saved) === Full pipeline (real tokenizer) === redundant CSV tokens 768 -> 394 ( 48.7% saved) === Binary spreadsheet (.xlsx) === 2-sheet workbook tokens 1092 -> 683 ( 37.5% saved) ``` - **Not tested:** legacy `.xls` binary path (needs optional `xlrd` + binary fixture; `# pragma: no cover`); base64-embedded `.xlsx` inside multimodal blocks (out of scope, noted as a follow-up). ## 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 - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - CHANGELOG/version are intentionally untouched: this repo uses **release-please**, which bumps the version and CHANGELOG via automated `chore: release main` PRs, not per-feature PRs. - The `.xls` path is `# pragma: no cover` (legacy, needs optional `xlrd` + a binary fixture). - Follow-up (out of scope): base64-embedded `.xlsx` inside tool-result/multimodal blocks; porting tabular parsers into the Rust core for parity. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:30:20 -07:00
# Binary spreadsheet ingestion (.xlsx / .xls -> tabular text)
spreadsheet = [
"openpyxl>=3.1.0", # .xlsx
"xlrd>=2.0.1", # legacy .xls
]
# OpenTelemetry metrics export
otel = [
"opentelemetry-sdk>=1.24.0",
"opentelemetry-exporter-otlp-proto-http>=1.24.0",
]
2026-02-06 11:40:23 -06:00
# any-llm multi-provider backend (requires Python 3.11+)
anyllm = [
"any-llm-sdk>=1.0.0; python_version >= '3.11'",
2026-02-06 11:40:23 -06:00
]
# LangChain integration
langchain = [
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"langchain-core>=1.3.3,<4.0",
"langchain-openai>=1.1.14,<2.0",
]
# Agno agent framework integration
agno = [
"agno>=1.0.0",
]
feat: Add AWS Strands Agents SDK integration ## Description Add Headroom integration with AWS Strands Agents SDK, enabling automatic context optimization and tool output compression for Strands-based agents. Fixes #14 ## Type of Change - [x] New feature (non-breaking change that adds functionality) - [x] Documentation update ## Changes Made ### Core Integration (`headroom/integrations/strands/`) - **HeadroomHookProvider** - Implements Strands `HookProvider` interface for automatic tool output compression via `AfterToolCallEvent`. Compresses verbose tool outputs before they enter conversation context. - **HeadroomStrandsModel** - Model wrapper that extends Strands `Model` base class for message-level optimization. Implements all required abstract methods: `stream()`, `get_config()`, `update_config()`, `structured_output()`. - **Provider auto-detection** - Automatically detects appropriate Headroom provider (Anthropic, OpenAI, Google) based on wrapped Strands model type. - **`strands-agents` as optional dependency** - Install with `pip install headroom-ai[strands]` ### Testing (`tests/integrations/test_strands/`) - **Real integration tests (25 tests)** - Use actual AWS Bedrock API calls with Claude 3 Haiku. Skip automatically when credentials unavailable. - **Unit tests (57 tests)** - Mock-based tests for internal logic, edge cases, and error handling. No credentials required. ### Demo (`examples/strands_bedrock_demo.py`) - Interactive demo showcasing both integration patterns - Visual before/after compression comparison with token savings - 4 verbose tools (search, logs, database, metrics) demonstrating real savings - Supports `--hook` and `--model` flags for individual demos ## Testing All tests verified: - [x] Unit tests pass (57 tests) - [x] Integration tests pass (25 tests with real Bedrock API) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom/integrations/strands/`) - [x] Formatting passes (`ruff format --check`) - [x] Demo runs successfully with ~50% token savings ## Test Output ``` $ pytest tests/integrations/test_strands/ -v =================== 82 passed in 90.09s =================== $ ruff check headroom/integrations/strands/ --ignore E402 All checks passed! $ mypy headroom/integrations/strands/ --ignore-missing-imports Success: no issues found ``` ## Demo Results ``` ╭────────────────────────────────────────────────────────────╮ │ HeadroomHookProvider Results │ │────────────────────────────────────────────────────────────│ │ Tokens BEFORE compression: 51,961 │ │ Tokens AFTER compression: 25,658 │ │ Tokens SAVED: 26,303 (50.6%) │ ╰────────────────────────────────────────────────────────────╯ ```
2026-01-31 00:31:37 -08:00
# AWS Strands Agents SDK integration
strands = [
"strands-agents>=0.1.0",
]
# MCP server for Claude Code integration
mcp = [
"mcp>=1.0.0",
"httpx>=0.24.0",
]
# Voice filler detection
voice = [
"onnxruntime>=1.16.0",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"transformers>=4.30.0,<6.0",
fix(deps): remediate dependency CVEs and publish SBOM (#1509) ## Description Supply-chain hardening: takes the **shipped** dependency surface from **26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with no known vulnerabilities (verified with Anchore syft + grype). Also publishes a checked-in SBOM package (`sbom/`) so any user — especially pilots running their own security review — can verify what's inside and that we track it. This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7 low at time of writing). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **Rust** - `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both required by the 0.25+ API). - `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j). **Python** - `torch` → 2.12.1, `mem0ai` → 2.x. - Floor-pinned transitive CVE deps via `[tool.uv] constraint-dependencies`: `pygments>=2.20.0`, `pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`. - **Removed `benchmark` from the `[all]` aggregate** so the default install is CVE-free. `lm-eval` is invoked as an external subprocess (`python -m lm_eval`) and never imported, so it is not a true runtime dep — it remains available via the opt-in `[benchmark]` extra. See [Accepted Risks](#additional-notes). **npm (build/test tooling — never shipped in the wheel/container/published SDK)** - `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw` (GHSA-g7r4-m6w7-qqqr). - `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf), `postcss` override to force Next.js's bundled copy ≥8.5.10 (GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a **Critical** vitest/vite. **CI** - Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0` (GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`. **SBOM** - New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan evidence, 330-package license inventory, and a regeneration guide. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no Python source changed (deps/config only) - [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is unaffected - [x] Type checking passes — `cargo check --workspace` (0 errors) - [ ] New tests added — N/A (dependency bumps; covered by existing suites) - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text # headroom-ai[all] product surface — the number that matters $ grype sbom:sbom/headroom-sbom-all-extra.cdx.json No vulnerabilities found # full repo scan (universal lock incl. opt-in [benchmark] + dev) $ grype sbom:sbom/headroom-sbom.cdx.json NAME INSTALLED TYPE VULNERABILITY SEVERITY sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted # pyo3 0.29 migration — extension builds + imports + runs $ cargo check --workspace Finished `dev` profile [unoptimized + debuginfo] target(s) $ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..." extension OK — detach + from_py_object paths exercised # lru 0.18 — eviction path $ cargo test -p headroom-proxy --lib drift 14 passed, 213 filtered out # per-ecosystem npm audits $ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities $ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities $ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust 1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3. - Exact command / steps: (1) `uv export --extra all --no-dev --no-emit-project | syft → grype` for the product surface; (2) `cargo check --workspace` + `maturin develop` + extension import/compress smoke test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt --check` + `cargo clippy -p headroom-py`; (5) `npm audit` in sdk/openclaw/docs + `bun audit` in docs. - Observed result: `headroom-ai[all]` resolution scans clean — "No vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2 documented accepted CVEs; pyo3 0.29 extension imports and runs (detach + from_py_object paths exercised); drift tests 14/14 pass; cargo fmt + clippy clean; all npm/bun audits report 0. - Not tested: full `pytest` suite (no Python source changed); release-profile wheel build (used dev-profile `maturin develop` for the import proof — the extension is semantically identical). ## 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 (`sbom/README.md`) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective — N/A (dependency bumps; existing suites + scans cover it) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (Release Please auto-generates from the conventional commit) ## Additional Notes **Accepted risks (the 2 residual CVEs).** Both originate solely from the EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]` extra**, which Headroom invokes as a subprocess (never imports): - `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package abandoned (last release 2021), **no upstream fix exists**. - `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`; affects ≤3.9.4 (current latest), **no patched release**. Neither is in `[all]`, the published wheel, or the container. They are documented in `sbom/README.md` and will be picked up automatically once upstream ships fixes. **Release/CHANGELOG:** N/A items above are because this is a dependency/security PR with no Python source changes; CHANGELOG is Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
"torch>=2.12.1",
]
# Voice training (includes voice deps + training extras)
voice-train = [
"headroom-ai[voice]",
"datasets>=2.14.0",
"accelerate>=0.20.0",
]
# Evaluation framework
evals = [
"datasets>=2.14.0",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"sentence-transformers>=2.2.0,<6.0",
"numpy>=1.24.0",
"scikit-learn>=1.3.0",
"anthropic>=0.18.0",
"openai>=1.0.0",
]
# AWS Bedrock backend
bedrock = [
"boto3>=1.28.0",
]
# HTML content extraction
html = [
"trafilatura>=1.6.0",
]
# Comprehensive LLM benchmarks
benchmark = [
"lm-eval[api]>=0.4.0",
"openai>=1.0.0",
"anthropic>=0.18.0",
]
# Development dependencies
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"pytest-asyncio>=0.21.0",
"ruff>=0.1.0",
"mypy>=1.0.0",
"pre-commit>=3.0.0",
"openai>=1.0.0",
"anthropic>=0.18.0",
fix(deps): make litellm optional on Python 3.14 (#956) (#993) ## Description `litellm` is a hard dependency and its metadata caps `Requires-Python >=3.10,<3.14`, so `pip install headroom-ai` is unsatisfiable on Python 3.14. But litellm is only used for model registry / pricing / non-core providers — all lazily imported behind `ImportError` guards — never on the core compression or Anthropic proxy path. Refs #956 (install half). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - Add a `python_version < '3.14'` marker to both litellm declarations (core deps + dev extra); installs unchanged on <=3.13, skipped on 3.14 (matches the existing rapidocr/tomli marker pattern). ## Testing - [x] Unit tests pass (`pytest`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_litellm_optional.py -q 2 passed in 0.10s $ python3.14 -m pip install dist/headroom_ai-0.25.0-cp310-abi3-linux_x86_64.whl Successfully installed headroom-ai-0.25.0 ... # litellm NOT installed $ python3.14 -c "import importlib.util as u; print(u.find_spec('litellm') is not None)" False ``` ## Real Behavior Proof - Environment: fresh venv on CPython 3.14.5, Linux - Exact command / steps: built the abi3 wheel, `pip install` it on Python 3.14, then `import headroom` + start the proxy + send a compressible request - Observed result: install exits 0 with litellm skipped; `import headroom` works; the proxy compresses (29913 -> 27626 tokens). Stock 0.25.0 cannot install on 3.14 at all. - Not tested: litellm-backed features on 3.14 (intentionally unavailable there until litellm supports 3.14) ## 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>
2026-06-16 22:11:12 -04:00
"litellm>=1.86.2,<2.0; python_version < '3.14'", # see core deps note (GH #956)
"fastapi>=0.100.0",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"uvicorn>=0.23.0,<1.0",
"httpx[http2]>=0.24.0",
"websockets>=13.0",
"opentelemetry-sdk>=1.24.0",
"opentelemetry-exporter-otlp-proto-http>=1.24.0",
"ollama>=0.4.0",
"langchain-ollama>=0.2.0",
"hnswlib>=0.8.0",
"sqlite-vec>=0.1.6",
chore(deps): loosen over-pinned constraints and add upper bounds (#538) ## What Loosen over-pinned Python dependency constraints and add missing upper bounds in `pyproject.toml`. Also bump the neo4j Docker image and uv builder version. ## Why Several dependencies had constraints that either blocked security patches or allowed silent major-version jumps: - `litellm==1.82.3` was an exact pin — every security patch release requires a manual lockfile bump - `transformers`, `sentence-transformers` had no upper bound and have already crossed major version boundaries without a constraint gate - `neo4j>=5.20.0` had no upper cap; the driver has already reached 6.x in the wild - `mem0ai>=0.1.100` had a pre-1.0 floor while the locked version is already 1.0.11 - `langchain-core`, `langchain-openai`, `qdrant-client`, `uvicorn` had no upper bound on a range with active major-version churn - `docker-compose.yml` pinned neo4j at `5.15.0`, which is 11 patch releases behind the current 5.x LTS - `Dockerfile` pinned uv at `0.11.16`; latest stable is `0.11.18` ## How Constraint changes only — no code changes, no `uv lock --upgrade`. The existing locked versions all satisfy the new bounds (we added caps, not floors). `uv` re-resolved the lockfile to format revision 3 (adds `upload-time` metadata fields) and cleaned up the defunct `llmlingua` extra entries. | Dependency | Before | After | |---|---|---| | `litellm` | `==1.82.3` | `>=1.82.3,<2.0` | | `transformers` | `>=4.30.0` | `>=4.30.0,<6.0` | | `sentence-transformers` | `>=2.2.0` | `>=2.2.0,<6.0` | | `neo4j` | `>=5.20.0` | `>=5.20.0,<7.0` | | `mem0ai` | `>=0.1.100` | `>=1.0.0,<2.0` | | `langchain-core` | `>=0.2.0` | `>=0.2.0,<4.0` | | `langchain-openai` | `>=0.1.0` | `>=0.1.0,<2.0` | | `qdrant-client` | `>=1.9.0` | `>=1.9.0,<2.0` | | `uvicorn` | `>=0.23.0` | `>=0.23.0,<1.0` | | neo4j Docker image | `5.15.0` | `5.26` | | uv (Dockerfile ARG) | `0.11.16` | `0.11.18` | ## Breaking changes None. All currently installed versions fall within the new ranges. Installers that previously resolved `litellm` to an older exact pin may now resolve newer patch releases — which is the desired behavior. --------- Co-authored-by: Tejas Chopra <chopratejas@gmail.com>
2026-06-09 02:06:24 -04:00
"sentence-transformers>=2.2.0,<6.0",
"numpy>=1.24.0",
feat(transforms): tabular + spreadsheet (.xlsx/.xls) compression (#1128) ## Description Adds a content-type-aware path for **tabular data** — CSV/TSV, markdown tables, fixed-width text, and binary `.xlsx`/`.xls` spreadsheets — by routing them through the existing, battle-tested `SmartCrusher` instead of letting them fall through to `PLAIN_TEXT → Kompress`. The pipeline already compressed tables losslessly when handed a JSON array of records. This wires up the missing front door: detect tabular text (and ingest binary spreadsheets), convert to JSON records, and reuse `SmartCrusher.crush()`. No new compression algorithm. Closes # ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - **Detection** (`content_detector.py`): new `ContentType.TABULAR` + `_try_detect_tabular()` for CSV/TSV, markdown tables, and fixed-width columns. Ordered after search/log (which also look "delimited") and before code, with a prose-rejection guard so it never steals `file:line:content` search output, `key: value` logs, or sentences with incidental commas. Rust backend returns `plain_text` for unknown types and the router already falls back to the Python detector, so **no Rust change**. - **Bridge** (`tabular_ingest.py`): stdlib parsers + `to_records()` + a `TabularCompressor` that parses → JSON records → `SmartCrusher` (lossless `csv-schema` first; lossy row-drop with reversible `<<ccr:HASH>>` markers stays SmartCrusher's built-in fallback). Only adopts a result when it actually saves bytes. - **Spreadsheets** (`spreadsheet_ingest.py`): `.xlsx`/`.xls` → per-sheet CSV text at the SDK boundary. Optional deps (`pip install headroom-ai[spreadsheet]`) fail loudly with an install hint, never silently degrade. - **Routing** (`content_router.py`): `CompressionStrategy.TABULAR`, `enable_tabular_compressor` flag, lazy getter, apply branch, strategy maps, Kompress fallback eligibility. - **SDK** (`compress.py`): `compress_spreadsheet(path, ...)` helper (one message per sheet). - **Packaging** (`pyproject.toml`): new `[spreadsheet]` extra; `openpyxl` added to `[dev]` so the xlsx path is exercised in CI. - **Docs/demo**: `examples/tabular_compression_demo.py` + README entry. ### Design note: lossless-only Compact, all-unique tables with no query yield ~0 savings — this is correct, not a bug. SmartCrusher returns `skip:unique_entities_no_signal` and won't drop unique rows without a duplicate/relevance signal. Real wins come from verbose/redundant tables and query-driven selection. A pressure-driven lossy row sampler was considered and intentionally not added. ## 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 $ python -m pytest tests/test_transforms_tabular.py -q collected 20 items tests/test_transforms_tabular.py .................... [100%] ============================== 20 passed in 7.15s ============================== $ ruff check headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py All checks passed! $ mypy headroom/transforms/tabular_ingest.py headroom/transforms/spreadsheet_ingest.py Success: no issues found in 2 source files ``` `tests/test_transforms_tabular.py` (20 tests): detection true positives + no-misroute negatives (search/log/JSON/prose), parser units (incl. fixed-width), the CSV→SmartCrusher bridge, router routing + disable flag, and `.xlsx` ingestion (skipif openpyxl missing) + error paths. `spreadsheet_ingest` 100% / `tabular_ingest` 90% line coverage. ## Real Behavior Proof - **Environment:** local checkout of `feat/tabular-compression`, Python 3.x, `pip install -e ".[dev]"`. - **Exact command / steps:** `python examples/tabular_compression_demo.py` (no API key required). - **Observed result:** ```text === Raw tabular text (ContentRouter, char-level) === compact unique CSV strat=tabular chars 1306 -> 1072 ( 17.9% saved) redundant CSV strat=tabular chars 2661 -> 1350 ( 49.3% saved) verbose markdown strat=tabular chars 2019 -> 1580 ( 21.7% saved) === Full pipeline (real tokenizer) === redundant CSV tokens 768 -> 394 ( 48.7% saved) === Binary spreadsheet (.xlsx) === 2-sheet workbook tokens 1092 -> 683 ( 37.5% saved) ``` - **Not tested:** legacy `.xls` binary path (needs optional `xlrd` + binary fixture; `# pragma: no cover`); base64-embedded `.xlsx` inside multimodal blocks (out of scope, noted as a follow-up). ## 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 - [ ] I have updated the CHANGELOG.md if applicable ## Additional Notes - CHANGELOG/version are intentionally untouched: this repo uses **release-please**, which bumps the version and CHANGELOG via automated `chore: release main` PRs, not per-feature PRs. - The `.xls` path is `# pragma: no cover` (legacy, needs optional `xlrd` + a binary fixture). - Follow-up (out of scope): base64-embedded `.xlsx` inside tool-result/multimodal blocks; porting tabular parsers into the Rust core for parity. --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 09:30:20 -07:00
"openpyxl>=3.1.0", # exercises spreadsheet_ingest (.xlsx) in the test suite
]
# All optional dependencies (everything you need)
fix(deps): remediate dependency CVEs and publish SBOM (#1509) ## Description Supply-chain hardening: takes the **shipped** dependency surface from **26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with no known vulnerabilities (verified with Anchore syft + grype). Also publishes a checked-in SBOM package (`sbom/`) so any user — especially pilots running their own security review — can verify what's inside and that we track it. This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7 low at time of writing). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **Rust** - `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both required by the 0.25+ API). - `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j). **Python** - `torch` → 2.12.1, `mem0ai` → 2.x. - Floor-pinned transitive CVE deps via `[tool.uv] constraint-dependencies`: `pygments>=2.20.0`, `pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`. - **Removed `benchmark` from the `[all]` aggregate** so the default install is CVE-free. `lm-eval` is invoked as an external subprocess (`python -m lm_eval`) and never imported, so it is not a true runtime dep — it remains available via the opt-in `[benchmark]` extra. See [Accepted Risks](#additional-notes). **npm (build/test tooling — never shipped in the wheel/container/published SDK)** - `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw` (GHSA-g7r4-m6w7-qqqr). - `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf), `postcss` override to force Next.js's bundled copy ≥8.5.10 (GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a **Critical** vitest/vite. **CI** - Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0` (GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`. **SBOM** - New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan evidence, 330-package license inventory, and a regeneration guide. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no Python source changed (deps/config only) - [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is unaffected - [x] Type checking passes — `cargo check --workspace` (0 errors) - [ ] New tests added — N/A (dependency bumps; covered by existing suites) - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text # headroom-ai[all] product surface — the number that matters $ grype sbom:sbom/headroom-sbom-all-extra.cdx.json No vulnerabilities found # full repo scan (universal lock incl. opt-in [benchmark] + dev) $ grype sbom:sbom/headroom-sbom.cdx.json NAME INSTALLED TYPE VULNERABILITY SEVERITY sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted # pyo3 0.29 migration — extension builds + imports + runs $ cargo check --workspace Finished `dev` profile [unoptimized + debuginfo] target(s) $ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..." extension OK — detach + from_py_object paths exercised # lru 0.18 — eviction path $ cargo test -p headroom-proxy --lib drift 14 passed, 213 filtered out # per-ecosystem npm audits $ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities $ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities $ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust 1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3. - Exact command / steps: (1) `uv export --extra all --no-dev --no-emit-project | syft → grype` for the product surface; (2) `cargo check --workspace` + `maturin develop` + extension import/compress smoke test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt --check` + `cargo clippy -p headroom-py`; (5) `npm audit` in sdk/openclaw/docs + `bun audit` in docs. - Observed result: `headroom-ai[all]` resolution scans clean — "No vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2 documented accepted CVEs; pyo3 0.29 extension imports and runs (detach + from_py_object paths exercised); drift tests 14/14 pass; cargo fmt + clippy clean; all npm/bun audits report 0. - Not tested: full `pytest` suite (no Python source changed); release-profile wheel build (used dev-profile `maturin develop` for the import proof — the extension is semantically identical). ## 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 (`sbom/README.md`) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective — N/A (dependency bumps; existing suites + scans cover it) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (Release Please auto-generates from the conventional commit) ## Additional Notes **Accepted risks (the 2 residual CVEs).** Both originate solely from the EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]` extra**, which Headroom invokes as a subprocess (never imports): - `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package abandoned (last release 2021), **no upstream fix exists**. - `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`; affects ≤3.9.4 (current latest), **no patched release**. Neither is in `[all]`, the published wheel, or the container. They are documented in `sbom/README.md` and will be picked up automatically once upstream ships fixes. **Release/CHANGELOG:** N/A items above are because this is a dependency/security PR with no Python source changes; CHANGELOG is Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
#
# `benchmark` is deliberately EXCLUDED from `[all]`. It installs the
# EleutherAI lm-evaluation-harness (lm-eval), which headroom invokes as an
# external subprocess (`python -m lm_eval`) — it is never imported as a
# library, so it is not a true runtime dependency. lm-eval pulls two
# transitive deps with unpatchable High CVEs (sqlitedict CVE-2024-35515,
# nltk CVE-2026-54293 via rouge-score), neither of which has an upstream
# fix. Keeping `benchmark` out of `[all]` means `pip install
# headroom-ai[all]` is CVE-free; researchers who need the accuracy harness
# opt in explicitly with `pip install headroom-ai[benchmark]`.
all = [
fix(deps): remediate dependency CVEs and publish SBOM (#1509) ## Description Supply-chain hardening: takes the **shipped** dependency surface from **26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with no known vulnerabilities (verified with Anchore syft + grype). Also publishes a checked-in SBOM package (`sbom/`) so any user — especially pilots running their own security review — can verify what's inside and that we track it. This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7 low at time of writing). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **Rust** - `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both required by the 0.25+ API). - `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j). **Python** - `torch` → 2.12.1, `mem0ai` → 2.x. - Floor-pinned transitive CVE deps via `[tool.uv] constraint-dependencies`: `pygments>=2.20.0`, `pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`. - **Removed `benchmark` from the `[all]` aggregate** so the default install is CVE-free. `lm-eval` is invoked as an external subprocess (`python -m lm_eval`) and never imported, so it is not a true runtime dep — it remains available via the opt-in `[benchmark]` extra. See [Accepted Risks](#additional-notes). **npm (build/test tooling — never shipped in the wheel/container/published SDK)** - `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw` (GHSA-g7r4-m6w7-qqqr). - `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf), `postcss` override to force Next.js's bundled copy ≥8.5.10 (GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a **Critical** vitest/vite. **CI** - Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0` (GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`. **SBOM** - New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan evidence, 330-package license inventory, and a regeneration guide. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no Python source changed (deps/config only) - [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is unaffected - [x] Type checking passes — `cargo check --workspace` (0 errors) - [ ] New tests added — N/A (dependency bumps; covered by existing suites) - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text # headroom-ai[all] product surface — the number that matters $ grype sbom:sbom/headroom-sbom-all-extra.cdx.json No vulnerabilities found # full repo scan (universal lock incl. opt-in [benchmark] + dev) $ grype sbom:sbom/headroom-sbom.cdx.json NAME INSTALLED TYPE VULNERABILITY SEVERITY sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted # pyo3 0.29 migration — extension builds + imports + runs $ cargo check --workspace Finished `dev` profile [unoptimized + debuginfo] target(s) $ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..." extension OK — detach + from_py_object paths exercised # lru 0.18 — eviction path $ cargo test -p headroom-proxy --lib drift 14 passed, 213 filtered out # per-ecosystem npm audits $ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities $ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities $ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust 1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3. - Exact command / steps: (1) `uv export --extra all --no-dev --no-emit-project | syft → grype` for the product surface; (2) `cargo check --workspace` + `maturin develop` + extension import/compress smoke test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt --check` + `cargo clippy -p headroom-py`; (5) `npm audit` in sdk/openclaw/docs + `bun audit` in docs. - Observed result: `headroom-ai[all]` resolution scans clean — "No vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2 documented accepted CVEs; pyo3 0.29 extension imports and runs (detach + from_py_object paths exercised); drift tests 14/14 pass; cargo fmt + clippy clean; all npm/bun audits report 0. - Not tested: full `pytest` suite (no Python source changed); release-profile wheel build (used dev-profile `maturin develop` for the import proof — the extension is semantically identical). ## 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 (`sbom/README.md`) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective — N/A (dependency bumps; existing suites + scans cover it) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (Release Please auto-generates from the conventional commit) ## Additional Notes **Accepted risks (the 2 residual CVEs).** Both originate solely from the EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]` extra**, which Headroom invokes as a subprocess (never imports): - `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package abandoned (last release 2021), **no upstream fix exists**. - `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`; affects ≤3.9.4 (current latest), **no patched release**. Neither is in `[all]`, the published wheel, or the container. They are documented in `sbom/README.md` and will be picked up automatically once upstream ships fixes. **Release/CHANGELOG:** N/A items above are because this is a dependency/security PR with no Python source changes; CHANGELOG is Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
"headroom-ai[proxy,code,ml,memory,relevance,image,reports,otel,evals,voice,html,mcp,spreadsheet]",
]
[project.scripts]
headroom = "headroom.cli:main"
[project.urls]
docs: improve discoverability for AI agents and search crawlers Several signals AI agents and search engines use to discover and install a project were misaligned or missing: * ``docs/app/layout.tsx`` set ``metadataBase`` to ``https://chopratejas.github.io/headroom/`` while the live docs run on Vercel — every page's ``og:url`` and ``twitter:url`` resolved to a URL that returns 404 for ``/llms.txt``. Now points at the live Vercel host (overridable via ``NEXT_PUBLIC_SITE_URL`` for a future custom domain). Adds explicit ``openGraph`` and ``twitter`` metadata so social shares render a card with the project's pitch. * No ``llms.txt`` at the GitHub repo root. AI agents crawling ``github.com/chopratejas/headroom/`` saw only the README. The new ``llms.txt`` follows the llmstxt.org convention: 1-line pitch, canonical docs links, copy-paste install commands (pip / npm / Docker / proxy / ``headroom wrap``), and entry points for the library, proxy, MCP server, and SDK integrations. Points at the Fumadocs-generated ``/llms.txt`` and ``/llms-full.txt`` for the full picture. * ``pyproject.toml`` ``Documentation`` URL pointed at the GitHub README anchor. Updated to point at the docs site so PyPI visitors land on searchable docs, and adds an ``AI / LLM Index`` URL pointing at the Fumadocs ``/llms.txt``. * No explicit AI-bot allow list. Added ``docs/app/robots.ts`` (Next 13+ App Router convention) with explicit allows for GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, ChatGPT-User, Cohere-AI, CCBot, and Applebot-Extended. Wildcard allow as the catch-all. Advertises the sitemap. * No ``sitemap.xml`` route. Added ``docs/app/sitemap.ts`` that pulls every Fumadocs page out of ``source`` (same source backing ``/llms.txt``, search, and OG images) so search and AI crawlers can enumerate doc pages without scraping HTML. * README didn't tell AI agents where to look. Added a 2-line pointer near the top nav row: read ``/llms.txt`` here, or fetch the live index / full docs blob. Also tightened the GitHub repo description and added five topics (``claude-code``, ``cursor``, ``tokens``, ``prompt-engineering``, ``typescript``) via ``gh repo edit`` — that's already live on the repo, not part of this commit. No Python or Rust code changes; ``make ci-precheck`` was run to confirm the test slice still passes.
2026-05-13 17:36:06 -07:00
Homepage = "https://headroom-docs.vercel.app"
Documentation = "https://headroom-docs.vercel.app/docs"
Repository = "https://github.com/chopratejas/headroom"
Issues = "https://github.com/chopratejas/headroom/issues"
Changelog = "https://github.com/chopratejas/headroom/blob/main/CHANGELOG.md"
docs: improve discoverability for AI agents and search crawlers Several signals AI agents and search engines use to discover and install a project were misaligned or missing: * ``docs/app/layout.tsx`` set ``metadataBase`` to ``https://chopratejas.github.io/headroom/`` while the live docs run on Vercel — every page's ``og:url`` and ``twitter:url`` resolved to a URL that returns 404 for ``/llms.txt``. Now points at the live Vercel host (overridable via ``NEXT_PUBLIC_SITE_URL`` for a future custom domain). Adds explicit ``openGraph`` and ``twitter`` metadata so social shares render a card with the project's pitch. * No ``llms.txt`` at the GitHub repo root. AI agents crawling ``github.com/chopratejas/headroom/`` saw only the README. The new ``llms.txt`` follows the llmstxt.org convention: 1-line pitch, canonical docs links, copy-paste install commands (pip / npm / Docker / proxy / ``headroom wrap``), and entry points for the library, proxy, MCP server, and SDK integrations. Points at the Fumadocs-generated ``/llms.txt`` and ``/llms-full.txt`` for the full picture. * ``pyproject.toml`` ``Documentation`` URL pointed at the GitHub README anchor. Updated to point at the docs site so PyPI visitors land on searchable docs, and adds an ``AI / LLM Index`` URL pointing at the Fumadocs ``/llms.txt``. * No explicit AI-bot allow list. Added ``docs/app/robots.ts`` (Next 13+ App Router convention) with explicit allows for GPTBot, ClaudeBot, PerplexityBot, Google-Extended, OAI-SearchBot, ChatGPT-User, Cohere-AI, CCBot, and Applebot-Extended. Wildcard allow as the catch-all. Advertises the sitemap. * No ``sitemap.xml`` route. Added ``docs/app/sitemap.ts`` that pulls every Fumadocs page out of ``source`` (same source backing ``/llms.txt``, search, and OG images) so search and AI crawlers can enumerate doc pages without scraping HTML. * README didn't tell AI agents where to look. Added a 2-line pointer near the top nav row: read ``/llms.txt`` here, or fetch the live index / full docs blob. Also tightened the GitHub repo description and added five topics (``claude-code``, ``cursor``, ``tokens``, ``prompt-engineering``, ``typescript``) via ``gh repo edit`` — that's already live on the repo, not part of this commit. No Python or Rust code changes; ``make ci-precheck`` was run to confirm the test slice still passes.
2026-05-13 17:36:06 -07:00
# llms.txt convention (llmstxt.org) — point AI agents / LLM crawlers
# at the auto-generated docs index so they can resolve install paths
# and entry points without a follow-up fetch.
"AI / LLM Index" = "https://headroom-docs.vercel.app/llms.txt"
refactor: single-wheel maturin build backend (fixes #355) Eliminates the dual-package architecture that was the root cause of #355. `pip install headroom-ai` now produces ONE wheel containing both the Python source (headroom/*.py) and the compiled Rust extension (headroom/_core.so). No more separate `headroom-core-py` package, no more chicken-and-egg with PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action plumbing in CI. This is the canonical pattern used by cryptography, polars, ruff, pydantic-core, and other Rust-as-core Python packages. Honors the "Rust as core engine" direction. ## What changed - pyproject.toml: `[build-system]` swapped from hatchling to maturin. `[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at `crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."` picks up the root `headroom/` package directly (dashboard HTML templates and other non-Python files included automatically). - crates/headroom-py/pyproject.toml: deleted. The crate is no longer a separate published package; its Cargo.toml stays as the cdylib build target invoked via `[tool.maturin] manifest-path`. - crates/headroom-py/python/: deleted (placeholder layout for the old separate package). ## CI updates - ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust toolchain set up before `pip install -e .` (which now invokes maturin via build-system). Removed the "build wheel + symlink .so" dance. `build` job swapped from `python -m build` (hatch) to `maturin build` + `maturin sdist`. - release.yml: collapsed dual-package matrix into one. New `build-wheels` matrix produces cross-platform wheels for cp310/11/12/13 × {linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New `collect-dist` aggregator merges artifacts. publish-pypi consumes the merged dist. - init-native-e2e.yml: dropped windows-latest from the matrix — upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting MSVC C runtime libraries, so the Rust extension cannot build for win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS. - headroom-e2e-setup: composite action now sets up Rust toolchain + Swatinem/rust-cache before `pip install -e .[proxy]`. - eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before install. rust.yml's wheels job builds from root pyproject.toml (no more `-m crates/headroom-py/Cargo.toml`). - e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false` from wrap-e2e — the image now ships the full Rust core. - Dockerfile (main): simplified — no more Layer 2/3 dance with `headroom-core-py` install + symlink. Single `uv pip install` builds + installs everything. - .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin added so `uv sync` builds the extension inside the devcontainer. ## Lockfile + script - uv.lock: regenerated. No `headroom-core-py` entries remain. - scripts/build_rust_extension.sh: simplified from a symlink-into-tree workaround to a thin wrapper around `pip install -e .`. The maturin build-backend handles placement automatically. ## Local validation (all green on macOS aarch64) 1. Clean venv `pip install -e .` → `from headroom._core import …` works. 2. `maturin build --release` → 13.8 MB wheel, 336 files including `headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and `headroom/dashboard/templates/dashboard.html`. 3. `pip install <wheel>` in fresh venv → import works. 4. Wheel contents verified via `unzip -l`. 5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed. 6. `pytest tests/test_relevance.py` — 30 passed. 7. `cargo build --workspace` + `cargo test --workspace` — all green. 8. `make ci-precheck` — 176 Python tests + Rust + commitlint green. ## Migration notes Users on `pip install headroom-ai` get the Rust core automatically (linux + macos wheels). sdist installs require rust toolchain available locally — pip will build via maturin. Closes #355 Supersedes #357 (workarounds-based fix abandoned in favor of architectural fix)
2026-05-03 13:16:41 -07:00
# Maturin builds a single wheel containing both the Python source under
# `headroom/` AND the compiled Rust extension `headroom/_core.so` (cdylib
# from `crates/headroom-py`). One `pip install headroom-ai` ships everything
# atomically — no separate `headroom-core-py` package, no chicken-and-egg,
# no PIP_FIND_LINKS plumbing. Phase A0's runtime fail-loud check still
# exists but only fires if someone forces an sdist install on a platform
# without a wheel and the rust toolchain isn't available to compile it.
fix(deps): remediate dependency CVEs and publish SBOM (#1509) ## Description Supply-chain hardening: takes the **shipped** dependency surface from **26 known CVEs to 0**. `pip install headroom-ai[all]` now resolves with no known vulnerabilities (verified with Anchore syft + grype). Also publishes a checked-in SBOM package (`sbom/`) so any user — especially pilots running their own security review — can verify what's inside and that we track it. This addresses the Dependabot alerts on `main` (9 high / 4 moderate / 7 low at time of writing). Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made **Rust** - `pyo3` 0.24 → 0.29 (GHSA-36hh-v3qg-5jq4 High, GHSA-chgr-c6px-7xpp Med). Migrated `Python::allow_threads` → `Python::detach` (10 sites) and added `from_py_object` to the `Clone`-deriving `#[pyclass]` types (both required by the 0.25+ API). - `pyo3-log` 0.12 → 0.13; `lru` 0.12 → 0.18 (GHSA-rhfx-m35p-ff5j). **Python** - `torch` → 2.12.1, `mem0ai` → 2.x. - Floor-pinned transitive CVE deps via `[tool.uv] constraint-dependencies`: `pygments>=2.20.0`, `pydantic-settings>=2.14.2`, `gitpython>=3.1.50`, `langsmith>=0.9.0`. - **Removed `benchmark` from the `[all]` aggregate** so the default install is CVE-free. `lm-eval` is invoked as an external subprocess (`python -m lm_eval`) and never imported, so it is not a true runtime dep — it remains available via the opt-in `[benchmark]` extra. See [Accepted Risks](#additional-notes). **npm (build/test tooling — never shipped in the wheel/container/published SDK)** - `esbuild` override `>=0.28.1` in `sdk/typescript` + `plugins/openclaw` (GHSA-g7r4-m6w7-qqqr). - `docs/`: `@anthropic-ai/sdk` → `^0.106.0` (GHSA-p7fg-763f-g4gf), `postcss` override to force Next.js's bundled copy ≥8.5.10 (GHSA-qx2v-qp2m-jg93); regenerated a stale `bun.lock` that carried a **Critical** vitest/vite. **CI** - Pinned `pypa/gh-action-pypi-publish` `@release/v1` → `@v1.13.0` (GHSA-vxmw-7h4f-hqxh) in `release.yml` + `publish.yml`. **SBOM** - New `sbom/` directory: CycloneDX 1.7 + SPDX 2.3 SBOMs, grype scan evidence, 330-package license inventory, and a regeneration guide. ## Testing - [ ] Unit tests pass (`pytest`) — N/A, no Python source changed (deps/config only) - [x] Linting passes — `cargo fmt --check` + `cargo clippy` clean on the changed crate; 0 `.py` files changed so `ruff`/`mypy` scope is unaffected - [x] Type checking passes — `cargo check --workspace` (0 errors) - [ ] New tests added — N/A (dependency bumps; covered by existing suites) - [x] Manual testing performed — see Real Behavior Proof ### Test Output ```text # headroom-ai[all] product surface — the number that matters $ grype sbom:sbom/headroom-sbom-all-extra.cdx.json No vulnerabilities found # full repo scan (universal lock incl. opt-in [benchmark] + dev) $ grype sbom:sbom/headroom-sbom.cdx.json NAME INSTALLED TYPE VULNERABILITY SEVERITY sqlitedict 2.1.0 python GHSA-g4r7-86gm-pgqc High # [benchmark]-only, unpatchable, accepted nltk 3.9.4 python GHSA-p4gq-832x-fm9v High # [benchmark]-only, unpatchable, accepted # pyo3 0.29 migration — extension builds + imports + runs $ cargo check --workspace Finished `dev` profile [unoptimized + debuginfo] target(s) $ maturin develop && python -c "from headroom._core import DiffCompressor, SmartCrusher; ..." extension OK — detach + from_py_object paths exercised # lru 0.18 — eviction path $ cargo test -p headroom-proxy --lib drift 14 passed, 213 filtered out # per-ecosystem npm audits $ (cd sdk/typescript && npm audit) -> found 0 vulnerabilities $ (cd plugins/openclaw && npm audit) -> found 0 vulnerabilities $ (cd docs && npm audit && bun audit) -> found 0 vulnerabilities / No vulnerabilities found ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0, arm64), Python 3.12 `.venv`, Rust 1.95 toolchain, syft 1.46.0, grype 0.115.0, bun 1.3.14, maturin 1.13.3. - Exact command / steps: (1) `uv export --extra all --no-dev --no-emit-project | syft → grype` for the product surface; (2) `cargo check --workspace` + `maturin develop` + extension import/compress smoke test; (3) `cargo test -p headroom-proxy --lib drift`; (4) `cargo fmt --check` + `cargo clippy -p headroom-py`; (5) `npm audit` in sdk/openclaw/docs + `bun audit` in docs. - Observed result: `headroom-ai[all]` resolution scans clean — "No vulnerabilities found" (179 pkgs); full/prod SBOM shows only the 2 documented accepted CVEs; pyo3 0.29 extension imports and runs (detach + from_py_object paths exercised); drift tests 14/14 pass; cargo fmt + clippy clean; all npm/bun audits report 0. - Not tested: full `pytest` suite (no Python source changed); release-profile wheel build (used dev-profile `maturin develop` for the import proof — the extension is semantically identical). ## 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 (`sbom/README.md`) - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective — N/A (dependency bumps; existing suites + scans cover it) - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A (Release Please auto-generates from the conventional commit) ## Additional Notes **Accepted risks (the 2 residual CVEs).** Both originate solely from the EleutherAI `lm-evaluation-harness` under the **opt-in `[benchmark]` extra**, which Headroom invokes as a subprocess (never imports): - `sqlitedict` CVE-2024-35515 (High) — pickle deserialization; package abandoned (last release 2021), **no upstream fix exists**. - `nltk` CVE-2026-54293 (High) — path traversal in `nltk.data.load()`; affects ≤3.9.4 (current latest), **no patched release**. Neither is in `[all]`, the published wheel, or the container. They are documented in `sbom/README.md` and will be picked up automatically once upstream ships fixes. **Release/CHANGELOG:** N/A items above are because this is a dependency/security PR with no Python source changes; CHANGELOG is Release-Please-managed via the conventional commit message.
2026-06-27 15:28:12 -07:00
# Constrain transitive dependencies that have CVEs requiring minimum versions.
# These packages don't appear as direct headroom deps but are pulled in
# transitively; the floor pins below ensure uv resolves to patched versions.
[tool.uv]
constraint-dependencies = [
# GHSA-5239-wwwm-4pmq (Low) — transitive via rich; fix at 2.20.0
"pygments>=2.20.0",
# GHSA-4xgf-cpjx-pc3j (Medium) — transitive via mcp; fix at 2.14.2
"pydantic-settings>=2.14.2",
# GHSA-mv93-w799-cj2w + 4 others (High) — transitive via lm-eval; fix at 3.1.50
"gitpython>=3.1.50",
# GHSA-f4xh-w4cj-qxq8 (High) — transitive via langchain-core; fix at 0.8.18
"langsmith>=0.9.0",
]
# Pin the project's package index to public PyPI. Without this, `uv lock`
# inherits the developer's user-level `~/.config/uv/uv.toml` index
# setting — including private/internal mirrors like
# `pypi.netflix.net/simple` — and bakes those URLs into uv.lock, which
# then breaks CI on every public runner that can't reach the mirror.
# Declaring the index in pyproject.toml makes the project authoritative
# regardless of who runs `uv lock`.
[[tool.uv.index]]
name = "pypi"
url = "https://pypi.org/simple/"
default = true
refactor: single-wheel maturin build backend (fixes #355) Eliminates the dual-package architecture that was the root cause of #355. `pip install headroom-ai` now produces ONE wheel containing both the Python source (headroom/*.py) and the compiled Rust extension (headroom/_core.so). No more separate `headroom-core-py` package, no more chicken-and-egg with PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action plumbing in CI. This is the canonical pattern used by cryptography, polars, ruff, pydantic-core, and other Rust-as-core Python packages. Honors the "Rust as core engine" direction. ## What changed - pyproject.toml: `[build-system]` swapped from hatchling to maturin. `[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at `crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."` picks up the root `headroom/` package directly (dashboard HTML templates and other non-Python files included automatically). - crates/headroom-py/pyproject.toml: deleted. The crate is no longer a separate published package; its Cargo.toml stays as the cdylib build target invoked via `[tool.maturin] manifest-path`. - crates/headroom-py/python/: deleted (placeholder layout for the old separate package). ## CI updates - ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust toolchain set up before `pip install -e .` (which now invokes maturin via build-system). Removed the "build wheel + symlink .so" dance. `build` job swapped from `python -m build` (hatch) to `maturin build` + `maturin sdist`. - release.yml: collapsed dual-package matrix into one. New `build-wheels` matrix produces cross-platform wheels for cp310/11/12/13 × {linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New `collect-dist` aggregator merges artifacts. publish-pypi consumes the merged dist. - init-native-e2e.yml: dropped windows-latest from the matrix — upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting MSVC C runtime libraries, so the Rust extension cannot build for win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS. - headroom-e2e-setup: composite action now sets up Rust toolchain + Swatinem/rust-cache before `pip install -e .[proxy]`. - eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before install. rust.yml's wheels job builds from root pyproject.toml (no more `-m crates/headroom-py/Cargo.toml`). - e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false` from wrap-e2e — the image now ships the full Rust core. - Dockerfile (main): simplified — no more Layer 2/3 dance with `headroom-core-py` install + symlink. Single `uv pip install` builds + installs everything. - .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin added so `uv sync` builds the extension inside the devcontainer. ## Lockfile + script - uv.lock: regenerated. No `headroom-core-py` entries remain. - scripts/build_rust_extension.sh: simplified from a symlink-into-tree workaround to a thin wrapper around `pip install -e .`. The maturin build-backend handles placement automatically. ## Local validation (all green on macOS aarch64) 1. Clean venv `pip install -e .` → `from headroom._core import …` works. 2. `maturin build --release` → 13.8 MB wheel, 336 files including `headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and `headroom/dashboard/templates/dashboard.html`. 3. `pip install <wheel>` in fresh venv → import works. 4. Wheel contents verified via `unzip -l`. 5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed. 6. `pytest tests/test_relevance.py` — 30 passed. 7. `cargo build --workspace` + `cargo test --workspace` — all green. 8. `make ci-precheck` — 176 Python tests + Rust + commitlint green. ## Migration notes Users on `pip install headroom-ai` get the Rust core automatically (linux + macos wheels). sdist installs require rust toolchain available locally — pip will build via maturin. Closes #355 Supersedes #357 (workarounds-based fix abandoned in favor of architectural fix)
2026-05-03 13:16:41 -07:00
[tool.maturin]
# Where the Python package lives. With `python-source = "."` and the
# package directory `headroom/` at repo root, maturin includes every file
# under `headroom/` in the wheel — that picks up the dashboard HTML
fix(ci): include NOTICE in sdist + assert License-File metadata matches tarball Every release since v0.20.16 has uploaded 12 wheels but no sdist. The underlying failure is a 400 from PyPI: 400 License-File NOTICE does not exist in distribution file headroom_ai-X.Y.Z.tar.gz at headroom_ai-X.Y.Z/NOTICE Two-part regression: 1. The hatch -> maturin migration in 2a91cbb (single-wheel maturin build backend, May 4) replaced `[tool.hatch.build.targets.sdist].include`, which listed both `LICENSE` and `NOTICE`, with maturin's own include directive that only carried `LICENSE` over. Maturin's PEP 639 license auto-discovery still emits `License-File: NOTICE` into the sdist's PKG-INFO (because NOTICE exists at the project root and matches the default glob), so the sdist tarball declares a license file it doesn't physically contain. PyPI's PEP 639 validator rejects with 400. Wheels were unaffected because maturin auto-injects both files into `*.dist-info/licenses/`. 2. CI showed "publish-pypi" green for ~22 releases despite this break because twine was bailing earlier with `400 File already exists` on the wheels (the version detector kept computing the same v0.21.5). PR #412 added `skip-existing: true` (May 6) to make wheel re-uploads idempotent. With wheels now silently skipping, twine proceeded to upload the sdist for the first time in three weeks - and the dormant License-File error surfaced as a hard 400. Fix: - Add `NOTICE` alongside `LICENSE` in `[tool.maturin].include` for the `sdist` format. Both files now ship in the tarball, matching what PEP 639 already declares in PKG-INFO. - Replace the existing "verify sdist contains LICENSE" check with a generic "every License-File entry in PKG-INFO resolves to a real tarball member" check. This catches the same bug class for any future addition (COPYING, AUTHORS, etc.) without another bespoke literal. Verified locally: $ maturin sdist --out dist Including license file `LICENSE` Including license file `NOTICE` Including files matching "LICENSE" Including files matching "NOTICE" Built source distribution to dist/headroom_ai-0.9.1.tar.gz $ tar -tzf dist/headroom_ai-0.9.1.tar.gz | grep -E '(LICENSE|NOTICE)$' headroom_ai-0.9.1/LICENSE headroom_ai-0.9.1/NOTICE $ twine check dist/headroom_ai-0.9.1.tar.gz Checking dist/headroom_ai-0.9.1.tar.gz: PASSED
2026-05-07 16:29:05 -07:00
# templates and bundled YAML configs. `LICENSE` and `NOTICE` are listed
# explicitly because maturin sdists do not get the package-directory
# treatment wheels do, and PEP 639 auto-discovery emits both files into
# `License-File:` metadata — PyPI rejects sdists whose declared license
# files are missing from the tarball with `400 License-File X does not
# exist in distribution file`.
include = [
{ path = "LICENSE", format = "sdist" },
{ path = "NOTICE", format = "sdist" },
]
refactor: single-wheel maturin build backend (fixes #355) Eliminates the dual-package architecture that was the root cause of #355. `pip install headroom-ai` now produces ONE wheel containing both the Python source (headroom/*.py) and the compiled Rust extension (headroom/_core.so). No more separate `headroom-core-py` package, no more chicken-and-egg with PyPI publication, no more wheelhouse / PIP_FIND_LINKS / composite-action plumbing in CI. This is the canonical pattern used by cryptography, polars, ruff, pydantic-core, and other Rust-as-core Python packages. Honors the "Rust as core engine" direction. ## What changed - pyproject.toml: `[build-system]` swapped from hatchling to maturin. `[tool.hatch.*]` deleted; `[tool.maturin]` added pointing at `crates/headroom-py/Cargo.toml` for the cdylib. `python-source = "."` picks up the root `headroom/` package directly (dashboard HTML templates and other non-Python files included automatically). - crates/headroom-py/pyproject.toml: deleted. The crate is no longer a separate published package; its Cargo.toml stays as the cdylib build target invoked via `[tool.maturin] manifest-path`. - crates/headroom-py/python/: deleted (placeholder layout for the old separate package). ## CI updates - ci.yml: `test` / `test-extras` / `test-agno` jobs simplified — Rust toolchain set up before `pip install -e .` (which now invokes maturin via build-system). Removed the "build wheel + symlink .so" dance. `build` job swapped from `python -m build` (hatch) to `maturin build` + `maturin sdist`. - release.yml: collapsed dual-package matrix into one. New `build-wheels` matrix produces cross-platform wheels for cp310/11/12/13 × {linux x86_64, linux aarch64, macos x86_64, macos aarch64}. New `collect-dist` aggregator merges artifacts. publish-pypi consumes the merged dist. - init-native-e2e.yml: dropped windows-latest from the matrix — upstream `esaxx-rs` (/MT) and `ort-sys` (/MD) link with conflicting MSVC C runtime libraries, so the Rust extension cannot build for win_amd64 today. Tracked as a follow-up; not a blocker for Linux+macOS. - headroom-e2e-setup: composite action now sets up Rust toolchain + Swatinem/rust-cache before `pip install -e .[proxy]`. - eval.yml, publish.yml, rust.yml: same pattern — rust toolchain before install. rust.yml's wheels job builds from root pyproject.toml (no more `-m crates/headroom-py/Cargo.toml`). - e2e/init/Dockerfile, e2e/wrap/Dockerfile: install rust + maturin in the build stage; copy `crates/` + workspace `Cargo.toml/lock` so the install can build the extension. Dropped `HEADROOM_REQUIRE_RUST_CORE=false` from wrap-e2e — the image now ships the full Rust core. - Dockerfile (main): simplified — no more Layer 2/3 dance with `headroom-core-py` install + symlink. Single `uv pip install` builds + installs everything. - .devcontainer/Dockerfile: rust toolchain + libssl-dev + maturin added so `uv sync` builds the extension inside the devcontainer. ## Lockfile + script - uv.lock: regenerated. No `headroom-core-py` entries remain. - scripts/build_rust_extension.sh: simplified from a symlink-into-tree workaround to a thin wrapper around `pip install -e .`. The maturin build-backend handles placement automatically. ## Local validation (all green on macOS aarch64) 1. Clean venv `pip install -e .` → `from headroom._core import …` works. 2. `maturin build --release` → 13.8 MB wheel, 336 files including `headroom/_core.cpython-311-darwin.so` (32 MB cdylib) and `headroom/dashboard/templates/dashboard.html`. 3. `pip install <wheel>` in fresh venv → import works. 4. Wheel contents verified via `unzip -l`. 5. `pytest tests/test_transforms/test_diff_compressor.py` — 29 passed. 6. `pytest tests/test_relevance.py` — 30 passed. 7. `cargo build --workspace` + `cargo test --workspace` — all green. 8. `make ci-precheck` — 176 Python tests + Rust + commitlint green. ## Migration notes Users on `pip install headroom-ai` get the Rust core automatically (linux + macos wheels). sdist installs require rust toolchain available locally — pip will build via maturin. Closes #355 Supersedes #357 (workarounds-based fix abandoned in favor of architectural fix)
2026-05-03 13:16:41 -07:00
python-source = "."
module-name = "headroom._core"
# The cdylib source lives under `crates/headroom-py`. Maturin invokes
# `cargo build` with this manifest to produce `_core.cdylib`, then injects
# the resulting `.so` into the wheel at `headroom/_core.so`.
manifest-path = "crates/headroom-py/Cargo.toml"
features = ["extension-module"]
# Forbid building without the cdylib feature — bare `cargo build` won't
# produce a usable Python extension. Maturin's default `bindings` is "pyo3"
# which is correct here (see `crates/headroom-py/src/`).
bindings = "pyo3"
[tool.ruff]
target-version = "py310"
line-length = 100
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by formatter)
"B008", # do not perform function calls in argument defaults
"B905", # zip without strict parameter
]
[tool.ruff.lint.isort]
known-first-party = ["headroom"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
ignore_missing_imports = true
# Per-module overrides for modules with dynamic typing patterns
[[tool.mypy.overrides]]
module = [
"headroom.proxy.server",
"headroom.proxy.cost",
"headroom.proxy.prometheus_metrics",
"headroom.proxy.semantic_cache",
"headroom.proxy.rate_limiter",
"headroom.proxy.request_logger",
"headroom.proxy.helpers",
"headroom.integrations.langchain",
"headroom.integrations.mcp",
"headroom.ccr.mcp_server",
"headroom.relevance.embedding",
"headroom.reporting.generator",
]
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = [
"headroom.tokenizers.*",
"headroom.providers.litellm",
"headroom.providers.google",
]
disallow_untyped_defs = false
warn_return_any = false
# Handler mixins use self.* from HeadroomProxy via duck typing — mypy can't resolve these
[[tool.mypy.overrides]]
module = ["headroom.proxy.handlers.*"]
disallow_untyped_defs = false
ignore_errors = true
# Ignore third-party stubs with syntax errors
[[tool.mypy.overrides]]
module = ["mlx.*"]
ignore_errors = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_functions = ["test_*"]
addopts = "-v --tb=short"
asyncio_mode = "auto"
fix(transforms): use thread-local tree-sitter parsers to prevent pyo3 Unsendable panic (#604) ## Problem pyo3 marks `_native::Parser` as `#[pyclass(unsendable)]`, which causes a hard thread-assertion panic when a parser created on one thread is accessed from another: ``` thread '<unnamed>' panicked at pyo3-0.28.3/src/impl_/pyclass.rs:1055:9: assertion `left == right` failed: _native::Parser is unsendable, but sent to another thread left: ThreadId(2) right: ThreadId(1) ``` The prior implementation stored parsers in a module-level `dict[str, Any]` (`_tree_sitter_languages`). When `_run_compression_in_executor` dispatched compression work to a `ThreadPoolExecutor`, pool workers grabbed parsers from that shared dict that were originally created on the main asyncio thread and panicked. This produces a 500 on every request where code compression is attempted via a pool thread. ## Fix Replace the global dict with `threading.local()` so each thread creates and owns its own parser instances. No cross-thread parser access is possible. ```python # before _tree_sitter_languages: dict[str, Any] = {} # shared — crosses threads # after _tree_sitter_local = threading.local() # per-thread — isolated ``` `is_tree_sitter_loaded()` and `unload_tree_sitter()` updated to operate on the current thread's local cache (semantics unchanged for single-threaded callers). ## Tests 9 regression tests added in `tests/test_transforms/test_tree_sitter_thread_safety.py`: - Thread isolation: two threads get distinct parser instances - Within-thread reuse: same thread gets the same cached instance - Thread pool: parsers usable from `ThreadPoolExecutor` workers without panic - Concurrent workers: each distinct pool thread owns a unique parser - `is_tree_sitter_loaded` / `unload_tree_sitter` lifecycle Also adds a `filterwarnings` entry for `PytestUnraisableExceptionWarning`: pyo3 emits this when short-lived test threads drop parsers at teardown; it does not occur in production where pool threads are long-lived. ## Relation to #564 PR #564 proposes the same `threading.local()` approach but was blocked on missing tests (`CHANGES_REQUESTED`). This PR includes the full test suite.
2026-06-10 19:30:00 -04:00
filterwarnings = [
# pyo3 Unsendable parsers emit an unraisable warning when GC drops them on a
# test-teardown thread; this is a test-harness artifact, not a production issue
# (production threads are long-lived and drop their parsers on themselves).
"ignore::pytest.PytestUnraisableExceptionWarning",
]
markers = [
"slow: slow tests (model loads, large fixtures)",
"real_llm: tests that hit real LLM APIs; skipped unless explicitly enabled",
fix: Wave 3 — multi-turn live integration tests for A+B realignment Adds tests/test_realignment_live_multi_turn.py with 9 OPT-IN live tests that validate the load-bearing claims of the Phase A+B megamerge against real upstream APIs (Anthropic, OpenAI, Gemini). Each test maps to one or more realignment PRs: 1. test_anthropic_cache_hit_across_two_turns — A2/A6/E Identical cache_control'd system+messages on two turns must eventually produce cache_read_input_tokens > 0. Guards the cache hot zone invariant (I2): proxy must not mutate frozen prefix bytes. Uses a bounded retry loop (max 4 attempts) to absorb Anthropic's eventually-consistent prompt-cache write latency without masking a real "proxy broke cache stability" regression. 2. test_anthropic_cache_stable_when_live_zone_compresses — B2/B3 Turn 2 mutates only the LATEST user content (8KB+ JSON tail); cache_read on turn 2 must still be > 0 AND the proxy must emit compression headers — proving the live-zone block dispatcher ran on the new tail without disturbing the cached prefix. 3. test_anthropic_cache_control_passthrough_byte_faithful — A3/A4 Wraps proxy._retry_request to snapshot the upstream-bound body and assert cache_control on system blocks survives verbatim, and user content is not flattened from list to string form. 4. test_openai_chat_completions_multi_turn_through_proxy — A8/B Three-turn conversation through /v1/chat/completions; each turn returns valid content, prior assistant turns survive in the messages list (proxy doesn't drop them). 5. test_openai_streaming_sse_chunks_arrive_in_order — A8 (SSE wire) Streams /v1/chat/completions; asserts each event is 'data: ...\\n\\n', terminator is 'data: [DONE]\\n\\n', reassembled content non-empty, no malformed events. 6. test_gemini_multi_turn_through_proxy — Gemini reach Two-turn conversation through native /v1beta/models/{model}:generateContent. Proves Gemini handler wiring stayed intact through the megamerge. 7. test_ccr_marker_round_trip_live — B7 (CCR) Pre-populates compression_store with a fixture entry, embeds a CCR marker on a tool_result, verifies (a) headroom_retrieve tool is injected into the upstream tools array (PR-B7 always-on), and (b) /v1/retrieve returns the original bytes by hash with all rows intact. Pre-populating the Python store (vs. driving SmartCrusher's internal Rust store) matches the established pattern in tests/test_proxy_ccr.py and exercises the surface served by /v1/retrieve. 8. test_memory_tail_injection_does_not_modify_system_prompt_live — B6/A2 Spins up a memory-enabled proxy with MemoryMode.AUTO_TAIL, seeds LocalBackend, captures upstream-bound body. Asserts: (a) system prompt byte-identical to input; (b) memory text lands on latest user message tail; (c) earlier messages untouched. Guards the live-zone-only injection contract. 9. test_classify_auth_mode_routes_payg_vs_oauth — Phase F-prep / B5 NOT a live API call. Sends three header shapes through the proxy (x-api-key=..., Bearer sk-ant-oat01-..., Bearer sk-ant-api03-...), captures dispatcher headers via a wrap on _retry_request, and asserts the canonical auth-mode classifier maps each correctly. Codifies the Phase F contract. Conventions: * file-level pytestmark = pytest.mark.live → excluded by default via 'pytest -m "not live"'. Adds a 'live' marker registration in pyproject.toml's [tool.pytest.ini_options].markers. * each test skipif's on the relevant API key — no silent fallbacks, no real-API runs against fake keys. * uses tests/_dotenv.py helpers (load_env_overrides + autouse_apply_env) rather than re-implementing env loading. * model IDs and thresholds live in a top-of-file LIVE_CONFIG dict (no hardcodes); Anthropic primary/fallback resolves at runtime per key entitlement. * assertions are direction-only (cache_read > 0, tokens_after <= tokens_before) — never tied to upstream pricing/tokenizer drift. * shared module-scoped TestClient fixture for performance; CCR and memory tests build dedicated proxies for their config-specific paths. Verification: * pytest tests/test_realignment_live_multi_turn.py -v → 9 passed, 0 skipped, 0 failed in ~25s (with all keys set) * pytest -m "not live" --tb=short -q → 4694 passed, 265 skipped, 9 deselected — same baseline as today * make ci-precheck → green (rust + python + commitlint) Per-realignment-plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 17:38:15 -07:00
"live: opt-in multi-turn tests that hit real upstream APIs; require provider keys",
]
[tool.coverage.run]
source = ["headroom"]
branch = true
omit = [
"headroom/cli.py",
"*/tests/*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise NotImplementedError",
"if TYPE_CHECKING:",
"if __name__ == .__main__.:",
]