ADR 0019: Untrusted mail input and a policy gateway that AI cannot bypass
Status
Accepted
Context
Duckbill™ ingests HTML, URLs, and attachments from untrusted senders. Model output will recommend actions, but 01 AI §8 and the Sprint 05 goal require that AI never call provider adapters. Pipeline §§114–118 require sanitizer failure to be plain text and scanner failure to hold rather than mark CLEAN. Complex rules-builder UI is out of MVP (04 MVP §3).
Decision
- Sanitize all HTML through
packages/application/src/html-sanitize.ts. Fail closed to plain text. Do not render raw provider HTML. - Inspect every outbound fetch URL in
ssrf-guard.ts. Deny private, loopback, link-local, and metadata destinations. On scanner failure setunknown/held, neverclean. Quarantined items do not auto-archive. Extend Stream 04scan_state(unknown/held) rather than replace it. - Put
evaluatePolicyin front of send, delete, archive, and label. AI actors are denied those side effects. The gateway never invokes adapters. Denied results usePOLICY_DENIED. - Ship conservative deterministic rules (sender/subject/List-Id) with list/create/pause only. Suggested rules cannot start
ACTIVE. Security and user rules beat AI by skipping the model. Rules emit command requests; they do not call adapters.
Alternatives
- Render provider HTML and sanitize in the browser — rejected; fail-closed must happen before render.
- Fail-open
cleanwhen the scanner is down — rejected; Pipeline §115. - Let AI authorize archive/label as "safe" — rejected; MAIL-34/87 require policy, not model output, for those side effects.
- Visual rules builder in MVP — rejected; 04 MVP §3.
Consequences
Application services own sanitizer, SSRF, policy, and rules. Stream B (classification, extraction, drafts) remains separate and still cannot call adapters. Attachment hold states from migration 0014 stay valid. Dangerous feature flags remain default off (ADR 0003).
