Your PII redactor does not know what a name is
There is no pattern for a person's name that does not also match ordinary prose. So redact will not remove one. Priya Raghunathan reaches your model provider unchanged, and guard_flags comes back empty, because nothing matched.

I want to write this down plainly because "PII redaction" is a phrase that gets typed into a compliance questionnaire and ticked, and the gap between what the phrase suggests and what any pattern matcher can actually do is where a real obligation goes unmet.
What is actually detected
Pattern matching, not a model — fast, deterministic, free. It finds identifiers, not people. This is the whole list:
| Flag | Matches |
|---|---|
pii:EMAIL | email addresses |
pii:CARD | 13–16 digit payment card numbers, spaced or hyphenated |
pii:PHONE | international (+44 7700 900123), national trunk (07700 900123), US (555-123-4567) |
pii:SSN | US Social Security numbers |
pii:NINO | UK National Insurance numbers |
pii:IP | IPv4 addresses |
Anything on that list we do not match is a bug worth reporting. Anything not on it, we do not claim to find.
Not detected: names. Street addresses. Dates of birth. Free-text notes about a person — which, on a product where users describe situations involving other people, is most of the personal data in the system.
So redact reduces what reaches your provider. It does not guarantee that nothing identifying does.
Which means the control has to match the obligation
If your obligation is "no personal data leaves our infrastructure", the control that meets it is a self-hosted or region-pinned model, configured per project under Models. Redaction is a second layer on top of that, not the first one.
If your obligation is "card numbers and national identifiers must not reach a third-party model", redaction meets it, because those are exactly the things a pattern can find with certainty.
Those are different obligations and they have different answers, and the failure I have watched happen is a team meeting the first with a control that only delivers the second — then finding out during a customer's security review rather than during their own design.
Why it is off by default
This surprises people, so: on a "talk to your own data" product, the end-user is sharing their own information on purpose. That is the feature.
A user uploading their own medical records to ask questions about them does not want their date of birth redacted before the model reads it. A support agent pasting a customer's order confirmation needs the order confirmation. Redaction that fires by default breaks the product for the majority case in service of a minority obligation.
Three modes:
| Mode | Behaviour |
|---|---|
off | the model receives the text unchanged (default) |
detect | flagged in guard_flags for audit, the model still gets the raw text |
redact | replaced before the model or provider sees it |
detect is the mode most people actually want and the one nobody picks first. You get the audit trail — which turns carried card numbers, how often, from which subjects — without breaking anything. It is the right place to start if you are trying to find out whether you have a problem, rather than acting on one you already know about.
Choose redact only if you must keep identifiers away from your model provider, and accept the consequence: the model can no longer use them to answer. A user asking "did my payment on card ending 4419 go through" is asking a question that redact has just made unanswerable.
The audit side is the part worth setting up
Every turn is recorded with the guardrail flags that fired. So is every change to the corpus and every search of it — and the rows are shaped so that the two questions an audit exists to answer are answerable from the rows themselves:
| Question | The rows |
|---|---|
| When did this document arrive, and who put it there? | document_upload |
| Who removed it, and what was it? | document_delete — written before the row goes, so it still describes what went |
| Who changed who may see it? | document_update, with previous and new value of each changed field |
| Who has seen this document? | retrieve and chat, both carrying metadata.document_ids |
A document_update row is written only when a field actually changed. Setting a document to what it already was is not an event, and an audit log full of non-events is one nobody reads.
const rows = await ai.audit.list({ document: doc.id, limit: 100 });That finds both kinds — the rows that name the document and the rows that merely saw it.
Erasure
await ai.audit.forget("acme:fin:ana");
// { documents_deleted, sessions_deleted, vectors_purged, objects_deleted }One call removes a subject's documents, sessions and vectors. It returns what it reached, rather than a bare 204, because "we deleted your data" is a claim somebody may have to substantiate.
Worth knowing where erasure interacts with the rest: memories are scoped to subject, so they go with it. A self-visibility document is unlistable by anyone but its uploader — which means an operator's own count of the corpus is short by however many exist. with_total answers hidden alongside total for an admin, so you can know the number without being able to read them. A count is not a read: it names no filename, no owner and no text.
That last one is the pattern I would take away from this whole area. The useful question is rarely "may this person see this data". It is "what is the smallest true thing I can tell them", and it is usually a number.
