feat(guard): wire Doors B and C to the real guard (Phase 2 step 4)

Adds llm_ingestion_guard>=0.2,<0.3 as this library's first and only runtime
dependency, and guard_adapter.py -- the one module that imports it. The flows
themselves are unchanged: they still take an injected gate, and importing the
package still does not import the guard, so a Door A consumer is unaffected by
the dependency's state.

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 -- the verdict is then a statement about the bytes
actually written. This supersedes the plan's "bookends" wording, recorded
there under "Settled during implementation (step 4)".

It follows that the gate refuses rather than repairs: a file carrying an
invisible carrier is rejected, not stripped and persisted. Sanitizing first
would write a document differing invisibly from the operator's file while
source_sha256 still points at the original bytes. Operator decision; the
policy is PRESET_USER_UPLOAD, so any finding at all is held back.

Door C hands the bundle over whole to okf.import_bundle, which resolves the
cross-link graph across concepts. Per-concept reasons are derived from the
scan findings (severity:label) because stamp_concept keeps the disposition and
drops the reason strings behind it.

Assumption B1 closes as a signature smoke test over what the adapters actually
call -- screen_output and okf.import_bundle signatures, the Disposition values
both doors compare by value, the Origin/Channel vocabularies Door C validates,
the result fields read, and the upload preset's shape. prepare_input is not
pinned: drift there cannot reach this library. Behaviour is pinned against the
real scanner too, including the persist-gate proof that a fail-secure fixture
leaves the bundle byte-identical.

B2 closes with it: git+https tag install over anonymously readable HTTPS, no
credential. A direct reference is an install-time channel, not the pin -- the
range stays in pyproject, is satisfied by the tag install today, and resolves
normally once the package index exists. A packaging test enforces that the
guard remains the only runtime dependency (verified by hand-mutation).
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 07:32:45 +02:00
commit 241e00f27a
8 changed files with 578 additions and 38 deletions

View file

@ -5,24 +5,28 @@ deterministic materialization -> index), a bundle inbox converting common
file types to OKF concepts, and import of external OKF bundles.
Security is owned by llm-ingestion-guard, never reimplemented here. Note
what that does and does not mean today: Door A is UNGATED. It has zero
runtime dependencies and calls no guard function on the way to disk. A
caller that materializes external or otherwise untrusted content is
responsible for gating it -- via guard's okf.import_bundle for received
bundles, or prepare_input/screen_output around extracted text. Do not read
"security is delegated" as "safe by default": materialize_bundle writes
what it is given. The guard-calling persist gates arrive with Doors B and C.
what that does and does not mean today: Door A is UNGATED. It calls no guard
function on the way to disk, so a caller that materializes external or
otherwise untrusted content is responsible for gating it. Do not read
"security is delegated" as "safe by default": materialize_bundle writes what
it is given.
Doors B and C are gated by an INJECTED adapter, and `guard_adapter` is the
one this library ships over the real guard -- the only module here that
imports it. Importing this package does not import the guard; a Door A
consumer keeps working whatever state the dependency is in.
Door A (spec-based ingestion) public surface: materialize_bundle plus the
typed error hierarchy rooted in IngestError.
Door B (bundle inbox) public surface: process_inbox plus its result types.
Its persist gate is INJECTED -- the caller supplies a Gate adapter over
llm-ingestion-guard and process_inbox obeys the verdict; the library imports
no guard function itself and makes no security decision of its own.
llm-ingestion-guard (guard_adapter.inbox_gate is the shipped one) and
process_inbox obeys the verdict, making no security decision of its own.
Door C (external bundle import) public surface: import_bundle plus its result
types. Its gate is injected the same way, over the guard's okf.import_bundle:
types. Its gate is injected the same way, over the guard's okf.import_bundle
(guard_adapter.import_gate is the shipped one):
the caller declares origin and channel at the door, the gate assesses every
concept, and only concepts clearing the non-blocking floor are merged. Merged
concepts are written verbatim -- ownership is proven by content identity