feat(retrieval-gate): row 8 names the bundle it measured, and the three real sets are pinned here

REAL_SET_PINS states what each set IS -- questions, fasit entries, controls
and sha256 -- so a one-question file in the right shape is refused
(`check_real_pin`, exit 2) instead of reading `1 of 1 | 3 of 3 | GREEN`.
Three counts rather than one: the digest says the bytes are the pinned
bytes, the counts are what a reader can check against the source.

Every set's line now carries `measured against <key> = <path> | bundle_id
<id> | ref <sha256-tree:...>`, for the bundles the run actually used and
SS 3.3's two halves both printed -- three builds on this machine share one
bundle_id at three refs.

Stated limit: the table lives in the file a capability session edits, as
SYNTHETIC_SETS and SPECS_SHA256 do. It raises the cost of the attack and
does not remove it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-20 08:06:23 +02:00
commit 34fe70a401
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q

View file

@ -1819,6 +1819,111 @@ def read_real_set(name: str, path: Path, expected_sha256: str) -> QuestionSet:
REQUIRED_REAL_SETS: tuple[str, ...] = ("wiki-20", "r761-sk2", "vegnormal-32")
@dataclass(frozen=True)
class RealSetPin:
"""What a real set IS, stated here rather than taken from the command line.
Until 2026-09-20 BOTH the file and its expected sha256 came from the
caller, and `set_id` was decided by the adapter rather than by the file:
three one-question files written in the three shapes, against a
self-written bundle, read `wiki-20: 1 of 1 ... | 3 of 3 | GREEN`. Nothing
said how big `wiki-20` is. Row 9 has had the mechanism since 2026-09-19
(`K2_QUESTIONS` refuses a set of another size); this is that mechanism for
the three sets row 8 requires.
THREE COUNTS, NOT ONE. The sha256 is the strongest and the least
informative: it says the bytes are the pinned bytes and nothing about what
they contain. The two counts are what a reader can check against the
source, and they are what a re-freeze of a set would move. All three are
facts about files this repository never holds -- a digest and two integers
name no document.
THE LIMIT, STATED: this table is in the file a capability session edits,
exactly as `SYNTHETIC_SETS` and `SPECS_SHA256` are. It raises the cost of
the attack (the set, the bundle AND this table) and does not remove it;
the suite is the rest of the gate, and says so.
"""
questions: int
fasit_entries: int
controls: int
sha256: str
#: Measured 2026-09-20 against the three sources, each read through its own
#: adapter. `questions` is the number of `Question` objects the adapter
#: produces, which is why `vegnormal-32` is 37: five of its 32 questions cite
#: two standards, and a payload is built against one bundle.
REAL_SET_PINS: Mapping[str, RealSetPin] = {
"wiki-20": RealSetPin(
questions=20,
fasit_entries=29,
controls=0,
sha256="972d0f5715d1377b3d89b8ddf391612709b96cd0fe8b96dfe517fe1931a9e333",
),
"r761-sk2": RealSetPin(
questions=7,
fasit_entries=7,
controls=1,
sha256="c834a478e4888300845de9e166808a3942085cb73c6e9e5fd2a3e1a6e9c5e6fd",
),
"vegnormal-32": RealSetPin(
questions=37,
fasit_entries=43,
controls=0,
sha256="c3932fc9abd144989bdbc50c4e4627ac5cc59937c4204f92422b7fe10af87faa",
),
}
def check_real_pin(question_set: QuestionSet) -> None:
"""The set the adapter produced, against what this gate says that set is.
Raises rather than warning: a set that is not the pinned set measured
something else, and row 8's number is then attached to a name it has not
earned.
"""
pin = REAL_SET_PINS.get(question_set.set_id)
if pin is None:
raise GateUsage(
f"{question_set.set_id}: no pin for this set; row 8 measures the "
f"pinned sets {', '.join(REQUIRED_REAL_SETS)} and no others"
)
measured = (
len(question_set.questions),
question_set.units,
len(question_set.controls),
question_set.sha256,
)
expected = (pin.questions, pin.fasit_entries, pin.controls, pin.sha256)
if measured != expected:
raise GateUsage(
f"{question_set.set_id}: pinned as {pin.questions} question(s), "
f"{pin.fasit_entries} fasit entr(ies), {pin.controls} control(s), "
f"sha256 {pin.sha256[:12]}; measured {measured[0]}, {measured[1]}, "
f"{measured[2]}, sha256 {question_set.sha256[:12]} -- a set of another "
"size or another content is another set wearing this one's name"
)
def bundle_identity(bundle: Path) -> str:
"""What a reader needs to run the same measurement again: the path, the
`bundle_id` the root index declares and the content ref.
SS 3.3's own distinction, both halves printed: a `bundle_id` is the
producer's assertion and a ref is a fact about bytes. Three builds on this
machine carry one `bundle_id` at three refs, so the id alone names a
bundle no better than the set's sha256 names a bundle.
"""
try:
return (
f"{_display(bundle)} | bundle_id {consume.root_bundle_id_of(bundle)} "
f"| ref {consume.bundle_ref(bundle)}"
)
except Exception as error: # an unreadable bundle is a line, never a crash
return f"{_display(bundle)} | identity unreadable: {type(error).__name__}: {error}"
def row_eight(real: Sequence[tuple[QuestionSet, Mapping[str, Path]]]) -> Row:
"""The three real sets. RED when they have not run -- always, in this
order -- and never green by leaving a set out.
@ -1879,6 +1984,15 @@ def row_eight(real: Sequence[tuple[QuestionSet, Mapping[str, Path]]]) -> Row:
f"({'citation' if question_set.quoted else 'concept'} granularity) | "
f"{answered} of {asked} questions | sha256 {question_set.sha256[:12]}"
)
# THE BUNDLE IS NAMED, not only the set. Until 2026-09-20 the row
# printed the set's digest and nothing about what it was measured
# against, so `44 of 64` could neither be reproduced nor felled by
# anyone reading the output.
used = sorted({question.bundle or question_set.bundle for question, _ in cases})
details.extend(
f" measured against {key or '-'} = {bundle_identity(bundles[key])}"
for key in used
)
for unit in units:
if not unit.hit:
details.append(
@ -2089,6 +2203,8 @@ def _real_sets(
real: list[tuple[QuestionSet, Mapping[str, Path]]] = []
for name, path, sha, bundle in arguments:
question_set = read_real_set(name, Path(path).expanduser(), sha)
# The command line said what the file is; this says what the set is.
check_real_pin(question_set)
bundles = _bundle_map(bundle)
if list(bundles) == [""] and question_set.bundle:
bundles = {question_set.bundle: bundles[""]}