llm-security-commons/CLAUDE.md
Kjell Tore Guttormsen cb784fea6f fix(spec): section 6 forbade a case the corpus ships, and the predicate under it was wrong too
The normative spec read "Such a payload MUST NOT be given a discriminated case
id" while manifest.json defined case_id_derivation.variant_suffix and
conformance/hybrid-xss__script-tag--src-no-close/ sat on disk under it. The
manifest was the correct party: the derivation was extended in corpus 0.3.0 and
the spec was never updated. No data moves here, only the text describing it.

The derivation block now carries the optional '--' suffix and the truncating
reverse, with the '--'-absence measurement stated as the reason the reverse
stays LEXICAL - re-measured at this commit rather than copied from 0.3.0's
numbers, and scoped to id spaces because '--' does occur inside pattern values.

The part that would have passed review while still being wrong: fixing only the
permission. Section 6 also reasoned that equal in-scope finding sets mean the
second case "cannot fail in any way the first does not" - and the shipped
variant falsifies exactly that. It expects the same single finding, same scope,
same match, and still gates what the base cannot, because the base input matches
the pattern under both its published and its superseded stricter form. The
discriminating signal is INSIDE the scope, in the form of the scoped rule. So
the MUST NOT is replaced by a predicate about failure surface rather than
finding sets, checked in both directions: it admits the shipped variant and
still excludes the omitted markdown-image payload, whose only distinguisher
lives in a table this repository does not publish.

manifest.json is untouched and stays at 0.5.0; no case directory moved. The
spec has no version of its own - "Through version 0.1.1" in section 4 is the
CORPUS version, verified against CHANGELOG [0.2.0] before acting, because the
session brief said otherwise. Section 6's stable-id and BREAKING sentences were
read, not edited.

Verified in scratchpad, never in the repo: the amended derivation transcribed
into a checker that reads all 94 cases back from disk, derives each pattern id,
round-trips it forward, and asserts the case expects it. All 94 reproduce.

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

212 lines
12 KiB
Markdown

# 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/<case-id>/input.txt` + `conformance/<case-id>/expected.json`.
- `<case-id>` 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.<runtime>` 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 `<file://...>`.
- 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)