portfolio-optimiser-claude/docs/2026-07-16-llm-ingestion-guard-plan.md
Kjell Tore Guttormsen a7e8ffecb8 chore(deps): re-pin llm-ingestion-okf to v0.3.1 + migrate tests to stable error codes
Pin dae0bd1a -> v0.3.1 (=692f2df) on the public Forgejo mirror; uv.lock pins
the exact commit behind the tag.

- Drop the mypy override: the library ships py.typed from v0.2.0, so strict
  mode now follows its real types instead of follow_untyped_imports.
- Migrate 8 library-error assertions from pytest.raises(match=...) to
  exc.value.code — message text is explicitly unstable from v0.3.0, the
  codes are the stability contract.
- Fix a real breakage the bump surfaced: IngestResult gained a required
  `stamp` field (d3a3bcc), which the delegation fake did not construct.
- The read-only SQL test loses resolution under the code contract
  (`sql_failed` is generic), so it now proves read-onlyness by effect —
  the write never lands — instead of by message wording.
- Correct the guard plan: G1's persist-gate anchor (ingest.py:372-387) died
  with the 2026-07-16 adoption. Door A is ungated by the library's own
  README, so gating stays our responsibility at the call site.

Verified: 426 tests green, golden output byte-exact unchanged, full gate
clean (ruff + format + mypy strict).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RmNAgbRXUgvoSKxVK4Bevv
2026-07-20 07:22:09 +02:00

