1
0
Fork 0

release(0.4.0): the raise is breaking, so the number says so

okf answered the exposure question with a measured no — zero call sites for the
three capped surfaces anywhere in their src/, and their screen_output path
reaches only scan_output, which truncates. So the blast radius is zero and 0.3.5
would have hurt nobody.

It is still 0.4.0. Adding a raise to a previously total public function is
breaking under SemVer, and this file declares SemVer; a survey that comes back
clean tells you the upgrade is easy, not that the contract held. The version
number describes the change, not the luck of who happens to call it.

Cost, stated: 0.4.0 was penciled in for the axis separation. That moves to 0.5.0.
Tags are never moved, so the choice is made once.

README still advertises v0.3.4 — it is updated after a clean-venv install proves
the tag resolves, so the install block never points at something unproven.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-10 14:49:47 +02:00
commit 73ad5e1ed8
3 changed files with 42 additions and 20 deletions

View file

@ -7,19 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
> **Behaviour change, not a pure fix.** The three transform surfaces gain a
> refusal path they did not have. A caller that today passes a document larger
> than 1 000 000 characters gets an exception where it previously got a result.
Nothing yet.
## [0.4.0] — 2026-08-10
> **Behaviour change, not a pure fix — and that is why this is 0.4.0 and not
> 0.3.5.** The three transform surfaces gain a refusal path they did not have. A
> caller that passes a document larger than 1 000 000 characters now gets an
> exception where it previously got a result. Adding a raise to a function that
> was previously total is breaking under SemVer whatever the measured blast
> radius turns out to be, so the number follows the change, not the survey.
>
> **The two consumers are not pinned the same way, and it matters here.**
> `linkedin-studio` pins an exact tag, so nothing reaches it until it re-pins.
> `llm-ingestion-okf` moved to the range `>=0.3,<0.4` (their `f536e13`), so any
> release on the 0.3 line lands on them at their next resolve, without an action
> on their part. Whether that can affect them at all depends on which surfaces
> their pipeline calls — `okf.py` itself calls none of the three — and that is
> being measured with them before this ships. Which is also why the version this
> lands under is not yet decided: on the 0.3 line it arrives unasked; as 0.4.0 it
> sits outside their ceiling and they opt in.
> **The measured blast radius, for the record: zero.** `linkedin-studio` pins an
> exact tag, so nothing reaches it until it re-pins. `llm-ingestion-okf` moved to
> the range `>=0.3,<0.4` (their `f536e13`), so a 0.3.5 would have landed on them
> at their next resolve without an action on their part — but they answered our
> query (`20260802T193351Z`) with a measured **no**: zero call sites for
> `sanitize` / `fence` / `neutralize` / `prepare_input` anywhere in their `src/`.
> Their `screen_output` path reaches only `scan_output`, which truncates and does
> not raise. Releasing as 0.4.0 puts this outside their ceiling regardless, so
> they cross it deliberately rather than by resolving.
### Added — input-size cap on the transform surfaces (OWASP LLM10)
@ -47,13 +55,27 @@ the cap, `details` names the refusing surface, and neither carries input.
`max_input_chars` is a per-call parameter, defaulting to the single calibrated
constant.
### Still uncapped, and deliberately
### Added — the last two detection surfaces bound their input too
`scan_active_content` **called directly** and the okf link graph. Reached through
`scan_output`, `scan_active_content` inherits that function's cap. Both are
detection-shaped, so the truncate-and-flag mechanism transfers to them unchanged
— mechanical follow-up work, not a policy question. Recorded in
`docs/LIMITATIONS.md`.
`scan_active_content` **called directly** and `okf.link_graph` were the two
surfaces still reading attacker-supplied text with no cap. Both truncate and
record, the way the other scanners do:
- `scan_active_content(text, source, max_scan_chars=MAX_SCAN_CHARS)` emits one
`active:oversize-input` finding (MEDIUM, LLM10) and scans the prefix. Reached
through `scan_output` the text is already under that surface's cap, so the flag
is raised once, there — `max_scan_chars` is now passed down.
- `link_graph(bundle, max_scan_chars=MAX_SCAN_CHARS)` caps each body and records
`(from_id, body_length)` in the new `LinkGraphResult.truncated` field. The
field is additive with a default, so existing positional construction and
attribute access are unaffected.
**What truncation costs is named rather than implied:** past the cap, "no
finding" means "not looked at". That is precisely what a silent truncation would
hide, and why `truncated` exists as a field instead of a log line — it is what
separates "no links past here" from "no links *read* past here".
Recorded in `docs/LIMITATIONS.md`.
## [0.3.4] — 2026-08-01

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "llm-ingestion-guard"
version = "0.3.4"
version = "0.4.0"
description = "Write-time defensive layer for Python pipelines that persist LLM output: sanitize, fence, tool-less quarantined transform, capability isolation, scan before persist, fail-secure."
readme = "README.md"
requires-python = ">=3.10"

View file

@ -61,7 +61,7 @@ from .grounding import (
)
from . import okf
__version__ = "0.3.4"
__version__ = "0.4.0"
# --- §6 bookends: the two library-side halves around the transform ---------