# llm-security-commons ## Kontekst Runtime-nøytral kjerne for LLM/agent-sikkerhetsdeteksjon: detektor-data, normative kontrakter og en conformance-korpus som **flere uavhengige runtimes** kan kjøre mot og få **identisk verdikt** fra. Repoet er delt kjerne, ikke et produkt. Kjente konsumenter (vendorer dette repoet, endrer det ikke): - `llm-security` — Claude Code-plugin, Node/ESM-scannere. - et Python-guard-repo — samme deteksjon i en annen runtime. - en wiki/advisory-flate — konsumerer samme lexicon og mapping. Å dele én identisk kjerne er hele poenget: to implementasjoner som gir ulikt verdikt på samme input er per definisjon en bug i én av dem — ikke en meningsforskjell. ## Charter (HARD — bryter du denne, er endringen feil uansett hvor god den er) **Ingen engine-kode. Ingenting her kjører.** - ❌ Ingen `.mjs`, `.js`, `.ts`, `.py`, `.sh` som implementerer deteksjon, scanning, normalisering, scoring eller I/O. - ❌ Ingen `package.json`, `pyproject.toml`, lockfiler, dependencies, build-steg. - ❌ Ingen import fra — eller kjennskap til — noe rammeverk, SDK eller runtime. - ❌ Ingen nettverk, ingen modellkall, ingen tidsavhengighet, ingen tilfeldighet. Alt her er **offline og deterministisk**. - ✅ Kun: JSON-data, normative spesifikasjoner (Markdown), og fixtures (`input` + `expected`). Regelen finnes fordi kjernen skal være **fork-and-own**: en konsument på en runtime vi ikke har tenkt på skal kunne vendore dette uten å arve et eneste teknologivalg. Mønsteret er kopiert fra søsterrepoet `portfolio-optimiser-commons` (samme harde charter: «nothing here may import/depend on a framework»). ## Stack Ingen. Data + prosa. Filformater: JSON (data + schema), Markdown (spec), rå tekst (conformance-input). ## Konvensjoner ### Data (JSON) - **Hver JSON-fil har et topnivå `"version"`-felt** (semver-streng). Uten unntak. - Hver JSON-fil har et topnivå `"$comment"` eller `"description"` som sier hva filen er og hvor dataene kom fra (provenance). - 2 mellomrom indentering, LF, avsluttende newline. UTF-8 uten BOM. - Kodepunkter skrives som `"U+200B"`-strenger (lesbare i review), aldri som rå usynlige tegn i JSON-kilden — bortsett fra i `conformance/*/input.txt`, som per definisjon inneholder de faktiske tegnene. - Nøkler er stabile identifikatorer. **Å endre en nøkkel er en breaking change** — konsumenter matcher på dem. ### Spec (Markdown) - Hver normativ spec har en `**Status: normative**`-markør øverst. - RFC 2119-språk (MUST / MUST NOT / SHOULD / MAY) i store bokstaver, brukt bevisst. - Informative dokumenter (`docs/`) har `**Status: informative**` og er aldri ground truth. ### Conformance - Én katalog per case: `conformance//input.txt` + `conformance//expected.json`. - `` er stabil og beskrivende. **Å endre en case-id er en breaking change.** - `expected.json` er ground truth. Er en runtime uenig med `expected.json`, er runtimen feil — med mindre fixturen selv bevises feil, og da endres fixturen i eget commit med begrunnelse. - **En case er ikke mintbar uten inngangspunkt for sitt scope.** Korpuset pinner ikke lenger ett inngangspunkt per runtime for alt — `manifest.json` → `entry_points_by_scope` bærer inngangspunkt, **findings-accessor** og **fixture-presentasjon** per scope per runtime. En runtime hvis flate er sti-basert kan ikke måle en løs `input.txt`, og en fixture den får som løs tekst måler ingenting samtidig som den ser ut som en pass. Nytt scope ⇒ fyll ut alle tre FØR første case. - **Generatoren verifiserer aldri seg selv.** En mint krever en separat sjekker som leser fixturene tilbake fra disk og utleder alt på nytt (digest, id fra case-id, scope, exact-within-scope). Ligger i scratchpad, aldri i repoet. - **En innsnevring måles mot HELE korpuset, ikke mot casene den handler om.** Endrer en oppdatering hva en runtime *slutter* å rapportere, kan den tømme et `observed_out_of_scope` hvor som helst — også på caser scopet til en helt annen tabell. Det feltet er evidens (spec §5), så **ingen testsuite noe sted vokter det**: en foreldet oppføring består hver kjøring for alltid. Kjør hver committet case gjennom sitt eget scopes inngangspunkt og sammenlign mot BEGGE stedene fixturen fører en runtime-label — `findings` (mappet via tabellens aliases) og `observed_out_of_scope`. Målt 2026-08-13: seks «åpenbare» caser flyttet seg ikke, én lexicon-case gjorde det. - **En foreldet residue-oppføring skrives ikke om — den pinnes.** `observed_out_of_scope` er sann ved commiten `measurement` pinner. Retter du én av 83, står 82 målinger ved én commit og én ved en annen, under en header som navngir én. Før avviket i manifestet med dato og commit i stedet. Å re-pinne hele korpuset er en egen beslutning. - **Like funn-sett betyr ikke lik feilflate.** Spørsmålet som avgjør om en variant-case skal mintes er ikke om de to inputene gir ulike funn innenfor scope — det er om den andre inputen kan FEILE, innenfor scope, under en endring i den scopede datafila som den første ville bestå. Korpusets første variant forventer nøyaktig samme funn som base-casen og gater likevel noe base-casen ikke ser: base-inputen matcher mønsteret både i publisert og i tidligere, strengere form. En payload hvis skille ligger i en tabell vi ikke publiserer består ikke terskelen og føres som navngitt utelatelse. Spec §6 bærer regelen. ### Id-rom: adoptert vs. navngitt Standard er **adopsjon verbatim** fra en runtimes egne labels (leksikonets 83, active contents 6). Å NAVNGI en id her er unntaket og krever at begge runtimes er spurt først — `carrier:*` er den eneste så langt, og `carriers.json` bærer begrunnelsen. To regler som ikke er utledbare fra dataene: - **Et prefiks som bare betyr noe inne i én runtime kan ikke bære et DELT id-rom.** Guarden korrigerte oss selv på at «prefiks == `detector`-feltet» gjelder seks carrier-labels og er ingen lov i deres runtime. Skriv aldri den generelle formen; skop påstanden til de konkrete id-ene. - **Å publisere `aliases.` er den irreversible handlingen, ikke å minte casen.** Konsumentens testsuite utleder sitt registrerte tabellsett ved å gå gjennom HELE den vendorede fila og registrere tabellen om ÉN node bærer aliaset. Granulariteten er FILA. Ett alias tvinger tabellen inn i deres `DECLARED_TABLES` og forplikter dem på hver case scopet dit. Mangler alias-strengen: **la slotten stå tom og si det** — `not-applicable` som registrerer et manglende NAVN er ærlig; en gjettet alias-streng er det ikke. ### Sikkerhetskritiske tabeller — aldri fra hukommelse `codepoints/carriers.json` (inkl. homoglyph-map), `signatures/secret-egress.json`, `signatures/malware-signatures.json` og `signatures/active-content.json` er **deteksjonsdata**. Et gjettet kodepunkt eller et regex med feil escaping er en stille falsk negativ — en detektor som ser ut som den virker. **Disse filene endres KUN fra verifisert kildedata** (dump fra konsument-repo, Unicode-standarden, publisert leverandør-doc). Aldri fra egen hukommelse, aldri «fylt ut for konsistens». Kan en oppføring ikke verifiseres: utelat den, eller marker den eksplisitt uverifisert i `$comment`. ### Å adoptere én ny id fra en runtime som har flyttet seg Standardtilfellet er additivt: en ny nøkkel, en ny id, ferdig. Det holder bare når **produsenten** av den nye id-en er uendret. Er den nye id-en et nytt utfall av en klassifiserer runtimen har skrevet om, må hele klassifisereren adopteres — en publisert id oppå den gamle prediktoren er en kontrakt som ser komplett ut og er feil, og en konsument som implementerer fra den divergerer fra dag én. Test: kan du skrive den nye id-ens gate uten å røre de andre grenene i samme funksjon? Kan du ikke, er scope hele funksjonen. To ting som følger av det: - **En datafil kan ha TO provenance-pins, og da skal begge stå.** Én pin over en fil som er halvt gammel og halvt ny beskriver ingen av halvdelene. Skop re-pinnen til de blokkene den faktisk dekker, og si hvilke. - **Mål drift felt for felt før du re-pinner, ikke etterpå.** Importer modulen ved taggen og sammenlign hvert regex, hver severity, hver liste og hvert tallgulv — etter å ha strippet Pythons inline-flagg-rendering og anvendt filas egne deklarerte normaliseringer, ellers rapporterer du staveforskjeller som drift. Målt 2026-08-13 over 0.3.4 → 0.7.0: 24 felt holdt, **ett** hadde driftet, og det var ikke det oppgaven handlet om. Og den sterkeste kontrollen når du publiserer en klassifiserer som data: **bygg den opp igjen fra JSON-en alene** — ingen import fra runtimen — og differensialtest mot runtimens funksjon over et probe-korpus som treffer hver gren. Består den, er fila bevist tilstrekkelig som spesifikasjon. Består den ikke, mangler fila noe prosa aldri ville avslørt. ### Behaviour preservation (v0.1.0-invariant) v0.1.0 er en **ekstraksjon**, ikke en revisjon. Data som er hentet ut av en konsument skal gi **eksakt samme funn** når konsumenten senere leser dem herfra. Ser du noe du mener er feil i seed-dataene: **ikke fiks det her**. Dokumentér avviket, send det til konsumenten via `coord-send`, og la beslutningen tas der dataene er testet. ## Kommandoer Repoet har ingen build og ingen test-runner (charter). Validering er ad hoc: ```bash # Alle JSON-filer er velformet find . -name '*.json' -not -path './.git/*' -print0 | xargs -0 -n1 python3 -m json.tool > /dev/null # Hver JSON-fil har topnivå "version" for f in $(find . -name '*.json' -not -path './.git/*' -not -path './conformance/*'); do python3 -c "import json,sys; d=json.load(open('$f')); sys.exit(0 if 'version' in d else 1)" \ || echo "MANGLER version: $f" done # Hver spec har normativ-markør grep -L 'Status: normative' spec/*.md # Charter-guard: ingen kjørbar kode har sneket seg inn find . -type f \( -name '*.mjs' -o -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.sh' \) \ -not -path './.git/*' | grep . && echo 'CHARTER-BRUDD: kjørbar kode i commons' ``` ## Arbeidsflyt - **Versjonering:** semver på repo-nivå (tag `vX.Y.Z`). Hver JSON-fils `"version"` er filens egen semver og bumpes når *den filen* endres — de er ikke låst til repo-taggen. Nytt datafelt eller ny oppføring = minor. Endret/fjernet nøkkel, case-id eller disposisjon = **major** (konsumenter bryter). - **Versjonssync før commit:** endrer du en JSON-fil, bump dens `"version"`; endrer du repoets kontrakt, bump repo-taggen + CHANGELOG. - **Konsumenter varsles via `coord-send`**, ikke via antakelse. Et repo som vendorer denne kjernen får ikke vite at kontrakten endret seg med mindre du sier det. - **Aldri jobb i konsument-repoene fra en økt her.** Vendoring, oppgradering og behaviour-verifisering skjer i konsumentens egen økt, med konsumentens tester. - **Forgejo only** (`git.fromaitochitta.com`). Aldri GitHub, aldri `gh` CLI. - `STATE.md` er LOCAL-ONLY (gitignored) — remote er en offentlig flate. ## Communication patterns ### Linking to local files When pointing to local files in responses, always use markdown link syntax with a descriptive name: - Use `[Human-friendly name](file:///absolute/path)` — never bare `file:///...` URLs or autolinks ``. - Always use absolute paths. Never `~/` or relative paths. - For multiple files, render as a bullet list of named markdown links. Why: bare `file://` URLs only render the first as clickable across multiple lines. Named markdown links make each entry independently clickable and look cleaner. Example: - [Brief](file:///Users/ktg/.../brief.html) - [Research summary](file:///Users/ktg/.../research/summary.md)