Skip to content

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.

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

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.

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 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 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.

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.