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:
parent
f10fc60de2
commit
241e00f27a
8 changed files with 578 additions and 38 deletions
60
README.md
60
README.md
|
|
@ -6,9 +6,9 @@ Status: phase 1 (spec-based ingestion) is implemented — manifest validation,
|
|||
the `file`/`sql`/`http` connectors, deterministic materialization, index
|
||||
generation, and the golden fixture suite under `examples/`. Phase 2 is in
|
||||
progress: the bundle inbox (`process_inbox`) and external-bundle import
|
||||
(`import_bundle`) are implemented against an **injected** persist gate; the
|
||||
integration with the real guard is not done yet (see below). Phases 3–4 are
|
||||
planned (see `docs/plan/`).
|
||||
(`import_bundle`) are implemented against an **injected** persist gate, and
|
||||
`llm_ingestion_okf.guard_adapter` wires that gate to the real guard (see
|
||||
below). Phases 3–4 are planned (see `docs/plan/`).
|
||||
|
||||
## Planned scope (v1)
|
||||
|
||||
|
|
@ -45,27 +45,40 @@ No security functionality is reimplemented here.
|
|||
|
||||
### What is gated today: read this before trusting a door
|
||||
|
||||
The package still has **zero runtime dependencies** and imports no guard
|
||||
function anywhere. That has a direct consequence for what "gated" means here:
|
||||
|
||||
- **Door A (`materialize_bundle`) is ungated.** It calls nothing before
|
||||
writing to disk and writes what it is given.
|
||||
writing to disk and writes what it is given. A caller materializing
|
||||
untrusted content is responsible for gating it.
|
||||
- **Doors B and C (`process_inbox`, `import_bundle`) gate through an adapter
|
||||
you supply.** Each takes a `gate` argument; the flow hands it the content
|
||||
you pass in.** Each takes a `gate` argument; the flow hands it the content
|
||||
and obeys the verdict, refusing to persist anything that does not clear the
|
||||
guard's non-blocking floor — including a disposition it does not recognise,
|
||||
and (at Door C) a concept the gate returned no verdict for. What it cannot
|
||||
do is check that your adapter is a real guard. A permissive stub approves
|
||||
do is check that your adapter is a real guard: a permissive stub approves
|
||||
everything, and the flow will believe it.
|
||||
|
||||
So gating remains **your** responsibility at the call site: pass an adapter
|
||||
over `prepare_input`/`screen_output` (Door B) or `okf.import_bundle`
|
||||
(Door C). Wiring the doors to the real guard, and pinning it as a dependency,
|
||||
is step 4 of phase 2 and is not done yet.
|
||||
`llm_ingestion_okf.guard_adapter` is the adapter over the real guard, and the
|
||||
only module here that imports it — importing the package itself does not:
|
||||
|
||||
This is stated plainly because earlier wording ("calls the guard at every
|
||||
persist gate") described the intended end state in the present tense, and a
|
||||
consumer reasonably read it as safe-by-default.
|
||||
```python
|
||||
from llm_ingestion_okf import process_inbox
|
||||
from llm_ingestion_okf.guard_adapter import inbox_gate
|
||||
|
||||
result = process_inbox(inbox_dir, bundle_dir, "2026-07-25T12:00:00Z",
|
||||
okf_type="reference", gate=inbox_gate)
|
||||
```
|
||||
|
||||
Two properties of that adapter are worth knowing before you rely on it.
|
||||
It screens the **exact bytes it persists** — the guard's `prepare_input`
|
||||
bookend prepares text for a model call, which this library never makes, so
|
||||
only `screen_output` is used and the screened string is the written string.
|
||||
And it **refuses rather than repairs**: a file carrying an invisible
|
||||
zero-width or bidi character is rejected, not silently stripped and written.
|
||||
Door B screens under the untrusted-upload policy, so any finding at all is
|
||||
held back rather than persisted.
|
||||
|
||||
This section is stated plainly because earlier wording ("calls the guard at
|
||||
every persist gate") described the intended end state in the present tense,
|
||||
and a consumer reasonably read it as safe-by-default.
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
|
@ -98,8 +111,19 @@ verification criteria:
|
|||
|
||||
## Requirements
|
||||
|
||||
Python 3.10+. The core has zero runtime dependencies. The planned Node half
|
||||
targets Node/ESM with zero npm dependencies.
|
||||
Python 3.10+, and exactly one runtime dependency — the security boundary,
|
||||
`llm-ingestion-guard>=0.2,<0.3`. Everything else is stdlib. Until that
|
||||
package is published to an index, install it from its tag:
|
||||
|
||||
```
|
||||
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v0.2.0"
|
||||
```
|
||||
|
||||
A git URL is a PEP 508 direct reference and pins one exact tag, so it is an
|
||||
install-time *channel*, not the pin: the range above stays the declared
|
||||
dependency and resolves normally once the package index exists. The optional
|
||||
`[extract]` extra (pdf/docx/xlsx parsers) is not populated yet. The planned
|
||||
Node half targets Node/ESM with zero npm dependencies.
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue