> ## Documentation index
> Fetch the complete documentation index at: https://rolter-ai-rolter.docs7.io/llms.txt
> Ask it with GET https://rolter-ai-rolter.docs7.io/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# Guardrails

> Manage in-gateway inspection rules and external guardrail services without restarting gateways.

Rolter can inspect request or response content with built-in detectors, custom regular expressions, and a self-hosted HTTP guardrail service. Open **Guardrails → Guardrail Rules** or **Guardrails → Providers** in the dashboard. These screens require superadmin access.

## Inspection rules

Rules run by ascending position, after any rules defined in the `[[guardrails.rules]]` tables of the [config file](/configuration/config-file). The dashboard lists them in that order, and a new rule starts 10 after the last one. Each rule uses exactly one source:

- **Built-in detector** — email address, phone number, API token, or payment card.
- **Custom regex** — compiled by Rolter's bounded, linear-time regex engine before the rule is published.

Choose `annotate` to count a match without changing traffic, `block` to reject matching traffic, or `redact` to replace matches. System messages are excluded unless **Inspect system messages** is enabled. A paused rule stays stored and ordered but does not enter gateway snapshots.

### Try a rule before you publish it

The rule dialog has a **Try it on a sample** panel. Paste some text, optionally mark it as a system message, and select **Run test**. The control plane runs the draft exactly as filled in, with the same matcher and the same pattern limits the gateway uses, and answers with what would happen: no match, annotated, redacted (with the redacted text), would block, or not scanned (a system message the rule does not inspect, or a sample longer than the gateway scans per request). Each match is marked in the sample. A pattern the gateway would refuse comes back with the gateway's message, so you learn that before publishing.

The test does not save or publish anything, and the sample is not logged or stored. It runs one rule on its own. To see how it combines with the rest, select **Run in the whole policy** in the same panel. A sample is limited to 64 KiB. Testing needs the same access as editing a rule.

### Try the whole policy

Rules do not act alone. Config-file rules run first, then the enabled dashboard rules by position. A block ends the scan, and a redact rule hides its matches from the rules after it, so an earlier redaction can stop a later block rule from firing. **Try the policy** at the top of the Guardrail Rules screen runs a sample through all of it with the gateway's own scan and lists every rule in the order it runs: where it comes from (config file, dashboard or draft), what it did, and why it did not act when it was skipped (other stage, system message, an earlier block, guardrails off). Below the list is the verdict and the text that would be forwarded after every redaction. Pick **Request** or **Response** to try the pre-call or the post-call rules.

**Run in the whole policy** in the rule dialog does the same with the rule you are editing slotted in at its position. If a saved rule has the same name, the draft takes its place. Route overrides are not applied; the answer is for the policy, not for one route. Like the single-rule test, it saves nothing, logs nothing and needs the access that editing a rule needs.

The detector and action pickers describe what each choice does, and the **Source** and **Stage** fields carry an **(i)** note beside the label that says what they choose between. `payment_card` matches any run of 13 to 19 digits and does not check the card number, so a 13-digit timestamp matches too; `phone` matches 8 to 20 digits.

A response rule that blocks runs after the provider has generated and billed the answer. The caller gets a `403 guardrail_blocked`, but the request is still billed and counts toward budgets. Its log row is marked `withheld` and names the rule, and the blocked text is never stored. See [Withheld responses](/observability/logs-and-cost#withheld-responses).

A response rule (stage **Before response**) needs the whole answer, so it cannot act on a streamed one. The deployment-wide `streaming_post_call` setting in the `[guardrails]` block of `rolter.toml` ([reference](/configuration/config-file)) decides what happens instead:

- `reject` (the default) — while the rule is enforced, a request with `"stream": true` on a route the rule covers is refused with `400 guardrail_streaming_unsupported`. Chat clients and coding agents usually stream, so one response rule can refuse most interactive traffic.
- `passthrough` — the stream is served and the rule does not see it. Only non-streamed responses are checked.

The rule dialog shows which of the two applies as soon as **Before response** is picked, and each response rule's card repeats it in one line. Both read the value from the effective configuration (`GET /api/v1/config`). When that cannot be read, they say so and describe both outcomes rather than assuming the default. The dashboard shows the setting but does not change it.

### Audio transcriptions and translations

`/v1/audio/transcriptions` and `/v1/audio/translations` send an audio file upstream and answer with its text. Nothing on the request side can read audio, so only the response side applies, and it applies to the transcript as it would to a chat completion:

| Subsystem                                                    | On a transcription or translation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Response rules (**Before response**)                         | Run on the transcript: the whole `text`, plus each `segments[].text` of a `verbose_json` answer. A `text`, `srt` or `vtt` answer is scanned as one text and delivered in its own format. `redact` masks, `block` withholds with `403 guardrail_blocked`. `words[]` and `logprobs[]` split the text one token at a time, so a card or phone number spoken as several words matches no rule in them: when any rule redacted the transcript, both are removed from the answer rather than delivered with the value still in pieces. |
| Request rules (**Before request**)                           | Do not run: the request is audio.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| [Semantic guard policies](/security/semantic-guard-policies) | The policy's `post_call` providers judge the transcript; its `pre_call` providers do not run.                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `post_response` [plugins](/configuration/plugins)            | Run on the transcript, shown as the JSON answer or, for a plain-text one, as `{"text": "…"}`. `pre_route` and `pre_upstream` plugins do not run.                                                                                                                                                                                                                                                                                                                                                                                 |
| [PII sanitizer](/security/pii-sanitizer)                     | The response leg sanitizes PII the transcript contains when `direction` covers responses. The request leg does not run, so there is nothing to restore.                                                                                                                                                                                                                                                                                                                                                                          |

A transcription asked for with `stream=true` (on a model that streams, such as `gpt-4o-transcribe`) is not streamed while a response-side check applies: the gateway holds the whole answer and judges its event text before delivering it, and `streaming_post_call` is not consulted. A transcript that is withheld is billed by its audio minutes like any other [withheld response](/observability/logs-and-cost#withheld-responses). A successful answer that is not even text is withheld as `unreadable_transcript` whenever one of the response-side checks applies, rather than delivered unchecked.

The deployment-wide `guardrails` feature flag remains the kill switch. Turning it off pauses every built-in and custom rule, and every config-file rule, without deleting the registry. While it is off, **Guardrails → Guardrail Rules** shows a banner saying no rule inspects traffic, with a link to **Feature Flags**, and every rule's card says **not enforced** instead of **enforced**.

### Redaction tokens

A `redact` rule replaces each match with its **Replacement token**. Leave the field empty and the gateway writes the default token for the rule's source:

| Source                    | Default token          |
| ------------------------- | ---------------------- |
| Email address detector    | `[REDACTED:EMAIL]`     |
| Phone number detector     | `[REDACTED:PHONE]`     |
| API token detector        | `[REDACTED:API_TOKEN]` |
| Payment card detector     | `[REDACTED:CARD]`      |
| Custom regular expression | `[REDACTED]`           |

A new rule starts with the field empty, and the empty field shows the default for the selected detector. Changing the detector or the source updates a token you have not edited; a token you typed is kept. `annotate` and `block` rules never rewrite traffic, so the dialog saves them with no replacement and their cards say the content is not rewritten. A redact rule's card names the token it writes, the default included.

Over the API, omit `replacement` or send `null` (or an empty string) to get the default token.

### What the Rules screen reports

A card's status comes from the policy the control plane hands to gateways, read from the effective configuration (`GET /api/v1/config`), not only from the rule's own switch:

- **enforced** — the rule is in the effective policy and the `guardrails` flag is on.
- **not enforced** — the rule is in the policy, but the flag is off.
- **paused** — the rule is switched off on its card.
- **overridden by config file** — the config file defines a rule with the same name. The file's rule wins a name clash, so this card's rule does not run. The card names the file rule, which is listed below under **Config-file rules**. A paused rule under a file rule's name says it would not run if resumed, and the rule dialog warns when you type a name the config file already uses.
- **status unknown** — the effective configuration could not be read. A banner says so and offers a retry; the screen does not assume the rules are enforced.

Rules defined in the config file are listed under **Config-file rules**, read-only, in the order they run. They run before the rules added on the screen and change only when the file does.

The effective configuration does not say where each rule came from, so the screen infers it from names. A rule the configuration carries is a config-file rule when no card has its name, when the card with its name is paused, or when it differs from that card in any field. A config-file rule identical in every field to an enabled card cannot be told apart from it and is shown as that card. Enforcement is the same either way, but deleting or pausing the card would leave the file's copy running. [#2249](https://github.com/rolter-ai/rolter/issues/2249) tracks reporting the source directly.

### What each rule did

Every request a guardrail acts on leaves its outcome on the request's row in **LLM Logs**: the rule's name and what it did, never the text it matched. A rule's card on **Guardrails → Rules** counts them over the last seven days.

| Outcome     | When                                                                                                                             |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `blocked`   | a `block` rule refused the request (a pre-call rule, `400 guardrail_blocked`) or withheld the response (a post-call rule, `403`) |
| `redacted`  | a `redact` rule rewrote the request before it went upstream                                                                      |
| `annotated` | an `annotate` rule matched; the request was forwarded unchanged, and this is the only trace it leaves                            |

A request counts once per rule however many times the rule matched in it. Each non-zero count on a card links to the **LLM Logs** rows behind it (the log's **Guardrail** filter, `guardrail=blocked:<rule name>` in the address). A rule that has not matched says **No matches in the last 7 days**. The active provider's card on **Guardrails → Providers** counts the requests the provider blocked and the ones it replaced the content of (`provider_blocked:<provider name>`, `provider_transformed:<provider name>`); a provider that is not the one the gateway calls shows nothing. Each entry carries the name the provider had when it acted, so after you switch providers the old one's history stays out of the new one's card (renaming a provider starts its count over). Requests recorded before the name was added, and those of a config-file `[guardrail_webhook]` that sets no `name`, have the bare form; the enforced provider's card lists them apart under **Recorded without a provider name**. A pre-call block keeps the `redacted:<rule>` entries of the rules that ran before it, so the redaction counts include requests that were refused afterwards.

The counts are read from ClickHouse. Without `CLICKHOUSE_URL` the screens say so once, and with ClickHouse but without `clickhouse/018_guardrail_events.sql` applied they say the counts are not being kept (see [Upgrading](/deployment/upgrading)); neither is shown as "no matches". Output rules are counted on every response they act on: a response a rule redacted or annotated has the `redacted:<rule>` or `annotated:<rule>` entry on its row, and one it withheld has `blocked:<rule>`. A guardrail provider that gave no verdict leaves `provider_error:<provider name>`.

The same rollup is `GET /api/v1/analytics/guardrails` (see [Dashboard analytics](/api/dashboard-analytics#guardrail-outcomes)).

### Turning a rule off for one route

A route can switch a rule off for its own traffic. Name the rule in the route's `advanced.guardrails.disable` list. Every other route keeps running it, and a rule added later reaches every route that does not name it. A rule the route lists under `advanced.guardrails.enable` runs there even if `disable` names it too.

A name that no rule has is a typo, and a configuration file that contains one is refused at load. The control plane is more forgiving about dashboard rules: if you pause, rename or delete a rule that a route still names, the route's override is left out of the configuration sent to the gateways and reported on `GET /api/v1/config/problems`, rather than blocking every other change. Update or remove the override to clear the notice.

```toml
[[routes]]
model = "internal-support-bot"

[routes.advanced.guardrails]
disable = ["Redact customer email"]
```

Rules are matched by their exact name. For a config-file route, set the lists in `rolter.toml` as above. For a route created in the dashboard, open its model sheet and use the Guardrails section: one list of rules to switch off on the route and one of rules to force on, picked from the rules in force. A stored name that matches no rule stays in the list, marked, so you can remove it. The sheet shows a config-file route's lists read-only. Without the dashboard, send them in the `advanced` object of `PUT /api/v1/routes/{id}/advanced`. A name that no rule has fails config validation, so a typo in `disable` cannot read as "this rule is off here" while the rule keeps running. Take a rule out of every route's lists before you pause, rename or delete it: until [#2306](https://github.com/rolter-ai/rolter/issues/2306) is fixed, a route that still names it stops config changes from reaching gateways.

**Guardrails → Guardrail Rules** shows each exception on the rule's card, for dashboard and config-file rules alike. The card keeps its status, since the rule still runs on every other route, and adds a line such as **Off for traffic on 2 routes** with the route names under it. Past three routes the names fold behind that line; select it to list them. The routes are read from the same effective configuration as the rest of the screen. A paused card lists no routes, since its rule is not in the policy. Neither does a card the config file overrides: the override names a rule, and the rule under that name is the file's, so its card lists the routes. An override that names a rule the policy no longer has appears on no card.

## External providers

Register any number of vendor-neutral HTTP services and enable as many as you need: enabling one never pauses another. Which providers judge a request is decided by [semantic guard policies](/security/semantic-guard-policies), scoped to the deployment, an organization, a project, a route or a virtual key. A provider no policy lists runs on every request no policy covers.

Each field of the provider dialog that is not obvious from its label carries an **(i)** note: the stage, the failure policy, the timeout, the retries, the body cap and the authentication say what they do, and the defaults where there is one. Point at the **(i)** or focus it to read the note; Escape closes it without closing the dialog.

Choose the failure policy deliberately:

- **Fail closed** rejects traffic when the provider times out, is overloaded, errors or answers something that is not a decision. This favors enforcement.
- **Fail open** lets traffic continue when the provider cannot decide. This favors availability.

Each provider has its own; see [failure handling](/security/semantic-guard-policies#failure-handling) for every case.

The **Evaluation URL** starts empty and is required. The address shown in the empty field is an example, not a default, so a provider cannot be saved pointing at a host nobody typed.

The evaluation URL is checked against the [egress policy](/configuration/egress) when the provider is saved, so an address such as the cloud metadata service is refused with a `400` naming the field, and again before every call. Rolter does not follow a redirect from the service. The same applies to `[guardrail_webhook].url` in a config file.

Enabling a provider asks for confirmation. The dialog says it joins the providers already running, with nothing paused, and states what traffic it will then see: every request no policy covers when no policy lists it, or the scopes of the policies that do. It also states what its failure policy does to that traffic. Cancelling returns to the form with nothing saved. Saving a provider that is already enabled, with the switch left on, does not ask again.

Pausing a provider asks too. The dialog names it, says the other enabled providers keep running, and names the policies that lose it.

### Test a provider before you activate it

The provider dialog has a **Send a test envelope** panel. It works on the form as filled in, so you can test before the first save. The control plane sends the same JSON envelope the gateway sends before a request (`direction: "request"`, a user message holding your sample, or a short harmless sentence when you leave it empty) to the URL, with the provider's timeout (at most 10 seconds) and body cap. It makes one attempt, with no retries, so a flaky service shows up as a failure rather than being retried away.

The answer is always a result, never a server error: the decision (allow, block with its reason, transform or annotate) with the round-trip time, or the kind of failure: refused by the egress policy, credential variable not set, timeout, connection failed, TLS failed, a non-2xx status, an answer that is not a decision, or an answer over 1 MiB. An answer that is not a decision is reported as a failure, the same reading the gateway applies: there it resolves through the provider's failure policy. A body with scored `categories` and no `action` is a decision (an allow carrying scores). For a failure, the dialog also says what the provider's failure policy does to live traffic.

The call obeys the same [egress policy](/configuration/egress) as saving does, checked before connecting and again on the addresses the name resolves to, and does not follow redirects. The response body is never shown on an error status, because an endpoint can echo the credential it just rejected.

Credentials are read from the **control plane's** environment, while production gateways read them from their own. If the variable named in **Secret environment variable** is not set on the control plane, the test says so and does not call the service; this says nothing about the gateways. Either set it on the control plane for the test, or turn on **Send without the credential**. A variable that holds a control-plane secret (`ROLTER_KEK`, `ROLTER_DATABASE_URL` and any other `ROLTER_` name except `ROLTER_GUARDRAIL_TOKEN`) is refused with a `400`, so a test URL cannot be used to send one out. Testing needs the same access as editing a provider.

Rolter stores only an environment variable name for bearer or shared-secret authentication. Put the secret value in every gateway's environment; it is never returned by the control-plane API.

Both stages run: **Before upstream** (`pre_call`) judges the request and **Before response** (`post_call`) the response. A `post_call` provider on a streamed response is refused or buffered, never skipped; see [streamed responses](/security/semantic-guard-policies#streamed-responses).

The banner at the top of **Guardrails → Providers** reports the providers the control plane hands to gateways, read from `GET /api/v1/guardrails/effective`, not which registry rows are switched on. It names every running provider, from the registry or the config file, and which of them are unattached, and shows the status as unknown when the effective configuration cannot be read.

## Hot reload and config files

Creating, editing, enabling, or deleting a registry entry increments `config_version`, so gateway snapshots update without a restart. Rules defined in the config file remain immutable and win a name collision; dashboard-managed rules extend them. The Rules screen lists the config-file rules and marks a dashboard rule whose name the file takes as overridden. An enabled config-file webhook runs beside the registry's providers as one more provider; it no longer displaces them. A config-file provider or policy wins a registry entry of the same name. The control plane states where the webhook and each rule come from (see `GET /api/v1/guardrails/effective` below), so a config-file rule or webhook that is an exact copy of an enabled dashboard entry is still reported as the file's, and the dashboard entry as overridden.

## API endpoints

| Method          | Endpoint                            | Purpose                                         |
| --------------- | ----------------------------------- | ----------------------------------------------- |
| `GET`, `POST`   | `/api/v1/guardrails/rules`          | List or create rules                            |
| `PUT`, `DELETE` | `/api/v1/guardrails/rules/{id}`     | Update or delete a rule                         |
| `GET`           | `/api/v1/guardrails/effective`      | Rules and providers in force, with their source |
| `GET`, `POST`   | `/api/v1/guardrails/providers`      | List or register providers                      |
| `PUT`, `DELETE` | `/api/v1/guardrails/providers/{id}` | Update or delete a provider                     |
| `POST`          | `/api/v1/guardrails/rules/test`     | Run an unsaved rule on text                     |
| `POST`          | `/api/v1/guardrails/policy/test`    | Run the whole policy on text                    |
| `POST`          | `/api/v1/guardrails/providers/test` | Send an unsaved provider a sample envelope      |

Reading rules and providers is for a **superadmin** or a **security auditor** (a [deployment role](/security/rbac#deployment-roles)); creating, changing and deleting them is **superadmin only**. `GET /api/v1/guardrails/effective` takes the same read access and returns what the gateways run: `guardrails` (the feature flag), `streaming_post_call`, `rules`, and `webhook` (`null` when none is on). Each rule carries `source` (`file` or `registry`), `rule_id` (the dashboard row behind a registry rule) and `overrides` (the enabled dashboard row a config-file rule displaced). The webhook is a single-provider view kept for older clients: the config-file webhook when it is on, else the first enabled registry provider, with `source` (`file`, `registry` or `none`) and `provider_id`; `overrides` is always `null`, since nothing displaces a provider any more. The `semantic` object lists every provider and policy the gateways run; see [semantic guard policies](/security/semantic-guard-policies#api). Deleting a provider a policy lists is a `409`.

Every mutation is audit logged. The `test` endpoints write nothing and are not audit logged; the rule and policy ones need `guardrail_rule:update`, the provider one `guardrail_provider:update`.

`POST /api/v1/guardrails/rules/test` takes `sample` plus the rule fields (`builtin` or `pattern`, `stage`, `action`, `replacement`, `include_system`) and `system` (treat the sample as a system message). It returns `outcome` (`no_match`, `annotated`, `redacted`, `blocked` or `skipped`, with `skip` saying why), `match_count`, `spans` (UTF-16 offsets into the sample, at most 200) and `redacted`. An invalid rule or a sample over 64 KiB is a `400`.

`POST /api/v1/guardrails/policy/test` takes `sample`, `stage` (`pre_call` by default, or `post_call`), `system` and an optional `draft`: the rule fields above plus `name` (default `draft`) and `position`. It returns `outcome`, `blocked_by`, `redacted` (the text after every redaction, only when it changed) and `steps`, one per rule in run order, each with `rule`, `source` (`file`, `dashboard` or `draft`), `stage`, `action`, `outcome`, `match_count` (against the text as that rule saw it) and, for `skipped`, `skip` (`system_message`, `over_scan_budget`, `other_stage`, `after_block`, `policy_disabled` or `not_compiled`). A draft named like a config-file rule, an invalid draft or a sample over 64 KiB is a `400`.

`POST /api/v1/guardrails/providers/test` takes `url` plus `stage`, `timeout_ms`, `max_body_bytes`, `auth_kind`, `auth_env`, an optional `sample` and `skip_credential`. It returns `ok`, `decision` or `error` (`kind` and `message`), `credential` (`not_configured`, `sent`, `skipped` or `unset`), `http_status`, `latency_ms` and `truncated`. Only a malformed draft is a `400`; a refused address or a failing service is a `200` with `ok: false`. Provider bodies accept `auth_kind` as `none`, `bearer`, or `shared_secret`; `auth_env` is required for the latter two and contains only the environment variable name.
