llm-security/scanners/commons/CONVENTIONS.md

11 KiB

Conventions

The rules a change to this repository is held to, in one place.

Nothing here is new policy. Every rule below was already being applied — some of it stated in README.md, some in SECURITY.md, some only visible in the shape of the files themselves. It is collected here because a convention that only exists in the maintainer's head is not a convention an outside reader can meet, and because two of the decisions this repository makes — that nothing here runs, and that pull requests are switched off — look arbitrary until the reason is written down next to them.

This file binds contributions to this repository. It does not bind the runtimes that read the data; that is what spec/ is for, and those files say Status: normative and mean it.

The charter: nothing here runs

This repository contains no executable code, and it will not acquire any.

Not permitted, without exception:

  • .mjs, .js, .ts, .py, .sh — or any other file that implements detection, scanning, normalisation, scoring or I/O;
  • package.json, pyproject.toml, lockfiles, dependencies, build steps;
  • an import of, or knowledge of, any framework, SDK or runtime;
  • network access, model calls, dependence on the clock, or randomness.

Permitted: JSON data, normative specifications in Markdown, and conformance fixtures (input.txt plus expected.json).

The reason is fork-and-own. A consumer on a runtime nobody here has thought of should be able to vendor this repository without inheriting a single technology choice. A build step is a technology choice; so is a test runner. The moment one exists, the set of runtimes that can adopt this core shrinks to the set that tolerates it.

The consequence is that this repository cannot validate itself. There is no CI in this organisation and nothing runs on push. The checks below are yours to run, and they are the only ones there are.

How a change gets in — and why not by pull request

Pull requests are switched off on the canonical repository at git.fromaitochitta.com/open/llm-security-commons, and issues are not the reporting channel either. That is deliberate, and the reason is stronger than a preference about tooling.

This repository is vendored into independent runtimes — a Claude Code plugin on Node/ESM, a Python guard, an advisory surface — each pinning a tag. The contract between them is semver, and a change to detection data changes what those runtimes find. A patch to a pattern table is not a contribution that can be merged and then socialised; it is a contract change that has to be coordinated with every consumer before it exists, because the moment it is tagged, the next consumer to pull it gets different findings than the one that pulled yesterday. A merge button does not do that, and nothing downstream of a merge button can.

So the routes in are:

  1. Fork and own it. MIT, and an intended use rather than a tolerated one. If you need a different value in your runtime, this is the fast path and it is fully supported.
  2. Report it privately — see SECURITY.md. A wrong entry in a detection table is a silent false negative in every runtime that reads it, so a report about one is a security report even though nothing here executes. That file also explains why a confirmed defect in extracted data is usually decided in the runtime it came from before it changes here.

If you maintain a consumer, the coordination channel is direct contact with the maintainer, not this repository's forge surface.

