fix(accounting,gate): the conversion claim comes from the run's ledger
Chose the side channel over neutralising pointer-shaped document text, because the second fix changes what every document SAYS in order to defend a tool outside the build: a source quoting a bundle listing would come out altered and existing bundles would move bytes. This reads a file the run already writes. `assets.conversion` names the pair, `DocumentAssets.conversions` carries it out of the run, `DocumentAccount.conversions` books it, and the accounting JSON states it per document. `_declared_conversions` reads it; `_conversions` now believes a pair only when the RUN booked it AND a pointer block confirms it for the asset it names. The confirmation can be forged and the ledger cannot, which is why the ledger decides. Measured through the real `okf build`: the three arms PM reproduced (two `<p>`, one `<p>` with `<br>`, a markdown note beside the carrier) go forged -> refused, 3 of 3, with the known-positive True in all three. The text-level regression guard goes 3 arms to 13, the two new ones being a perfectly written pointer block the run never booked. R761, rebuilt: 25 BMP sources, 19 held, 19 of 19 conversions confirmed against 19 declared, 50 assets (29 JPEG + 21 PNG, 0 BMP), SHY 71, u = 0, d = 0, exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
aa2abe8293
commit
1c958ab8d6
5 changed files with 226 additions and 59 deletions
|
|
@ -383,35 +383,78 @@ _CONVERSION = re.compile(
|
|||
)
|
||||
|
||||
|
||||
def _conversions(bundle_text: str) -> dict[str, str]:
|
||||
"""source digest -> the digest the bundle says it carried instead.
|
||||
def _declared_conversions(build: Build) -> set[tuple[str, str]]:
|
||||
"""Every `(source digest, asset digest)` pair THE RUN booked.
|
||||
|
||||
READ ONLY FROM A POINTER BLOCK THIS BUILD WROTE, and only where the clause
|
||||
names the asset that block points at. The expression used to run over the
|
||||
whole bundle text, which put an untrusted document inside the judge's own
|
||||
input: measured by PM 2026-09-19, a BMP refused `asset_too_large` and never
|
||||
carried was reported as held, both through an image's alt text and through
|
||||
ordinary body text, because the document simply wrote the sentence. The
|
||||
gate's first sentence is that the fasit never comes from the reader it
|
||||
judges, and this route had quietly stopped obeying it.
|
||||
Read from the accounting the build wrote, which is a side channel no
|
||||
document can reach -- and that is the whole of the change. The pair used
|
||||
to be read out of the bundle text, so a claim was accepted because it had
|
||||
the SHAPE of a pointer block. Measured by PM 2026-09-19 through the real
|
||||
`okf build`: one HTML file with two `<p>` elements writes those two lines
|
||||
into a concept body, and a BMP refused `asset_too_large` and absent from
|
||||
`assets/` read as carried. Narrowing the shape cannot close that -- a
|
||||
document that can write the form can write any form -- so the claim is
|
||||
bound to the run instead.
|
||||
|
||||
The other half of the boundary is in the build, not here: a label is
|
||||
document text written INSIDE a pointer block, so `assets._inline` is what
|
||||
keeps it from emitting this grammar. Neither half is sufficient alone --
|
||||
this one cannot tell a label from a field, and that one does not reach
|
||||
body text or a table cell at all.
|
||||
CHOSEN over neutralising pointer-shaped text at extraction, because that
|
||||
fix would change what every document says to defend a judge: a source
|
||||
quoting a bundle listing would come out altered, the bytes of existing
|
||||
bundles would move, and the production path would carry a rule that
|
||||
exists for a tool outside it. This reads a file the run already writes.
|
||||
|
||||
A build with no accounting door has no ledger, so no conversion is
|
||||
provable and `asset_holds` falls back to its first route alone. That is
|
||||
the honest reading and it is VISIBLE: the images concerned are reported
|
||||
claimed-and-not-found, the same as before the conversion route existed.
|
||||
"""
|
||||
accounting = build.accounting
|
||||
if not isinstance(accounting, dict):
|
||||
return set()
|
||||
documents = accounting.get("documents")
|
||||
if not isinstance(documents, list):
|
||||
return set()
|
||||
pairs: set[tuple[str, str]] = set()
|
||||
for document in documents:
|
||||
if not isinstance(document, dict):
|
||||
continue
|
||||
for entry in document.get("conversions") or ():
|
||||
if isinstance(entry, dict):
|
||||
before, after = entry.get("from"), entry.get("to")
|
||||
if isinstance(before, str) and isinstance(after, str):
|
||||
pairs.add((before, after))
|
||||
return pairs
|
||||
|
||||
|
||||
def _conversions(build: Build) -> dict[str, str]:
|
||||
"""source digest -> the digest the run says it carried instead.
|
||||
|
||||
THE RUN'S LEDGER DECIDES AND THE BUNDLE ONLY CONFIRMS. A pair counts here
|
||||
when `_declared_conversions` holds it AND a pointer block in the bundle
|
||||
states the same pair for the asset it points at, so the two halves of the
|
||||
same run have to agree before the judge believes either. The confirmation
|
||||
can be forged and the ledger cannot, which is why the ledger is the one
|
||||
that decides: a document adding a pointer block adds nothing, and a
|
||||
document REMOVING the run's own is not a thing a document can do.
|
||||
|
||||
The gate's first sentence is that the fasit never comes from the reader it
|
||||
judges. Reading the claim itself out of the bundle had quietly stopped
|
||||
obeying it -- twice, and the second time the text had the exact shape this
|
||||
code writes.
|
||||
"""
|
||||
declared = _declared_conversions(build)
|
||||
if not declared:
|
||||
return {}
|
||||
found: dict[str, str] = {}
|
||||
for pointer in _POINTER.finditer(bundle_text):
|
||||
for pointer in _POINTER.finditer(build.bundle_text):
|
||||
clause = _CONVERSION.search(pointer.group("detail"))
|
||||
if clause is None:
|
||||
continue
|
||||
after = clause.group("after")
|
||||
before, after = clause.group("before"), clause.group("after")
|
||||
# The claim has to be about the picture the block points at. A clause
|
||||
# standing in one asset's block while naming another's digest is a
|
||||
# sentence nothing in this build writes.
|
||||
if pointer.group("asset").startswith(after[:12]):
|
||||
found[clause.group("before")] = after
|
||||
if (before, after) in declared and pointer.group("asset").startswith(after[:12]):
|
||||
found[before] = after
|
||||
return found
|
||||
|
||||
|
||||
|
|
@ -436,12 +479,12 @@ def asset_holds(build: Build, source: Path) -> bool:
|
|||
round a source in a format no model can be shown reaches the bundle as a
|
||||
PNG, so its own bytes are not in `assets/` and never will be -- measured,
|
||||
the day that landed R761 went from 0 to 19 claimed-and-not-found, which is
|
||||
exactly its RLE8 BMP count. The bundle states both digests on the pointer
|
||||
line, and this reads them and then HASHES THE ASSET ITSELF: the claim is
|
||||
accepted only when a file in `assets/` really holds the bytes the bundle
|
||||
says were written. A bundle claiming a conversion it did not perform still
|
||||
fails, which is the difference between reading the bundle and believing
|
||||
the report.
|
||||
exactly its RLE8 BMP count. The pair of digests comes from the RUN's own
|
||||
accounting (`_conversions`), never from the bundle's prose, and this then
|
||||
HASHES THE ASSET ITSELF: the claim is accepted only when a file in
|
||||
`assets/` really holds the bytes the run says it wrote. A bundle claiming
|
||||
a conversion the run did not book still fails, which is the difference
|
||||
between reading the artifacts and believing the report.
|
||||
|
||||
WHAT NEITHER ROUTE PROVES IS FIDELITY. Both ask whether a file in
|
||||
`assets/` holds the bytes the bundle names, and neither decodes a PIXEL:
|
||||
|
|
@ -458,7 +501,7 @@ def asset_holds(build: Build, source: Path) -> bool:
|
|||
found == digest and name.startswith(digest[:12]) for name, found in build.assets.items()
|
||||
):
|
||||
return True
|
||||
written = _conversions(build.bundle_text).get(digest)
|
||||
written = _conversions(build).get(digest)
|
||||
return written is not None and any(
|
||||
found == written and name.startswith(written[:12]) for name, found in build.assets.items()
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue