# Changelog All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and the project uses [Semantic Versioning](https://semver.org/). ## [0.8.2] - 2026-11-09 ### Added - Memory bounds for detection, both off by default. See [configuration](docs/configuration.md#bounding-detection-memory). - `pii.detectors.presidio.max_chunk_chars`: spaCy needs about 1GB per 100,000 characters per language, and an agent request can carry a whole document in one message, so a long enough one got the process killed. spaCy now parses longer text in pieces joined back into one document, and the recognizers still run once over the whole text: pattern detections are unchanged whatever the length of the value, only spaCy's named entities can differ next to a cut. Texts over spaCy's `max_length` are analysed too. - `pii.max_concurrent_detections`: how many texts run through the detectors at once. Six requests carrying the same new 100,000-character message (an agent's `count_tokens` burst) took a gateway from 2 to 8GB; with a limit of 1 a request that waited is served from the detection cache, and a cached text never waits. ## [0.8.1] - 2026-11-02 ### Added - `pii.allow_list`: exact values or regexes that a detection must match in full to be forwarded unredacted, optionally per entity type, for strings that only look like PII (OpenSSH algorithm names such as `chacha20-poly1305@openssh.com` read as e-mail addresses, #40). A span is let through only when every detection under it is listed; entries without `entity_types` never apply to `SECRET` or a blocked type; a pattern that matches the empty string or a sample PII shape in full is refused at startup. `/v1/pii/inspect` lists what the list let through under `allowed` and what kept a listed value redacted under `not_allowed`. See [configuration](docs/configuration.md#allowing-known-non-pii-values). - Private key blocks (armor included, also JSON-escaped or cut short), JSON Web Tokens and HTTP Basic credentials are recognized as secrets by their shape, in every preset that enables Presidio; Bearer and Basic values inside a header map too. On the test side of Samsung's CredData (154 repositories), the `minilm` preset goes from 28% to 35% of credentials fully masked; `onnx` stays at 60%. ### Changed - The docs no longer present `ultrafast` as the preset for coding agents: on secrets in code, the Privacy Filter model of the default `onnx` preset finds far more generic keys and secrets than PrivAiTe's model (see [presets](docs/detection.md#presets)). - The published benchmark is now the 3 privaite-bench corpora PrivAiTe's model never trained on (627 documents labeled by their authors) instead of 120 AI4Privacy documents labeled by agents. The README table, the presets table and the `light` preset's startup warning quote it: `light` ~45%, `onnx` ~73% of direct identifiers fully masked. ## [0.8.0] - 2026-10-30 ### Added - Two opt-in presets run PrivAiTe's own PII model, [`crp4222/privaite-pii-minilm`](https://huggingface.co/crp4222/privaite-pii-minilm) (multilingual MiniLM, 15 types), in place of the Privacy Filter: `minilm` (ONNX on CPU, any platform) and `ultrafast` (Core ML on the Apple Neural Engine, with Presidio on blank spaCy pipelines; macOS, `pip install 'privaite[ane]'`). The default stays `onnx`: nothing changes unless you set one. The detector is also available on its own (`detectors.minilm`, `backend: onnx | onnx_int8 | coreml`), and Presidio can run without spaCy models (`detectors.presidio.nlp_engine: blank`). The Open WebUI filter (0.1.19) and the LiteLLM guardrail offer both presets and require `privaite>=0.8.0`; the Docker image can bake the model with `--build-arg MINILM=onnx`. ### Security - LiteLLM guardrail: `extra_body` was not scanned, and LiteLLM merges it over the request it sends, so `extra_body.messages` reached the provider raw, past `block_entities` too (reproduced through a LiteLLM proxy). Every string leaf of `extra_body` is now scrubbed under the request's mapping. With `block_entities` set, a prompt or input sent as token IDs is refused, since no detector reads them. Replace your copy of `privaite_guardrail.py`. ## [0.7.3] - 2026-10-23 ### Security - The OpenAI-compatible endpoints passed every request field to LiteLLM, which reads call settings from them. A client could send `api_base` and, when the provider entry did not set one (the OpenAI entries of the quick-start `config/privaite.openai.yaml`), get the request sent to its own server with the operator's API key. With `extra_body.messages` (or `extra_body.input` on embeddings) it could replace the anonymized payload, so the provider received the raw text. Only the OpenAI parameters of each endpoint are forwarded now; anything else gets a 400 `unsupported_parameter` before it is scanned or sent (a field sent as `null` is dropped). A provider-specific field (`top_k` for Ollama, say) is allowed per provider with `providers[].forward_params`, which refuses call settings at startup. A client that sent such a field gets a 400 after the upgrade: see [request parameters](docs/configuration.md#request-parameters). - The Docker image is built on the current `python:3.13-slim` base (Python 3.13.17, published 21 October). Images up to 0.7.2 used a base from 24 June, without the Debian security fixes released since. The base digest is now refreshed at every release. ### Changed - Open WebUI filter 0.1.18 and the LiteLLM guardrail require `privaite>=0.7.3`. - The example config uses `glm-5.3-flash:cloud`: Ollama retired `glm-4.6:cloud` on 16 June. ## [0.7.2] - 2026-10-08 ### Security - Dependencies no longer contact third parties on their own. ONNX Runtime 1.29 and later ships with telemetry enabled and uploads runtime, hardware and model metadata with a hashed device identifier to Microsoft; LiteLLM fetches its price table from GitHub at import; the Hugging Face Hub is asked about model files already in the cache; tldextract downloads the public suffix list. None of it carries request text, and a packet capture on the 0.7.1 image showed all four. PrivAiTe now sets `ORT_DISABLE_TELEMETRY`, `HF_HUB_DISABLE_TELEMETRY` and `LITELLM_LOCAL_MODEL_COST_MAP` before those libraries load (the Docker image sets them too), reads a pinned model and tokenizer from the cache without a request, and uses tldextract's bundled list. On an earlier version, export `ORT_DISABLE_TELEMETRY=1` and `LITELLM_LOCAL_MODEL_COST_MAP=True` yourself. See [outbound connections](docs/configuration.md#outbound-connections). ### Fixed - ONNX: on `.env` files and shell exports the model's spans did not follow the values. A password in a connection URI ran on through the host and into the next line, which removed the variable declared there; a fragment of a variable name was replaced on its own, so `SUPPORT_CONTACT=
` could come out as `SUP[SECRET]`. A span is now cut where the next line assigns an upper-case name, a piece that stays inside such a name is dropped, a span that starts in the name is cut to the value, and a URI password stops at the `@` that closes it. Only structure is given back, never a character of a value. Lower-case names (`user=...`) stay as the detector returned them. - Structured secrets: recognize upper-case environment names ending in `TOKEN`, `SECRET` or `KEY` at the start of a line when the value looks opaque (`API_TOKEN`, `JWT_SECRET`, `OPENAI_KEY`), names ending in `PASS` or `_PWD`, a URI password with no user name (`redis://:password@host`), and the documented key formats of common providers wherever they appear (OpenAI and Anthropic `sk-`, Stripe, GitHub, GitLab, Slack, AWS access key ids, Google API keys, Hugging Face). The model missed these on some files. Both sets of rules also apply behind the line numbers an agent's read tool adds, a diff marker or a compose list item. - Email addresses: a trailing `,`, `;` or `.` is no longer part of the span. - spaCy's English date entity also fired on code (`connect(api_key`, `f.write(json.dumps(entry`), rewriting it with date placeholders when English is the first configured language. A date span that holds code (a call, a snake_case identifier, brackets, an assignment) is now cut there and only the pieces holding a digit are kept, so every number it covered stays covered. Other spans are unchanged. - `privaite verify` printed a single "Round trip" time that was mostly the detection model loading, which read as the latency of a request. The report now gives the startup and the request through the proxy separately, and `--json` gains `startup_ms` and `request_ms`. `elapsed_ms` is unchanged. ### Documentation - The order of `languages` matters: spaCy's statistical NER is kept only for the first one. Documented with its effect on the `light` preset. - README rewritten around coding-agent traffic and the `privaite verify` demo. The full threat model now lives in `docs/threat-model.md`, the presets table in `docs/detection.md`, and the comparison page lists the gateways built for agent traffic from what their own docs state. ## [0.7.1] - 2026-10-05 ### Fixed - ONNX: newer Transformers and tokenizers releases could drop every window after the first, leaving long requests partly unscanned. PrivAiTe now slices a full tokenization itself, preserving overlap and absolute offsets. Coverage checks block requests when tokens or source text are omitted; tokenizers that add special tokens are refused at startup. - Structured secrets: protect credentials passed as command-line options (`--password`, `--passwd`, `--api-key`, `--client-secret`, the token options, `--mot-de-passe`, and prefixed forms such as `--db-password`), including escaped quotes in commands echoed back by agent CLI clients. The original command syntax is preserved on restoration. Help text and prose are left alone: a metavariable (``, `PASSWORD`), a variable reference (`$DB_PASSWORD`) or a plain word after the option is not a value. - A credential the structured secret rules identified is treated as a secret wherever the same request repeats it afterwards. An agent handed a restored password repeats it bare, in its reasoning or in the next command, where no detector has the context to recognise it: in a real OpenCode session the vault password came back inside the assistant's reasoning and reached the provider on the next turn. Limited to values of 8 characters or more that contain a digit, in the texts that follow the first detection. - Chat: scrub plaintext reasoning, refusals and audio transcripts echoed in subsequent requests. OpenCode could resend restored values in `reasoning_content`, exposing them to the provider on the next turn. ### Changed - Open WebUI filter 0.1.16 and the LiteLLM guardrail require `privaite>=0.7.1`. ## [0.7.0] - 2026-10-02 ### Changed - Open WebUI filter 0.1.15 and the LiteLLM guardrail require `privaite>=0.7.0`. - The `onnx` preset loads the `q4` export of the Privacy Filter instead of `q4f16`. Same 4-bit weights, computed in fp32 rather than fp16, which CPUs do not run natively: a window runs 1.4x to 1.6x faster on CPU with the same detections (246 of 256 outputs identical, no labelled value lost, recall 84.9% to 85.2%). `onnx_variant: q4f16` keeps the previous model, and `docs/detection.md` has the measured comparison. An upgraded install downloads `q4` (about 917 MB) once on its first start; a host with no network access must fetch it beforehand or pin `onnx_variant: q4f16`, or the proxy refuses to start. - The model download follows `onnx_variant` when no variant is passed, so the Docker image prefetch and the detector always agree. `--build-arg ONNX_VARIANT=q4f16` bakes the other variant into the image. ## [0.6.1] - 2026-10-02 ### Changed - Open WebUI filter 0.1.14 and the LiteLLM guardrail require `privaite>=0.6.1`. - The Open WebUI filter docs say what to do about title, tag and follow-up generation: Open WebUI sends them without running any filter, so with a cloud model the standalone proxy is what covers them, since they go through the same connection. ### Fixed - Gateway: Codex refreshes its model catalog with `GET /v1/models` on the provider it is pointed at, and PrivAiTe answered with its own alias list behind PrivAiTe auth, so a live Codex session logged a 401 on every turn. In gateway mode the route now serves PrivAiTe's list only to a caller holding a valid PrivAiTe key and relays anyone else to the `openai_responses` upstream with their own credentials, so a PrivAiTe key is never sent to the provider. With `auth.enabled: false` the route always relays in gateway mode. ## [0.6.0] - 2026-09-28 ### Added - `docs/redact-pii-before-llm.md`: the page someone lands on when they are still deciding how to do this at all. Where PII hides in a request, the three ways to remove it (in your own code with Presidio, in a gateway guardrail, or in a proxy), what each one costs, the alternatives including where PrivAiTe loses, and why the check belongs on the outbound request body rather than the restored reply. - `privaite verify`: sends an agent-shaped request through a real PrivAiTe app to a throwaway provider on 127.0.0.1 and prints the request body that provider actually received, next to the same request sent straight to it. Planted values sit in message text, tool-call arguments and tool output. Exits non-zero if any of them reached the wire, so it works as a gate, and `--json` gives the machine-readable form. No credential is used and nothing leaves the machine. `docs/verify.md` led with reading the restored reply instead, which reports what came back rather than what went out: that hid a span defect that left part of a name on the wire while the reply looked clean. ### Changed - Open WebUI filter 0.1.13 and the LiteLLM guardrail require `privaite>=0.6.0`: both now pass the rest of the request body to the engine for the placeholder reservation below. ### Fixed - A placeholder-shaped string already in the request (`` in a test fixture, a template, a tool definition or the agent's system prompt) could also be issued to a detected value. The provider saw one placeholder with two meanings, and the restore wrote the real value into the client's own string, a planted `?to=` included. Numbering now skips every such string in the request, and one that arrives after its number was taken is sent as a fresh placeholder and restored to itself. The non-streaming restore is now single-pass like the streaming one, so a restored value is never rewritten a second time. The Open WebUI filter and the LiteLLM guardrail reserve the whole request body too (`tools`, Responses `instructions`). - The default `onnx` preset no longer started from a fresh Hugging Face cache once huggingface_hub 1.33 was installed, which a new pip install or Docker build resolves today: that release stores cached files in a shared blob store sharded by hash prefix, so the model and its `.onnx_data` weights resolved to different folders and ONNX Runtime refused the weights ("External data path escapes model directory"). The two files are now hard-linked side by side in the cache (copied only where links are refused), with no second download. Caches written by earlier hub versions are used as before. ## [0.5.0] - 2026-09-14 ### Added - `pii.detectors.presidio.disabled_recognizers`: switch off a recognizer PrivAiTe registers itself, by name. Empty by default, so secret and contextual detection are unchanged. A startup warning now names the types those recognizers can emit that the `entities` allowlist does not scope, which is the behaviour that made an allowlist read as if it had disabled them. An unknown name in the list is refused when the config loads: this knob widens what reaches the provider, so a typo must not read as a working setting. - Local structured-secret recognition for common credential assignments in plaintext tool outputs, connection URI passwords and Authorization bearer values. These rules run with Presidio under both `light` and `onnx`, preserve labels and quoted delimiters, and supplement all other detectors. - Documentation of the external Privy protocol-trace evaluation and experimental Kiji ONNX comparison, including lower coverage, excessive redaction and remaining SQL password misses. No new detection preset is enabled. ### Fixed - The date recognizer applied its French AND German month names to every configured language, so a deployment on other languages masked exactly the months spelled like the German ones (`April`, `August`, `September`, `November`, `juni`, `juli`, `oktober`) and left the rest of the calendar alone: a third of the year, with no way to turn it off, since the recognizers PrivAiTe registers are exempt from the `entities` allowlist. Each month list now applies only to its own language, and the numeric birth-date pattern, which carries no month vocabulary, stays active everywhere (issue #31). - The same union covered the contextual name and location recognizers, whose cues also applied to every configured language: `via` is a street in Italian but "by way of" in English, so "configured via Terraform" reported a location, and Dutch "ik ben klaar" reported a person under an English deployment. Every cue now belongs to the language its words come from. Stop words stay unioned on purpose, since they only ever shorten a match. - A cue that merely means "I am" ("je suis", "I'm", "ik ben") now requires a capitalised name, which is what separates "I'm Marie Dupont" from "I'm ready to go". Cues that announce a name ("my name is") still accept a lowercase one. - Overlapping types now obey the configured privacy policy before detector confidence: blocked types win, then irreversible types, then the existing resolution strategy. A detected secret can no longer become reversible or bypass a block rule just because an overlapping email has a higher score. - Detection-cache keys include the overlap-relevant policy to prevent stale merged types after a policy change. Cached data remains hashes and spans only. - The ONNX detector attached a path separator to the value the same way it attached a preceding space, and in that position it sometimes tagged only the first sub-token of the segment. `/Users/marie` was reported as `/m`, so the provider received `/Usersarie`: a broken path for an agent to act on, with `arie` still readable. Separators are now trimmed and a span starting right after one is extended to the end of its word. Both are limited to types whose value cannot itself begin with a separator, so a URL keeps its path and a secret keeps every character it has, a span made only of separators is never dropped, and a short span away from a separator is left as the model reported it. ## [0.4.3] - 2026-09-12 ### Added - Copyable English instructions for agents to preserve privacy placeholders in responses and tool arguments, with the distinction between prompting, detection boundaries and irreversible redaction documented. ### Changed - ONNX reuses identical input-window predictions during an engine/gateway scrub call. The request-local, 128-window cache holds salted hashes and detection metadata only, uses each text's live offsets, and closes on completion, failure or cancellation. It is isolated between requests and can be disabled with `pii.detectors.onnx.deduplicate_windows: false` for comparison. - Open WebUI filter 0.1.11 requires `privaite>=0.4.3`; both in-process integrations inherit the detector changes from the shared engine. ### Fixed - Built-in email, phone and URL detections no longer consume matched surrounding delimiters, common email assignment labels or `` wrappers. Boundaries are refined before union merging, so a padded detection cannot widen a correctly bounded neighbour. Secret punctuation and custom-pattern spans stay intact. - The ONNX detector reported spans that included the space preceding the value (`' Marie Dupont'`), because the tokenizer attaches that space to the word. The anonymizer then swallowed the separator (`'I am'`) and the restore put the space back INSIDE the value: replies came back with double spaces, and a tool-call argument came back as `{"to": " marie@..."}`, which the client sends verbatim. Spans are now trimmed where they are decoded, which also stops the union merge from widening a correctly bounded Presidio span onto its neighbour. - LiteLLM guardrail: the streaming restore dropped a held-back tail whenever the finish chunk did not carry that channel. The common bare `delta: {}` finish lost the end of a reasoning trace or a refusal, and the fragment of a tool-call or function-call argument cut by `finish_reason: length`; a stream closed without any `finish_reason` lost the tail of every channel, content included. The finish chunk now flushes every open buffer of its choice, creating the carrier it lacks, and trailing chunks drain what remains, mirroring the core streaming handler. - `fake_replacement`: the first retry after a collision asked the generator for variant 0, the seed of the initial candidate, so it reproduced the collision and the retry budget was effectively 9 of 10. Retries now walk variants 1 to 10. ## [0.4.2] - 2026-08-23 ### Added - A policy page (`docs/policy.md`) presenting `custom_patterns`, `entity_overrides` and `block_entities` as one declarative policy: a worked example combining the three, the four properties that make it enforceable (determinism, the inspect dry-run, the startup refusal of dead block rules, the preset-allowlist exemption), the boundary where a rule on an ML-detected type inherits the detector's recall, and the structured-only limit. Linked from the README preset section and the docs index. - A measurement page (`docs/agent-leak-measurement.md`): what a coding agent actually sends to its provider, measured at the wire on real Claude Code and Codex sessions. Includes the controls that answer "your prompt did the work" (an ordinary question with no mention of `.env` leaks the same 23 of 24 on the realistic fixture), the reproduced miss mechanism, and the cost. ### Changed - Open WebUI filter 0.1.9: the version had stayed at 0.1.8 while the file gained the tool-call argument escaping and moved its floor to `privaite>=0.4.1`, so an installed copy could not be told apart from the current one. - The comparison page gains a guard-model section (Llama Guard, gpt-oss-safeguard, Shieldstral): document-level verdicts carry no spans, so they cannot replace or restore values, and a model-judged policy is probabilistic where PrivAiTe's policy layer is deterministic; guard models in turn cover semantic moderation PrivAiTe deliberately does not. - The README leads with the measurement rather than with a per-type caveat: the irreversible handling of `CREDIT_CARD` and `SECRET` is unchanged and still stated, one paragraph lower. - Gateway scrub cost is restated from the 0.4.1 benchmark run in the README and `docs/gateway.md`: the per-request maximum without the detection cache is 42 s (Claude Code) and 72 s (Codex), against a 1 to 3 s median with it on. The previous "roughly 50 seconds per turn" predated that run. - Open WebUI filter 0.1.10: the declared floor moves to `privaite>=0.4.2`, which changes the file, so the version moves with it. ### Fixed - `llms.txt` announced privaite 0.4.0 and filter 0.1.8 for a whole release. It states the current version in prose, which the concatenation check could not see, so a test now pins those strings to the shipped versions. ## [0.4.1] - 2026-07-28 ### Fixed - Restored values are escaped when they are spliced into tool-call arguments, so a value carrying a quote, a backslash or a newline keeps the arguments valid JSON on the streaming path and in both in-process integrations. The gateway already did this. - Gateway restore handles a `type` key holding an object, which any tool schema with a property named `type` produces, and an unexpected restore failure returns the documented error shape. - The gateway no longer forwards the client's `accept-encoding`, so responses always arrive in an encoding it can decode. Duplicate header names are relayed verbatim, and a header allowlist keeps `content-type` and `anthropic-version`. - Anthropic blocks of an unrecognized type are scanned through the engine. Binary, opaque, thinking and encrypted payloads still travel byte for byte. - `block_entities` covers the agent prompt fields the gateway relays verbatim (Responses `instructions`, Anthropic `system`), so a blocked type there stops the request. - An unknown configuration key refuses startup and names the offending path. - `/ready` reports the engine's real state, the container healthcheck targets it, and the image runs as a non-root user. - Registered recognizers contribute their entity types to the Presidio allowlist, a named capture group in a custom pattern defines the span, and an unknown `device` value is refused at startup. ### Changed - Detection recall is 84.9% span and 81.0% strict, up from 84.5 and 80.6, with false positives unchanged. The `light` preset moves to 36.5% for the same reason. - Documentation now covers the measured limit of secret detection (a `KEY=value` assignment is detected on its own and missed once about one preceding line of log-shaped context is present, on every surface), the irreversible entity overrides in the shipped configs, and the gateway routes accepting requests without a PrivAiTe key by design. ## [0.4.0] - 2026-07-20 ### Added - Opt-in agent CLI gateway (`gateway.enabled`, off by default): native `/v1/messages` and `/v1/messages/count_tokens` routes for Claude Code (Anthropic Messages API, validated live end to end) and a `/v1/responses` route for Codex (OpenAI Responses API, beta). Requests are scrubbed at the same engine choke point, tool-call arguments included, the client's own auth is relayed verbatim (no key injected or validated on gateway routes), and responses are restored, streaming included. See `docs/gateway.md`. - Opt-in engine-level detection cache (`pii.detection_cache`) for clients that resend conversation history every turn (agent CLIs). Caches merged detection spans per exact text leaf, keyed by salted hash, bounded LRU with TTL; stores span metadata only, never text or values. Output is byte-identical with the cache on or off, and `block_entities` still blocks on cache hits. Off by default; the README threat model documents what enabling it changes. ### Fixed - LiteLLM guardrail parity with the 0.3.3 core fixes: the `/v1/completions` `prompt` (string and batch list shapes) and `suffix`, chat `prediction.content` and `web_search_options.user_location` are now scrubbed through the engine choke point (masking and `block_entities` included), and `message.refusal` and `message.audio.transcript` are restored in both the non-streaming and streaming paths. This fix also ships on its own as the 0.3.4 patch, so it is not new to this release. ## [0.3.4] - 2026-07-19 ### Fixed - LiteLLM guardrail: the auxiliary request fields the core proxy already scrubs were forwarded raw by the guardrail, and a `/v1/completions` request reached the model with its whole `prompt` intact (live-reachable through LiteLLM text completions, whose pre-call hook received the top-level `prompt` and returned early). The guardrail now scrubs the completions `prompt` (string and batch list-of-strings shapes; tokenized integer-array prompts stay unscanned as documented) and `suffix`, chat `prediction.content` (string or content-part list) and `web_search_options.user_location` through the same engine choke point, so masking and the `block_entities` gate apply, and the proxy's shallow body snapshot is overwritten for the top-level string fields. A request whose only user text sits in these fields no longer bypasses the pre-call scan. - LiteLLM guardrail restore parity with the core: `message.refusal` and `message.audio.transcript` are now restored in both the non-streaming and streaming paths. ## [0.3.3] - 2026-07-19 ### Fixed - Text-bearing request fields outside `messages` are now scrubbed before forwarding: chat `prediction.content` (OpenAI predicted outputs carry the client's current document), `web_search_options.user_location`, and the `/v1/completions` `suffix`. They previously rode the request passthrough to the provider unscrubbed. Request inputs only, so they scrub without a restore path, and the `block_entities` gate now applies to them. - Startup refuses a `block_entities` policy that lists a type no enabled detector can emit, instead of leaving a gate that silently never fires. The check is conservative: it never rejects a configuration whose producible set cannot be determined. - Custom patterns are no longer filtered out by the Presidio entity allowlist under the `onnx`/`max` presets, so a configured `custom_patterns` entity now actually fires. - `message.refusal` and `message.audio.transcript` are restored on the response, in both the non-streaming and streaming paths (previously placeholders reached the client). - Anonymization fails closed if a unique fake cannot be generated after repeated collisions, rather than returning a value that could cross-restore. - The pip quickstart in the README launched the server without exporting `PRIVAITE_API_KEYS`, so every request hit the fail-closed 401. The variable is now on the launch line. ### Changed - Documentation and discovery assets refreshed: `llms.txt`/`llms-full.txt` regenerated for the current release, per-page metadata and a sitemap on the docs site, and read-only default token scope plus SHA-pinned actions in the CI and publish workflows. ## [0.3.2] - 2026-07-17 ### Fixed - ONNX detector no longer selects the CoreML execution provider under `device: "auto"` on Apple Silicon. CoreML ran only a fraction of the model graph, was slower than CPU at every input size, and grew memory per input shape until the host process was killed. Because the Open WebUI filter and the LiteLLM guardrail run the engine in-process, a large request could take the host down with it. `auto` now uses CPU (or CUDA when present); `device: "coreml"` stays available as an explicit opt-in. ### Changed - ONNX detector scans long inputs through overlapping token windows instead of a single full-sequence pass. Large tool outputs and file contents that used to run multi-minute inferences at multi-GB peak memory (or were silently truncated past 128k tokens) are now scanned whole in bounded time and memory. Detection spans are identical to full inference on the benchmark corpus, so recall is unchanged. ## [0.3.1] - 2026-07-10 ### Security - Dependency floors raised past known CVE fixes (litellm 1.84, pydantic 2.4, transformers 5.3, torch 2.6, setuptools 78.1.1); the Docker base image is digest-pinned, with Dependabot keeping the digest and floors current. - **Failure logs can no longer serialize PII from detector/anonymizer exceptions.** Unexpected PII-processing and streaming failures now cross request boundaries as a safe, non-chained error; the proxy's JSON and text formatters omit exception tracebacks, which third-party libraries may populate with inspected text. - Built-in Hugging Face detector models are pinned to immutable commits by default, including the Docker image pre-download path. Fresh installs no longer follow a mutable model branch unless the operator explicitly sets `revision: null` or another ref. ### Fixed - Open WebUI now restores placeholders inside structured `function_call` output-item arguments, not only chat-shaped tool calls and text parts. ### Changed - CI and the PyPI publish workflow enforce `ruff format --check`; the repository is normalized with the pinned Ruff formatter version. ## [0.3.0] - 2026-07-09 ### Added - Official Docker image at `ghcr.io/crp4222/privaite` (multi-arch, detection model baked in, runs offline). Drop-in for OpenAI: `docker run -e PRIVAITE_API_KEYS=change-me -e OPENAI_API_KEY=sk-... ghcr.io/crp4222/privaite`. ### Changed - Internal refactoring for a cleaner, less-duplicated codebase. No change in behavior or detection. - Clearer Open WebUI / Docker connection docs (which URL, which key). ## [0.2.13] - 2026-07-03 ### Added - **Dry-run inspection endpoint** `POST /v1/pii/inspect` (off by default, `pii.inspect.enabled: true` to expose it): submit a text, get back the detections (type, exact span, score, source detector), the anonymized preview exactly as the provider would have received it, and `would_block` (the types your `block_entities` policy would reject). Nothing is forwarded to any provider, nothing is logged, nothing is counted in `/stats`, and no mapping outlives the request. The caller already knows the text it submitted, so returning its own detections leaks nothing. Built for "how do I check what was redacted?" (asked in discussion #1). README gained a "Verify what gets redacted" section covering this and the `deanonymization.enabled: false` recipe. ### Changed - The chat, completions and embeddings endpoints now share one request pipeline (`privaite/api/pipeline.py`) for the policy-sensitive plumbing: model validation, the fail-closed anonymization error policy, provider error mapping, SSE headers and response serialization. Previously three near-identical copies; a security fix could land in one and miss the others (the bare-string content scan fix had to be applied more than once). No behavior change: the whole pre-existing test suite passes unchanged. ## [0.2.12] - 2026-07-03 ### Added - **`max` preset**: the onnx suite (Presidio + `openai/privacy-filter`) plus a GLiNER detector (`urchade/gliner_multi_pii-v1`), an independent PII model trained on non-AI4Privacy data. It raises out-of-distribution recall at the cost of more false positives, so it is opt-in and never the default (`onnx` stays the default). GLiNER needs torch + the `gliner` package, installed via the new optional extra `pip install 'privaite[gliner]'`; with the `max` preset selected but the package absent, startup fails loudly with an install hint (fail closed, not silently off). Rationale and off-distribution numbers: the OOD cross-check in privaite-bench (`OOD_COMPARISON.md`). ## [0.2.11] - 2026-07-02 ### Fixed - Second audit pass (a three-reviewer sweep of the 0.2.10 diff, the integrations, and the never-reviewed periphery), everything below reproduced by a test that failed first: - **Fuzzy de-anonymization no longer injects the wrong person's PII.** A hallucinated placeholder with attached punctuation or different case (`,`, ``) defeated the guard and got rewritten to a known identity; matching now runs on the punctuation-stripped core, skips any angle-bracketed token case-insensitively, replaces only the core (so punctuation survives), and compares whitespace-normalized so a fake re-typed across a newline is still caught. - **Bare strings inside a `content` list are scanned.** `content: ["...", 42]` (and a mixed `/v1/completions` prompt list) forwarded the string raw and skipped the block gate. - **LiteLLM guardrail: Responses `input` is scanned item by item.** A mixed agentic turn (a message plus a `function_call_output`/tool call, or a bare string) bypassed detection AND the block gate; every text-bearing item is now scanned through the shared mapping. - **Open WebUI filter: original PII no longer lingers in metadata.** `outlet` now pops the reversible map (Open WebUI may persist message metadata), the map is only stashed when restore is on, an incoming client-supplied map is cleared, and list/reasoning assistant content is restored. Adds a build lock and moves the first-use spaCy download off the event loop. - **`huggingface_hub` import floor.** `EntryNotFoundError` was imported from a module that only exists in hub >= 0.25 while pyproject floors at 0.23; imported defensively now. - **Streaming: a fully-held-back chunk carrying `logprobs`, `refusal`, `usage` or any other payload is emitted** instead of suppressed; a nonstandard finish chunk with `function: None` no longer crashes the stream. - **Auth runs before the size limiter**, so an unauthenticated request is rejected on its headers instead of having up to `max_request_bytes` buffered first. - **Contextual name recognizer** keeps exact offsets on names with a double space (the last character used to leak); **French date recognizer** no longer matches month-prefix words (`3 maintenant`, `1 marseillais`). - **Config loader** raises on an explicitly requested missing file (a `--config` typo used to start an empty proxy) and never echoes interpolated secret values in a validation error. - `pii.enabled: true` with zero detectors is refused at startup; the numeric tool-argument scan is gated to values with >= 7 digits (ordinary counts/years/coordinates keep their type); `intersection` merge requires two DIFFERENT detectors to confirm a span; non-streaming chat restores the reasoning trace; batch `/v1/embeddings` counts one request in `/stats`, not one per item. ### Changed - The example config (also the Docker fallback) comments out the cloud providers that need `${OPENAI_API_KEY}`, so the image boots as-is; `docker-compose` marks `.env` optional; `CONTRIBUTING.md` code-style matches the actual (commented, docstringed) codebase; the FastAPI app version tracks `__version__`. ## [0.2.10] - 2026-07-02 ### Fixed - Fuzzy de-anonymization no longer destroys formatting: it used to rebuild the text with split()/join(), flattening every newline and indent (markdown, code blocks) in any response that carried PII. Replacements are now spliced into the original string. Fuzzy is also **opt-in now** (`fuzzy_matching` defaults to `false`): it carries a small wrong-substitution risk on lookalike spans, and it never rewrites an intact-but-unknown placeholder. - `/v1/completions` actually works against real providers: it is backed by litellm's text-completion path now, so the response is a real `text_completion` (not a chat object), PII restoration in `choices[].text` works, batched list prompts stay a list of strings, and streaming emits `text_completion` chunks instead of chat deltas. - The Docker `CMD` pointed at `config/privaite.yaml`, which is gitignored (it is the operator's own file), so an image built from a fresh clone had no config at all; it now falls back to the tracked example config. The Dockerfile pre-download step also stops passing `trust_remote_code=True`. - Overlapping detections merge to the UNION of their spans: a short higher-scored entity overlapping a longer one used to discard the longer one entirely, forwarding its uncovered remainder raw to the provider. `presidio_priority` overlap resolution falls back to score when neither entity is from Presidio. - `mask` and `redact` are truly irreversible: the original no longer enters the reversible map, so it is never restored into responses, and two values that mask to the same string ("****") can no longer cross-restore each other's PII. Detections are still counted in `/stats`. - Numeric JSON values in tool-call arguments are scanned: a card number sent as a bare JSON number used to bypass both masking and the `block_entities` gate. On a hit the leaf becomes the masked string; ordinary numbers keep their type. - Streaming rework: provider chunks are forwarded as-is (ids, `usage`, `logprobs` and the real finish chunk survive; no more synthetic duplicate finish chunk), `n>1` choices keep independent restore buffers instead of sharing one, the initial role chunk is no longer swallowed, held-back text is flushed even when the stream ends without a `finish_reason`, and `reasoning_content` / `reasoning` deltas are restored too. - Misconfigurations fail fast at startup instead of silently or per-request: `merge_strategy: intersection` with fewer than 2 enabled detectors (it would detect nothing and forward all PII raw), duplicate provider `model_name` aliases (last-wins overwrite), and a Presidio language with no spaCy model mapping (it used to pass init and then crash every request). - JSON log lines are built with `json.dumps`, so messages containing quotes or newlines no longer produce invalid JSON. - `/stats` now counts detections from `/v1/completions` and `/v1/embeddings`, not just chat. - The `bert_ner` and `mlmodel` detectors (presets `standard`/`full`) scan long inputs in overlapping windows: the HF pipeline silently truncates at model_max_length (~512 tokens), so PII past that point was invisible to them. The default `onnx` path was not affected (128k max_length). - `download_onnx_model` no longer requires a `.onnx_data` side file: a variant packed into a single `.onnx` used to fail at startup. ### Security - Hugging Face model loading defaults to `trust_remote_code=False` (the default model needs no repo code; the flag was granting an unused permission) and every detector accepts a `revision` pin so a rewritten model repo cannot silently swap the weights this proxy runs. ### Removed - Dead config knobs that were accepted but never read: `logging.redact_fields` and `anonymization.entity_overrides..domain_preserve`. ### Documentation - The README now states exactly which request fields are NOT scanned (`messages[].name`, `user`, `metadata`, tool definitions, JSON object keys) and that `passthrough.*` also bypasses `block_entities`. ## [0.2.9] - 2026-07-01 ### Added - `pii.block_entities`: a hard policy gate. A request containing any listed PII type is rejected with HTTP 400 and nothing is forwarded, instead of being pseudonymized. Empty by default, so the default behavior (mask everything with a placeholder) is unchanged. Enforced at the single anonymization choke point, so it covers message content, multimodal text, and tool-call arguments alike. The error names the blocked type(s) only, never the value. ### Changed - Anonymization now fails closed. On an internal detector error the request is blocked by default (`pii.on_error`, default `block`) rather than forwarded with raw PII; set `on_error: allow` to opt back into the old behavior. - The `light` preset runs full Presidio instead of a pinned entity allowlist that quietly capped its recall. - The engine warms its detectors once at startup, so the first real request does not pay the model cold-start cost. ### Integrations - LiteLLM custom guardrail (`integrations/litellm/privaite_guardrail.py`). Mount it next to a LiteLLM `config.yaml` and reference it by dot-notation to anonymize requests and restore responses inline, including inside tool-call arguments and the legacy `function_call`, the Responses API, and streaming, which LiteLLM's built-in Presidio guardrail does not cover. This is a repo integration, not part of the PyPI package; it imports `privaite` at run time. - Both the LiteLLM guardrail and the Open WebUI filter honor `block_entities`: they reject blocked types (an HTTP 400 for the guardrail, a raised error for the filter) and fail closed if the installed `privaite` is too old to enforce the gate, rather than silently forward the PII. ### Documentation - The benchmark now cites its data source: the open AI4Privacy `pii-masking-200k` dataset on Hugging Face. Because that dataset declares no explicit license, the benchmark repo commits only derived labels and fetches the source text on demand. ## [0.2.8] - 2026-06-27 ### Documentation - Discoverability pass. Richer PyPI keywords and Trove classifiers, a clearer one-line description, and a canonical positioning tagline in the README. Added `docs/comparison.md` (PrivAiTe vs Presidio, LLM Guard, and LiteLLM PII masking) and an `llms.txt` so AI coding assistants can identify PrivAiTe as a PII-redaction proxy. GitHub repository topics and About were refreshed too. ## [0.2.7] - 2026-06-27 ### Fixed - Anonymization method dispatch now lives in one place (the `Anonymizer`); the faker generator is reduced to producing fake values only, removing a duplicate implementation that had drifted. Two entity-override bugs are fixed as a result: an override with method `placeholder` now produces a numbered `` placeholder (it previously emitted a literal ``), and an override with method `fake_replacement` now generates a fake for the correct entity type (it previously used a generic fallback). An override with method `redact` now returns `[TYPE]`, matching the global redact output and the documentation (it previously returned `[REDACTED]`). ## [0.2.6] - 2026-06-27 ### Changed - The default anonymization method is now `placeholder` (``), matching the example config, the README, and the threat model, which already documented placeholders as the default. Previously the schema default was `fake_replacement`, so running with no config file produced realistic fake names instead of placeholders. Set `method: "fake_replacement"` to keep fakes. - The default Presidio language order is now `["fr", "en"]`, matching the example config and README. ### Documentation - Reworked the README hero around the differentiator (tool-call arguments, multimodal, agent egress, zero telemetry) with legally careful wording (pseudonymization, not anonymization). Corrected the "What's NOT detected by default" section: the default `onnx` preset does detect personal addresses and URLs; only Presidio's broad LOCATION/URL recognizers stay off. Added website landing copy under `docs/landing.md`, fixed stale latency and language figures, and noted the removed `[onnx]` extra in the 0.2.3 changelog entry. ### Tooling - CI typecheck now also covers the Open WebUI filter; the publish workflow runs lint, typecheck, and tests before publishing; a `.dockerignore` keeps a local config out of the image. Added a test that locks the Open WebUI filter contract. ## [0.2.5] - 2026-06-25 ### Fixed - The ONNX preset was discarding two entity types the Privacy Filter model actually detects: `private_address` and `private_url`. They are now mapped to `LOCATION` and `URL`, so addresses and personal URLs are anonymized instead of silently reaching the provider. On the comparative benchmark this lifts ONNX recall from about 72% to 84%. - Streaming responses now de-anonymize tool-call argument deltas and the legacy `function_call` arguments, not just message content. Each tool call buffers its own argument stream, so a placeholder split across deltas is still restored. Previously a streamed tool call delivered placeholders to the client instead of the real values; non-streaming already restored them. ## [0.2.4] - 2026-06-23 ### Changed - The default preset is now `onnx` (the full Privacy Filter suite). A fresh install detects everything including secrets and passwords out of the box, not only the classic regex and spaCy PII. Set `preset: "light"` for the fast, zero false-positive Presidio-only path, or `preset: null` to drive detectors by hand. - `onnxruntime`, `transformers`, and `huggingface_hub` are now core dependencies (previously the separate `onnx` extra), so the default preset works without an extra install. The Privacy Filter model is downloaded on first start. The `ml` extra now only adds torch, used by the `standard` and `full` BERT presets. ### Documentation - README leads with the ONNX full suite as the default preset and emphasizes that secrets and passwords are detected by default. ### Integrations - The Open WebUI filter now defaults to the `onnx` preset as well (valve), and requires `privaite>=0.2.4` so the ONNX dependencies are present. Bumped to v0.1.2. - The Docker image pre-downloads the ONNX Privacy Filter model at build time, so a container with the default preset starts fast and works offline from the first request instead of downloading the model on startup. ## [0.2.3] - 2026-06-22 ### Security - The request size limit is now enforced at the body-stream level, not only from the `Content-Length` header. The middleware was rewritten as pure ASGI and counts bytes as they arrive, so a chunked request that omits `Content-Length` can no longer exceed `server.max_request_bytes`. The buffered body is replayed to the application unchanged. - The size check runs against the projected total before each chunk is appended, so a single oversized frame is rejected without first being copied into the buffer. ### Added - Open WebUI Filter Function (`integrations/openwebui/`) that runs the engine in-process, with a setup guide. The response path now also restores PII inside the legacy `function_call`, matching the proxy. - `onnx` install extra (`pip install "privaite[onnx]"`, removed in 0.2.4 once onnxruntime, transformers and huggingface_hub became core dependencies, so no extra is needed anymore). The ONNX privacy-filter preset runs on onnxruntime plus the transformers tokenizer and does not need torch, so this extra installed that path without pulling torch or scipy. The `ml` extra (BERT NER) still installs torch, as that detector requires it. ### Tooling - CI lint now runs `ruff check .` over the whole tree, including `integrations/`, rather than only `privaite/` and `tests/`. ## [0.2.2] - 2026-06-22 ### Security - The `/stats` tracker now stores a salted hash of the session identifier instead of the raw value. The identifier can be derived from the `Authorization` header (an API key), so it is no longer kept in memory or exposed by `/stats`. ## [0.2.1] - 2026-06-22 ### Added - Request bodies are capped at `server.max_request_bytes` (10 MB by default); larger requests are rejected with HTTP 413. ### Documentation - Added an explicit **Threat model** section to the README (what the proxy does and does not protect against) so the privacy posture is not over-claimed. ## [0.2.0] - 2026-06-22 ### Added - PII anonymization for structured request content, not just plain-string message bodies: - **Multimodal content** (`content` as a list of parts): text parts are anonymized, other parts (images, audio) are passed through untouched. - **Tool / function calls** (`tool_calls[].function.arguments` and the legacy `function_call.arguments`): the argument string is parsed as JSON and scrubbed value by value (object keys and the function name are left intact), or treated as free text when it is not valid JSON. Nested objects and arrays are walked recursively. - **`/v1/completions` list prompts**: a `prompt` given as a list of strings is anonymized element by element under a single mapping. - Response-side de-anonymization of tool-call arguments for non-streaming `/v1/chat/completions`, so the client receives the real values back. - The `pii.passthrough.tool_calls` config flag is now honored. It was previously declared in the schema but never read. Default is `false` (anonymize); set it to `true` to forward tool-call arguments unchanged. - `pii.strict` option (default `false`): when enabled, a request whose content cannot be inspected (a shape that is neither text nor a known media part) is rejected with HTTP 400 instead of being forwarded to the provider. ### Security - Authentication now fails closed: when `auth.enabled` is set but no API keys are configured (`PRIVAITE_API_KEYS` empty), requests are rejected with 401 instead of being forwarded to the provider. A startup warning is logged for this case. ### Fixed - `--config ` is now honored by the app factory: the CLI exports `PRIVAITE_CONFIG_PATH`, and config loading uses `load_dotenv(override=False)` so the explicit value is not overwritten by a `.env`. Providers, PII, and auth now load from the file passed on the command line (previously only host and port were honored). - Contextual name detection now normalizes typographic apostrophes (U+2019 and similar) before matching intro patterns, so "je m'appelle X" typed with smart quotes (the default on macOS/iOS and most chat UIs) is recognized. Previously a lowercase name after a curly-apostrophe intro leaked to the provider. - Integration test fixture used `asyncio.get_event_loop()`, which raises on Python 3.12+. It now creates a dedicated event loop, so the suite is green on Python 3.11 through 3.13. ### Tooling - The type checker is now clean (`mypy privaite/` passes) and runs in CI as a dedicated job. CI also installs the spaCy models so the integration suite runs. - Added endpoint tests (chat, completions, embeddings) and streaming-handler tests covering the request and response anonymization paths. ### Notes - Streaming responses do not yet de-anonymize tool-call argument deltas. This is restoration only and never leaks: request-side anonymization runs before the stream opens, so placeholders, not real PII, are what may appear in streamed tool calls. ## [0.1.0] ### Added - Initial release: OpenAI-compatible proxy with local PII detection (Presidio + optional ONNX privacy filter), placeholder/fake/redact/mask anonymization, reversible de-anonymization, streaming support, and YAML configuration.