Annotation reference
A detector never denies anything and never rewrites anything. It produces annotations, and policy sees nothing but those annotations. This page is the vocabulary a guardrails.rules[].when is written against, and the shape a finding keeps all the way into the audit trail.
The schema is versioned as pistra.guardrails/v1, because it is three contracts at once: what an in-process detector returns, what a remote analyzer answering Presidio’s /analyze is read as, and what a signed audit record carries. A detector written against any one of the three is written against all of them.
The record is Presidio’s RecognizerResult with two fields added. The namespace is there because Presidio’s entity types are flat and personal-data only, and the segment reference because Presidio analyses one bare string and has nowhere to put it. Presidio’s own parser reads unknown keys with dict.get, so the superset round-trips through it unharmed.
See also the policy reference for the fields a CEL rule reads off one of these, and the audit trail reference for how a finding is recorded once a rule has acted on it.
Namespaces
Section titled “Namespaces”category is the namespace an annotation sits in, and type is the namespace and entity type joined, as in pii/CREDIT_CARD. A rule should usually match on type. A rule that names the namespace alone covers every type in it, including ones added after the rule was written.
Shape is the difference a redact rule cares about. A located span names bytes that can be rewritten, and a whole-segment verdict covers the segment because the detector judged it rather than found something at those offsets. Verdicts still carry real offsets rather than a sentinel, so they stay valid Presidio results, and pistra.span: segment is how they say which they are.
category |
Shape | Produced by | |
|---|---|---|---|
pii |
a located span | pii, nlp, remote |
A personal-data span: Presidio’s entity vocabulary, with Presidio’s exact entity_type values |
credential |
a located span | pii (the credential rows, and any recognizer that declares it) |
A credential, key or token |
injection |
a whole-segment verdict | classify, llm |
A prompt-injection or jailbreak verdict |
topic |
a whole-segment verdict | embed |
A content-classification verdict |
lang |
a whole-segment verdict | nothing yet, reserved | A detected language |
Segment kinds
Section titled “Segment kinds”kind is what the segmenter asserted a piece of the request is. It is also what apply_to selects, so one list answers both questions.
Writable is a property of the kind rather than a rule policy has to remember. A header is not addressable by a path into any document that exists, so there is nothing for the redaction stage to write into, and a query cannot be rewritten because rewriting a query means rewriting the request’s path. A rule may still deny or annotate on either.
kind |
Writable | |
|---|---|---|
system |
yes | The system or developer prompt |
user |
yes | Client-authored text: a conversation turn on an LLM route, and every string in the body on a generic one |
assistant |
yes | Model-authored text replayed in the request |
tool_args |
yes | Arguments the model asked to pass to a tool, the values that will actually be executed, so they are governed separately from prose |
tool_result |
yes | Data a tool returned into the conversation |
output |
yes | Response text: everything a provider sent back, from prose to the arguments of a tool call the model asked for |
header |
no | One request header value, reached through the synthetic Envelope rather than through the body |
query |
no | One query-string parameter value, or the whole raw query when it does not parse |
An empty apply_to means every writable request kind: not output, and not header or query. Each of the three is opt-in for a cost of its own. See the configuration reference.
Offsets
Section titled “Offsets”start and end are half-open offsets into the segment’s text, and the unit is declared rather than assumed. There are three answers in practice because there are three things a runtime counts in, and the third is the one that is silent. UTF-16 and code points agree on every character in the Basic Multilingual Plane and disagree by one per character outside it. A detector that reports UTF-16 and is read as code points passes every test anyone writes and mis-slices on the first emoji. A mis-slice is not a near miss. The rewrite lands at the wrong byte, which can leave part of the value in place.
pistra.offsets |
Counted in | Where it comes from |
|---|---|---|
bytes |
bytes | Go, Rust, and so every in-process detector here |
chars |
code points | Python, and so Presidio’s own /analyze |
utf16 |
UTF-16 code units | JavaScript, Java, .NET, Swift’s UTF16View |
Conversion happens at Presidio-facing edges, never silently.
Recognition metadata
Section titled “Recognition metadata”recognition_metadata is free-form by contract with Presidio. The extensions live there for that reason. The first three keys are Presidio’s own and its consumers read them; the pistra.-prefixed ones are ours, and a Presidio consumer ignores them.
| Key | |
|---|---|
recognizer_name |
The human-readable detector name. |
recognizer_identifier |
The recognizer that produced the result. |
is_score_enhanced_by_context |
Set when the context enhancer raised the score. |
pistra.detector |
The pistra detector (not the inner recognizer). |
pistra.offsets |
The offset unit. It is always “bytes” from our detectors, while Presidio’s own results are “chars”. |
pistra.span |
How the span was derived: absent for a located entity, “segment” for a whole-segment verdict. |
pistra.narrowed_from |
The span an annotation had before a narrowing confirm detector shrank it, as “start,end”. Its presence is how an audit record shows that the bytes finally rewritten were not the bytes originally reported. |
pistra.schema |
SchemaVersion on annotations that cross a process or sandbox boundary. |
pistra.cached |
An annotation reused from the delta-scan cache rather than recomputed, so audit records stay honest about it. |
pistra.guard_categories |
The categories a fixed-format guard model named that the configured labels do not claim, comma separated, in the order the model gave them. It is the checkpoint’s own taxonomy showing through its fine-tune, as when a Nemotron Content Safety model names “Sexual Content” on a turn it was asked only about INJECTION. |
The built-in vocabulary
Section titled “The built-in vocabulary”The pattern tier produces these types with nothing configured. Every other namespace is opened by configuration (a classify detector’s labels, an embed detector’s topics, an llm judge’s verdict vocabulary), so there is no built-in list to print for those, and the types are whatever the operator named.
| Namespace | Types | |
|---|---|---|
credential |
19 | ANTHROPIC_API_KEY, AWS_ACCESS_KEY_ID, AWS_BEDROCK_API_KEY, … |
pii |
83 | ABA_ROUTING_NUMBER, AE_EMIRATES_ID, AE_PASSPORT, … |
Every one of them, with its recognizer, checksum, fpe alphabet and streaming span, is on the PII coverage page.
When the vocabulary is open
Section titled “When the vocabulary is open”A configuration naming an entity type the engine cannot produce is refused where its author is reading. A rule that can never fire is worse than one that is wrong, because nothing reports it. That check needs the engine to enumerate its own types, and not every detector can. A remote analyzer with no entities: filter cannot say what it will return. One such detector leaves the vocabulary open, and an open vocabulary refuses nothing, because a name the engine cannot see is still one a detector may produce.
GET /admin/v1/guardrails/entity-types answers with the live list and whether it is complete.