- Python 100%
Ground-truth pass over the named flagship consumer (portfolio-optimiser's 'OKF-upload-inbox') found it does not exist as a seam: both optimiser siblings are frozen at release, carry their own OKF layer, receive no external bundles, and take no dependency on this guard. Defer consumer integration (stream 2) until the guard is mature; mature it here first, Node-port-friendly. Next concrete build is the in-repo OKF inbox showcase (mode-b import_bundle as a receive/quarantine gate), spec'd under 'The OKF inbox showcase'. |
||
|---|---|---|
| docs | ||
| src/llm_ingestion_guard | ||
| tests | ||
| .gitignore | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
llm-ingestion-guard
A reusable, minimal, dependency-light defensive layer for LLM ingestion pipelines — the write-time siblings of query-time chatbot guardrails.
Where mature guardrails (LLM Guard, NeMo Guardrails, Rebuff, Vigil, …) sit between a user and a model at query time, this library hardens the other shape: untrusted content flowing through an LLM enrichment/summarization/extraction step into a persisted, downstream-consumed artifact (RAG corpus, knowledge base, wiki). It packages the architectural contract — sanitize → fence → tool-less quarantined transform → per-stage capability isolation → scan output before commit → fail-secure — as composable, stdlib-first, framework-agnostic code.
The gap it fills is not "no one detects injection." It is a small library (not a hosted service, not a fine-tuned model) that packages the write-time ingestion contract — the part query-time tooling structurally cannot see, because a poisoned artifact committed at write time is read by a downstream agent whose guardrail never sees where it came from.
Status: v0.1, alpha. The stdlib-only core is built and tested — ten
detector/contract modules and the top-level wiring, exercised by an end-to-end
showcase and adversarial + false-positive corpora. The public API may still
change. There are real limitations, stated plainly below; read them.
Install
pip install llm-ingestion-guard # stdlib-only core, zero dependencies
Optional ML/judge detectors live behind extras ([ml], [judge]) and are not
required — the core is deterministic and dependency-free.
Quickstart — the two bookends
The library never makes the model call itself. It gives you the two library-side halves around your own tool-less transform:
from llm_ingestion_guard import (
prepare_input, screen_output, Disposition, PRESET_USER_UPLOAD,
)
prepared = prepare_input(untrusted_content) # §6 1-2: sanitize + fence
enriched = your_model(prepared.fenced) # §6 3: tool-less — YOUR call
decision = screen_output(enriched, PRESET_USER_UPLOAD) # §6 6-7: scan + dispose
if decision.disposition is Disposition.FAIL_SECURE:
alert(gate_code=decision.reasons) # §6 8: minimal payload, no content
raise SystemExit # §6 7: halt — never persist
screen_output fails closed: if the scanner itself errors on crafted input,
the disposition is FAIL_SECURE, never a silent persist. Pass
transform_failed=True when your model call raised or fell back — a scan hit
together with a transform failure is treated as a probable forced-fallback attack
and halts regardless of trust tier.
Every primitive is also exported for pipelines that compose the checklist
themselves — sanitize, scan_lexicon, scan_entropy, scan_output,
neutralize, the decide / guard disposition machinery, and the contract
asserters assert_tool_less / assert_credential_allowlist / scoped_env. See
the end-to-end showcase for a full worked pipeline.
The reusable contract (adopt-this checklist)
The actual product is this checklist, encoded as code you wire in order:
- Sanitize before fence. Strip carrier classes (zero-width, BIDI,
Unicode-tag, HTML comment,
data:) from untrusted input first. - Fence untrusted input. Spotlight-mark it in a randomized per-call delimiter; strip attacker fence markers from the payload.
- Tool-less transform. Call the model with zero tools. A successful injection then has nothing to act with.
- Per-stage capability isolation. The enrichment stage holds only the model key; the publish stage holds only the publish credential; no stage holds both.
- Treat output as data. Parse to a frozen schema; reject on structural violation. The output never reaches a shell, git, or a filesystem path.
- Scan output before persist. Run the lexicon + entropy over the emitted text. Verbatim-carried payloads and model-emitted instructions are caught here.
- Fail-secure on compound signals. Injection hit + transform failure = halt
- alert, never a silent verbatim commit.
- Minimal alert payloads. Alert with a gate code + run ID, never content.
Steps 1-2 are prepare_input; steps 6-7 are screen_output; steps 3-5 are
yours; the contract asserters harden step 3-4.
Honest limitations (shipped as a control)
Conceding these plainly is itself a control — it prevents the false assurance that a green scan means safe content:
- Structural unsolvability at the text layer. Pattern/lexicon detection is bypassable in isolation; character-injection and novel phrasings evade it. The contract (tool-less transform, capability isolation, fail-secure) is what carries the security — the lexicon is defense-in-depth, not a wall.
- Semantic / factual poisoning is invisible to lexicon + entropy: a
factually false claim in clean prose carries no suspicious token. The
groundingmodule ships only aSourceGroundingCheckseam — the deterministic core does not judge semantics; a[judge]implementation must be plugged in. - Adversarial-ML evasion can survive normalization; tokenizer mismatch between scanner and model leaves gaps.
- Latent / dormant memory poisoning is not judgeable at write time.
- Dormant / broken-link injection in a linked corpus (e.g. an OKF bundle): a link to a not-yet-existing target passes a per-concept write-time scan clean — the payload is planted later, when that target is written. A scanner that sees one document at a time cannot catch it; it needs cross-write graph re-scan (tracked for the OKF adapter).
- A document that describes attacks is a false positive. Content whose legitimate purpose is to document prompt-injection payloads (security notes, this project's own corpus) trips carrier-strip / fail-secure. At the text layer there is no way to distinguish "about an attack" from "carrying an attack"; such content needs a deliberate, explicitly-marked escaped path, never a silent allow.
- Insider in-place edits by a trusted author are out of the untrusted-content threat model.
- Text-only. The core is
text -> findings: it parses no files (nopypdf/python-docx/archive deps). Extract text first, then scan it with the high-untrust upload provenance. OCR-embedded instructions and multimodal stego in images/PDFs are out of scope beyond the sanitizer's character-layer stripping. - Lexicon findings are deduplicated by pattern id —
count=1and the first offset are reported, so the same class matched across several channels/variants collapses to one finding at its first location. This keeps reports readable, but a caller that counts occurrences or needs every offset of a repeated pattern sees only the first: a deliberate readability tradeoff, not full positional coverage.
Out-of-scope (documented boundary)
Embedding/vector-layer defenses (OWASP LLM08, downstream of persist); multimodal steganography; query-time / runtime guardrails; semantic factuality verification.
Design & threat model
- Design brief — what this repo contains and why.
- Build plan — module build order and the reuse map.
The contract is extracted from a working reference implementation (the
claude-code-llm-wiki Stage B enrichment pipeline). Threat-model anchors: OWASP
LLM Top-10 2025 (LLM01/02/04/05/06 strongest, LLM08 boundary, LLM09/10),
PoisonedRAG, guardrail-evasion (arXiv 2504.11168), EchoLeak (CVE-2025-32711).
License
MIT — see LICENSE.