fix(assets): bound what the run pays, not what the document claims (0.10.1)
A second independent review read `230d1cb` -- the commit that closed the `v0.10.0` review's two MAJOR findings -- and found one of them open. The bound read `/Width` and `/Height`, which an untrusted document writes, while `get_data()` pays for the stream beside them; `/Length` is the COMPRESSED length and the two numbers are independent. Re-measured here on `ed8d9d7` before anything changed, in its own interpreter: a 408 516-byte PDF declaring 1x1 and carrying 400 MB of deflated zeros was CARRIED, no rejection, 891 904 000 B peak RSS. After: 0 carried, `asset_too_large`, 57 065 472 B. At 1,2 GB of zeros, 2 436 MB -> 64 569 344 B -- the cost no longer scales with the bomb. End to end through the CLI with the shipped defaults: 838 000 640 B and an asset written -> exit 0, 79 650 816 B, `0 carried of 1 found`, no `assets/`. Three numbers are bounded now, not one: what a container DECLARES, what a carried FILE measures (`read_image`, so a 49 MP PNG of 47 705 bytes is not passed on to a consumer), and what a PDF stream DECOMPRESSES to (`assets.inflated_size`, a chunk at a time, output discarded, before `get_data()`). The limit is stated rather than implied: the stream measurement runs where `FlateDecode` is the first filter and the document is not encrypted; every other chain is a check on the decoded length AFTER the decode, a counted refusal and not a bounded one. A non-positive declared dimension is `asset_size_invalid`, its own code, raised before the stream is read. `-1 x 40000000000` is a NEGATIVE pixel count, under which every `>` bound read as satisfied, so the check returned silently and the refusal arrived from `encode_png` as `asset_samples_invalid`. Its own code because a publisher shipping a picture bigger than this package carries and a dictionary written to be read wrong are different facts about a document. Two smaller findings in the line that says what is missing, both introduced by the first fix: the address was written twice, once bare, and a linkifying renderer autolinks a bare URL -- written once now, in one code span; and `label` became a dead parameter, so the figure's caption was dropped, a regression against 0.10.0. It is written again in the `-- <label>` form a carried pointer uses. Version bumped to 0.10.1 across all ten places. Nine were unbound and stale: four README install lines naming the previous release, two prose lines, the "current tag" entry, `uv.lock`, and a CHANGELOG whose 0.10.1 content sat under `[Unreleased]`. Two new packaging tests bind them to `__version__`, and the README's guard tag to `[tool.uv.sources]`. Every test was red first. The fate of every image is identical with and without the new bound on three K2 PDFs carrying 800 images (464/464, 311/311 with the same 12 rejections, 25/25), and the second inflate is below the noise floor there. 0 shipped artifacts move: no bundle under `examples/`, `skills/` or `tests/fixtures/` carries an image pointer at all, measured against a known-positive control. `asset_too_large` was undocumented in the error registry; both codes are there now. `tools/okf_accounting_gate.py` gains the new code in its closed list -- one string, no behaviour change, stated because that file belongs to another order. Suite 2141 passed / 1 skipped, ruff + format + mypy --strict clean. Report: docs/2026-09-18-bildestien-holder-0-10-1.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ed8d9d709f
commit
0f308c1f56
15 changed files with 842 additions and 81 deletions
77
README.md
77
README.md
|
|
@ -10,7 +10,7 @@ types](#supported-file-types) lists each one with the evidence behind it.
|
|||
Python 3.10+ and [uv](https://docs.astral.sh/uv/). One line:
|
||||
|
||||
```sh
|
||||
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.0"
|
||||
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.1"
|
||||
```
|
||||
|
||||
## Use it
|
||||
|
|
@ -120,11 +120,11 @@ Neither this package nor the guard it depends on is on a package index yet, so
|
|||
both install by direct reference. With uv, one command resolves both:
|
||||
|
||||
```sh
|
||||
uv pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.0"
|
||||
uv pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.1"
|
||||
```
|
||||
|
||||
uv resolves the guard on its own, because it reads the `[tool.uv.sources]`
|
||||
entry in the `pyproject.toml` **of the tag it is installing**, and `v0.10.0`
|
||||
entry in the `pyproject.toml` **of the tag it is installing**, and `v0.10.1`
|
||||
points that entry at `llm-ingestion-guard` `v1.4.0`. Use `uv tool install`
|
||||
instead of `uv pip install` when you want the `okf` command on `PATH` without an
|
||||
active virtualenv — that is the form the first screen shows.
|
||||
|
|
@ -135,10 +135,10 @@ With plain pip, the transitive git dependency does not resolve on its own —
|
|||
|
||||
```sh
|
||||
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v1.4.0"
|
||||
pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.0"
|
||||
pip install "llm-ingestion-okf @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.1"
|
||||
```
|
||||
|
||||
The guard tag is paired to the okf tag, not to this branch. `v0.10.0` declares
|
||||
The guard tag is paired to the okf tag, not to this branch. `v0.10.1` declares
|
||||
`llm-ingestion-guard>=1.2,<2.0`, which `v1.4.0` satisfies; the pairing above is
|
||||
read off that tag's own `[tool.uv.sources]`, not off this branch. Reading a pin
|
||||
off `main` and installing it against an older okf tag is the one combination
|
||||
|
|
@ -149,7 +149,16 @@ that fails.
|
|||
These are not install lines. They record what each earlier tag was, so a reader
|
||||
who meets one in an older document knows what they are looking at.
|
||||
|
||||
- `v0.10.0` — the current tag: a bundle carries the IMAGES its sources
|
||||
- `v0.10.1` — the current tag: the image path of `v0.10.0`, with the two
|
||||
defects an independent review found in it closed. A remote `<img src>` or
|
||||
`xlink:href` is inert text with the address in one code span, never a live
|
||||
markdown image link, and no longer loses the figure's caption. An image is
|
||||
bounded in three places rather than one: the size a container DECLARES, the
|
||||
size a carried file has, and — new in this tag — what the stream behind a
|
||||
PDF image actually DECOMPRESSES to, which is an independent number. A
|
||||
declared size that is not positive is refused with its own code,
|
||||
`asset_size_invalid`, before the stream is read.
|
||||
- `v0.10.0` — a bundle carries the IMAGES its sources
|
||||
declare. Five readers place them (`pdf`, the converter's office rows,
|
||||
`html`, `xml`), `assets/` at the bundle root holds the bytes under a
|
||||
content-addressed name, and the concept carries a two-line pointer where the
|
||||
|
|
@ -299,32 +308,54 @@ in must not read like a bundle of documents that had none.
|
|||
text. The pointer block passes the gate like any other body line, and the file
|
||||
beside it passes nothing. `log.md` says so on every run that carries one.
|
||||
|
||||
**A size CEILING, read off the same corpora (0.10.1).** An image whose
|
||||
DECLARED size is over `MAX_IMAGE_PIXELS` (40 000 000 pixels) or
|
||||
`MAX_IMAGE_BYTES` (256 MiB of samples) is refused with `asset_too_large`,
|
||||
counted like every other refusal, and never decoded. The largest image in the
|
||||
43-document reference corpus is 4 515 x 4 128 (18.6 MP) and the largest of
|
||||
R761's 109 pictures is 2 072 x 656 (1.4 MP), so the bound is an order of
|
||||
magnitude above anything measured.
|
||||
**A size CEILING, read off the same corpora (0.10.1).** An image over
|
||||
`MAX_IMAGE_PIXELS` (40 000 000 pixels) or `MAX_IMAGE_BYTES` (256 MiB) is
|
||||
refused with `asset_too_large`, counted like every other refusal. The largest
|
||||
image in the 43-document reference corpus is 4 515 x 4 128 (18.6 MP) and the
|
||||
largest of R761's 109 pictures is 2 072 x 656 (1.4 MP), so the bound is an
|
||||
order of magnitude above anything measured.
|
||||
|
||||
It exists because a few kilobytes can declare an enormous picture: a 9.6 KB
|
||||
PDF declaring 3 000 x 3 000 grayscale zeros took 83 MB of peak RSS, a 63 KB
|
||||
one declaring 8 000 x 8 000 took 276 MB, and the cost is linear in the pixel
|
||||
count, so one document could take a whole batch build with it — before any
|
||||
gate, because the guard never sees image bytes. The bound is checked on what
|
||||
the container declares, before anything is decompressed, on the two paths that
|
||||
decode: a PDF image's raw samples and an inline `data:` URI. An image file
|
||||
carried verbatim is not decoded at all, so its cost is its own file size.
|
||||
gate, because the guard never sees image bytes.
|
||||
|
||||
**Three numbers are bounded, not one, because a claim is not a cost.** What a
|
||||
container DECLARES (`/Width` x `/Height`, a PNG header, a `data:` payload's
|
||||
encoded length) is read before anything is decoded. What a carried FILE
|
||||
measures is read the same way — this package never decodes such a file, so it
|
||||
pays nothing for it, but writing a 7 000 x 7 000 PNG of 47 705 bytes into a
|
||||
bundle would hand the consumer the same bomb with `7000x7000 px` printed
|
||||
beside it. And what a PDF image's STREAM decompresses to is measured before it
|
||||
is held, a chunk at a time and discarded, because `/Length` is the COMPRESSED
|
||||
length and a dictionary declaring 1x1 may hang 400 MB of deflated zeros off
|
||||
it. Measured: that document is 408 516 bytes and cost 892 MB of peak RSS with
|
||||
only the declared size bounded; with the stream bounded it is refused at
|
||||
54 MB, and a three-times-larger bomb costs 62 MB rather than 2 436 MB.
|
||||
|
||||
**What the stream bound does NOT reach**, stated because the difference
|
||||
matters: it runs where `FlateDecode` is the first filter and the document is
|
||||
not encrypted. Any other filter chain is caught by a check on the decoded
|
||||
length AFTER the decode, which makes it a counted refusal rather than a
|
||||
bounded one — the memory is spent and then the picture is dropped.
|
||||
|
||||
**A declared size that is not a size** — a zero or negative `/Width` or
|
||||
`/Height` — is refused with its own code, `asset_size_invalid`, before the
|
||||
stream is read. Distinct from `asset_too_large` on purpose: one is a publisher
|
||||
shipping a picture bigger than this package carries, the other is a dictionary
|
||||
written wrong or written to be read wrong.
|
||||
|
||||
<!-- asset-max-pixels: 40000000 -->
|
||||
|
||||
**A remote reference is INERT (0.10.1).** `<img src="https://...">` and an STS
|
||||
`xlink:href` to an address off this machine are written as text with the
|
||||
address in a code span, never as ``. Extraction opens no
|
||||
socket, but a markdown renderer or an agent that fetches what it renders does,
|
||||
which would turn "this bundle was opened" into a beacon to whoever wrote the
|
||||
document. The address is still stated, because a reader has to know what stood
|
||||
there.
|
||||
address in one code span, never as `` and never as a bare
|
||||
URL a linkifying renderer would autolink. Extraction opens no socket, but a
|
||||
markdown renderer or an agent that fetches what it renders does, which would
|
||||
turn "this bundle was opened" into a beacon to whoever wrote the document. The
|
||||
address is still stated, and so is the figure's caption, because a reader has
|
||||
to know what stood there.
|
||||
|
||||
**`images: N` in a concept counts POINTER BLOCKS, not unique pictures.** One
|
||||
image referenced twelve times in one concept is `images: 12` and one file in
|
||||
|
|
@ -877,7 +908,7 @@ not need this repository — the first line installs the command, the second
|
|||
builds the bundle and writes a skill beside it, the third asks.
|
||||
|
||||
```sh
|
||||
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.0"
|
||||
uv tool install "llm-ingestion-okf[extract] @ git+https://git.fromaitochitta.com/open/llm-ingestion-okf.git@v0.10.1"
|
||||
okf project ~/my-documents
|
||||
claude
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue