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
|
|
@ -65,6 +65,44 @@ No scanning, sanitizing, or quarantine logic is implemented here.
|
|||
(reserved-file policy differs per consumer and becomes configurable in
|
||||
Phase 3).
|
||||
|
||||
### Settled during implementation (step 4)
|
||||
|
||||
- **Door B calls `screen_output` alone, and this supersedes deliverable 3's
|
||||
"`prepare_input`/`screen_output` bookends".** The bookends assume a model
|
||||
call between them; 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. The adapter therefore screens the extracted
|
||||
text as it stands and persists that same string, so the verdict is a
|
||||
statement about the bytes actually written.
|
||||
- **The gate refuses; it does not repair.** Sanitizing before persisting
|
||||
would write a document differing invisibly from the operator's file while
|
||||
`source_sha256` still points at the original bytes. A file carrying an
|
||||
invisible carrier is rejected instead — the guard's own doctrine is that a
|
||||
carrier has no legitimate place in a reference file, and this library's
|
||||
posture everywhere else is fail-fast, never repair. Operator decision.
|
||||
- **The policy is `PRESET_USER_UPLOAD`** (untrusted tier, quarantine floor):
|
||||
an inbox drop is an untrusted upload, so any finding at all is held rather
|
||||
than written. A caller needing another tier injects their own adapter.
|
||||
- **`guard_adapter.py` is the only module that imports the guard**, and the
|
||||
package `__init__` does not import it, so a Door A consumer's import path
|
||||
is unaffected by the dependency's state.
|
||||
- **Assumption B1 pins what the adapters call, not the whole guard.**
|
||||
`prepare_input` is dropped from the smoke test: drift there cannot reach
|
||||
this library. What is pinned: `screen_output` and `okf.import_bundle`
|
||||
signatures, the `Disposition` values both doors compare against, the
|
||||
`Origin`/`Channel` vocabularies Door C validates, the result fields the
|
||||
adapters read, and the upload preset's shape.
|
||||
- **The pin stays a range; the git URL is an install channel.** A PEP 508
|
||||
direct reference pins one tag and cannot express `>=0.2,<0.3`, but it is an
|
||||
install-time channel rather than a dependency declaration: the range is
|
||||
what `pyproject.toml` carries, it is satisfied by the tag install today,
|
||||
and it resolves normally once the package index exists (confirmed by the
|
||||
guard repo, superseding an earlier reading that the range had to go).
|
||||
- **Door C reasons are derived, not carried.** The guard's `stamp_concept`
|
||||
keeps a concept's disposition and drops the reason strings behind it, so
|
||||
the adapter reports `severity:label` per finding — the audit trail actually
|
||||
available at that seam.
|
||||
|
||||
### Settled during implementation (step 5)
|
||||
|
||||
- **Verbatim merge, no frontmatter of ours.** A merged concept is written
|
||||
|
|
@ -128,8 +166,8 @@ No scanning, sanitizing, or quarantine logic is implemented here.
|
|||
|
||||
| # | Assumption | Test |
|
||||
|---|---|---|
|
||||
| B1 | Guard 0.2 API matches the pinned surface (`prepare_input`, `screen_output`, `okf.import_bundle`, disposition enum) | Import-and-signature smoke test that fails on upgrade drift |
|
||||
| B2 | Guard is installable where this library's CI runs | Resolve the distribution channel with the operator before implementation starts (risk: not yet decided) |
|
||||
| B1 | Guard 0.2 API matches the pinned surface (`screen_output`, `okf.import_bundle`, disposition enum, origin/channel vocabularies) | Import-and-signature smoke test that fails on upgrade drift (`tests/test_guard_adapter.py`) — CLOSED at step 4 |
|
||||
| B2 | Guard is installable where this library's CI runs | CLOSED: git+https tag install over anonymously readable HTTPS, no credential; Forgejo package index becomes the durable channel later without a pyproject edit |
|
||||
| B3 | `html.parser`-based extraction is adequate for v1 | Golden fixtures for representative HTML; anything richer is explicitly out of scope |
|
||||
| B4 | Guard verdicts are deterministic for fixed input + version | Same fixture run twice → identical `InboxResult` |
|
||||
|
||||
|
|
@ -156,4 +194,5 @@ No scanning, sanitizing, or quarantine logic is implemented here.
|
|||
5. Phase 1 golden suite still passes byte-for-byte (no regression from reuse).
|
||||
6. Grep-gate: `grep -rn "sanitize\|quarantine\|lexicon" src/` shows no local
|
||||
security reimplementation (guard imports only).
|
||||
7. `pyproject.toml` runtime dependencies == exactly `llm-ingestion-guard>=0.2,<0.3`.
|
||||
7. `pyproject.toml` runtime dependencies == exactly `llm-ingestion-guard>=0.2,<0.3`
|
||||
(automated: `test_the_only_runtime_dependency_is_the_security_boundary`).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue