release(0.4.0): two values moved by two mechanisms, and the difference is the release

One table re-extracted from a pinned upstream commit, the way every value here
has moved until now. Two lexicon rows authored here at the source owner's
direction, which has never happened before and needed a reason that is not "we
measured it and we were right."

Consumers: byte-identity against v0.3.0 goes red by construction - pattern text
changed. Ids, labels, aliases, family membership, case ids and every count are
unchanged.

README version sync: vendoring examples move to v0.4.0 (both subtree and
fork-and-own), the secret-egress table count 18 -> 19, and the divergence-doc row
stops claiming no data file was changed because of it. Two were - and the row now
says why that did not come from the document.

Verified: JSON well-formed, every JSON has top-level version, every spec still
normative, charter clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLEZ4XCSnSrQUFA8SzkQB4
This commit is contained in:
Kjell Tore Guttormsen 2026-08-11 21:52:53 +02:00
commit f1e5c20fb7
2 changed files with 81 additions and 16 deletions

View file

@ -9,26 +9,91 @@ Versioning note: the repository tag versions **the contract** (file set, key nam
case ids, disposition semantics). Each JSON file additionally carries its own
`"version"` field, bumped when that file changes.
## [Unreleased]
## [0.4.0] — 2026-08-11
**Two detection values changed, by two different mechanisms, and the difference between those
mechanisms is the point of the release.** One pattern table was **re-extracted** from a pinned
upstream commit, the way every value in this repository has moved until now. Two lexicon rows were
**authored here at the source owner's direction**, which has never happened before and required a
reason that is not "we measured it and we were right."
A runtime that vendors this repository will see findings change. Any consumer asserting
byte-identity against `v0.3.0` goes red by construction — `lexicon/injection-lexicon.json` changed
pattern text. Ids, labels, aliases, family membership, case ids and every count are unchanged.
### Changed
- `lexicon/injection-lexicon.json` `0.7.0``0.8.0`**`hybrid-xss:script-tag` and
`hybrid-xss:iframe-src` narrow their unbounded negated class from `[^>]*` to `[^><]*`.** Both
forms are quadratic in scan length on input that repeats the tag prefix and never supplies a
`>`: each occurrence is a match start and `[^>]*` scans to end of input from each one. Measured
in Node v25.8.2 at 16k / 32k / 64k / 128k / 256k chars — script-tag 32.65 / 113.36 / 479.02 /
1988.83 / **7772.25** ms, iframe-src 39.23 / 131.76 / 574.94 / 2469.55 / **9449.94** ms, ×4 per
doubling for both. Under `[^><]*` the same inputs cost 0.080.66 ms and 0.101.00 ms: flat, not
merely faster.
**Why this is not commons correcting seed data.** The dependency direction inverted. As of
`llm-security` `be14867` their four injection tables are built from this file and hold zero local
regex literals — measured on their published surface at `47905da`, with their vendored copy of
the lexicon confirmed byte-identical to `0.7.0`. So re-extraction was not available as a
mechanism: there is no upstream literal left to re-read. They re-measured the finding rather than
accepting it, rejected `[^>]{0,256}` because a bound is paddable and `[^>]{1,256}` because it
drops bare `<script>` and two corpus cases with it, chose `[^><]*`, and asked commons to carry
it. Recorded in a new `source_fidelity.owner_directed_changes` block — deliberately **not** in
`post_extraction_drift`, which would have said the source moved and commons followed, when the
source now reads commons.
Not decided by majority. The guard reached `[^><]` first and independently (`cff0437`), so all
three runtimes now agree, but a 3-of-3 count is not what moved this value and would not have
been sufficient. The justification is the same one that kept commons on `[^>]` through `0.7.0`:
this file tracks its declared source, and the declared source chose.
Accepted cost, stated plainly: content carrying a literal `<` between the tag name and the `>`
(`<script <x>`, `<script<div>`) stops matching. Measured across **all 90** conformance cases,
not only the four that cite these ids: zero lost a match, zero gained one. The dropped class is
real and unwitnessed by the corpus.
- `signatures/secret-egress.json` `0.2.0``0.3.0` — **the one-entry staleness disclosed in
`0.2.0` is closed by re-extraction, 18 → 19 patterns.** `OpenAI Legacy API Key`
(`\bsk-[A-Za-z0-9]{20}T3BlbkFJ[A-Za-z0-9]{20}\b`) enters at `order` 17, second to last; `JWT
(three-part token)` moves to 18 and stays last, which `ordering.last_entry_is_load_bearing`
requires.
Read out of the module text at a pinned public commit, never transcribed from the coord message
that reported it — the message contained the regex, and that is exactly the path
`evidence_limits` had ruled out. `refs/heads/main` on the public remote is `47905da`; `088e458`
carries the entry and was confirmed an ancestor with `git merge-base --is-ancestor` rather than
read off their log.
A side effect worth more than the entry: **all 19 positions were compared against the module**
name, regex source, flags, order — with 0 divergences. Positions 016 came from a 2026-08-09
transcription whose module fidelity had stood recorded as llm-security's assertion rather than a
reproduced result. It is now reproduced, and that `evidence_limits` bullet is retired along with
the staleness one.
- `conformance/manifest.json` `0.3.1``0.3.2` — the `secret-egress` blocker prose said its note
would stand until the re-extraction landed. It landed, so item (2) (`openai-api-key-legacy` is a
real hole here) is marked closed, and the entry count moves 18 → 19. **The blocker itself does
not close**: 19 entries against the guard's 25 at different cut points is still a table
reconciliation nobody has performed, and one closed hole is not that reconciliation.
- `docs/lexicon-port-divergence.md` (informative) — the `[^>]` vs `[^><]` row gets the number
it never had for **this** side. The guard disclosed that their `[^><]` is a measured ReDoS fix
(`cff0437`), not a preference, and asked commons to measure its own form rather than take
their word. Measured here in Node and Python: `<script\b[^>]*>` is **quadratic** in scan
position on input that denies it a `>`×4.0 per doubling, 6.7 s at 256 000 chars against
0.41 ms for the guard's form.
it never had for **this** side, and then gets closed. The guard disclosed that their `[^><]` is a
measured ReDoS fix (`cff0437`), not a preference, and asked commons to measure its own form
rather than take their word. Measured here in Node and Python: `<script\b[^>]*>` is **quadratic**
in scan position on input that denies it a `>`×4.0 per doubling, 6.7 s at 256 000 chars
against 0.41 ms for the guard's form.
Counted across the whole lexicon rather than assumed from the one row: 8 of 83 patterns carry
a bare `[^>]`, six of them bounded (`{1,256}`, measured linear) and **two unbounded**. The
second unbounded one, `hybrid-xss:iframe-src`, had not been named by any party — 16.9 s at
256 000 chars.
**No data file is touched and neither pattern changes here.** Both are byte-identical to
`llm-security`'s source, and behaviour preservation is not suspended because a measurement is
unwelcome. The finding is routed to the owning runtime, which is what `SECURITY.md` promises
an outsider would happen — this is the first time that route has been walked from the inside.
The finding was routed to the owning runtime, which is what `SECURITY.md` promises an outsider
would happen — the first time that route was walked from the inside. It came back as a decision
the same day, and the document now records the closure with the invariant intact: the
measurement travelled, the authority did not. Had `llm-security` declined, both rows would still
read `[^>]*` and this file would record a permanent divergence instead.
## [0.3.1] — 2026-08-11

