1
0
Fork 0

chore(release): 1.4.2

This commit is contained in:
Kjell Tore Guttormsen 2026-10-29 08:31:59 +01:00
commit 1241075ef8
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
7 changed files with 108 additions and 21 deletions

View file

@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.2] — 2026-10-29
### Fixed — invisible-character smuggling passed with 0 findings
Two published 2025 techniques reached the model through every gate untouched.
**Variation-selector smuggling** (Butler, 2025-02; Gao et al., 2025-10) hangs one
selector per hidden byte off a visible character; **Sneaky Bits** (Rehberger,
2025-03) encodes bits in U+2062 and U+2064. On `1.4.1` both gave **0 findings**
on the input path and on the output gate, and every hidden codepoint stayed in the
"sanitized" text. Measured more broadly in session 1: 17 of 19 invisible
character classes gave 0 findings.
The fix starts from a principled denominator instead of a hand-picked list:
Default_Ignorable_Code_Point | format characters (Cf) | private use (Co) in
Unicode 18.0, **141 674 codepoints**, frozen with the source files' sha256 in
`docs/threat-2026/codepoints.json`. The guard knew 142 of them. Every one is now
stripped by `sanitize` and flagged by the output gate unless a known legitimate
use exempts it **on its context**. One predicate serves both surfaces, and a test
checks all 141 674 codepoints, one by one, on both.
The exemptions are those the ZWJ fix established: judge by context, never by
identity alone. A *run* is a carrier whatever it stands after.
- one VS15/VS16 after an emoji base (371/371 of Unicode 18.0's emoji variation
sequences): ⚠️ ⏱️ ❤️ 1️⃣ survive byte-identical;
- one ideographic selector after a CJK ideograph (registered name variants, and
the 1 002 CJK compatibility sequences);
- ZWNJ/ZWJ next to an Arabic-script or Indic letter, with neither neighbour
invisible. Persian, Urdu, Hindi and Malayalam orthography stops being an
any-tier carrier. That closes the ZWNJ false-positive class 0.6.1 parked for
want of a corpus to verify a rule against. Measured on 9 Wikipedia articles
(fa ×3, hi ×2, ml, ur, he, ar), articles with a carrier finding went from
**6/9 to 2/9**, and the two left are real (a ZWSP where ZWNJ belongs; two ZWNJ
in a row);
- LRM/RLM/ALM in a document that carries right-to-left text;
- an Arabic number sign (U+0600–0605 and kin) directly before a digit.
Private use is caught in full: an icon-font glyph is blocked rather than leaving
a ~17-bit channel per visible gap. That decision, the number-sign rule and the
one-selector-per-ideograph rule are the operator's (2026-10-28). What each
exemption leaves open, and what is now blocked despite a legitimate use, is in
LIMITATIONS.
**The lexicon's word-split row** (`unicode:zero-width-in-word`, `\w[ZW]\w`)
matched every Persian word that carries its required ZWNJ. It now judges the
joiner it matched by the same predicate. The regex itself is unchanged and stays
byte-identical with the commons lexicon, and a Latin word split still fires.
### Added — labels
`sanitize:variation-selector`, `sanitize:private-use`, `sanitize:invisible-format`
and their output-gate counterparts `output:variation-selector-present`,
`output:private-use-present` and `output:invisible-format-present`, plus
`output:unicode-tag-present`. That last one is the complete tag check on the
output side, because the lexicon's narrower one misses U+E0000. All seven are
any-tier carriers (`FAIL_SECURE` in every tier), as the zero-width and BIDI labels
were. **A consumer filtering on the old labels sees new ones.** Labels are
detection behaviour, not the frozen surface.
### Measured — the guard rule, before and after
- **Attack cases:** A01 and A02 are green on both arrivals. Row 3 of the
threat-2026 plan stands at 10 caught, 1 documented gap and 8 red rows (8 of 31
arrivals, down from 12).
- **Test suite:** 1049 passed, 8 strict xfail (was 935 + 12).
- **Coverage matrix:** 136/136 caught classes (was 130/130; six carrier cases
added); 6/6 documented gaps hold.
- **Pinned OKF corpus** (`_okf-upstream` @ `3fcbb9f`): unmoved, 6/53 parse and
47 `fail_secure` / 31 `warn` on import, identical before and after.
- **False-positive populations** (`docs/fp-sweep.py`, upload preset, documents
disposed non-WARN): unmoved — vendor-harvest 106/198, generated-notes 196/2918, reference-corpus 108/389,
identical line for line before and after. Positive control: doubling the
VS16 that a reference-corpus document already carries moves it from
`quarantine_review` to `fail_secure`.
- **Runtime:** the classes are located with one regex pass and judged per hit.
A 1 000 000-character document made entirely of carriers costs 1.4 s in
`sanitize`, and a clean one 0.05 s. The RTL context is looked up once per
document, so a document of directional marks stays linear (pinned). The ReDoS
sweep collects 154 patterns (the two new character classes included) and
flags neither.
## [1.4.1] — 2026-09-23
### Fixed — a flow sequence refused `#` and `:` where YAML reads them as text

