CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# 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=<address>` 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 (`<value>`, `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 (`<PERSON_1>` 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=<EMAIL_ADDRESS_1>` 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 `/Users<PERSON_1>arie`: 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 `<path>` 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<PERSON_1>'`) 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
(`<PERSON_3>,`, `<person_3>`) 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.<TYPE>.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 `<TYPE_n>`
placeholder (it previously emitted a literal `<REDACTED>`), 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` (`<PERSON_1>`), 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 <path>` 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.