132 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plan — når/hvor `llm-ingestion-guard` wires inn i dette repoet
> **Status:** PLAN (ikke wiret). Forfattet 2026-07-16 etter guard-repoets adopsjons-brief
> (`~/repos/llm-ingestion-pipeline-security/docs/ADOPTION-BRIEF.md`, guard `v0.2` alpha).
> Dette dokumentet KLASSIFISERER repoets ingest-flater mot briefens §7-checklist og fastsetter
> NÅR/HVOR guarden skal wires — det IMPLEMENTERER ingenting. Maskinlesbar status i `STATE.md`
> (markørlinjen `llm-ingestion-guard: planned — …`).
>
> Ærlighets-regelen (metode-spec §1) gjelder også her: en grønn guard-scan er IKKE «trygt».
> Guarden er write-time defense-in-depth FORAN validator/checker/kostbaseline — ikke en erstatning.
## 1. Hvorfor dette repoet er i målgruppen
Guarden hardner **write-time**-formen: untrusted innhold som flyter gjennom et
enrichment-/ekstraksjons-steg og persisteres i en artefakt en *downstream* agent senere leser
som **betrodd kontekst**. Dette repoet ER den formen:
- Ingest-laget materialiserer en **OKF-bundle** (markdown-konseptfiler + `index.md`) —
`ingest.py:348-391 materialize`.
- Loopens Steg 1 navigerer bundelen og renderer den som **read-context** rett inn i
hypotese-/debatt-prompten — `okf.py:122-134 bundle_context``loop.py:119-133`
(`context` i proposer/checker-promptene). Bundelen leses altså som betrodd kontekst.
- Den lange lærings-sløyfa (Steg 7) folder ekspert-**verdikter** inn i samme prompt:
`experience.py:157-171 fold_experience` bygger linjer `- {id} [{decision}]: {rationale}`
`decision` og `rationale` når prompten **verbatim** (`experience.py:3,6`).
Bundelen/verdikt-storen er trust-grensen: en forgiftet konsept- eller verdikt-linje skrevet
ved write-time leses senere som betrodd. Provenansen er kun kjent ved write-gaten.
## 2. Trust-grense-kartlegging (briefens §7 mot dette repoet)
| # | Inngang | Klassifisering | Belegg (verifisert) | Guard-relevans |
|---|---|---|---|---|
| 1 | `ingest.py` **file**-kilde (lokal CSV under `root`) | **Førstepart** i D7-scope | Operatør peker manifest mot lokal CSV; boundary-sjekket fail-closed (`ingest.py:264-276`); **null modellkall, ingen nett** (`ingest.py:1-6`); celler rendres **verbatim** (`ingest.py:164-185`) | Ikke mål *som scopet*. **Forbehold (trust følger opphav, ikke kanal):** hvis CSV-radene selv er eksternt hentet, blir cellene en untrusted bærer inn i Steg-1-prompten |
| 2 | `ingest.py` **sql**-kilde (lokal sqlite, read-only) | **Førstepart** i D7-scope | `connection_ref` → env, read-only URI, verbatim typing (`ingest.py:230-268`) | Samme som (1): opphavs-forbeholdet gjelder |
| 3 | `ingest.py` **http**-kilde | **DORMANT untrusted-sti** (avvist i dag) | `type:"http"` avvises fail-fast av discriminated union (`ingest.py:129`) — bevisst ikke bygd (`extending.md:128-160`; sesjonsplan §4). Ingen egress-call-sites i `src/` (grep: 0 treff) | **DEN avgjørende untrusted-grensen** når den går live — auto-fetch av web/vendor-innhold → materialiseres i bundelen → leses av loopen. Guardens hovedplass |
| 4 | `inbox.py` verdikt-inbox | **Live injeksjonsflate, men tiltenkt førstepart** | `decision` valideres kun som ikke-tom streng; ingen sjekk på merge-stien (`inbox.py:102-132`); `rationale` verbatim til prompten (§1). C-F7: kjørt bevis at injeksjonstekst når prompten. **Ennå ikke wiret til noen kjøresti** (C-N2, `merge_inbox_into_store` kalles fra ingen leverbar sti) | Depth-komplement til den alt planlagte C2.5-herdingen — ikke primærbehovet (forfatteren er førstepart; C2.5-allowlist lukker den kjørte injeksjonen) |
| 5 | Mottatt tredjeparts **OKF-bundle** | **Ikke en sti i dag** | Repoet PRODUSERER sin egen bundle (ingest); ingen `import_bundle`-inngang finnes | `okf.import_bundle` er det eksakte verktøyet HVIS en bundle-utvekslings-sti åpnes (delt D-E: klynge/flerbruker) |
| 6 | Kuraterte / promoterte konseptfiler | **Førstepart** | Håndforfattet (golden) / gated promoterings-gate (`promotion.py`) | Utenfor guardens trusselmodell (in-place trusted-author-edits, brief §7) |
**LLM-steget:** i dag sitter modell-steget (loop) MELLOM bundelen og *proposal-output* — ikke
mellom en untrusted kilde og *storen*. Ingest→bundle er **deterministisk** (null modellkall).
Den klassiske «untrusted → LLM-enrich → persist»-formen oppstår FØRST når http-kilden (rad 3)
går live. Det er derfor guarden er «når, ikke hvis».
## 3. Klassifisering + status: **`planned`**
- Repoet HAR en OKF-bundle-store som leses som betrodd (rad 12, 6 mater den; loop leser den).
- Ingen **live** untrusted-ingest-sti i en leverbar kommando i dag: http er avvist fail-fast og
bevisst utsatt (rad 3); inbox-injeksjonen er reell men tiltenkt førstepart OG ikke wiret til
noen kjøresti ennå (rad 4 / C-N2).
-**Ingenting å integrere nå**, men klart applikabelt. `not-applicable` ville vært feil
(repoet ER en OKF-second-brain med en designet-inn untrusted utvidelse + en kjørt-bevist
inbox-injeksjonsflate). `integrated` ville vært feil (ingen wiring; briefen ber oss ikke
implementere).
## 4. Integrasjonspunkter (persist-gaten) + minimal wiring — NÅR
Tre trigger-gatede wiringer, hver forankret i en eksisterende roadmap-beslutning:
### G1 — http-kilden går live · gate: D-B-utvidelse (i dag IKKE på D7-roadmap)
- **Persist-gate:** MELLOM http-fetch og skriving av `ingest-{id}.md`.
- **KORRIGERT 2026-07-20 (pin-bump til v0.3.1):** den opprinnelige forankringen
(`_read_extraction`/`materialize` i `ingest.py:372-387`) er DØD — adopsjonen 2026-07-16
flyttet den koden til `llm-ingestion-okf`; lokal `ingest.py` er nå en 63-linjers adapter.
Biblioteket bekrefter selv at dør A er **ugatet** («What is gated today: nothing» — README):
null runtime-avhengigheter, ingen guard-kall før skriving. Gating er derfor VÅRT ansvar på
kallstedet. To mulige forankringer når http går live: (a) wrap i vår egen seam
(`ingest.materialize`) rundt bibliotekkallet, eller (b) løft gaten inn i biblioteket via
operatør-køen. Valget er IKKE tatt — det hører til D-B.
- **Minimal wiring:** ingest har intet modell-steg, så bookend-formen kollapser til
sanitér+scan over den hentede ekstraksjons-kroppen:
`prepared = prepare_input(fetched_body)` → deterministisk render →
`screen_output(rendered, PRESET_…)`; `FAIL_SECURE`**ikke** skriv konseptfila, alert med
gate-kode (`decision.reasons`), ingen innhold i alert. Provenance-frontmatteret (§5) bærer
allerede `source_system`/`ingested_at` — behold som er.
- **Dette er guardens hovedplass.** Utsettes nøyaktig så lenge http er utsatt.
### G2 — inbox-herding · gate: sesjon C2.5 (byggbar nå) — DEPTH, ikke påkrevd
- **Persist-gate:** `merge_inbox_into_store`/`load_inbox`, FØR `store.add` (`inbox.py:122-132`).
- **C2.5s primære fiks** (sesjonsplan §2) = vokabular-allowlist {approved, rejected,
approved_with_adjustment} + tak på rationale-lengde/antall. Den lukker den kjørte C-F7-
injeksjonen alene.
- **Guard-komplement (valgfritt depth):** `scan_output(rationale + " " + decision,
PRESET_USER_UPLOAD)` over fritekst-`rationale` (injeksjons-bæreren). MÅ beholde inboxens
tolerante semantikk: scan-treff → **SKIP den verdikten** (aldri raise) — konsistent med
`load_inbox`s skip-never-raise (`inbox.py:102-119`). Merkes «vurder ved C2.5», ikke blokkerende.
### G3 — mottatt tredjeparts OKF-bundle · gate: delt D-E (klynge/flerbruker) eller enhver receive-bundle-sti
- **Wiring:** `okf.import_bundle(bundle, origin=Origin.EXTERNAL, channel=…)`; per-konsept
`FAIL_SECURE`/`c.error` → **ikke** merge det konseptet. `result.links` = kryss-lenke-graf
(dormant-injeksjons-signal).
- **Trigger:** hvis/når en kryss-prosjekt-bundle-utveksling åpnes (delt D-E-retning). Ikke en
sti i dag.
## 5. Forutsetninger å avklare FØR en wiring
- **Guard er `v0.2` alpha** — public API kan endres. Ved wiring: pin + versjonsvakt, SAMME
disiplin som SDK-pinnen (C-N3/C2.5): `>=0.2,<0.3` + test som tvinger re-verifisering ved bump.
- **Offline/no-egress-invarianten består:** guard-core er stdlib-only, gjør ingen nettkall,
parser ingen filer (brief §6) → «testsuiten kjører uten nøkkel og uten nettverk» (S5-invariant)
holder. Verifiseres eksplisitt ved wiring (guard-core i `[dev]`/core, ikke `[ml]`/`[judge]`).
- **Ærlighets-grensene (brief §8) som betyr mest for et kostbesparings-wiki:**
*semantisk/faktisk poisoning er usynlig* for lexicon+entropi — feil join-path/metrikk/tall
passerer rent. For dette repoet er nettopp plausible-men-feil `claimed_saving_nok`/koder den
farligste klassen; guarden fanger den IKKE. Det er validatorens/checkerens/kostbaselinens
(C4.0) jobb. Guarden legges FORAN dem, ikke i stedet for.
## 6. Hva som IKKE gjøres nå (per inclusion-prompt)
- Ingen guard-avhengighet i `pyproject.toml`, ingen import, ingen wiring, ingen kode.
- http-kilden forblir avvist fail-fast (uendret).
- Kun: dette plandokumentet + markørlinjen i `STATE.md`.
## 7. Verifisering NÅR en wiring engang skjer (ikke nå)
- `python -m llm_ingestion_guard.coverage` exit 0 før man stoler på guarden (brief §5).
- **Load-bearing test (§11):** detach guard-gaten → en injeksjons-fixture MÅ da nå
bundelen/storen → testen RØD. Grønn-men-død er feilmoden.
- Full gate grønn (`pytest`/`ruff`/`mypy`); golden-bytene uendret; offline-invarianten består
(ingen nettkall introdusert i suiten).
## 8. Sekvens-forankring
1. **G2** vurderes SAMMEN med C2.5 (byggbar nå) — men C2.5-allowlist er den nødvendige delen;
guard-depth er additiv.
2. **G1** materialiseres når/hvis D-B åpner en live-kilde-utvidelse som inkluderer http
(i dag eksplisitt IKKE på D7-roadmap, `extending.md:128-160`).
3. **G3** materialiseres hvis delt D-E-retning åpner bundle-utveksling.
Ingen av de tre er live i dag → status `planned`. Oppdater markørlinjen til `integrated` når
en av dem faktisk wires (og legg load-bearing detach-beviset ved).