Skip to main content
Version: Latest (main)

PII Signal

Overview​

pii locates personal data in the request, with the exact characters of every span. It asks the pii preset, a span question over the 11 personal data labels, and reads its whole input. A rule matches when the model finds a span of a label it counts; the masking plugin replaces the same spans with placeholders.

Key Advantages​

  • Makes privacy-sensitive routing explicit: keep such requests on a private model, or refuse them.
  • Returns positions, so masking replaces exactly the personal data.
  • Ignores the labels a route accepts, such as email addresses.
  • Reads tool results and earlier turns when told to.

What Problem Does It Solve?​

Without a personal-data signal, sensitive traffic can reach the wrong model or plugin stack before anything notices. Ad hoc filters make the policy hard to audit. pii turns personal-data detection into a reusable routing input.

When to Use​

Use pii when:

  • requests may carry regulated or sensitive personal data;
  • some labels are acceptable but others must take a safer route;
  • a decision masks personal data before the provider sees it;
  • tool results may carry personal data back into the conversation.

Configuration​

recipes:
- name: default
signals:
pii:
- name: personal_data
predicate: {gte: 0.85}
ignore_labels: [EMAIL_ADDRESS]
input:
history: true
- name: tool_result_pii
input:
tool_results: true
request: false
decisions:
- name: keep-private
priority: 200
rules:
operator: OR
conditions:
- {type: pii, name: personal_data}
- {type: pii, name: tool_result_pii}
modelRefs:
- model: private-model
FieldMeaning
nameThe rule's name, which {type: pii, name: ...} conditions and the masking plugin's signal use
predicateThe least probability of a span; by default the model's calibrated threshold (0.18 on vela3-fast), else 0.5
ignore_labelsLabels the rule never counts
input.historyAlso read the earlier user turns
input.tool_resultsAlso read the request's tool results
input.requestfalse leaves the current user turn out, so the rule reads only the earlier user turns or tool results it names
on_unscannedWhat the rule reports when only some windows of a long input were read: best_effort (the default), match, no_match or unknown
min_coverageWith best_effort, the share of the windows, 0 to 1, that must have been read to decide on them; below it the rule is unknown. Off by default
on_unknownWhat the rule reports when its model gives no answer: match (the default) or no_match
deployment, timeout_msThe deployment that answers, and the answer's deadline

A rule's predicate is policy: calibrate the value for the model you use, or leave it out to use the model's calibrated threshold. vela3-fast's span adapter applies its own per-length thresholds before it returns a span, so its 0.18 accepts every span it returns.

What it reads​

The question reads the current user turn, the earlier user turns with input.history, and the textual tool results with input.tool_results; tool calls and non-text content are not read. It also reads what global.router.routing_input removes from the routing text, so personal data cannot hide in a fake memory block.

A coding agent's user turn carries its harness's own instructions: Claude Code 2.1.317, for example, puts a commit trailer with an email address into a reminder in the first user message. A rule meant for what the agent's tools bring back sets input.request: false with input.tool_results: true, as tool_result_pii above does, so it reads the tool results alone, and leaves out the current turn with what routing_input removed from it. Such a rule is not asked, and does not match, before the request has a tool result.

It reads its whole input. An input longer than one of the deployment's scan_windows is read in overlapping windows, the tail first, then the head, then the middle by halves, and every window's spans count, at their places in the input, so masking replaces each one. See Long inputs. When the deadline ends before every window is read and no window read found personal data, on_unscanned decides: best_effort (the default) decides on the windows read, once they are the rule's min_coverage share of them if it sets one (below it the rule is unknown), match counts the request as personal data (strict), no_match does not, and unknown leaves the rule to on_unknown. Personal data found in a window read counts whatever on_unscanned says. The masking plugin always needs every window: a text it could not read whole is not sent. A tool result the Router cannot read as text, such as an image or an unsupported block, counts as unread input too, which on_unscanned decides the same way, even when no tool result holds text.

A model may find a value once and miss its later copies. Every other exact, whole-word copy of a found value of two or more characters is reported too, as the same label, so masking replaces every copy.

When the model gives no answer​

A rule none of whose windows gets an answer (the model fails, is overloaded or misses its deadline) matches, with the match type unavailable: it fails closed, and a decision's rules.on_unknown still decides. To let such a request through instead, write on_unknown: no_match:

recipes:
- name: default
signals:
pii:
- name: personal_data
on_unknown: no_match
decisions:
- name: keep-private
priority: 200
rules:
operator: AND
conditions:
- {type: pii, name: personal_data}
modelRefs:
- model: private-model

The model that answers​

The decision model answers every PII rule, unless the pii binding or the rule's deployment names another; see Choose a deployment. The question needs spans: a rule that resolves to a deployment without span answers, such as a d3 model, fails to load with span_required, unless that deployment declares the pii preset in its capabilities. vela3-fast (the default), the Vela 3.0 d3 sizes and Vela 2.0 answer it with their own PII question, in the same call as the request's other questions about the same text.

global:
model_catalog:
deployments:
d3-remote:
provider: systemone
endpoint: https://systemone.example.com/v1/systemone
model: vllm-sr/d3-ultra
schema: vllm-sr/5.0
auth: {api_key_env: SYSTEMONE_API_KEY}
system:
decision_model: {deployment: d3-remote}
bindings:
pii: primary

The signal is a routing control, not a substitute for redaction, encryption, access control or data-loss prevention. See a complete example: config/fragments/signal/pii/strict.yaml.