1
0
Fork 0
llm-ingestion-pipeline-secu.../src/llm_ingestion_guard/__init__.py
Kjell Tore Guttormsen ca4f97c8c9 release: 1.1.0 -- the first behaviour change shipped under the freeze
Six live version surfaces bumped by hand (no sed -- provenance is never
bumped): pyproject.toml, __init__.__version__, README badge + Status + the
pinned pip install tag, docs/BRIEF.md, docs/ADOPTION-BRIEF.md Status, and
CLAUDE.md. ADOPTION-BRIEF's test count 792 -> 802.

NOT bumped, and deliberately: SECURITY.md's two `1.0.0` references name the
freeze BASELINE, not the current version -- "a payload that disposes WARN on
1.0.0 may dispose FAIL_SECURE on a later 1.x" is the promise this release
instantiates, so rewriting it to 1.1.0 would erase what it promised. The
GATE-G and PLAN-v1 numbers are the 1.0.0 gate record. The Forge repo
description carries no version (verified against the API last session).

This is the case SECURITY.md and the 1.0.0 CHANGELOG entry described in
advance: the exported surface is frozen, detection behaviour is not. No
exported name moved. A document that disposed WARN on 1.0.0 may dispose
FAIL_SECURE here; a consumer whose frontmatter carries an unquoted ": " in a
value will see those concepts refused at import, and quoting it parses.

Re-measured after the bump, alone: 802 passed, 129/129 classes, 6/6 gaps hold,
35 limitations.
2026-08-13 22:58:36 +02:00

154 lines
5.7 KiB
Python

"""llm-ingestion-guard — a write-time defensive layer for LLM ingestion pipelines.
Query-time guardrails guard the answer; this library guards the *artifact*. It
packages the ingestion-side security contract — sanitize -> fence -> tool-less
quarantined transform -> per-stage capability isolation -> scan output before
persist -> fail-secure — as composable, stdlib-first, framework-agnostic code.
The library never makes the model call itself (no SDK is imported by the core),
so the public surface is a **toolkit plus two bookends** around the caller's
tool-less transform (BRIEF §6):
prepared = prepare_input(untrusted_content) # §6 steps 1-2: sanitize + fence
output = your_model(prepared.fenced) # §6 step 3: tool-less, caller's job
decision = screen_output(output, policy) # §6 steps 6-7: scan + dispose
if decision.disposition is Disposition.FAIL_SECURE:
raise SystemExit # halt + alert; never persist (§6 steps 7-8)
The individual detectors (:func:`sanitize`, :func:`scan_lexicon`,
:func:`scan_output`, ...), the contract asserters (:func:`assert_tool_less`,
:func:`scoped_env`, ...) and the disposition machinery are all exported for
pipelines that compose the checklist themselves. See ``docs/PLAN.md`` for the
build order and ``docs/BRIEF.md`` §6 for the contract this wiring encodes.
"""
from __future__ import annotations
from dataclasses import dataclass
from .report import Finding, Report, Severity, Source, severity_rank
from .sanitize import sanitize, SanitizeResult
from .entropy import scan_entropy, EntropyResult, DecodedBlob
from .lexicon import scan_lexicon, load_lexicon, LexiconPattern
from .fence import fence, FenceResult
from .neutralize import neutralize, NeutralizeResult
from .active_content import scan_active_content
from .output import scan_output, scan_secret_egress
from .disposition import (
decide,
guard,
Policy,
Trust,
Provenance,
Risk,
Disposition,
DispositionResult,
DEFAULT_ACTION_MAP,
PRESET_TRUSTED_SOURCE,
PRESET_USER_UPLOAD,
)
from .contract import (
assert_tool_less,
assert_credential_allowlist,
assert_within_input_cap,
credential_env_names,
scoped_env,
ContractViolation,
OversizeInputError,
)
from .grounding import (
SourceGroundingCheck,
no_grounding_check,
DEFAULT_GROUNDING_CHECK,
)
from . import okf
__version__ = "1.1.0"
# --- §6 bookends: the two library-side halves around the transform ---------
@dataclass(frozen=True)
class PreparedInput:
"""Untrusted content made ready for a tool-less transform (§6 steps 1-2).
``fenced`` is the sanitized, spotlight-fenced text to hand the model.
``nonce`` is the per-call fence delimiter — a caller may check for it in the
output to detect a fence breakout. ``report`` merges the sanitize and fence
findings so the caller can gate on the *input* too, not only the output.
"""
fenced: str
nonce: str
report: Report
def prepare_input(text: str, source: Source = Source.INPUT) -> PreparedInput:
"""Sanitize then fence untrusted ``text`` (BRIEF §6 steps 1-2).
Strips carrier classes first (:func:`sanitize`), then spotlight-fences the
cleaned payload in a randomized per-call delimiter (:func:`fence`) — sanitize
*before* fence so a carrier can never smuggle a forged delimiter. The merged
report carries both steps' findings; ``prepare_input`` itself renders no
disposition (design principle 4 — the caller decides).
"""
sanitized = sanitize(text, source)
fenced = fence(sanitized.text, source)
report = Report()
report.extend(sanitized.report.findings)
report.extend(fenced.report.findings)
return PreparedInput(fenced=fenced.text, nonce=fenced.nonce, report=report)
def screen_output(
text: str,
policy: Policy,
*,
provenance: Provenance = Provenance.PROSE,
transform_failed: bool = False,
) -> DispositionResult:
"""Scan the emitted ``text`` and dispose it, failing *closed* (§6 steps 6-7).
Runs :func:`scan_output` (lexicon + entropy + decode-and-rescan + secret
egress) under :func:`guard`, so a scanner that errors on crafted input yields
``FAIL_SECURE`` rather than an auto-persist — an un-scannable artifact is
never committed. ``transform_failed=True`` (the caller's model call raised or
fell back) plus any finding is treated as a probable forced-fallback attack
and also halts, regardless of trust tier.
"""
return guard(
lambda: scan_output(text, source=Source.OUTPUT),
policy,
provenance=provenance,
transform_failed=transform_failed,
)
__all__ = [
"__version__",
# shared types
"Finding", "Report", "Severity", "Source", "severity_rank",
# input-side detectors + result types
"sanitize", "SanitizeResult",
"scan_entropy", "EntropyResult", "DecodedBlob",
"scan_lexicon", "load_lexicon", "LexiconPattern",
"fence", "FenceResult",
"neutralize", "NeutralizeResult",
# output-side
"scan_output", "scan_secret_egress", "scan_active_content",
# disposition — `Risk` is the assessment axis, `Disposition` the action
"decide", "guard", "Policy", "Trust", "Provenance",
"Risk", "Disposition", "DispositionResult", "DEFAULT_ACTION_MAP",
"PRESET_TRUSTED_SOURCE", "PRESET_USER_UPLOAD",
# contract asserters
"assert_tool_less", "assert_credential_allowlist",
"credential_env_names", "scoped_env", "ContractViolation",
"assert_within_input_cap", "OversizeInputError",
# grounding seam
"SourceGroundingCheck", "no_grounding_check", "DEFAULT_GROUNDING_CHECK",
# §6 bookends
"prepare_input", "screen_output", "PreparedInput",
# OKF adapter (v0.2) — the format-specific layer, as its own namespace
"okf",
]