portfolio-optimiser/docs/plan/2026-07-16-llm-ingestion-guard-inclusion.md
Kjell Tore Guttormsen c255662802 feat(ingest): P2/S1.b — innholdsgaten står, rundt materialize og ikke i den
Planens §3 sa at `ingest.materialize` er repoets ENE skrivepunkt på Door A, og
det premisset ble felt av måling FØR bygging: `materialize` er en ren delegasjon
til pinnet llm_ingestion_okf v0.3.2s `materialize_bundle`, som stager i minnet og
utfører sin egen disk-fase. Det finnes ingen callback mellom de to, så en gate
plassert der kunne bare kjørt ETTER at bytene landet — en opprydding, ikke en gate.

Sømmen ble i stedet kopier bundelen → materialiser inn i kopien → skann det som
ble generert → publiser eller forkast. Kopien er bærende, ikke bekvemmelighet:
bibliotekets §3 eierskaps-skann, kollisjonsgaten mot kuratert innhold og §6
index-merge leser alle den EKSISTERENDE bundelen. Staging i tom katalog mister
alle tre og publiserer en bundle uten kuraterte naboer — datatap forkledd som
sikkerhetsfiks.

De fire §4-beslutningene, tatt og målt: (1) ingen av guardens to preset —
Origin.EXTERNAL/AUTOMATIC, fordi trust_for utleder policy fra origin alene og
PRESET_USER_UPLOAD bærer en quarantine-semantikk Door A ikke har; (2) utfall per
BUNDLE, diagnostikk per DOKUMENT — delvis publisering ville etterlatt bundle +
index som svarer til intet manifest, men import_bundle itererer forbi første
avvisning; (3) Report til log.md, aldri konsept-frontmatter, der fire golden-suiter
pinner bytene; (4) mypy-override OG adapter, siden override alene gjør sømmen
type-blind i stedet for type-sikker.

`materialize` forblir ugatet med vilje — goldenene pinner den, og en kaller som
vil ha gaten ber om den ved navn.

Fem mutasjoner alle røde + grønn kontroll (hele suiten, ~120 s hver): detach
gaten · la den fyre ETTER publisering · Origin.INTERNAL · tom staging-katalog ·
rapporter kun første avvisning.

Målingen felte en VAKUØS test først: en hard injeksjon scorer fail_secure under
BEGGE trust-tierene, så Origin.INTERNAL-mutasjonen lot alle tre avvisningstestene
stå grønne — beslutning 1 så dekket ut uten å være testet. Båndet der tieren
faktisk avgjør er høy-entropi-innhold (quarantine_review vs warn), og testen ble
skrevet mot nøyaktig det før mutasjonen ble re-målt. Mutasjon 4 ble på sin side
felt av KUN én test; 809 andre merket ikke at bundle-kopien forsvant.

Laveste disposition er `warn`, ikke `allow` — `allow` finnes ikke i guarden. En
gate skrevet mot == allow ville avvist hvert dokument som noensinne ingestes.

Kriterium 5 står: demo-stdout er byte-identisk med tests/golden/demo-transcript.stdout,
målt både i suiten og ved eksplisitt kjøring. shared/ er urørt.

801 -> 810 passed / 4 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDu94KoyxAmhJsG2n63X8Q
2026-08-09 22:58:00 +02:00

14 KiB
Raw Blame History

Inkluderingsplan — llm-ingestion-guard som write-time persist-gate

⚠️ UTDATERT PÅ TO PUNKTER — REALISERT 2026-08-09 av innholdsgate-planen (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 26 D-G («Guard forblir eget sikkerhets-repo (avhengighet)», linje 120) og toolkit-repo-brief §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.addExpeLContextProvider (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_dirstore.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_verdictokf.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_outputrationale → 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).