Skip to content

fix(llm): normalize caller IDs before the TRUSTED_CALLERS check - #29

Merged
volschin merged 1 commit into
mainfrom
fix/trusted-callers-normalization
Aug 1, 2026
Merged

fix(llm): normalize caller IDs before the TRUSTED_CALLERS check#29
volschin merged 1 commit into
mainfrom
fix/trusted-callers-normalization

Conversation

@volschin

@volschin volschin commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

The FRITZ!Box delivers external callers in national format (015100000001) while operators naturally write E.164 (+4915100000001) in TRUSTED_CALLERS. LlmClient._is_authorized did an exact string match, so an E.164 entry failed closed silently — the caller could converse but got no RAG/calendar access, with nothing in the logs indicating why. The allowlist is empty today, so this fixes the trap before it is first populated.

Changes

  • agent/answer_policy.py — new normalize_caller_id: strips separators (space, -, /, (, ), .), maps 00…+… and a leading 0+49….
  • agent/llm.py — both sides of the comparison run through it: the constructor normalizes allowlist entries, _is_authorized normalizes the incoming caller ID and rejects an empty result.

Why this does not widen the allowlist

This is an authorization boundary, so only unambiguous dialling-plan transforms apply. Anything that is not a plain number after separator removal is returned stripped but otherwise untouched, staying exact-match:

  • internal FRITZ!Box extensions (**613) — no E.164 form
  • bare digits with no trunk or country prefix (15100000001) — no country context, never guessed
  • a withheld CLI (anonymous), and empty input — never authorized

_COUNTRY_CODE is hardcoded +49, matching the German trunk; a non-German trunk needs it changed, and the code says so. Caller-ID authorization still trusts the SIP CLI, which is spoofable at the telephony layer — unchanged by this PR, and now called out in .env.example.

Testing

TDD — every test watched failing before the implementation existed:

  • 13 parametrized normalizer cases (national / E.164 / 00 / four separator spellings / extension / bare digits / anonymous / empty / whitespace), plus idempotence and distinctness
  • E.164 allowlist entry matches a national caller ID (and a spaced one)
  • national allowlist entry matches an E.164 caller ID
  • internal extension matches exactly
  • test_normalization_does_not_widen_the_allowlist: different subscriber, bare digits, anonymous, empty, None all rejected
  • empty allowlist authorizes nobody

venv/bin/pytest -q → 337 passed. ruff format --check / ruff check clean. from agent.main import main imports.

Docs

.env.example documents that either format works and that extensions must match exactly; CLAUDE.md tool-authorization decision updated; the TODO.md live-findings item is closed.

Also replaces the operator's real mobile number with a placeholder in TODO.md (it remains in earlier commit history).

The FRITZ!Box delivers external callers in national format (015100000001)
while operators naturally write E.164 in TRUSTED_CALLERS. _is_authorized did
an exact string match, so an E.164 entry failed closed silently: the caller
could converse but got no RAG/calendar access, with nothing indicating why.

normalize_caller_id (agent/answer_policy.py) strips separators and maps 00 ->
+ and a leading 0 -> +49; LlmClient runs both the allowlist and the incoming
caller ID through it. Only unambiguous dialling-plan transforms apply -
internal extensions (**613), bare digits without a trunk prefix, and a
withheld CLI stay exact-match, so the allowlist is never widened. The country
code is hardcoded +49, matching the German trunk.

Also replaces the operator's real mobile number in TODO.md with a
placeholder.
@volschin
volschin merged commit bf98daa into main Aug 1, 2026
5 checks passed
@volschin
volschin deleted the fix/trusted-callers-normalization branch August 1, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant