> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vikat.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Guardrails

> Configure the content policy declaratively in config.json, through the plugins array.

<Warning>
  There is **no** `guardrails_config` top-level block. Earlier revisions of this
  page described one, along with per-provider blocks for AWS Bedrock Guardrails,
  Azure Content Safety, Google Model Armor and others. None of that is read by the
  gateway.

  A `config.json` containing `guardrails_config` parses, the gateway boots and the
  block is discarded — so a deployment that declares its entire content policy that
  way runs with **no** guardrails. As of this release the gateway logs every
  top-level section it does not read at startup, so the situation is at least
  visible. Configure guardrails through the `plugins` array as shown below.
</Warning>

## Where guardrails are configured

Guardrails are a plugin. Everything about the policy lives in that plugin's
`config` object.

```json theme={null}
{
  "$schema": "https://schema.vikat.ai",
  "plugins": [
    {
      "name": "guardrails",
      "enabled": true,
      "config": {
        "blocked_keywords": ["project-atlas", "acquisition-target"],
        "block_message": "This request was blocked by the content policy.",
        "apply_to": "input",
        "providers": []
      }
    }
  ]
}
```

## Fields

### Top level

| Field              | Type       | Default   | Meaning                                                          |
| ------------------ | ---------- | --------- | ---------------------------------------------------------------- |
| `blocked_keywords` | `string[]` | `[]`      | Case-insensitive substrings that block a match.                  |
| `block_message`    | `string`   | built-in  | Returned when the blocklist matches.                             |
| `apply_to`         | `string`   | `"input"` | `input`, `output` or `both` — which side the blocklist inspects. |
| `providers`        | `object[]` | `[]`      | Named rules, evaluated in order.                                 |

### A provider

| Field                 | Type       | Applies to                | Meaning                                                                                                                                                          |
| --------------------- | ---------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | `string`   | all                       | Stable identifier.                                                                                                                                               |
| `name`                | `string`   | all                       | Label, also used in the default block message.                                                                                                                   |
| `type`                | `string`   | all                       | `keyword`, `regex`, `pii`, `injection` or `llm`.                                                                                                                 |
| `enabled`             | `bool`     | all                       | A disabled provider is not compiled or evaluated.                                                                                                                |
| `apply_to`            | `string`   | all                       | `input` (default), `output` or `both`.                                                                                                                           |
| `message`             | `string`   | all                       | Overrides the block message for this rule.                                                                                                                       |
| `keywords`            | `string[]` | `keyword`                 | Case-insensitive substrings.                                                                                                                                     |
| `patterns`            | `string[]` | `regex`                   | RE2 expressions.                                                                                                                                                 |
| `categories`          | `string[]` | `pii`                     | `email`, `ssn`, `credit_card`, `phone`, `ip`.                                                                                                                    |
| `endpoint`            | `string`   | `llm`                     | Chat-completions URL for the classifier.                                                                                                                         |
| `model`               | `string`   | `llm`                     | Classifier model.                                                                                                                                                |
| `api_key`             | `string`   | `llm`                     | Optional bearer token. Masked in API responses.                                                                                                                  |
| `virtual_keys`        | `string[]` | all                       | Apply only to these keys, by id or name. Empty means all.                                                                                                        |
| `teams`               | `string[]` | all                       | Apply only to these teams.                                                                                                                                       |
| `models`              | `string[]` | all                       | Apply only to these models. Accepts `provider/*`.                                                                                                                |
| `action`              | `string`   | `keyword`, `regex`, `pii` | `block` (default) or `redact`. Redaction masks the matched span and lets the exchange continue. Rejected on `llm` and `injection`, which locate no span.         |
| `injection_threshold` | `int`      | `injection`               | Score at which the rule blocks. 0 uses the default (100). Raise it on traffic that legitimately discusses prompts and roles; lower it for a high-assurance path. |
| `fail_open`           | `bool`     | `llm`                     | `false` by default — an unreachable classifier **blocks**. Set `true` to prefer availability over enforcement.                                                   |

<Note>
  `api_key` is a plain string. Unlike provider credentials it cannot yet be given
  as `"env.VAR_NAME"` or read from a secret manager.
</Note>

## Scoping a rule

A rule with no scope applies to everything. Naming virtual keys, teams or models
narrows it:

```json theme={null}
{
  "id": "legal-only",
  "type": "pii",
  "enabled": true,
  "teams": ["team-legal"],
  "models": ["openai/*"],
  "action": "redact",
  "categories": ["ssn"]
}
```

Dimensions are **ANDed**, values within one are **ORed** — the example applies to
the legal team, and only on OpenAI models. Virtual keys match on id or name,
because operators think in names and automation carries ids.

This exists because one policy for the whole gateway ends up set to whatever the
most permissive consumer can tolerate, leaving the strict consumer unprotected.

<Warning>
  A scoped rule applies only when the scope is **known**. A request carrying no
  virtual key escapes every scoped rule and is covered only by the unscoped ones.

  That is safe when `client.enforce_auth_on_inference` is set, because there is
  then no such thing as a request without a virtual key. With it unset, an
  anonymous caller was already reaching the model with no governance at all.

  The alternative — applying scoped rules when identity is unknown — sounds safer
  and is worse: one team's rule would start blocking every other team's traffic the
  moment identity resolution failed.
</Warning>

## Redaction

`action: "redact"` masks the matched span instead of refusing the exchange.

```json theme={null}
{
  "id": "customer-pii",
  "type": "pii",
  "enabled": true,
  "apply_to": "both",
  "action": "redact",
  "categories": ["email", "phone"]
}
```

The provider receives `[CUSTOMER PII REDACTED]` in place of the address; the rest
of the message is untouched.

This exists because blocking alone is blunt in the way that gets a rule
disabled: a support transcript containing one customer email is refused
entirely, so the team switches the PII rule off and ends with no coverage rather
than partial coverage. A rule people leave on is worth more than a stricter one
they turn off.

The mask is a fixed marker, not a same-length run of asterisks: preserving the
length leaks how long the secret was, which narrows a search for a credential and
is most of the information for a phone number.

<Note>
  `redact` is rejected at validation on `llm` and `injection` rules. Those judge the
  whole text rather than locating a span, so redacting them would delete everything
  or nothing — and a policy that says "redact" while blocking is doing something
  other than what it says.
</Note>

## Prompt-injection detection

The `injection` type scores weighted signals — instruction override, role
hijack, named jailbreaks, policy subversion, system-prompt extraction, chat
delimiter injection — and blocks when they pass a threshold.

```json theme={null}
{
  "id": "injection",
  "name": "Prompt injection",
  "type": "injection",
  "enabled": true,
  "apply_to": "input"
}
```

It normalises before matching, so zero-width characters, full-width forms and
base64-encoded payloads do not evade it. It also inspects tool results, since
`extract` walks every message regardless of role — which is the MCP attack where
a malicious server returns instructions rather than data.

<Warning>
  This is a detector, not a solution. Prompt injection is not solvable by pattern
  matching: an attacker who knows the rules can phrase around them. It catches the
  large majority of real attempts — copied jailbreak prompts, encoded payloads,
  injected tool results — and raises the cost of the rest. It does not remove the
  need for least privilege on tools, or the rule that model output is never
  authorization.
</Warning>

Signals are calibrated in two classes. **Conclusive** signals block alone —
phrasings with no legitimate use, like "ignore all previous instructions".
**Ambiguous** signals need corroboration, because they occur in ordinary traffic:
a line beginning `system:` is how people paste a transcript. A test suite of 24
legitimate messages that talk *about* instructions, roles and prompts guards the
false-positive rate, and a held-out corpus checks that it generalises.

## A full example

```json theme={null}
{
  "$schema": "https://schema.vikat.ai",
  "plugins": [
    {
      "name": "guardrails",
      "enabled": true,
      "config": {
        "blocked_keywords": ["project-atlas"],
        "block_message": "This request was blocked by the content policy.",
        "apply_to": "both",
        "providers": [
          {
            "id": "outbound-pii",
            "name": "Outbound PII",
            "type": "pii",
            "enabled": true,
            "apply_to": "output",
            "categories": ["ssn", "credit_card"],
            "message": "The response was withheld because it contained personal data."
          },
          {
            "id": "secret-shapes",
            "name": "Credential shapes",
            "type": "regex",
            "enabled": true,
            "apply_to": "both",
            "patterns": ["AKIA[0-9A-Z]{16}", "sk-[A-Za-z0-9]{20,}"]
          },
          {
            "id": "judge",
            "name": "Model judge",
            "type": "llm",
            "enabled": true,
            "apply_to": "input",
            "endpoint": "http://127.0.0.1:8080/v1/chat/completions",
            "model": "openai/gpt-4o-mini"
          }
        ]
      }
    }
  ]
}
```

## Validation

A policy the gateway could not fully enforce is **rejected** rather than saved:
an invalid RE2 pattern, an unknown PII category, a rule left with nothing to
match, an `llm` rule missing its endpoint or model, or an unrecognised
`apply_to`. Over the API that is a 400; at startup the affected element is
dropped, the rest of the policy runs, and the plugin reports status `degraded`.

## See also

* [Guardrails](/enterprise/guardrails) — what the policy does, what it covers,
  and what it does not do.
