"""Defence in depth for guardrail #1 ("only scrubbed text goes to the LLM"). `ScrubbedText` is a `NewType`, i.e. a plain `str` at runtime, so the type checker is the primary guard. `assert_scrubbed` is a cheap runtime backstop right before text leaves the process: if an obvious identifier (phone, e-mail, SNILS) is still present, the call to the provider is refused. It is NOT a replacement for `app.core.privacy.scrub()` — names, addresses etc. are not detected here. """ import re from app.core.types import ScrubbedText _LEAK_PATTERNS: tuple[re.Pattern[str], ...] = ( # Russian phone numbers: +7 / 8 followed by 10 digits with optional separators. re.compile(r"(?:\+7|(? ScrubbedText: for pattern in _LEAK_PATTERNS: if pattern.search(text): # Never include the text itself in the message: it would end up in logs. raise UnscrubbedTextError("identifier pattern found in text bound for LLM") return ScrubbedText(text)