Scan coverage
What the scanners actually see — and, just as importantly, what they do not. The coverage model is deliberately explicit: every gap is bounded by a knob and surfaced in the audit record.
Text extraction
extract_text (guardrails/scanners.py) flattens the MCP payload into one
scan string:
tools/callresult —content[].textitems joined (non-text items like images fall back to their JSON dump so embedded instruction strings stay visible).resources/readresult —contents[]text, and base64blobitems decoded (capped at 256KiB decoded bytes) so file contents are scanned, not just their base64 wrapping.prompts/getresult —messages[]content flattened.tools/listresult — every tool'sdescriptionplus itsinputSchemaJSON (tool-poisoning surface).- Fallback — a JSON dump with
ensure_ascii=False, so hidden control / zero-width characters are not\u-escaped away before the regex scanner sees them.
Byte windows (head / mid / tail)
scan_windows(text, MAX_CONTENT_BYTES, SCAN_TAIL_BYTES) decides which parts
of an over-budget payload are scanned:
| Window | Size (default) | What it covers |
|---|---|---|
| head | MAX_CONTENT_BYTES = 32KiB |
The attacker-relevant prefix. |
| mid | SCAN_TAIL_BYTES = 8KiB |
Centred on the unscanned remainder between head and tail — closes the mid-payload blind spot where an injection padded past the head but short of the tail previously went unseen. |
| tail | SCAN_TAIL_BYTES = 8KiB |
The last bytes — catches the classic padding-prefix truncation bypass. |
All windows are UTF-8-safe (truncated on character boundaries).
SCAN_TAIL_BYTES=0 restores legacy head-only behaviour. Windows that would
fully overlap the head are skipped, so short over-budget payloads may scan
fewer than three chunks. Decisions on truncated payloads are flagged
truncated=true in the audit record, with scanned_bytes / total_bytes.
Payload hard cap
Payloads whose extracted text exceeds SCAN_MAX_PAYLOAD_BYTES (default
1MiB) are still scanned via the three windows and gain a payload_size
HUMAN_REVIEW result carrying the scanned/total byte counts —
HUMAN_REVIEW_MODE=deny turns it into a hard deny. This bounds the
unscanned fraction of giant payloads.
PromptGuard token windows
Within each scanned chunk, PromptGuard scores 512-token sliding windows
(stride 64) with an adaptive budget clamp(ceil(tokens/step)+1, 4,
PG_MAX_WINDOWS) — the first budget-1 strided windows plus a tail-aligned
window, MAX score wins (see PromptGuard).
Coverage matrix
| Payload size | Regex scanner | PromptGuard | Notes |
|---|---|---|---|
| ≤ 32KiB | Full text | Up to 4 windows (adaptive budget floor); short payloads usually scored whole | Full coverage. |
| 32KiB – 1MiB | head + mid + tail windows | Each scanned chunk gets up to PG_MAX_WINDOWS (16) token windows (~7.7K tokens per chunk) |
Regions between the byte windows are unscanned. Within each chunk, tokens past the window budget are unscanned. |
| > 1MiB | Same three windows | Same per-chunk budget | Plus a payload_size HUMAN_REVIEW flag so the under-scan is a decision-level event, not a silent gap. |
Known residual gaps (honest list)
- Unscanned middle regions. Between the byte windows (and beyond the
per-chunk token budget) an injection can hide. This is a bounded,
documented trade-off: raise
SCAN_TAIL_BYTES/PG_MAX_WINDOWSfor more coverage at higher latency, or setHUMAN_REVIEW_MODE=deny+ lowerSCAN_MAX_PAYLOAD_BYTESfor a stricter posture. - Multi-chunk split attacks. An injection split across a window boundary may score below threshold in both halves (the 64-token stride mitigates but does not eliminate this).
- Non-text semantics. Scanners see extracted text, not rendered meaning — a payload that is only dangerous after the agent acts on it is the Invariant rules layer's job, not the content scanners'.
- Redaction coverage. Redaction runs on the full untruncated payload but
is skipped entirely beyond
REDACTION_MAX_BYTES(256KiB) — flaggedredaction_skipped=size. Block-grade secrets in such payloads are still caught by the scanner windows; only best-effort masking of ALLOW-grade PII is skipped.
Audit-driven detection: alert on truncated=true and
scanned_bytes << total_bytes instead of assuming full coverage (see
Metrics).