Commit graph

37 commits

Author SHA1 Message Date
d467324380 feat(signatures): the staleness we disclosed is closed by reading the module, not the message
secret-egress.json 0.2.0 -> 0.3.0. `OpenAI Legacy API Key` enters at order 17,
second to last; JWT stays last because ordering.last_entry_is_load_bearing says
it must. 18 -> 19.

The regex was in the coord message that reported it. That is the path
evidence_limits explicitly ruled out, so it was read out of the module text at
a pinned public commit instead: refs/heads/main = 47905da, and 088e458 (which
carries the entry) confirmed an ancestor with `git merge-base --is-ancestor`
rather than accepted from their log.

The entry is the smaller half. All 19 positions were compared against the
module - name, source, flags, order - with 0 divergences, so positions 0-16 are
no longer resting on a 2026-08-09 transcription whose module fidelity stood
recorded as llm-security's assertion. It is reproduced now, and both the
fidelity bullet and the staleness bullet retire.

manifest.json 0.3.2: the blocker prose promised its note would stand until this
landed. Item (2) is marked closed and the count moves 18 -> 19. The blocker
itself does NOT close - 19 against the guard's 25 at different cut points is a
table reconciliation nobody has performed, and one closed hole is not that.

Verified: JSON well-formed, orders contiguous 0..18, count matches array length,
new pattern compiles in Node bare, Node `u` and Python `re`, and does not match
sk-ant-/sk-proj- shapes. Charter clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLEZ4XCSnSrQUFA8SzkQB4
2026-08-11 21:52:24 +02:00
4187715ab7 docs(divergence): our own form has a number now, and it is quadratic
The guard answered the span-byte question with evidence rather than preference:
their [^><] is a measured ReDoS fix (cff0437, 2026-07-31), and they asked us to
measure our form rather than take their word for it. We did.

Node v25.8.2 and Python 3.14.0, patterns read out of the lexicon, run over
'<script ' repeated — the unit that denies the pattern the > it needs:
25.5ms at 16k chars, 6681.6ms at 256k. x4.0 per doubling against the guard
form's x2. Quadratic against linear, reproduced in both engines. It is
scan-position quadratic, not catastrophic backtracking: <script matches at ~n
positions and [^>]* scans to end of input at each one.

Counted across the whole file instead of stopping at the row we were told
about: 8 of 83 patterns carry a bare [^>], none carries [^><]. Six are the
bounded html-obfuscation family — measured linear, so bounding does work, at
the recall cost this file already records. Two are unbounded, and the second
one, hybrid-xss:iframe-src, had not been named by any party: 16.9s at 256k
chars. That row is ours, found by counting rather than by being told.

No data file touched. Both patterns stay byte-identical to llm-security's
source, because behaviour preservation is not suspended when a measurement is
unwelcome — commons carries [^>] on provenance and would carry [^><] had the
source been the guard's. The finding is routed to the owning runtime instead,
which is exactly the path SECURITY.md promised an outsider three commits ago.
First time that route is walked from the inside.

Also recorded: the guard's own disclosure that their suite passes under both
forms (717 green), because their DoS row repeats '<script>' — which supplies
the > and never exercises the run.

Verified: JSON well-formed, every spec still normative, charter clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T85QqeiWBEoBMjWaMiBnxD
2026-08-11 14:13:29 +02:00
0e765a02eb docs(security): the attack surface here is data, so the report route had to say where a wrong entry gets fixed v0.3.1
org-ops recorded SECURITY.md as missing against the org standard (coord,
2026-08-11) and this repository owed it for a sharper reason than "given what
the repo is about": nothing here runs, so a report is never a crash — it is a
detection entry that looks like it works and is not looking.

SECURITY.md therefore answers what an ordinary policy does not have to: how to
report that a detection-table entry is WRONG, and why a confirmed defect in
extracted data is decided in the runtime it was extracted from before it is
changed here. Correcting it here would make the copy disagree with the
implementation it was taken from — two runtimes, two answers on one input, the
exact failure this repository exists to prevent. Two classes skip that routing:
a real secret in the history, and data authored here rather than extracted.
Fix latency is stated plainly as bounded by the owning runtime's schedule and
the consumer's pull, not by ours.

secret-egress 0.1.0 -> 0.2.0 is a staleness DISCLOSURE, not a data change: all
18 patterns byte-identical, one evidence_limits entry added. llm-security
reports the source table at 19 entries now; recorded as their report and not
reproduced, because the commit carrying it is not on their public remote —
measured at b1ba1fb today. What was measured here: none of the 18 patterns
matches a legacy sk-...T3BlbkFJ... shape. A consumer vendoring this file
under-matches the seed hook by one entry, and now reads that in the file.

manifest 0.3.0 -> 0.3.1 corrects the secret-egress blocker. Through 0.3.0 it
named gcp-service-account-json and openai-api-key-legacy together as ids
"absent here". Measured against the guard at e671edb by running this file's own
18 patterns over a service-account document: a COMPLETE service-account key
file is matched here at order 11, since the PEM entry's prefix group is
optional and the bare PKCS#8 header matches; the same document with private_key
removed matches nothing here while the guard's marker still fires. That is a
cut-point difference, which is what the blocker is about, not a missing entry.
openai-api-key-legacy IS a real hole and is now recorded as one. Folded into
the existing blocker string rather than a sibling key, because blockers is a
map from table path to text.

Verified: all JSON well-formed; every non-fixture JSON has a top-level version;
charter clean (no executable code); patterns[] and count byte-identical to HEAD
for secret-egress; manifest key set unchanged and count still 90; 90 case
directories untouched; every spec still carries its normative marker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T85QqeiWBEoBMjWaMiBnxD
2026-08-11 14:03:24 +02:00
d96fbbf791 docs(divergence): the span row had one witness; llm-security measured five more
Their coord message of 2026-08-11T11:01:59Z measured the three <script> forms as
totally ordered by what they match — [^>] strictly contains [^><] strictly contains
the pre-0.7.0 closing-tag form — and named two input classes the guard's narrower
class drops that commons had not recorded: `<script a="<" >x` and `<script<div>`.

Reproduced independently before writing it down, including the strict-superset
property in both directions. Their table came with a claim attached; the claim is
now a measurement here rather than a relayed assertion.

Their argument for [^>] — the narrower class buys an empty false-positive surface,
since anything reaching [^>]-and-not-[^><] already contains a literal <script tag —
is recorded as THEIRS and marked explicitly as not what decided commons' form.
Commons carries [^>] on provenance and would have carried [^><] had the source been
the guard's. Keeping that distinction visible matters more than the regex: if merit
ever becomes the reason a commons pattern moves, behaviour-preservation is gone.

Also records honestly that they asked to hear the guard's reason for [^><] before
commons shipped, and commons shipped first — with why that order is defensible
rather than an oversight. A reason that beats their argument is a reason for the
guard's own port; commons would still track its declared source.

Informative file only. v0.3.0 unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuudQLGMyMenuyeZk8fKs5
2026-08-11 13:44:28 +02:00
4641a7b518 release(0.3.0): a detection pattern changed value — that is new here v0.3.0
v0.2.0 changed what a runtime must DECLARE. This one changes what a conforming
runtime FINDS. A consumer that vendors 0.3.0 and re-runs its suite will see a
finding on inputs that produced none under 0.2.0. In 0.x that is a minor bump by
the rules; the CHANGELOG says so at the top rather than leaving it to be found in
a diff.

The lexicon 0.6.0 entry that sat under [Unreleased] is folded into this release —
committed but never tagged, and 0.7.0 supersedes its central claim (that commons
does not follow the upstream change).

Version sync: README's subtree-add and clone examples move v0.2.0 -> v0.3.0, the
two "as of v0.2.0" markers move with them, the corpus row goes 89 -> 90 cases and
83 -> 84 lexicon cases, and the scope breakdown now says 84 cases over 83 patterns
rather than implying one case per pattern. schema/conformance-declaration.schema.json
gains its own row; the corpus-spec row now mentions the declared table set, since
§1.1 is the reason a reader would go looking.

Verification run before tagging: all JSON well-formed; every non-conformance JSON
carries a top-level version; every spec carries Status: normative; charter guard
clean (no executable code); manifest count == cases[] length == directories on disk
== 90, no duplicate ids; every case's recorded bytes and sha256 recomputed from
disk, zero mismatches, no trailing newlines; measured count_by_scope equals the
manifest's; declaration schema valid draft 2020-12 with its example validating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuudQLGMyMenuyeZk8fKs5
2026-08-11 13:39:51 +02:00
1482c0bc48 feat(schema,spec): give the §1.1 MUST a shape, since v0.2.0 shipped it without one
schema/conformance-declaration.schema.json 0.1.0 — what a runtime publishes
alongside a conformance result: runtime, the commons commit measured, the declared
table set, and the four verdict counts.

Requested by llm-security (coord 2026-08-11T10:46:28Z) in those terms, with the
reason that matters: two runtimes publishing free-form declarations makes
"83/83 + 6 not-applicable" unparseable by anyone but its author, which is most of
the value of the MUST gone.

The schema does more than name fields, and each addition beyond their ask exists to
turn a §1.1 MUST into a failure rather than a sentence:

- The two arithmetic invariants §1.1 implies but cannot state unambiguously in
  prose — counts MUST sum to total, total MUST equal the corpus case count at the
  commit measured — are stated normatively in `invariants`. JSON Schema cannot
  express either.
- The enumeration arrays are REQUIRED whenever their counts are non-zero, which is
  §1.1's "MUST still be enumerated" made checkable. A bare count would satisfy the
  arithmetic and defeat the purpose.
- `error` and `not_applicable` stay structurally distinct. §1.1 turns on that
  distinction: the first means this runtime tried and could not, the second means
  the question was never addressed to it.
- All five counts are required including zeros. An absent count is
  indistinguishable from one the runtime never tracked.
- `declaration_source` records whether the declared set is DERIVED from the
  constant the runner uses to accept or reject a scope, or hand-maintained beside
  it. Only the derived form makes the anti-narrowing fence structural: narrowing it
  is then a code change with tests behind it. Formulation owed to llm-security, who
  stated the constraint for their own side before commons had a field for it.

Deliberately not a gate. Nothing in this repository runs (charter), and no
validation was asked for. Publishing the shape is the whole contribution.

Mutation-tested rather than eyeballed: the example validates as written, and five
distinct defect classes are rejected — missing enumeration when n/a > 0, missing
enumeration when failed > 0, a dropped zero-count, a non-conforming table path, an
unknown property. The example's `$comment` sits outside the example object because
`declaration` closes additionalProperties, so an example carrying its own $comment
would not validate against the schema it illustrates.

Its six example case ids were checked against the directories on disk — the first
guess had one wrong name. The caseIdList pattern was checked against all 89
existing ids, not just the new shape.

spec §1.1 gains the normative pointer plus a SHOULD that a runtime derive its
declared set from the runner's own constant and record which it did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuudQLGMyMenuyeZk8fKs5
2026-08-11 13:39:37 +02:00
25a2cf9643 feat(conformance): the witness case, and the derivation rule that had no room for it
conformance/hybrid-xss__script-tag--src-no-close/ — input `<script src=x.js>`,
17 bytes, expecting hybrid-xss:script-tag. This is the regression gate for the
convergence in the previous commit, and the reason the corpus could not see that
change coming: the existing hybrid-xss__script-tag input `<script>steal()</script>`
matches the pattern under BOTH forms, so it passes either way.

Mutation-verified in both directions across all 90 cases: reverting the pattern to
its 0.6.0 form fails this case and only this case.

The case-id derivation blocked it, and the fix is an extension rather than a
workaround. case_id_derivation gains an optional `--<variant>` suffix; the reverse
transform truncates at the first `--` then maps `__` to `:`. `--` was measured
absent from all 83 ratified pattern ids and all 89 pre-existing case ids, so the
reverse transform stays purely lexical — no lookup against the id list — which is
the property the original one-to-one rule was protecting. No existing case id
moves, so this is additive.

one_case_per_pattern_id is removed, superseded by variant_suffix.supersedes, which
quotes its text. It documented the constraint rather than carrying data a consumer
matches on, but a removed key is normally breaking here, so it is called out.

That same rule cost a real case: omitted_payloads gains
derivation_ground_withdrawn_in_0_3_0. The guard's seventh active-content payload
was omitted on TWO grounds and this change retires one. The other stands — its
in-scope finding set is identical to a case already built — so the payload stays
omitted, on one ground instead of two. It is NOT added back; that is a separate
decision, not a consequence of this one.

First case input authored in this repository rather than reproduced verbatim from
a runtime's payload set, so it goes in a new authored_payloads block instead of
being folded into payload_provenance, whose value is exactly the claim that its
inputs are verbatim upstream. That claim stays as strong as it was: 83 of 83.

Both witnesses for this axis were named by llm-security on 2026-08-10; this is the
first of the two. It was declined that day on the ground that a fixture encoding a
DISAGREEMENT is worse than an absent one — it then contradicted commons' own
published lexicon. Lexicon 0.7.0 removed the contradiction. The stated order was
"settle the row, then the case is trivial to add".

Findings measured through the guard's public API (scan_lexicon,
scan_active_content) at 0dce50f / 0.5.0 — not read off the regex. The same harness
reproduced the existing case's committed bytes and sha256 in the same run as a
control, which is what licenses trusting its output for the new one. Digest
independently recomputed with shasum over the file on disk: agrees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuudQLGMyMenuyeZk8fKs5
2026-08-11 13:39:19 +02:00
c1b2385de6 fix(lexicon): converge script-tag on its source — re-extraction, not revision
hybrid-xss:script-tag drops the closing-tag requirement, becoming byte-identical
to llm-security's RegExp.source at b1ba1fb (injection-patterns.mjs:170). Verified
by compiling both, not by reading them.

Why this does not breach behaviour-preservation: that invariant forbids commons
from CORRECTING seed data it believes is wrong. It does not forbid re-reading the
source after the source itself moved and its owner asked. This file's declared
provenance is llm-security's injection table, and being loadable verbatim by that
table's owner is the one thing it exists to do. The standing alternative was a
per-pattern override in their loader — a published core its own source repository
could not load as published.

Direction is what makes it safe: the new form matches a strict superset of the
old, so it can add matches and cannot introduce a false negative. The reverse
change would not have been adoptable on this reasoning.

NOT decided by the 2-of-3 majority across the three ports. A count of
implementations is not a mandate over detection data; the provenance argument
would hold with the guard on either side. Operator decision, 2026-08-11, on
llm-security's blocking coord request (their 10:46:28Z message, step-4 swap
parked behind it).

Measured collateral: none. Full corpus run under both patterns — 84/84
lexicon-scoped cases pass under 0.7.0, and exactly one case's finding set differs
between the forms. The widening added no finding to any other case's input.

source_fidelity keeps patterns_byte_identical_to_source at 83 and gains the field
it was missing: byte_identical_against_commit: b1ba1fb. Against the original
extraction commit b0de0ca the count is now 82/83, recorded explicitly.
post_extraction_drift is marked resolved and carries before/after pattern text,
so a consumer diffing against either commit has a coordinate for what it finds.

The divergence row is closed on the closing-tag axis after reversing twice in
three days. What stays open is the one-byte span difference: the guard excludes
`<` from its negated class and the other two do not, so `<script <x>` matches
commons and llm-security but not the guard. Measured by compiling all three.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HuudQLGMyMenuyeZk8fKs5
2026-08-11 13:39:01 +02:00
6f79a6ea57 fix(lexicon,docs): the script-tag row reversed — commons is now the sole diverger
llm-security commit 90f576f (2026-08-10) dropped the closing-</script>-tag
requirement from hybrid-xss:script-tag, matching what the guard has always had.
Both now agree in shape (no closing tag required), still not byte-identical
([^><] vs [^>]). Commons' own extracted pattern is unchanged under
behaviour-preservation, so it is the only one of the three still requiring a
closing tag — the opposite of what the divergence doc said when it was written.

injection-lexicon.json gains source_fidelity.post_extraction_drift: the
existing 83/83-at-b0de0ca claim is untouched (extraction-commit fidelity never
decays), and the new 82/83-at-b1ba1fb count carries its own commit and date so
it doesn't read as a bare, decaying "vs HEAD" claim.

Verified directly against both upstream trees this session, not relayed:
llm-security's public remote re-fetched to b1ba1fb, guard read at HEAD 0dce50f.
2026-08-11 12:45:44 +02:00
532d70d5ed release(0.2.0): the contract gained a MUST, so the tag has to move v0.2.0
A runtime that conformed to v0.1.0 does not conform to this one until it declares the
set of commons data files it implements (spec section 1.1). In 0.x that is a minor
bump by the rules, but it is breaking in substance, and the CHANGELOG entry says so
rather than leaving a consumer to discover it in a failing suite.

Both consumers were notified by coord before this tag existed, which is the wrong
order in one respect: they were told to expect a contract change they could not yet
vendor. README says to vendor a tag and never main, so the notice was unactionable
until now. Tagging closes that gap rather than opening a new one.

Version sync: README's subtree-add and clone examples pinned v0.1.0 and now pin
v0.2.0; the subtree-pull example is a placeholder rather than a version, since it
demonstrated "move to a newer tag" while naming the current one. The two remaining
v0.1.0 mentions are historical statements about what that release promised and stay
as they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVouC9nsfrfV5jRSejxbvQ
2026-08-11 06:27:23 +02:00
946f51d35e fix(active-content,conformance): cite line numbers per commit — they do not resolve at the pin
pattern_id_space.verified cited active_content.py:337-369 and :309. Those resolve at
de09711, where the check was run; this file's provenance pins 0bf0729, where the same
six call sites are at 316-348 and the emitter at 288. The 23-line scan-cap insert
shifts everything below it by 21, so a reader following the pin landed on the wrong
lines - and on lines that look plausible rather than obviously wrong.

Same defect class as the at_commit_note corrected before the first commit, one layer
deeper: a measured fact stated without the coordinate it is true in. Both commits'
numbers are now given, plus the symbol names, which are stable across the diff and
are what a reader should actually match on.

omitted_payloads[0].source gets the same treatment - coverage.py:484 is de09711-
relative, and the structural description now carries the load instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVouC9nsfrfV5jRSejxbvQ
2026-08-10 21:16:55 +02:00
bdcb1f1080 feat(conformance): ship the six active-content cases; the id space already existed
The corpus goes 83 -> 89 and scope_covered gains signatures/active-content.json.

The blocker in STATE dissolved under measurement, the same way last session's
13-pattern one did. "An id space for carriers/active-content/secret-egress" was one
question in name only; the three tables have three unrelated problems:

- active-content needed NO id space invented. label_format ("active:{class}") and the
  constructs keys were already extracted verbatim from the seed runtime, and their
  concatenation IS what it emits - verified by comparing the six keys to the six class
  strings at its _flag call sites. What blocked these cases was never naming; it was
  spec section 1, fixed in the parent commit.
- carriers has no adoptable id space AND an entry-point dependence underneath it.
- secret-egress is not an id question at all: the two runtimes carry DIFFERENT tables.
  18 entries here against the guard's 25, cut at different granularities - this file's
  single `GitHub Token` is four ids there, `Private Key PEM Block` three, `Database
  connection string` four - with membership diverging both ways. `aws-access-key-id`
  is the one clean 1:1, which is why exactly one egress case was ever offered. That
  number was a symptom, not modesty.

Both blockers are now recorded under scope_planned.blockers, measured, replacing a
blanket "no runtime has agreed to an id space" that was wrong for both.

Generated from measurement, not written. Payloads were extracted from the seed
runtime's coverage.py by AST - evaluating each _scan_case argument in that module's
own namespace rather than retyping detection data - then run through its public
output gate, the same entry point the 83 lexicon cases used. The fixtures were then
re-read from disk by a separate checker that re-computed every digest, re-scanned the
bytes and applied exact-within-scope independently of the generator: 6 cases, 0
failed checks.

Six built from seven offered. The runtime's matrix drives two payloads at
`active:markdown-image`; measured, their in-scope finding sets are identical, and the
second's only distinguishing signal (entropy:base64-blob) falls outside every table
this repository publishes. Dropped rather than given a discriminated case id, and
named under omitted_payloads so the count reads as a decision.

These six prove LESS than the 83, and the manifest says so: their payloads come from
the only runtime implementing the table, so no second implementation's agreement
could be measured. They pin one runtime's behaviour as a contract a future
implementer can be held to - less than cross-runtime agreement, more than nothing.

llm-security's absence of the table is measured at b0de0ca, not assumed: a tree-wide
search finds no implementation, and `git log -S` over --all returns zero commits,
closing the "it was there once" reading. Absent table is not absent capability -
their entropy scanner reaches markdown-image URLs by another route - and the manifest
says that too.

Provenance and measurement for the active-content half are kept in their own blocks:
a different source structure at a different commit, and one pin must not stand for
two measurements. The guard's HEAD moved twice during the work (3c56d50 -> de09711 ->
398eb74); measurement ran at de09711 and the drift is recorded, including that
active_content.py is NOT identical to the 0bf0729 the data file pins - the change
adds a scan-cap self-safety finding and touches no construct.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVouC9nsfrfV5jRSejxbvQ
2026-08-10 21:15:25 +02:00
807c0d45d8 feat(spec): add not-applicable, so a single-runtime table stops reading as 7 defects
Section 1 required every case to be run and every unrunnable one reported as an
error. Scoping a case to signatures/active-content.json would therefore have made
llm-security permanently fail seven cases for having no such table - reporting an
architectural difference as a defect, and telling a reader nothing.

Section 1.1: a runtime declares which commons data files it implements, and a case
scoped outside that set is `not-applicable` - a third verdict, distinct from section
1's error. Section 1's error means the runtime tried and could not; this means the
question was never addressed to it.

Fenced so it cannot become an exit. It attaches to a TABLE, never to a case, since
per-case opt-out is exactly the silent skip section 1 forbids. A declared set may not
be narrowed to convert failures into not-applicable ones. Such cases stay in the
denominator: `76/83` and `76 passed, 7 not-applicable` describe different runtimes,
and only the second can be checked.

Three consequences, written where they are read:

- Section 4 no longer claims scoping "asks a question both can answer". That held
  only while every case was scoped to the one table both runtimes implement. Scope
  narrows what is compared; it does not make every runtime a valid addressee. The
  superseded sentence is named in place rather than edited away.
- Section 4 now states that "belongs to a data file" means published there, never
  "shares its prefix". Live witness: the guard emits `active:oversize-input`, a flag
  about its own scan cap, which carries the prefix but is no construct in the table.
  A prefix-matching runtime would fail a case over a finding the corpus never claimed.
- Section 6 states the derivation's cost: case_id derives from pattern_id alone, so a
  single-finding scope holds at most one case per pattern id. When a source runtime
  drives two payloads at one pattern, they must be compared within scope before a
  second case is minted - and a discriminated case id is forbidden, since it would
  break the reverse transform.

Section 8: a pass count is unreadable without the declared set beside it, and a
not-applicable verdict proves nothing about detection in either direction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CVouC9nsfrfV5jRSejxbvQ
2026-08-10 21:14:58 +02:00
a1578e6f3f fix(conformance): record the guard's internal-surface position on _LEX_PAYLOADS
llm-ingestion-pipeline-security states (coord message 2026-08-10T12:42:31Z)
that _LEX_PAYLOADS and the 83 pattern ids are an internal surface on their
side, with no README/CHANGELOG/docs statement promising id or payload
stability. Their gate is their own test suite, not a promise to this
repository. Their stated position: a future payload change diverges the
pin and should be re-pinned, not treated as a broken contract.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJTZgfnjsaN14ti5HXiKhQ
2026-08-10 20:47:44 +02:00
4d351d2a31 fix(mapping): state that three of four OWASP maps have no production consumer
llm-security reports (repo-wide grep on their side, coord message
2026-08-10T12:38:19Z) that OWASP_AGENTIC_MAP, OWASP_SKILLS_MAP and
OWASP_MCP_MAP have no production consumer at all — only OWASP_MAP is read
by running code, and only as a fallback when a finding lacks an owasp
field. "Extracted without behaviour change" was true but vacuous for
three of the four maps; this records that instead of leaving it implied.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJTZgfnjsaN14ti5HXiKhQ
2026-08-10 20:47:37 +02:00
f082a91995 fix(lexicon,docs): retract the claim that the guard's port cites severity.mjs
The guard asked twice which file:line carried it. Tracing it produced an
uncomfortable answer: no file of ours measured it. The claim arrived from
llm-security (coord 20260809T201048Z) and was written down here as a commons
finding in three places.

Measured against the guard's own tree: severity.mjs has never appeared in
src/llm_ingestion_guard/injection_lexicon.json at any point in that file's
history (git log -S: no commits), and at 0bf0729 - the commit our manifest
pins - the only tree-wide occurrence is docs/PLAN.md:114, correctly about the
report module. Their only lexicon source statement is the note at line 3, and
it names injection-patterns.mjs. The right file.

No detection data moves. hybrid severity is still high, still sourced to
injection-patterns.mjs:274-281 at b0de0ca; severity.mjs still has zero
occurrences of the four pattern arrays, re-measured today. Only the sentence
about the other repository falls.

Marked in place, not edited away. The claim survived review because it came
bundled with a correct measurement of the same question - a wrong citation to
a right value, which is exactly what the field it lived in was written to warn
against.

lexicon/injection-lexicon.json 0.5.0 -> 0.5.1 (provenance metadata only).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DWKkmKQBsWtrkod4MusUBC
2026-08-10 20:37:53 +02:00
0ffee85a4b fix(conformance): name the fixture field pattern_id, not id — the schema already owns that word v0.1.0
schema/finding.schema.json defines a finding `id` as DS-<scanner>-<counter>,
built from a process-global counter: stable across neither runs nor
processes, and the schema says so itself. The corpus keys its comparison on
the lexicon's stable rule identity. Two normative documents in one
repository using one word for both would produce runtimes failing every case
for a reason unrelated to detection.

Also adds spec section 3.1, which publishes the bridge a consumer actually
needs and which neither normative document named: a runtime's own label
reaches a pattern_id through the lexicon's `aliases` object, and a runtime
absent from that object has no published way to be compared -- a mapping
kept privately in a consumer is the drift this repository exists to prevent.

Regenerated all 83 fixtures; re-verified from the corpus alone against both
runtimes, 83 cases, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhXDL82FRrQWedEmUg12Pj
2026-08-10 04:44:59 +02:00
49e1e79807 feat(conformance): build the 83-case corpus; the 13-pattern blocker dissolves under measurement
The corpus was blocked on a decision nobody had to take. The 13 divergent
lexicon patterns were measured on witness inputs -- an attribute run padded
past 256 characters, an interior '<', an unclosed <script> -- and the corpus
payloads contain none of those shapes. Run through both runtimes' public
entry points, all 83 patterns produce identical lexicon finding sets, the 13
included. No winner picked, because the question was never reachable from
these inputs.

Measured at each runtime's entry point (scanForInjection() at b0de0ca,
scan_output(source=OUTPUT) at 0bf0729), never at a rebuilt regex table --
the layer mistake the divergence document already had to retract once.

- conformance/<case>/{input.txt,expected.json} x83, plus manifest.json
- spec/conformance-corpus.md, normative: bytes not text, id-only findings,
  exact-within-scope, and observed_out_of_scope as evidence not expectation

Verified by a harness that does not share the generator's knowledge: reads
only the case directories, re-runs both runtimes, checks every field
including digests -- 83 cases, 0 failures. Severity agrees with what commons
publishes 83/83. Deleting the middle third of each input breaks 76 of 83
expectations; the 7 survivors are the shortest payloads, a weak mutation
rather than a weak fixture, and are recorded as such.

Scope is 83 and not 94 for a different reason than expected: the 11
non-lexicon convertible cases have no ratified cross-runtime finding id, and
writing them would mint a contract unilaterally in the same stroke as the
tag. Named in manifest.json under scope_planned.

Docs corrected in place rather than edited away: extraction-plan and
lexicon-port-divergence both claimed the 13 blocked the corpus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhXDL82FRrQWedEmUg12Pj
2026-08-10 04:40:56 +02:00
ff18768b71 docs: retract the "neither runtime misses an attack" claim, fix three counts
Four corrections across the two informative documents, all of them to
claims this repository made and none of them to a data file.

The retraction is the important one. lexicon-port-divergence.md said
neither runtime misses an attack: every witness payload still produced a
finding, so the divergence was only in which labels were raised. That was
wrong in method rather than in arithmetic. The payloads had been run
against the UNION of every pattern table this repository holds - 111 rules
across the lexicon, active-content.json and secret-egress.json - and the
hit that rescued them came from active-content.json, which is the Python
guard's table. llm-security has no active-content table at all. Unioning
two runtimes' tables and reading the result as a statement about each of
them turns combined reach into individual coverage.

Re-measured through llm-security's own entry point scanForInjection() at
b0de0ca - the whole engine, normalisation, homoglyph folding, rot13 and all
four arrays - all three witness payloads return found: false. Not a
different label set: no finding at all. Controls in the same run behave
normally, so the {0,256} window is a real evasion window and the <script>
pattern really does require a closing tag. llm-security reached the same
three results independently and has logged them as a v8.x task.

Second: one of the 13 divergences does not reach report level. On the
iframe witness the guard misses hybrid-xss:iframe-src but its
hybrid-xss:javascript-uri fires at the same severity and the same OWASP
anchor, so a reader of its report still sees the payload flagged. Both
sides were re-measured to confirm. The report-level number is 12; the 13
stands as the number that blocks conformance/, because a fixture names
labels.

Third: extraction-plan.md said six of the confusables have no entry in the
fold map. It is zero - all 13 are keys in the 28-entry map, and
carriers.json has said so correctly since it was written. The prose, not
the data, carried the error, and the number six most likely came from
running the comparison the other way (five fold-map Cyrillic keys are not
confusables). It also called the diverging sets deliberately distinct;
llm-security states the U+0456 / U+0445 divergence is drift, not design,
and has logged it for v8.x. Those two readings imply opposite things about
whether the sets should be expected to converge.

Fourth: the seed-sources section still asserted that sessions here have no
read access to llm-security and that content arrives only as a dump. The
operator changed that on 2026-08-09. It now records the read-only pinned-
commit boundary, the still-absolute prohibition on the sibling working copy
and on writing, and the reason the change matters - four attribution
defects caught so far, every one a transcription artefact.

Status blocks updated: all eight JSON artefacts now exist, seven verified
differentially and calibration.json still honestly verified: false; the
conformance blocker is half-cleared, with naming ratified and the 13
divergent patterns still unresolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:54:41 +02:00
47760d2264 feat(signatures): add the malware identity table, drawn from source not memory
The last missing data file. Seven known-bad-identity signatures over four
families - webshell, reverse_shell, cryptominer, hacktool - reproduced
verbatim from llm-security/knowledge/signatures.json at b0de0ca, key order
included. The file was generated from the parsed source rather than typed,
and provenance pins the source's byte length (2494) and SHA-256 so the
claim is checkable rather than asserted.

Note the family spellings: reverse_shell with an underscore, and
cryptominer rather than miner. The working note this file was planned from
had both wrong. They are policy keys - the engine filters on them and
interpolates them into every finding title - so a rename is a breaking
change, which is exactly why the table was read instead of recalled.

The rules were the easy half. The substance is engine_behaviour_not_data,
which draws the line between the table and the runtime around it. No rule
carries a flags field, because the engine compiles every pattern with `i`
unconditionally at signature-scanner.mjs:48 - so a consumer compiling these
case-sensitively silently under-matches all seven, and the dialect block
records that where a reader will hit it. Also engine, not data: matching
against five decode variants rather than raw bytes, the enabled-families
policy filter, per-file rule dedup, custom-rule merging, and a loader that
defaults four missing fields instead of rejecting a rule.

Two limits are stated as evidence limits rather than left implied. Seven
signatures are not malware coverage; a clean SIG result is not "no
malware", and the seed runtime's own header calls the table deliberately
tight. And three of the seven match on names - xmrig, mimikatz,
meterpreter - so a document discussing those tools matches. The seed
runtime hides that by excluding knowledge/, tests/, docs/ and
node_modules/, which is scan scoping and does not travel with the table.

Verified: 7/7 rule objects field-identical to source including key order,
no non-ASCII bytes, all seven compile in Node bare, i and iu (21/21) and in
Python re (7/7). Charter guard clean - no executable code in the repository.

README and CHANGELOG updated: the file moves out of "planned, not in
v0.1.0" and out of "not included".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:49:21 +02:00
7e2e92eecf feat(schema): close the finding contract against its producer
The schema was seeded from sarif-formatter.mjs, which consumes findings.
That could only ever establish a lower bound on the property set, so
additionalProperties had to stay open. The producer is now readable:
finding() in scanners/lib/output.mjs line 32, at b0de0ca. It returns an
object literal with exactly ten keys and no spread, so the set is complete
and the schema closes.

Added: id and evidence, the two keys a consumer-side reading could not see.
id gets its own definition, DS-<prefix>-<counter>, with pattern
^DS-[A-Za-z]+-[0-9]{3,} - the {3,} because padStart(3) is a minimum, so a
run past 999 findings produces four digits. It comes from a process-global
counter and is stable neither across runs nor across processes; the
definition says so before someone keys on it.

Nullability is evidence now, not convention. Five keys are emitted as null
rather than omitted, so a serialised finding always carries all ten. The
four assigned straight from opts are the exception: omit description and
the key is undefined and disappears from the JSON. Reproduced against the
real producer - ten keys in memory, nine serialised.

owasp is a string, not an array, and not one code. Multiple codes are
joined with ", ". Measured across the seed runtime: 31 distinct values over
157 sites, 13 multi-code, and four that mix taxonomies inside a single
value with no discriminator. That has a consequence nobody had written
down: sarif-formatter builds tags: [f.owasp], so "LLM06, ASI02" becomes ONE
tag with a comma in it and nothing filtering on LLM06 matches. Reproduced
end to end through the real finding() and toSARIF(), logged as
known_lossiness.owasp-tag-not-split. It is consumer behaviour, not data, so
it is reported rather than fixed here.

The JSONL profile is set to "not applicable" rather than "unspecified".
The distinction carries weight: unspecified would assert a profile exists
and has merely not been written down. There is no finding-JSONL - findings
are emitted only inside one JSON envelope. The single module that does
write JSONL, audit-trail.mjs, writes audit events under a different schema
where owasp is an ARRAY. Same field name, different type, same repository.
The profile records that trap instead of leaving a TODO.

Verified: valid Draft 2020-12, every finding built by the real producer
validates, and four negative controls are rejected.

One new open question left unpatched: the producer's JSDoc lists seventeen
scanner prefixes including IDE, while all four maps in owasp-map.json are
keyed on sixteen without it. An IDE finding has no taxonomy mapping
anywhere. Adding the key would be inventing detection data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:46:13 +02:00
be84121604 feat(lexicon): set hybrid severity to high, record the id space as ratified
Two things the file was waiting on arrived, and both are now data rather
than open questions.

families[hybrid].severity was null by design - the seed dump supplied the
array but not the severity. It is "high". The value is not stored anywhere
as a field: the engine assigns it by pushing HYBRID_PATTERNS matches
straight into the high bucket at injection-patterns.mjs:274-281, under the
comment "Hybrid patterns are HIGH severity (v5.0 S6)".

severity_provenance names the file it is NOT in. Both this repository and
the Python guard first looked in severity.mjs, which contains no injection-
family severity at all - grep it for any of the four pattern constants and
you get nothing. The guard's port carries the right value while citing that
file, so a consumer following its citation finds no answer. A wrong citation
to a right value survives review in a way a wrong value does not, so it is
recorded rather than quietly fixed.

pattern_id_space.not_yet_confirmed is replaced by a ratification block.
Both seeding runtimes agreed on 2026-08-09: llm-security ratified the 0.2.0
proposal as-is and treats an id change as breaking on the same terms, and
the guard confirmed the space its own port supplied. The id is a
cross-runtime contract now, which is what conformance/ needed before a
fixture could name a finding at all.

alias_evidence.llm_security is sharpened, not upgraded. All 83 alias
strings were confirmed equal to the module's label field in array order, so
the alias is certainly the pattern's name in the table. It is still not
established that a FINDING carries it: output.mjs:finding() emits title and
has no label key. Verified at table level, one level short of where it
would matter.

No detection data moved: all 83 patterns still byte-identical to the module
at b0de0ca, and no pattern entry differs from HEAD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:41:06 +02:00
344bc52adf fix(lexicon): retract two false provenance claims, reach 83/83 to source
The lexicon was seeded from a transcribed dump. With the source module now
readable read-only at b0de0ca on the public remote, it was imported in Node
and compared entry by entry - RegExp source, flags, label, family, array
order. That check contradicted the file twice, in mirror-image ways.

The file claimed it had rewritten raw code points into \uXXXX escapes in
homoglyph:cyrillic-latin-mix and unicode:zero-width-in-word, and carried
byte_identical_to_source: false for both. The module already writes them
escaped. Nothing was ever rewritten. The stored pattern text was correct
throughout - only the account of its origin was wrong, because the dump had
rendered the module's escapes as the characters they denote and this
repository escaped them back.

The same inversion ran the other way in multi-lang:french, which stored a raw
accented Latin e where the module writes the é escape. That was the one
pattern of 83 not byte-identical to source, and it is corrected here, making
it 83/83. Both spellings are the same regular expression: compiled in Node
bare and under u, and in Python re, over accented, unaccented, uppercase and
non-matching French input, with identical match offsets. No behaviour moved.

Exactly one pattern entry differs from HEAD, and no pattern in the file
contains a non-ASCII byte any more - which matches the module, whose regex
literals are ASCII throughout. A third claim fell with them: the translation
note asserting that the leetspeak and multi-language patterns embed accented
Latin directly was wrong in both halves.

normalisations is now [] with a normalisations_note, the convention already
in signatures/secret-egress.json. A new source_fidelity block carries the
counts, the method, the verified class membership, and both retractions in
full - retracted, not deleted. The equivalence evidence behind the withdrawn
claim (692 Node comparisons, 236 Python) is still a true statement about raw
versus escaped; it is just no longer load-bearing.

Provenance upgraded accordingly: source_commit pinned, and the caveat saying
these checks only proved agreement with a dump is gone, because they no
longer do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:39:32 +02:00
95dea9ba90 feat(mapping): name each taxonomy, close the edition question
All four maps were re-verified directly against
llm-security/scanners/lib/severity.mjs at b0de0ca on the public remote,
not against the transcribed dump they were seeded from. Every entry, key
order, codes_used and unmapped_prefixes reproduce exactly, so
source_commit is now pinned and the dump-fidelity caveat is dropped.

taxonomy_name was null on all four. It is now set:

  OWASP_MAP         OWASP Top 10 for LLM Applications 2025
  OWASP_AGENTIC_MAP OWASP Top 10 for Agentic AI Applications 2026
  OWASP_MCP_MAP     OWASP MCP Top 10 2025, beta
  OWASP_SKILLS_MAP  not an OWASP taxonomy at all

The last one is the finding, not the formality. Despite the
OWASP_SKILLS_MAP constant name there is no OWASP skills edition; AST is
derived in-house from Snyk ToxicSkills (Feb 2026) and ClawHavoc (Jan
2026). A consumer that trusted the constant name would print AST codes
as OWASP codes. taxonomy_warning says so in the data.

MCP is the one taxonomy checked against the standards body rather than
only against the consumer: owasp.org confirms an Incubator project in
Phase 3 (Beta Release and Pilot Testing), published as 2025 v0.1. That
check also surfaced a name divergence - OWASP calls MCP06 "Intent Flow
Subversion", llm-security calls it "Prompt Injection", and MCP06 is the
most used MCP code in this map. Logged as an open question; this file
carries codes, not names, so no entry moves.

The edition open_question closes; two narrower ones replace it rather
than being folded away.

Detection data verified unmoved against HEAD: prefixes, totals, and all
four map objects byte-identical, key order included.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNMcqrfNyoLRQ7qXUFZnb9
2026-08-09 22:33:50 +02:00
f68f1e19d5 docs(lexicon): split the alias evidence, correct a false absence claim
Advisor review found six gaps the session's own checks did not cover. The one
that mattered was outward-facing: the divergence report told the guard repo
that two sub-agent bounds were undocumented. That was wrong. lexicon.py's
module docstring documents them explicitly under "Bounded token gaps", and
git log -S dates {0,12} to the original port commit f397cd9 and [^><] to the
ReDoS fix cff0437. Every divergence on the guard's side is documented and
traceable. The claim rested on two sed slices of one file; an absence claim
needs a search over the whole repository. Corrected here and by coord.

lexicon/injection-lexicon.json 0.2.0 -> 0.3.0:

- pattern_id_space.alias_evidence records the two aliases separately instead
  of averaging them. llm_ingestion_guard is verified — coverage.py asserts on
  that exact string, so it is demonstrably what a guard finding carries.
  llm_security is not: it is the pattern table's name, the finding producer
  was never supplied, and the known Node finding shape uses title, not label.
- normalisations[].affects now keys on id, with the prose names kept beside it
  as affects_labels. An internal cross-reference on label was a second
  identity space inside the file the id exists to unify.

Detection data unmoved again: labels, patterns, flags and the ids and aliases
added in 7b70f5b are all byte-identical in sequence; 166/166 Node compiles.

Also: the README lexicon row described thematic families the file does not
have (they are severity families; the theme is the id prefix), and the
conformance convertibility table gained its missing second condition — a case
is buildable only if the label it asserts maps to data this repository
publishes. Eleven cases fail that test (entropy, decoded, the sanitize rows,
the OKF scans), so the buildable set is ~94, not 105.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:56:45 +02:00
7b70f5b52f feat(lexicon): publish a commons-owned pattern id with per-runtime aliases
A conformance fixture has to name a finding. The two runtimes that seed this
repository do not name the same pattern the same way -- llm-security says
"override: ignore previous instructions", the guard says
"override:ignore-previous" -- so until now no expected.json could be written
at all, including for the 64 patterns whose regex is byte-identical in both.

Each pattern now carries `id` plus an `aliases` object naming what each
seeding runtime calls it, and a top-level pattern_id_space block explains the
field, its provenance and its stability contract.

The id is adopted verbatim from the guard's port rather than invented here:
that port already carries both names, so the mapping is read from source data.
Matched by label <-> desc with em-dash normalised to hyphen, 83/83 one-to-one,
83 unique ids.

No detection data moved, and that is proven rather than asserted: labels,
patterns and flags are byte-identical in sequence, no flags key was invented
(78 before, 78 after), and stripping id + aliases + pattern_id_space
reproduces the previous committed file byte for byte -- 23566 bytes. All 83
patterns still compile in Node bare and under u (166/166) and in Python re
(83/83).

Neither consumer has ratified this id space; both were asked by coord today,
and the file records it as a proposal rather than implying agreement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:46:48 +02:00
22d3a78591 docs: measure the lexicon port divergence, correct the case count
The guard's coverage.py turned out to be readable from a session here — the
read boundary covers llm-security only. Counting it instead of estimating
corrected two published numbers and surfaced the real conformance blocker.

Counted, not estimated: CORE_CASES holds 134 cases (128 caught, 6 gap), not
the "126 classes + 4 gaps-must-hold" the extraction plan claimed. 105 are
convertible to static input/expected; 29 assert a runtime API surface this
repository does not own, and are named as out of scope rather than faked.

Measured while counting, and the reason the corpus is still blocked: the
commons lexicon and the guard's are two ports of one source file, and they
disagree. 83/83 patterns correspond, 64 byte-identical, 6 differ only by
escaping and are proven equivalent by string identity, and 13 behave
differently — with a witness input for each and misses in both directions.
Cause is two different ReDoS mitigations of the same table: the Node side
bounds the run ({0,256}), the guard excludes the anchor character ([^><]).
Each has a recall cost the other does not.

No data file changed. Behaviour preservation is a v0.1.0 invariant, so the
divergence is reported to the owning repositories, not fixed here. The 8
hybrid severities the guard's port assigns are deliberately NOT copied in —
a second-hand port is not the producing module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:40:26 +02:00
93f7bc024f docs: close three honesty gaps the validation pass missed
1. The layout block still described calibration.json as holding "entropy
   floors, scan caps, disposition ranks" — the same never-delivered content
   the seed-source row and the README row were already corrected for. Third
   copy, same defect. Grepped the repo to confirm no fourth: the remaining
   hits either describe the absence or belong to active-content.json's own
   Shannon-entropy floor, which is a real thing from a different source.
   Tightened the calibration log entry so it cannot be read as denying that.

2. Definition of Done item 2 claimed every file is present or "explicitly and
   visibly stubbed". Three are neither — they are absent and marked Planned.
   Item 2 now records that it is unmet, which three files are missing, why
   absent beats stubbed (an empty conformance/ and a contentless normative
   spec would both PASS the mechanical checks in this document while making
   the repo look more finished than it is), and that the tag decision is the
   operator's. The DoD is what a future session reads to decide whether
   v0.1.0 is done; it must not assert a bar the repo does not clear.

3. The lexicon's "83/83 compile in both runtimes" claim had been produced
   against the raw dump, not against the committed JSON — this repo's stated
   standard is that a fidelity claim comes from a command over the artifact.
   Re-run against lexicon/injection-lexicon.json: 83/83 in Python re, 83/83
   in Node bare and under `u`. Also confirmed the \uXXXX escapes survive the
   JSON round-trip with the right semantics — the zero-width class matches
   U+200B and rejects U+00AD, the Cyrillic class matches U+0430 and rejects
   U+0445. The claim stands as written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:20:06 +02:00
4742c067a8 docs: unlink the three files that do not exist yet, correct the changelog
repo-standard's gate flagged 10 dead internal README links. Seven now
resolve because the files landed this session. The remaining three are
unlinked and marked "Planned, not in v0.1.0" rather than invented:

- signatures/malware-signatures.json — seed data not delivered
- spec/decode-pipeline.md — needs the decode implementation, which is engine
  code and was not supplied. A normative spec guessed from a data dump would
  be worse than an absent one: it would carry a Status: normative marker and
  pass every convention check while saying nothing anyone verified.
- conformance/ — corpus form is still an open decision

The changelog claimed 0.1.0 shipped "a conformance corpus". It does not.
Rewritten to enumerate what is actually present, to state which files were
proven differentially and which one (calibration) carries verified: false,
and to name what is not included. Moved back under [Unreleased] because the
tag has not been cut.

README also now tells a reader that each data file records its own provenance
and verification strength, and which file currently says false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:15:54 +02:00
8ee297c585 feat(calibration): add calibration.json, marked transcribed-only
The risk-score tier constants, verdict thresholds, risk-band cutoffs and
posture grade thresholds. Constants only — the log scaling and the if/else
chains that consume them are engine and stay in the consumer.

This file is the exception in this repository and is marked as such in its
own verification block. Every other data file here was rebuilt from its
commons JSON and diffed against an imported module. This one arrived as a
human-written PROSE SUMMARY of severity.mjs, so there was nothing to import
and nothing to diff. verified: false, with the two skipped checks named
rather than a caveat attached to a pass.

The consequence inverts this repository's central rule and the file says so:
for every other file a disagreeing consumer is wrong; for this one, until the
module arrives in executable form, a disagreement is not automatically the
consumer's bug.

What COULD be checked was: bands are contiguous and non-overlapping across
0-100, the BLOCK trigger (65) is exactly the Critical band floor, the WARNING
trigger (15) is exactly the Medium band floor, and the per-tier reachable
minima recompute exactly (80/48/20/4, exact because log2(2) is 1).

Corrects README and the extraction plan in the same commit: both promised
entropy floors, scan caps and disposition ranks. None arrived — 0 occurrences
each across the whole dump. Named in the file under not_supplied so the
absence is visible rather than inferred, and both rows now describe what is
actually present.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:14:13 +02:00
dd6ae51d60 feat(mapping): add owasp-map.json from verified llm-security dump
Four parallel taxonomy maps — LLM, ASI (agentic), AST (skills) and MCP —
each keyed by the same 16 scanner prefixes, so a finding can be placed in
whichever taxonomy a report is written against.

Proven, not transcribed: each exported object was rebuilt from the commons
JSON alone and compared against the imported dump module — 4/4 identical on
keys, order, values and empty arrays. The shared 16-key order was verified
across all eight objects rather than assumed, and the count is the counted
one (the dump's own aside says 14).

Empty arrays are data and are preserved as arrays: agentic TRG/AST, skills
WFL/SIG, mcp WFL/TRG/SIG/AST all mean "deliberately mapped to nothing", not
"gap to fill".

Recorded as an open question in the file rather than papered over: the dump
does not state which EDITION of each taxonomy the codes belong to. OWASP's
LLM Top 10 was renumbered between editions — LLM06 is Excessive Agency in the
2025 list, with earlier entries consolidated and LLM07/LLM08 newly added — so
a bare LLM06 does not identify a risk. Two runtimes can match this map
perfectly and still disagree about what a finding means, which is the exact
failure this repository exists to prevent. taxonomy_name is left null rather
than guessed; the question goes to llm-security.

Scanner prefix meanings were not supplied and are reproduced as opaque keys.
Verification log in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:11:50 +02:00
c3852c9148 feat(signatures): add secret-egress.json from verified llm-security dump
The 18 fixed credential and token shapes a pre-write guard matches before
content is persisted: cloud keys, vendor tokens, PEM blocks, connection
strings, JWTs.

Proven, not transcribed: the table was rebuilt from the commons JSON alone
and diffed against the imported dump module — 18/18 identical on name, source
and flags, and all 18 byte-identical, so no normalisation was needed. All 18
compile in Node bare, Node under `u`, and Python `re`.

Array order is normative and is tested as such, not merely asserted: a Bearer
header containing a JWT must be labelled "Authorization header with token"
rather than "JWT (three-part token)", which is why the source puts the bare
JWT entry last. Reproduced from the commons order, and shown to change under a
reversed table. Every entry carries an explicit `order` field so a JSON
round-trip cannot reorder the contract silently.

Corrects the extraction plan's seed-source row in the same commit: it named
knowledge/secrets-patterns.md, but the dump named hooks/scripts/
pre-edit-secrets.mjs and stated the two are different tables. Recording a
source file that was never delivered is the defect class this repository
already caught once in finding.schema.json.

Neither severity nor disposition was supplied, so neither is invented — the
source table carries a name and a pattern and nothing else. Verification log
in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:10:01 +02:00
f4aa8b66f4 feat(codepoints): add carriers.json from verified llm-security dump
Six independent carrier tables: zero-width characters (5), the Unicode Tags
block with its subtraction decode rule, the two Supplementary Private Use
Areas, BIDI controls (9, the Trojan Source class CVE-2021-42574), the
Cyrillic presence set (13), and the fold-to-Latin homoglyph map (28).

Proven, not transcribed: five of the six tables were rebuilt from the commons
JSON alone and diffed against the imported dump module — every constant
identical, and the homoglyph map identical down to insertion order. Folding a
corpus through the rebuilt table and the source table gives identical results.

The tables overlap but are NOT merged, and cross_table_notes states each
divergence as fact: the zero-width carrier set includes U+00AD while the
lexicon's pattern class does not; the lexicon class holds U+0456 which
CYRILLIC_CONFUSABLES lacks, and CYRILLIC_CONFUSABLES holds U+0445 which the
class lacks. Reporting that to llm-security, not fixing it here.

Two honest limits, marked in the file rather than smoothed over: the private
use ranges arrived as a source comment with no constant behind them and carry
verified: false, and the dump's own "~25 entries" estimate for the homoglyph
map is wrong — counted mechanically it is 28.

Character names resolved through Python unicodedata against the Unicode
character database, not written from recollection. Verification log in
docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:08:01 +02:00
23cf3e383a feat(lexicon): add injection-lexicon.json from verified llm-security dump
The four prompt-injection pattern families — 21 critical, 32 high, 22 medium,
8 hybrid — with per-pattern label, ECMAScript source and declared flags.

Data only. The dump names checkCognitiveLoadTrap and scanForInjection's
variant building (normalise → fold → rot13 → unicode-tag escalation) as
ENGINE logic that never moves here; they are absent by intent, not omission.

Proven, not transcribed: all four arrays were rebuilt from the commons JSON
alone and diffed against the imported dump module — 83/83 identical on label,
source and flags, 81 of them byte-identical. All 83 compile in Node bare, Node
under `u`, and Python `re`.

Two deliberate deviations from byte-identity, declared in the file: the
zero-width and Cyrillic character classes carried raw code points, four of
them invisible. Escaped to \uXXXX (the convention forbids raw invisible code
points outside conformance inputs — an unreviewable class is where a silent
false negative hides) and proven equivalent by differential match-set
comparison over 208 adversarial inputs, 832 comparisons, 0 differences.
Class membership was counted from the dump bytes: the zero-width class holds
4 code points and does NOT include U+00AD.

HYBRID_PATTERNS' severity was not supplied and is left null rather than
inferred from its siblings. Verification log in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FaYqid3mejFmd9ZHsiHgp3
2026-08-09 21:05:15 +02:00
0433240b14 feat(schema): add finding.schema.json with normative SARIF profile
The finding contract plus the SARIF 2.1.0 output profile, modelled on
llm-security's sarif-formatter.mjs (operator dump). Draft 2020-12, valid
against the metaschema.

The SARIF profile is proven, not transcribed: the mapping was re-implemented
from the commons JSON alone and diffed against the real toSARIF over 10
envelope shapes (all severities incl. unknown/undefined, slug edge cases,
rule-id collision, every optional-field combination, multi-scanner, explicit
version) — 0 differences.

Three things left honest rather than closed:
- additionalProperties stays open. The formatter CONSUMES findings, so fields
  it ignores are invisible in this evidence; the property list is a lower
  bound until the producer is supplied.
- The JSONL profile is status=unspecified with a TODO. "One finding per line"
  is inference, and a guessed normative contract is worse than a missing one.
- Node and the Python guard share only severity+owasp; the rest of the field
  names diverge (scanner/title vs detector/label). Recorded as an open
  question for v0.2.0, not settled silently here.

One published claim was wrong and is corrected: rule-id collision does NOT
cover punctuation, only case and whitespace runs. Caught by executing the
claim instead of asserting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191AKc2qW6tmXDFSx1xn53q
2026-08-09 14:52:53 +02:00
43f682bedd feat(signatures): add active-content.json from verified guard source
The EchoLeak class (CVE-2025-32711): markdown image/link/refdef/autolink,
data: URIs and active HTML, plus the URL-shape analysis that separates a URL
naming a remote document from one carrying bytes outward.

Seeded from llm-ingestion-pipeline-security v0.3.4 @ 0bf0729 (read-only) —
active_content.py's pattern table and calibration.py's severities/floors.
That module documents itself as the canonical home of this table, which is
why the guard is the source here rather than llm-security.

One deliberate deviation from byte-identity, recorded in the file: five
patterns carried `\"` from Python raw strings, which is a SyntaxError under
ECMAScript's u/v flags. Normalised to `"` and proven equivalent by
differential match-set comparison (5 patterns x 30 adversarial inputs, 0
differences). Verification log in docs/extraction-plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191AKc2qW6tmXDFSx1xn53q
2026-08-09 14:37:48 +02:00
ef80288c57 feat: initialize llm-security-commons (charter, license, extraction plan)
Data+contract+fixtures repository for runtime-neutral LLM/agent security
detection. Charter is hard: no engine code, no dependencies, no runtime —
JSON data, normative specs and conformance fixtures only.

Scaffolding only; the seed data files land in follow-up commits from
verified source dumps (never from recollection).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0191AKc2qW6tmXDFSx1xn53q
2026-08-09 14:29:16 +02:00