Refused-sender backoff + burst anomaly + alert transport
RFD-AGT-013 T2f-b (D23, CC-41/CC-47; CC-34 retired by Bug-3142) — the Q1 compensating stack
that justifies skipping step-up auth (founder posture:
contain + detect + accept). Contain: repeat refused
senders earn geometrically escalating silence on the Story-2856 attempt
ledger. Detect: list+fetch bursts on bound identities trip one deduped,
metadata-only alert per bucket. Accept: no step-up, no new blocking —
enforcement caps stayed in T2f-a (caps.py, Story-2858).
Channel alerts reuse the audit ledger and the scheduled email worker;
no parallel refusal or alert ledger was created.
The three layers
| Backoff escalation (CC-41) | Burst anomaly (CC-47) | Alert email transport | |
|---|---|---|---|
| Where | src/domains/channel/refusal.py — inside the existing zero-amp shield, no fork. |
src/domains/channel/anomaly.py, hooked after the fetch-token mint (fetch_link.py) and each logical disambiguation listing (disambiguation.py); transport chunks never inflate the count. |
src/domains/channel/alerts.py + src/workers/channel_alert_digest/. |
| Mechanism | Refusal window × multiplier^min(rate_limited rows in horizon, max_exp). Defaults 1h × 2^k, cap 64×, horizon 24h — quiet for the horizon decays back to base. |
Counts records the seams already write: channel_delivery_records (kind disambiguation — every menu leaks portfolio shape, A-6) + channel_fetch_tokens, per binding per 15-min window; thresholds 5 lists / 8 fetches. |
The scheduled worker sends every pending alert email on its next run. Counsel replies dispatch immediately on their existing WhatsApp paths. |
| Alert row | channel_refusal.alerted (2856): known revoked/expired identities retain their historical firm; a truly unregistered sender remains NULL-firm/staff. |
channel_anomaly.alerted — FIRM-scoped (the firm audit feed / T2k register is the in-app transport), masked number + counts only. |
channel_alert_digest.completed — an opaque per-recipient audit-sequence cursor. A successful admin advances independently; a failed recipient remains pending. |
Escalating silence (CC-41)
flood → cap hit → rate_limited rows land (counted silence, Story-2856)
next attempt: k = rate_limited rows in 24h horizon
window = 1h × 2^min(k, 6)
→ a sender who flooded an hour ago is STILL silent (window grew)
→ a sender quiet for the horizon decays to the base window and
earns the ordinary one reply per window again
every silenced attempt still lands ONE ledger row + feeds the
windowed CC-41 alert digest — suppression is counted, never dropped
Alert transport — one immediate email door
Seams write audit rows only (in-app transport: firm audit feed/admin
channel register T2k; staff feed for NULL-firm alerts). The
schedule-triggered worker sends each firm-scoped alert digest to every
active Firm Admin and sends truly unregistered-sender alerts to
CHANNEL_ALERT_OPS_EMAIL. Per-recipient high-water marks mean
one failed admin retries without duplicating mail to admins who already
received it. Bodies contain fixed labels, masked numbers, counts, and
timestamps only — never message, matter, or document content.
Infra twin (R2)
Required twin
Chore-2961
owns the VPC Lambda shell, EventBridge cadence, DB-secret/SES endpoint
paths, the ses:SendEmail grant, and
CHANNEL_ALERT_OPS_EMAIL. Functions CI publishes code and
the live alias only. Its R4 canary performs a staff-scoped
real alert-drain path and sends to the AWS SES mailbox simulator. The
deploy also requires an enabled
EventBridge schedule targeting :live; any missing
capability stays red.
LIVE line — staged burst → alert received
scripts/channel_burst_alert_probe.py fires a signed
“list my matters” burst from a dedicated bound staging number, then
polls ops.audit_log for the firm-scoped
channel_anomaly.alerted row and rejects raw-number or
unexpected metadata. After Chore-2961 applies, the human ceremony
confirms the next scheduled-run admin email lands.
Proofs
tests/unit/test_channel_zero_amp_shield.py— escalation math (geometric, capped, multiplier=1 disables), the escalated window driving the recent-count, rate_limited-only horizon SQL pin.tests/unit/test_channel_alert_digest.py— metadata-only digest bodies and planted-negative DB/SES capability canary proofs (R2/R4).tests/features/channel_safety_controls.feature— executable pytest-bdd mappings for the remaining CC-41/47 wording.tests/integration/test_channel_burst_anomaly_db.py— CC-41: aged attempts still silence a sender with flood history, and the same rows with no history get the base-window reply (the red/green pair); CC-47: listing burst → ONE firm-scoped masked alert, deduped in-bucket, below-threshold silent, fetch burst alone trips; the alert digest sends on every run, and recipient cursors retry only the failed address.tests/integration/test_channel_terminal_silence.py— the 2856 flood test updated to the escalated contract: the next-bucket attempt stays silent (no recovery inside the horizon) and still carries the bucket's one alert. Its pre-2860 red run is the escalation's observed-red evidence.