Commit graph

48 commits

Author SHA1 Message Date
Ruben A.
e530de5ad2
feat(rust): port CodeCompressor AST compressor to Rust (parity-only) (#1154)
Adds crates/headroom-core/src/transforms/code_compressor.rs (1,882 lines): the
AST-aware CodeCompressor ported to Rust on tree-sitter, with grammars for
Python, JavaScript, TypeScript, Go, Rust, Java, C and C++.

Parity-only, like #1153. Nothing calls it: the only references outside the
module are the pub mod / pub use declarations in transforms/mod.rs, and
live_zone.rs still routes SourceCode to a no-op. The pyo3 bridge is untouched
and no Python source changes, so the engine is unreachable from the shipped
package. #1155 wires it into live-zone dispatch.

Every grammar is pinned with '=' to the exact version of the corresponding
Python tree-sitter-<lang> PyPI wheel. Same version on crates.io and PyPI means
the same grammar.js, hence the same generated parser.c, hence node-for-node
identical ASTs — the precondition for byte-parity. A canary over 9 samples x 8
languages confirmed identical node-type and line-span trees at these pins;
bumping any pin requires re-running it and re-recording the fixtures.

Ships 30 recorded parity fixtures, a CodeCompressorComparator in
headroom-parity, and scripts/record_code_compressor_fixtures.py.

Verified byte-identical to the recorded Python output:

  [code_aware_compressor] total=30 matched=30 skipped=0 diffed=0

Full harness on the merge result: 227 fixtures, 182 matched, 45 skipped
(cache_aligner + ccr stubs), 0 diffed, exit 0 — with kompress at 21/21 under
ONNX Runtime 1.24.4 (see #2591).

Also verified cargo check -p headroom-core --no-default-features passes, so the
static-musl path stays intact.
2026-07-27 09:21:57 -07:00
dependabot[bot]
3266ed7641
deps: bump the cargo-minor-patch group with 10 updates (#2284)
Bumps the cargo-minor-patch group with 10 updates:

| Package | From | To |
| --- | --- | --- |
| [clap](https://github.com/clap-rs/clap) | `4.6.1` | `4.6.2` |
| [aws-sigv4](https://github.com/smithy-lang/smithy-rs) | `1.4.5` |
`1.5.1` |
| [aws-config](https://github.com/smithy-lang/smithy-rs) | `1.8.18` |
`1.9.0` |
| [regex](https://github.com/rust-lang/regex) | `1.12.4` | `1.13.1` |
| [toml](https://github.com/toml-rs/toml) | `1.1.2+spec-1.1.0` |
`1.1.3+spec-1.1.0` |
| [fastembed](https://github.com/Anush008/fastembed-rs) | `5.17.2` |
`5.17.3` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.4` | `1.24.0` |
| [http-body-util](https://github.com/hyperium/http-body) | `0.1.3` |
`0.1.4` |
| [lru](https://github.com/jeromefroe/lru-rs) | `0.18.0` | `0.18.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.66` | `1.2.67` |

Updates `clap` from 4.6.1 to 4.6.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/releases">clap's
releases</a>.</em></p>
<blockquote>
<h2>v4.6.2</h2>
<h2>[4.6.2] - 2026-07-15</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Say <code>alias</code> when there is only one</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/clap-rs/clap/blob/master/CHANGELOG.md">clap's
changelog</a>.</em></p>
<blockquote>
<h2>[4.6.2] - 2026-07-15</h2>
<h3>Fixes</h3>
<ul>
<li><em>(help)</em> Say <code>alias</code> when there is only one</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="0fe0be3027"><code>0fe0be3</code></a>
chore: Release</li>
<li><a
href="480af9d045"><code>480af9d</code></a>
docs: Update changelog</li>
<li><a
href="2b3ddd0294"><code>2b3ddd0</code></a>
Merge pull request <a
href="https://redirect.github.com/clap-rs/clap/issues/6340">#6340</a>
from liskin/fix-completion-escape</li>
<li><a
href="7ffe7399ff"><code>7ffe739</code></a>
fix(complete): Do not suggest options after &quot;--&quot;</li>
<li><a
href="d47fc4f8a5"><code>d47fc4f</code></a>
test(complete): Options suggested after escape (<code>--</code>)</li>
<li>See full diff in <a
href="https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.2">compare
view</a></li>
</ul>
</details>
<br />

Updates `aws-sigv4` from 1.4.5 to 1.5.1
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/smithy-lang/smithy-rs/commits">compare
view</a></li>
</ul>
</details>
<br />

Updates `aws-config` from 1.8.18 to 1.9.0
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/smithy-lang/smithy-rs/commits">compare
view</a></li>
</ul>
</details>
<br />

Updates `regex` from 1.12.4 to 1.13.1
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/regex/blob/master/CHANGELOG.md">regex's
changelog</a>.</em></p>
<blockquote>
<h1>1.13.1 (2026-07-15)</h1>
<p>This is a release that fixes a bug where incorrect regex match
offsets could be
reported. Note that this doesn't impact whether a match occurs or not,
just
where it occurs. The match offsets are still valid for slicing, they
just may
not refer to the correct leftmost-first match. See
<a
href="https://redirect.github.com/rust-lang/regex/pull/1364">#1364</a>
for (many) more details.</p>
<p>Bug fixes:</p>
<ul>
<li><a
href="https://redirect.github.com/rust-lang/regex/issues/1354">#1354</a>:
Fixes previously unsound reverse suffix and inner optimizations.</li>
</ul>
<h1>1.13.0 (2026-07-09)</h1>
<p>This release includes a new API, a <code>regex!</code> macro, for
lazy compilation of
a regex from a string literal. If you use regexes a lot, it's likely
you've
already written one exactly like it. The new macro can be used like
this:</p>
<pre lang="rust"><code>use regex::regex;
<p>fn is_match(line: &amp;str) -&gt; bool {<br />
// The regex will be compiled approximately once and reused
automatically.<br />
// This avoids the footgun of using <code>Regex::new</code> here, which
would<br />
// guarantee that it would be compiled every time this routine is
called.<br />
// This would likely make this routine much slower than it needs to
be.<br />
regex!(r&quot;bar|baz&quot;).is_match(line)<br />
}</p>
<p>let hay = &quot;<br />
path/to/foo:54:Blue Harvest<br />
path/to/bar:90:Something, Something, Something, Dark Side<br />
path/to/baz:3:It's a Trap!<br />
&quot;;</p>
<p>let matches = hay.lines().filter(|line| is_match(line)).count();<br
/>
assert_eq!(matches, 2);<br />
</code></pre></p>
<p>Improvements:</p>
<ul>
<li><a
href="https://redirect.github.com/rust-lang/regex/issues/709">#709</a>:
Add a new <code>regex!</code> macro for efficient and automatic reuse of
a compiled regex.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="2b527599eb"><code>2b52759</code></a>
1.13.1, redux</li>
<li><a
href="40e98238ff"><code>40e9823</code></a>
1.13.1</li>
<li><a
href="75fcb962d6"><code>75fcb96</code></a>
changelog: 1.13.1</li>
<li><a
href="64ad0b618e"><code>64ad0b6</code></a>
automata: fix bug in reverse suffix/inner optimization</li>
<li><a
href="fa91c31a42"><code>fa91c31</code></a>
automata: fix a bug caught by Codex review</li>
<li><a
href="30390ec3e8"><code>30390ec</code></a>
automata: formatting tweaks</li>
<li><a
href="821a8eb1ad"><code>821a8eb</code></a>
automata: refactor reverse suffix/inner search slightly</li>
<li><a
href="10afd704d8"><code>10afd70</code></a>
automata: expose the extracted literals for inner literal
extraction</li>
<li><a
href="8c34f41d3c"><code>8c34f41</code></a>
automata: avoid reverse suffix optimization for non-leftmost-first</li>
<li><a
href="5524f02430"><code>5524f02</code></a>
test: add regression tests for failed reverse suffix/inner
optimizations</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/regex/compare/1.12.4...1.13.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `toml` from 1.1.2+spec-1.1.0 to 1.1.3+spec-1.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="eb251609a3"><code>eb25160</code></a>
chore: Release</li>
<li><a
href="f36fb52c37"><code>f36fb52</code></a>
docs: Update changelog</li>
<li><a
href="3adbbb7860"><code>3adbbb7</code></a>
fix(writer): Don't overflow (<a
href="https://redirect.github.com/toml-rs/toml/issues/1189">#1189</a>)</li>
<li><a
href="fb0c1b376d"><code>fb0c1b3</code></a>
fix(writer): Don't overflow</li>
<li><a
href="5e70a7031d"><code>5e70a70</code></a>
test(writer): Add overflow test</li>
<li><a
href="771a975840"><code>771a975</code></a>
chore: Upgrade toml-test (<a
href="https://redirect.github.com/toml-rs/toml/issues/1186">#1186</a>)</li>
<li><a
href="28f6c9cbc8"><code>28f6c9c</code></a>
chore: Upgrade toml-test</li>
<li><a
href="30d75ca443"><code>30d75ca</code></a>
chore(deps): Update Prek to v0.4.9 (<a
href="https://redirect.github.com/toml-rs/toml/issues/1185">#1185</a>)</li>
<li><a
href="17efe57deb"><code>17efe57</code></a>
chore(deps): Update Rust Stable to v1.97 (<a
href="https://redirect.github.com/toml-rs/toml/issues/1184">#1184</a>)</li>
<li><a
href="c9d0d54b21"><code>c9d0d54</code></a>
style: Make clippy happy</li>
<li>Additional commits viewable in <a
href="https://github.com/toml-rs/toml/compare/toml-v1.1.2...toml-v1.1.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `fastembed` from 5.17.2 to 5.17.3
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Anush008/fastembed-rs/releases">fastembed's
releases</a>.</em></p>
<blockquote>
<h2>v5.17.3</h2>
<h2><a
href="https://github.com/Anush008/fastembed-rs/compare/v5.17.2...v5.17.3">5.17.3</a>
(2026-07-15)</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): update candle-core requirement from 0.10.2 to 0.11.0 by
<a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/Anush008/fastembed-rs/pull/270">Anush008/fastembed-rs#270</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/Anush008/fastembed-rs/compare/v5.17.2...v5.17.3">https://github.com/Anush008/fastembed-rs/compare/v5.17.2...v5.17.3</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="4e9dc551ba"><code>4e9dc55</code></a>
chore(release): 5.17.3 [skip ci]</li>
<li><a
href="3fd10f984f"><code>3fd10f9</code></a>
chore(deps): update candle-core requirement from 0.10.2 to 0.11.0 (<a
href="https://redirect.github.com/Anush008/fastembed-rs/issues/270">#270</a>)</li>
<li>See full diff in <a
href="https://github.com/Anush008/fastembed-rs/compare/v5.17.2...v5.17.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `uuid` from 1.23.4 to 1.24.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/uuid-rs/uuid/releases">uuid's
releases</a>.</em></p>
<blockquote>
<h2>v1.24.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(fmt): support encoding into MaybeUninit buffers by <a
href="https://github.com/weifanglab"><code>@​weifanglab</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/892">uuid-rs/uuid#892</a></li>
<li>Prepare for 1.24.0 release by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/896">uuid-rs/uuid#896</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/weifanglab"><code>@​weifanglab</code></a> made
their first contribution in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/892">uuid-rs/uuid#892</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.5...v1.24.0">https://github.com/uuid-rs/uuid/compare/v1.23.5...v1.24.0</a></p>
<h2>v1.23.5</h2>
<h2>What's Changed</h2>
<ul>
<li>doc: Fix broken link by <a
href="https://github.com/frostyplanet"><code>@​frostyplanet</code></a>
in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/891">uuid-rs/uuid#891</a></li>
<li>perf: Optimize UUID hex parsing and formatting by <a
href="https://github.com/geeknoid"><code>@​geeknoid</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/894">uuid-rs/uuid#894</a></li>
<li>Prepare for 1.23.5 release by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/895">uuid-rs/uuid#895</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/geeknoid"><code>@​geeknoid</code></a>
made their first contribution in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/894">uuid-rs/uuid#894</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.4...v1.23.5">https://github.com/uuid-rs/uuid/compare/v1.23.4...v1.23.5</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="6a8aeab3d0"><code>6a8aeab</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/896">#896</a> from
uuid-rs/cargo/v1.24.0</li>
<li><a
href="e6db8ec087"><code>e6db8ec</code></a>
prepare for 1.24.0 release</li>
<li><a
href="606f2365c7"><code>606f236</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/892">#892</a> from
weifanglab/main</li>
<li><a
href="ab848dbdf6"><code>ab848db</code></a>
feat(fmt): support encoding into MaybeUninit buffers</li>
<li><a
href="5dc6b3d1a9"><code>5dc6b3d</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/895">#895</a> from
uuid-rs/cargo/v1.23.5</li>
<li><a
href="5a7dfe50e2"><code>5a7dfe5</code></a>
prepare for 1.23.5 release</li>
<li><a
href="9b4bfc8fe3"><code>9b4bfc8</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/894">#894</a> from
geeknoid/main</li>
<li><a
href="5acc5a550e"><code>5acc5a5</code></a>
perf: Optimize UUID hex parsing and formatting</li>
<li><a
href="6fa1a1e38a"><code>6fa1a1e</code></a>
feat(fmt): support encoding into MaybeUninit buffers</li>
<li><a
href="1e5d867954"><code>1e5d867</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/891">#891</a> from
frostyplanet/doc</li>
<li>Additional commits viewable in <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.4...v1.24.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `http-body-util` from 0.1.3 to 0.1.4
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/hyperium/http-body/releases">http-body-util's
releases</a>.</em></p>
<blockquote>
<h2>http-body-util-v0.1.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Add <code>Fused</code> body combinator that always returns
<code>None</code> once completed.</li>
<li>Add <code>BodyExt::into_stream()</code> to convert a body into a
<code>Stream</code>.</li>
<li>Add <code>Full::into_inner()</code> to get the full
<code>Buf</code>.</li>
<li>Add <code>InspectFrame</code> and <code>InspectErr</code>
combinators.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="768504721c"><code>7685047</code></a>
http-body-util v0.1.4</li>
<li><a
href="3396328602"><code>3396328</code></a>
http-body v1.1.0</li>
<li><a
href="2fb78de9c8"><code>2fb78de</code></a>
chore: bump license year (<a
href="https://redirect.github.com/hyperium/http-body/issues/170">#170</a>)</li>
<li><a
href="b16554b604"><code>b16554b</code></a>
chore(ci): bump checkout to v7</li>
<li><a
href="c0c53caee7"><code>c0c53ca</code></a>
chore(ci): use msrv aware update for msrv job</li>
<li><a
href="5ed15d2c3d"><code>5ed15d2</code></a>
tests: fix clippy::double_parens</li>
<li><a
href="c8cb37f9ce"><code>c8cb37f</code></a>
Derive <code>Copy</code> trait to <code>SizeHint</code> struct (<a
href="https://redirect.github.com/hyperium/http-body/issues/164">#164</a>)</li>
<li><a
href="915d6d5cbb"><code>915d6d5</code></a>
feat(util): add <code>InspectErr</code>, <code>InspectFrame</code>
combinators (<a
href="https://redirect.github.com/hyperium/http-body/issues/161">#161</a>)</li>
<li><a
href="0fc0a9415c"><code>0fc0a94</code></a>
docs: fix broken intradoc links (<a
href="https://redirect.github.com/hyperium/http-body/issues/162">#162</a>)</li>
<li><a
href="5a849d49dc"><code>5a849d4</code></a>
chore: add FUNDING.yml</li>
<li>Additional commits viewable in <a
href="https://github.com/hyperium/http-body/compare/http-body-util-v0.1.3...http-body-util-v0.1.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `lru` from 0.18.0 to 0.18.1
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/jeromefroe/lru-rs/blob/master/CHANGELOG.md">lru's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/jeromefroe/lru-rs/tree/0.18.1">v0.18.1</a> -
2026-07-09</h2>
<ul>
<li>Add <code>find_and_promote</code> method.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c6620d1165"><code>c6620d1</code></a>
Merge pull request <a
href="https://redirect.github.com/jeromefroe/lru-rs/issues/237">#237</a>
from jeromefroe/jerome/prepare-0-18-1-release</li>
<li><a
href="da3c0fc276"><code>da3c0fc</code></a>
Prepare 0.18.1 release</li>
<li><a
href="11662633b9"><code>1166263</code></a>
Merge pull request <a
href="https://redirect.github.com/jeromefroe/lru-rs/issues/236">#236</a>
from pixmaip/find-and-promote</li>
<li><a
href="2daba12567"><code>2daba12</code></a>
Apply suggestions from code review</li>
<li><a
href="2ea00dc674"><code>2ea00dc</code></a>
feat: add find_and_promote API</li>
<li>See full diff in <a
href="https://github.com/jeromefroe/lru-rs/compare/0.18.0...0.18.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `cc` from 1.2.66 to 1.2.67
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/cc-rs/releases">cc's
releases</a>.</em></p>
<blockquote>
<h2>cc-v1.2.67</h2>
<h3>Other</h3>
<ul>
<li>Fix clippy warning (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1788">#1788</a>)</li>
<li>Regenerate target info (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1785">#1785</a>)</li>
<li>Add support for <code>aarch64-unknown-linux-pauthtest</code> target
(<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1713">#1713</a>)</li>
<li>Fix nightly compilation error (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1783">#1783</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md">cc's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/rust-lang/cc-rs/compare/cc-v1.2.66...cc-v1.2.67">1.2.67</a>
- 2026-07-11</h2>
<h3>Other</h3>
<ul>
<li>Fix clippy warning (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1788">#1788</a>)</li>
<li>Regenerate target info (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1785">#1785</a>)</li>
<li>Add support for <code>aarch64-unknown-linux-pauthtest</code> target
(<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1713">#1713</a>)</li>
<li>Fix nightly compilation error (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1783">#1783</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fa031a077a"><code>fa031a0</code></a>
chore(cc): release v1.2.67 (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1789">#1789</a>)</li>
<li><a
href="842aab16d2"><code>842aab1</code></a>
Bump taiki-e/install-action from 2.81.8 to 2.82.8 (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1786">#1786</a>)</li>
<li><a
href="e2f07d0d68"><code>e2f07d0</code></a>
Fix clippy warning (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1788">#1788</a>)</li>
<li><a
href="8ced615d2c"><code>8ced615</code></a>
Regenerate target info (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1785">#1785</a>)</li>
<li><a
href="2943b52512"><code>2943b52</code></a>
Add missing todo for deprecated API</li>
<li><a
href="43ae1bf3ff"><code>43ae1bf</code></a>
Add support for <code>aarch64-unknown-linux-pauthtest</code> target (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1713">#1713</a>)</li>
<li><a
href="a5c584a1fa"><code>a5c584a</code></a>
Fix nightly compilation error (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1783">#1783</a>)</li>
<li>See full diff in <a
href="https://github.com/rust-lang/cc-rs/compare/cc-v1.2.66...cc-v1.2.67">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-21 11:20:59 -05:00
Zhenjia ZHOU
4035c04187
feat(text-crusher): CJK-aware segmentation + relevance via ICU (#1504)
## Description

`TextCrusher` (the native extractive prose compressor added in #1171)
only handled ASCII: `split_segments` split on `.!?`+whitespace and
`tokens` split on whitespace/alphanumeric runs. CJK
(Chinese/Japanese/Korean) has neither spaces nor ASCII terminators, so a
whole CJK paragraph collapsed into **one segment / one token** — it
passed through at ~0% compression, and BM25 relevance + salience scored
zero terms.

This makes `TextCrusher` CJK-aware. CJK-bearing content takes an ICU
(`icu_segmenter`, UAX#29 sentence + dictionary word) segmentation path,
with a length fallback for terminator-sparse runs, a local BM25 over the
ICU word tokens, and ICU-token salience. Dispatch is on **content
only**, so pure-ASCII text is byte-identical to before — the shared
`BM25Scorer` and the ASCII path are untouched.

It also adds a committed, reproducible answer-retention eval
(`benchmarks/i18n_compression_eval.py`) with a deterministic zh/ja/ko CI
regression gate, so the improvement below is permanently verifiable
rather than a one-off measurement.

Extends #1171.

## Type of Change

- [x] Bug fix (CJK passed through near-uncompressed)
- [x] New feature (CJK segmentation / relevance support)
- [x] Performance improvement (CJK now compresses; ICU segmenters
cached, not rebuilt per call)

## Changes Made

- `is_cjk` predicate gates a CJK path (ideographs, kana, Hangul, CJK
punctuation, full/half-width forms).
- `split_segments` → ICU `SentenceSegmenter` for CJK + a mandatory
length fallback (whitespace / CJK punctuation / hard cap) for
terminator-sparse runs; ASCII path unchanged.
- `tokens` → ICU `WordSegmenter` (dictionary) for CJK; ASCII path
unchanged.
- `relevance_cjk`: a local BM25 over ICU word tokens — the shared ASCII
`BM25Scorer` scores zero terms for CJK and is parity-locked, so this is
an intentional separate scorer (documented in code).
- CJK salience uses ICU tokens (whitespace-split gave one giant "word" →
zero salience).
- `count_tokens`: CJK-aware so `compression_ratio` isn't nonsense for
space-free text.
- ICU segmenters resolved once in `static LazyLock` (compiled_data is
static) instead of rebuilt per call.
- New dep `icu_segmenter` 2.2, `compiled_data` only (see Dependency
below).
- `benchmarks/i18n_compression_eval.py` +
`tests/test_transforms/test_text_crusher_cjk_eval.py`: a zh/ja/ko
answer-retention eval — a deterministic needle CI gate (always-runs, no
external data), real-transcript fidelity with CJK-aware salient, and
optional `multi-wiki-qa` natural-data retention (loaded via the
`[evals]` `datasets` extra, skipped if absent; data never vendored —
CC-BY-NC-SA).

## Testing

- [x] Unit tests pass (`pytest` + `cargo test`)
- [x] Linting passes (`ruff check`/`format` on the new eval + test —
clean)
- [ ] Type checking passes (`mypy headroom`) — N/A, the only Python
added is a benchmark + test, not `headroom/` source
- [x] New tests added for new functionality
- [x] Manual testing performed (see Real Behavior Proof)

### Test Output

```text
$ cargo test -p headroom-core --lib text_crusher
running 12 tests
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 841 filtered out

$ .venv/bin/python -m pytest tests/test_transforms/test_text_crusher*.py
15 passed

$ .venv/bin/python -m pytest tests/test_transforms/test_text_crusher_cjk_eval.py
6 passed   # deterministic zh/ja/ko needle CI gate

$ cargo clippy -p headroom-core && ruff check benchmarks/i18n_compression_eval.py   # both clean
```

## Real Behavior Proof

- Environment: macOS (Darwin 25.3.0), Python in a uv venv,
`headroom-core` built via `uv pip install -e .` (maturin), branch
`feat/cjk-text-compression`.
- Exact command / steps: built `_core`, then ran a mixed
Chinese+Japanese doc (no spaces, `。` terminators) through
`TextCrusher().compress(doc, "认证令牌缓存策略", 0.3)`; separately evaluated
answer-retention on the public CMRC2018 Chinese QA dev set (bury the
gold-answer paragraph among 25 distractors, query = the question,
compress to 30%, check the gold answer survives), and end-to-end through
`ContentRouter`.
- Observed result: a mixed Chinese+Japanese doc compressed 189 → 78
tokens (ratio 0.41, kept 3/8 segments) with the query-relevant sentence
surviving — before this change the same doc was a single segment → 100%
passthrough. On the public CMRC2018 Chinese QA dev set, answer-retention
under 30% compression rose 34% → 93% (multiple seeds). End-to-end
through `ContentRouter` on real CJK content, aggregate savings rose 16%
→ 40%. Pure-ASCII (English) output stayed byte-identical (the English
parity fixtures did not move). Demo terminal output:

    ```text
    ORIGINAL  tokens= 189  chars=189
    COMPRESS  tokens=  78  ratio=0.41  segments kept 3/8
    QUERY-RELEVANT sentence survived: True
    --- compressed output (verbatim kept CJK sentences) ---
    认证令牌的缓存策略采用最近最少使用淘汰算法来管理过期条目。
    请求重试使用指数退避并设置最大次数上限。
    数据备份每天凌晨执行并保留最近三十天的快照。
    ```
The committed eval now demonstrates this across all three CJK languages.
The deterministic needle gate (in CI via
`tests/test_transforms/test_text_crusher_cjk_eval.py`, 6 passed) has
TextCrusher keep the query-relevant needle while truncate/random drop it
in zh, ja, and ko. On real `multi-wiki-qa` natural data (n=80/lang),
query-aware answer-retention is **zh 74% / ja 70% / ko 50%** vs
**25–41%** for the truncate/random baselines:

    ```text
=== Part A: multi-wiki-qa answer-retention (n=80/lang, target_ratio=0.3)
===
      lang    text_crusher  truncate  random
      zh-cn           74%       25%     38%
      ja              70%       31%     39%
      ko              50%       26%     41%
    ```
Korean is measurably weaker (ICU has no Korean dictionary and falls back
to UAX#29 word-breaking) — still well above baselines, and scoped as a
follow-up.
- Not tested: the live proxy HTTP path (validated at the `ContentRouter`
/ `TextCrusher` layer, not via a running proxy); no-space Korean
(standard Korean is space-delimited and is covered); non-CJK SE-Asian
scripts (out of scope).

## Dependency (per CONTRIBUTING supply-chain policy)

`icu_segmenter` 2.2 (ICU4X), `features = ["compiled_data"]`:

- **Why this package (vs. ourselves / existing deps):** CJK needs
dictionary/UAX#29 segmentation. A hand-rolled char-bigram scored
slightly worse on real data (CMRC2018 answer-retention: 92.5% ICU vs 91%
bigram, 4 seeds); jieba/lindera are ZH-only or 13–207 MB dicts. ICU4X
covers zh/ja/ko in one crate. The existing `unicode-segmentation` does
UAX#29 only (no CJK dictionary), so it can't word-segment space-free
CJK.
- **Who maintains it:** the official `unicode-org` ICU4X project; active
release cadence (2.2 in 2025); no known CVEs.
- **Install surface:** ~13 new pure-Rust crates, no build scripts, no
native code, no build/runtime network. `compiled_data` bundles locale
data at compile time (hermetic). `auto`/`lstm` deliberately NOT enabled
— LSTM covers SE-Asian scripts (Thai/Lao), not CJK, and would pull in
`libm` for nothing.
- **Why this version:** 2.x is the stabilized ICU4X API (1.x used a
different data-provider model); floored at 2.2 (Cargo.lock pins the
patch) since segmenter boundaries are observable in output and bumps
should be deliberate.

## 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 (CHANGELOG)
- [x] My changes generate no new warnings (clippy + fmt clean)
- [x] I have added tests that prove my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md

## Additional Notes

- **Parity:** the shared `BM25Scorer` (byte-exact parity-locked with
`headroom/relevance/bm25.py`) is untouched. `relevance_cjk` is a
separate local scorer because the shared one's tokenizer is ASCII-only.
The whole CJK path lives in Rust (`text_crusher.py` is a thin wrapper
over `_core`), so there is no Python mirror to keep in sync; the parity
fixtures stay green (only the CJK `unicode` fixture was re-recorded,
intentionally; English fixtures unchanged).
- **Known by-design gap (not a bug):** CJK content + a pure-ASCII query
yields no token overlap, so relevance falls back to recency + salience
(cross-script query matching is unsupported).
- The Python added is a benchmark
(`benchmarks/i18n_compression_eval.py`) plus its test, not `headroom/`
runtime source — both are `ruff`-clean; `mypy headroom` is unaffected.
- **License:** the optional Part A pulls `alexandrainst/multi-wiki-qa`
(CC-BY-NC-SA-4.0) at run time via the `[evals]` extra and is skipped if
absent — the dataset is never vendored into the repo, and the always-run
CI gate (Part C) uses only our own deterministic data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 19:58:48 +00:00
Parideboy
c46cd8f950
fix(core): load ONNX Runtime dynamically so headroom._core imports on non-AVX2 x86-64 (#1715)
## Description

`import headroom._core` dies with SIGILL (`Illegal instruction`) on
x86-64 CPUs without AVX2 (Pentium N4200, Celeron N4500, AMD FX 8350 —
all reported on the issue). The repo sets no `RUSTFLAGS`/`target-cpu`
anywhere, so first-party Rust code is baseline x86-64; the AVX2 code
comes from Microsoft's prebuilt ONNX Runtime, statically linked into the
extension by fastembed's `ort-download-binaries-rustls-tls` feature on
non-Windows targets. Because it is statically linked, its code is mapped
and initialized when the extension module loads — **before** the runtime
AVX2 guard from #1162 can run, which is why that fix helped Magika init
but not the import-time crash.

Fix, mirroring what Windows already does for its own reasons (DirectML
link libs): build with `ort-load-dynamic` on every platform, so ONNX
Runtime is only `dlopen`'d at first use, where the #1162 AVX2 guard
falls back to the non-ONNX detection tiers on unsupported CPUs. Since
both target blocks became identical, they are collapsed into one
platform-independent `fastembed` dependency.

To keep Magika/fastembed working out of the box on Linux/macOS, the
existing `ORT_DYLIB_PATH` auto-pin (`headroom/_ort.py`, previously
Windows-only) now resolves the pip `onnxruntime` package's shared
library on all platforms (`onnxruntime.dll` / `libonnxruntime.so*` /
`libonnxruntime*.dylib`). The pip `onnxruntime` CPU wheels use runtime
CPU dispatch, so they also work on pre-AVX2 machines — non-AVX2 users
get working ML detection instead of a crash. Without the `onnxruntime`
package, ML detection degrades gracefully to the non-ONNX tiers exactly
as it already does on Windows.

Fixes #1278

## 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

- `crates/headroom-core/Cargo.toml`: replaced the per-target `fastembed`
blocks (`ort-download-binaries-rustls-tls` on non-Windows,
`ort-load-dynamic` on Windows) with a single platform-independent
dependency on `ort-load-dynamic`, with a comment documenting both the
DirectML and the AVX2/#1278 rationale.
- `Cargo.lock`: regenerated — `ort-sys` drops its static-download
dependencies (`hmac-sha256`, `lzma-rust2`, `ureq`); no version bumps.
- `headroom/_ort.py`: `ORT_DYLIB_PATH` auto-pin extended from
Windows-only to all platforms via a small `_find_dylib` helper that
resolves the platform's shared-library name inside the pip `onnxruntime`
package.
- `tests/test_transforms/test_ort_dylib.py`: replaced the obsolete
`test_noop_on_non_windows` with Linux (versioned `.so`) and macOS
(`.dylib`) pin tests; module docstring updated.
- `docs/content/docs/configuration.mdx`: `ORT_DYLIB_PATH` row updated
from Windows-only wording to the cross-platform behavior.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings && cargo test -p headroom-core --lib
clean
test result: 844 passed; 0 failed; 1 ignored

$ python -m pytest tests/test_transforms/test_ort_dylib.py -q
8 passed

$ ruff check headroom/_ort.py tests/test_transforms/test_ort_dylib.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11 (AVX2-capable — the SIGILL itself is not
reproducible on this machine), Python 3.13, Rust 1.95.0, local checkout
branched from `upstream/main` (9fbd47ba).
- Exact command / steps: `cargo check -p headroom-core` after the
feature switch; inspected the `Cargo.lock` diff; rebuilt and ran `python
-c "import headroom; from headroom._core import detect_content_type;
print(detect_content_type('hello world'))"`; ran the ort-pin test suite
with monkeypatched `linux`/`darwin` platforms.
- Observed result: build succeeds with `ort-load-dynamic`; the lockfile
shows `ort-sys` no longer pulls the binary-download machinery
(`hmac-sha256`, `lzma-rust2`, `ureq` removed), confirming the
statically-linked prebuilt ORT is gone; import + content detection works
with `ORT_DYLIB_PATH` auto-pinned to the pip onnxruntime library; all 8
pin tests pass including the new Linux/macOS branches.
- Not tested: actual pre-AVX2 x86-64 hardware (none available — the fix
removes AVX2 code from the import path by construction, and the issue
reporters on #1278 can verify); Linux/macOS wheel runtime behavior
beyond CI's ubuntu/macOS wheel-build jobs; embedding quality/performance
under a pip-provided ORT version differing from the previously vendored
one.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 13:25:41 -04:00
JD Davis
2c9eb7c5f1
feat(simulators): add provider simulator service (#2014)
## Description  
Adds a Rust-only `headroom-simulators` workspace crate: a deterministic
local upstream simulator service for Headroom proxy and pipeline
validation. It supplies configurable stubs plus bottled provider-shaped
responses for supported provider/path surfaces without calling real
LLMs.

## Type of Change  
- [x] Bug fix (non-breaking change that fixes an issue)  
- [x] New feature (non-breaking change that adds functionality)  
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update  
- [ ] Performance improvement  
- [ ] Code refactoring (no functional changes)

## Changes Made  
- Added `crates/headroom-simulators` Rust crate with library and
`headroom-simulators` binary.
- Added clean domain classification for supported surfaces: Anthropic
`/v1/messages`, OpenAI chat/responses/conversations, Bedrock
invoke/stream routes, Vertex raw/stream predict, health, and generic
fallback.
- Added JSON-configured stub matching by method, path, body substring,
and JSON pointer.
- Added bottled provider-shaped JSON, SSE, and Bedrock EventStream
responses for unconfigured requests.
- Added a container `Dockerfile` and README for local/GitHub Actions
usage.
- Added unit and HTTP integration tests for defaults, configured stubs,
SSE, Vertex, and Bedrock EventStream behavior.
- Added proxy-level simulator-backed E2E tests that run Headroom against
the simulator across Anthropic, OpenAI Chat, OpenAI Responses, OpenAI
Conversations, Bedrock invoke/converse/streaming, Vertex raw/stream
predict, and upstream health.
- Added simulator-backed provider error-path E2E coverage for OpenAI
429, Anthropic 529, Bedrock 502, and Vertex 503 responses flowing
through Headroom unchanged.
- Added Headroom-owned preflight error E2E coverage proving Bedrock
missing credentials and invalid Vertex envelopes stop inside the proxy
instead of silently falling through to the simulator/provider.
- Fixed direct Rust `headroom-core` binaries/tests on Windows so Magika
initializes ONNX Runtime via `ort::init_from` from an explicit pip
`onnxruntime` library path, with fail-fast fallback only when no safe
runtime is discoverable.
- Added a Rust CI `simulator-e2e` matrix for `ubuntu-latest`,
`macos-latest`, and `windows-latest` that runs `cargo test -p
headroom-proxy --test e2e_simulators`.
- Gated dynamic Magika `Path`/`PathBuf` imports to Windows and x86_64
macOS so Linux clippy does not see unused dynamic-ORT-only imports.

## Testing  
- [ ] Unit tests pass (`pytest`)  
- [ ] Linting passes (`ruff check .`)  
- [ ] Type checking passes (`mypy headroom`)  
- [x] New tests added for new functionality  
- [x] Manual testing performed

### Test Output  
cargo fmt --all -- --check  
# passed  

cargo clippy --workspace -- -D warnings  
# passed  

$env:ORT_DYLIB_PATH=$null  
cargo test -p headroom-core transforms::magika_detector::tests:: --lib  
# 17 passed, 0 failed; Magika initialized from discovered pip
onnxruntime DLL

$env:ORT_DYLIB_PATH=$null  
cargo test --workspace  
# passed  

gitleaks protect --staged --no-banner --redact  
# no leaks found  

gitleaks git --log-opts="headroomlabs/main..HEAD" --no-banner --redact  
# 5 commits scanned; no leaks found

## Real Behavior Proof  
- **Environment:** Windows PowerShell, Rust toolchain `1.95.0`, clean
worktree from `headroomlabs/main` at `9bacf481`.
- **Exact simulator command / steps:**  
  - `cargo run -p headroom-simulators -- --listen 127.0.0.1:8789`  
- Point Headroom proxy upstream at `http://127.0.0.1:8789` for local
deterministic provider responses.
- Use optional `--config path/to/simulator.json` to bind exact request
fixtures.
- **Observed simulator result:**  
  - OpenAI chat default returns `chat.completion` shape.  
  - OpenAI Responses stream returns named SSE events.  
  - Vertex raw predict returns Anthropic message shape.  
- Bedrock stream can return binary `application/vnd.amazon.eventstream`
bytes.
  - Configured stubs override bottled defaults.  
- **Observed Magika result:**  
- Direct Rust `headroom-core` tests pass with `ORT_DYLIB_PATH` unset.
- Magika discovers the installed pip `onnxruntime.dll`, loads it via
`ort::init_from`, and only falls back if no safe runtime is available.
- **Not tested:**  
- No live provider calls; simulator behavior is intentionally offline
and deterministic.

## 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

## Screenshots (if applicable)  
N/A

## Additional Notes  
No CHANGELOG entry was added because this introduces a developer/CI
simulator crate plus a Windows direct-Rust Magika runtime fix, without
changing shipped Python package behavior. The simulator intentionally
does not include a lightweight fallback LLM in this slice; unbound
inputs receive deterministic bottled responses so tests stay
reproducible and offline.
2026-07-11 09:41:49 -07:00
dependabot[bot]
5229c98228
deps: bump prometheus from 0.13.4 to 0.14.0 (#1518)
Bumps [prometheus](https://github.com/tikv/rust-prometheus) from 0.13.4
to 0.14.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tikv/rust-prometheus/blob/master/CHANGELOG.md">prometheus's
changelog</a>.</em></p>
<blockquote>
<h2>0.14.0</h2>
<ul>
<li>
<p>API change: Use <code>AsRef&lt;str&gt;</code> for owned label values
(<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/537">#537</a>)</p>
</li>
<li>
<p>Improvement: Hashing improvements (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/532">#532</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>hyper</code> to 1.6 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/524">#524</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>procfs</code> to 0.17 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/543">#543</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>protobuf</code> to 3.7.2 for
RUSTSEC-2024-0437 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/541">#541</a>)</p>
</li>
<li>
<p>Dependency upgrade: Update <code>thiserror</code> to 2.0 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/534">#534</a>)</p>
</li>
<li>
<p>Internal change: Fix LSP and Clippy warnings (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/540">#540</a>)</p>
</li>
<li>
<p>Internal change: Bump MSRV to 1.81 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/539">#539</a>)</p>
</li>
<li>
<p>Documentation: Fix <code>register_histogram_vec_with_registry</code>
docstring (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/528">#528</a>)</p>
</li>
<li>
<p>Documentation: Fix typos in static-metric docstrings (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/479">#479</a>)</p>
</li>
<li>
<p>Documentation: Add missing <code>protobuf</code> feature to README
list (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/531">#531</a>)</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e07efb4f37"><code>e07efb4</code></a>
prometheus: release 0.14.0 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/545">#545</a>)</li>
<li><a
href="26e46ec03a"><code>26e46ec</code></a>
Hashing improvements (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/532">#532</a>)</li>
<li><a
href="e17c5ced2b"><code>e17c5ce</code></a>
build(deps): update procfs requirement from ^0.16 to ^0.17 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/543">#543</a>)</li>
<li><a
href="e5809b7ab9"><code>e5809b7</code></a>
build(deps): update hyper requirement from ^0.14 to ^1.4 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/524">#524</a>)</li>
<li><a
href="4a0e282888"><code>4a0e282</code></a>
Use AsRef&lt;str&gt; for owned label values (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/537">#537</a>)</li>
<li><a
href="c3865f3c40"><code>c3865f3</code></a>
cargo: upgrade to protobuf 3.7 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/541">#541</a>)</li>
<li><a
href="7e4e6f2d33"><code>7e4e6f2</code></a>
docs: fix <code>register_histogram_vec_with_registry</code> docstring
(<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/528">#528</a>)</li>
<li><a
href="5b62f4b78b"><code>5b62f4b</code></a>
Fix LSP and Clippy warnings and errors (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/540">#540</a>)</li>
<li><a
href="52d76fc2d8"><code>52d76fc</code></a>
cargo: bump MSRV to 1.81 (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/539">#539</a>)</li>
<li><a
href="3bd0e82f1f"><code>3bd0e82</code></a>
Upgrade <code>thiserror</code> crate from 1.0 to 2.0 version (<a
href="https://redirect.github.com/tikv/rust-prometheus/issues/534">#534</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tikv/rust-prometheus/compare/v0.13.4...v0.14.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=prometheus&package-manager=cargo&previous-version=0.13.4&new-version=0.14.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:33:54 -05:00
dependabot[bot]
e448d7ba4d
deps: bump thiserror from 1.0.69 to 2.0.18 (#1519)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.69 to
2.0.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/thiserror/releases">thiserror's
releases</a>.</em></p>
<blockquote>
<h2>2.0.18</h2>
<ul>
<li>Make compatible with project-level <code>needless_lifetimes =
&quot;forbid&quot;</code> (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/443">#443</a>,
thanks <a
href="https://github.com/LucaCappelletti94"><code>@​LucaCappelletti94</code></a>)</li>
</ul>
<h2>2.0.17</h2>
<ul>
<li>Use differently named __private module per patch release (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/434">#434</a>)</li>
</ul>
<h2>2.0.16</h2>
<ul>
<li>Add to &quot;no-std&quot; crates.io category (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/429">#429</a>)</li>
</ul>
<h2>2.0.15</h2>
<ul>
<li>Prevent <code>Error::provide</code> API becoming unavailable from a
future new compiler lint (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/427">#427</a>)</li>
</ul>
<h2>2.0.14</h2>
<ul>
<li>Allow build-script cleanup failure with NFSv3 output directory to be
non-fatal (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/426">#426</a>)</li>
</ul>
<h2>2.0.13</h2>
<ul>
<li>Documentation improvements</li>
</ul>
<h2>2.0.12</h2>
<ul>
<li>Prevent elidable_lifetime_names pedantic clippy lint in generated
impl (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/413">#413</a>)</li>
</ul>
<h2>2.0.11</h2>
<ul>
<li>Add feature gate to tests that use std (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/409">#409</a>,
<a
href="https://redirect.github.com/dtolnay/thiserror/issues/410">#410</a>,
thanks <a
href="https://github.com/Maytha8"><code>@​Maytha8</code></a>)</li>
</ul>
<h2>2.0.10</h2>
<ul>
<li>Support errors containing a generic type parameter's associated type
in a field (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/408">#408</a>)</li>
</ul>
<h2>2.0.9</h2>
<ul>
<li>Work around <code>missing_inline_in_public_items</code> clippy
restriction being triggered in macro-generated code (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/404">#404</a>)</li>
</ul>
<h2>2.0.8</h2>
<ul>
<li>Improve support for macro-generated <code>derive(Error)</code> call
sites (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/399">#399</a>)</li>
</ul>
<h2>2.0.7</h2>
<ul>
<li>Work around conflict with #[deny(clippy::allow_attributes)] (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/397">#397</a>,
thanks <a
href="https://github.com/zertosh"><code>@​zertosh</code></a>)</li>
</ul>
<h2>2.0.6</h2>
<ul>
<li>Suppress deprecation warning on generated From impls (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/396">#396</a>)</li>
</ul>
<h2>2.0.5</h2>
<ul>
<li>Prevent deprecation warning on generated impl for deprecated type
(<a
href="https://redirect.github.com/dtolnay/thiserror/issues/394">#394</a>)</li>
</ul>
<h2>2.0.4</h2>
<ul>
<li>Eliminate needless_lifetimes clippy lint in generated
<code>From</code> impls (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/391">#391</a>,
thanks <a
href="https://github.com/matt-phylum"><code>@​matt-phylum</code></a>)</li>
</ul>
<h2>2.0.3</h2>
<ul>
<li>Support the same Path field being repeated in both Debug and Display
representation in error message (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/383">#383</a>)</li>
<li>Improve error message when a format trait used in error message is
not implemented by some field (<a
href="https://redirect.github.com/dtolnay/thiserror/issues/384">#384</a>)</li>
</ul>
<h2>2.0.2</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="dc0f6a23a3"><code>dc0f6a2</code></a>
Release 2.0.18</li>
<li><a
href="0275292735"><code>0275292</code></a>
Touch up PR 443</li>
<li><a
href="3c33bc60ad"><code>3c33bc6</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/thiserror/issues/443">#443</a>
from LucaCappelletti94/master</li>
<li><a
href="995939cc2e"><code>995939c</code></a>
Reproduce issue 442</li>
<li><a
href="21653d1d33"><code>21653d1</code></a>
Made clippy lifetime allows conditional</li>
<li><a
href="45e5388009"><code>45e5388</code></a>
Update actions/upload-artifact@v5 -&gt; v6</li>
<li><a
href="386aac126a"><code>386aac1</code></a>
Update actions/upload-artifact@v4 -&gt; v5</li>
<li><a
href="ec50561375"><code>ec50561</code></a>
Update actions/checkout@v5 -&gt; v6</li>
<li><a
href="247eab5d79"><code>247eab5</code></a>
Update name of empty_enum clippy lint</li>
<li><a
href="91b181f089"><code>91b181f</code></a>
Raise required compiler to Rust 1.68</li>
<li>Additional commits viewable in <a
href="https://github.com/dtolnay/thiserror/compare/1.0.69...2.0.18">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=thiserror&package-manager=cargo&previous-version=1.0.69&new-version=2.0.18)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:33:38 -05:00
dependabot[bot]
98f7f1c2a3
deps: bump tower-http from 0.6.11 to 0.7.0 (#1520)
Bumps [tower-http](https://github.com/tower-rs/tower-http) from 0.6.11
to 0.7.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tower-rs/tower-http/releases">tower-http's
releases</a>.</em></p>
<blockquote>
<h2>tower-http-0.7.0</h2>
<p><a
href="https://github.com/tower-rs/tower-http/compare/tower-http-0.6.11...tower-http-0.7.0">Changes
since 0.6.11</a></p>
<h2>Added</h2>
<ul>
<li>
<p><code>csrf</code>: add cross-site request forgery (CSRF) protection
middleware, porting the cross-origin protection scheme introduced in Go
1.25 (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/699">#699</a>)</p>
<pre lang="rust"><code>use tower::ServiceBuilder;
use tower_http::csrf::CsrfLayer;
<p>// Rejects cross-origin state-changing requests using
<code>Sec-Fetch-Site</code>,<br />
// an <code>Origin</code> allow-list, and an
<code>Origin</code>/<code>Host</code> fallback. No per-request<br />
// token state required.<br />
let layer = CsrfLayer::new().add_trusted_origin(&quot;<a
href="https://example.com">https://example.com</a>&quot;)?;</p>
<p>let service =
ServiceBuilder::new().layer(layer).service_fn(handler);<br />
</code></pre></p>
</li>
<li>
<p><code>timeout</code>: add <code>DeadlineBody</code> for non-resetting
body timeouts, applied via the new <code>RequestBodyDeadlineLayer</code>
and <code>ResponseBodyDeadlineLayer</code> (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/688">#688</a>)</p>
<p>Unlike <code>TimeoutBody</code>, which resets its deadline on every
frame, <code>DeadlineBody</code> caps the total time of a body transfer.
A slow client trickling one byte at a time never trips an idle timeout
but will trip a deadline.</p>
<pre lang="rust"><code>use std::time::Duration;
use tower::ServiceBuilder;
use tower_http::timeout::RequestBodyDeadlineLayer;
<p>// Abort the request body transfer after 30s total, regardless of
how<br />
// frequently data arrives.<br />
let service = ServiceBuilder::new()<br />
.layer(RequestBodyDeadlineLayer::new(Duration::from_secs(30)))<br />
.service_fn(handler);<br />
</code></pre></p>
</li>
<li>
<p><code>fs</code>: add strong <code>ETag</code> support to
<code>ServeDir</code>, including <code>If-Match</code> and
<code>If-None-Match</code> precondition handling per RFC 9110. <code>304
Not Modified</code> responses now carry the <code>ETag</code> and
<code>Last-Modified</code> validators (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/691">#691</a>)</p>
</li>
<li>
<p><code>fs</code>: add a <code>Backend</code> trait to make
<code>ServeDir</code> work with non-filesystem sources (e.g. embedded
assets or object storage). The default <code>TokioBackend</code>
preserves existing behavior. Use <code>ServeDir::with_backend()</code>
to plug in custom implementations (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/684">#684</a>)</p>
<pre lang="rust"><code>use tower_http::services::fs::ServeDir;
<p>// <code>MyBackend</code> implements
<code>tower_http::services::fs::Backend</code>.<br />
// The default <code>ServeDir::new()</code> continues to use
<code>TokioBackend</code> (local FS).<br />
let service = ServeDir::with_backend(&quot;assets&quot;,
MyBackend::new());<br />
</code></pre></p>
</li>
<li>
<p><code>fs</code>: add <code>html_as_default_extension</code> option to
<code>ServeDir</code>, appending <code>.html</code> when the request
path has no extension (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/519">#519</a>)</p>
</li>
<li>
<p><code>fs</code>: add <code>redirect_path_prefix</code> option to
<code>ServeDir</code>, prepending a prefix on trailing-slash redirects
so the service can be mounted under a sub-path (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/486">#486</a>)</p>
</li>
<li>
<p><code>validate-request</code>: add
<code>ValidateRequestHeaderLayer::has_header_value()</code> to reject
requests when a header does not have an expected value (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/360">#360</a>)</p>
</li>
<li>
<p><code>body</code>: <code>UnsyncBoxBody::new()</code> constructor and
<code>From&lt;ServeFileSystemResponseBody&gt;</code> conversion to avoid
double-boxing when combining <code>ServeDir</code> responses with other
body types (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/537">#537</a>)</p>
</li>
<li>
<p><code>limit</code>: implement <code>Default</code> for
<code>limit::ResponseBody</code> when the wrapped body also implements
<code>Default</code> (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/679">#679</a>)</p>
</li>
</ul>
<h2>Changed</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="b194fcfef3"><code>b194fcf</code></a>
v0.7.0</li>
<li><a
href="af828a6ec9"><code>af828a6</code></a>
feat(follow_redirect)!: preserve request extensions across redirects (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/706">#706</a>)</li>
<li><a
href="8cb8d99a84"><code>8cb8d99</code></a>
feat(ValidateRequestHeaderLayer): add
has_header(&quot;...&quot;).with_value(&quot;...&quot;) fun...</li>
<li><a
href="3b56d2d2e8"><code>3b56d2d</code></a>
feat!: Add configurable Backend trait for ServeDir, bump MSRV 1.65 (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/684">#684</a>)</li>
<li><a
href="8508716431"><code>8508716</code></a>
Add <code>redirect_path_prefix</code> option (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/486">#486</a>)</li>
<li><a
href="56327b27f4"><code>56327b2</code></a>
Add Windows drive-prefix path regression test (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/705">#705</a>)</li>
<li><a
href="54c6db8590"><code>54c6db8</code></a>
feat(compression)!: upgrade SizeAbove threshold from u16 to u64 (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/704">#704</a>)</li>
<li><a
href="68cd6d8f3c"><code>68cd6d8</code></a>
Add DeadlineBody for non-resetting body timeouts (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/688">#688</a>)</li>
<li><a
href="fa8a98cb3e"><code>fa8a98c</code></a>
feat(fs): add strong ETag support to ServeDir (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/691">#691</a>)</li>
<li><a
href="36d2205eb6"><code>36d2205</code></a>
fix: Make SetMultiple*Header Clone for !Clone http bodies (<a
href="https://redirect.github.com/tower-rs/tower-http/issues/703">#703</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tower-rs/tower-http/compare/tower-http-0.6.11...tower-http-0.7.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tower-http&package-manager=cargo&previous-version=0.6.11&new-version=0.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:33:28 -05:00
dependabot[bot]
6c705b4066
deps: bump toml from 0.8.23 to 1.1.2+spec-1.1.0 (#1517)
Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to
1.1.2+spec-1.1.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a3d0047c95"><code>a3d0047</code></a>
chore: Release</li>
<li><a
href="cc37615fc8"><code>cc37615</code></a>
docs: Update changelog</li>
<li><a
href="7f5e9e130a"><code>7f5e9e1</code></a>
fix(parser): Consolidate invalid unquoted key into one error (<a
href="https://redirect.github.com/toml-rs/toml/issues/1138">#1138</a>)</li>
<li><a
href="52feb9070c"><code>52feb90</code></a>
fix(parser): Consolidate invalid unquoted key into one error</li>
<li><a
href="aad85d4921"><code>aad85d4</code></a>
chore(deps): Update j178/prek-action action to v2 (<a
href="https://redirect.github.com/toml-rs/toml/issues/1136">#1136</a>)</li>
<li><a
href="8b1ac44bca"><code>8b1ac44</code></a>
chore(deps): Update compatible (dev) (<a
href="https://redirect.github.com/toml-rs/toml/issues/1135">#1135</a>)</li>
<li><a
href="9effd79ff2"><code>9effd79</code></a>
chore(deps): Update j178/prek-action action to v2</li>
<li><a
href="9db8aad6ea"><code>9db8aad</code></a>
chore: Release</li>
<li><a
href="e55a6633d9"><code>e55a663</code></a>
docs: Update changelog</li>
<li><a
href="c11d7d7ad3"><code>c11d7d7</code></a>
Optimisations (<a
href="https://redirect.github.com/toml-rs/toml/issues/1133">#1133</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/toml-rs/toml/compare/toml-v0.8.23...toml-v1.1.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=toml&package-manager=cargo&previous-version=0.8.23&new-version=1.1.2+spec-1.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 10:32:53 -05:00
dependabot[bot]
45601d93bc
deps: bump the cargo-minor-patch group across 1 directory with 7 updates (#1909)
Bumps the cargo-minor-patch group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [bytes](https://github.com/tokio-rs/bytes) | `1.12.0` | `1.12.1` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.102` | `1.0.103` |
| [aws-credential-types](https://github.com/smithy-lang/smithy-rs) |
`1.2.14` | `1.3.0` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.3` | `1.23.4` |
| [humantime](https://github.com/chronotope/humantime) | `2.3.0` |
`2.4.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.65` | `1.2.66` |


Updates `bytes` from 1.12.0 to 1.12.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/bytes/releases">bytes's
releases</a>.</em></p>
<blockquote>
<h2>Bytes v1.12.1</h2>
<h1>1.12.1 (July 8th, 2026)</h1>
<h3>Fixed</h3>
<ul>
<li>Properly handle when <code>Box::new</code> panics (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/837">#837</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md">bytes's
changelog</a>.</em></p>
<blockquote>
<h1>1.12.1 (July 8th, 2026)</h1>
<h3>Fixed</h3>
<ul>
<li>Properly handle when <code>Box::new</code> panics (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/837">#837</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="76c0fbb54e"><code>76c0fbb</code></a>
Release bytes v1.12.1 (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/838">#838</a>)</li>
<li><a
href="924c82bf00"><code>924c82b</code></a>
Handle unwinding from Box::new (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/837">#837</a>)</li>
<li>See full diff in <a
href="https://github.com/tokio-rs/bytes/compare/v1.12.0...v1.12.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `anyhow` from 1.0.102 to 1.0.103
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/anyhow/releases">anyhow's
releases</a>.</em></p>
<blockquote>
<h2>1.0.103</h2>
<ul>
<li>Fix Stacked Borrows violation (UB) in
<code>Error::downcast_mut</code> (<a
href="https://redirect.github.com/dtolnay/anyhow/issues/451">#451</a>,
<a
href="https://redirect.github.com/dtolnay/anyhow/issues/452">#452</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="5bdb0e24db"><code>5bdb0e2</code></a>
Release 1.0.103</li>
<li><a
href="e621bd35dd"><code>e621bd3</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/anyhow/issues/452">#452</a>
from dtolnay/downcast</li>
<li><a
href="6e8c000690"><code>6e8c000</code></a>
Eliminate pointer-&gt;reference-&gt;pointer during downcast</li>
<li><a
href="67c4abd771"><code>67c4abd</code></a>
Add regression test for issue 451</li>
<li><a
href="917a169320"><code>917a169</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="d9dc3faf78"><code>d9dc3fa</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="841522b2aa"><code>841522b</code></a>
Raise minimum tested compiler to rust 1.85</li>
<li>See full diff in <a
href="https://github.com/dtolnay/anyhow/compare/1.0.102...1.0.103">compare
view</a></li>
</ul>
</details>
<br />

Updates `aws-credential-types` from 1.2.14 to 1.3.0
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/smithy-lang/smithy-rs/commits">compare
view</a></li>
</ul>
</details>
<br />

Updates `aws-smithy-runtime-api` from 1.12.3 to 1.13.0
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/smithy-lang/smithy-rs/commits">compare
view</a></li>
</ul>
</details>
<br />

Updates `uuid` from 1.23.3 to 1.23.4
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/uuid-rs/uuid/releases">uuid's
releases</a>.</em></p>
<blockquote>
<h2>v1.23.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix up name of fuzz script in readme by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/888">uuid-rs/uuid#888</a></li>
<li>document fixes by <a
href="https://github.com/frostyplanet"><code>@​frostyplanet</code></a>
in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/889">uuid-rs/uuid#889</a></li>
<li>Prepare for 1.23.4 release by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/890">uuid-rs/uuid#890</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/frostyplanet"><code>@​frostyplanet</code></a>
made their first contribution in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/889">uuid-rs/uuid#889</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4">https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3296d64a19"><code>3296d64</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/890">#890</a> from
uuid-rs/cargo/v1.23.4</li>
<li><a
href="cba53d0da2"><code>cba53d0</code></a>
prepare for 1.23.4 release</li>
<li><a
href="e347af48aa"><code>e347af4</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/889">#889</a> from
frostyplanet/main</li>
<li><a
href="e9bf55c222"><code>e9bf55c</code></a>
doc: Fix broken link warnings</li>
<li><a
href="5351af40a0"><code>5351af4</code></a>
doc: Enable feature flag label for docs.rs</li>
<li><a
href="1e6a9669e3"><code>1e6a966</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/888">#888</a> from
uuid-rs/KodrAus-patch-1</li>
<li><a
href="c9619f639c"><code>c9619f6</code></a>
fix up name of fuzz script in readme</li>
<li>See full diff in <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `humantime` from 2.3.0 to 2.4.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/chronotope/humantime/releases">humantime's
releases</a>.</em></p>
<blockquote>
<h2>2.4.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat: allow creating Duration in const context by <a
href="https://github.com/ctron"><code>@​ctron</code></a> in <a
href="https://redirect.github.com/chronotope/humantime/pull/70">chronotope/humantime#70</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fc092817fa"><code>fc09281</code></a>
chore: prepare release 2.4.0</li>
<li><a
href="8a022cce55"><code>8a022cc</code></a>
feat: allow creating Duration in const context</li>
<li><a
href="27a4f77519"><code>27a4f77</code></a>
Explicitly set rust-version to 1.60</li>
<li><a
href="acc3c19658"><code>acc3c19</code></a>
ci: upgrade to actions/checkout v7</li>
<li><a
href="3acf96bb88"><code>3acf96b</code></a>
ci: fix workflow formatting</li>
<li>See full diff in <a
href="https://github.com/chronotope/humantime/compare/v2.3.0...v2.4.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `cc` from 1.2.65 to 1.2.66
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/cc-rs/releases">cc's
releases</a>.</em></p>
<blockquote>
<h2>cc-v1.2.66</h2>
<h3>Other</h3>
<ul>
<li>Fix target parsing for aarch64-unknown-linux-pauthtest (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1779">#1779</a>)</li>
<li>Support new QNX targets (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1775">#1775</a>)</li>
<li>Add kache to the supported compiler wrappers (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1770">#1770</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md">cc's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/rust-lang/cc-rs/compare/cc-v1.2.65...cc-v1.2.66">1.2.66</a>
- 2026-07-05</h2>
<h3>Other</h3>
<ul>
<li>Fix target parsing for aarch64-unknown-linux-pauthtest (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1779">#1779</a>)</li>
<li>Support new QNX targets (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1775">#1775</a>)</li>
<li>Add kache to the supported compiler wrappers (<a
href="https://redirect.github.com/rust-lang/cc-rs/pull/1770">#1770</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="9c53ddc649"><code>9c53ddc</code></a>
chore(cc): release v1.2.66 (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1777">#1777</a>)</li>
<li><a
href="f2836c0fc5"><code>f2836c0</code></a>
Fix target parsing (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1779">#1779</a>)</li>
<li><a
href="32996914ec"><code>3299691</code></a>
ci(test-wasm): do not download pre release of wasi-sdk (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1778">#1778</a>)</li>
<li><a
href="bfd1ac98d4"><code>bfd1ac9</code></a>
Support new QNX targets (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1775">#1775</a>)</li>
<li><a
href="1887a73fdd"><code>1887a73</code></a>
Add kache to the supported compiler wrappers (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1770">#1770</a>)</li>
<li><a
href="e4847e1720"><code>e4847e1</code></a>
Bump actions/checkout from 6.0.3 to 7.0.0 (<a
href="https://redirect.github.com/rust-lang/cc-rs/issues/1767">#1767</a>)</li>
<li>See full diff in <a
href="https://github.com/rust-lang/cc-rs/compare/cc-v1.2.65...cc-v1.2.66">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 09:21:38 -04:00
Tejas Chopra
5771a8020e
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
Paperinik
dca9853ed9
feat(wrap): make tokensave the primary coding-task compressor, Serena the backup (#1230)
## Description

Makes **tokensave**
([github.com/aovestdipaperino/tokensave](https://github.com/aovestdipaperino/tokensave))
the **primary coding-task compressor** that `headroom wrap` installs,
and demotes **Serena** to a **backup**. tokensave is a local semantic
code-graph MCP server (`tokensave serve`): the agent queries it for
symbols, call chains, and impact analysis instead of grepping/reading
whole files — the same role Serena filled, but as a pre-indexed graph.
Serena now only registers when tokensave is unavailable (or when forced
with `--serena`).

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

- `headroom/graph/tokensave_installer.py` (new): fetch the prebuilt
tokensave release binary for the platform (release-binary only — no
`cargo` compile at wrap time); honors `HEADROOM_BINARIES_OFFLINE`;
returns `None` (→ Serena) when no asset exists (e.g. x86_64 macOS) or
the download fails.
- `mcp_registry`: `build_tokensave_spec()`; registration/disable/migrate
go through the existing `ServerSpec` + ownership-ledger flow, identical
to Serena.
- `cli/wrap.py`: new `_setup_coding_compressor` primary/backup policy;
tokensave setup/disable/migrate/index helpers. New flags
`--no-tokensave` (skip primary) and `--serena` (force backup on);
`--no-serena` now means "never register the backup". Default wrap
removes a previously Headroom-installed Serena entry once tokensave is
primary (user-managed entries preserved). `--code-graph` repointed to
tokensave; the legacy `codebase-memory-mcp` install path is dropped
(unwrap still cleans up legacy entries). `unwrap claude|codex` remove a
ledger-owned tokensave entry.
- Strands `HeadroomBundle`: `enable_tokensave_mcp=True` (primary);
`enable_serena_mcp` now defaults `False` (backup).
- `docs/content/docs/proxy.mdx`: `--code-graph` description updated from
codebase-memory-mcp to tokensave.
- Tests: tokensave installer (incl. error paths),
register/disable/migrate, primary/backup policy, and the
binary-resolution/indexing helpers. A scoped
`tests/test_cli/conftest.py` offline guard keeps the CLI suite hermetic.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed

### Test Output

```text
$ uv run pytest -q tests/test_graph_tokensave.py tests/test_cli/test_tokensave_setup.py tests/test_cli/test_tokensave_helpers.py
41 passed

$ uv run pytest -q tests/test_cli/ tests/test_graph.py tests/test_graph_tokensave.py
421 passed   # full CLI + graph suites, incl. all pre-existing Serena/unwrap/registry tests

$ uv run pytest -q tests/test_mcp_registry/ tests/test_proxy_healthchecks.py
passed

$ uv run ruff format --check headroom/ tests/      # 822 files already formatted
$ uv run ruff check <changed files>                # All checks passed!
$ uv run mypy headroom/graph/tokensave_installer.py headroom/mcp_registry/install.py
Success: no issues found in 2 source files

# Coverage on new module
headroom/graph/tokensave_installer.py    99%
```

## Real Behavior Proof

- Environment: macOS (darwin arm64), Python 3.14, `uv` dev env;
tokensave 7.0.2 binary present on PATH and exercised against this repo's
`.tokensave/` graph during development. The installer pins release
**v7.0.2** (SHA-256-verified) across macOS arm64, Linux aarch64/x86_64,
and Windows x86_64/aarch64.
- Exact command / steps: `headroom wrap claude` registers `tokensave
serve` as the primary MCP code-graph server and indexes the project;
with the binary removed from PATH and `HEADROOM_BINARIES_OFFLINE=1`, the
same command falls back to registering Serena. Behavior is pinned by the
unit tests (binary-present → tokensave registered + Serena entry
removed; binary-absent → Serena fallback; `--serena` forces backup on;
`--no-serena` suppresses it; `--no-tokensave` disables primary).
- Observed result: tokensave registered as primary on the binary-present
path; Serena registered on the unavailable path; unwrap removes only
ledger-owned entries.
- Not tested: live end-to-end agent session inside Claude Code / Codex
against a real provider API; Windows/Linux release-asset download
(covered by unit tests with mocked archives, not a live fetch).

## 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 is left untouched: this repo generates it via release-please
from Conventional Commits, so a manual edit is N/A.
- `strands/bundle.py` shows 0% patch coverage because that module
hard-imports the optional `strands` SDK, which CI does not install (the
pre-existing `_make_serena_client` was likewise uncovered) — not a
regression.
- A `test (3)` shard failure on `headroom.memory.bridge` is a
pre-existing offline-CI flake (cannot reach huggingface.co); it touches
no file in this PR and the scoped offline guard only applies under
`tests/test_cli/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 16:55:37 -05:00
Parideboy
3ccdad6c67
Pin ORT dylib on Windows; init Python logging (#1010)
## Description

On Windows, headroom's Rust core resolves `onnxruntime.dll` at runtime
via `ort-load-dynamic`. Without an explicit `ORT_DYLIB_PATH`, the bare
DLL search can land on `C:\Windows\System32\onnxruntime.dll`, the
Windows ML OS component, and `Session::new()` can deadlock instead of
returning an error. Since a hang is not an `Err`, the tiered fallback
cannot engage until the proxy-level timeout fires.

This PR pins `ORT_DYLIB_PATH` to the pip-installed `onnxruntime` DLL at
import time, and wires Rust `tracing` events into Python logging so the
proxy log surfaces these failures when they occur.

Closes #928

## 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

- Added `headroom/_ort.py` with a Windows-only, idempotent
`ensure_ort_dylib_pinned()` resolver that respects an existing
`ORT_DYLIB_PATH`.
- Call the pin from `headroom/__init__.py` before importing `_core`
consumers.
- Log the effective ORT dylib path from the content router startup path
on Windows.
- Enable Rust tracing-to-log compatibility and initialize `pyo3-log` in
the `_core` module.
- Add timeout diagnostics in the Magika detector with the effective
`ORT_DYLIB_PATH`.
- Document `ORT_DYLIB_PATH` and `HEADROOM_MAGIKA_INIT_TIMEOUT_SECS`.
- Add unit coverage for the resolver behavior.

## Testing

- [x] Unit tests pass (`python -m pytest
tests/test_transforms/test_ort_dylib.py -q`)
- [x] Linting passes (`ruff check headroom/_ort.py headroom/__init__.py
headroom/transforms/content_router.py
tests/test_transforms/test_ort_dylib.py`)
- [x] Formatting passes (`ruff format --check headroom/_ort.py
headroom/__init__.py headroom/transforms/content_router.py
tests/test_transforms/test_ort_dylib.py`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ python -m pytest tests/test_transforms/test_ort_dylib.py -q
7 passed in 0.19s

$ ruff check headroom/_ort.py headroom/__init__.py headroom/transforms/content_router.py tests/test_transforms/test_ort_dylib.py
All checks passed!

$ ruff format --check headroom/_ort.py headroom/__init__.py headroom/transforms/content_router.py tests/test_transforms/test_ort_dylib.py
4 files already formatted

$ cargo check -p headroom-py
cargo: The term 'cargo' is not recognized as a name of a cmdlet, function, script file, or executable program.
```

## Real Behavior Proof

- Environment: Windows 11 24H2, Python 3.13, RTX 4080
- Exact command / steps: `python -c "import headroom; from
headroom._core import detect_content_type as d;
print(d(open('headroom/compress.py').read()).content_type)"`
- Observed result: `source_code` in 301ms, clean exit, `Magika: ENABLED`
in proxy log
- Not tested: macOS/Linux manual runtime behavior; `_ort.py` is a no-op
outside Windows, and CI covers cross-platform build/test behavior.

## 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 (N/A: repo uses
release-please)

## Additional Notes

The branch was rebased onto current `main` and the commit subject was
updated to satisfy commitlint. Local Rust verification could not be run
on this Windows machine because `cargo` is not installed; GitHub CI
should be treated as the Rust build verification for the `pyo3-log`
dependency and workspace lockfile changes.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 07:46:24 -05:00
Yasser Sheikh
0dc2e1cb3f
feat(bedrock): cross-region + Converse compression; bundle proxy binary in images (#999)
## Description

The native Bedrock path (Phase D) compresses + signs
Anthropic-on-Bedrock requests, but
two real-world cases slipped through, and the native binary that powers
it was never
shipped. This PR closes those gaps as a focused set of give-backs.

Aligns with the Rust migration plan (see below).

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- **Cross-region inference-profile detection** via a new
`bedrock::vendor` module
(`canonical_vendor()`), following the design proposed in #953: strip a
known geo prefix
(`eu.`/`us.`/`apac.`/`global.`) then match the canonical vendor.
Geo-prefixed Anthropic
profiles (`eu.anthropic.…`) now get live-zone compression instead of
being silently
  skipped; geo-prefixed non-Anthropic vendors stay correctly excluded.
- **Converse-body compression (two parts)**:
1. `run_anthropic_compression` no longer bails to passthrough when the
body lacks an
InvokeModel `anthropic_version` envelope; envelope re-emit stays gated
on successful parse.
2. The **live-zone dispatcher now recognizes Bedrock Converse content
blocks**. Converse
blocks carry no `type` discriminator (the variant is the key: `{"text":
…}` vs
Anthropic's `{"type":"text","text":…}`), so real Converse user-message
text was still
passing through uncompressed. A typeless block whose `text` is a JSON
string now routes
through the same surgical text path. Anthropic blocks always carry
`type`, so the
Anthropic path is byte-for-byte unchanged; non-text Converse blocks
(`{"image":…}`,
     `{"toolUse":…}`) stay unrecognized and no-op.
- **Correct `/converse` upstream routing**: the non-streaming handler
resolved the upstream
action from a hard-coded `"invoke"`, so `/converse` requests were
forwarded to Bedrock's
`/invoke` endpoint. It now resolves the action from the inbound path
(`extract_invoke_action`),
mirroring the streaming handler's `extract_streaming_action`. SigV4
signs the same URL it
  forwards, so the signature stays consistent.
- **`aws-config` `sso` feature**: SSO profiles now resolve through the
default credential
chain for SigV4 — the credential chain in `docs/bedrock.md` already
promised SSO; this
  makes the code match.
- **Ship the `headroom-proxy` binary in published images**
(`Dockerfile`): built in the
builder stage (`--locked`, with the cargo registry cache mounted at
`CARGO_HOME`) and
  copied into both the debian and distroless runtime images.
- **Docs** (`docs/bedrock.md`): document cross-region inference profiles
and a "Running the
proxy" section. AWS credentials mount at `/home/nonroot/.aws` (the
default nonroot image
home) where the SDK looks for `~/.aws`, with a note on the root-image
alternative.

## Related issues

- Closes #976 — ship the `headroom-proxy` binary in published images
(this PR implements
  the exact fix proposed there).
- Addresses the **cross-region inference-profile** half of #953 via its
proposed
`canonical_vendor()` design. Non-Anthropic vendor compression parity
(Nova/GLM/MiniMax/
Kimi) is the natural follow-up — `bedrock::vendor` is the shared
resolver it can build on.
- Extends the native Bedrock InvokeModel compression requested in #734
(the Bedrock slice of
  #510) to cross-region profiles and Converse bodies.
- Partially enables #181 (native, Python-free packaging): the native
binary now ships in the
  images, though full Python-free distribution remains out of scope.

## Alignment with the Rust migration plan

Per `docs/spec/022-rust-migration.md`, the migration is **proxy-first**:
`headroom-proxy` is
the deployable Rust artifact, native routes replace Python passthroughs
one at a time
(Stage 4 = provider expansion, Bedrock included), and the binary is
meant to be "built,
tested, and **released together with the Python package**." Two ways
this PR advances that:

- The binary-in-images change makes the codebase do what the spec
already states (ship the
artifact) — closing the gap that forced downstreams to build from
source.
- Hardening the native Bedrock route (cross-region, Converse routing +
body compression) is
exactly the Stage-4 provider-expansion work, keeping the native path at
parity with real
  traffic so it can be the default rather than a passthrough.

## Testing

- [x] Unit tests pass (`cargo test -p headroom-core -p headroom-proxy` —
full suites, 0 failures)
- [x] Linting passes (`cargo clippy -p headroom-core -p headroom-proxy
--all-targets -- -D warnings`)
- [x] Formatting passes (`cargo fmt -- --check`)
- [x] New tests added — `bedrock::vendor` (foundation +
inference-profile matching),
`extract_invoke_action` + converse upstream URL, and live-zone Converse
text-block routing
(`block_has_string_text_field`, converse-vs-anthropic dispatch
equivalence).
- [x] Manual testing performed

### Test Output

```text
$ cargo test -p headroom-core -p headroom-proxy   # all suites: ok, 0 failed
$ cargo clippy -p headroom-core -p headroom-proxy --all-targets -- -D warnings   # Finished, no warnings
$ cargo fmt -- --check                            # clean
# image validation (local, proxy/code extras):
$ docker build --target runtime ...      # debian: /usr/local/bin/headroom-proxy, --help OK
$ docker build --target runtime-slim ... # distroless: binary links + --help OK
```

## Real Behavior Proof

- Environment: native Bedrock proxy against `bedrock-runtime.eu-west-2`,
SSO profile, model
  `eu.anthropic.claude-haiku-4-5-20251001-v1:0`.
- Exact command / steps: POST a large multi-turn Converse body to
`/model/eu.anthropic.claude-haiku-4-5-20251001-v1:0/converse`;
separately build the
`runtime` + `runtime-slim` targets and run
`/usr/local/bin/headroom-proxy --help`.
- Observed result: before — `bedrock_compression_skipped` (geo-prefixed
id not recognized),
forwarded uncompressed to the wrong `/invoke` upstream; after —
geo-prefixed id recognized,
`/converse` forwarded to the `/converse` upstream, live-zone dispatcher
compresses the
Converse user-message text, measurable token savings. Images contain a
runnable
  `headroom-proxy` in both variants.
- Not tested: non-Anthropic vendor compression parity (#953 follow-up);
Converse
`toolResult` nested-text compression (follow-up — only top-level
Converse text blocks
  compress today).

## 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
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

- An earlier revision flipped the EventStream `Accept` default
(`*/*`/absent → passthrough);
**dropped** — `*/*` is what most clients (incl. reqwest and the proxy's
own metrics tests)
send while expecting SSE, so forcing passthrough breaks the standard SSE
path.
- The binary build adds the native-proxy compile to the image build;
happy to gate it behind
  a build arg if maintainers prefer it opt-in.
- Addressed a Copilot review round: corrected the `/converse` upstream
routing, the stale
`run_anthropic_compression` comment, the Dockerfile cargo cache mount +
`--locked`, and the
  nonroot AWS-credentials docs example.
2026-06-16 09:45:24 -05:00
dependabot[bot]
4ff7b4426d
ci: bump pyo3 from 0.22.6 to 0.24.1 in the cargo group across 1 directory (#270)
Bumps the cargo group with 1 update in the / directory:
[pyo3](https://github.com/pyo3/pyo3).

Updates `pyo3` from 0.22.6 to 0.24.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pyo3/pyo3/releases">pyo3's
releases</a>.</em></p>
<blockquote>
<h2>PyO3 0.24.1</h2>
<p>This release is a security fix for the
<code>PyString::from_object</code> method, which passed
<code>&amp;str</code> data to the Python C API without checking for a
terminating nul byte. All historical PyO3 versions are affected, and we
recommend you upgrade if you are using
<code>PyString::from_object</code>. Thank you to <a
href="https://github.com/vthib"><code>@​vthib</code></a> for the report
and <a href="https://github.com/Dr-Emann"><code>@​Dr-Emann</code></a>
for the fix. A RUSTSEC advisory will be published shortly.</p>
<p>Aside from the security fix, this release contains a number of other
non-breaking additions:</p>
<ul>
<li>An <code>abi3-py313</code> feature to support compiling with the
Python 3.13 stable ABI.</li>
<li><code>PyAnyMethods::getattr_opt</code> to get optional attributes
without paying the cost of a Python exception when the attribute in
question does not exist.</li>
<li>Constructor for <code>PyInt::new</code>.</li>
<li><code>with_critical_section2</code> for locking two objects at the
same time on the free-threaded build.</li>
<li>Fix for a PyO3 0.24.0 regression with
<code>Option&lt;&amp;str&gt;</code> and
<code>Option&lt;&amp;T&gt;</code> (where <code>T: PyClass</code>)
function arguments no longer being permitted</li>
</ul>
<p>There are also a few other small bug fixes for edge cases, mostly
related to compile errors from PyO3's macro code.</p>
<p>Thank you to the following contributors for the improvements:</p>
<p><a
href="https://github.com/bschoenmaeckers"><code>@​bschoenmaeckers</code></a>
<a href="https://github.com/davidhewitt"><code>@​davidhewitt</code></a>
<a href="https://github.com/Dr-Emann"><code>@​Dr-Emann</code></a>
<a href="https://github.com/emmagordon"><code>@​emmagordon</code></a>
<a href="https://github.com/epontan"><code>@​epontan</code></a>
<a href="https://github.com/Icxolu"><code>@​Icxolu</code></a>
<a
href="https://github.com/IvanIsCoding"><code>@​IvanIsCoding</code></a>
<a href="https://github.com/jelmer"><code>@​jelmer</code></a>
<a href="https://github.com/jonaspleyer"><code>@​jonaspleyer</code></a>
<a href="https://github.com/ngoldbaum"><code>@​ngoldbaum</code></a>
<a
href="https://github.com/Owen-CH-Leung"><code>@​Owen-CH-Leung</code></a>
<a href="https://github.com/Tpt"><code>@​Tpt</code></a>
<a
href="https://github.com/Trolldemorted"><code>@​Trolldemorted</code></a>
<a href="https://github.com/XuehaiPan"><code>@​XuehaiPan</code></a></p>
<h2>PyO3 0.24.0</h2>
<p>This release is an incremental improvement of refinements and
optimizations following the new APIs established in PyO3's last few
releases.</p>
<p>Support for <code>jiff</code> datetime conversions have been added,
and also UUID conversions.</p>
<p>The <code>FromPyObject</code> derive macro has gained new
<code>#[pyo3(default = ...)]</code> and <code>#[pyo3(rename_all =
...)]</code> options, and the <code>IntoPyObject</code> derive macro has
gained a new <code>#[pyo3(into_py_with = ...)]</code> option.</p>
<p>PyO3 will now pass positional arguments to Python functions using the
&quot;vectorcall&quot; protocol in many cases, which should be an
optimization over the previous behaviour (of creating a Python tuple of
positional arguments).</p>
<p>Many methods on iterators of Python collections have been
optimized.</p>
<p>There are also many other incremental improvements, bug fixes and
smaller features.</p>
<p>Thank you to everyone who contributed code, documentation, design
ideas, bug reports, and feedback. The following contributors' commits
are included in this release:</p>
<p><a href="https://github.com/0x676e67"><code>@​0x676e67</code></a>
<a href="https://github.com/alex"><code>@​alex</code></a>
<a href="https://github.com/arielb1"><code>@​arielb1</code></a>
<a
href="https://github.com/bschoenmaeckers"><code>@​bschoenmaeckers</code></a>
<a
href="https://github.com/davidhewitt"><code>@​davidhewitt</code></a></p>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md">pyo3's
changelog</a>.</em></p>
<blockquote>
<h2>[0.24.1] - 2025-03-31</h2>
<h3>Added</h3>
<ul>
<li>Add <code>abi3-py313</code> feature. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4969">#4969</a></li>
<li>Add <code>PyAnyMethods::getattr_opt</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4978">#4978</a></li>
<li>Add <code>PyInt::new</code> constructor for all supported number
types (i32, u32, i64, u64, isize, usize). <a
href="https://redirect.github.com/PyO3/pyo3/pull/4984">#4984</a></li>
<li>Add <code>pyo3::sync::with_critical_section2</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4992">#4992</a></li>
<li>Implement <code>PyCallArgs</code> for <code>Borrowed&lt;'_, 'py,
PyTuple&gt;</code>, <code>&amp;Bound&lt;'py, PyTuple&gt;</code>, and
<code>&amp;Py&lt;PyTuple&gt;</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/5013">#5013</a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix <code>is_type_of</code> for native types not using same
specialized check as <code>is_type_of_bound</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4981">#4981</a></li>
<li>Fix <code>Probe</code> class naming issue with
<code>#[pymethods]</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4988">#4988</a></li>
<li>Fix compile failure with required <code>#[pyfunction]</code>
arguments taking <code>Option&lt;&amp;str&gt;</code> and
<code>Option&lt;&amp;T&gt;</code> (for <code>#[pyclass]</code> types).
<a href="https://redirect.github.com/PyO3/pyo3/pull/5002">#5002</a></li>
<li>Fix <code>PyString::from_object</code> causing of bounds reads with
<code>encoding</code> and <code>errors</code> parameters which are not
nul-terminated. <a
href="https://redirect.github.com/PyO3/pyo3/pull/5008">#5008</a></li>
<li>Fix compile error when additional options follow after
<code>crate</code> for <code>#[pyfunction]</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/5015">#5015</a></li>
</ul>
<h2>[0.24.0] - 2025-03-09</h2>
<h3>Packaging</h3>
<ul>
<li>Add supported CPython/PyPy versions to cargo package metadata. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4756">#4756</a></li>
<li>Bump <code>target-lexicon</code> dependency to 0.13. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4822">#4822</a></li>
<li>Add optional <code>jiff</code> dependency to add conversions for
<code>jiff</code> datetime types. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4823">#4823</a></li>
<li>Add optional <code>uuid</code> dependency to add conversions for
<code>uuid::Uuid</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4864">#4864</a></li>
<li>Bump minimum supported <code>inventory</code> version to 0.3.5. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4954">#4954</a></li>
</ul>
<h3>Added</h3>
<ul>
<li>Add <code>PyIterator::send</code> method to allow sending values
into a python generator. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4746">#4746</a></li>
<li>Add <code>PyCallArgs</code> trait for passing arguments into the
Python calling protocol. This enabled using a faster calling convention
for certain types, improving performance. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4768">#4768</a></li>
<li>Add <code>#[pyo3(default = ...']</code> option for
<code>#[derive(FromPyObject)]</code> to set a default value for
extracted fields of named structs. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4829">#4829</a></li>
<li>Add <code>#[pyo3(into_py_with = ...)]</code> option for
<code>#[derive(IntoPyObject, IntoPyObjectRef)]</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4850">#4850</a></li>
<li>Add FFI definitions <code>PyThreadState_GetFrame</code> and
<code>PyFrame_GetBack</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4866">#4866</a></li>
<li>Optimize <code>last</code> for <code>BoundListIterator</code>,
<code>BoundTupleIterator</code> and <code>BorrowedTupleIterator</code>.
<a href="https://redirect.github.com/PyO3/pyo3/pull/4878">#4878</a></li>
<li>Optimize <code>Iterator::count()</code> for <code>PyDict</code>,
<code>PyList</code>, <code>PyTuple</code> &amp; <code>PySet</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4878">#4878</a></li>
<li>Optimize <code>nth</code>, <code>nth_back</code>,
<code>advance_by</code> and <code>advance_back_by</code> for
<code>BoundTupleIterator</code> <a
href="https://redirect.github.com/PyO3/pyo3/pull/4897">#4897</a></li>
<li>Add support for <code>types.GenericAlias</code> as
<code>pyo3::types::PyGenericAlias</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4917">#4917</a></li>
<li>Add <code>MutextExt</code> trait to help avoid deadlocks with the
GIL while locking a <code>std::sync::Mutex</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4934">#4934</a></li>
<li>Add <code>#[pyo3(rename_all = &quot;...&quot;)]</code> option for
<code>#[derive(FromPyObject)]</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4941">#4941</a></li>
</ul>
<h3>Changed</h3>
<ul>
<li>Optimize <code>nth</code>, <code>nth_back</code>,
<code>advance_by</code> and <code>advance_back_by</code> for
<code>BoundListIterator</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4810">#4810</a></li>
<li>Use <code>DerefToPyAny</code> in blanket implementations of
<code>From&lt;Py&lt;T&gt;&gt;</code> and <code>From&lt;Bound&lt;'py,
T&gt;&gt;</code> for <code>PyObject</code>. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4593">#4593</a></li>
<li>Map
<code>io::ErrorKind::IsADirectory</code>/<code>NotADirectory</code> to
the corresponding Python exception on Rust 1.83+. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4747">#4747</a></li>
<li><code>PyAnyMethods::call</code> and friends now require
<code>PyCallArgs</code> for their positional arguments. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4768">#4768</a></li>
<li>Expose FFI definitions for <code>PyObject_Vectorcall(Method)</code>
on the stable abi on 3.12+. <a
href="https://redirect.github.com/PyO3/pyo3/pull/4853">#4853</a></li>
<li><code>#[pyo3(from_py_with = ...)]</code> now take a path rather than
a string literal <a
href="https://redirect.github.com/PyO3/pyo3/pull/4860">#4860</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a213b368bd"><code>a213b36</code></a>
release: 0.24.1 (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5021">#5021</a>)</li>
<li><a
href="d85a02d9b1"><code>d85a02d</code></a>
split <code>PyFunctionArgument</code> to specialize <code>Option</code>
(<a
href="https://redirect.github.com/pyo3/pyo3/issues/5002">#5002</a>)</li>
<li><a
href="c37a50a7a3"><code>c37a50a</code></a>
Add example of more complex exceptions (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5014">#5014</a>)</li>
<li><a
href="dcacb9bbbc"><code>dcacb9b</code></a>
Simplify PyFunctionArgument impl on &amp;Bound&lt;T&gt; (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5018">#5018</a>)</li>
<li><a
href="03c31c5c7a"><code>03c31c5</code></a>
fix <code>#[pyfunction]</code> option parsing (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5015">#5015</a>)</li>
<li><a
href="0f49eb14b0"><code>0f49eb1</code></a>
docs: Remove examples with outdated PyO3 and unmaintained projects (<a
href="https://redirect.github.com/pyo3/pyo3/issues/4952">#4952</a>)</li>
<li><a
href="1b00b0d27f"><code>1b00b0d</code></a>
implement <code>PyCallArgs</code> for borrowed types (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5013">#5013</a>)</li>
<li><a
href="5caaa371dc"><code>5caaa37</code></a>
fix: convert to cstrings in PyString::from_object (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5008">#5008</a>)</li>
<li><a
href="4aca459fd3"><code>4aca459</code></a>
docs: guide - add link to tables and traits (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5001">#5001</a>)</li>
<li><a
href="0452c0ee52"><code>0452c0e</code></a>
replace quansight-labs/setup-python with actions/setup-python (<a
href="https://redirect.github.com/pyo3/pyo3/issues/5007">#5007</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/pyo3/pyo3/compare/v0.22.6...v0.24.1">compare
view</a></li>
</ul>
</details>
<br />

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-06-10 23:01:33 -05:00
Tejas Chopra
c83687798b Fix Windows ORT builds and Docker signing retries 2026-05-10 20:59:28 -07:00
chopratejas
4a3b76bcc8 fix: PR-E1 tool array deterministic sort (Phase E)
Sort `tools[]` alphabetically by name on the way out so cache hits no
longer depend on the customer-side iteration order (commonly hash-
randomized via `set()` / `dict`). Mutates request bytes only when:

  1. Auth mode is PAYG (`headroom_core::auth_mode::classify`).
  2. No tool already carries a `cache_control` marker (reordering
     would shift cache scope and silently void customer intent).

Every gate skip emits a structured `e1_skipped` event with `reason =
auth_mode | marker_present` so dashboards can see policy adoption.

Wired into all three live-zone walkers — Anthropic `/v1/messages`,
OpenAI `/v1/chat/completions`, OpenAI `/v1/responses` — plus the
Bedrock invoke + invoke-streaming entry points. Each passes
`auth_mode` (already pre-classified by Phase F PR-F1 middleware)
into the dispatcher so the gate evaluates without re-classifying.

Sort key uses `tool["name"]` (Anthropic) or `tool["function"]["name"]`
(OpenAI). Unnamed tools (rare; malformed inputs only) fall back to
MD5 of canonical-JSON serialization for a stable in-process key —
collision odds are astronomically small and `Vec::sort_by` is stable.

Tests: unit tests for sort + marker detection + idempotency + the
permutation property; integration tests boot the real proxy in front
of a wiremock upstream and assert PAYG -> sorted, OAuth/Subscription/
marker -> byte-equal passthrough (SHA-256).
2026-05-05 15:35:27 -07:00
chopratejas
e2146724af fix: ship glibc 2.38 compat shim + wheel symbol audit (closes #355)
Issue #355: published headroom_ai-*-manylinux_2_28_*.whl fails to import on Ubuntu 22.04, Debian 11/12, Conda envs with libc < 2.38: 'ImportError: undefined symbol: __isoc23_strtoll'.

Root cause: ORT prebuilt artifacts (downloaded via fastembed's ort-download-binaries-rustls-tls feature) are compiled with gcc-14.2.1 on a glibc-2.38+ host and reference __isoc23_strtoll. Our manylinux build host has glibc 2.38 so the link succeeds; end users with older glibc don't.

Two-part fix: (1) crates/headroom-py/glibc_compat.c provides weak-alias definitions for __isoc23_strtol/strtoll/strtoul/strtoull delegating to the older strtol* family, compiled by build.rs on Linux/glibc only. The dynamic linker prefers glibc's strong symbol when present (>= 2.38) and falls back to ours when not (< 2.38). (2) scripts/audit_wheel_glibc_symbols.py is a release.yml gate that runs objdump -T on every Linux wheel and rejects any UND symbol whose required glibc version exceeds the wheel's manylinux floor.

Validated: the audit correctly rejects the actually-broken v0.20.26 wheel with a precise diagnostic. The shim itself is a tiny static link with zero runtime cost.

Regression tests in tests/test_release_workflows.py pin the shim's load-bearing pieces (.c file, build.rs trigger, [build-dependencies] cc dep) and the audit invocation in release.yml. Future drift fails at PR time, not in the next release.

This is the same bug class as PR #371 (rustls-everywhere). Each instance gets fixed; the audit gate now catches the *class* — any future static linkage that introduces a post-floor symbol gets blocked before publish.
2026-05-04 21:31:19 -07:00
chopratejas
c10a2195af fix(proxy): PR-D4 native Vertex publisher path + ADC bearer auth
Adds a Rust-native Vertex AI publisher route ahead of the LiteLLM
Python converter (which dropped `thinking`, `redacted_thinking`,
`document`, `image`, `server_tool_use`, `mcp_tool_use` block kinds —
the P4-37 / P4-38 bug). After this PR the Vertex `:rawPredict` and
`:streamRawPredict` calls survive byte-equal upstream and benefit
from the live-zone Anthropic dispatcher (PR-B-series) running over
the body — same behaviour as `/v1/messages`.

New module `crates/headroom-proxy/src/vertex/`:

- `mod.rs` — single dispatch handler at the
  `/v1beta1/.../models/:model_action` route. Splits the trailing
  `:<verb>` segment with `str::rsplit_once(':')` (no regex) and
  flips an `attach_sse_tee` flag to dispatch to the streaming or
  non-streaming arm. Both verbs share one axum route shape because
  matchit can't distinguish two patterns that overlap on a
  parameter.
- `envelope.rs` — `VertexEnvelope` parser. Confirms
  `anthropic_version` present + `model` field absent (the two
  fingerprints of the Vertex envelope vs `/v1/messages`).
- `adc.rs` — `TokenSource` trait + `GcpAdcTokenSource` (production,
  `gcp_auth` 0.12) + `StaticTokenSource` (tests). Caches tokens
  with a 60s refresh-ahead-of-expiry window. Emits structured
  `event = "vertex_adc_token_refreshed"` per refresh.
- `raw_predict.rs` — POST handler + shared `forward_vertex_request`.
  Buffers body, parses envelope, runs live-zone Anthropic
  compression, fetches ADC bearer, attaches
  `Authorization: Bearer <token>` (overwrites client-supplied
  Authorization header), forwards. SSE telemetry tee for the
  streaming verb reuses PR-C1's `AnthropicStreamState` directly
  (Vertex streams plain SSE, unlike Bedrock's binary EventStream).
- `stream_raw_predict.rs` — module-level docs + alias to the
  shared dispatcher (the streaming-vs-non-streaming difference is
  one boolean flag inside the shared forwarder).

Modifications:

- `proxy.rs::build_app` — registers the single Vertex route.
- `proxy.rs::AppState` — new `vertex_token_source: Arc<dyn TokenSource>`
  field. Production constructs `GcpAdcTokenSource` lazily (no GCP
  call until first `bearer()`); tests inject `StaticTokenSource`
  via the new `AppState::with_token_source` helper.
- `config.rs` — adds `--vertex-region` / `HEADROOM_PROXY_VERTEX_REGION`
  (default `us-central1`, observability tag only — the upstream URL
  is `--upstream`) and `--vertex-adc-scope` /
  `HEADROOM_PROXY_VERTEX_ADC_SCOPE` (default `cloud-platform`).
- `Cargo.toml` (workspace + proxy) — adds `gcp_auth = "0.12"` and
  `async-trait = "0.1"`.
- `tests/common/mod.rs` — `start_proxy_with_state` accepts both
  config + state customizers; `install_static_token_source` helper
  for tests.

`crates/headroom-proxy/tests/integration_vertex_raw_predict.rs` —
all five tests pass:

1. `native_envelope_round_trip_byte_equal` — Vertex-shape body
   (with `anthropic_version`, no `model`) round-trips SHA-256
   byte-equal upstream.
2. `adc_bearer_token_signed_correctly` — `Authorization: Bearer
   <static-test-token>` reaches upstream verbatim and OVERWRITES a
   client-supplied Authorization header.
3. `thinking_block_preserved` — request with `thinking` (incl.
   signature) + `redacted_thinking` (incl. opaque `data`) blocks
   round-trips byte-equal even with `LiveZone` compression mode
   enabled. This is the P4-37 / P4-38 teeth.
4. `stream_raw_predict_sse_handled` — `:streamRawPredict` proxies
   an Anthropic SSE response (full `message_start` →
   `content_block_delta` → `message_stop` sequence) back to the
   client without corruption; SSE content-type preserved end-to-end;
   bearer attached.
5. (bonus, no-silent-fallback contract)
   `adc_failure_returns_5xx_no_silent_forward` — when the token
   source returns `Err`, the proxy returns 5xx and never reaches
   upstream. Verifies the `event = "vertex_adc_fetch_failed"`
   error path.

Workspace: `cargo test --workspace` green; `cargo clippy --workspace
-- -D warnings` clean; `make ci-precheck` passes.

- No silent fallbacks: ADC failure → structured 5xx, never an
  unauthenticated forward.
- No hardcodes: every knob (region, ADC scope, upstream URL) is
  CLI-flag + env-var configurable.
- No regexes: axum path parameters + `str::rsplit_once` only.
- Comprehensive structured logs: `event` field on every decision
  point — `vertex_envelope_parsed`, `vertex_envelope_invalid`,
  `vertex_compression_skipped`, `vertex_compression_applied`,
  `vertex_adc_token_refreshed`, `vertex_adc_fetch_failed`,
  `vertex_streaming_pipeline_active`, `vertex_sse_stream_closed`,
  `vertex_forwarded`, `vertex_unknown_verb`, etc.
- Performant: no body clone; ADC token cached + refreshed
  ahead-of-expiry, not fetched per request.
- Comprehensive tests: realistic Anthropic block content
  (signature payload, redacted_thinking opaque blob) in
  `thinking_block_preserved`.

The local `gcloud auth application-default print-access-token`
returns no credentials, so manual validation against a real Vertex
endpoint is not possible in this PR. Follow-up: the user runs
`gcloud auth application-default login` once and exercises a live
Vertex request — should be a no-code-change check.

PR-D1 (Bedrock native) is running concurrently and will land its
own envelope module at `crates/headroom-proxy/src/bedrock/envelope.rs`.
The two envelope modules are intentionally siblings (not a shared
trait) — the shapes differ (Bedrock has a different
`anthropic_version` value, no `model` field, AWS SigV4 instead of
GCP ADC), and a premature shared abstraction would obscure the
provider-specific contracts. Whichever PR merges second rebases
without conflict.

Retires P4-38 (and the Vertex parts of P4-39); marketplace BYOC
pitch (per project memory) gets one more native provider.
2026-05-04 16:24:38 -07:00
chopratejas
ce37940d17 fix: PR-E6 cache-bust drift detector telemetry (Phase E)
Per-session SHA-256 fingerprint of the cache hot zone (system / tools /
first 3 messages) with structured-log emission on drift. Detector is
read-only: never mutates request bytes, preserves the Phase A
passthrough invariant. Surfaces invisible cache busts (system prompt
edited mid-session, tools reshuffled, early message changed) without
rewriting them.

* crates/headroom-proxy/src/cache_stabilization/drift_detector.rs:
  StructuralHash (system, tools, early_messages digests),
  compute_structural_hash, observe_drift, derive_session_key,
  DriftState (LRU bounded to 1000 sessions in production).
* Session keys derive from Authorization / x-api-key / client IP /
  (IP, user-agent). Bearer tokens and API keys are SHA-256 hashed
  before they ever reach the log line; the raw secret is never logged.
* Wired into forward_http after the body is buffered, before the
  compression dispatcher runs. Skips paths whose wire shape is not
  Anthropic / OpenAI Chat / OpenAI Responses.
* AppState gains drift_state: DriftState. Bedrock unit-test
  literal-construction sites updated.
* 14 unit tests + 1 integration test covering first-request,
  no-drift, per-dimension drift, multi-dim drift, LRU eviction,
  non-mutation invariant, and bearer-token-never-logged.

Adds lru = "0.12" and promotes sha2 = "0.10" to a normal dependency
on headroom-proxy.
2026-05-04 14:54:20 -07:00
chopratejas
90ef66213d fix(proxy): PR-D3 Bedrock observability + auth-mode integration
Phase D close. Adds the operator-facing observability surface that
PRs D1 (native invoke) and D2 (streaming EventStream) deferred, and
wires the Phase F PR-F1 auth-mode classifier into the Bedrock route
so downstream cache/compression policy gates have something to read.

Changes
-------

* New `bedrock::auth_mode_layer` middleware. Classifies every
  inbound Bedrock request via F1's `classify`, coerces the result
  to `AuthMode::OAuth` per the Bedrock policy matrix (SigV4 IAM is
  OAuth-equivalent), and stores the resolved value in
  `request.extensions()` so PR-F2/F3 can read it without
  re-classifying. Mismatches are logged at WARN with
  `event=bedrock_auth_mode_unexpected` — no silent coercion.

* New `observability` module with three Prometheus families:
    - `bedrock_invoke_count_total{model, region, auth_mode}` (counter)
    - `bedrock_invoke_latency_seconds{model, region}` (histogram)
    - `bedrock_eventstream_message_count_total{model, region, event_type}`
      (counter)
  Registered lazily via `OnceLock` so per-request work is just
  `inc_with_label_values` / `observe`. Latency observed via an
  RAII `LatencyGuard` so every error path is instrumented; a
  future regression that adds a new return path can't drop the
  observation.

* New `GET /metrics` endpoint serves the registry in Prometheus
  text format. Mounted unconditionally — no feature flag gate — so
  scrape works regardless of which provider routes are mounted.

* Bedrock invoke + invoke-streaming handlers now extract
  `Extension<AuthMode>`, log it in their entry breadcrumbs
  (`event=bedrock_invoke_received`, `event=bedrock_invoke_streaming_received`),
  and pass `model`/`region` into `translate_stream` so per-message
  metrics carry the right labels.

* Operator docs at `docs/bedrock.md`: AWS credential chain,
  region/endpoint config, supported model IDs (`anthropic.*`
  literal-match — no regexes), compression behaviour, sample
  PromQL queries, structured-log correlation, rollback path.

Tests added (6, all green)
--------------------------

Auth-mode (`integration_bedrock_authmode.rs`):
  1. `bedrock_classified_as_oauth` — empty headers → OAuth in
     extensions.
  2. `oauth_policy_passthrough_prefer` — body byte-equal upstream;
     no auto cache_control / prompt_cache_key injected.

Metrics (`integration_bedrock_metrics.rs`):
  3. `metrics_increment_per_invoke` — 3 invokes → counter=3 with
     correct labels.
  4. `metrics_observe_latency` — 1 invoke → histogram count=1,
     sum>0.
  5. `eventstream_metrics_per_message_type` — 5 chunks → counter=5
     with `event_type=chunk`.
  6. `metrics_endpoint_serves_scrape` — `/metrics` returns 200,
     `text/plain`, all three metric families' HELP/TYPE lines
     present.

Each metrics test owns a unique (model, region) tuple so the
global `prometheus` registry — shared across parallel tests in
the same binary — gives each test isolated label rows. Without
isolation, parallel tests cross-contaminate counters.

Constraints honoured
--------------------

* No silent fallbacks — auth-mode coercion is logged at WARN.
* No hardcodes — region from `--bedrock-region`, model from axum
  path parameter.
* No regexes — vendor prefix is literal `anthropic.`.
* Comprehensive structured logs — every metric increment paired
  with `tracing::debug!` carrying the same labels for incident
  correlation.
* Performant — `OnceLock`-cached descriptors, RAII guard, total
  D3 overhead well under 1us per request.
* Cardinality bounded — labels driven by config + bounded enums,
  never by user-controlled bytes.

Live cloud validation deferred
------------------------------

The wiremock-backed integration tests are the canonical correctness
gate for D3. A real Bedrock smoke test requires `bedrock:InvokeModel`
permissions in the developer's AWS account and is documented in
`docs/bedrock.md` — both D1 and D2 hit sandbox permission issues
trying this path; D3 follows the same convention.

Stacked on
----------

PR #364 (D1 native invoke), PR #365 (D2 streaming EventStream),
PR #366 (F1 classifier helper). Merge those first; this PR will be
rebased onto main once they land.
2026-05-04 11:07:47 -07:00
chopratejas
6f2c0a8400 fix(ci): rustls-everywhere — eliminate openssl-sys from build tree
# Root cause of the wheel-build cascade

We have shipped 5 release-pipeline hot-fixes in 12 hours, each
addressing a different symptom of the same architectural problem:

1. PR #363 — npm artifact downloads + tried `yum openssl-devel`
2. PR #367 — vendored OpenSSL in `headroom-proxy` + dropped Intel mac
3. PR #369 — Debian-cross perl install (`perl` not `libipc-cmd-perl`)
4. PR #370 — moved `openssl/vendored` from headroom-proxy to headroom-py
5. (this PR) — ELIMINATE OpenSSL entirely

Each fix exposed a different missing system package or feature flag in
a different build surface (manylinux x86_64 vs aarch64-cross-Debian vs
macOS Intel vs e2e/wrap Dockerfile vs e2e/init Dockerfile vs main
Dockerfile vs devcontainer). We were playing whack-a-mole because every
Cargo dep change to the OpenSSL surface required matching system-package
updates in 6+ different Dockerfiles and workflows, and the PR-level CI
didn't exercise all of them.

# Why this PR is the structural fix

`fastembed` exposes clean rustls feature flags:
- `hf-hub-rustls-tls`               (replaces default `hf-hub-native-tls`)
- `ort-download-binaries-rustls-tls` (replaces default `…native-tls`)

By disabling fastembed's default features and enabling the rustls
variants explicitly, we remove `native-tls` (and therefore `openssl-sys`,
`openssl`, `openssl-src`, perl modules, OpenSSL build-time deps,
vendored OpenSSL ~30s build cost) from the entire workspace dep tree.

Verified locally:

    $ cargo tree -p headroom-py -i openssl-sys
    error: package ID specification `openssl-sys` did not match any packages

    $ cargo tree -p headroom-py -i native-tls
    error: package ID specification `native-tls` did not match any packages

    $ cargo build --release -p headroom-py
    Finished `release` profile [optimized] target(s) in 25.57s

(Down from 1m+ with vendored OpenSSL.)

# Cleanups enabled by this change

- crates/headroom-py/Cargo.toml — dropped the `openssl/vendored`
  workaround from PR #370.
- crates/headroom-proxy/Cargo.toml — same dep removed.
- e2e/wrap/Dockerfile — dropped `yum install openssl-devel pkgconfig
  perl-IPC-Cmd`. Comment retained explaining why.
- e2e/init/Dockerfile — same.
- Dockerfile (main) — dropped `pkg-config libssl-dev` from apt-get.
- .devcontainer/Dockerfile — dropped `pkg-config libssl-dev`.
- .github/workflows/release.yml — removed the entire before-script-linux
  block (perl install probe + multi-package-manager dispatch + fail-loud
  assertion). No longer needed.

# Regression gate

Three new structural tests in tests/test_release_workflows.py:

- test_no_openssl_sys_in_wheel_build_tree — runs `cargo tree -p <crate>
  -i openssl-sys` for headroom-py / headroom-proxy / headroom-core. If
  openssl-sys reappears (a future native-tls enabler creeping in via a
  new dep), this fails AT PR TIME with an actionable message.
- test_no_native_tls_in_wheel_build_tree — same shape, native-tls is
  the proximate cause.
- test_fastembed_uses_rustls_features — checks the Cargo.toml so a
  future "let me bump fastembed and forget the features" doesn't
  silently re-introduce OpenSSL.

Plus two cleanup gates:
- test_dockerfiles_no_longer_install_openssl_devel
- test_release_yml_does_not_install_openssl_or_perl_for_wheels

All 13 release-workflow tests pass. `make ci-precheck` PASSED.

# What this teaches us about rollouts (per user's ultrathink ask)

The 5-fix cascade exposed three meta-problems:

1. PR checks don't block merges. PR #370 had docker-init-e2e,
   docker-wrap-e2e, docker-native-e2e all FAILED yet got merged.
   Branch protection should require these checks. Operator action
   needed (cannot fix in code).

2. Local validation is misleading. `cargo build -p headroom-py` from
   the workspace root used the workspace lockfile and looked green;
   CI did fresh resolution against headroom-py's manifest alone where
   the feature wasn't enabled. Lesson: verify structural invariants
   with `cargo tree -e features` before trusting that a build "works."

3. 6+ build surfaces with independent system-dep state. Every Cargo
   change required matching updates in 6 places. The structural answer
   (this PR) is to NOT depend on system OpenSSL at all. Where structural
   fixes are not possible, the answer is a single shared
   scripts/install-rust-build-deps.sh — but with this PR there's
   nothing left to install.
2026-05-03 23:26:04 -07:00
chopratejas
a5c7f6fed9 fix(ci): vendored OpenSSL must live in headroom-py, not headroom-proxy
PR #367 added `openssl = { features = ["vendored"] }` to
`crates/headroom-proxy/Cargo.toml`, expecting Cargo's feature
unification to propagate the vendored feature throughout the
workspace. PR #369 unblocked aarch64 by fixing the perl install.
The next release run on `1323830f` (the #369 merge) showed aarch64
+ macOS wheels build successfully but the **x86_64 Linux wheel still
fails** with:

    The system library `openssl` required by crate `openssl-sys`
    was not found.

# Why my previous reasoning was wrong

Cargo's feature unification only applies to the resolution graph
that's actually built. `maturin build --manifest-path
crates/headroom-py/Cargo.toml` resolves headroom-py's deps only —
headroom-py does NOT depend on headroom-proxy, so the
`openssl/vendored` enable in headroom-proxy never propagates to the
wheel build. `cargo tree -p headroom-py -e features` confirms:
openssl-sys is built with `default` only, no `vendored`.

(I missed this when I tested locally because `cargo build -p
headroom-py` from the workspace root reads the workspace's Cargo.lock,
which was already populated with `openssl-src` from when I added
the dep to headroom-proxy. The CI job runs `cargo rustc` with a
fresh resolution against headroom-py's manifest, where the vendored
feature isn't enabled.)

# Fix

Move the `openssl = { version = "0.10", features = ["vendored"] }`
dep from `headroom-proxy/Cargo.toml` to `headroom-py/Cargo.toml`.
The wheel-producing crate now declares the feature directly.

`cargo tree -p headroom-py -e features` after this change:

    openssl-sys feature "vendored"
        └── openssl-sys feature "openssl-src"
            └── openssl-src feature "default"
                └── openssl-src v300.6.0+3.6.2

Local `cargo build --release -p headroom-py` confirms openssl-src
is now compiled (35s wheel build).

# Tests

`test_headroom_py_vendors_openssl` (renamed from
`test_headroom_proxy_vendors_openssl`) gates the dep on the right
crate. Docstring explains WHY it must live in headroom-py — so a
future refactor doesn't move it back to headroom-proxy and silently
break the wheel build.

All 11 release-workflow tests pass. `make ci-precheck` PASSED.
2026-05-03 21:53:58 -07:00
Tejas Chopra
fb25a26180
Merge pull request #366 from chopratejas/realign-F1-classify-auth-mode
fix: PR-F1 classify_auth_mode helper (Phase F kickoff)
2026-05-03 17:56:04 -07:00
chopratejas
1314842b19 fix(ci): vendor OpenSSL via cargo + drop x86_64 macOS from wheel matrix
The previous hot-fix (#363) addressed npm artifact downloads and added
openssl-devel installs in the manylinux container, but the wheel build
still fails on three of four matrix entries with three distinct errors:

1. ubuntu-x86_64 with `manylinux: auto` resolved to manylinux2014
   (CentOS 7 / OpenSSL 1.0.2k). `openssl-sys 0.9` requires OpenSSL
   1.1.0+ — "different version of OpenSSL was found".

2. ubuntu-aarch64 cross-compiles via `aarch64-unknown-linux-gnu-gcc`
   from an x86_64 manylinux container. The `yum install openssl-devel`
   we added installs x86_64 headers; `/usr/aarch64-unknown-linux-gnu/
   include/` has no OpenSSL — "openssl/opensslv.h: No such file or
   directory".

3. macos-15-intel fails on `ort-sys` (transitive via the ML compression
   backend), which has no prebuilt ONNX Runtime binaries for
   `x86_64-apple-darwin`. Unrelated to OpenSSL; an upstream limitation.

Why the workspace pulls openssl-sys at all: `hf-hub` (transitive via
`fastembed`) hard-codes `native-tls` as a default feature. Cargo's
feature unification then enables openssl-sys for the whole workspace
despite our `reqwest`/`tokio-tungstenite`/`tokio-rustls` preferences.

# Fix 1: vendored OpenSSL

Add `openssl = { version = "0.10", features = ["vendored"] }` to
`crates/headroom-proxy/Cargo.toml`. The `vendored` feature compiles
OpenSSL from source as part of the cargo build — works on every target
uniformly. Local build verified: cargo now pulls
`openssl-src v300.6.0+3.6.2` and compiles it. ~30s extra one-time
build cost.

The `openssl/vendored` feature DEFEATS `OPENSSL_DIR`. We therefore
remove the previous hot-fix's "Install OpenSSL (macOS)" step that
exported `OPENSSL_DIR` — leaving it would silently regress to the
system-OpenSSL path that broke originally.

# Fix 2: pin manylinux floor to 2_28

Change x86_64-unknown-linux-gnu from `manylinux: auto` to
`manylinux: 2_28` (matching aarch64 + the e2e Dockerfiles). This
isn't strictly required with vendored OpenSSL — the floor is now
glibc 2.28 / AlmaLinux 8 which has modern toolchain — but it removes
the CentOS-7 surface entirely and matches our runtime container
target.

# Fix 3: drop x86_64-apple-darwin from the matrix

`ort-sys 2.0.0-rc.12` has no prebuilt ONNX Runtime binaries for that
target. Building ORT from source would add CMake + ~5 minutes per
build. Apple Silicon macOS (`aarch64-apple-darwin`) is fully covered;
Intel-mac users install from the platform-independent sdist this
matrix also produces.

Tracked as a follow-up: switch the ML backend to `ort-tract` or
upstream a request for x86_64 macOS prebuilts.

# before-script-linux: keep perl-IPC-Cmd, drop openssl-devel

OpenSSL's vendored `Configure` script needs `IPC::Cmd` (without it
the build fails with "Can't locate IPC/Cmd.pm"). System
openssl-devel is no longer needed.

# Tests

4 new regression tests gate this:
- `test_headroom_proxy_vendors_openssl`
- `test_build_wheels_installs_perl_ipc_cmd_for_vendored_openssl`
- `test_build_wheels_does_not_set_openssl_dir`
- `test_build_wheels_matrix_excludes_intel_macos`

Plus the previous 7. All 11 release-workflow tests pass.
`make ci-precheck` PASSED. Local `cargo build --release -p headroom-py`
green.
2026-05-03 17:41:24 -07:00
chopratejas
ca9de93cfc fix: PR-F1 classify_auth_mode helper (Phase F kickoff)
Add the classify_auth_mode helper that maps inbound request headers to
one of three auth modes — Payg / OAuth / Subscription — at request
entry. The mode is the first-class policy axis Phase F's remaining PRs
(F2 cache+lossy gates, F3 TOIN per-tenant aggregation, F4
X-Forwarded-* skip) gate behavior on.

Detection rules (most-specific signal wins):
- Subscription UA prefix in user-agent → Subscription
- Bearer sk-ant-oat-* → OAuth (Claude Pro/Max)
- Bearer sk-ant-api* / Bearer sk-* → Payg
- Bearer <jwt> (3 dot-segments) → OAuth (Codex/Cursor/Copilot)
- Authorization present but not Bearer (AWS SigV4) → OAuth (Bedrock)
- x-api-key / x-goog-api-key → Payg
- Default → Payg

Hard constraints met: pure function, no regex, no silent fallback
(non-UTF-8 headers warn! and fall through), no hardcoded list (UA
prefixes in module-scope const ready to swap for config in a follow-up).

Files:
- crates/headroom-core/src/auth_mode.rs (new) — Rust impl
- crates/headroom-core/tests/auth_mode.rs (new) — 14 unit + 1 perf
- crates/headroom-core/benches/auth_mode.rs (new) — Criterion bench
- crates/headroom-core/Cargo.toml — add http dep + bench entry
- crates/headroom-core/src/lib.rs — pub mod auth_mode
- crates/headroom-proxy/src/proxy.rs — classify at request entry,
  store in extensions, log event=auth_mode_classified
- headroom/proxy/auth_mode.py (new) — Python port (parity)
- headroom/proxy/handlers/anthropic.py — wire into messages handler
- headroom/proxy/handlers/openai.py — wire into chat + responses
- tests/test_auth_mode.py (new) — 23 Python parity tests
- docs/auth-modes.md (new) — detection rules + how-to-extend

Tests: 15 Rust + 23 Python all green. cargo fmt + clippy + workspace
tests + ci-precheck all green.

Performance (criterion, M-series):
- auth_mode/classify/empty: 68 ns
- auth_mode/classify/payg_anthropic_api_key: 75 ns
- auth_mode/classify/oauth_jwt: 182 ns
- auth_mode/classify/subscription_claude_code: 81 ns

All paths well under the <10us budget (~50-150x headroom).

Refs: REALIGNMENT/08-phase-F-auth-mode.md PR-F1.
2026-05-03 17:20:14 -07:00
chopratejas
66426e7b75 fix(proxy): PR-D2 Bedrock streaming via binary EventStream
Add the Phase D PR-D2 streaming counterpart to PR-D1's native
Bedrock InvokeModel route.

Bedrock's `/model/{id}/invoke-with-response-stream` returns
`application/vnd.amazon.eventstream` — a binary, length-prefixed,
CRC32-checksummed framing format. This PR adds an incremental
parser, an SSE translator, and the streaming POST handler.

Components:
- `bedrock/eventstream.rs` — stateful incremental EventStream
  parser. Validates prelude + message CRC32 (configurable via
  `--bedrock-validate-eventstream-crc`, default on). Returns
  structured `ParseError` on every malformed-bytes path; never
  panics. Supports all 10 AWS header value types; bytes-typed
  values surfaced via `HeaderValue::Bytes`, strings via
  `HeaderValue::String`.
- `bedrock/eventstream_to_sse.rs` — translator. Picks output mode
  per `Accept` header: `application/vnd.amazon.eventstream` →
  byte-equal passthrough; everything else (default) → SSE
  translation. Each `chunk` payload becomes a canonical Anthropic
  `event: <type>\ndata: <json>\n\n` SSE frame so existing
  `AnthropicStreamState` telemetry runs unchanged.
- `bedrock/invoke_streaming.rs` — POST handler. Reuses D1's
  `BedrockEnvelope`, live-zone compression, SigV4 signing.
  Tees translated SSE frames into `AnthropicStreamState` via the
  same bounded-mpsc tee pattern as `/v1/messages` — byte path
  never blocks on parser readiness.

Config:
- New `--bedrock-validate-eventstream-crc` / env
  `HEADROOM_PROXY_BEDROCK_VALIDATE_EVENTSTREAM_CRC` flag, default
  on. Disabling logs a warn at app-build time.

Routing:
- `proxy.rs::build_app` mounts
  `POST /model/:model_id/invoke-with-response-stream` only when
  `enable_bedrock_native` is on (matches D1).

Failure modes (all loud; no silent fallbacks):
- CRC mismatch → `event=bedrock_eventstream_crc_mismatch` warn,
  closes the stream with an SSE error frame.
- Parse error → `event=bedrock_eventstream_parse_failed` warn +
  SSE error frame.
- `:message-type == exception` → `event=bedrock_eventstream_upstream_exception`
  warn + SSE error frame.
- Unknown `:event-type` →
  `event=bedrock_eventstream_unknown_event_type` warn, skipped.
- Missing creds / SigV4 fail → 5xx, identical to D1.

Tests added (12 total):
- 4 parser unit-style integration: byte-equal round trip, drip-feed
  one-byte-at-a-time, CRC mismatch surfaces structured error,
  validation-off accepts corrupt.
- 3 end-to-end: `eventstream_translated_to_sse`,
  `usage_extracted_from_translated_stream`,
  `client_can_choose_eventstream_or_sse`.
- 2 property tests via `proptest`: random bytes never panic the
  parser (1024 cases each: bulk + drip-feed).
- 3 trivial smoke tests in unit modules
  (`eventstream::tests::*`, `eventstream_to_sse::tests::*`).

Manual cloud validation:
- Not exercised — running `aws bedrock-runtime invoke-model-with-
  response-stream` against the proxy in the sandbox would require
  AWS API access this environment does not have. The wiremock-
  served binary EventStream + property tests cover the parser
  semantics and CRC validation rigorously.

Stacked on PR-D1 (#364). Will be rebased onto main once D1 lands.
2026-05-03 16:48:28 -07:00
chopratejas
f2d4fe39cb fix(proxy): PR-D1 native Bedrock InvokeModel route + SigV4
Phase D PR-D1 lands the first native Rust path for AWS Bedrock,
replacing the lossy LiteLLM Python converter for Anthropic-on-Bedrock
non-streaming requests. Eliminates part of P4-37 and P4-39.

What landed
-----------

- New crates/headroom-proxy/src/bedrock/ module:
  - envelope.rs: parses the {"anthropic_version": "...", ...}
    Bedrock body shape; re-emits with anthropic_version preserved
    as the first key (relies on serde_json preserve_order).
  - sigv4.rs: AWS SigV4 signer wrapping the aws-sigv4 crate.
    Forces PayloadChecksumKind::XAmzSha256 so x-amz-content-sha256
    is in the canonical request, hashed over the post-compression
    body bytes (the bytes that actually hit Bedrock). No silent
    fallback: signing failures return 5xx with
    event=bedrock_sigv4_failed.
  - invoke.rs: POST handler for /model/{model_id}/invoke
    (and /converse - same wire shape for anthropic.claude-*).
    Detects Anthropic vendor via literal starts_with("anthropic.")
    (no regex per project rule), routes Anthropic-shape bodies
    through the existing compress_anthropic_request live-zone
    dispatcher, then signs and forwards to the configured Bedrock
    endpoint.

- Modified:
  - proxy.rs: routes /model/:model_id/invoke and
    /model/:model_id/converse when enable_bedrock_native is on
    (default). Adds bedrock_credentials: Option<Arc<Credentials>>
    to AppState.
  - config.rs: new flags --bedrock-region (default us-east-1,
    env HEADROOM_PROXY_BEDROCK_REGION), --bedrock-endpoint
    (operator override for FIPS/VPC/test setups),
    --enable-bedrock-native (default true), --aws-profile.
  - main.rs: resolves AWS credentials at startup via
    aws_config::defaults(BehaviorVersion::latest()). Failure logs
    event=bedrock_credentials_unavailable at WARN; the handler
    refuses to forward unsigned (event=bedrock_credentials_missing).
  - Cargo.toml: workspace deps aws-sigv4, aws-config,
    aws-credential-types, aws-smithy-runtime-api.

Tests
-----

8 integration tests under
crates/headroom-proxy/tests/integration_bedrock_invoke.rs:

1. native_envelope_round_trip_byte_equal
2. sigv4_signed_correctly_after_compression - confirms
   authorization is SigV4-shape and x-amz-content-sha256
   matches sha256(body received by upstream).
3. thinking_block_preserved_through_bedrock
4. redacted_thinking_preserved
5. document_block_preserved
6. tool_result_array_with_image_preserved
7. stop_sequence_null_only_when_present - pins that the proxy
   does NOT inject stop_sequence: null (P4-37 hardcode).
8. tool_use_input_byte_equal_preserves_key_order

All eight pass. Full workspace test run is green; clippy + fmt
clean. make ci-precheck (rust + python + commitlint) passes
locally.

Build constraints honoured
--------------------------

- No silent fallbacks: missing creds / signing failures return
  5xx with structured event=... log; no path ever forwards
  unsigned.
- No hardcodes: region, endpoint, profile, enable-flag all
  configurable via CLI + env.
- No regexes: vendor detection is str::starts_with.
- Comprehensive structured logs: event=bedrock_invoke_received,
  bedrock_envelope_parsed, bedrock_compression_skipped,
  bedrock_credentials_missing, sigv4_signed,
  bedrock_invoke_forwarded, etc.
- Performant: body buffered once, passed by &[u8] to signer
  (zero-copy), Bytes::clone only for ownership transfer to
  reqwest. Sign exactly once per request.
- Elegant: 4 small focused modules mirror handlers/ + sse/.
- Tests use realistic Anthropic block content (real thinking,
  redacted_thinking, document, base64 image fixtures).

Streaming (PR-D2) and observability (PR-D3) follow.
2026-05-03 16:22:32 -07:00
chopratejas
ddc6f6ceb0 fix: C1 — byte-level SSE parser + state machines
Foundation of Phase C. Delivers:

* Byte-level SSE framing (bytes::Bytes / BytesMut) with UTF-8
  decoded only at \n\n event boundaries — no per-chunk decode,
  no errors=ignore data loss across TCP reads.
* Three provider state machines:
  - Anthropic: blocks keyed by index, all delta types
    (text/thinking/input_json/citations/signature) preserved
    byte-equal.
  - OpenAI Chat: ToolCallState concatenation, refusal field,
    include_usage final chunk handling.
  - OpenAI Responses: items keyed by id (not position) for
    out-of-order completion; full event coverage.
* State machine runs in parallel with byte-passthrough via a
  tokio::spawn task fed by a bounded mpsc — clients see raw
  bytes immediately; telemetry populates without blocking.

Retires P1-8, P1-9, P1-14, P1-15, P1-17, P4-48 in the Rust path
(Python A8 hotfix preserved as fallback until Phase H).

Per-PR-C1 plan: REALIGNMENT/05-phase-C-rust-proxy.md.
2026-05-02 21:12:41 -07:00
chopratejas
00902b8fea fix: B7 — CCR hardening: persistent backends + always-on tool
P2-25, P2-26: CCR (Compress-Cache-Retrieve) used an in-memory store
that fragmented across uvicorn workers and was wiped on restart, and
the `headroom_retrieve` tool was registered/unregistered per-request
based on whether the latest body happened to contain compression
markers — every flip busted the prompt cache. Both are sticky
side-channels: once a session has done CCR, the tool list bytes and
the retrieval store must stay stable. This PR fixes both.

Rust:
* Split `ccr.rs` into `ccr/` with `backends/` submodule
  (`in_memory.rs`, `sqlite.rs`, `redis.rs` cfg-gated).
* `SqliteCcrStore` (production default): WAL mode, prepared upsert,
  lazy TTL purge on read, persistent across worker restarts and
  shareable across workers on the same host via SQLite file locking.
* `RedisCcrStore` (cfg-gated behind `feature = "redis"`): SETEX with
  startup PING smoke-test, no key-prefix collision risk, no sticky
  session required at the LB.
* `CcrBackendConfig::{InMemory, Sqlite, Redis}` + `from_config(...)`
  factory — every init failure surfaces (no silent fallback per
  `feedback_no_silent_fallbacks.md`).
* `ccr::compute_key` (BLAKE3 → first 24 hex chars) and
  `ccr::marker_for("HASH") -> "<<ccr:HASH>>"` centralize the hash +
  marker format; one definition for the live-zone dispatcher and the
  Python regex (`headroom/ccr/tool_injection.py:211`).
* `compress_anthropic_live_zone_with_ccr` accepts
  `Option<&dyn CcrStore>`. When wired, every accepted compression
  puts the original bytes into the backend and appends `<<ccr:HASH>>`
  to the compressed string. The token-validation gate runs on the
  marker-augmented string so the `compressed_tokens >=
  original_tokens` rejection stays honest.

Python:
* `SessionCcrTracker` + `apply_session_sticky_ccr_tool` mirror the
  PR-A7 `SessionToolTracker` / `apply_session_sticky_memory_tools`
  pattern: once a session has done CCR, every subsequent request
  injects the recorded golden tool-definition bytes. Tool list bytes
  are byte-stable across turns (snapshot test pins them).
* `headroom/ccr/tool_injection.py::inject_tool_definition` accepts a
  new `session_has_done_ccr` kwarg per the PR-B7 spec change at line
  302-328. The legacy per-request path stays intact for callers that
  don't yet thread a session id (e.g. Google handler).
* Anthropic + OpenAI handlers route their CCR tool-list updates
  through `apply_session_sticky_ccr_tool`, keyed off the existing
  `session_tracker_store.compute_session_id(...)` plumbing.

Backend selection model: `CcrBackendConfig::Sqlite { path }` is the
production default — single host, persistent, multi-worker safe with
sticky session. `CcrBackendConfig::Redis { url }` is the multi-host
scale-out option — no stickiness needed. `InMemory` is for tests
and single-worker dev only. RUST_DEV.md "Multi-worker deployment —
CCR fragmentation" rewritten around this matrix.

Tests:
* `crates/headroom-core/tests/ccr_backends.rs` — 7 tests covering
  SQLite round-trip, TTL purge, proxy-restart survival, cross-backend
  byte-equal keys, `from_config` paths, and the no-redis-feature
  loud-failure check (+ 2 redis tests gated behind the feature).
* `crates/headroom-core/tests/live_zone_ccr.rs` — confirms
  `<<ccr:HASH>>` marker injection, store population, and
  no-marker-when-no-store invariants end-to-end.
* `tests/test_ccr_tool_always_on.py` — 12 tests pinning the
  always-on behaviour, session/provider isolation, LRU bound, no-
  session-id fallback, and (per-acceptance-criterion) the byte-stable
  tool-definition snapshot.

Per-PR-B7 plan: REALIGNMENT/04-phase-B-live-zone.md.
2026-05-02 16:52:33 -07:00
chopratejas
a974bb153a fix(rust): PR-A1 — make /v1/messages compression a passthrough
Stop calling IntelligentContextManager from the Rust proxy on
/v1/messages. The proxy is now a byte-faithful passthrough on this
endpoint. Eliminates the C1+C2+C3+C4 cache-killer cluster (P0-3,
P0-4, P0-5, P1-13) by not running ICM with `frozen_message_count: 0`
hardcoded — Phase B PR-B2 brings live-zone-only compression back.

Per REALIGNMENT/03-phase-A-lockdown.md.

Changes:
- Add `--compression-mode {off,live_zone}` flag and
  `HEADROOM_PROXY_COMPRESSION_MODE` env var. Default `off`. Both
  modes passthrough in PR-A1; `live_zone` warns loudly because
  Phase B isn't implemented yet (no silent fallback).
- Replace `compress_anthropic_request` body with a passthrough
  stub that emits a structured `tracing::info!` decision log line
  (request_id, path, method, compression_mode, decision,
  reason="phase_a_lockdown", body_bytes) and returns
  `Outcome::NoCompression`. Function signature preserved so
  Phase B PR-B2 is a pure body swap.
- Delete `compression/icm.rs` (per the realignment plan: ICM
  modules in headroom-core are deleted in PR-B1).
- Drop the `Arc<IntelligentContextManager>` field from `AppState`
  — no longer used.
- Add request-entry `tracing::debug!` with auth_mode_placeholder
  ("unknown" until Phase F PR-F1 wires the auth-mode classifier).
- Add `debug_assert!` on the NoCompression branch that the
  buffered bytes length is stable, locking in Phase A's
  cache-safety invariant at the call site.
- Tighten existing tests from `len()` equality to SHA-256 byte
  equality. Rename `compression_on_oversized_body_trims_messages`
  → `compression_on_long_body_passes_through_in_phase_a` and
  flip the assertion to byte-equal.
- Add new tests: passthrough_mode_off_byte_equal_sha256,
  passthrough_mode_live_zone_currently_passthrough_byte_equal_sha256,
  passthrough_preserves_numeric_precision (literal-byte body so
  serde_json's f64 quantization can't mask a regression),
  passthrough_preserves_cache_control_markers,
  passthrough_preserves_thinking_signature,
  passthrough_preserves_redacted_thinking_data,
  passthrough_recorded_fixture_byte_equal_sha256,
  tracing_capture::compression_decision_logged.
- Add fixture
  `crates/headroom-proxy/tests/fixtures/anthropic_messages_request_real.json`
  with system block list + cache_control markers, tools with
  nested JSON Schema, messages containing text + thinking +
  signature + tool_use + tool_result + image, non-ASCII content,
  large numbers. Used as the canonical SHA-256 round-trip gate.

Constraints honored: configurable (compression_mode is the only
new knob), no hardcoded thresholds, no regex usage, no silent
fallbacks (live_zone-not-implemented warns), structured tracing
on every cache-affecting decision, comprehensive tests.

Acceptance criteria from PR-A1 spec:
- `cargo build --workspace` clean
- `cargo test --workspace` green (886 tests pass)
- `cargo clippy --workspace -- -D warnings` clean
- `cargo fmt --all --check` clean
- `make ci-precheck` green
- New SHA-256 byte-equality tests pass against the recorded fixture
- `tracing::info!` decision-log line is observable
- `--compression-mode` CLI + env var work
- No regex import added
2026-05-01 23:58:20 -07:00
chopratejas
378d8a0f05 fix(rust): audit cleanup — DiffCompressor CCR leak, CCR TOCTOU race, clippy debt, dep dedup
Closes findings from the post-Phase-3g audit. Five surgical fixes
plus telemetry-discoverability docs. PyO3 0.22 → 0.24 security
upgrade is its own PR (issue #335).

1. DiffCompressor cache_key persistence (production bug)
---------------------------------------------------------
Pre-fix: `RustDiffCompressor.compress()` minted a `cache_key`,
embedded `[... hash=abc123]` in the wire marker, and returned
without storing the original anywhere. Python ContentRouter then
returned the compressed text with a dangling marker — every
retrieval tool call from the LLM 404'd.

Sibling compressors (LogCompressor, SearchCompressor) already had
the right pattern: Rust mints the key, Python's
`_persist_to_python_ccr` writes the original to the production
`CompressionStore`. DiffCompressor was the asymmetric one.

Fix:
- Rust: add `DiffCompressor::compress_with_store(content, context,
  Option<&dyn CcrStore>)` mirroring siblings. Calls `store.put`
  when a key is minted; legacy `compress()` and
  `compress_with_stats()` delegate with `None` for parity.
- Python: add `_persist_to_python_ccr` helper to
  `headroom/transforms/diff_compressor.py.compress()` mirroring
  `log_compressor.py` and `search_compressor.py`.
- Pipeline `DiffOffload`: switch to `compress_with_store(Some(store))`
  and drop the post-hoc double-store hack that papered over this
  bug at the orchestrator boundary.

2. CCR store TOCTOU race in `get()`
-----------------------------------
`InMemoryCcrStore::get()` checked TTL under a read lock, dropped
the lock, then called `remove()`. Between drop and remove a
concurrent `put()` of the same hash with fresh data could land —
and our `remove` would then wipe that fresh entry. Under
multi-worker proxy load this manifested as "I just stored it; why
is it gone?"

Fix: use `DashMap::remove_if`. Predicate runs under the shard
write lock so check-and-remove is atomic. New regression test
exercises a tight contention loop between writer and reader on
the same key.

3. Pre-existing clippy debt in smart_crusher
--------------------------------------------
- 3× `field_reassign_with_default` in `crusher.rs` test setup —
  switch to struct-update syntax `Config { field: x, ..Default }`.
- `hash_array_for_ccr` was `#[cfg(test)]` but unused; deleted with
  a comment so a future test can reintroduce it as a one-liner.

`cargo clippy --workspace --all-targets -- -D warnings` is now
clean across the whole workspace; previous CI patches that allowed
these warnings can be removed in a follow-up.

4. Tokenizers dependency dedup
------------------------------
`tokenizers 0.21` (direct dep) + `tokenizers 0.22` (transitive via
fastembed) compiled twice into the binary. Bumped direct dep to
`0.22` to align; API is compatible (verified by full tokenizer
test suite). Saves compile time + binary bloat.

5. Telemetry-discoverability doc (no new code)
----------------------------------------------
The audit recommended a per-transform invocation counter to
inform the next Python → Rust port. Discovered the infrastructure
already exists at `/stats`:
- `compressions_by_strategy` — invocation count per strategy
- `pipeline_timing` — count + avg/max ms per transform name
- `tokens_saved_by_strategy` — savings attribution

Added a section to `RUST_DEV.md` showing the `curl + jq` recipes
to read this data, with example output highlighting how to spot
zero-invocation deferral candidates (e.g. `code_compressor`).

Verification: workspace tests 734 + 14 + 5 + 4 + 6 + 5 + 2 + 2 +
3 + 4 + 2 + 2 + 1 = all green; cargo fmt clean; cargo clippy
--all-targets clean; Python tests 185 pass; commitlint clean.
2026-04-30 20:54:22 -07:00
chopratejas
01a423a316 fix(rust): reformat/offload pipeline + log templates + diff noise (Phase 3g rework)
Replaces PR1's lossless/lossy split with ReformatTransform (pack
denser, no info lost) and OffloadTransform (drop bytes, CCR-stash
original via required cache_key). With CCR every transform is
information-preserving end-to-end, so the lossless/lossy distinction
misnamed the architecture.

OffloadTransform carries a cheap, structural estimate_bloat() method
scoped to its domain — generic byte-redundancy heuristics miss domain
semantics. The orchestrator runs reformat phase + per-offload bloat
estimation in parallel via rayon::join + par_iter, then runs offload
iff bloat clears threshold OR reformat underwhelmed.

Transforms shipped:

REFORMATS (lossless):
- JsonMinifier: serde_json round-trip whitespace stripping.
- LogTemplate: Drain-inspired order-preserving template miner.
  Collapses consecutive runs of same-template lines into
  [Template Tn: ...] (Nx) + variant table. Win comes from emitting
  the constant-token prefix once instead of N times. Lossless: every
  original line reconstructible from template + variants.

OFFLOADS (drop bytes, stash original via CCR):
- LogOffload: wraps existing LogCompressor; bloat = repetition x
  uniqueness_weight + dilution x priority_dilution_weight.
- DiffOffload: wraps existing DiffCompressor; bloat = context-to-
  change ratio. Bug-fix-on-port — persists original under the
  cache_key the parity-bound DiffCompressor mints (closes a leak).
- DiffNoise: drops lockfile hunks (Cargo.lock, package-lock.json,
  yarn.lock, etc., suffix list configurable in TOML) and
  whitespace-only hunks. Stashes original via CCR for retrieval.

Search offload exists but is not in default re-exports — modern
agents (Claude Code, Codex) use scoped rg/grep, the marginal value
didn't justify default registration. Reach via the explicit module
path if opting in.

JSON Offload is intentionally absent from this PR — already lives at
SmartCrusher; Phase 3g PR3 wraps it in the OffloadTransform contract.

Thresholds and weights live in config/pipeline.toml, embedded via
include_str!; PipelineConfig::from_toml_str loads runtime overrides.

98 new pipeline tests; full headroom-core suite (714) and workspace
tests green; cargo fmt clean. No regex, per project convention.
2026-04-30 11:10:24 -07:00
chopratejas
12c2665531 feat(rust): signals trait module + KeywordDetector (Phase 3e.1)
Establish `crates/headroom-core/src/signals/` as a top-level module
holding cross-cutting detection traits. Phase 3e.1 ports
`error_detection.py` to a `LineImportanceDetector` trait + a
`Tiered<T>` combinator + a single concrete `KeywordDetector` impl
backed by aho-corasick. Three traits at three granularities are
sketched (line / blob / item); only line-importance is implemented
today.

Two bug fixes from the Python source bake into both the Rust impl
and the Python regex shim:

1. `ERROR_KEYWORDS` listed `timeout|abort|denied|rejected` but
   `ERROR_PATTERN` regex omitted them. Lines like `"Connection
   timeout"` were silently neutral despite the keyword being canonical.
   Both surfaces now flag them.
2. `SECURITY_KEYWORDS` carried `token`, which false-positived on
   every reference to LLM tokens (`input_tokens`, `tokens_saved`, ...)
   in our own product. Dropped from the security set.

The Python `error_detection.py` shim now reflects keyword data out of
Rust via `keyword_registry_snapshot()` and recompiles the legacy
`re.Pattern` objects on the fly. Existing callers (text_compressor,
search_compressor, intelligent_context) continue to import the same
names with no source changes; caller migration to the trait API
happens in their own port PRs.

The trait architecture is the seam where a future ML detector slots
in without touching `KeywordDetector` or any caller. The canonical
extension is documented in `signals/README.md` as a classifier head
on the existing `bge-small-en-v1.5` embedder loaded by
`relevance::EmbeddingScorer` -- 384-dim -> 4-class softmax,
~1.5 KB head, ~1 ms inference, no extra model file. Two alternatives
(distilled tinyBERT in ONNX, logistic regression on lexical
features) are kept open in case BGE-head underfits.

Per the no-silent-fallbacks rule: only `KeywordDetector` lands as a
concrete impl. No NoOp, no MockDetector, no stub-ML -- those will
arrive with their real implementations.

Phase 3g (Compression Pipeline Formalization, issue #315) is queued
as the cross-cutting follow-up that will make lossless-then-lossy-
then-CCR ordering an explicit, observable architecture rather than
implicit per-compressor logic. Trait shapes there will reuse the
signals primitive landed in this PR.
2026-04-29 15:55:13 -07:00
chopratejas
19fc49ac39 chore(rust): port unidiff Tier 2 diff detector (Stage 3d PR4)
Adds the second tier of the Stage-3d ContentRouter detection arch.
Magika (PR3) is a probabilistic ML classifier — short, prose-prefixed,
or "looks like code because the lines are code" diffs can slip past
it into PlainText. PR4 catches those by running the [`unidiff`]
parser as a deterministic oracle: anything that parses to ≥1
PatchedFile with ≥1 hunk is a diff.

What lands:
- `crates/headroom-core/src/transforms/unidiff_detector.rs`:
  - `is_diff(content) -> bool`: predicate.
  - `detect_diff(content) -> Option<ContentType>`: typed wrapper for
    the router (PR5) to chain after Magika.
  - Empty input shortcuts to false without invoking the parser.
  - "Found zero hunks" is treated as **not** a diff — `unidiff::PatchSet
    ::parse` returns Ok(()) on plain text (just finds zero files);
    we explicitly require non-empty patch + non-empty hunk to avoid
    silently routing prose through the diff compressor.
- 14 unit tests: standard git diff, naked hunk without git header,
  multi-file, added/removed-only files, JSON/HTML/YAML/source/prose
  negatives, "almost looks like a diff" prose with @@/--- in passing,
  truncated-diff canary.

Known gaps (deliberately punted to PR5+):
- Combined-merge headers (`@@@ ... @@@`) — `unidiff`'s hunk regex is
  for plain `@@`. Rare in proxy traffic; PR5 router can fall back
  to the regex content_detector if needed.
- Pathological CRLF-stripped inputs — `input.lines()` strips `\r`
  only when paired with `\n`. Acceptable.

What does NOT land here (per PR scope):
- No PyO3 surface — module-only.
- No router rewiring — the existing regex `content_detector` still
  drives `ContentRouter`. PR5 chains magika → unidiff → PlainText.

The `unidiff` crate brings `regex` (already in tree) and `encoding_rs`
(default features) — small dep impact.

`make ci-precheck` green.
2026-04-28 23:19:09 -07:00
chopratejas
d34658b22e chore(rust): port Magika detection (Stage 3d PR3 — Tier 1)
Adds Google's `magika` ONNX-backed content classifier as the first
tier of the new Stage-3d ContentRouter detection arch (`magika` →
`unidiff-rs` → `PlainText` fall-through; no regex tier on the Rust
side).

What lands:
- New module `crates/headroom-core/src/transforms/magika_detector.rs`:
  - `magika_detect(content: &str) -> Result<ContentType, _>`
  - `OnceLock<Mutex<Result<Session, _>>>` singleton: model loads
    once per process; init failure is recorded once and cheaply
    replayed (no retry — rust-side `feedback_no_silent_fallbacks`).
  - `map_magika_label(&str) -> ContentType`: explicit match arms
    against magika's 200+ labels, mapped onto Headroom's existing
    `ContentType` enum so the dispatch (PR5) stays enum-stable.
    Unmapped labels passthrough to `PlainText` rather than misroute.
- 16 unit tests: empty fast-path, JSON / Python / Rust / JS /
  diff / markdown / plain prose / HTML / YAML / shell / SQL,
  singleton-reuse smoke, default-passthrough for unmapped labels,
  pure-table-lookup sanity.

What does NOT land here (per PR scope):
- No PyO3 surface yet — PR3 is detector-only.
- No router rewiring — the existing regex `content_detector` still
  drives `ContentRouter` until PR5 flips the dispatch.
- No `unidiff-rs` Tier-2 — that's PR4.

The `magika` crate brings `ndarray` + `ort` (already in our dep
tree via `fastembed`); adding it shares the ONNX Runtime singleton
rather than pulling a second ML stack.

`make ci-precheck` green.
2026-04-28 22:36:28 -07:00
chopratejas
29aadb1054 perf(rust): tier-1 multi-worker wins — GIL release, sharded CCR store, single-serialize CCR write
Three orthogonal hot-path fixes targeting concurrent-request throughput.
Each is independently bench-measured below; the proxy hot path benefits
from all three at once.

== 1. PyO3 GIL release on heavy compute ==

PyO3 methods (crush, smart_crush_content, crush_array_json,
compact_document_json, compress, compress_with_stats) used to hold the
GIL across the entire Rust call. Result: a 100ms compress() blocked
EVERY other Python thread for 100ms — multi-worker uvicorn deployments
serialized through SmartCrusher.

Wrap each compute call in `py.allow_threads(|| ...)`. Inputs (`&str`
from Python) are copied to owned `String` first because PyO3 ties them
to the GIL hold. PyDict construction stays on the GIL side.

Measured: 4 Python threads each running 20 crushes:
  before (GIL held): ~3.3s wall    (serialized — equivalent to 4×0.83s)
  after (allow_threads): 826ms wall (4.01x speedup, perfect parallel)

== 2. CcrStore: Mutex<HashMap> -> DashMap-backed sharded ==

Single Mutex was the dominant bottleneck under multi-worker load — every
put/get serialized through one lock. Replace with DashMap (sharded
concurrent map, lock-free reads within a shard) plus a separate
small Mutex<VecDeque> for FIFO insertion-order eviction. Reads of
distinct keys never contend; writes only contend during the brief
order-queue push or capacity-sweep.

A/B bench (200 mixed put/get ops × N threads, in benches/ccr_store.rs):
  Threads | DashMap   Legacy Mutex  Speedup
  -------------------------------------------
       1  |   63 µs        71 µs       1.13x
       2  |   98 µs       194 µs       2.0x
       4  |  178 µs       707 µs       4.0x
       8  |  342 µs      1267 µs       3.7x

Legacy degrades ~linearly with thread count; DashMap stays near-flat
per-thread. Real multi-worker scaling.

== 3. Single-serialize the lossy CCR payload ==

The lossy `crush_array` path used to serialize the full array TWICE:
once in `hash_array_for_ccr` (allocates `Value::Array(items.to_vec())`,
deep-clones every Value subtree, then serializes), and a second time
in the store-write site. For a 50-item dict array that's ~MB of
allocator pressure per crushed array.

Introduce `canonical_array_json` (serializes `&[Value]` directly — same
bytes as `Value::Array(items.to_vec())` but no wrapper allocation +
no tree clone), call it ONCE per lossy path, then both hash and store
from those same bytes. Hash-format stable — all 17 parity fixtures
match byte-for-byte.

== Tests ==

- 8 ccr.rs unit tests including a new concurrent-stress test (8 threads
  × 200 puts/gets, every key readable afterwards)
- 14 ccr_roundtrip integration tests stay green
- parity-run smart_crusher: 17/17 fixtures match
- 479 lib + 14 integration + 185 Python tests all pass
- New benches/ccr_store.rs runs the A/B and is committed for regression
  visibility

== Dependencies added ==

- dashmap v6  (mature, widely-used in tokio/linkerd ecosystem)
2026-04-27 22:25:47 -07:00
chopratejas
22c8fec4c1 chore(rust): SmartCrusher CCR storage layer + roundtrip verification
CcrStore trait + InMemoryCcrStore (1000 entries, 5-min TTL, FIFO
eviction, idempotent re-store) live at the crate root. SmartCrusher's
lossy crush_array path now actually stashes the full original [items]
canonical-JSON into the configured store keyed by the same ccr_hash it
embeds in the prompt marker -- closing the no-data-loss contract that
was previously hash-only.

PyO3 surface:
- crusher.crush_array_json(items_json) -> dict with ccr_hash + kept items
- crusher.ccr_get(hash) -> Optional[str] for retrieval
- crusher.ccr_len() -> int for telemetry

Python shim passes both through. Default constructors enable the store
(matches Python's CCR-enabled default); without_compaction() also gets
it because CCR is a contract, not an opt-in extra.

Tests proving compress -> store -> retrieve -> reconstruct:
- 7 unit tests in ccr.rs (put/get/eviction/expiry)
- 9 Rust integration tests (crates/headroom-core/tests/ccr_roundtrip.rs)
- 10 Python tests including 4 explicit before/after element-equality
  assertions through both the native PyO3 surface and the Python shim

Plugin manifest versions auto-bumped by the sync-plugin-versions
pre-commit hook (unrelated to CCR but co-resident in the working tree).
2026-04-27 19:36:14 -07:00
chopratejas
1945e5f55b feat(rust): real fastembed-rs EmbeddingScorer (BAAI/bge-small-en-v1.5)
Replace the embedding scorer stub with a real fastembed-rs
implementation. Same library + same model as the Python side will
use after the next commit, giving byte-equal embeddings on identical
inputs.

Cargo.toml: fastembed = "5". Default features pull in `ort` (ONNX
Runtime) with auto-download of the runtime binary at build time
(~21s additional first-build); model weights (BAAI/bge-small-en-v1.5,
~30 MB int8-quantized ONNX) auto-download from HuggingFace Hub on
first use.

embedding.rs:
- EmbeddingScorer wraps Option<Mutex<TextEmbedding>>. Mutex required
  because TextEmbedding::embed needs &mut self (single-threaded ONNX
  session); concurrent callers serialize on the lock, fine for the
  SmartCrusher hot path where inference dominates lock contention.
- EmbeddingScorer::try_new() — explicit construction with HF Hub
  download. Returns Result; surface errors to callers.
- EmbeddingScorer::try_new_with_model(EmbeddingModel) — bring your
  own model from fastembed's catalog.
- EmbeddingScorer::default() — STUB only (model=None,
  is_available()=false). Mirrors Python's "sentence-transformers
  not installed" branch byte-for-byte. To get a real scorer, call
  try_new() and pass via HybridScorer::with_scorers().

Why default() is a stub: with auto-load Default, model availability
would depend on whether HF Hub cache has the file — non-deterministic
in tests. Explicit try_new() keeps Default cheap and predictable.

cosine_similarity:
- f32 vec inputs (fastembed returns Vec<Vec<f32>>).
- Clamped to [0, 1] (mirrors Python _cosine_similarity — only
  positive similarity matters for relevance).
- Defensive: zero vectors / mismatched dims → 0.0.

score / score_batch:
- Empty input / unavailable model → empty score with explanatory
  reason.
- Batch encodes items + context in one model call (Python parity:
  amortizes model dispatch).
- Inference failures degrade gracefully with empty scores rather
  than panicking.

Tests:
- 5 cosine-similarity unit tests (offline).
- 3 unavailable-scorer tests (model=None path).
- 3 model-backed integration tests gated on RUN_FASTEMBED_TESTS=1
  (semantic-match-outranks-unrelated, batch-shape, model-loads).
- All 388 headroom-core tests pass without RUN_FASTEMBED_TESTS;
  with it set, the gated 3 also pass.

Net: 388 unit tests, clippy clean. HybridScorer's BM25-fallback path
remains correct (default embedding scorer reports unavailable).
Stage 3c.1 next: switch Python's relevance/embedding.py to fastembed
PyPI package + record parity fixtures with real embeddings on both
sides.
2026-04-26 23:24:59 -07:00
chopratejas
9d515fb78e feat(rust): smart_crusher universal crushers — string, number, object
Three crushers from headroom/transforms/smart_crusher.py ported.
Each takes a SmartCrusherConfig + bias and returns
(crushed_items, strategy_string). All schema-preserving — output is
items/values from the original; no generated text.

What's in:

1. compute_k_split (smart_crusher.py:2693)
   Wraps adaptive_sizer::compute_optimal_k. Splits k_total into
   first/last/importance via config.first_fraction / last_fraction.
   Uses f64::round_ties_even() (Rust 1.77+) to match Python's
   banker's-rounding round() — important for off-by-one parity on
   .5-edged k computations.

2. crush_string_array (smart_crusher.py:2727)
   Adaptive K via Kneedle. Mandatory-keep: error-keyword strings +
   length-anomaly strings (>variance_threshold σ from mean length).
   Boundary-keep: first K_first + last K_last. Stride-based diverse
   fill with content-dedup. Output preserves original array order
   (BTreeSet iteration). Strategy includes dedup= and errors= counts
   when nonzero.

3. crush_number_array (smart_crusher.py:2810) — CARRIES BUG #1
   Statistics-driven (mean/median/stdev/p25/p75). Outliers flagged
   at variance_threshold σ. Change-points via window-mean comparison
   (config.preserve_change_points + n>10 gates). Strategy string
   embeds full stats summary via format_g (Python's :.4g approximation).
   BUG #1 — percentile off-by-one — ported AS-IS:
   sorted_finite[len/4] / sorted_finite[3*len/4]. Cosmetic
   (strategy-string only). Test bug1_percentile_off_by_one_documented
   pins the buggy index choice; commit 7 fixes both languages and
   regenerates fixtures.

4. crush_object (smart_crusher.py:3015)
   Token-budget gate (config.min_tokens_to_crush=200). Three
   passthrough exits: n<=8, total tokens too low, k_total>=n. Always
   keeps: error-keyword values + small values (<=12 tokens via
   len/4 + len/4 + 2 heuristic). Boundary keys + stride fill with
   Python's recompute-each-iter cap (mirrored faithfully — slower
   but parity-true). Output preserves key insertion order via
   serde_json/preserve_order's IndexMap.

Supporting helpers in stats_math.rs:
  - median(values) — Python statistics.median (mean-of-middles for
    even, total_cmp sort for NaN determinism).
  - format_g(x) — approximate Python f"{x:.4g}" (4 sig figs,
    scientific outside [-4, 4) exponent range, trailing-zero strip,
    explicit-sign 2-digit exponent). Pinned by 5 fixed-output tests.

Field iteration order: key/object iteration uses BTreeMap-sorted (in
analyzer) and IndexMap-insertion-order (in serde_json::Map for
crush_object). The Python sorted-key fix scheduled for commit 7 also
covers crush_object's iteration paths.

Net: 266 unit tests passing in headroom-core, clippy clean (MSRV 1.80),
parity harness intact (4/4 diff_compressor).

Next commit: planning + execution layer (_create_plan, _execute_plan,
plan-builder methods) with BUG #4 fix (k-split overshoot).
2026-04-26 18:02:23 -07:00
chopratejas
a64716d5d1 fix(rust): smart_crusher scaffold review findings — hash truncation, int parse, python-repr matcher
Code review (`/code-review` on commit `d219bee`) caught one critical
bug, two important parity gaps, and a few quality nits. Fixed all of
them; all 135 unit tests pass; diff_compressor parity harness
unaffected (27/27 still matched).

# Critical fix — `hash_field_name` truncation length

Rust truncated SHA-256 to **16** hex chars; Python uses **8** (per
`smart_crusher.py:177`: `hashlib.sha256(...).hexdigest()[:8]`). 16-char
hashes would never collide with TOIN's 8-char `preserve_fields`,
silently disabling the entire `use_feedback_hints` cache lookup path.

Fix: `hex[..8]` instead of `hex[..16]`. Three pinning tests re-verified
against actual Python reference output. Doc comment now warns
explicitly that the length must match Python or TOIN lookups silently miss.

# Important fix — `python_int_parse` mirrors Python's `int()` semantics

`statistics.rs::detect_sequential_pattern` previously called
`s.parse::<i64>()`. Python's `int()` differs in three ways that affect
realistic payloads:
  - strips ASCII whitespace (Rust's `parse` rejects)
  - accepts leading `+` (Rust accepts; same)
  - accepts PEP 515 underscores like `"3_000"` (Rust rejects)

A field with `["  1  ", "  2  ", " 3 ", "4", "5"]` would parse all five
in Python (sequential = True) but only one in Rust (`nums.len() < 5`
→ False). Silent parity break.

Fix: new private `python_int_parse` helper that strips whitespace,
handles underscore separators, and rejects edge cases Python rejects.
Six new tests pin the behavior.

# Important fix — `python_repr` for `item_matches_anchors`

Python compares anchors via `anchor in str(item).lower()`. We were
using `serde_json::to_string(&item).to_lowercase()`, which differs in
three ways that affect substring matching:
  - quote chars (`'` vs `"`)
  - bool/null literals (`True`/`False`/`None` vs `true`/`false`/`null`)
  - spacing (`key: value, ...` vs `key:value,...`)

Anchor `"none"` would match Python form but not JSON. Inverse for
`"null"`. Real divergence.

Fix: new private `python_repr` walks `serde_json::Value` and emits
Python-equivalent form. Plus enable `serde_json/preserve_order` at
workspace level so `Value::Object` preserves JSON parse order
(matching Python `dict` since 3.7).

# Suggestion fixes

- Classifier comment for `[True, False, 1] -> MIXED_ARRAY` now walks
  both Python and Rust paths step by step.
- `ArrayAnalysis::field_stats` doc notes the BTreeMap vs Python-dict
  order nuance for the analyzer port to resolve.
- Added regression tests for "all unparseable strings", "single int
  among strings", fractional-step sequential, and the email-typo
  pattern.

# Build / test

- `cargo build -p headroom-core` clean.
- `cargo clippy -p headroom-core -- -D warnings` clean.
- 135 unit tests in `headroom-core`, all passing (was 55).
- `cargo run -p headroom-parity run` — diff_compressor 27/27 still matched.
2026-04-26 17:01:46 -07:00
chopratejas
d219beecab feat(rust): scaffold smart_crusher module + foundational helpers
Stage 3c.1 — like-for-like Rust port of `headroom/transforms/smart_crusher.py`.
This commit lays the foundation: module layout, configuration, foundational
data types, and the simpler helpers (classification, hashing, anchors,
basic statistics). Subsequent commits add the analyzer, crushers, plan
execution, and the orchestrator.

# What's in this commit

`crates/headroom-core/src/transforms/smart_crusher/`:
- `mod.rs` — module entry, public re-exports, port narrative.
- `classifier.rs` — `classify_array` / `ArrayType` (dict/string/number/
  bool/nested/mixed/empty). Direct port of `_classify_array`.
- `config.rs` — `SmartCrusherConfig` with defaults pinned to Python
  byte-for-byte.
- `hashing.rs` — `hash_field_name` (SHA-256 truncated to 16 hex chars),
  matches `hashlib.sha256(name.encode()).hexdigest()[:16]` exactly.
- `statistics.rs` — `is_uuid_format`, `calculate_string_entropy`,
  `detect_sequential_pattern` (with **BUG #2 fix** — see below).
- `anchors.rs` — `extract_query_anchors`, `item_matches_anchors`. Five
  regex patterns ported via `std::sync::LazyLock`.
- `types.rs` — `CompressionStrategy`, `FieldStats`, `CrushabilityAnalysis`,
  `ArrayAnalysis`, `CompressionPlan`, `CrushResult`. Field-by-field
  mirror of the Python @dataclasses so the PyO3 bridge in 3c.1b can
  reconstruct them without manual translators.

# Bug #2 fixed in this commit (Python fix lands later in same PR)

`smart_crusher.py:444-448` — `_detect_sequential_pattern` calls
`int(string_value)` and silently strips zero-padding, so padded string
IDs like `["001", "002", ..., "100"]` get misclassified as a sequential
numeric pattern. Fix: track whether each parsed numeric value
originated as a string. If EVERY parsed value was a string, refuse to
flag as sequential. Mixed numeric+string fields still detect
correctly because the unambiguous numerics dominate. Test:
`bug2_zero_padded_strings_no_longer_misclassified`.

# What's NOT in this commit (subsequent commits)

- `SmartAnalyzer` — `analyze_array`, `_analyze_field`, `_detect_change_points`,
  `_detect_pattern`, `_detect_temporal_field`, `analyze_crushability`,
  `_select_strategy`, `_estimate_reduction`.
- The five array crushers (`_crush_array`, `_crush_string_array`,
  `_crush_number_array`, `_crush_mixed_array`, `_crush_object`).
- Planning (`_compute_k_split`, `_create_plan`, `_plan_*` family).
- Orchestration (`_prioritize_indices`, `_deduplicate_indices_by_content`,
  `_fill_remaining_slots`).
- `SmartCrusher` orchestrator class itself.
- Parity harness fixtures.
- The remaining 3 Python bug fixes (#1, #3, #4) — landed alongside the
  code paths they affect.

# Build / test

- `cargo build -p headroom-core` — clean.
- `cargo clippy -p headroom-core -- -D warnings` — clean.
- 55 new unit tests across the 6 new files, all passing.

Architectural improvements (lossless-first, unified saliency score,
structured CCR markers) are deferred to Stage 3c.2 — see design doc at
`~/Desktop/SmartCrusher-Architecture-Improvements.md`.
2026-04-26 16:45:42 -07:00
chopratejas
5c3c9c49f2 feat(rust): diff_compressor port — byte-equal parity + sidecar stats
Stage 3a: first real transform port. Faithful Rust port of
`headroom.transforms.diff_compressor` with byte-equal parity against all
20 recorded fixtures.

# Algorithm (matching Python)

1. Hand-rolled unified-diff parser (state machine over `diff --git`,
   `index`, `--- a/`, `+++ b/`, `@@`, mode/binary/rename markers, +/- /
   space lines, "other" lines like `\ No newline at end of file`).
2. File cap (`max_files=20`): when fired, sort by total changes (most
   first) and keep top N.
3. Per-file hunk cap (`max_hunks_per_file=10`): keep first + last + top
   relevance-scored middle, then resort by hunk-header start line to
   restore appearance order.
4. Relevance scoring: change-density base + user-query word overlap
   + priority patterns (ERROR / IMPORTANCE / SECURITY regexes —
   matches `error_detection.PRIORITY_PATTERNS_DIFF`).
5. Per-hunk context trim: keep `max_context_lines=2` lines either side
   of each `+`/`-` line.
6. CCR cache_key: `md5(original)[:24]` (matches
   `compression_store.CompressionStore.store`). Emitted only when
   compression saved >20% of lines.

Parity result: `[diff_compressor ] total=20 matched=20 skipped=0 diffed=0`.

# Information preservation hardening

Three pass-through paths inherited from Python that we keep deliberate
(would lose info if we changed them):
- Below `min_lines_for_ccr` (50): return input unchanged.
- No diff sections parsed: return input unchanged.
- Below 20% compression savings: emit compressed output but no CCR
  marker (the original is the cheaper representation anyway).

Plus a parity-bound subtlety: `compressed_line_count` is captured BEFORE
the CCR retrieval marker is appended, both for the marker text
(`compressed to N`) and the result field. The output string therefore
ends up with one more line than the field reports — by design, matching
Python exactly. An off-by-one bug from recounting after appending the
CCR marker was caught and pinned by a synthetic 8-file diff test.

# Observability — the Rust escape hatch

Python's `DiffCompressionResult` has thin observability: input/output
line counts, additions/deletions, hunks_kept/removed, files_affected,
cache_key. The Rust port adds a sidecar `DiffCompressorStats` struct
with metrics Python doesn't emit:

- `files_dropped: Vec<String>` — names (old → new path) of files
  silently discarded by the `max_files` cap. Python loses these.
- `hunks_dropped_per_file: BTreeMap<String, usize>` — per-file hunk
  drops, stable iteration via `BTreeMap`.
- `context_lines_input` / `context_lines_kept` / `context_lines_trimmed`
  — directly proxies info loss from the context trim.
- `largest_hunk_kept_lines` / `largest_hunk_dropped_lines` — outlier
  detection (a single huge dropped hunk is much worse than many small).
- `parse_warnings: Vec<String>` — surfaces malformed input rather than
  dropping silently.
- `processing_duration_us` — latency budget.
- `cache_key_emitted` + `ccr_skipped_reason: Option<String>` — explicit
  signal for "we chose not to emit CCR and this is why".

A `tracing::info!(target: "diff_compressor", ...)` event is emitted on
every call, carrying these fields for OTel scraping in prod. The
sidecar struct is returned alongside via `compress_with_stats`; the
parity-only `compress` API discards it.

# Module layout

- `crates/headroom-core/src/transforms/mod.rs` — namespace, doc comment
  with the guiding principle ("information preservation > aggressive
  compression") so future ports inherit the philosophy.
- `crates/headroom-core/src/transforms/diff_compressor.rs` — full port
  (parser, scorer, hunk selector, context trimmer, formatter, CCR layer,
  stats, tracing).

# Dependencies added to headroom-core

- `md-5 = "0.10"` — for the CCR cache_key (matches Python MD5[:24]).
- `regex = "1"` — was a transitive dep via tokenizers; now a direct
  dependency for the hunk-header parser and priority patterns.

# Tests

6 unit tests covering pass-through paths, MD5 hex truncation, the
Python `split("\n")` line-count semantics, sidecar stats emission,
and a synthetic 8-file diff that locks the byte-equal behavior found
in the parity fixtures.
2026-04-26 09:13:08 -07:00
chopratejas
a23ee8e70b feat(rust): HfTokenizer::from_pretrained — HuggingFace Hub auto-download
Stage 2.1: closes the loop on the HuggingFace tokenizer story. Stage 2
shipped `HfTokenizer::from_bytes`/`from_file`, which required callers to
manage their own tokenizer.json files. This adds the third constructor:

    let t = HfTokenizer::from_pretrained("CohereForAI/c4ai-command-r-v01")?;
    register_hf("command-", t);

`from_pretrained` is a thin wrapper around the `hf-hub` crate's blocking
`ureq` API. First call downloads `tokenizer.json` to `~/.cache/huggingface/
hub` (or `$HF_HOME` if set); subsequent calls reuse the on-disk cache. Uses
the `main` revision; gated repos (Llama, Mistral) require `HF_TOKEN` in env
or `~/.cache/huggingface/token`.

Also adds `try_register_hf(prefix, repo)` as the obvious one-liner for
proxy startup code:

    let _ = try_register_hf("command-", "CohereForAI/c4ai-command-r-v01");
    let _ = try_register_hf("mistral-", "mistralai/Mistral-7B-v0.1");

Each call is independent — a download failure for one model (e.g. gated
without a token) does not affect others.

`HfTokenizerError` gains a new `Hub` variant so callers can distinguish
"couldn't fetch" from "fetched but malformed" — relevant when deciding
whether to retry, surface to the user, or fall back to the estimator.

Why blocking, not async: `from_pretrained` is called once at startup. A
sync API works from `main()`, from a `OnceLock` initializer, or from
`tokio::task::spawn_blocking` if a tokio caller needs it later. The async
hf-hub backend would force callers to await at startup, which doesn't fit
the `register_hf` registry pattern.

Why rustls, not native-tls: keeps the binary statically linkable for AWS
deploys (no system OpenSSL dependency).

Tests: a network-dependent integration test (`#[ignore]`d in CI; hits HF
for `gpt2`, ~1.4 MB) verifies the real download + load + count path. A
non-network negative test verifies that an invalid repo name surfaces as
`HfTokenizerError::Hub`, not a panic. 44 unit tests + 5 proptests +
1 doctest pass; parity stays 40/40 byte-equal.
2026-04-25 15:05:09 -07:00
chopratejas
9ce1c01b87 feat(rust): tokenizer crate with tiktoken-rs + HuggingFace + estimator
Stage 2 of the Rust port: a `headroom_core::tokenizer` module mirroring the
Python `headroom.tokenizers` surface, with three backends behind a single
`Tokenizer` trait.

Backends, in dispatch order:

1. HuggingFace (`HfTokenizer`) — pure-Rust `tokenizers` crate loading any
   public `tokenizer.json`. Covers the gap between OpenAI (tiktoken) and the
   Anthropic/Gemini estimator: Cohere `command-*`, Llama-3.x, Mistral, Qwen,
   BERT, T5, etc. Construct from bytes or a file path; register against a
   model-name prefix via `register_hf` for automatic dispatch. No `hf-hub`
   auto-download yet — keeps networking, auth, and `~/.cache/huggingface` out
   of core. Longest-prefix wins; lookups are RwLock-protected.
2. Tiktoken (`TiktokenCounter`) — `tiktoken-rs` 0.11 BPE for OpenAI / o-series
   families. Byte-identical to Python `tiktoken` for ordinary text. Lazy
   shared `Arc<CoreBPE>` per encoding (o200k_base, cl100k_base, p50k_base,
   r50k_base).
3. Estimation (`EstimatingCounter`) — `chars / cpt` last-resort fallback.
   Matches Python's `max(1, int(len(text) / cpt + 0.5))` round-half-up
   formula (a self-review caught and fixed an earlier `ceil`-based version
   that diverged in the middle of the range, e.g. 5 chars at 4.0 cpt).

Tests: 43 unit tests + 5 proptests; parity 40/40 byte-equal.
Bench: criterion baseline on small/medium/large inputs.
Workspace MSRV bumped 1.78 → 1.80 for `LazyLock`/`OnceLock`.

No proxy wiring. Library-only; production behavior unchanged.
2026-04-25 14:22:09 -07:00
chopratejas
c2749c0fb6 docs(rust): lockfile + RUST_DEV.md for proxy CLI
Cargo.lock: pick up tokio-util added in the WS half-close fix.
RUST_DEV.md: document how to run headroom-proxy in passthrough mode
(listen + upstream flags, e2e test gate, env vars).
2026-04-25 12:49:36 -07:00
chopratejas
128a910ebb feat(rust): axum reverse proxy skeleton + http catch-all (phase-1)
Builds out crates/headroom-proxy from a /healthz stub into a transparent
reverse proxy: catch-all router that forwards every method/path/query to
--upstream verbatim, streaming both request and response bodies through
reqwest without buffering. Adds clap-based config (CLI + env), thiserror
error type with sane upstream-status mapping, JSON tracing-subscriber
logging, and graceful shutdown. The library surface (build_app, AppState,
Config) is reused by the integration tests.
2026-04-24 15:47:07 -07:00
chopratejas
0414cb70e4 feat(rust): scaffold workspace + parity harness (phase-0)
Bootstrap the Rust port of Headroom. Additive only — no existing Python
code modified. Ships the workshop, not the widgets.

Layout
  Cargo.toml (workspace) + rust-toolchain.toml
  crates/headroom-core    — transform library, stub only
  crates/headroom-proxy   — axum binary, /healthz only
  crates/headroom-py      — PyO3 cdylib, exposes headroom._core.hello()
  crates/headroom-parity  — Rust-vs-Python oracle harness + parity-run CLI

Tooling
  Makefile: test, test-parity, bench, build-proxy, build-wheel, fmt, lint
  .github/workflows/rust.yml: test, wheels (linux/mac), audit, parity-nightly
  deny.toml for cargo-deny

Parity corpus
  tests/parity/recorder.py + scripts/record_fixtures.py
  125 recorded fixtures across 5 leaf transforms (ccr, tokenizer,
  log_compressor, diff_compressor, cache_aligner)

Docs
  RUST_DEV.md — developer setup and workspace reference
  docs/spec/022-rust-migration.md — migration plan and stage breakdown

.gitignore: whitelist scripts/record_fixtures.py; ignore target/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:39:48 -07:00