Control PII in AI traffic
Personal data can enter an AI system in a prompt, a tool result, or a model response. DVARA finds that data on the request path and applies the action you choose before the call reaches a provider. It can also inspect the response before your application receives it.
Start with LOG to learn what your traffic contains. Move to BLOCK or REDACT when you are confident that the detector matches the data you intend to control. Use TOKENIZE only when your application must recover the original value later.
Choose the distribution
| Capability | Open Source distribution | Enterprise platform |
|---|---|---|
| Regex and checksum detection | Yes | Yes |
LOG, BLOCK, and REDACT | Yes | Yes |
Reversible TOKENIZE | No | Yes, with a token store and key material |
| Response and streaming inspection | Yes | Yes |
| Custom workspace patterns | Yes, through file configuration | Yes, managed through workspace settings |
| Optional Presidio and embedded detectors | Extension point only | Available when the selected detector is packaged and configured |
| LLM traffic | Yes | Yes |
| MCP and A2A traffic | No | Yes |
The Open Source distribution contains the deterministic regex detector. Presidio and embedded-detector settings bind in the runtime, but a setting on its own does not install a detector. Presidio becomes active only when you give it an endpoint and run a compatible analyzer service.
The Phileas embedded scanner is different from 1.8.1: it ships inside the Enterprise Gateway image, so nothing has to be built or mounted to use it. It stays off until you turn it on, with either dvara.llm-gateway.pii.embedded.enabled (redaction and tokenization) or dvara.llm-gateway.guardrail.embedded.enabled (detection only). Turn on one, not both — the same value would otherwise be caught twice, and the Gateway warns about it at startup. Flightdeck does not carry the library.
If you build your own embedding application and want Phileas there, add the dependency yourself:
<dependency>
<groupId>ai.philterd</groupId>
<artifactId>phileas</artifactId>
<version>3.3.0</version>
</dependency>
Check the startup report before you rely on any of these layers. It names the ones that are actually running.
Choose the action
PII inspection is enabled by default. The default action is LOG, so a new installation records detections but still sends the original value to the provider.
| Action | Provider-bound request | Newly detected response PII |
|---|---|---|
LOG | Record the detection and send the value unchanged | Record the leak and return the value unchanged |
BLOCK | Refuse the request with HTTP 400 | Record the leak and refuse the response with HTTP 400 |
REDACT | Replace the value with [REDACTED_TYPE]; store nothing | Replace the value with [REDACTED_TYPE]; store nothing |
TOKENIZE | Replace the value with a recoverable token | Remove the value with [REDACTED_TYPE]; do not mint a response token |
REDACT is permanent removal. TOKENIZE is reversible pseudonymisation. They are separate actions in 1.8 and should not be described as interchangeable.
From 1.8.5 Claude Code can call DVARA through /v1/messages. Claude Code's own prompt carries an email address, so the email detector matches on every call. With LOG that is an audit entry and nothing else. REDACT or TOKENIZE edits Claude Code's own prompt before it reaches the model, and BLOCK refuses every call. For a workspace that runs Claude Code, keep the email action at LOG, or expect that edit. See Govern Claude Code.
Block PII in Open Source
Start with the Open Source distribution quickstart. Restart the gateway with the PII action set to BLOCK:
DVARA_LLM_GATEWAY_PII_DEFAULT_ACTION=BLOCK \
DVARA_AUDIT_FILE_PATH="$PWD/var/audit.jsonl" \
DVARA_AUDIT_HMAC_SECRET='<generated-hmac-secret>' \
java -jar dvara-gateway-server-1.8.5-app.jar
The example uses the built-in Mock provider. Send a synthetic SSN to the local gateway:
curl -s -w '\nHTTP %{http_code}\n' \
http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $DVARA_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"model": "mock/approved",
"messages": [
{"role": "user", "content": "The test SSN is 123-45-6789."}
]
}'
The Open Source quickstart mints DVARA_API_KEY and stores only its fingerprint
in gateway.yaml.
DVARA refuses the request before provider dispatch:
{
"error": {
"message": "Request blocked: PII detected (SSN)",
"type": "pii_violation",
"code": "pii_detected",
"trace_id": "01K4V5R9X21MDN8W5A6E7ZQ3TC"
}
}
HTTP 400
The trace ID changes on every request. Do not retry this response without changing the content or the workspace action.
Verify the block
Find the decision in the local audit file:
jq 'select(.eventType == "PII_DETECTED") |
{eventType, workspaceId, source: .payload.source,
count: .payload.entity_count, types: .payload.entity_types}' \
var/audit.jsonl
The matching record looks like this:
{
"eventType": "PII_DETECTED",
"workspaceId": null,
"source": "request",
"count": 1,
"types": "SSN"
}
The audit payload records types and counts, not the detected value. The full JSONL line also carries the sequence, previous hash, HMAC, and signing time.
Remove PII without blocking the call
Restart the Open Source distribution with:
DVARA_LLM_GATEWAY_PII_DEFAULT_ACTION=REDACT
With the same request, the provider receives:
The test SSN is [REDACTED_SSN].
The original value is discarded. It is not written to a token store and cannot be restored. The request continues, and the audit trail contains PII_REDACTED.
This action needs no database, encryption key, or Enterprise service.
PII in replayed assistant tool-call arguments is governed too. With REDACT, this request continues with the SSN replaced inside the JSON argument string while preserving the tool name and call ID:
curl -s http://localhost:8080/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "mock/approved",
"messages": [
{"role": "user", "content": "Store the approved test record."},
{
"role": "assistant",
"tool_calls": [{
"id": "call_approved_record",
"type": "function",
"function": {
"name": "store_record",
"arguments": "{\"customer_id\":\"CUST-2048\",\"ssn\":\"123-45-6789\"}"
}
}]
}
]
}'
The provider receives "ssn":"[REDACTED_SSN]", and the audit trail records PII_REDACTED with source: request. DVARA also scans text in multimodal message blocks and tool results; non-text image and audio payloads are not decoded by the regex detector.
Use reversible tokenization
Choose TOKENIZE only when the model must work with a stable placeholder and your application must recover the original value later. It requires the Enterprise token store and its key material.
On the request path, DVARA stores the original value under the resolved workspace and sends an opaque token to the provider. Automatic response detokenization is off by default. When enabled, it works only for non-streaming responses and restores only tokens created from that request. It never restores new PII produced by the model.
Manual detokenization and token-store purge in Flightdeck require approval from a different PII manager. The original requester executes the exact approved input or workspace before the 15-minute approval expires. This prevents one signed-in user from authorizing their own disclosure or irreversible erasure.
The Open Source distribution has no token store. If it is configured with TOKENIZE, it refuses a request containing PII with HTTP 403 and code: pii_tokenize_unavailable. It does not silently fall back to REDACT or send the raw value:
{
"error": {
"message": "pii.action=TOKENIZE needs a reversible token store, which is not part of this build. Use REDACT for irreversible removal, which needs no key material and always works.",
"type": "pii_violation",
"code": "pii_tokenize_unavailable",
"trace_id": "01K4V6A7F2B8Q4P0W1N9X3C5ZD"
}
}
In the Enterprise platform, an unavailable token store follows pii.degraded-action. The default is BLOCK. A blocked request returns HTTP 403 with the retained compatibility code pii_redact_unavailable and attempted_action: TOKENIZE. The audit event also retains the name PII_REDACT_DEGRADED; use its attempted_action and degraded_action fields when building alerts.
Control PII in model responses
Response scanning is on by default. A newly detected value is handled according to the same workspace action, with one important boundary: TOKENIZE never creates a recoverable token for new model output.
LOGrecordsPII_OUTPUT_LEAKand returns the response unchanged.BLOCKrecords the leak and refuses a non-streaming response with HTTP 400 andcode: pii_detected. The provider has already answered, but its content is not delivered to the caller.REDACTandTOKENIZErecord the leak and replace the new value with an irreversible placeholder.
This distinction prevents a response-side detector from giving the caller a recovery token for a value the model introduced.
For a non-streaming response blocked after provider dispatch, the caller receives:
{
"error": {
"message": "Response blocked: PII detected in the model response (SSN)",
"type": "pii_violation",
"code": "pii_detected",
"trace_id": "01K4V6KG4T1H9W8M7R2C0N5BQP"
}
}
The response status is HTTP 400. Search the audit trail for PII_OUTPUT_LEAK with the same workspace and time window to verify why delivery was refused.
Govern streamed responses
Streaming inspection is on by default. In 1.8, the behavior depends on whether the selected action can withhold content:
LOGrelays text as it arrives and records the final inspection summary.BLOCK,REDACT, andTOKENIZEhold the complete response, inspect it once, and then release safe content or refuse it.- A blocked stream ends with
finish_reason: content_filterand emitsPII_BLOCKED_STREAMING.
The old scan-window and overlap settings are retained for configuration compatibility but do not control this whole-response enforcement path. The held response is limited to 1,000,000 characters by default. If it exceeds the bound, DVARA refuses deferred delivery instead of releasing partially inspected text and records STREAM_TOO_LARGE_TO_SCAN.
Review built-in detection
The Open Source detector uses 16 built-in pattern definitions across 15 entity families, plus workspace-defined patterns. Phone numbers account for two definitions: US formats and international E.164.
| Group | Types |
|---|---|
| Contact and identity | Email, US and international phone, person name, date of birth |
| Government identifiers | US SSN, US passport, driver's licence, India Aadhaar, India PAN |
| Financial | Credit card, IBAN |
| Health | Medical record number, DEA number, NPI number |
| Network | IPv4 address |
| Custom | Workspace-defined regular expressions |
Credit cards and NPI numbers use Luhn validation, DEA numbers use their checksum, and Aadhaar uses Verhoeff validation. PAN is checked structurally. These checks reduce false matches from arbitrary digit strings.
Optional detectors can add broader names, locations, and unstructured entities. Their presence depends on the selected Enterprise artifact or on an embedding application supplying an implementation. A configured endpoint or filter list is not proof that the detector is running.
Add Presidio detection in Enterprise
Use Presidio when the deterministic patterns are not enough for names, locations, organizations, or other context-dependent entities. Run a compatible Presidio Analyzer service, then set its /analyze endpoint:
dvara:
llm-gateway:
pii:
presidio:
endpoint: http://presidio-analyzer:3000/analyze
language: en
score-threshold: 0.5
timeout-seconds: 5
The endpoint is the switch: when it is present in the Enterprise platform, Presidio runs alongside the regex detector. You do not need to change provider from its regex default. DVARA merges overlapping findings and keeps the higher-confidence result.
Presidio is an additional, fail-open detection layer. If the analyzer times out, cannot be reached, or returns an invalid response, the request still receives the regex detector's result. Monitor the gateway warning and do not treat the configured endpoint as proof that NER inspected a request.
| Setting | Default | Meaning |
|---|---|---|
presidio.endpoint | unset | Analyzer /analyze URL; an unset endpoint leaves Presidio off |
presidio.language | en | Language sent to the analyzer |
presidio.score-threshold | 0.5 | Lowest accepted confidence score |
presidio.timeout-seconds | 5 | HTTP timeout for one analysis call |
presidio.cache-max-size | 1000 | Most texts whose analyzer result each Gateway pod keeps in memory; 0 or less turns the cache off |
presidio.cache-ttl-seconds | 300 | Seconds a cached result is reused before the analyzer is asked again; a read does not extend it; 0 or less turns the cache off |
Presidio analyzer results are cached locally by input text, so the same text is
not sent to the analyzer again within the TTL. To send every text to the
analyzer, set either cache setting to 0.
From 1.8.4 a failed analyzer call is never cached. A timeout, an error status or an unreadable answer still fails open for that scan, and the next scan of the same text asks the analyzer again. Up to 1.8.3 the failure was cached as "no PII", so that text skipped Presidio for the whole TTL.
At startup the Gateway logs the cache state: Presidio result cache on: up to 1000 texts for 300s each, or Presidio result cache off with the two
settings.
Built-in US and international phone, card, SSN, and IPv4 patterns require a
standalone numeric value rather than digits embedded in a longer identifier.
A four-part version such as 10.2.30.4 is not treated as an IPv4 address.
Workspace custom patterns still match exactly what the operator writes.
Set installation defaults
Use these installation-wide settings:
dvara:
llm-gateway:
pii:
enabled: true
provider: regex
default-action: LOG
scan-responses: true
auto-detokenize-response: false
scan-streaming-responses: true
strip-before-cache: true
streaming-max-held-characters: 1000000
| Setting | Default | Meaning |
|---|---|---|
enabled | true | Run request inspection |
provider | regex | Use the built-in deterministic detector |
default-action | LOG | Record detections without changing traffic |
scan-responses | true | Inspect non-streaming responses |
auto-detokenize-response | false | Restore request-minted tokens on supported non-streaming responses when the action is TOKENIZE |
scan-streaming-responses | true | Inspect streamed responses |
strip-before-cache | true | Remove PII before building or storing cache content |
streaming-max-held-characters | 1000000 | Bound a response held for deferred enforcement |
Cache protection always uses irreversible placeholders. It does not preserve a recoverable token, even when the workspace action is TOKENIZE.
Override settings by workspace
In the Enterprise platform, configure a workspace from its Data Protection page. The stored workspace settings take precedence over installation defaults. Legacy metadata is used only for fields that do not have a stored value.
The main workspace fields are pii.enabled, pii.action, pii.scan-responses, pii.auto-detokenize-response, pii.degraded-action, and pii.custom-patterns.
Custom patterns must be a JSON object, not a JSON string:
{
"pii.custom-patterns": {
"employee_id": "EMP-\\d{6}",
"internal_project": "PROJ-[A-Z]{2,4}-\\d{4}"
}
}
Each pattern is reported as CUSTOM. Flightdeck rejects an invalid regular expression when you save it. Test valid patterns with representative safe and unsafe text before changing the action from LOG.
Upgrade safely to 1.8
In 1.7, REDACT created a recoverable token. In 1.8, REDACT removes the value permanently and TOKENIZE owns the old reversible behavior.
The Enterprise upgrade migrates stored workspace actions from REDACT to TOKENIZE, including the legacy workspace metadata value. This preserves the behavior those workspaces already selected.
The installation-wide dvara.llm-gateway.pii.default-action is not migrated. If it was REDACT and you need the old reversible behavior, change it to TOKENIZE and verify the token store and key material before serving traffic. Leave it as REDACT when permanent removal is the intended result.
After the upgrade, test one request for every action in use and confirm the expected audit event before completing rollout.
Monitor PII audit events
| Event | Meaning |
|---|---|
PII_DETECTED | Request PII was logged or blocked |
PII_REDACTED | Request PII was removed permanently |
PII_TOKENIZED | Request PII was replaced with a recoverable token |
PII_OUTPUT_LEAK | PII was detected in a model response |
PII_RESPONSE_DETOKENIZED | Request-created tokens were restored in a non-streaming response |
PII_REDACT_DEGRADED | Tokenization could not run; inspect attempted_action and degraded_action |
PII_BLOCKED_STREAMING | A streamed response was refused by the PII action |
For MCP traffic, the equivalent events start with MCP_PII_. A2A enforcement records its decisions on the A2A audit path. See Agentic governance for those planes.
Continue the governance path
- Add Policy as Code when model, token, or tool rules should run before PII enforcement.
- Add guardrails for prompt injection, content safety, and response checks.
- Use audit and observability to query evidence across the Enterprise platform.