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 c7d50b5250
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] ## [Unreleased]
> **Behaviour change, not a pure fix.** The three transform surfaces gain a Nothing yet.
> 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.
## [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.** > **The measured blast radius, for the record: zero.** `linkedin-studio` pins an
> `linkedin-studio` pins an exact tag, so nothing reaches it until it re-pins. > exact tag, so nothing reaches it until it re-pins. `llm-ingestion-okf` moved to
> `llm-ingestion-okf` moved to the range `>=0.3,<0.4` (their `f536e13`), so any > the range `>=0.3,<0.4` (their `f536e13`), so a 0.3.5 would have landed on them
> release on the 0.3 line lands on them at their next resolve, without an action > at their next resolve without an action on their part — but they answered our
> on their part. Whether that can affect them at all depends on which surfaces > query (`20260802T193351Z`) with a measured **no**: zero call sites for
> their pipeline calls — `okf.py` itself calls none of the three — and that is > `sanitize` / `fence` / `neutralize` / `prepare_input` anywhere in their `src/`.
> being measured with them before this ships. Which is also why the version this > Their `screen_output` path reaches only `scan_output`, which truncates and does
> lands under is not yet decided: on the 0.3 line it arrives unasked; as 0.4.0 it > not raise. Releasing as 0.4.0 puts this outside their ceiling regardless, so
> sits outside their ceiling and they opt in. > they cross it deliberately rather than by resolving.
### Added — input-size cap on the transform surfaces (OWASP LLM10) ### 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 `max_input_chars` is a per-call parameter, defaulting to the single calibrated
constant. 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_active_content` **called directly** and `okf.link_graph` were the two
`scan_output`, `scan_active_content` inherits that function's cap. Both are surfaces still reading attacker-supplied text with no cap. Both truncate and
detection-shaped, so the truncate-and-flag mechanism transfers to them unchanged record, the way the other scanners do:
— mechanical follow-up work, not a policy question. Recorded in
`docs/LIMITATIONS.md`. - `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 ## [0.3.4] — 2026-08-01

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "llm-ingestion-guard" 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." 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" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"

View file

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