Merge remote-tracking branch 'upstream/main' into pr2145

This commit is contained in:
Tejas Chopra 2026-07-13 15:01:45 -07:00
commit 05e9ee4b9b
3 changed files with 112 additions and 12 deletions

View file

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Fixed
- **memory:** annotate `_EMBEDDER_CACHE` as `dict[tuple[str, str, str], Embedder]` to match the 3-element key (backend, model, ollama_base_url). The stale 2-tuple annotation made `mypy headroom` fail on `main`, which broke the `lint` CI job on every open PR.
- **install:** include `orjson` in the `[proxy]` extra so `uv tool install "headroom-ai[all]"` satisfies LiteLLM OpenRouter/provider backends that import it at runtime ([#2056](https://github.com/headroomlabs-ai/headroom/issues/2056)).
- The dashboard's per-request metadata (the `recent_requests` / `request_logs`
tail and the `config` block with upstream URLs) is gated to loopback callers

View file

@ -300,8 +300,15 @@ class RegexDetector:
DynamicCategory.REQUEST_ID,
"api_key",
),
# Common prefixed IDs (req_, sess_, txn_, etc.)
(r"\b[a-z]{2,6}_[a-zA-Z0-9]{8,}", DynamicCategory.REQUEST_ID, "prefixed_id"),
# Common prefixed IDs (req_, sess_, txn_, etc.). The suffix must
# contain at least one digit (lookahead) so genuine generated ids
# like "req_a1b2c3d4" match while plain snake_case compound words
# like "in_progress" or "is_valid" (all letters, no digit) do not.
(
r"\b[a-z]{2,6}_(?=[a-zA-Z0-9]*\d)[a-zA-Z0-9]{8,}",
DynamicCategory.REQUEST_ID,
"prefixed_id",
),
# Hex strings of common ID lengths (32 = MD5, 40 = SHA1, 64 = SHA256)
(r"\b[a-fA-F0-9]{32}\b", DynamicCategory.IDENTIFIER, "hex_32"),
(r"\b[a-fA-F0-9]{40}\b", DynamicCategory.IDENTIFIER, "hex_40"),
@ -325,10 +332,20 @@ class RegexDetector:
]
# Build structural pattern from dynamic labels
# Pattern: "label" followed by separator then value
# Pattern: "label" followed by an explicit key/value separator then value.
#
# Two constraints keep this from firing on ordinary prose and code:
# * A word boundary (\b) and a trailing negative-lookahead on word
# characters anchor the label as a whole word. Without them a label
# like "token" or "last" matched as a substring inside unrelated
# identifiers such as "getAuthToken" or "blast".
# * The separator must be an explicit ":" or "=" (optionally spaced).
# A bare-whitespace separator turned any English sentence beginning
# with a label word ("current work is ...", "name of the file ...")
# into a bogus label/value pair that swallowed the rest of the clause.
labels_pattern = "|".join(re.escape(label) for label in config.dynamic_labels)
self._structural_pattern = re.compile(
rf"(?P<label>(?:{labels_pattern}))(?P<sep>\s*[:=]\s*|\s+)(?P<value>[^\n,;]+)",
rf"\b(?P<label>(?:{labels_pattern}))(?!\w)(?P<sep>\s*[:=]\s*)(?P<value>[^\n,;]+)",
re.IGNORECASE,
)
@ -459,12 +476,18 @@ class RegexDetector:
if len(text) < self.config.min_entropy_length:
continue
# Skip if all letters or all numbers (not random-looking)
if text.isalpha() or text.isdigit():
# Require genuinely id-shaped structure rather than a random-looking
# spelling. Generated identifiers (session ids, request ids, hashes,
# tokens) essentially always mix in at least one digit, whereas
# ordinary words and compound identifiers are letters (plus "-"/"_"
# separators) only. Skipping the letters-only case avoids flagging
# prose words as well as snake_case / kebab-case vocabulary like
# "in_progress", "system-reminder" or "total_tokens" that recurs
# identically every turn and must stay in the cacheable prefix.
if text.isdigit():
continue
# Skip common words that might look like IDs
if text.lower() in {"username", "password", "localhost", "undefined"}:
letters_only = text.replace("-", "").replace("_", "")
if not letters_only or letters_only.isalpha():
continue
# Calculate entropy

View file

@ -85,10 +85,16 @@ class TestRegexDetector:
assert spans[0].category == DynamicCategory.VERSION
def test_date_prefix_pattern(self, detector):
"""Test full date prefix phrase detection."""
spans = detector.detect("Today is Monday, January 15, 2024. You are an assistant.")
"""Test labeled date phrase detection.
Structural detection requires an explicit ``:``/``=`` separator (a
bare-whitespace separator used to swallow ordinary prose such as
"Today is Monday..." see issue #2110). With the label properly
delimited, the locale-formatted date value is still extracted.
"""
spans = detector.detect("Today: Monday, January 15, 2024. You are an assistant.")
assert len(spans) >= 1
# Should detect the full phrase
# Should detect the labeled value
date_spans = [s for s in spans if s.category == DynamicCategory.DATE]
assert len(date_spans) >= 1
@ -287,6 +293,76 @@ class TestEntropyDetection:
assert "password" not in flagged_words
class TestIssue2110FalsePositives:
"""Regression tests for issue #2110.
The detector misclassified ordinary English words and code identifiers
(e.g. ``in_progress``, ``is_valid``, ``getAuthToken``) as dynamic content,
extracting them from the system prompt and re-appending them as a growing
``[Dynamic Context]`` tail that corrupted the cached prefix. Genuinely
dynamic *shapes* (UUIDs, timestamps, hashes, prefixed ids with a digit)
must still be detected.
"""
@pytest.fixture
def detector(self):
return DynamicContentDetector(DetectorConfig(tiers=["regex"]))
# --- must NOT be flagged (the reported false positives) ------------------
@pytest.mark.parametrize(
"text",
[
"in_progress", # snake_case status word (prefixed_id false positive)
"is_valid", # snake_case identifier (entropy false positive)
"in_pr", # ordinary short token
"total_tokens", # snake_case compound word
"system-reminder", # kebab-case tag name
"getAuthToken (function - src/services/firebase.ts:92)", # code identifier + path
"DebugModal (function - src/components/layout/DebugModal.tsx:11)",
"The current work is being done", # prose starting with a label word
"last updated the file yesterday", # prose starting with a label word
"the user should review this", # prose containing a label word
"the name of the file is unknown", # prose containing a label word
],
)
def test_ordinary_words_and_identifiers_not_extracted(self, detector, text):
result = detector.detect(text)
assert result.spans == [], f"unexpected dynamic spans for {text!r}: {result.spans}"
# Nothing extracted -> the static content is preserved verbatim and the
# dynamic tail stays empty (so it can't grow over a session).
assert result.dynamic_content == ""
# --- MUST still be flagged (genuinely dynamic shapes) --------------------
def test_uuid_still_detected(self, detector):
text = "550e8400-e29b-41d4-a716-446655440000"
spans = detector.detect(text).spans
assert any(s.category == DynamicCategory.UUID and s.text == text for s in spans)
def test_timestamp_still_detected(self, detector):
spans = detector.detect("event at 2026-07-12T10:30:00Z happened").spans
assert any(s.text == "2026-07-12T10:30:00Z" for s in spans)
def test_long_hex_hash_still_detected(self, detector):
sha1 = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
spans = detector.detect(sha1).spans
assert any(s.category == DynamicCategory.IDENTIFIER and s.text == sha1 for s in spans)
def test_prefixed_id_with_digit_still_detected(self, detector):
spans = detector.detect("req_a1b2c3d4").spans
assert any(s.category == DynamicCategory.REQUEST_ID for s in spans)
def test_labeled_dynamic_value_still_detected(self, detector):
# Explicit "label: value" — the label stays static, the value is dynamic.
spans = detector.detect("session_id: 8f3e2a1c9d").spans
assert any(s.text == "8f3e2a1c9d" for s in spans)
def test_high_entropy_id_with_digits_still_detected(self, detector):
spans = detector.detect("a1b2c3d4e5f6g7h8").spans
assert any(s.category == DynamicCategory.IDENTIFIER for s in spans)
class TestEdgeCases:
"""Test edge cases and tricky inputs."""