# Inkluderingsplan — `llm-ingestion-guard` som write-time persist-gate > **⚠️ UTDATERT PÅ TO PUNKTER — REALISERT 2026-08-09 av > [innholdsgate-planen](2026-08-09-innholdsgate-og-aerlighet.md) (P2/S1.b).** Dokumentet beholdes > som kartleggingen den var; disse to påstandene skal IKKE leses som gjeldende: > > 1. **«Guard-modenhet: `v0.2` (alpha)» er FEIL** (målt 2026-08-09: syv publiserte tagger, siste > `v0.3.4`, `dependencies = []`). Det premisset gjorde adopsjonen urimelig risikabel og er > grunnen til at innholdsgate-planen ble skrevet. Adoptert pin: `v0.3.4`. > 2. **Gate-plasseringen i denne planen er FEIL.** Den plasserer gaten i `ingest.materialize`, > «repoets ene skrivepunkt». Målt er `materialize` en ren delegasjon til det pinnede > `llm_ingestion_okf`-bibliotekets `materialize_bundle`, som stager i minnet og skriver selv — > ingen callback mellom staging og disk. Gaten bor derfor i `materialize_gated` > (kopi → materialiser → skann → publiser eller avvis), ikke der denne planen sier. > > **Status:** PLAN (ikke implementert). Denne økta KARTLA repoets untrusted-ingest og la denne > planen; guarden er IKKE wiret inn ennå (per adopsjons-briefens §7: «kun planlegg og oppdater > planene»). Utløst av konsument-inkluderings-prompten fra `llm-ingestion-pipeline-security`. > **Guard-modenhet:** `v0.2` (alpha) — stdlib-only kjerne, framework-agnostisk, offentlig API kan > fortsatt endres. Pin ved adopsjon. > > **Kildepekere:** guardens adopsjons-brief (open-remote: > `open/llm-ingestion-pipeline-security/docs/ADOPTION-BRIEF.md`). Guarden er allerede anerkjent i > repoets veikart: [sesjonsplan Fase 2–6](2026-07-10-sesjonsplan-fase2-6.md) D-G («Guard forblir > eget sikkerhets-repo (avhengighet)», linje 120) og [toolkit-repo-brief](2026-07-14-toolkit-repo-brief.md) > §5/§8 («migreres IKKE; forblir eget repo … fabrikkens sikkerhetsvask krever `llm-ingestion-guard` > som publisert»). --- ## 1. Hva guarden er, og hvorfor den er relevant HER `llm-ingestion-guard` er **write-time**-søsknet til query-time chatbot-guardrails. Den sitter ikke mellom bruker og modell ved spørring, men herder den andre formen: **untrusted innhold som flyter gjennom et LLM-berikelses-/ekstraksjons-steg inn i et persistert, downstream-lest artefakt** — en RAG-base, kunnskapsbase, wiki eller **OKF-bundle**. Dette treffer arkitekturen vår direkte: `ingest.py` materialiserer eksterne kilder inn i OKF-bundles, og optimiser-loopen konsumerer bundelen **uendret** som betrodd agent-kontekst (`bundle_context`). En forgiftet konseptfil skrevet ved *write*-tid leses senere av forslagsstiller-agenten som **betrodd kontekst** — agentens (framtidige) query-time-guardrail ser aldri hvor konseptet kom fra. Write-gaten er det ENESTE stedet provenansen fortsatt er kjent. OKF har ingen skjema-registry, ingen sentral autoritet og ingen signering — en mottatt bundles påståtte opphav er ikke verifiserbart på format-nivå. Guarden gjør aldri modellkallet selv (relevant for kostdisiplinen: null modellkall, stdlib-only, ingen nett). Den gir de to bibliotek-halvdelene rundt vår egen tool-less transform (`prepare_input` / `screen_output`), pluss en OKF-adapter for hele bundles (`okf.import_bundle`). ## 2. §7-checklist scoret for dette repoet | §7-kriterium | Repoet | Kilde | |---|---|---| | Persisterer LLM-beriket/eksternt-mottatt innhold i en store en *downstream* agent leser som betrodd | **JA** | `ingest.materialize` → OKF-bundle → `bundle_context` (run.py:302); verdict-inbox → `store.add` → `ExpeLContextProvider` (run.py:287-290) | | **Minst én ingest-sti tar UNTRUSTED innhold** (ekstern URL / opplastet fil / mottatt bundle / auto-hentet web) — *den avgjørende* | **JA** | `http`-kilde (I6) auto-henter web/vendor-innhold (`read_http`/`_urllib_get`, ingest.py); verdict-inbox er «inbox som tar imot eksterne drops» (S2.5/F7/F11) | | Et LLM-steg (summarize/extract/classify/rewrite) mellom untrusted kilde og store | Delvis — LLM-steget leser bundelen *downstream* (ingest er deterministisk, pre-loop); verdict-inbox injiseres direkte i agent-instruksjoner | ingest.py §1-docstring; verdicts.py:299-311 | | Ønsker **fail-secure** (stopp før persist) framfor best-effort med stille commit | **JA** — hele repoets kontrakt er fail-fast/fail-closed (validator obligatorisk-blokkerende; `allow_network`; verdict-lag reservert) | CLAUDE.md-invarianter | **Konklusjon: den avgjørende betingelsen holder → guarden er `applicable` (marker = `planned`).** Ærlig motargument (vurdert, forkastet): man kunne hevde `not-applicable` fordi de eneste kildene som kjøres i praksis i dag er operatør-forfattet lokal data (`reference_projects.json`, lokale CSV-er). Men (a) `http`-konnektoren (I6) FINNES i koden og ER en untrusted auto-fetch-sti, og (b) verdict-inboxen er eksplisitt anerkjent som en injeksjonsflate i veikartet (S2.5: «verdict-inboxen slutter å være en uvalidert injeksjonsflate»). Derfor er `planned` den ærlige klassifiseringen — ikke `not-applicable`, og ikke `integrated` (ingenting er wiret ennå). ## 3. Untrusted-grenser vs first-party **Untrusted-grenser (guarden hører hjemme her) — persist-gater rangert etter eksponering:** | # | Persist-gate | Fil | Hva krysser inn | Nåværende delforsvar | Gap guarden lukker | |---|---|---|---|---|---| | 1 | `ingest.materialize` / `read_http` | ingest.py:472 (http-gate 506-511) | Ekstern kilde (http/sql/csv) → `ingest-{id}.md` i bundelen → betrodd kontekst | `allow_network`-egress-gate, `safe_resolve`-baneinnesperring, kodefence-marker-avvisning, tabellcelle-escaping, verdict-lag-reservasjon | Ingen **carrier-strip** (zero-width/BIDI/Unicode-tag/HTML-comment/`data:`), ingen **lexicon/entropy/active-content-scan** av rendret kropp, ingen EchoLeak markdown-bilde/lenke-deteksjon. En zero-width-kodet instruksjon i en CSV-celle overlever i dag verbatim. | | 2 | `load_verdicts_from_dir` → `store.add` | verdicts.py:181, kalt run.py:287-290 | Eksternt-droppet JSON (`id`+`rationale` verbatim) → store → injisert i agent-instruksjoner via `ExpeLContextProvider` | Tolerant skip, `decision`-vokabular (`{approved,rejected}`), `max_rationale_len`, `max_files` fail-fast (S2.5) | Ingen innholds-scan av `rationale`/`description` før den blir few-shot. Høyest frekvens i normal-loopen. | | 3 | `promote_verdict` → `okf.write_concept_file` | verdicts.py:513 (skriv 563-564) | Godkjent dom → `type: verdict`-konseptfil i wikien → betrodd av ALLE framtidige kjøringer | Fail-closed HITL-gate (`PromotionRefused`, kun `approved`/`approved_with_adjustment`), nøytral index-label, `_safe_filename_token` | Innholdet stammer fra (untrusted) inboxen; guarden = defense-in-depth (scan `rationale`/`description` FØR permanent betrodd kontekst). Størst blast-radius. | | 4 | Mottatt OKF-bundle (`okf.import_bundle`-mode) | *finnes ikke i dag* | En hel tredjeparts-bundle merges | — | Roadmapen ÅPNER den: D-H demo-sti («unzip energi-eksempel i bundle-innboks»), D7-portabilitet, toolkit-fabrikk. «Når, ikke hvis»: bruk `okf.import_bundle(origin=EXTERNAL)` per-konsept-gate når en received-bundle-sti legges til. | **First-party (guarden gjelder IKKE):** - **`datasource.py` / `reference_domain.py`** — operatør/syntetisk data, JSON-Schema-validert fail-fast. Deployerens egen kontekst. - **`file`/`sql`-kilder mot operatørens egen lokale katalog** — first-party *etter opphav*. Tillit følger dataens opphav, ikke innsettings-kanalen: en CSV lastet ned fra en vendor er fortsatt ekstern og hører under gate 1. Deployeren eier klassifiseringen (jf. «rent teknisk rammeverk»-invarianten). - **`write_verdict`** (verdicts.py:161) — forfatter-primitiv; tillitsgrensen er *lastingen* (gate 2), ikke skrivingen. - **`outbox.write_outbox`** — systemets EGET output-lag (må aldri re-ingesteres som inbox, run.py:261-263). - **`persona.load_persona_example`** (persona.py:47) — first-party fil-les; men NB: i simuleringen forplanter den seg gjennom `promote_verdict` inn i wiki/ExpeL-kjeden — dekkes indirekte av gate 3. - **Optimiser-loopens interne resonnement** (proposer/debatt/validator) — intet eksternt innhold hentes; resonnement over allerede-materialisert bundle-kontekst. ## 4. NÅR — innplassering i veikartet Guarden er en **persist-gate**, så den skal wires **før den første untrusted-ingest-stien går live**, festet der beriket/mottatt innhold committes. Veikartet gir tidspunktet eksplisitt: - **Hard grense: FØR M3** (Fase 6-pilot). M3 er der «én ekte kilde … herdet og materialisert med ekte provenance; én ekte fagekspert onboardet på inbox/outbox-kontrakten» — dvs. første ekte untrusted-ingest (gate 1) OG første ekte inbox-drop (gate 2) samtidig. Guarden MÅ stå før M3. - **Naturlige wiring-økter (utvider eksisterende steg, ingen ny fase):** - **Gate 1 → utvid S2.4** («Konnektor-herding mot mock»): S2.4 herder allerede http-konnektoren mot injisert transport. Legg guardens `sanitize` + `scan_output` på rendret ekstraksjons-kropp der. - **Gate 2 → utvid S2.5** («Inbox-herding»): S2.5 gjør inboxen til «ikke lenger uvalidert injeksjonsflate» med vokabular + lengde-tak. Guardens innholds-scan av `rationale`/`description` er den manglende brikka. - **Gate 3 → ny opt-in ved `promote_verdict`**: defense-in-depth-scan, opt-in (speiler at `promote_verdict` selv er opt-in, ikke wiret i `run_project`). - **Gate 4 → når en received-bundle-sti faktisk legges til** (D-H demo-sti / toolkit-fabrikk): `okf.import_bundle`. - **Avhengighet allerede erklært:** guarden er «eget sikkerhets-repo (avhengighet)» (D-G) og toolkit-fabrikkens «sikkerhetsvask krever `llm-ingestion-guard` som publisert» (toolkit-brief §8). Denne planen KONKRETISERER hvor i DETTE repoet den avhengigheten lander. Rekkefølge-anbefaling: gate 2 (S2.5) og gate 1 (S2.4) først (de er allerede planlagte herdings-økter i Fase 2-bolk B, gated på D-B), gate 3 som liten opt-in når som helst, gate 4 kun når received-bundle åpnes. Ingen av dem er på kritisk sti før M3-forberedelsen begynner. ## 5. HVOR + minimal wiring (per gate) Skjematisk (adopsjons-briefens §2/§4 API; IKKE implementer nå): **Gate 1 — `ingest.materialize` (staging-loopen, før `_write_bytes`):** ``` prepared = prepare_input(raw_source_text) # carrier-strip + fence FØR render # ... eksisterende render_table / _render_fenced_block på prepared ... decision = screen_output(rendered_body, PRESET_*) # scan rendret konseptkropp if decision.disposition is Disposition.FAIL_SECURE: raise IngestError(gate_code=decision.reasons) # fail-fast, matcher modulens kontrakt ``` Fail-secure = `IngestError` (byttet inn i modulens eksisterende fail-fast-mønster). Alert-payload = gate-kode + run-id, ALDRI innhold (§8). **Gate 2 — `load_verdicts_from_dir` (i skip-loopen):** kjør `rationale`/`description` gjennom `screen_output` med EXTERNAL/MANUAL-provenance; fail-secure = **skip fila** (konsistent med den eksisterende tolerante skip-kontrakten Steg-7-loopen hviler på) + logg gate-kode. Aldri raise (bevarer per-fil-toleransen). **Gate 3 — `promote_verdict` (opt-in, før `write_concept_file`):** `screen_output` på `rationale` → fail-secure hever `PromotionRefused` (gjenbruk eksisterende fail-closed-sti). **Gate 4 — received-bundle:** `okf.import_bundle(bundle, origin=EXTERNAL, channel=…)` per-konsept-gate FØR merge; `c.error`-konsepter skippes (FAIL_SECURE), aldri merget. ## 6. Invariant-forenlighet (må holde ved wiring) - **MAF-fri / D7-portabel:** guarden er stdlib-only, null deps → forenlig med `ingest.py`/`okf.py`'s «pure stdlib + pydantic, MAF-fri»-krav (håndhevet av `test_okf_is_maf_free` + `test_ingest_loadbearing`). Legg guarden til i `_MAF_FREE_MODULES`-probens tillatte tredjeparts-deps eller verifiser at den ikke drar inn `agent_framework`/`mcp` (den gjør ikke). **D7-speiling påkrevd:** SDK-søsknet må wire samme guard (framework-agnostisk → samme pakke) i egen økt. - **`shared/` er PULL-ONLY + commons-koordinering:** ingest-SPEC-en bor i `shared/ingest-spec.md` (frossen, commons). Hvis wiring krever et spec-felt (f.eks. `screened_at`/trust-tier i provenance-frontmatteren), MÅ amendmenten skrives i `portfolio-optimiser-commons` og pulles hit — aldri lokalt. **NB:** samme inkluderings-prompt kjører i commons-repoet; koordiner så guard-vask i den delte `shared/`-halvdelen (persona/skills, ekspert-artefakt) eies der, og denne planen kun dekker repo-lokale moduler. - **Alpha-pin:** `v0.2` — pin eksakt versjon i `pyproject.toml`; behandle API som ustabilt til v1.0. - **Kostdisiplin:** guarden gjør null modellkall og null nett → forenlig med offline-først. Semantisk/faktisk forgiftning er USYNLIG for lexicon+entropy (guardens ærlige grense §8) — den fanges av HITL-dommen + validatoren, ikke guarden. Ikke overselg: guarden er defense-in-depth på tekst-laget, ikke en vegg. ## 7. IKKE i scope nå (scope-grense) - Ingen kildekode-endring i denne økta. Ingen ny dependency lagt til `pyproject.toml`. - Ingen endring i `shared/` eller ingest-spec (commons, PULL-ONLY). - Ingen wiring i `run_project`. - **Åpne operatør-beslutninger (ikke-blokkerende):** (a) hvilke gater som faktisk skal wires ved M3-forberedelse (anbefaling: gate 1+2 obligatorisk, gate 3 opt-in, gate 4 kun ved received-bundle); (b) om et `screened_at`-provenance-felt skal inn i ingest-spec-en (krever commons-amendment — koordiner med D-B/S2.4). ## 8. Verifisering (testbare kriterier — for den framtidige wiring-økta, ikke nå) Når guarden faktisk wires (S2.4/S2.5-utvidelse), skal disse holde: - **Load-bearing detach:** en test der en kjent injeksjons-payload (zero-width-kodet instruksjon i en CSV-celle / `rationale`) MÅ gi FAIL_SECURE (gate 1: `IngestError`; gate 2: skip); detach guard-kallet → payloaden når bundelen/storen → RØD. - **Fail-secure retning:** gate 1 raiser (fail-fast), gate 2 skipper (tolerant) — bevist per gate. - **Ingen regresjon:** `test_okf_is_maf_free` + `test_ingest_loadbearing` grønne (guarden drar ikke inn MAF). - **Determinisme/offline:** `grep`-guard bekrefter ingen ny socket-sti; guard-kallet gjør null nett/modell. - **Denne planen (nå):** `docs/plan/2026-07-16-llm-ingestion-guard-inclusion.md` finnes; STATE.md bærer markørlinja `llm-ingestion-guard: planned — …` (grep-bar av guard-repoets rollup).