fix(proxy/auth): match real Anthropic OAuth token prefix (sk-ant-oat)#1672
Merged
chopratejas merged 2 commits intoJul 2, 2026
Merged
Conversation
classify_auth_mode checked for OAuth tokens with token.startswith( "sk-ant-oat-"), but real Anthropic OAuth access tokens are sk-ant-oat01-... — a version number, no dash after "oat". So every real OAuth/subscription token failed that check and fell through to the broad sk- branch, getting classified PAYG. That's the exact failure the module is designed to prevent: a subscription-bound request tagged PAYG gets aggressive lossy compression, auto cache_control, and prompt_cache_key injection instead of the passthrough-prefer path. Match the dash-less sk-ant-oat prefix (still matches the legacy dashed shape, so ordering vs sk-ant-api / sk- is unchanged). The existing parity tests only passed because they used a synthetic sk-ant-oat-01- fixture that happened to match the buggy prefix; add a regression test for the real sk-ant-oat01- format. Note: crates/headroom-core/src/auth_mode.rs carries the same dashed prefix; happy to mirror this there in a follow-up (couldn't cargo-build locally to verify).
Contributor
PR governanceThis PR follows the template and is marked ready for human review. |
# Conflicts: # CHANGELOG.md
JerrettDavis
approved these changes
Jul 2, 2026
JerrettDavis
left a comment
Collaborator
There was a problem hiding this comment.
Looks good after the branch update. The Python classifier now catches the real sk-ant-oat01-... Anthropic OAuth token shape before the broad sk- PAYG branch, and I added the same prefix fix plus regression coverage to crates/headroom-core so the Python and Rust classifiers do not diverge.
I updated the branch from main, resolved the changelog-only conflict, and verified:
python -m pytest tests/test_auth_mode.py -qcargo test -p headroom-core --test auth_mode
GitHub checks re-queued after the push and should finish before merge.
Merged
chopratejas
pushed a commit
that referenced
this pull request
Jul 3, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.29.0</summary> ## [0.29.0](v0.28.0...v0.29.0) (2026-07-03) ### Features * **proxy:** add --lossless no-CCR mode with format-native compaction ([#1721](#1721)) ([c75ebde](c75ebde)) * **stats:** surface Codex WS compression counters in /stats summary ([#1680](#1680)) ([2fe19c3](2fe19c3)) * **transforms:** adaptive Otsu KEEP/DROP threshold (+ land relevance split on main) ([#1726](#1726)) ([eea667a](eea667a)) ### Bug Fixes * **bedrock:** fail fast when session-token auth lacks botocore ([#1553](#1553)) ([54cfa36](54cfa36)) * **bedrock:** route ARNs via converse, named AWS profiles, and au. re… ([#1456](#1456)) ([7d87aa2](7d87aa2)) * **ccr:** honor workspace dir for sqlite store ([#1564](#1564)) ([96e1dfe](96e1dfe)) * **claude:** surface Remote Control proxy incompatibility ([#1610](#1610)) ([4bf7f92](4bf7f92)) * **cli:** stop advertising unwired compression tuning env vars in banner ([#1634](#1634)) ([d5bf98d](d5bf98d)) * **codex:** avoid duplicate headroom provider config ([#1431](#1431)) ([ddd4adf](ddd4adf)) * **compression:** reject lossy unmarked tool output in unit router path ([#1479](#1479)) ([de24cd5](de24cd5)) * **cortex-code:** migrate to current Cortex REST API endpoints + add e2e benchmarks ([#1474](#1474)) ([f00ace6](f00ace6)) * **dashboard:** align token savings headline denominator ([#1653](#1653)) ([646e705](646e705)) * **dashboard:** derive per-project setup URL from live origin ([#1511](#1511)) ([e035aef](e035aef)) * **detection:** contain unidiff panic on orphaned +++ target line ([#1548](#1548)) ([e386c09](e386c09)) * **evals:** CJK-aware F1 tokenization + token estimation ([#1527](#1527)) ([99a8540](99a8540)) * **install:** close parent log fd in start_detached_agent ([#1576](#1576)) ([816cb85](816cb85)) * **install:** use Windows-safe PID liveness probe in runtime_status ([#1544](#1544)) ([#1560](#1560)) ([6b227b9](6b227b9)) * **learn:** aggregate verbosity baselines across projects instead of overwriting ([#1288](#1288)) ([27a5468](27a5468)) * **mcp:** show lifetime totals and label rolling session scope in headroom_stats ([#1428](#1428)) ([1c0e152](1c0e152)) * **memory:** cap local embedder CPU thread oversubscription ([#198](#198)) ([#1559](#1559)) ([b84afbf](b84afbf)) * **memory:** singleflight LocalBackend init to stop cold-start races ([#1691](#1691)) ([bec47a1](bec47a1)) * **openclaw:** detect uv-installed headroom binary in ~/.local/bin ([#1459](#1459)) ([adaeb88](adaeb88)) * **opencode:** preserve custom OpenAI gateway paths ([#1596](#1596)) ([c19347c](c19347c)) * **opencode:** route native providers + load transport plugin, fix Serena context ([#1573](#1573)) ([ad0034f](ad0034f)) * preserve anthropic passthrough tool order ([#1427](#1427)) ([a932247](a932247)) * **proxy/auth:** match real Anthropic OAuth token prefix (sk-ant-oat) ([#1672](#1672)) ([8cddf9b](8cddf9b)) * **proxy:** expose persistent savings metrics ([#1647](#1647)) ([5fe4e7b](5fe4e7b)) * **proxy:** fail open when kompress saturation would exhaust pre-upstream budget ([#1430](#1430)) ([15ac650](15ac650)) * **proxy:** handle streaming CCR retrieval ([#1451](#1451)) ([d337e3b](d337e3b)) * **proxy:** include system/tools/sampling in cache key ([#1473](#1473)) ([312129a](312129a)) * **proxy:** preserve Responses passthrough bytes ([#1598](#1598)) ([2a34a82](2a34a82)) * **proxy:** strip Codex lite header on the HTTP /responses path ([#1663](#1663)) ([9fbd47b](9fbd47b)) * **proxy:** wire --compression-max-workers / HEADROOM_COMPRESSION_MAX_WORKERS ([#1632](#1632)) ([814ffa3](814ffa3)) * **savings:** count cache-read tokens in input cost estimate ([#1429](#1429)) ([72ade37](72ade37)) * skip Magika backend on x86 CPUs without AVX2 ([#1162](#1162)) ([64783d8](64783d8)) * **transforms/content-router:** route grep/log output away from HTML extractor ([#1719](#1719)) ([0d18ef2](0d18ef2)) * **transforms:** bound native content detection with a Windows watchdog ([#575](#575)) ([#1563](#1563)) ([95abca3](95abca3)) * Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL ([#1393](#1393)) ([cff7247](cff7247)) * **wrap:** detach the shared proxy on Windows so it survives an ungraceful agent close ([#1464](#1464)) ([6cba441](6cba441)) * **wrap:** preserve custom Vertex base URL ([#1477](#1477)) ([75427bb](75427bb)) * **wrap:** remove rtk instructions from Codex AGENTS.md on unwrap ([#1604](#1604)) ([c9d717c](c9d717c)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
JerrettDavis
added a commit
to peterlodri-sec/headroom
that referenced
this pull request
Jul 14, 2026
…headroomlabs-ai#1672) ## Description `classify_auth_mode` (in `headroom/proxy/auth_mode.py`) checks for Anthropic OAuth tokens with: ```python if token.startswith("sk-ant-oat-"): return AuthMode.OAUTH if token.startswith("sk-ant-api") or token.startswith("sk-"): return AuthMode.PAYG ``` But real Anthropic OAuth access tokens are **`sk-ant-oat01-...`** — a version number right after `oat`, **no dash**. So the `sk-ant-oat-` check never matches a real token; it falls through to the broad `sk-` rule and gets classified **`PAYG`**. That's exactly the misclassification the module is built to prevent: a subscription/OAuth-bound request tagged `PAYG` gets the aggressive-compression policy — lossy compression, auto `cache_control`, `prompt_cache_key` injection — instead of the passthrough-prefer path OAuth is meant to get. The existing tests didn't catch it because they use a synthetic `sk-ant-oat-01-` fixture (dashed) that happens to match the buggy prefix. Corroboration that the real shape is dash-less: - `.gitguardian.yaml` fixture: `sk-ant-oat01-oauth-fixture` - `tests/test_oauth_bearer_routing.py`: `sk-ant-oat01-xxx` - the sibling helper `headroom/proxy/helpers.py` matches on `sk-ant-` (no `oat-`) ## Fix Match the dash-less `sk-ant-oat` prefix. It still matches the legacy dashed shape, and ordering relative to `sk-ant-api` / `sk-` is unchanged (OAuth is still checked first). ```python if token.startswith("sk-ant-oat"): return AuthMode.OAUTH ``` ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/proxy/auth_mode.py`: match OAuth tokens on the dash-less `sk-ant-oat` prefix. - `tests/test_auth_mode.py`: add a regression test using the real `sk-ant-oat01-...` format (the existing test keeps the legacy dashed fixture, which still classifies correctly). - `CHANGELOG.md`: Bug Fixes entry under Unreleased. ## Testing - [x] New regression test added (`tests/test_auth_mode.py`) - [x] Linting passes (`ruff check`) and formatting is clean (`ruff format --check`) - [ ] Full `pytest` deferred to CI (local-OOM reason below). ```text $ uv run ruff check headroom/proxy/auth_mode.py tests/test_auth_mode.py All checks passed! ``` ## Real Behavior Proof - Environment: Windows 11, Python 3.12.11, headroom from this branch. Importing `headroom` loads the torch/transformers stack and a full `pytest` gets OOM-killed on this box, so I verified the classification logic with a dependency-free script and left the full pytest to CI. - Exact command / steps: replicated the Bearer-token branch of `classify_auth_mode` in a standalone script (only stdlib, no `headroom` import) and ran the real and legacy token shapes plus PAYG keys through it. - Observed result: the real `sk-ant-oat01-...` now classifies OAUTH (was PAYG before the change); the legacy dashed fixture still classifies OAUTH; `sk-ant-api*` / `sk-*` keys still classify PAYG: ```text OK: sk-ant-oat01-... -> OAUTH (was PAYG before fix) OK: sk-ant-oat-01-... -> OAUTH (legacy fixture still matches) OK: sk-ant-api* / sk-* -> PAYG (unchanged) AUTH LOGIC VERIFIED ``` - Not tested: a live proxied Anthropic OAuth request end-to-end (needs a real subscription token); the classification is pure and covered by the regression test. Full local `pytest` deferred to CI (OOM, per above). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes — ran lint + a standalone logic check; full pytest deferred to CI (local OOM, disclosed above) - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - `crates/headroom-core/src/auth_mode.rs` carries the identical dashed prefix (its Rust test matrix uses the same synthetic dashed fixture). I scoped this PR to the Python runtime classifier since that's the request-time path; happy to mirror the one-line fix in Rust in the same PR or a follow-up — I just couldn't `cargo build` locally to verify, so I left it out rather than push an unverified Rust edit. - @JerrettDavis tagging you since you've been triaging these — small, contained fix with a regression test if you have a moment. Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
JerrettDavis
pushed a commit
to peterlodri-sec/headroom
that referenced
this pull request
Jul 14, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.29.0</summary> ## [0.29.0](headroomlabs-ai/headroom@v0.28.0...v0.29.0) (2026-07-03) ### Features * **proxy:** add --lossless no-CCR mode with format-native compaction ([headroomlabs-ai#1721](headroomlabs-ai#1721)) ([c75ebde](headroomlabs-ai@c75ebde)) * **stats:** surface Codex WS compression counters in /stats summary ([headroomlabs-ai#1680](headroomlabs-ai#1680)) ([2fe19c3](headroomlabs-ai@2fe19c3)) * **transforms:** adaptive Otsu KEEP/DROP threshold (+ land relevance split on main) ([headroomlabs-ai#1726](headroomlabs-ai#1726)) ([eea667a](headroomlabs-ai@eea667a)) ### Bug Fixes * **bedrock:** fail fast when session-token auth lacks botocore ([headroomlabs-ai#1553](headroomlabs-ai#1553)) ([54cfa36](headroomlabs-ai@54cfa36)) * **bedrock:** route ARNs via converse, named AWS profiles, and au. re… ([headroomlabs-ai#1456](headroomlabs-ai#1456)) ([7d87aa2](headroomlabs-ai@7d87aa2)) * **ccr:** honor workspace dir for sqlite store ([headroomlabs-ai#1564](headroomlabs-ai#1564)) ([96e1dfe](headroomlabs-ai@96e1dfe)) * **claude:** surface Remote Control proxy incompatibility ([headroomlabs-ai#1610](headroomlabs-ai#1610)) ([4bf7f92](headroomlabs-ai@4bf7f92)) * **cli:** stop advertising unwired compression tuning env vars in banner ([headroomlabs-ai#1634](headroomlabs-ai#1634)) ([d5bf98d](headroomlabs-ai@d5bf98d)) * **codex:** avoid duplicate headroom provider config ([headroomlabs-ai#1431](headroomlabs-ai#1431)) ([ddd4adf](headroomlabs-ai@ddd4adf)) * **compression:** reject lossy unmarked tool output in unit router path ([headroomlabs-ai#1479](headroomlabs-ai#1479)) ([de24cd5](headroomlabs-ai@de24cd5)) * **cortex-code:** migrate to current Cortex REST API endpoints + add e2e benchmarks ([headroomlabs-ai#1474](headroomlabs-ai#1474)) ([f00ace6](headroomlabs-ai@f00ace6)) * **dashboard:** align token savings headline denominator ([headroomlabs-ai#1653](headroomlabs-ai#1653)) ([646e705](headroomlabs-ai@646e705)) * **dashboard:** derive per-project setup URL from live origin ([headroomlabs-ai#1511](headroomlabs-ai#1511)) ([e035aef](headroomlabs-ai@e035aef)) * **detection:** contain unidiff panic on orphaned +++ target line ([headroomlabs-ai#1548](headroomlabs-ai#1548)) ([e386c09](headroomlabs-ai@e386c09)) * **evals:** CJK-aware F1 tokenization + token estimation ([headroomlabs-ai#1527](headroomlabs-ai#1527)) ([99a8540](headroomlabs-ai@99a8540)) * **install:** close parent log fd in start_detached_agent ([headroomlabs-ai#1576](headroomlabs-ai#1576)) ([816cb85](headroomlabs-ai@816cb85)) * **install:** use Windows-safe PID liveness probe in runtime_status ([headroomlabs-ai#1544](headroomlabs-ai#1544)) ([headroomlabs-ai#1560](headroomlabs-ai#1560)) ([6b227b9](headroomlabs-ai@6b227b9)) * **learn:** aggregate verbosity baselines across projects instead of overwriting ([headroomlabs-ai#1288](headroomlabs-ai#1288)) ([27a5468](headroomlabs-ai@27a5468)) * **mcp:** show lifetime totals and label rolling session scope in headroom_stats ([headroomlabs-ai#1428](headroomlabs-ai#1428)) ([1c0e152](headroomlabs-ai@1c0e152)) * **memory:** cap local embedder CPU thread oversubscription ([headroomlabs-ai#198](headroomlabs-ai#198)) ([headroomlabs-ai#1559](headroomlabs-ai#1559)) ([b84afbf](headroomlabs-ai@b84afbf)) * **memory:** singleflight LocalBackend init to stop cold-start races ([headroomlabs-ai#1691](headroomlabs-ai#1691)) ([bec47a1](headroomlabs-ai@bec47a1)) * **openclaw:** detect uv-installed headroom binary in ~/.local/bin ([headroomlabs-ai#1459](headroomlabs-ai#1459)) ([adaeb88](headroomlabs-ai@adaeb88)) * **opencode:** preserve custom OpenAI gateway paths ([headroomlabs-ai#1596](headroomlabs-ai#1596)) ([c19347c](headroomlabs-ai@c19347c)) * **opencode:** route native providers + load transport plugin, fix Serena context ([headroomlabs-ai#1573](headroomlabs-ai#1573)) ([ad0034f](headroomlabs-ai@ad0034f)) * preserve anthropic passthrough tool order ([headroomlabs-ai#1427](headroomlabs-ai#1427)) ([a932247](headroomlabs-ai@a932247)) * **proxy/auth:** match real Anthropic OAuth token prefix (sk-ant-oat) ([headroomlabs-ai#1672](headroomlabs-ai#1672)) ([8cddf9b](headroomlabs-ai@8cddf9b)) * **proxy:** expose persistent savings metrics ([headroomlabs-ai#1647](headroomlabs-ai#1647)) ([5fe4e7b](headroomlabs-ai@5fe4e7b)) * **proxy:** fail open when kompress saturation would exhaust pre-upstream budget ([headroomlabs-ai#1430](headroomlabs-ai#1430)) ([15ac650](headroomlabs-ai@15ac650)) * **proxy:** handle streaming CCR retrieval ([headroomlabs-ai#1451](headroomlabs-ai#1451)) ([d337e3b](headroomlabs-ai@d337e3b)) * **proxy:** include system/tools/sampling in cache key ([headroomlabs-ai#1473](headroomlabs-ai#1473)) ([312129a](headroomlabs-ai@312129a)) * **proxy:** preserve Responses passthrough bytes ([headroomlabs-ai#1598](headroomlabs-ai#1598)) ([2a34a82](headroomlabs-ai@2a34a82)) * **proxy:** strip Codex lite header on the HTTP /responses path ([headroomlabs-ai#1663](headroomlabs-ai#1663)) ([9fbd47b](headroomlabs-ai@9fbd47b)) * **proxy:** wire --compression-max-workers / HEADROOM_COMPRESSION_MAX_WORKERS ([headroomlabs-ai#1632](headroomlabs-ai#1632)) ([814ffa3](headroomlabs-ai@814ffa3)) * **savings:** count cache-read tokens in input cost estimate ([headroomlabs-ai#1429](headroomlabs-ai#1429)) ([72ade37](headroomlabs-ai@72ade37)) * skip Magika backend on x86 CPUs without AVX2 ([headroomlabs-ai#1162](headroomlabs-ai#1162)) ([64783d8](headroomlabs-ai@64783d8)) * **transforms/content-router:** route grep/log output away from HTML extractor ([headroomlabs-ai#1719](headroomlabs-ai#1719)) ([0d18ef2](headroomlabs-ai@0d18ef2)) * **transforms:** bound native content detection with a Windows watchdog ([headroomlabs-ai#575](headroomlabs-ai#575)) ([headroomlabs-ai#1563](headroomlabs-ai#1563)) ([95abca3](headroomlabs-ai@95abca3)) * Vertex AI support for Claude Code with ANTHROPIC_VERTEX_BASE_URL ([headroomlabs-ai#1393](headroomlabs-ai#1393)) ([cff7247](headroomlabs-ai@cff7247)) * **wrap:** detach the shared proxy on Windows so it survives an ungraceful agent close ([headroomlabs-ai#1464](headroomlabs-ai#1464)) ([6cba441](headroomlabs-ai@6cba441)) * **wrap:** preserve custom Vertex base URL ([headroomlabs-ai#1477](headroomlabs-ai#1477)) ([75427bb](headroomlabs-ai@75427bb)) * **wrap:** remove rtk instructions from Codex AGENTS.md on unwrap ([headroomlabs-ai#1604](headroomlabs-ai#1604)) ([c9d717c](headroomlabs-ai@c9d717c)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
classify_auth_mode(inheadroom/proxy/auth_mode.py) checks for AnthropicOAuth tokens with:
But real Anthropic OAuth access tokens are
sk-ant-oat01-...— a versionnumber right after
oat, no dash. So thesk-ant-oat-check never matches areal token; it falls through to the broad
sk-rule and gets classifiedPAYG.That's exactly the misclassification the module is built to prevent: a
subscription/OAuth-bound request tagged
PAYGgets the aggressive-compressionpolicy — lossy compression, auto
cache_control,prompt_cache_keyinjection —instead of the passthrough-prefer path OAuth is meant to get.
The existing tests didn't catch it because they use a synthetic
sk-ant-oat-01-fixture (dashed) that happens to match the buggy prefix. Corroboration that the
real shape is dash-less:
.gitguardian.yamlfixture:sk-ant-oat01-oauth-fixturetests/test_oauth_bearer_routing.py:sk-ant-oat01-xxxheadroom/proxy/helpers.pymatches onsk-ant-(nooat-)Fix
Match the dash-less
sk-ant-oatprefix. It still matches the legacy dashedshape, and ordering relative to
sk-ant-api/sk-is unchanged (OAuth isstill checked first).
Type of Change
Changes Made
headroom/proxy/auth_mode.py: match OAuth tokens on the dash-lesssk-ant-oatprefix.tests/test_auth_mode.py: add a regression test using the realsk-ant-oat01-...format (the existing test keeps the legacy dashed fixture, which still classifies correctly).CHANGELOG.md: Bug Fixes entry under Unreleased.Testing
tests/test_auth_mode.py)ruff check) and formatting is clean (ruff format --check)pytestdeferred to CI (local-OOM reason below).Real Behavior Proof
headroomloads the torch/transformers stack and a fullpytestgets OOM-killed on this box, so I verified the classification logic with a dependency-free script and left the full pytest to CI.classify_auth_modein a standalone script (only stdlib, noheadroomimport) and ran the real and legacy token shapes plus PAYG keys through it.sk-ant-oat01-...now classifies OAUTH (was PAYG before the change); the legacy dashed fixture still classifies OAUTH;sk-ant-api*/sk-*keys still classify PAYG:pytestdeferred to CI (OOM, per above).Review Readiness
Checklist
Additional Notes
crates/headroom-core/src/auth_mode.rscarries the identical dashed prefix (its Rust test matrix uses the same synthetic dashed fixture). I scoped this PR to the Python runtime classifier since that's the request-time path; happy to mirror the one-line fix in Rust in the same PR or a follow-up — I just couldn'tcargo buildlocally to verify, so I left it out rather than push an unverified Rust edit.