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.

1

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.

Two sides of a dashed boundary: the AI gets 'PERSON_1 owes $400' in code names; you get 'John Smith owes $400' — scoped to your documents, with every reveal logged.
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.

2

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.

A page passes through three nets: checksum-proven pattern rules, the AI reader on every page (the main net, fails closed), and a final smoke alarm that can warn but never block.
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.

3

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.

Three cards: the code name A1B2C3D4_PERSON_1 unique per document, the AES-256 vault holding the real value, and the hashed fingerprint list that answers name searches without decryption.
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.

4

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.

Four stations: scope to the answer's own documents (empty scope refuses), the logbook records every reveal, damaged codes are restored only when every candidate agrees, and a soft broom degrades anything still code-shaped. A note explains the brief placeholder flash while streaming is by design.
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.

5

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.

Three edge cards: the page reader receives originals (governed by a retention agreement), the redaction AI sees each page but its content never reaches outside logs, and seven financial document types read the raw PDF with their outputs re-scrubbed and vendor copies deleted. Note: redaction and retention agreements are not substitutes — both are used.
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.