Data files (JSON)

  • Every JSON file carries a top-level "version" — a semver string. No exceptions.
  • Every JSON file states what it is and where its data came from, in a top-level "$comment" or "description". Provenance is not optional metadata here; it is what makes the difference between a table and a rumour.
  • 2-space indentation, LF line endings, a trailing newline, UTF-8 without BOM.
  • Code points are written as strings"U+200B" — never as the raw invisible character. Review cannot see what it cannot render, and a reviewer who cannot see a character cannot check it. The single exception is conformance/*/input.txt, which by definition contains the actual bytes.
  • Keys are stable identifiers. Consumers match on them. Changing a key is a breaking change and is versioned as one.

The four files that carry detection material — codepoints/carriers.json, signatures/secret-egress.json, signatures/malware-signatures.json, signatures/active-content.json — take one further rule, which is the most important line in this document:

They are changed only from verified source data — a dump from the owning repository, the Unicode standard, published vendor documentation. Never from memory, never "filled in for consistency". A guessed code point or a regex with wrong escaping is a silent false negative: a detector that looks like it is working and is not looking. If an entry cannot be verified, leave it out, or mark it explicitly unverified in its $comment.

Specifications (Markdown)

  • A normative specification carries Status: normative at the top and uses RFC 2119 terms (MUST / MUST NOT / SHOULD / MAY) in uppercase, deliberately. These files bind the implementations that read them.
  • An informative document (docs/) carries Status: informative and is never ground truth. It records measurements, history and open disagreements; a runtime is not wrong for disagreeing with one.
  • Naming a file that does not exist yet is allowed where the layout is part of the contract — spec/decode-pipeline.md is named in README.md and marked Planned. It is not a link, and nothing depends on it. A normative spec guessed at would be worse than an absent one.

Conformance cases

  • One directory per case: conformance/<case-id>/input.txt and conformance/<case-id>/expected.json.
  • <case-id> is stable and descriptive. Changing a case id is a breaking change — a published conformance result names it.
  • expected.json is ground truth. If a runtime disagrees with it, the runtime is wrong.
  • The one way that reverses: the fixture is proven wrong. Then the fixture changes in its own commit, with the reason written down — never folded into a change that does something else, because a fixture edit is the one edit that can make every conforming runtime wrong identically.
  • A case declares the data files it is scoped to. A runtime that does not implement a scoped table reports the case not-applicable — a third verdict beside pass and fail, and one that must be reported rather than dropped from the denominator. See spec/conformance-corpus.md.

When a value may change

Detection values do not change here because someone here judged them wrong. Three mechanisms have moved a value so far, and each is recorded in the file itself rather than only in the changelog:

  1. Re-extraction — the owning runtime changed its own value, and this repository re-read the source at a pinned public commit. Recorded in source_fidelity.post_extraction_drift.
  2. Retraction — this repository described its own provenance wrongly. The stored value may have been right all along; the account of where it came from was not. Recorded in source_fidelity.retracted.
  3. Owner-directed authoring — the owning runtime decided a value and asked this repository to carry it, because the dependency has inverted: the source now reads this file and holds no literal to re-read. Recorded separately, in source_fidelity.owner_directed_changes, precisely because calling it drift would assert that the source moved and commons followed — which would be false in the one direction that matters.

What is not on that list is merit. Data extracted from an implementation is kept behaviour-identical to it on purpose, because a copy that disagrees with its source is the exact failure this repository exists to prevent. Producing one as a fix would be self-defeating. If you believe an extracted value is wrong, say so — and expect the decision to be taken in the runtime where the pattern is under test.

Data authored here rather than extracted — conformance payloads, flagged as authored_payloads in conformance/manifest.json — is this repository's own to correct.

Versioning

Two version numbers, and they are not locked to each other:

  • The repository tag (vX.Y.Z) versions the contract: the file set, the key names, the case ids, the disposition semantics.
  • Each JSON file's own "version" is bumped when that file changes.

What counts as which:

Change Bump
New data field, new entry minor
Changed or removed key, case id, or layout major — consumers break
A change to what a conforming runtime finds minor in 0.x, and the changelog says so

That last row is why the changelog entry is the thing to read before upgrading, not the version number. Pre-1.0, a release that changes findings is still a minor bump; only the entry tells you whether your assertions move.

Consumers vendor a tag, never main — a conformance result can only be attributed to a tag. Nothing polls for updates; when a change moves detection data, the maintainer notifies known consumers directly, and their upgrade remains their own action on their own schedule.

Checks to run before proposing a change

There is no CI. These four are the validation surface, they run offline in a second, and each one has been confirmed to go red on a violation rather than merely green on a clean tree.

# 1. Every JSON file is well-formed
find . -name '*.json' -not -path './.git/*' -print0 \
  | xargs -0 -n1 python3 -m json.tool > /dev/null && echo OK

# 2. Every JSON file outside conformance/ carries a top-level "version"
find . -name '*.json' -not -path './.git/*' -not -path './conformance/*' -print0 \
  | xargs -0 python3 -c 'import json,sys
missing=[p for p in sys.argv[1:] if "version" not in json.load(open(p))]
print("\n".join("MISSING version: "+p for p in missing) or "OK")'

# 3. Every spec carries its normative marker (prints offending files, nothing = clean)
grep -L 'Status: normative' spec/*.md || echo OK

# 4. Charter guard: no executable code has crept in
find . -type f \( -name '*.mjs' -o -name '*.js' -o -name '*.ts' -o -name '*.py' -o -name '*.sh' \) \
  -not -path './.git/*' | grep . && echo 'CHARTER VIOLATION' || echo OK

They are written as shell one-liners rather than shipped as a script because a script would be .sh, and check 4 would then fail on the tooling meant to enforce it.

What they do not check: whether a value is correct. Nothing offline can. That is what the conformance corpus is for, and it runs in each consumer's own test suite against a pinned tag — constraining two of the seven data files, which is a real limit and is stated in README.md under Known limitations.