View file

@ -11,7 +11,7 @@ framework-agnostisk kode.
Referanse-implementasjon: `claude-code-llm-wiki` Stage B (`tools/wiki_ingest/`).
Lexikon-seed: `injection-patterns.mjs` fra `llm-security`-pluginen.
Repoet er på **v1.4.1** (release-commiten bumper alle levende versjonsflater:
Repoet er på **v1.4.2** (release-commiten bumper alle levende versjonsflater:
`pyproject.toml`, `__init__.py`, README-badge + status + install-pin,
ADOPTION-BRIEF, BRIEF, CHANGELOG-overskrift). NB: v1.4.0-taggen bærer `1.3.0` i
dokumentflatene — dens release-commit daterte bare CHANGELOG-en — og tags
@ -61,8 +61,14 @@ som `active:raw-html-link` (MEDIUM), og en tagg hvis hele affordans ER en URL de
ikke bærer (`</a>`, `<Frame>`, `<video />`) er inert. Klassifisering skjer i
`active_tag_class`; `is_active_tag` er en tynn wrapper, og census patcher den
FØRSTE (en boolsk patch kan ikke uttrykke en regradering).
ZWJ (U+200D) dømmes på KONTEKST, ikke identitet — unntas kun mellom to emoji, på
begge flater (`sanitize` eier predikatet, `output` importerer det).
**Usynlige tegn (1.4.2):** hele nevneren i `docs/threat-2026/codepoints.json`
(DICP ∪ Cf ∪ Co, Unicode 18.0, 141 674 tegn) fanges, eller unntas på KONTEKST:
én VS15/16 etter emoji, én ideografisk velger etter CJK, ZWNJ/ZWJ ved arabisk
skrift eller indisk skrift, LRM/RLM/ALM i dokument med RTL, arabisk talltegn foran
siffer, ZWJ mellom to emoji. En rekke er alltid bærer, og privat bruk fanges helt
(operatøren 28.10). Ett predikat (`sanitize._invisible_carriers`) på begge flater.
Leksikonraden for orddeling dømmer leddet sitt med samme predikat, mens regex-en
er byte-lik med commons.
Start med `docs/BRIEF.md` for design, `README.md` for bruk, `docs/PLAN.md` for
byggerekkefølgen.

View file

@ -2,7 +2,7 @@
Write-time defensive layer for Python pipelines that persist LLM output: sanitize, fence, tool-less quarantined transform, capability isolation, scan before persist, fail-secure.
![Version](https://img.shields.io/badge/version-1.4.1-blue)
![Version](https://img.shields.io/badge/version-1.4.2-blue)
![Status](https://img.shields.io/badge/status-stable-brightgreen)
![Python](https://img.shields.io/badge/python-3.10%2B-purple)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
@ -33,7 +33,7 @@ at write time, never assumed from the format. Any pipeline ingesting external da
into an agent-read store has this shape; an OKF wiki is its canonical form — which
is why the guard ships a first-class OKF adapter (below).
**Status:** `v1.4.1`. The stdlib-only core — its detector, contract, and
**Status:** `v1.4.2`. The stdlib-only core — its detector, contract, and
OKF-adapter modules plus the top-level wiring — is built and tested, exercised by
an end-to-end showcase and adversarial + false-positive corpora. The exported
Python surface is now frozen under semver: nothing exported is removed, renamed or
@ -58,14 +58,14 @@ are real limitations, stated plainly below; read them.
Not on PyPI. The guard is distributed from its Forgejo origin — pin a release tag:
```bash
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v1.4.1"
pip install "llm-ingestion-guard @ git+https://git.fromaitochitta.com/open/llm-ingestion-pipeline-security.git@v1.4.2"
```
Or take a version range from the Forgejo package index, which carries `1.4.1`
(and `1.1.0`; `1.2.0`–`1.4.0` were never published there):
Or take a version range from the Forgejo package index, which carries `1.4.2`,
`1.4.1` and `1.1.0` (`1.2.0`–`1.4.0` were never published there):
```bash
pip install --index-url https://git.fromaitochitta.com/api/packages/open/pypi/simple/ "llm-ingestion-guard>=1.4.1,<2"
pip install --index-url https://git.fromaitochitta.com/api/packages/open/pypi/simple/ "llm-ingestion-guard>=1.4.2,<2"
```
The `open/` mirror is anonymously readable, so CI needs no deploy key, token, or
@ -204,16 +204,16 @@ tier + disposition, emitted to `log.md`).
## What it protects against
Concrete attack classes, grouped by OWASP LLM Top-10 (2025) anchor. Every row is
driven by a **live payload** in the coverage matrix — run it to watch all 134 pass
driven by a **live payload** in the coverage matrix — run it to watch all 142 pass
in your own environment:
```bash
python -m llm_ingestion_guard.coverage # 130/130 classes; exit 0 = all as documented
python -m llm_ingestion_guard.coverage # 136/136 classes; exit 0 = all as documented
```
| Anchor | Attack classes it stops (representative) |
|---|---|
| **LLM01 · prompt injection** | 83 instruction-override lexicon classes (ignore / forget / disregard / suspend-constraints, role-play, jailbreak, …); hidden carriers — zero-width stego, BIDI override, Unicode-tag stego, HTML-comment, `data:` URI — stripped on input **and** re-checked at the persist gate; high-entropy base64/hex blobs; an injection hidden inside a base64 blob (decoded, then re-scanned) |
| **LLM01 · prompt injection** | 83 instruction-override lexicon classes (ignore / forget / disregard / suspend-constraints, role-play, jailbreak, …); hidden carriers — every invisible codepoint in Unicode 18.0 (zero-width, BIDI override, Unicode-tag, variation-selector and Sneaky-Bits smuggling, private use; legitimate emoji, CJK, Arabic-script, Indic and right-to-left uses exempt on context), HTML-comment, `data:` URI — stripped on input **and** re-checked at the persist gate; high-entropy base64/hex blobs; an injection hidden inside a base64 blob (decoded, then re-scanned) |
| **LLM02 · sensitive-info disclosure** | Secret egress in output — AWS keys, tokens, private keys, the credential set; a base64-*wrapped* secret (decoded → `decoded:egress:*`) |
| **LLM05 · improper output handling** | Zero-click exfil carriers (EchoLeak, CVE-2025-32711) — markdown-image auto-fetch, inline / reference / autolink links, raw active HTML, standalone `data:` URIs; non-`https` `resource` URLs |
| **LLM06 · excessive agency** | A tool surface on the quarantined transform; credential use beyond the per-stage allowlist; capability isolation (`scoped_env`) |
@ -233,8 +233,8 @@ out in [Known limitations](#known-limitations).
The actual product is this checklist, encoded as code you wire in order:
1. **Sanitize before fence.** Strip carrier classes (zero-width, BIDI,
Unicode-tag, HTML comment, `data:`) from untrusted input first.
1. **Sanitize before fence.** Strip carrier classes (every invisible codepoint
not exempt on its context, HTML comment, `data:`) from untrusted input first.
2. **Fence untrusted input.** Spotlight-mark it in a randomized per-call
delimiter; strip attacker fence markers from the payload.
3. **Tool-less transform.** Call the model with zero tools. A successful
@ -290,7 +290,7 @@ a green scan means safe content. The highest-impact items:
two of the three corpora are living, so the cells are not rewritten in place.
Method and before/after: [`docs/rawhtml-census.py`](docs/rawhtml-census.py).
**Full list — 45 items, each with the mechanism, plus the out-of-scope boundary:**
**Full list — 46 items, each with the mechanism, plus the out-of-scope boundary:**
[`docs/LIMITATIONS.md`](docs/LIMITATIONS.md). Several carry field measurements from
consumer corpora, including the false positives the URL-shape rule actually produces.

View file

@ -4,7 +4,7 @@
especially one converging on Google's Open Knowledge Format (OKF v0.1) — and needs
to decide **when** and **where** to add a write-time ingestion guard.
**Status of the guard:** `v1.4.1`. Stdlib-only core, framework-agnostic. The
**Status of the guard:** `v1.4.2`. Stdlib-only core, framework-agnostic. The
exported Python surface is frozen under semver — nothing exported is removed,
renamed or given a different meaning without a `2.0.0`. Detection behaviour is
*not* frozen: severities, thresholds and lexicon entries are calibration and move
@ -142,9 +142,9 @@ live payload:
python -m llm_ingestion_guard.coverage # exit 0 = all as documented
```
As of `v1.4.1`: **130 / 130 defended classes demonstrated (recall 100%)** and **6 /
As of `v1.4.2`: **136 / 136 defended classes demonstrated (recall 100%)** and **6 /
6 documented gaps still hold** (a *closed* gap fails the test, forcing a doc
update). The matrix is the single source of truth for the test suite (**910
update). The matrix is the single source of truth for the test suite (**1049
passing**), which also asserts total recall, that every lexicon pattern has a
case (so the matrix cannot fall behind the lexicon), the full LLM02 secret-egress
set, and the container-layer front-end (CSV formula-injection, zip-slip/bomb,

View file

@ -3,7 +3,7 @@
**A reusable, minimal, dependency-light defensive layer for LLM *ingestion*
pipelines — the write-time siblings of query-time chatbot guardrails.**
Status: implemented — v1.4.1, exported surface frozen under semver. This document defines what the repo contains
Status: implemented — v1.4.2, exported surface frozen under semver. This document defines what the repo contains
and why; the stdlib-only core is built and tested (see `README.md` for usage and
`docs/PLAN.md` for the build order).

View file

@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "llm-ingestion-guard"
version = "1.4.1"
version = "1.4.2"
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

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