A public repository should not carry the layout of the machine it was written
on, and it should not carry the identifiers of a queue nobody outside can see.
Six places in material this span added or changed did.
Neutral text, same facts: a private queue path in the EOL report, a private
note path in two docstrings and one report header, and four order identifiers
opening four reports. Where a sentence lost its source it says `internal
measurement note` -- the measurement is still named, the path is not.
One of them was output, not prose: row 9 of the retrieval gate printed a
`~/corpora/...` path to stdout on every run. It says the bundles exist on the
machine the row was written against; the row already takes `--k2 SET SHA
BUNDLE` from anyone who holds them.
Measured with the patterns as given, and both validated against a
known-positive on `df5a118` first -- an unvalidated pattern reporting zero is
not a measurement:
* `~/\.claude|/Users/|coord/.*/orders` over the 74 files the span touches:
**9 -> 6**. The six left are deliberate: two README lines telling a reader to
install a skill into their OWN `~/.claude/skills`, one prose line arguing
that `/Users/...` is not more portable than a relative path, and three lines
of `tests/test_retrieval_gate.py`'s own known-positive for this exact check,
which removing would disarm it.
* `[0-9]{8}T[0-9]{6}Z-[0-9]+` over the 38 files the span ADDED: **4 -> 0**.
A note on the method, because the first run of the first grep reported 0 and
was wrong: zsh does not word-split an unquoted parameter, so
`git grep -- $FILES` passed 74 names as one argument and matched no file at
all. Both numbers above come from `$(git diff --name-only ...)`, which zsh does
split, and the known-positive is what made the false zero visible.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.9 KiB
Two findings in the image path, closed before push (0.10.1)
From an independent review of v0.10.0 (an internal measurement note, not
committed here). The review's verdict was "safe to push, with reservations": no blocker, and
byte-identity, determinism and --no-assets parity all hold. Both findings
land with the shipped defaults (--assets on, --gate guard-trusted-source),
and both are new in 0.10.0, because before it no reader read an <img>
attribute or opened an image stream.
Both repros were rebuilt here as tests before anything was fixed
(tests/test_asset_limits.py, 17 tests).
MAJOR-1: a remote reference was a live markdown image link
Reproduced:
render_missing('https://collect.example.net/p.gif?u=S', href=…)
-> '\nImage: … (not carried: …)'
The first line is a live image link to an address the document's author
controls, query string included. This package opens no socket. A consumer that
renders the bundle, or an agent that fetches what it renders, does — which
turns "this bundle was opened" into a beacon (and a server-side consumer into
an SSRF). Measured with guard 1.4.0: PRESET_USER_UPLOAD fails the document
secure, PRESET_TRUSTED_SOURCE — the build's default — persists it.
Fixed in render_missing: a reference with an href is written as
Image: <name> (not carried: <reason>) address:
Pinned as a property, not a string. FOREIGN_IMAGE_LINK matches any
markdown image whose target is not this bundle's own assets/. It is asserted
over the two readers that resolve references (HTML, STS), over three shapes of
remote address (https:, //host, upper-case scheme), and over a whole built
bundle of the shipped fixture inbox. The known-positive beside it: a local
image still produces a pointer block that IMAGE_POINTER matches, and a
data: URI image is still carried.
The tier asymmetry is the guard's own question and was sent to
llm-ingestion-pipeline-security with the repro
(20260917T221801Z-428505178). Nothing was built there.
MAJOR-2: nothing bounded a declared image size
Reproduced with the review's own generator, rebuilt in the test file: a PDF declaring one grayscale image of compressed zeros.
| declared | PDF file | peak RSS |
|---|---|---|
| 3 000 x 3 000 | 9.6 KB | 83 MB |
| 8 000 x 8 000 | 63 KB | 276 MB (review's measurement) |
The cost is linear in the pixel count, so 50 000 x 50 000 is several GB. One document — malicious, defective, or a legitimately enormous scan — could take a whole batch build with it, before any gate, because the guard never sees image bytes.
The bound is read off the corpora, not chosen. Over the 4 828 image objects of the 43-document reference corpus the largest is 4 515 x 4 128 (18.6 MP, a landscape drawing). Over R761's 109 delivered pictures the largest is 2 072 x 656 (1.4 MP).
MAX_IMAGE_PIXELS = 40 000 000(2.1x the largest measured).MAX_IMAGE_BYTES = 256 MiBof samples.- Over either:
asset_too_large, counted like every other refusal, with the declared size in the reason. Never a silent skip and never a killed build.
Checked on what the container DECLARES, before anything is decompressed.
stream.get_data() is what pays for the bomb, so the declared Width and
Height are read first. The order is observable, not asserted: the test feeds
a PDF whose image stream is corrupt AND whose declared size is over the bound.
Decoding first gives asset_pdf_unsupported; reading the size first gives
asset_too_large. encode_png refuses the same size on its own, so the
encoder does not trust its caller.
CORRECTION, 2026-09-18 — this paragraph claimed a defence this round did not build. A declared size and a decompressed stream size are two independent numbers:
/Lengthis the COMPRESSED length, and nothing in the dictionary states whatget_data()returns. A second independent review measured a 408 516-byte PDF declaring 1x1 and carrying 400 MB of deflated zeros being CARRIED, with no rejection, at 892 MB of peak RSS — the same failure mode this round set out to close, reached through the other number. The bound was real and it bound the wrong thing. What this round DID close is the declared size and thedata:URI; the stream itself is bounded fromdocs/2026-09-18-bildestien-holder-0-10-1.md, and the paragraph below about a verbatim file is corrected there too.
The data: URI, which the review flagged and did not measure, is closed by
the same bound, checked on the payload length before decoding (base64 expands
by 3/4). Measured: refused with asset_too_large as a row.
Not bounded, and stated rather than hidden: an image FILE carried verbatim is
never decoded, so its cost is its own file size. (Corrected 2026-09-18: it is
bounded now. This package pays nothing for such a file, but a 7 000 x 7 000
PNG of 47 705 bytes written into a bundle hands the consumer the same bomb
with 7000x7000 px printed beside it, and the README's own first sentence
about this bound said it was refused.)
The determinism defect PM added to this order
pdfminer names an inline image (BI … EI) from id() of a Python object.
Measured 2026-09-17: two concept files of the reference corpus differed
between two builds of one commit, in the pointer line
(Image: page-3-6344095824 against page-3-4555034320). That breaks the
bit-exact rebuild invariant.
Fixed: a name pdfminer derived from id() — all digits, no resource name —
is replaced by the image's position on the page (page-3-inline-2). The test
builds a PDF with an inline image and compares two extractions with the
_pdf_pages cache cleared between them.
The three MINOR findings
images: Ncounts pointer blocks, not unique pictures (12 pointers to 2 files isimages: 12). Documented in the README rather than changed: the number is the count of places a picture stands, dedup is on content, and changing it would move bundle bytes and lose the placement count.- A concept that is only a pointer block is persisted as substantive. Documented: "degenerate" means zero characters after stripping whitespace, and a pointer block is text. Changing the definition is a spec question.
data:URI: closed, see above.
Verification
tests/test_asset_limits.py: 17 tests, both repros red first.- Full suite 2 045 passed, 1 skipped;
ruff,ruff format --checkandmypy --strictclean. Re-run aftergit addagainst a clean tree. - R761 unchanged (PM's measurement, repeated):
--gate nonegives50 carried of 50 found, 50 files inassets/, anddiff -rqagainst the bundle built before this change reports no difference. tools/okf_accounting_gate.py: green on all six rows.
Proposal
Version 0.10.1. No tag, no release, no push — the content accounting sits in
the same tree and PM wants the order of the two settled first.