Commit graph

4 commits

Author SHA1 Message Date
cb784fea6f fix(spec): section 6 forbade a case the corpus ships, and the predicate under it was wrong too
The normative spec read "Such a payload MUST NOT be given a discriminated case
id" while manifest.json defined case_id_derivation.variant_suffix and
conformance/hybrid-xss__script-tag--src-no-close/ sat on disk under it. The
manifest was the correct party: the derivation was extended in corpus 0.3.0 and
the spec was never updated. No data moves here, only the text describing it.

The derivation block now carries the optional '--' suffix and the truncating
reverse, with the '--'-absence measurement stated as the reason the reverse
stays LEXICAL - re-measured at this commit rather than copied from 0.3.0's
numbers, and scoped to id spaces because '--' does occur inside pattern values.

The part that would have passed review while still being wrong: fixing only the
permission. Section 6 also reasoned that equal in-scope finding sets mean the
second case "cannot fail in any way the first does not" - and the shipped
variant falsifies exactly that. It expects the same single finding, same scope,
same match, and still gates what the base cannot, because the base input matches
the pattern under both its published and its superseded stricter form. The
discriminating signal is INSIDE the scope, in the form of the scoped rule. So
the MUST NOT is replaced by a predicate about failure surface rather than
finding sets, checked in both directions: it admits the shipped variant and
still excludes the omitted markdown-image payload, whose only distinguisher
lives in a table this repository does not publish.

manifest.json is untouched and stays at 0.5.0; no case directory moved. The
spec has no version of its own - "Through version 0.1.1" in section 4 is the
CORPUS version, verified against CHANGELOG [0.2.0] before acting, because the
session brief said otherwise. Section 6's stable-id and BREAKING sentences were
read, not edited.

Verified in scratchpad, never in the repo: the amended derivation transcribed
into a checker that reads all 94 cases back from disk, derives each pattern id,
round-trips it forward, and asserts the case expects it. All 94 reproduce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M4xrxV3EXbSALqvB23kpeY
2026-08-13 22:00:24 +02:00
e6ca5ae5ee feat(active-content): the seventh case, and the classifier it needed came with it
Adopting `active:raw-html-link` was one id. Publishing it honestly was the whole
of `active_tag_class` — one function, three branches, no way to state the split
without the no-URL narrowing and the 0.6.0 external-target rule. On the old
predicate a bare `</a>` is active by name, so a consumer implementing from the
hybrid would emit the new label where the seed runtime emits nothing.

The file is now two pins, stated as two: v0.3.4/0bf0729 everywhere except the
raw-HTML classifier, v0.7.0/be9759b there. The drift between them was measured
field by field against the imported module rather than assumed, after stripping
inline-flag rendering and applying the file's own declared quote normalisation
so a spelling difference could not masquerade as drift. Exactly one published
field had moved, and not the one this release was about: `html.active_tags`
carried the MUTATOR's 23-name set where the gate means the SCANNER's 22. Correct
at the 0.3.4 pin, wrong from 0.6.0 on. Kept as `html.mutator_tags`.

The sweep covered 93 cases, not the 6 obvious ones. The narrowing can silence an
`active:` finding inside the `observed_out_of_scope` evidence of a LEXICON case,
and that field is guarded by no test anywhere — stale entries there survive
forever. One case moved: html-obfuscation__aria-label, whose `<a aria-label=…>`
carries no URL attribute. Its fixture is deliberately not rewritten; the residue
is true at the commit `measurement` pins, and rewriting one of 83 would leave two
commits under a header naming one. Recorded, dated and pinned in the manifest.

The strongest check is not the digest: the checker rebuilds the published
classifier from the JSON alone, importing nothing from the runtime, and
differential-tests it against `active_tag_class` over 42 probe tags. 0
disagreements. That is what licenses shipping a classifier as data.

No `aliases.llm_security` published, on this file or on carriers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTTaT4quwNPwBYVqmgAt8t
2026-08-13 21:43:27 +02:00
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
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