Skip to content

The guardrail pipeline

The whole pipeline is reachable from a detector and a rule. This is a complete policy, and it refuses any request carrying a card number:

guardrails:
detectors:
- type: pii
entities: [CREDIT_CARD]
rules:
- name: block-cards
action: deny

The detector says what to look for. The rule says what to do about it, and with no when it fires on whatever was found. Every other concept on this page is a default that configuration took: which parts of the request the detector read, when it ran, whether policy listened to it, and how a span would have been rewritten. Each is named here so it can be changed on purpose.

Guardrails process each request in five stages. They select content, run detectors, produce annotations, schedule work, and apply policy. Each stage has a separate configuration and result.

Segments through detectors to annotations, then a schedule and a CEL policy; the policy stage is marked as the only one of them that decides

A segment is an addressable piece of content

Section titled “A segment is an addressable piece of content”

The engine works on segments, not whole requests. Each segment is a span of text at a path into the body, messages[3].content, tool_calls[0].function.arguments, result.content. Each one is tagged with a kind: system, user, assistant, tool_args, tool_result, output, header, query.

Each finding carries its path. A redaction edits that value without re-serializing the request. See Passthrough.

The kind narrows a rule. “Redact this in what the user sends but not in what the tool returns” is a kinds: list. The two read-only kinds stay read-only the same way. Naming header or query in a redact rule is refused when the configuration compiles. The compiler rejects header and query in redact rules. A dynamic select predicate cannot make those read-only values writable.

A response is inspected the same way, but only if you ask, apply_to: [output]. A streamed response is inspected against the commit horizon, so a stream is checked without being buffered.

At the A2A door the body is a JSON-RPC message and the paths are its own, params.message.parts.0.text going out and result.artifacts.0.parts.1.data.pnr coming back. The engine reads each part’s text, a file part’s URL and filename, and structured data leaf by leaf. Bytes carried inline in a part are left alone, because a base64 blob is not text. A method that only names a task yields no segments: GetTask, ListTasks, CancelTask and SubscribeToTask carry ids and no content.

Coming back it reads what the peer produced: a message, a task’s status message, its artifacts, its history, and each streamed event as a whole frame. A JSON-RPC error’s message and data are read as well, because a peer says things in a failure too. Sent parts are tagged user and everything read back is tagged output, so a rule written with kinds: reaches this door without naming it. See Front a peer agent.

Six types, and all of them produce only annotations:

type what it is annotates
pii 103 recognizers with their checksums and context words, plus your own patterns pii, credential
nlp a token-classification checkpoint from the Hub, in this process or on a GPU box pii
remote any presidio-analyzer you already run, over its own /analyze pii
classify a sequence classifier, an injection or content-safety checkpoint, scoring each segment injection
llm a chat model asked whether a policy written in prose applies injection
embed an embedding model and your own example sentences per topic topic

The category in that last column is the default. A detector can be told to file its findings under another one. It matters because a rule matches on it. category separates “a credit card was found” from “this looks like a jailbreak” when both arrive as the same struct.

Detectors do not deny requests. They produce annotations. CEL rules apply those annotations and define the gateway’s decisions in one place.

Every detector, whatever it is underneath, produces the same struct: a category, an entity type, a byte span, a score, optionally the reasoning that produced it, and the address of the segment it belongs to.

The shape extends Presidio’s RecognizerResult with two fields. A remote detector can return it without another translation step.

Scores combine findings from multiple detectors. A recognizer’s own confidence is lifted when its context words appear beside the match, and lifted again when a second detector found the same span. So a profile that drops a detector can quietly lower the score of a finding some other detector made, and a rule declares what it depends on.

They get confused because both sound like “how seriously do we take this”, and they are answers to different questions.

Schedule is when the detector runs, and it is a latency decision: sync makes the request wait, overlap runs it alongside the upstream call, async enforces from the next turn. Schedules is a page of its own, because the concessions are not the same shape.

Mode is whether it enforces at all. A detector in mode: shadow annotates and records what would have happened, and nothing is denied or rewritten on its findings. That is how a new detector earns its way in, on the evidence it recorded before it enforced anything.

A detector can be sync and shadow, or async and inline. Neither combination is a mistake, and the axes are separate for that reason.

A rule has a condition, an action, and, if the action is redact, an operator saying how the span is rewritten.

There are three actions, and allow is not one of them:

  • deny refuses the request with the rule’s message, and ends evaluation immediately.
  • redact rewrites the spans its select predicate picks.
  • annotate records and changes nothing. A rule written to produce evidence rather than enforcement uses it.

A request is allowed when no rule acts, and that is the same default the access rules run on. The engine evaluates every rule rather than stopping at the first match: a deny short-circuits, an annotate only takes effect if nothing stronger has, and redactions accumulate. One policy can rewrite several entity classes with different operators in a single pass.

Seven operators do the rewriting. Four are presidio-anonymizer’s, name and behaviour alike, replace, redact, mask, hash. Three are this gateway’s, because none of upstream’s can keep two distinct values distinct. Presidio’s replace turns every PERSON in a prompt into the same <PERSON>, so a request about two people arrives about one:

  • placeholder numbers the spans per entity type, <PERSON_1>, <PERSON_2>, consistently within one request and meaninglessly across them.
  • pseudonym substitutes a plausible stand-in derived from a key, so one value maps to one stand-in everywhere that key is in force.
  • fpe encrypts the span into another span of the same shape, and is the only reversible one.
The same three spans before and after: a card number replaced by another that still passes Luhn, a name replaced by PERSON, an address replaced by EMAIL_ADDRESS

fpe preserves the span’s shape. The provider receives a card number that still passes Luhn, so a model reasoning about the request still works, and the original can be restored on a hop that is authorized to see it. Restoration is not an operator a rule may choose. It is an authorization decision about a destination, so it is not spellable in a policy file.

A CEL condition fails when it reads a key that is not there, key.metadata.team on a key with no team, or when it exceeds its cost budget over a body the client sized.

The default is fail_closed, and the second case is why. A deny rule that a large enough argument list could switch off is not a deny rule, and the argument list is the client’s to make large.

Authorization covers which detectors a given team’s traffic runs and why that selection is gated. Measure a detector covers what a detector costs and how to measure whether it is any good. The threat model covers what is claimed about any of this under attack.