Metadata-Version: 2.5
Name: wattmind-pii-guard
Version: 0.1.0
Summary: An open-source privacy firewall for AI agents: reversible PII masking for text, documents and images.
Project-URL: Repository, https://github.com/watt-mind/pii-guard
Project-URL: Changelog, https://github.com/watt-mind/pii-guard/blob/main/CHANGELOG.md
Author: pii-guard contributors
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Framework :: FastAPI
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Requires-Python: <3.13,>=3.12
Requires-Dist: cryptography>=46.0.0
Requires-Dist: fastapi>=0.162.0
Requires-Dist: httpx>=0.28.1
Requires-Dist: pillow>=13.0.0
Requires-Dist: protobuf>=7.36.2
Requires-Dist: pydantic-settings>=2.16.1
Requires-Dist: pydantic>=2.14.1
Requires-Dist: sentencepiece>=0.2.2
Requires-Dist: torch>=2.14.1
Requires-Dist: transformers>=5.21.0
Requires-Dist: uvicorn>=0.54.0
Provides-Extra: baselines
Requires-Dist: de-core-news-lg; extra == 'baselines'
Requires-Dist: en-core-web-lg; extra == 'baselines'
Requires-Dist: huggingface-hub>=0.30; extra == 'baselines'
Requires-Dist: presidio-analyzer==2.2.365; extra == 'baselines'
Requires-Dist: presidio-anonymizer==2.2.365; extra == 'baselines'
Requires-Dist: spacy<3.9,>=3.8.7; extra == 'baselines'
Provides-Extra: hosted-azure
Requires-Dist: azure-identity>=1.27.0; extra == 'hosted-azure'
Requires-Dist: azure-storage-blob>=12.31.0; extra == 'hosted-azure'
Provides-Extra: local-vision
Requires-Dist: opencv-python-headless>=4.10; extra == 'local-vision'
Provides-Extra: ner
Provides-Extra: ocr
Requires-Dist: pytesseract>=0.3.13; extra == 'ocr'
Provides-Extra: sam
Requires-Dist: meta-sam-parser==0.1.0; extra == 'sam'
Description-Content-Type: text/markdown

# PII Guard

<picture>
  <source media="(max-width: 600px)" srcset="docs/assets/banner-narrow.svg">
  <img src="docs/assets/banner.svg" alt="PII Guard, a privacy gateway for AI agents: personal data becomes reversible placeholders before it leaves your network and comes back in the answer. You send 'Email Jane Placeholder: jane@example.net', the model sees 'Email [[PERSON_7f3a]]: [[EMAIL_20c2]]', you get back 'Done: emailed Jane Placeholder.' Built on Meta's open models, runs on your machine, Apache-2.0." width="100%">
</picture>

[![Try it live](https://img.shields.io/badge/try%20it%20live-guard.watt--mind.com-b6f36b)](https://guard.watt-mind.com)
[![Demo Video](https://img.shields.io/badge/demo%20video-3--min%20walkthrough-blue)](https://youtu.be/Lsr74ddhxRo)
[![CI](https://github.com/watt-mind/pii-guard/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/watt-mind/pii-guard/actions/workflows/ci.yml)
[![Model on Hugging Face](https://img.shields.io/badge/%F0%9F%A4%97%20model-pii--guard--ner--xlmr--base-yellow)](https://huggingface.co/watt-mind/pii-guard-ner-xlmr-base)
[![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)

<!-- intro:start -->
<!-- Generated by `pii-guard eval report` from the run records and the `readme.intro` section of docs/benchmarks/config.json; edit those, not this block. -->

- **What it does.** An OpenAI-compatible gateway for AI agents. Change one base URL, and the names, addresses, IDs and faces it detects become reversible placeholders (`[[PERSON_7f3a]]`) before the request leaves. The real values come back in the answer, mid-stream and inside tool calls. Detection is rules plus a small NER that runs on CPU: XLM-RoBERTa base, distilled from Meta's Muse Glimmer 30B.
- **How well.** On 990 fictional agent-traffic documents (de, en, hu), 0.2% of personal values reached the model and 0.7% of masks hit text that was not personal data. The lowest-leak alternative, PrivAiTe, leaked 2.4% and over-masked 35.2% on the same documents.
- **How fast.** The NER takes 43 ms per message on a laptop CPU (p50; p95 50 ms, [measurement](eval/results/2026-10-30-lab-1625-ner-v42/latency-cpu-local.json)). No LLM call and no API cost in fast mode.
- **Where your data goes.** In fast mode, the default (rules plus the on-device NER, no LLM call), the original text is processed only on your machine. The hosted demo receives what you type, so use fictional data there. The optional audit and SAM 3.1 endpoints receive the original text or pixels. Anything the detectors miss passes through: pseudonymization, not anonymization or a compliance guarantee.

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/benchmarks/leak-vs-overmasking-dark.svg">
  <img src="docs/benchmarks/leak-vs-overmasking.svg" alt="Leak rate against over-masking rate on 990 fictional agent-traffic documents; lower-left is better. PII Guard fast 0.2% leak, 0.7% over-masking; PrivAiTe 2.4% and 35.2%; Presidio 16.6% and 33.4%; LLM Guard 20.2% and 16.9%; PII Guard rules only 38.8% and 0.0%." width="100%">
</picture>

<p align="center"><sub>990 fictional documents (de, en, hu), drawn by <code>pii-guard eval report</code> from the <a href="eval/results/runs/">run records</a>. <a href="#results">All sets</a> · <a href="docs/benchmarks/README.md">Shareable PNGs</a></sub></p>
<!-- intro:end -->

<p align="center"><sub>Built for the Meta Global AI Developer Hackathon (Oct 26 – Nov 4, 2026) · <a href="https://youtu.be/Lsr74ddhxRo">3-minute video walkthrough</a>. Version 0.1.0, installed from git; no tagged release yet. <a href="docs/why-pii-guard.md#what-is-not-here-yet">Known gaps</a>.</sub></p>

## Quickstart

**Walkthrough:** [watch the 3-minute demo video on YouTube](https://youtu.be/Lsr74ddhxRo).

**No install:** the [live demo](https://guard.watt-mind.com) runs as a guest with no sign-up
(fictional data only; capped sponsored allowance).

**On your machine** (needs [uv](https://docs.astral.sh/uv/)): the API and the gateway on
http://127.0.0.1:8787, with a mock model that echoes what it receives:

```bash
uvx --python 3.12 \
  --from 'wattmind-pii-guard @ git+https://github.com/watt-mind/pii-guard' \
  pii-guard up
```

**Launch a coding agent directly** ([OpenCode](docs/integrations/opencode.md), [Codex](docs/integrations/codex.md), [Claude Code](docs/integrations/claude-code.md), [pi](docs/integrations/pi.md), [Muse Code](docs/integrations/muse-code.md)) behind an in-process local gateway with Meta's `muse-spark-1.3`:

```bash
export META_API_KEY=<your-key>
uvx --python 3.12 \
  --from 'wattmind-pii-guard @ git+https://github.com/watt-mind/pii-guard' \
  pii-guard run opencode --upstream meta --model muse-spark-1.3
```

- **First run:** downloads PyTorch and the NER once (the NER is 1.1 GB); `/healthz` reports
  `"ner": "ready"` when detection is live.
- **A real model:** `--upstream meta` with `META_API_KEY`; `openai`, `fireworks`, `openrouter`
  or any `--upstream-url` work the same way ([`pii-guard up` options](docs/operations.md#one-command-pii-guard-up), [coding agent integrations](docs/integrations/README.md)).
- **No auth by default**, bound to 127.0.0.1: set `PII_GUARD_API_TOKEN` before exposing it
  ([how to expose it safely](docs/operations.md#exposure-and-authentication)).

## Results

<!-- results:start -->
<!-- Generated by `pii-guard eval report` from the run records and the `readme` section of docs/benchmarks/config.json; edit those, not this block. -->

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/benchmarks/results-table-dark.svg">
  <img src="docs/benchmarks/results-table.svg" alt="Results table, leak · over-masking per fixture set for PII Guard fast, PrivAiTe, Presidio and LLM Guard; lower is better and the lowest per metric in each row is bold. PII Guard fast leaks 0.0% on the hand-written set, 0.2% on agent traffic, 0.6% on Nemotron-PII, 0.8% on CEE-PII and 19.1% on Gretel finance (Presidio 18.8%). Every number is in the text table below the image." width="100%">
</picture>

<details><summary>Table as text</summary>

| Leak · over-masking | **PII Guard fast** | PrivAiTe | Presidio | LLM Guard |
|---|---:|---:|---:|---:|
| Hand-written · 62 docs (de+en+hu+pl) | **0.0%**&nbsp;·&nbsp;**1.3%** | 13.0%&nbsp;·&nbsp;25.7% | 25.5%&nbsp;·&nbsp;26.1% | 46.3%&nbsp;·&nbsp;33.7% |
| Agent traffic · 990 docs (de+en+hu) | **0.2%**&nbsp;·&nbsp;**0.7%** | 2.4%&nbsp;·&nbsp;35.2% | 16.6%&nbsp;·&nbsp;33.4% | 20.2%&nbsp;·&nbsp;16.9% |
| Nemotron-PII · 300 docs (en) | **0.6%**&nbsp;·&nbsp;1.5% | 4.0%&nbsp;·&nbsp;5.6% | 12.8%&nbsp;·&nbsp;32.0% | 28.8%&nbsp;·&nbsp;**0.6%** |
| CEE-PII · 451 docs (en+hu) | **0.8%**&nbsp;·&nbsp;**7.3%** | 4.4%&nbsp;·&nbsp;59.2% | 36.5%&nbsp;·&nbsp;52.5% | 75.8%&nbsp;·&nbsp;66.4% |
| Gretel finance · 400 docs (de+en) | 19.1%&nbsp;·&nbsp;24.4% | 27.7%&nbsp;·&nbsp;46.5% | **18.8%**&nbsp;·&nbsp;75.4% | 59.3%&nbsp;·&nbsp;**17.0%** |

</details>

Leak: labeled personal values left fully readable in what the model receives. A value with only part left readable (a surname, the tail of a number) is a partial leak, counted separately: PII Guard fast 0.0% on agent traffic, 1.4% on the hand-written set. Over-masking: masks on text that was not personal data. Lower is better for both; in each row, the lowest leak and the lowest over-masking are bold.

**Caveats**

- The sets are small (tens to hundreds of documents per language): a point or two of difference is noise. Agent traffic is written by Muse models from fictional seeds, which can flatter a detector taught by Muse labels; that is why the hand-written and public sets sit next to it.
- Finance text is the weak spot. On Gretel finance's own labels PII Guard leaks 19.1%, about level with Presidio's 18.8%; on corrected labels ([why](docs/datasets.md)) 2.4% against 14.8%. Over-masking is high on Gretel finance (24.4%) and CEE-PII (7.3%), where reference numbers without a cue word come out as IDs.
- NER v4.2 leaks more than the previous v2.0 on CEE-PII (0.8% against 0.4%), the Polish pl-pii-bench (4.2% against 3.5%) and TAB court text (5.1% against 4.0%, scored locally only), and less on the hand-written sets, agent traffic and the EU multilingual set ([model notes](docs/models.md)).

<details><summary>How this is measured</summary>

Every tool ran on the same documents (byte-identical files) and was scored by the same code; comparison tools use their documented defaults ([configurations](eval/results/2026-10-27-comparison/README.md#systems)). All data is fictional. PII Guard fast is rules plus the published NER `v4.2` ([model card](training/ner/model-card.md)). The hand-written set pools de, en, hu, pl; the full tables, per-language numbers, Ai4Privacy (in-distribution for the NER, so not a win) and the corrected finance labels are in the [benchmark report](docs/benchmarks.md#results-by-fixture-set).

NER model time per message on CPU (the model alone, not the whole gateway): p95 50 ms (122 messages of about 382 characters, arm64 macOS 26.5, 8 threads; [measurement](eval/results/2026-10-30-lab-1625-ner-v42/latency-cpu-local.json)).

Records behind the PII Guard fast column: Hand-written ([1](eval/results/runs/2026-10-30-lab-1625-pl-smoke-v42-b3.json) · [2](eval/results/runs/2026-10-30-lab-1625-smoke-v42-b3.json)), [Agent traffic](eval/results/runs/2026-10-30-lab-1625-agent-traffic-v42-b3.json), [Nemotron-PII](eval/results/runs/2026-10-30-lab-1625-nemotron-pii-v42-b3.json), [CEE-PII](eval/results/runs/2026-10-30-lab-1625-cee-pii-v42-b3.json), [Gretel finance](eval/results/runs/2026-10-30-lab-1625-gretel-finance-v42-b3.json); every other cell is in the [run log](docs/benchmarks.md#run-log).

</details>
<!-- results:end -->

Per-language results, the open NER models compared behind the same rules, and latency are in the
[benchmark report](docs/benchmarks.md).

## Which Meta models do what

Muse Glimmer 30B was the best zero-shot detector we measured (F1 99.3, 0.0% leak on the 38
Hungarian and English hand-written documents of that run,
[record](eval/results/runs/2026-10-27-glimmer-zero-shot-rules-llm-low.json)) and far too slow
for a request path at 9.4 s per document (p50). So it labeled the training text instead, and
**XLM-RoBERTa base** learned from it: 43 ms on CPU, published as
[`watt-mind/pii-guard-ner-xlmr-base`](https://huggingface.co/watt-mind/pii-guard-ner-xlmr-base) `v4.2`.

> In the default fast mode, every model that sees original personal data runs on your machine.
> Audit mode (Muse Spark Open) and SAM 3.1 are opt-in and receive originals at the endpoint you
> configure.

| Job | Meta model |
|---|---|
| Fast-mode NER (CPU) | **XLM-RoBERTa base** (FacebookAI, MIT), fine-tuned: [model card](training/ner/model-card.md), [training](docs/models.md#ner-v4-v41-and-v42-v42-is-published) |
| Teacher: labels training text | **Muse Glimmer 30B** (open weights, Apache-2.0): [distillation](docs/models.md#distilled-from-muse-glimmer) |
| Benchmark writer; demo model | **Muse Spark** (Meta Model API): [which model does what](docs/models.md#which-model-does-what) |
| Audit mode, red team | **Muse Spark Open** (open weights): [audit runs](docs/benchmarks.md#smoke), [red team](eval/results/2026-10-27-redteam/README.md) |
| Fictional ID specimens | **Muse Image**: painted the portraits, signatures and stamps ([generation record](eval/fixtures/visual/generation.json)) |
| Signatures in images | **SAM 3.1** (Meta Model API, decoded with Meta's `meta_sam_parser`); faces stay on the device (YuNet): [specimen results](eval/results/visual/specimens.md) |

## Inside the gateway

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="docs/benchmarks/how-it-works-dark.svg">
  <img src="docs/benchmarks/how-it-works.svg" alt="How it works: your app or agent sends a request to PII Guard, which runs on your side. On the way out PII Guard does 1. extract text, tool calls and image regions; 2. detect personal data with rules, then NER, then an optional audit LLM; 3. apply policies and pseudonymize with keyed placeholders; 4. mask image regions (faces, SAM 3.1, OCR). Only the masked request, with placeholders such as [[PERSON_968e29]] in place of personal data, crosses to the LLM provider. The response streams back to PII Guard, which does 5. restore placeholders chunk by chunk, and your app or agent gets the answer with the real values. The placeholder-to-value mapping never leaves PII Guard." width="100%">
</picture>

The gateway speaks the OpenAI Chat Completions and Responses APIs. It masks messages, **tool
definitions, tool-call arguments, tool results** and images, restores placeholders in streams,
keeps one placeholder per person per session, and fails closed on anything it can't mask; the
mapping never leaves PII Guard. Every document in the hand-written set unmasks back to its
original exactly.

| Mode | Detection | Leaves your machine |
|---|---|---|
| `fast` (default): everyday traffic | Rules + on-device NER; no LLM call | Only the masked request, to your LLM provider |
| `fast` with `PII_GUARD_DETECTORS=rules`: offline runs, CI, structured data | Rules only; free-text names only as [known values](docs/api.md#sessions-and-restore) your app registers | Only the masked request |
| `audit` (opt-in, `X-PII-Guard-Mode: audit`): measuring the upper bound, training labels | Rules + NER + an LLM detector; fails closed if that call fails | The original text, to the audit endpoint you configure |

**Grammar-aware.** In Hungarian a name changes with every case; the ending stays outside the
placeholder:

| | Hungarian (real output), with an English gloss |
|---|---|
| You send | Kérem, írjon Próba Zsuzsannának a zsuzsa.proba@example.com címre.<br><sub>Please write *to Zsuzsanna Próba* at …</sub> |
| The model sees | Kérem, írjon [[PERSON_968e29]]nak a [[EMAIL_e7038b]] címre.<br><sub>… *to [[PERSON]]* …</sub> |
| You get back | Megírtam a levelet Próba Zsuzsannának.<br><sub>I wrote *to Zsuzsanna Próba*.</sub> |

**Language packs.** Hungarian, English, German and Polish each ship validated identifier rules,
cue words and case handling; a new language is one package
([how to add a language](docs/languages.md)).

**What it detects:** people, addresses, emails, phone numbers, dates of birth, national and
document IDs (including Hungarian tax and TAJ numbers), bank accounts and payment cards,
vehicle plates, parcel numbers and secrets such as API keys, plus faces, ID photos and
signatures in images ([full entity list](docs/entities.md)).

**Real agents behind it.** On 2026-10-29 four unmodified coding agents (pi, OpenCode, Codex,
Muse Code) worked a fictional customer file through the gateway: **115 requests, none of the
file's 9 personal values reached the model**, and the answers and the files on disk came back
with the real values. No turn was refused; in an earlier run on 2026-10-28, 7 of 15 were, before
two gateway fixes (a system note saying the placeholders are intentional, and the NER no longer
masking file names). Two Codex turns stopped on gateway bugs; ten turns is a sample, not a
benchmark ([harness matrix](docs/integrations/harness-matrix-2026-10-29.md)).

[Architecture](docs/architecture.md) · [sessions and modes](docs/architecture-gateway.md) ·
[threat model](docs/privacy-model.md) · [GDPR context](docs/eu-regulation.md)

[Thirteen differentiators, each with evidence](docs/why-pii-guard.md).

## Use it from code

### Round trip

Mask, then unmask with the session PII Guard keeps. With `pii-guard up` running, this script
takes the session id and the person placeholder from the first response (output from fast mode):

```bash
R=$(curl -sS http://127.0.0.1:8787/v1/mask \
  -H 'Content-Type: application/json' \
  -d '{"text": "Email Jane Placeholder, jane.placeholder@example.net, about the refund.", "language": "en"}')
echo "$R"
# {"masked_text":"Email [[PERSON_604260]], [[EMAIL_84bb25]], about the refund.",
#  "session_id":"s_a925963eb8819a782a68c56f41265424","entities":[...],"kept":[]}
SID=$(echo "$R" | python3 -c 'import json,sys; print(json.load(sys.stdin)["session_id"])')
P=$(echo "$R" | grep -o '\[\[PERSON_[0-9a-f]*\]\]' | head -1)
curl -sS http://127.0.0.1:8787/v1/unmask \
  -H 'Content-Type: application/json' \
  -d "{\"text\": \"Done: I emailed $P about the refund.\", \"session_id\": \"$SID\"}"
# {"text":"Done: I emailed Jane Placeholder about the refund.","unresolved":[]}
```

The session id and the placeholders differ on every run: each new session gets its own.

### Python client

Put it in front of an existing client; only the base URL changes:

```python
# pip install openai
from openai import OpenAI

client = OpenAI(base_url="http://127.0.0.1:8787/v1", api_key="unused")  # mock upstream
stream = client.chat.completions.create(
    model="mock",
    stream=True,
    messages=[
        {"role": "user", "content": "Refund Jane Placeholder, jane.placeholder@example.net."}
    ],
)
for chunk in stream:  # the mock echoes the masked text it got; the gateway restores the values
    print(chunk.choices[0].delta.content or "", end="")
# Echo: Refund Jane Placeholder, jane.placeholder@example.net.
```

What the model actually received: `curl -sS 'http://127.0.0.1:8787/v1/traffic?limit=1'` shows
the last gateway request as sent upstream, with placeholders (`pii-guard up` keeps a masked-only
[traffic log](docs/operations.md#traffic-log)). [API guide](docs/api.md) ·
[agents and harnesses](docs/integrations.md).

## Console tour

<p align="center">
  <img src="docs/assets/demo/demo.webp" alt="Console tour in the dark theme, recorded in fast mode (rules + NER) with fictional English data: Overview, a support email masked in the Playground with names replaced by placeholders, the same session in Chat, Logs and Connect" width="100%">
</p>

<p align="center"><sub>Console tour, recorded in fast mode (rules + NER) with fictional data.
<a href="ui/README.md#screens">More screens</a> · <a href="https://youtu.be/Lsr74ddhxRo">Video walkthrough</a>.</sub></p>

Run the console locally: [`make demo` from a checkout](docs/operations.md#the-demo-stack).

## Contributing, security, license

The [contributing guide](CONTRIBUTING.md) has the rule that matters most: **never commit real
personal data**, not even in a test.

**Security:** found a way to make personal data leak past PII Guard? Report it privately:
[SECURITY.md](SECURITY.md).

Licensed [Apache-2.0](LICENSE).
