Reusable, minimal, dependency-light defensive layer for LLM ingestion (write-time) pipelines — sanitize, fence, tool-less quarantined transform, capability isolation, output-scan-before-persist, fail-secure.
Find a file
Kjell Tore Guttormsen 86726ed109 docs(readme): add house-style shields.io badges (version/status/python/tests/license)
Mirror the badge convention from the ms-ai-architect plugin README — shields.io
STATIC badges (img.shields.io/badge/), which work on Forgejo where dynamic
build-status endpoints do not. Type-adapted for a library rather than a plugin:
the Platform slot becomes python-3.10+, the content-count slot becomes
tests-201-passing, plus an honest status-alpha badge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HyRCQMocjZ6SmSQ6JidJ2k
2026-07-05 07:05:16 +02:00
docs docs(plan): add end-to-end showcase pipeline test as the final deliverable 2026-07-04 17:23:17 +02:00
src/llm_ingestion_guard feat(wiring): §6 bookends (prepare_input/screen_output) + full public API + README v0.1 (TDD) 2026-07-04 23:34:28 +02:00
tests test(showcase): end-to-end multi-vuln pipeline + adversarial/FP corpora (recall) 2026-07-04 23:34:51 +02:00
.gitignore chore: repo scaffolding via /repo-init 2026-07-04 08:39:23 +02:00
CHANGELOG.md feat(wiring): §6 bookends (prepare_input/screen_output) + full public API + README v0.1 (TDD) 2026-07-04 23:34:28 +02:00
CLAUDE.md feat(wiring): §6 bookends (prepare_input/screen_output) + full public API + README v0.1 (TDD) 2026-07-04 23:34:28 +02:00
LICENSE feat: scaffold package + report and sanitize modules (TDD) 2026-07-04 09:24:20 +02:00
pyproject.toml feat: scaffold package + report and sanitize modules (TDD) 2026-07-04 09:24:20 +02:00
README.md docs(readme): add house-style shields.io badges (version/status/python/tests/license) 2026-07-05 07:05:16 +02:00

llm-ingestion-guard

Version Status Python Tests License

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:

  1. Sanitize before fence. Strip carrier classes (zero-width, BIDI, Unicode-tag, HTML comment, data:) from untrusted input first.
  2. Fence untrusted input. Spotlight-mark it in a randomized per-call delimiter; strip attacker fence markers from the payload.
  3. Tool-less transform. Call the model with zero tools. A successful injection then has nothing to act with.
  4. Per-stage capability isolation. The enrichment stage holds only the model key; the publish stage holds only the publish credential; no stage holds both.
  5. Treat output as data. Parse to a frozen schema; reject on structural violation. The output never reaches a shell, git, or a filesystem path.
  6. Scan output before persist. Run the lexicon + entropy over the emitted text. Verbatim-carried payloads and model-emitted instructions are caught here.
  7. Fail-secure on compound signals. Injection hit + transform failure = halt
    • alert, never a silent verbatim commit.
  8. 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 grounding module ships only a SourceGroundingCheck seam — 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.
  • 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 (no pypdf/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.

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

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.