The chain, and why nothing in it is a mistake
Three facts about this platform are individually reasonable and jointly a problem.- The IMAP connector ingests email. Email is written by anyone who can send one — a stranger, a vendor, an attacker who guessed a support address.
- Ingested content becomes retrieval grounding. That is the entire point of ingesting it.
- The same platform exposes MCP tools to the model, so an agentic turn can act: call an API, run a whitelisted command, write somewhere.
The rule
An untrusted-external chunk may be quoted in an answer. It must never
influence a tool call.
The asymmetry is the design, not a compromise.
Refusing to quote would break the product to fix the security problem — an
email corpus you cannot answer questions from is not a corpus. Refusing to
act costs the turn its actions and nothing else. The answer still comes
back, grounded in the same chunks, with the same citations. Only the tools are
withheld.
What it does not do
It never reads the content. The firewall inspects the provenance of the documents in context, never what they say. Detecting an injection by reading it is a losing game against an adversary who can rewrite; refusing to act on anything an outsider wrote does not depend on recognising the attack. It is not the Auto-Wiki curation firewall. That one ranks whether a human has vouched for a page. This one records who wrote it. A human-accepted wiki page summarising an external email isaccepted and
untrusted-external at once, and both facts matter — collapsing them would
let curation launder authorship.
It does not treat undeclared as untrusted. A null tier means no
connector said anything, which is every document written before the capability
existed. Reading those as untrusted would switch tools off for every existing
deployment on upgrade.
Where the verdict is computed
In the controller, because that is the only layer that has seen the retrieval result.McpToolCallingService receives the verdict through the
chat context and checks it before building the tool index.
Every block counts
primary, expanded (graph neighbours) and rejected (dismissed approaches)
are all assessed. The model sees one prompt; an attacker who lands a paragraph
in any of the three has the same leverage. Treating the rejected-approach
block as harmless because it is rendered under a warning marker would be
reasoning about presentation rather than about what reaches the context
window.
Configuration
NULL test on an indexed column.
The policy is reported alongside the corpus composition on all three
provenance surfaces (kb:provenance, GET /api/admin/kb/provenance, MCP
KbProvenanceTool), because the count only means something once a reader
knows whether it changes behaviour. “41 documents were written outside the
organisation” reads very differently depending on whether those documents can
drive a tool call.
Decision rationale
Why withhold rather than confirm. ADR 0028 allows either a policy exception or a human confirmation. Withholding ships first because it needs no new UI surface, no pending-action state, and no way for a confirmation prompt to itself become the injection target. A confirmation flow is the natural next step for deployments that need those turns to act. Why all-or-nothing per turn rather than per tool. The model composes one answer from one context. Once an untrusted chunk is in the window there is no supported way to establish that a particular tool call was uninfluenced by it — the argument would have to be about the model’s internals. Turn-level is the only boundary that means anything. Why the connector declares the tier rather than the host inferring it. Only the connector knows whether a mailbox is an internal distribution list or a public contact address. Inference at the host would be a heuristic over a fact the fetcher already had.Gotchas
- A blocked turn looks like a model that chose not to call a tool. That is
why the block is logged with the offending document ids
(
kb.provenance.tool_firewall.blocked). Check the log before debugging an agent that “stopped acting”. - One untrusted document blocks the turn. Narrowing the retrieval — a project filter, a tighter query — is the operator’s lever, not a firewall setting.
- The tier is per installation, not per connector class. Two IMAP installations can legitimately differ.
Related
- Ingest Provenance — phase 1, which records the tier this phase acts on.
- ADR 0028 in
docs/adr/— source ACL mirroring and ingest-time provenance.