Repository navigation
security(integrations): govern outbound sends on the live channel seams (#14270) - #14334
Conversation
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Applied Black, isort, and autoflake to match code-quality checks. Triggered by workflow auto-fix-formatting.yml.
Red CI — not merging, and not yet root-caused
I could not root-cause it: the run is still Recording this rather than leaving it, because a red check with an unavailable log is exactly the shape that gets read as noise and waved through. Next step for whoever picks this up: pull the shard log once the run finishes and check it against #14111 before assuming it is a real regression. #14111 tracks unrelated tests failing inside Either way it gets root-caused before merge — a tracking issue is not a substitute. |
Root-caused — it was mine, not the shard flakinessThe run finished and the log is retrievable now. Both reds are one root failure ( This PR caused it. It added I had flagged #14111 as the likely explanation. It was not — checking rather than assuming was the difference. FixAdded the two missing stubs alongside the existing ones, with a comment explaining why every new backend top-level import has to be mirrored there. Verified against the reproduction, not the predicate. The failure never appears from the repo root, which is why it looked CI-only. Running with the slm rootdir reproduces it exactly: Pushed as |
Thinking Path
#14067 put an egress control on
Gateway.send_message— and that Gateway is dormant, its initcommented out at
initialization/lifespan.py(Issue #881: TEMP DISABLED). This issue is thehalf that carries real traffic: the modules that actually put bytes on the wire crossed no gate
and left no record.
Two scoping decisions were taken before writing code, because implementing #14270 literally
would have been wrong:
1. Operational alerts are audited but never blocked.
notification_servicecarriesWORKFLOW_FAILED,SERVICE_FAILEDand — decisively —APPROVAL_NEEDED. Gating that behindthe approval system deadlocks it: the notification telling a human an approval is needed would
itself await approval. Gating the other two silences outage alerts exactly when they matter.
These paths pass
require_approval=Falseexplicitly rather than relying on the env default, soarming the policy cannot reach them.
2. Audit-only posture for the three agent-authored seams, matching what already merged for
the Gateway and the governor's own default. Fail-closed with no approver registered anywhere
would deny every send, so arming waits on #14068.
Two gaps in the issue's own scope, found by reading the producers rather than trusting the
table:
integration_communication.send_messagehas two send branches — aMessagingProtocoladapter and a Teams webhook fallback via
execute_action. A guard inside the first branchwould leave Teams ungoverned while the governor's name appears in the function, so the bypass
reads as covered. The guard goes before the branch.
send_text_message,send_media_message,send_template_message,mark_message_read. Thefirst three deliver something a person reads and are governed.
mark_message_readis a readreceipt — control traffic, the same reasoning as the Gateway's
_AGENT_MESSAGE_TYPES— and isdeliberately not governed, with a test pinning that so it stays a decision rather than drift.
What Changed
services/telegram_bot_service.py— guard insend_messagebefore the HTTP POST; newrequire_approval: Optional[bool] = Noneparameter so callers can opt into the alert exemption.integrations/whatsapp_integration.py—_egress_denied()helper; applied in the threeperson-facing senders. Recipient is masked in the audit record (
_mask_phone) — a phonenumber is PII and the record outlives the send.
api/integration_communication.py— guard before the branch, covering both send paths;denial raises
HTTPException(403), the right idiom for an API endpoint.services/notification_service.py—_send_webhook(covers Slack) and the Telegram alert pathboth pass
require_approval=False. The webhook audit record names the host only, not thefull URL, which often carries a secret token.
services/gateway/live_egress_seams_test.py— new, 10 tests.changelog/unreleased/14270-live-egress-seams.md— new fragment.Unrelated fix in a file I was already editing:
notification_service.py's module docstringhad an import injected into the middle of its usage example by some earlier tooling. Harmless
(it is inside a string) but wrong; removed. Pre-existing on base, not from this change.
Verification
./venv/bin/python -m pytest autobot-backend/services/gateway/ -q # 52 passed (42 existing + 10 new)Every test drives the producer, not the governor — a test asserting the import exists would
pass while a new branch bypasses it. Each patches the governor to deny and asserts nothing
reaches the transport.
Mutation-tested:
That fourth row is the one worth reading. The first version of these tests did not catch it —
they proved
TelegramBotServicehonoursrequire_approval=Falsewhen passed, but nothing provednotification_servicepasses it. Dropping the kwarg left all 9 tests green. That is invisibleuntil someone arms the policy and outage alerts stop, so a wiring test was added and the mutation
now fails.
Risks
audit record per outbound message. Alerts additionally cannot block even when armed.
never a dropped message.
AUTOBOT_GATEWAY_REQUIRE_OUTBOUND_APPROVALwith no approver registered will deny thethree agent-authored channels. That is the documented fail-closed behaviour, and it is why the
posture ships audit-only until feat(approvals): an approval can only be answered at the screen — no remote delivery, and 'nobody is watching' can only be expressed by lowering the guard profile #14068 provides an approver.
mark_message_readremains ungoverned by design; if that judgement is wrong the test thatpins it is the place to change it.
Pre-existing failure, not from this change:
integrations/integration_unknown_action_contract_test.py::test_at_least_one_subclass_discoveredfails identically on
origin/Dev_new_gui— verified in a clean base worktree. It is the localdependency gap (23/67 requirements absent), since CI's python-suite is green.
Model Used
Claude Opus 5 (claude-opus-5[1m])
Issue Link
Closes #14270
Related: #14067 (the dormant seam this complements), #14028 (inbound counterpart), #14068
(the approver that makes arming usable), #13421.
Changelog fragment
changelog/unreleased/14270-live-egress-seams.mdChecklist
CLAUDE.mdgit commitruns them automatically)Dev_new_gui(notmain)