"""The only module in this library that imports `llm-ingestion-guard`. Doors B and C take an INJECTED gate so the flows stay dependency-free and testable; this module is the adapter a caller injects when the gate should be the real guard. It translates in one direction only — guard verdict in, the door's decision type out — and takes no decision of its own. Everything a verdict depends on happens inside the guard. **Door B screens the exact bytes it persists.** The guard's §6 bookends (`prepare_input` -> model -> `screen_output`) assume a model call in between; this library makes none, and `prepare_input` returns prompt-shaped text (sanitized AND spotlight-fenced with a per-call nonce) that must never reach disk. So the adapter calls `screen_output` alone, on the extracted text as it stands, and hands that same text back. Two consequences, both deliberate: - an invisible carrier is REFUSED rather than stripped-and-persisted. The guard's own doctrine is that a carrier has no legitimate place in a reference file, and sanitizing before persisting would write a document that differs invisibly from the operator's file while `source_sha256` still points at the original bytes. This library validates and refuses; it does not repair. - the verdict is a statement about the persisted document, because the screened string and the written string are the same string. The policy is `PRESET_USER_UPLOAD`: an inbox drop is an untrusted upload, so any finding at all is held for review rather than written. A caller who needs another tier writes their own three-line adapter — that is what the injected seam is for. **Door C hands the bundle over whole.** `okf.import_bundle` resolves the cross-link graph across concepts, so gating them one at a time would throw half the gate away. The adapter maps each `ConceptResult` to an `ImportDecision` and returns the guard's log body unwritten. """ from __future__ import annotations from llm_ingestion_guard import PRESET_USER_UPLOAD, screen_output from llm_ingestion_guard import okf as guard_okf from .errors import MaterializationError from .importer import BundleDecision, ImportDecision from .inbox import GateDecision __all__ = ["import_gate", "inbox_gate"] def inbox_gate(text: str) -> GateDecision: """Door B's persist gate over the real guard (a `Gate`). `disposition` is the guard's `Disposition` VALUE, carried across as a plain string so the flow never imports the enum, and `reasons` is the guard's audit trail verbatim. """ decision = screen_output(text, PRESET_USER_UPLOAD) return GateDecision( sanitized_text=text, disposition=str(decision.disposition.value), reasons=tuple(str(reason) for reason in decision.reasons), ) def import_gate(bundle: dict[str, str], *, origin: str, channel: str) -> BundleDecision: """Door C's persist gate over `okf.import_bundle` (an `ImportGate`). `origin`/`channel` cross the seam as strings and are converted to the guard's enums HERE, because the guard derives trust from `origin` by enum identity: a value it does not recognise would arrive as a plain string, miss the identity check, and be silently classified untrusted. Refusing an unrecognised value is not a trust decision, it is a refusal to guess at one — the same code Door C's own validation raises. `reasons` is DERIVED from each concept's scan findings, not carried verbatim: the guard's per-concept `stamp_concept` keeps the disposition and drops the reason strings that produced it, so the findings are the audit trail actually available at this seam. """ try: guard_origin = guard_okf.Origin(origin) guard_channel = guard_okf.Channel(channel) except ValueError as exc: raise MaterializationError( f"origin={origin!r} channel={channel!r} is outside the guard's vocabulary — " "refusing to carry a provenance declaration it would not recognise " "(it decides trust from these values)", code="import_provenance_invalid", ) from exc result = guard_okf.import_bundle(dict(bundle), origin=guard_origin, channel=guard_channel) concepts = tuple( ImportDecision( path=str(concept.path), disposition=str(concept.disposition.value), error=None if concept.error is None else str(concept.error), reasons=tuple( f"{finding.severity.value}:{finding.label}" for finding in concept.report.findings ), ) for concept in result.concepts ) return BundleDecision(concepts=concepts, log=str(result.log()))