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:
Kjell Tore Guttormsen 2026-09-19 19:12:20 +02:00
commit 1c958ab8d6
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 226 additions and 59 deletions

View file

@ -501,6 +501,29 @@ def _build(
)
def _ledger(*pairs: tuple[str, str]) -> dict[str, Any]:
"""The accounting a run writes when it REWROTE these pictures.
The gate's conversion route reads its pairs from here and confirms them
against the bundle text, so a test constructing a `Build` by hand has to
say what the run booked. Passing none of them is the case where the run
converted nothing -- which is every bundle before the viewable-asset
round, and the state a forged pointer block leaves the ledger in.
"""
return {
"accounting_version": 1,
"documents": [
{
"source_file": "a.md",
"status": "persisted",
"code": None,
"conversions": [{"from": before, "to": after} for before, after in pairs],
}
],
"files": [],
}
def test_the_judge_proves_a_CONVERTED_image_was_carried(tmp_path: Path) -> None:
"""A BMP reaches the bundle as a PNG, so the SOURCE's bytes are not in
`assets/` and never will be.
@ -528,20 +551,28 @@ def test_the_judge_proves_a_CONVERTED_image_was_carried(tmp_path: Path) -> None:
f"Image: graphics/figur.bmp (8x4 px) -- converted from image/bmp "
f"sha256:{before} to image/png sha256:{after}\n"
)
build = _build(assets={f"{after[:12]}-figur.png": after}, bundle_text=text)
ledger = _ledger((before, after))
build = _build(assets={f"{after[:12]}-figur.png": after}, bundle_text=text, accounting=ledger)
assert gate.asset_holds(build, source) is True
# Known-negative on the same shape: the bundle says it converted, and the
# Known-negative on the same shape: the run booked the conversion, and the
# file it names is not there. A route that read the claim alone would pass.
empty = _build(assets={}, bundle_text=text)
empty = _build(assets={}, bundle_text=text, accounting=ledger)
assert gate.asset_holds(empty, source) is False
# Known-negative two: an asset IS there, under a digest the bundle never
# tied to this source.
# Known-negative two: an asset IS there, under a digest nothing tied to
# this source.
other = "0" * 64
stranger = _build(assets={f"{other[:12]}-x.png": other}, bundle_text=text)
stranger = _build(assets={f"{other[:12]}-x.png": other}, bundle_text=text, accounting=ledger)
assert gate.asset_holds(stranger, source) is False
# Known-negative three: the same bundle text, and a run that booked NO
# conversion. This is the pointer block a document can write for itself.
unbooked = _build(
assets={f"{after[:12]}-figur.png": after}, bundle_text=text, accounting=_ledger()
)
assert gate.asset_holds(unbooked, source) is False
# --- the claim the judge reads must be one the CODE wrote --------------------
@ -579,21 +610,26 @@ def _huge_bmp() -> bytes:
def test_a_document_cannot_forge_a_conversion_claim(tmp_path: Path) -> None:
"""THE FASIT NEVER COMES FROM THE READER IT JUDGES -- including this route.
The conversion route reads two digests out of the bundle. Before this
guard it read them out of ANY text in it, so a document could write the
sentence itself and the judge would believe it: measured by PM 2026-09-19,
a BMP declaring 50 000 x 50 000 that was refused `asset_too_large` and
never carried gave `asset_holds = True`, both through an image's alt text
and through ordinary body text. The route the judge had before the
conversion landed hashed the source file and nothing else, so no document
could reach it; this round opened a way IN for content this repository
does not trust.
The conversion route used to read its two digests out of the bundle text.
Measured by PM 2026-09-19 that was a way IN for content this repository
does not trust: a BMP declaring 50 000 x 50 000, refused
`asset_too_large` and never carried, gave `asset_holds = True` from an
image's alt text and from ordinary body text. Anchoring the claim to a
pointer block closed those two and not the class -- a pointer block is two
lines of markdown, and one HTML file with two `<p>` elements writes them.
A claim counts only where THIS CODE put it: inside a pointer block, tied
to the asset that block names. Every arm below carries a source that was
never carried, and a bundle holding one unrelated REAL asset -- so the
digest the forgery names really is in `assets/`, which is what made the
measured forgeries work.
So the pair the judge believes comes from the RUN's accounting, and the
bundle text only confirms it. Every arm below is a way a document can put
the sentence, or the whole block, into a bundle; the run's ledger holds
ONE conversion and it is about other pictures, so the route is live and no
arm may reach it. The known-positive at the end books the pair and shows
the route still works.
Thirteen arms. Eleven are PM's own list of ways the shape anchoring
already refused, kept as a regression guard -- narrowing this rule later
must not quietly widen one of them -- and two are the form that defeated
it: a perfectly written pointer block the run never booked, once in the
bundle's ordinary text and once naming the asset it points at exactly.
"""
never_carried = tmp_path / "figur.bmp"
never_carried.write_bytes(_huge_bmp())
@ -604,26 +640,63 @@ def test_a_document_cannot_forge_a_conversion_claim(tmp_path: Path) -> None:
assets = {f"{after[:12]}-ekte.png": after}
clause = f"converted from image/bmp sha256:{before} to image/png sha256:{after}"
pointer = f"![Ekte](/assets/{after[:12]}-ekte.png)\nImage: ekte.png (1x1 px)"
# The same clause with U+00A0 where the spaces are: PM's N6.
nbsp_clause = clause.replace(" ", "\u00a0")
other = "0" * 64
arms = {
"plain body text": f"{pointer}\n\nProsess 84. {clause}. Se figuren over.\n",
"a table cell": f"{pointer}\n\n| Krav | Kilde |\n| --- | --- |\n| 84-1 | {clause} |\n",
"a figure caption": f"{pointer}\n\nFigur 84-1 -- {clause}\n",
"a fenced code block": f"{pointer}\n\n```\n{clause}\n```\n",
"link text with a URL": f"{pointer}\n\n[{clause}](https://example.invalid/x)\n",
"the clause with non-breaking spaces": f"{pointer}\n\n{nbsp_clause}\n",
"a clause naming another asset's digest": (
f"![Ekte](/assets/{after[:12]}-ekte.png)\n"
f"Image: ekte.png (1x1 px) -- converted from image/bmp sha256:{before} "
f"to image/png sha256:{other}\n"
),
"a pointer block naming another asset": (
f"![Figur](/assets/000000000000-annen.png)\nImage: annen.png (1x1 px) -- {clause}\n"
f"![Figur](/assets/{other[:12]}-annen.png)\nImage: annen.png (1x1 px) -- {clause}\n"
f"{pointer}\n"
),
"an Image: line of its own": f"{pointer}\n\nImage: figur.bmp (8x4 px) -- {clause}\n",
"the whole block inside a code fence": (
f"{pointer}\n\n```\n![Figur](/assets/{after[:12]}-ekte.png)\n"
f"Image: figur.bmp (8x4 px) -- {clause}\n```\n"
),
"the whole block on one line": (
f"{pointer}\n\n![Figur](/assets/{after[:12]}-ekte.png) "
f"Image: figur.bmp (8x4 px) -- {clause}\n"
),
# The two that defeated the shape anchoring. Written exactly as the
# code writes them, because that is the point: the form is not a
# signature, and only the ledger can tell these from the real thing.
"a whole pointer block the run never booked": (
f"{pointer}\n\n![Figur](/assets/{after[:12]}-ekte.png)\n"
f"Image: figur.bmp (8x4 px) -- {clause}\n"
),
"that block alone in the bundle": (
f"![Figur](/assets/{after[:12]}-ekte.png)\nImage: figur.bmp (8x4 px) -- {clause}\n"
),
}
# The run booked ONE conversion and it is about neither of these files, so
# the route is live in every arm and nothing it could believe is true.
live = _ledger(("f" * 64, "e" * 64))
for label, text in arms.items():
build = _build(assets=assets, bundle_text=text)
build = _build(assets=assets, bundle_text=text, accounting=live)
assert gate.asset_holds(build, never_carried) is False, (
f"{label}: a document talked the judge into a carry that never happened"
)
# KNOWN-POSITIVE on the same bytes: the clause where the code writes it,
# in the pointer block for the asset it names. Without this the arms above
# would pass on a route that had simply stopped working.
honest_text = f"![Ekte](/assets/{after[:12]}-ekte.png)\nImage: figur.bmp (1x1 px) -- {clause}\n"
honest = _build(assets=assets, bundle_text=honest_text)
# KNOWN-POSITIVE on the same bytes: the last arm's text, believed once the
# RUN books the pair. Without it every arm above would pass on a route
# that had simply stopped working.
honest = _build(
assets=assets,
bundle_text=arms["that block alone in the bundle"],
accounting=_ledger((before, after)),
)
assert gate.asset_holds(honest, never_carried) is True
@ -810,7 +883,11 @@ def test_the_judge_proves_carriage_and_says_it_does_not_prove_fidelity(tmp_path:
f"Image: figur.bmp (8x4 px) -- converted from image/bmp sha256:{before} "
f"to image/png sha256:{after}\n"
)
build = _build(assets={f"{after[:12]}-figur.png": after}, bundle_text=text)
build = _build(
assets={f"{after[:12]}-figur.png": after},
bundle_text=text,
accounting=_ledger((before, after)),
)
assert gate.asset_holds(build, source) is True
doc = gate.asset_holds.__doc__ or ""