llm-ingestion-pipeline-secu.../docs/BRIEF.md
Kjell Tore Guttormsen d983aa3c95 docs(brief): fold in ms-ai-architect Layer B learnings (first output-side impl)
Based on the working, tested Layer B ingestion-gate in ms-ai-architect (2026-07-04):
- A: correct consumer 2 — it fetches authored Learn docs + code samples via the
  microsoft-learn MCP, NOT the open Q&A forum / MSDN / Stack Overflow (that claim
  was unverified/overstated); low-trust surface is intra-document. Repo/name
  reconciled: the consumer is ms-ai-architect, no separate "MS AI Security plugin".
- B: §4.7 — trust tiers WITHIN a document (code sample / localized string vs
  authored prose), not only across sources; carriers + critical block in any tier.
- C: §12 — consume the lexicon as pure imported functions, not the llm-security
  scan CLI (which under-covers Markdown prose + base64-in-code-block, verified).
- D: §4.6 — fail-secure extends to scanner-unavailable: un-scannable ⇒ BLOCK.
Adds ms-ai-architect as the second reference implementation (output side).
2026-07-04 08:32:38 +02:00

19 KiB

Brief: llm-ingestion-pipeline-security

A reusable, minimal, dependency-light defensive layer for LLM ingestion pipelines — the write-time siblings of query-time chatbot guardrails.

Status: brief / pre-implementation. This document defines what the repo should contain and why. No code yet.


1. One-line purpose

Give any pipeline that runs untrusted content through a large language model and then persists the result into a downstream-consumed artifact (RAG corpus, knowledge base, wiki, embeddings store) a small, framework-agnostic library that packages the architectural contract for doing it safely: sanitize → fence → tool-less quarantined transform → capability isolation → scan output before commit → fail-secure.

2. The problem, stated precisely

The mature tooling for prompt injection is query-time: it sits between a user (or an agent) and a model, at inference, on live traffic. Ingestion is a different shape and is underserved:

  • Direction. The danger flows from content (documents, changelogs, scraped web pages, user uploads, tickets) through an LLM enrichment/summarization/ extraction/classification step, into a persisted artifact that other systems and agents later read.
  • Two distinct failure modes, only the first of which query-time guardrails address:
    1. Injection steers the enrichment step — untrusted content contains instructions that hijack the summarizer/extractor.
    2. Poisoned content is published — the artifact itself becomes the attack. A verbatim-quoted payload, or an LLM-emitted instruction, lands in the knowledge base and later poisons a downstream agent's context. This is RAG/memory poisoning committed at write time, and a query-time guardrail on the downstream reader never sees where it came from.
  • Non-interactive, unattended. Ingestion typically runs headless on a schedule. There is no human in the loop to notice a weird answer. Failure discipline (fail-secure, alerting, no silent verbatim fallback under attack) matters more here than in a chat UI.

3. Prior art and honest positioning

Injection detection is a crowded space. This repo must not pretend otherwise.

Tool Orientation
LLM Guard, Guardrails AI, NeMo Guardrails Query-time I/O guardrails / dialogue policy
Rebuff Self-hardening query-time detector (vector store of past attacks)
Vigil, Lakera Guard, Vijil Query-time injection classifiers / detection servers
LlamaFirewall (Meta) Agent guardrail framework (I/O, tool calls)
Resk Python guardrail lib for LLM API calls
CleanBase, DataFilter (research) Detecting/cleaning malicious docs in RAG DBs

The gap this repo 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 — quarantined tool-less transform, per-stage capability isolation, output-scan-before-persist, fail-secure disposition — as composable, framework-agnostic, minimal-dependency code, with corpus-aware false-positive control.

Necessary honesty (verified, see §11): 2025 research shows character-level and adversarial-ML evasion defeats most pattern- and classifier-based detectors while keeping the payload legible to the target model. Therefore the value of this library is architecture and defense-in-depth, not a detector that "solves" injection. The lexicon is one WARN-class layer; the contract (a tool-less call that cannot act on a successful injection, credentials the hijacked step never holds, output scanned before it is trusted) is the load- bearing part. The brief must lead with the contract, not the regex.

4. Design principles

  1. Minimal dependencies. Stdlib-first core. Optional pluggable detectors (an embedding classifier, an LLM-judge) live behind extras, never in the core path. A pipeline should be able to adopt the core with zero new runtime deps.

  2. Framework-agnostic. Works with any SDK (Anthropic, OpenAI, local). The library never makes the model call; it hardens the call the pipeline makes.

  3. Pure functions. Detection is text -> findings. No network, no telemetry, no global state. Portable, testable, auditable.

  4. Disposition belongs to the caller. The library reports; the pipeline decides WARN vs quarantine-review vs fail-secure-halt. Defaults are provided, but blocking is never imposed — see principle 5.

  5. Corpus-aware false-positive control. A corpus that legitimately discusses prompt injection (security docs, changelogs, a wiki about AI safety) will trip any broad lexicon. The library must make WARN-not-block the easy default and hard-fail an explicit, calibrated opt-in. Silent over-blocking of legitimate content is a failure mode, not a safe default.

  6. Fail-secure under compound signals. Injection-scan hit and a transform failure is treated as a probable forced-fallback attack: halt + alert, never an auto-committed verbatim entry. The same default applies when the output scanner itself is unavailable or errors (a missing/unresolvable detector dependency, a scanner exception): treat an un-scannable artifact as a BLOCK — never persist it unscanned. Fail closed, not open. (Verified in the first output-side implementation, 2026-07-04: an unresolvable detector is turned into a hard-fail, not a silent pass.)

  7. Disposition scales with source trust. The same lexicon should not carry the same disposition for every source. A pinned, reputable, single-author source (an official changelog) warrants WARN — false positives from legitimate security vocabulary dominate the real signal. An open, user-generated source (a community Q&A forum, scraped web) warrants QUARANTINE_REVIEW or hard-fail on high-severity hits — a random contributor emitting a spoofed <system> block is not "legitimately discussing injection," and the cost of over-blocking one forum post is far lower than publishing a poisoned one. Source trust level is therefore a first-class input to the disposition policy, not a global constant.

    Trust also tiers within a document, not only across sources (verified in the first output-side implementation, 2026-07-04). A fenced code sample or a localized (non-English) string inside an otherwise-authoritative doc is the low-trust surface even when the document's source is reputable — a spoofed <system> block or a base64 blob in a code sample is far likelier a real payload than a false positive, whereas the same lexicon hit in authored, en-locale prose is likelier a doc legitimately discussing the pattern. So the same hit hard-fails inside a code block / localized string but WARNs in authored prose. Intra-document provenance (in-code-fence, non-en locale) is a first-class disposition input alongside source-level trust. Invisible carriers (zero-width / bidi / Unicode-tag) and critical injection are the exception: they have no legitimate place in a reference file and block in any tier.

5. Proposed module layout

src/llm_ingest_security/
  sanitize/      Carrier stripping: invisible chars (Unicode Tags, zero-width,
                 bidi), HTML comments, data: URIs. Byte-identical on clean input;
                 removes only, never rewrites. Per-class counts for a WARN gate.
  fence/         Spotlighting helpers: wrap untrusted input in markers, and strip
                 the markers FROM the input first so it cannot break out of its
                 own fence.
  lexicon/       Semantic injection patterns as DATA (regex + label + severity),
                 tiered CRITICAL/HIGH/MEDIUM: override, spoofed headers/tags,
                 identity redefinition, config attacks, NL-indirection
                 (fetch-and-execute, send-to-external, read-dotfiles,
                 extract-and-exfiltrate), sub-agent spawn, leetspeak/homoglyph,
                 multi-language. Curated, versioned, re-syncable.
  entropy/       base64/hex blob detection for smuggled encoded instructions or
                 encoded exfil (complements secret-pattern scanning).
  contract/      The quarantine asserters — the differentiator:
                   - assert the model request carries NO tools
                   - assert per-stage credential allowlist (the enrichment step
                     sees exactly the one key it needs, and none of the publish
                     credentials)
                   - capability-isolation helpers (env scoping)
  output/        Output-side scanner: run the lexicon + entropy over the model's
                 OUTPUT (summary, verbatim quotes, extracted fields) BEFORE the
                 artifact is persisted. This is the RAG-poisoning gate.
  disposition/   Policy object: WARN | QUARANTINE_REVIEW | FAIL_SECURE per gate,
                 plus the compound-signal fail-secure rule.
  report/        Structured findings: class, count, severity, offset, source
                 (input|output), disposition.