View file

@ -24,7 +24,7 @@ As a `git subtree` (recommended: history is preserved and upgrades are a single
```bash
git subtree add --prefix vendor/commons \
https://git.fromaitochitta.com/open/llm-security-commons.git v0.3.0 --squash
https://git.fromaitochitta.com/open/llm-security-commons.git v0.4.0 --squash
# later, to move to a newer tag
git subtree pull --prefix vendor/commons \
@ -34,7 +34,7 @@ git subtree pull --prefix vendor/commons \
Or pin a tag and copy — `fork-and-own` is an explicitly supported path:
```bash
git clone --depth 1 --branch v0.3.0 \
git clone --depth 1 --branch v0.4.0 \
https://git.fromaitochitta.com/open/llm-security-commons.git
```
@ -61,14 +61,14 @@ and keeping it that small is the point.
| [`schema/conformance-declaration.schema.json`](schema/conformance-declaration.schema.json) | **Normative.** The shape a runtime publishes alongside a conformance result: which commons tables it implements, the commons commit it measured, and the four verdict counts. Required by the corpus spec §1.1; not validated by anything here, because nothing here runs. |
| [`spec/conformance-corpus.md`](spec/conformance-corpus.md) | **Normative.** How to read the corpus: what a case is, why `input.txt` is bytes rather than text, what `exact-within-scope` requires of a runtime, and how a runtime declares its table set so a case scoped outside it reads as `not-applicable` rather than as a failure. |
| [`conformance/`](conformance/) | 90 cases. One directory per case: `input.txt` in, `expected.json` out. Ground truth. 84 cover the injection lexicon — 83 one per pattern, both seeding runtimes measured producing the same verdict on all 83, plus one variant case gating a pattern form against its predecessor. Six cover active content and are measured against the one runtime that implements that table — `not-applicable` for the other, not failing. See [`conformance/manifest.json`](conformance/manifest.json). |
| `spec/decode-pipeline.md` | **Planned, still not shipped as of v0.3.0.** The decode order, in RFC 2119 language. Two runtimes that decode in different orders will disagree on identical input. Writing it needs the decode implementation, which is engine code and has not been supplied — and a normative spec guessed from a data dump would be worse than an absent one. |
| `spec/decode-pipeline.md` | **Planned, still not shipped as of v0.4.0.** The decode order, in RFC 2119 language. Two runtimes that decode in different orders will disagree on identical input. Writing it needs the decode implementation, which is engine code and has not been supplied — and a normative spec guessed from a data dump would be worse than an absent one. |
| [`docs/extraction-plan.md`](docs/extraction-plan.md) | Informative: where each file was seeded from, and what v0.1.0 promised. |
| [`docs/lexicon-port-divergence.md`](docs/lexicon-port-divergence.md) | Informative: a measured disagreement between two ports of the injection lexicon — 13 patterns that behave differently, in both directions, and why no data file was changed because of it. |
| [`docs/lexicon-port-divergence.md`](docs/lexicon-port-divergence.md) | Informative: a measured disagreement between two ports of the injection lexicon — 13 patterns that behave differently, in both directions. Most of it is still open, and the two rows that closed in v0.4.0 closed because the runtime that owns the value decided, not because this document found them wrong. |
Every JSON file carries a top-level `version`. Every normative specification carries a
`Status: normative` marker. Rows marked **Planned** are named here because the layout is
part of the contract, but the file does not exist yet — they are not links, and nothing in
v0.3.0 depends on them.
v0.4.0 depends on them.
Each data file records its own provenance and, in `verified`, how strongly it is backed.
`calibration/calibration.json` is currently the one file that says `false`: it was
@ -105,7 +105,7 @@ Four cases remain unshipped, for the carrier and secret-egress tables, and neith
blocked on effort. Carriers has no adoptable id space: one runtime labels the same carrier
differently depending on pipeline stage, which would make a case's verdict depend on the
entry point it was measured through. Secret egress is not an id question at all — the two
runtimes carry *different tables*, 18 entries against 25, cut at different granularities.
runtimes carry *different tables*, 19 entries against 25, cut at different granularities.
`conformance/manifest.json` records both blockers under `scope_planned.blockers`, measured,
so the gap is visible rather than inferred.