You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Telegram chat_id is unmasked in egress audit records while WhatsApp numbers are masked #14540
The WhatsApp seam masks the recipient before it reaches the audit record — _mask_phone, applied to the number used as channel_id.
The Telegram seam passes chat_id through unmasked, into both the audit record and the warning log line on a denial.
A Telegram chat_id is durable and person-identifying: it is stable for the life of the account and resolves directly to one user for anyone with bot access. Whether it is "PII" in the same sense as a phone number is a fair question — but the two seams answer that question differently, in the same file family, added in the same change, with nothing recording why.
Why this needs a decision rather than a patch
Masking it is not obviously right. An operator reading an audit trail needs enough to identify which conversation was blocked, and a masked chat_id may make the record useless for its purpose. The WhatsApp choice happens to work because a phone number is recognisable even partially masked.
So the question is what the audit record is for, and the answer should be written down once and applied to every platform, rather than decided per-seam by whoever writes it.
Note the precedent already in the code: the webhook seam deliberately records urlparse(url).hostname rather than the full URL, specifically so a token embedded in the path does not reach the record — and that choice is pinned by a test. That is the pattern to follow: decide, then assert.
Acceptance criteria
A stated rule for what identifies a channel in an egress audit record, applied to every platform
Telegram and WhatsApp agree with that rule
The choice is pinned by a test, as the webhook host-only rule already is
Denial log lines are covered too, not only the audit record
Provenance
Found by code review of #14334 (#14270), which noted the distinction may well be deliberate but is neither commented nor tested — unlike the mark_message_read exclusion and the webhook host-only rule, both of which are pinned.
👋 Auto-triage could not confidently classify this issue (no confident signal). Could a maintainer add the appropriate labels (frontend, backend, infrastructure, docs, testing and good-first-issue, intermediate, advanced)?
Delivered by PR #14569, squash-merged as 734c3a253.
The stated rule, now in egress_governor.py's module docstring: a channel identifier directly usable outside this system on its own (a dialable number, a URL with an embedded token) is reduced before reaching evaluate(); an opaque identifier scoped to this system's own platform credential is recorded as-is. WhatsApp masked, webhook hostname-only, Telegram and Slack/Discord unmasked — all pinned by tests, log lines included.
The docstring carries a caveat rather than overstating its case: for a 1:1 private chat, Telegram's chat_idis the user's global account ID, resolvable by any bot or client with contact access — materially weaker than a workspace-scoped Slack/Discord channel_id. The decision stands; the reasoning no longer claims they are equivalent.
Asking for the Slack/Discord pin as a completeness matter found a live bug — verified against base:
the send path used _extract_channel_and_text, which reads message.channel for Slack and message.channel_id for Discord
the egress audit path read only message.channel_id
So every Slack egress record has been written with an empty channel_id — not leaky, the opposite: an audit row that looks well-formed and identifies nothing. Fixed at integration_communication.py:202. Worth recording that completeness-checking, not severity-checking, is what surfaced it.
Follow-up #14573: the email and Teams paths pass channel_id="" unconditionally — empty by construction rather than by bug, the inverse problem, needing a decision rather than a patch.
Activity
github-actions commentedon Aug 18, 2026
👋 Auto-triage could not confidently classify this issue (no confident signal). Could a maintainer add the appropriate labels (
frontend,backend,infrastructure,docs,testingandgood-first-issue,intermediate,advanced)?fix(egress): pin Slack/Discord channel identity, fix Slack's blank ch…
fix(egress): state and apply the channel-identity rule to the webhook…
test(egress): pin the channel-identity rule and the approver-exceptio…
fix(egress): pin Slack/Discord channel identity, fix Slack's blank ch…
fix(egress): split audit vs caller-facing denial text and state the c…
mrveiss commentedon Aug 18, 2026
Delivered by PR #14569, squash-merged as
734c3a253.The stated rule, now in
egress_governor.py's module docstring: a channel identifier directly usable outside this system on its own (a dialable number, a URL with an embedded token) is reduced before reachingevaluate(); an opaque identifier scoped to this system's own platform credential is recorded as-is. WhatsApp masked, webhook hostname-only, Telegram and Slack/Discord unmasked — all pinned by tests, log lines included.The docstring carries a caveat rather than overstating its case: for a 1:1 private chat, Telegram's
chat_idis the user's global account ID, resolvable by any bot or client with contact access — materially weaker than a workspace-scoped Slack/Discordchannel_id. The decision stands; the reasoning no longer claims they are equivalent.Asking for the Slack/Discord pin as a completeness matter found a live bug — verified against base:
_extract_channel_and_text, which readsmessage.channelfor Slack andmessage.channel_idfor Discordmessage.channel_idSo every Slack egress record has been written with an empty
channel_id— not leaky, the opposite: an audit row that looks well-formed and identifies nothing. Fixed atintegration_communication.py:202. Worth recording that completeness-checking, not severity-checking, is what surfaced it.Follow-up #14573: the email and Teams paths pass
channel_id=""unconditionally — empty by construction rather than by bug, the inverse problem, needing a decision rather than a patch.