6. The reusable contract (adopt-this checklist)

The actual product is this checklist, encoded as code a pipeline wires in order:

  1. Sanitize before fence. Strip carrier classes from untrusted input first.
  2. Fence untrusted input. Spotlight-mark it; strip 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. Treat the alert channel/topic as a secret.

7. Non-goals

  • Not a query-time chatbot / agent guardrail (that space is served).
  • Not a fine-tuned model or a required ML classifier — deterministic core; ML detectors are optional, pluggable extras.
  • Not a hosted service or SaaS.
  • Not a silver bullet — explicitly a defense-in-depth layer whose lexicon is bypassable in isolation; the contract is what carries the security.

8. Language, packaging, licensing

  • Python first — most ingestion pipelines are Python. Stdlib-only core; pyproject.toml with optional extras ([ml], [judge]).
  • Framework-agnostic public API; no SDK imported by the core.
  • A Node/TypeScript port is a plausible follow-on (a shared JSON lexicon as the single source of truth across both).
  • Permissive license (MIT or Apache-2.0) so it is droppable into any pipeline.

9. Test strategy

  • Adversarial corpus. A seeded set of payloads (one per lexicon class, including obfuscated and multi-language variants); measure and report recall.
  • False-positive corpus. Real content that legitimately discusses injection (security docs, changelogs); assert the default disposition is WARN, not block, and that hard-fail is an explicit opt-in.
  • Sanitizer invariant. Clean input returns byte-identical with an all-zero report; the sanitizer only removes.
  • Contract asserters. A tool-carrying request and a credential-leaking stage env both raise; the happy path passes.
  • No network in any test.

10. Threat-model grounding

  • OWASP LLM Top 10: LLM01 Prompt Injection, output-handling, and the RAG / knowledge-poisoning vectors.
  • Google DeepMind "AI Agent Traps" (Franklin et al., 2025): Latent Memory Poisoning and RAG Knowledge Poisoning map directly onto the write-time artifact.
  • Research anchors: CleanBase (malicious docs in RAG DBs), DataFilter, and the 2025 guardrail-evasion literature that motivates the defense-in-depth framing.

11. Verification log (prior-art claims + sources)

Per the operator's verification duty — key claims in this brief and where they are grounded:

Marked assumed, not verified: the specific claim that no existing library packages the full write-time contract as minimal-dependency code. The search found no such library, but absence of evidence is not proof; a focused survey of PyPI + GitHub topics should confirm before the README makes a novelty claim.

12. Reference implementation and target consumers

The contract in §6 is not hypothetical — it is extracted from a working pipeline: the claude-code-llm-wiki Stage B enrichment path (tools/wiki_ingest/), which already implements sanitize + spotlight-fence + tool-less quarantined SDK call + per-stage credential isolation + schema-validated output + fail-secure ForcedFallbackHalt. That pipeline is the reference implementation for the full contract (tool-less transform, credential isolation, fail-secure). The semantic lexicon seed is the injection-patterns.mjs table from the llm-security plugin (a pure regex+label+severity dataset, portable as data).

Integration lesson (verified 2026-07-04) — consume the lexicon as a pure function, not via the scanner CLI. The llm-security orchestrator CLI (llm-security scan) is repo/directory-oriented and, empirically, does not apply the injection lexicon to arbitrary Markdown prose, nor does its entropy-scanner flag a base64 blob inside a fenced code block — a full deep-scan over three seeded fixtures caught only the invisible-Unicode class. The load- bearing reuse seam is therefore the importable pure primitives (scanForInjection(text) from injection-patterns.mjs; isBase64Like / shannonEntropy from string-utils.mjs; the unicode-scanner module), driven per line/region so the consumer controls provenance tiering. This library should ship the lexicon as data and detection as pure text -> findings functions (design principle 3) so no consumer has to shell out to an orchestrator that under-covers its content type.

Target consumers (the reason the library exists):

  1. claude-code-llm-wiki — low-untrust source (a pinned, reputable Anthropic changelog). Here the contract is defense-in-depth hygiene; the architectural controls already close the severe outcomes, and the lexicon runs at WARN. This consumer is retrofitting the output-side widening late (at its A13), which is precisely the cost this library exists to avoid for the next one.

  2. ms-ai-architect (the plugin in ktg-plugin-marketplace; there is no separate "MS AI Security plugin" — repo/name reconciled 2026-07-04) — an ingestion pipeline over Microsoft Learn content fetched via the microsoft-learn MCP (microsoft_docs_fetch / _search / _code_sample_search): authored, reviewed Learn docs plus code samples and localized strings. Correction (verified against the live command/agent code 2026-07-04): the earlier characterization — that it is the high-untrust end ingesting the open Microsoft Q&A forum (learn.microsoft.com/answers), MSDN Forums and Stack Overflow — is unverified and overstated. The pipeline's fetch surface is authored Learn docs + code samples; it does not ingest the open Q&A forum as part of its flow. Its low-trust surface is therefore intra-document (a fenced code sample or a localized string inside an otherwise-authoritative doc), which is exactly why disposition must tier at the chunk level (§4.7), not treat the whole consumer as globally high-untrust. Even so the contract is load-bearing, not hygiene here: the KB is publicly distributed and re-served as instruction-adjacent context to every downstream agent session, so one poisoned reference file is a supply-chain compromise.

    A working, tested implementation of the output-scan-before-persist gate (§6 step 6) plus provenance-tiered disposition (§4.7) now exists in this plugin — "Layer B" of its ingestion security gate, wired as a sibling to the existing create-guard at the single write chokepoint, with a fail-closed exit-code contract (0 clean / 1 block / 2 warn). It is the second reference implementation for this library — specifically for the output side (the RAG-poisoning gate + disposition), complementing consumer 1's reference for the full contract (tool-less transform + credential isolation). It consumes the llm-security lexicon by in-process import of the pure functions (see the integration lesson above), which is a concrete data point for open decision §13.3.

Why day-1, not retrofit. The load-bearing parts of the contract are architectural — tool-less quarantine, per-stage credential isolation, output-scan-before-persist, fail-secure disposition. These are cheap to design in and expensive to bolt on after a pipeline exists (consumer 1 is proving that). Adopting the library from day 1 means the high-untrust consumer (2) inherits a correct, calibrated contract for free instead of discovering the gap in production. The library's value is not this pipeline; it is every pipeline after it whose input is less trustworthy than a pinned changelog.

13. Open decisions for the operator

  1. Namellm-ingestion-pipeline-security is descriptive but long; a shorter package name (e.g. ingestguard) may be worth it before first publish.
  2. License — MIT vs Apache-2.0.
  3. Relationship to llm-security — sibling repos sharing one lexicon dataset as the single source of truth, or fully independent? Avoid two drifting copies of the pattern table.
  4. Publish target — Forgejo (git.fromaitochitta.com), and whether a public open/ mirror. No GitHub per house policy.
  5. Python-only vs polyglot from day one.
  6. Maintenance model — solo fork-and-own (as llm-security), or open to PRs.