How privacy works
The design in one sentence: no AI that reasons about your files ever holds a real name — and every time a real value is shown to you, that reveal is scoped and written down. Five pictures.
The promise, precisely
Not “personal details never leave the database” — you see your real data every day, and that’s the point. The promise is narrower and stronger: on the AI’s side of the boundary there are only code names. It cannot leak what it never had — not even if a document tries to trick it.
Under the hood
The full statement (from the privacy document): PII never enters the context of a model reasoning about the corpus, and every plaintext reveal is scoped and audited. Chunks, embeddings, summaries, extracts, prompts and stored chat transcripts all hold placeholder text only. This is also the prompt-injection defence: an injected instruction cannot exfiltrate identities the model never received.
Three nets catch the details
Every page passes through three nets when it arrives. The middle one is the important one: an AI reader that checks every page for names, addresses and organisations — and if it can’t run, the document stops rather than slipping through unredacted.
Under the hood
Stage 1: eleven regex rules (nz-ird, bank accounts, BSB / ABN / NZBN,
credit-card, email, phone) — precision over recall: every
numeric rule carries a checksum, prefix gate or required label. Stage 2:
findResidualEntities (Haiku, temperature 0, two 75-second attempts, then
failed_redact) — the only detector for person / organisation / address,
which makes its prompt the highest-leverage knob in the system. Stage 3:
@redactpii/node smoke check, in-process, warn-only. Pages shard in tens,
in order, entities persisted before their page.
Code names, a vault, and a fingerprint list
Each detected value becomes a code name, unique to its document. The real value goes into an encrypted vault. And a list of hashed fingerprints makes names searchable without ever opening a lock.
Under the hood
Token grammar [<DOCCODE>_<TYPE>_<n>] over a closed vocabulary of ten
types — closure is what makes residual-token scrubbing safe. Vault:
AES-256-GCM under VAULT_ENCRYPTION_KEY, application-level (not a database
extension). Blind index: HMAC word hashes in
document_entities.value_word_hashes, so redactQuery() resolves
“Sarah” to tokens via an indexed lookup with zero decryption at query
time.
The way back out: three fences and a broom
Turning code names back into real names is the only sanctioned reveal, and it is fenced three ways — only your documents, always logged, never guessed — with a soft broom behind the fences so a stray code never reaches your screen raw.
Under the hood
hydrate() filters to context.documentIds and throws on an empty
list (“refusing to expose vault”); one audit_log row per batch
that actually revealed; mangled-token recovery accepts a match only when every
in-scope candidate decrypts to the same value. scrubResidualTokens()
degrades leftovers to [person] / [redacted]. Streaming sends the
redacted answer token by token and the hydrated text as a single trailing part
— the flash is not a leak.
The honest edges
Real details do travel in three named places — because some jobs are impossible without them. Each edge is walled in its own way, and none of them ever feeds an AI that reasons over your whole corpus.
Under the hood
The redaction call and the (off-by-default) resolution adjudicator are the two
calls that see real values; both are pinned payload-off in telemetry with
regression tests — the v7 AI SDK default that once shipped page text to
Langfuse was found by audit, closed the same day, and is now guarded by the
lib/ai/telemetry.ts chokepoint plus an ESLint ban on direct
"ai" imports (recordsRedactedPayload is the audit list).
The seven allowRawDocument schemas are the financially sensitive ones
(bank_statement, transaction_history…); their JSON passes
post-redact against the vault, and purgeLlamaCloudArtifacts() deletes
the upload and job on success and failure. Deleting a document cascades
through pages, chunks, extracts and vault; parse-job copies at the vendor are
the one thing deletion cannot reach.