llm-security-commons/CLAUDE.md
Kjell Tore Guttormsen 8f08e9ac73 feat(carriers): three cases minted, and the id is named rather than adopted
Both runtimes answered the 2026-08-11 decision request, and they did not ask
for the same name. The guard ruled `sanitize:` names its `Finding.detector`
and offered all three labels for verbatim adoption. llm-security, asked
directly, answered that the name would make its conformance result read as a
claim about neutralisation it does not perform.

Two things decided it. The guard's own unprompted correction: prefix ==
detector holds for those six labels and is no general law in its runtime
(`egress:*` carries detector="output"; decode-and-rescan yields two-part
`decoded:lexicon:*`). A prefix whose meaning is recoverable only by reading
one implementation cannot carry a shared id space. And a measurement taken
here at be9759b: on the surface the guard's own ruling pinned, `sanitize()`
returns changed text on all three carriers, so the counterargument's decisive
case -- that `scan_output` mutates nothing -- does not reach this surface.

Not a mediation. Neither runtime claimed the shared id must equal its label,
and `override:ignore-previous` already carries two different alias strings.

- carriers.json 0.1.0 -> 0.2.0: carrier:zero-width / :bidi-override /
  :unicode-tag, aliased to the guard's labels. No aliases.llm_security --
  that runtime's carrier findings carry no id yet, and publishing the alias
  is the irreversible act that forces the table into its declared set.
- manifest 0.3.4 -> 0.4.0: entry_points_by_scope, carrying findings accessor
  and fixture presentation per scope per runtime. This was objection (c), and
  it blocked minting harder than the name did.
- Corpus 90 -> 93. Measured through sanitize(text, source=Source.INPUT) at
  guard v0.7.0; verified by a separate checker that re-derives everything from
  disk -- a generator agreeing with itself proves nothing.
- CLAUDE.md gains the two rules that are not derivable from the data: a shared
  id space cannot rest on a one-runtime prefix, and publishing an alias -- not
  minting the case -- is the irreversible act.

Not minted on purpose: no artifact-side id (the other runtime would only fail
them), and no ZWJ-exemption case (U+200D between two emoji is exempt on both
guard surfaces since v0.6.1; the fixture avoids it rather than trips it).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U3o4zSQ2kwLsgJNU7apK3Z
2026-08-13 21:17:44 +02:00

8.3 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, .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.jsonentry_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.

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

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, 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: