portfolio-optimiser/docs/plan/2026-07-16-llm-ingestion-guard-inclusion.md
Kjell Tore Guttormsen 4ecd571961 docs(security): plan llm-ingestion-guard inclusion at ingest/inbox persist-gates
Map untrusted-ingest surface (ingest.materialize http/I6, verdict-inbox
load, promote_verdict, future received-bundle) vs first-party paths;
verdict = planned, wire scan/sanitize before M3 as S2.4/S2.5 extension.
Plan only — guard not wired. shared/ hardening owned by commons session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145ZKPLMVeqM47z2jxxokym
2026-07-16 07:43:30 +02:00

13 KiB
Raw Blame History

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

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).