Adopting `active:raw-html-link` was one id. Publishing it honestly was the whole of `active_tag_class` — one function, three branches, no way to state the split without the no-URL narrowing and the 0.6.0 external-target rule. On the old predicate a bare `</a>` is active by name, so a consumer implementing from the hybrid would emit the new label where the seed runtime emits nothing. The file is now two pins, stated as two: v0.3.4/0bf0729 everywhere except the raw-HTML classifier, v0.7.0/be9759b there. The drift between them was measured field by field against the imported module rather than assumed, after stripping inline-flag rendering and applying the file's own declared quote normalisation so a spelling difference could not masquerade as drift. Exactly one published field had moved, and not the one this release was about: `html.active_tags` carried the MUTATOR's 23-name set where the gate means the SCANNER's 22. Correct at the 0.3.4 pin, wrong from 0.6.0 on. Kept as `html.mutator_tags`. The sweep covered 93 cases, not the 6 obvious ones. The narrowing can silence an `active:` finding inside the `observed_out_of_scope` evidence of a LEXICON case, and that field is guarded by no test anywhere — stale entries there survive forever. One case moved: html-obfuscation__aria-label, whose `<a aria-label=…>` carries no URL attribute. Its fixture is deliberately not rewritten; the residue is true at the commit `measurement` pins, and rewriting one of 83 would leave two commits under a header naming one. Recorded, dated and pinned in the manifest. The strongest check is not the digest: the checker rebuilds the published classifier from the JSON alone, importing nothing from the runtime, and differential-tests it against `active_tag_class` over 42 probe tags. 0 disagreements. That is what licenses shipping a classifier as data. No `aliases.llm_security` published, on this file or on carriers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LTTaT4quwNPwBYVqmgAt8t
11 KiB
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,.shsom 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 iconformance/*/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.jsoner ground truth. Er en runtime uenig medexpected.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_scopebærer inngangspunkt, findings-accessor og fixture-presentasjon per scope per runtime. En runtime hvis flate er sti-basert kan ikke måle en løsinput.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_scopehvor 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) ogobserved_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_scopeer sann ved commitenmeasurementpinner. 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.
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 deresDECLARED_TABLESog forplikter dem på hver case scopet dit. Mangler alias-strengen: la slotten stå tom og si det —not-applicablesom 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:
# 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, aldrighCLI. STATE.mder 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 barefile:///...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: