Skip to content

privacy-gate-llm

License Python Dependencies Model on HF Demo Contributors

A very small model that answers one question about a piece of text:

Must this stay on this machine?

Try the demo · Model on Hugging Face · Using it in your own project

Not a medical device. This is a privacy tool. It decides whether text should leave a machine and says nothing about what a lesion is. MoleCare does not diagnose.

It exists to sit in front of llm-harness as a second gate, behind the regex ruleset in rules/v1.yaml, and to catch what patterns cannot see.

Why a model at all, when there are already rules

The rules are good at anything shaped like a marker: sk-ant-…, AKIA…, patient_id: 40219, a base64 image. They are blind to the same information written as ordinary English.

This sentence passes every privacy rule in the ruleset today:

"The woman from Tuesday's clinic, 34, has a 7mm asymmetric lesion on her left shoulder and a biopsy booked for the 20th."

Measured on the 206-example gold set in data/gold.jsonl:

catch rate friction rate
regex ruleset alone 9.0% (10 of 111) 1.1%

101 of the 111 sensitive examples go straight through. That is the gap this model is for.

What works

Not a fine-tuned chat model. An embedding model and a logistic head.

AUC catch friction
regex ruleset alone 9.0% 1.1%
hashed word unigrams 0.7818 98% 83.2%
Qwen3.5-0.8B, prompted 0.4609 at chance
bge-m3 + logistic head, v1 0.9927 99.1% 5.3%

Five-fold cross-validated: every score comes from a head that never saw that example. 110 of 111 sensitive examples caught, at 5.3% friction. On 20 sentences written after training (data/fresh.jsonl) it catches all 11 sensitive ones and holds 1 of 9 harmless ones. On 21 short prompts held out of training (data/short-probe.jsonl) it catches all 6 sensitive ones and holds none of the 15 harmless ones; head v0 held 8.

Prompting a small chat model asks it to reason its way to a word, and a 0.8B cannot. An embedding does one forward pass and a linear head decides. That is also the right shape for something that runs inline on every request: no generation step, and the output is a calibrated score, so catch against friction is a dial rather than whatever the model felt like saying.

The whole gate is bge-m3 plus 1024 weights and a biasmodel/head-v1.json. It costs 34–43 ms per call at p50 and under 50 ms at p95, measured on a loaded shared box, and 18 ms per text when batched.

from privacy_gate.gate import Gate

gate = Gate.load("model/head-v1.json")
gate.decide("the woman from Tuesday's clinic has a 7mm lesion on her shoulder")
# Decision(hold=True, score=6.72, threshold=0.1209)

It is not ready to ship. 247 examples written by one person are enough to choose an architecture, not enough to set a threshold that decides what leaves a machine holding real patient data. See the end of docs/JOURNAL.md for what would have to be true first.

How it composes

The model may only ever add a hold, never clear one.

text ──▶ regex ruleset ──▶ hold?  ──yes──▶ local, locked
              │
              no
              ▼
         this model ──▶ hold?  ──yes──▶ local, locked
              │
              no
              ▼
      the caller's own routing stands

Two consequences, and they are the point:

  • A false negative from the model leaves today's protection exactly as strong as it already is. The model cannot make things worse.
  • The model is never the reason something is sent, only ever a reason something is held back.

So it is not judged on accuracy in isolation. It is judged on residual catch (what it finds that the rules miss) against friction (how often it interrupts work that was fine). A guard that fires on ordinary engineering gets switched off, and then it protects nothing.

Where things are

Path What
docs/TAXONOMY.md The decision boundary. The spec every label obeys, and the owner decisions still open.
docs/JOURNAL.md Every run, with its numbers, in order. Including the ones that did not work.
data/gold.jsonl 206 hand-written examples: 95 clean, 111 sensitive. The test set. Never trained on.
data/rules-baseline.json What the regex ruleset catches, per example. Generated by scripts/rules_oracle.py.
docs/INTEGRATION.md How to use it from llm-harness, molecare-mcp, Spring Boot, CI, or over HTTP.
data/fresh.jsonl 20 examples written after training, as a generalisation check.
data/short-probe.jsonl 21 short prompts held out of training: the ones that showed v0 holding "ok" and "thanks!".
model/head-v1.json The gate: 1024 weights and a bias. 68 KB.
model/head-v0.json The previous head, kept so runs 6–9 stay reproducible.
src/privacy_gate/ Loading, prompting, scoring, serving. Standard library only.
scripts/serve.py A loopback HTTP sidecar, so any language can call it.

Running it

Everything here is standard library only. The one external dependency is an Ollama endpoint serving bge-m3, which can be your own machine:

ollama pull bge-m3

# Embed the gold set, then cross-validate the head over it.
PYTHONPATH=src python3 -m privacy_gate.embed \
    --data data/gold.jsonl --out runs/gold-bge-m3.jsonl
PYTHONPATH=src python3 -m privacy_gate.head \
    --embeddings runs/gold-bge-m3.jsonl --rules data/rules-baseline.json

Point it elsewhere with --url http://host:11434 if the models live on another box. If that box is shared, scripts/run-when-free.sh waits for it to go idle rather than queueing behind whatever else is running.

The no-model baseline needs nothing at all, and is worth running first so you know what a neural result has to beat:

PYTHONPATH=src python3 scripts/lexical_baseline.py

Regenerating data/rules-baseline.json needs the llm-harness CLI, which is a separate project; the generated file is committed so you do not need it. llm-harness explain decides locally and sends nothing anywhere, which is why it is safe to run over a file of fixtures.

Data

Every example in data/gold.jsonl is invented. No real person, no real credential, no real patient. Phone numbers come from Ofcom's drama range (07700 900xxx), the AWS key is Amazon's own documentation placeholder, and the private key fixture is base64 for a sentence saying it is not a key.

That is a deliberate property, not a convenience: it is what allows this repository to be public at all. SECURITY.md explains why each fixture is safe, and CONTRIBUTING.md explains what to do if you add one.

Status

Nothing consumes this in production yet. docs/JOURNAL.md records what has actually been measured, including the two architectures that did not work, and ends with the five things that would have to be true before it should guard anything real. docs/TAXONOMY.md lists the decisions still open.

Contributions that would help most are in CONTRIBUTING.md; the short version is more hard negatives — text that looks sensitive and is not.

Contributors

Thank you to everyone who has helped privacy-gate-llm. The most useful contribution is a hard negative: text that looks sensitive and is not.

Yauhen Bichel

The list is filled by Contributors from GitHub commits, bots omitted — never hand-maintained, because a stale list is worse than none. Contributor graph · good first issue

Licence

Apache-2.0, see LICENSE and NOTICE. The base encoder, BAAI/bge-m3, is MIT and is neither included nor redistributed here — only the head is ours.

About

A very small model that catches health data, credentials and personal data written as ordinary English, which pattern rules cannot see. bge-m3 plus 1024 weights. Not a medical device.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages