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
This commit is contained in:
parent
7e86896bc8
commit
4ecd571961
1 changed files with 159 additions and 0 deletions
159
docs/plan/2026-07-16-llm-ingestion-guard-inclusion.md
Normal file
159
docs/plan/2026-07-16-llm-ingestion-guard-inclusion.md
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
# 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 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue