Every prompt an agent sends is a transfer of whatever it contains to a model provider. For an agent that talks to customers, that means names, addresses, phone numbers and card numbers, in every turn, to a third party — and the more useful the agent is, the more of it there is. Protecting personal data inside an LLM flow is the first thing a security team asks about and, done the obvious way, the first thing that breaks the agent.
The obvious way is redaction. Replace every address with [redacted] before the model sees it and the provider learns nothing. But an agent that confirms an order by email has to put an address in a tool call: the mail tool receives [redacted] as the recipient, and the order confirmation goes nowhere. The address is necessary at exactly one point in the turn — inside the tool — and unnecessary at every other. A design that treats it the same everywhere fails in one direction or the other.
So the requirement has three parts: the model blind to the value, the tool able to use it, and the customer never shown anything but their own words. It is also the problem the confused-deputy case points at from the attack side: what an agent's tools receive is decided by a model, and data crosses that boundary in both directions.
Detection finds, the platform decides
Personal data is located by a detector, and the detector is a choice: the built-in patterns, or a Presidio analyzer — Microsoft's open-source PII engine — which the platform can run beside the agent runtime or reach at an address you own. Either way the contract is the same and deliberately narrow. We ask where the values are, and never accept rewritten text back. The analyzer answers with positions and entity types; what goes in their place is the platform's decision.
That split is a security property, not a tidiness one. Text returned by a remote service and inserted into a prompt would make the detector an injection surface — exactly the class of problem the checks on tool results exist to catch. Because only spans cross that boundary, a detector can be improved, replaced or pointed at your own recognizers without changing anything about what reaches the model.
The entities covered are email addresses, phone numbers, card numbers and national identifiers, plus person names when the Presidio detector is used and the policy asks for them.
A token is a keyed hash, not a mask
Where a value is found, the platform writes a token in its place:
| Where it appears | What is there | What the model reads |
|---|---|---|
| The customer's message | Send the confirmation to ana@example.com | Send the confirmation to [EMAIL_7f3a9c1b2d4e] |
| The same message, later | Ana's address again: ana@example.com | …again: [EMAIL_7f3a9c1b2d4e] |
| A callback number | +54 11 5555 0134 | [PHONE_2c81e0d95a47] |
| A card in a pasted invoice | 4111 1111 1111 1111 | [CREDIT_CARD_9b04f6a1c3d8] |
The suffix is 48 bits of an HMAC-SHA-256 taken over the entity type and the value, under a key derived from the server's secret, the tenant and the session. Three properties follow from that construction, and each does a job.
Stable inside a session. The same address is the same token on every turn, so the model can tell that two mentions are one person, and a token written in an earlier reply still matches the one in this turn's history. Reasoning about "the customer from before" needs identity, not content.
Different across sessions and tenants. The key changes with both, so a token copied out of one conversation restores nothing in another. This is one more place where tenant isolation is doing quiet work.
Not invertible. The value is not in the token, and no amount of collected tokens yields it. At 48 bits, the chance of two different values in one session sharing a token is about n² ÷ 2⁴⁹: for a thousand distinct values, roughly two in a billion.
One value, one turn
Follow the address from the first table row through a whole turn.
- The customer writes. Their message is stored exactly as typed — it is their record, and they see their own text in their own history.
- The address is swapped before the model. The boundary detects it and replaces it with
[EMAIL_7f3a9c1b2d4e], remembering the pair in memory for the length of the run. From here on the model reads the token and nothing else. - The model calls the mail tool. It is told to confirm by mail and writes the tool call with the token as the recipient. It has done its job without ever holding the address.
- The value is restored only if the tool was granted it. Before the call leaves, the platform looks up which kinds of data this tool may receive. The mail tool may receive email addresses, so the token becomes the real address, escaped correctly for the field it lands in, and the call goes out. A ticket tool in the same turn that was granted nothing receives
[EMAIL_7f3a9c1b2d4e], verbatim. Same model, same turn, same token, different tool, opposite outcome — and that difference is the whole control. - The reply is swapped again. The mail tool answers
{"recipient": "ana@example.com", "status": "sent"}. The address in the reply becomes the same token before the model reads it, so the protection holds on the way back as well. - The customer reads their own words. The model writes "the confirmation was sent to
[EMAIL_7f3a9c1b2d4e]", and the platform turns it back into the address the customer typed as the text goes by.
The mapping made in step 2 is never written down. Stored history keeps what the customer typed, and each time history enters a prompt it is tokenized again — which rebuilds the same mapping, in the same order, from the same rows. A persisted table of tokens and values would be a second copy of personal data with its own retention question, kept to save work that already happens.
Restoring is a permission
A token becomes a value only where a rule says so, for a named tool and a named kind of entity. There is no setting that restores everywhere, and the write path refuses to save one.
The reason is the attack the token was meant to blunt. An injected instruction — inside a retrieved document, say — can persuade the agent to send whatever it holds to a tool the attacker chose. If every token restored on every call, that call would carry the real address and the tokenization would have protected the model while giving the value away at the door. With per-tool grants, the worst the instruction obtains is a token, which is an opaque string.
Two more behaviours follow from treating restoration as a decision rather than a side effect. A token the run never issued — invented by the model, mangled in copying, lifted from another session — is left exactly as it is; it is never guessed into somebody's address. And a grant names a kind of data as well as a tool: the mail tool may receive addresses and still gets a token for a national identifier it has no use for.
The customer still reads their own address
The last step has a rule of its own: only values the customer wrote themselves are turned back in the answer. A value that first arrived in a tool's reply is somebody else's data, and it stays a token even where the model repeats it. The platform tracks where each value came from, and a value the customer typed stays theirs even if a tool later returns it too.
The answer also keeps streaming. Text arrives in pieces and a token can straddle two of them, so the platform releases everything up to an open bracket immediately and holds back only that unfinished tail — at most 32 characters, and only while a bracket is open. An ordinary sentence waits for nothing; a token is released, restored, the moment it completes. Streaming stops only where an agent has also asked for its answers to be inspected before delivery, which is a separate decision with a separate cost.
What the operator sees afterwards
Each restoration is an event that names the kind of data and the tool, and never the value. An operator can answer "which tools received an address this week" without the record becoming a second copy of the addresses. The audit of tool calls keeps the shape of every request and marks the personal fields as sanitised, and it now sits behind the restoration, so it records what the tool actually received.
Where it stops
This is what holds today, stated with its edges.
- Coverage follows the detector. The analyzer that ships beside the runtime loads English and Spanish models and recognizers, including Spain's NIF and NIE. Identifiers specific to another country are added as recognizers of your own.
- Names depend on the detector. A person's name is tokenized only through Presidio and only when the policy lists it; the built-in patterns have no name detector, so there the model can still read a name and infer context around a token.
- An approval pause can lose a value. The run's memory survives a wait for approval on the same server. If the resume lands elsewhere, values rebuilt from stored history are recovered, but one that appeared only in a tool's reply before the pause is not — and the next tool then receives a token, which is the safe direction to fail, and is recorded.
- The customer's own message is stored as typed. That is deliberate, and the control for it is retention and access rather than redaction.
- Outside a run there is no mapping. A check evaluated where no run exists degrades to plain redaction rather than issuing tokens nobody could restore.
Invariants worth testing on any system
Whether it is ours or anyone's, a design that claims "the model never sees the data" should survive these six checks.
| Invariant | How to test it |
|---|---|
| The model provider never receives a detected value | Read the exact request sent to the provider for a turn that contains an address |
| Identity survives without content | Mention the same address twice and confirm the model sees one identifier |
| A tool receives the value only if granted it | Call two tools in one turn and compare what each received |
| An unknown or foreign identifier is never resolved | Paste a token from another session into a message and confirm nothing is restored |
| The reply is protected as well as the request | Have a tool return the value and read what the model was shown next |
| The customer is not shown internals | Read the streamed answer and check for identifiers, and that it still arrives in pieces |
A system that fails the third or the fourth has protected the model and left the door open; one that fails the sixth has traded the customer's experience for the control.