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.
154 lines
5.7 KiB
Python
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",
|
|
]
|