From 4640587a06713595d22af0a62eff111d8525f7de Mon Sep 17 00:00:00 2001 From: JD Davis Date: Sun, 12 Jul 2026 02:36:46 +0000 Subject: [PATCH] Extract wire debug redaction policy (#1972) ## Description Extracts the pure secret-redaction logic used by opt-in Codex wire-debug capture from `helpers.py` into `headroom.proxy.wire_debug_redaction_policy`. This keeps the debug capture path behavior intact while making the sensitive-key policy directly testable. Closes # ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [x] Code refactoring (no functional changes) ## Changes Made - Added `wire_debug_redaction_policy.py` for secret-key matching and recursive wire-debug redaction. - Kept existing helper entry points and private compatibility names delegating to the extracted policy. - Added direct tests for direct secret headers, nested suffix-matched secrets, and key normalization. - Carried forward the LiteLLM callback compatibility shim needed for current mypy on `main`. ## 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 python -m pytest tests\test_wire_debug_redaction_policy.py 3 passed in 0.16s python -m ruff check . All checks passed! python -m ruff format --check . 1095 files already formatted python -m mypy headroom --ignore-missing-imports Success: no issues found in 409 source files gitleaks protect --staged --no-banner --redact no leaks found ``` ## Real Behavior Proof - Environment: Windows, Python 3.13.13, branch `jd/architecture-slice-25`. - Exact command / steps: ran focused wire-debug redaction tests, ruff, ruff format check, mypy, and staged gitleaks scan. - Observed result: redaction policy is directly covered and local lint/type/security checks pass. - Not tested: full proxy wire-debug capture runtime; this slice preserves the existing helper entry points. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes Documentation and changelog updates are N/A for this internal architecture-only refactor. The push reported existing default-branch Dependabot alerts; no staged secret leaks were found for this PR. --- headroom/proxy/helpers.py | 45 +++------------- headroom/proxy/wire_debug_redaction_policy.py | 52 +++++++++++++++++++ tests/test_wire_debug_redaction_policy.py | 45 ++++++++++++++++ 3 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 headroom/proxy/wire_debug_redaction_policy.py create mode 100644 tests/test_wire_debug_redaction_policy.py diff --git a/headroom/proxy/helpers.py b/headroom/proxy/helpers.py index 8e48aac03..e03634c7d 100644 --- a/headroom/proxy/helpers.py +++ b/headroom/proxy/helpers.py @@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Any, Literal, cast from headroom import paths as _paths from headroom._subprocess import run -from headroom.proxy import request_limit_policy +from headroom.proxy import request_limit_policy, wire_debug_redaction_policy from headroom.proxy.body_forwarding import ( BodyMutationTracker as BodyMutationTracker, # noqa: F401 - compatibility export ) @@ -57,24 +57,8 @@ logger = logging.getLogger("headroom.proxy") _CODEX_WIRE_DEBUG_ENV = "HEADROOM_CODEX_WIRE_DEBUG" _CODEX_WIRE_DEBUG_DIR_ENV = "HEADROOM_CODEX_WIRE_DEBUG_DIR" -_CODEX_WIRE_REDACTED = "[REDACTED]" -_CODEX_WIRE_SECRET_KEYS = ( - "authorization", - "cookie", - "set-cookie", - "api-key", - "x-api-key", - "openai-api-key", - "anthropic-api-key", - "access_token", - "refresh_token", - "id_token", - "bearer", - "password", - "secret", - "token", - "credential", -) +_CODEX_WIRE_REDACTED = wire_debug_redaction_policy.WIRE_DEBUG_REDACTED +_CODEX_WIRE_SECRET_KEYS = wire_debug_redaction_policy.WIRE_DEBUG_SECRET_KEYS def codex_wire_debug_enabled() -> bool: @@ -96,33 +80,16 @@ def _codex_wire_debug_dir() -> Path: def _should_redact_key(key: str) -> bool: - normalized = key.lower().replace("-", "_") - if normalized in {marker.replace("-", "_") for marker in _CODEX_WIRE_SECRET_KEYS}: - return True - return ( - normalized.endswith("_api_key") - or normalized.endswith("_secret") - or normalized.endswith("_password") - or normalized.endswith("_access_token") - or normalized.endswith("_refresh_token") - ) + return wire_debug_redaction_policy.should_redact_key(key) def _redact_value(value: Any) -> Any: - if isinstance(value, dict): - return { - k: (_CODEX_WIRE_REDACTED if _should_redact_key(str(k)) else _redact_value(v)) - for k, v in value.items() - } - if isinstance(value, list): - return [_redact_value(item) for item in value] - return value + return wire_debug_redaction_policy.redact_for_wire_debug(value) def redact_for_wire_debug(value: Any) -> Any: """Redact obvious secrets while preserving request/response shape.""" - - return _redact_value(value) + return wire_debug_redaction_policy.redact_for_wire_debug(value) def _safe_event_name(event: str) -> str: diff --git a/headroom/proxy/wire_debug_redaction_policy.py b/headroom/proxy/wire_debug_redaction_policy.py new file mode 100644 index 000000000..5b3c40813 --- /dev/null +++ b/headroom/proxy/wire_debug_redaction_policy.py @@ -0,0 +1,52 @@ +"""Secret redaction policy for opt-in proxy wire debug capture.""" + +from __future__ import annotations + +from typing import Any + +WIRE_DEBUG_REDACTED = "[REDACTED]" +WIRE_DEBUG_SECRET_KEYS = ( + "authorization", + "cookie", + "set-cookie", + "api-key", + "x-api-key", + "openai-api-key", + "anthropic-api-key", + "access_token", + "refresh_token", + "id_token", + "bearer", + "password", + "secret", + "token", + "credential", +) + + +def should_redact_key(key: str) -> bool: + """Return whether a wire-debug field name should be redacted.""" + normalized = key.lower().replace("-", "_") + if normalized in {marker.replace("-", "_") for marker in WIRE_DEBUG_SECRET_KEYS}: + return True + return ( + normalized.endswith("_api_key") + or normalized.endswith("_secret") + or normalized.endswith("_password") + or normalized.endswith("_access_token") + or normalized.endswith("_refresh_token") + ) + + +def redact_for_wire_debug(value: Any) -> Any: + """Redact obvious secrets while preserving request/response shape.""" + if isinstance(value, dict): + return { + key: ( + WIRE_DEBUG_REDACTED if should_redact_key(str(key)) else redact_for_wire_debug(item) + ) + for key, item in value.items() + } + if isinstance(value, list): + return [redact_for_wire_debug(item) for item in value] + return value diff --git a/tests/test_wire_debug_redaction_policy.py b/tests/test_wire_debug_redaction_policy.py new file mode 100644 index 000000000..e271a2969 --- /dev/null +++ b/tests/test_wire_debug_redaction_policy.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from headroom.proxy.wire_debug_redaction_policy import ( + WIRE_DEBUG_REDACTED, + redact_for_wire_debug, + should_redact_key, +) + + +def test_wire_debug_redacts_direct_secret_keys() -> None: + redacted = redact_for_wire_debug( + { + "Authorization": "Bearer test-token", + "x-api-key": "sk-test", + "safe": "visible", + } + ) + + assert redacted == { + "Authorization": WIRE_DEBUG_REDACTED, + "x-api-key": WIRE_DEBUG_REDACTED, + "safe": "visible", + } + + +def test_wire_debug_redacts_nested_secret_suffixes() -> None: + redacted = redact_for_wire_debug( + { + "messages": [ + {"content": "visible", "service_access_token": "secret-token"}, + {"metadata": {"database_password": "secret-password", "trace_id": "abc"}}, + ] + } + ) + + assert redacted["messages"][0]["content"] == "visible" + assert redacted["messages"][0]["service_access_token"] == WIRE_DEBUG_REDACTED + assert redacted["messages"][1]["metadata"]["database_password"] == WIRE_DEBUG_REDACTED + assert redacted["messages"][1]["metadata"]["trace_id"] == "abc" + + +def test_wire_debug_key_matching_normalizes_dashes_and_case() -> None: + assert should_redact_key("Anthropic-API-Key") + assert should_redact_key("custom-refresh-token") + assert not should_redact_key("token_count")