Input Controls
Safety Filter
safety-filter blocks or escalates requests and responses that contain dangerous terms that you list, such as "disable emergency shutdown" for an industrial control assistant. It matches text. It is not a moderation model.
Write your own term list for your application, and test harmless uses of the same words. For broader classification, use External Moderation.
Configuration
Save this as policy-config.yaml in a new test directory. To add it to a running gateway, use the config-first workflow.
pack:
name: safety-filter-example
version: 1.0.0
enabled: true
policies:
chain:
- safety-filter
policy:
safety-filter:
mode: critical_infrastructure
block_if:
- override safety interlock
- disable emergency shutdown
action: block
fuzzy_matching: false
max_distance: 1
max_age: 0Your block_if list replaces the built-in terms, so include every term you need. Exact matching keeps the test predictable.
Try it without a provider
Add these tests to the end of the file:
testing:
suites:
- name: safety-filter
cases:
- name: blocks-configured-term
input:
messages:
- role: user
content: "override safety interlock"
expected:
verdict: block
reason_code: safety.triggered.critical_infrastructure
- name: allows-status-question
input:
messages:
- role: user
content: "What is the current maintenance status?"
expected:
verdict: allow
reason_code: okverdictan policy lint --file policy-config.yaml
verdictan policy test --jsonBoth cases report passed: true. To check the whole request path, deploy the file to a test gateway and send the same two messages from your application.
Fields
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | critical_infrastructure | Industry mode. Selects the built-in terms and appears in the reason code. |
block_if | string[] | [] | Terms to match. A non-empty list replaces the built-in terms. The lists are not merged. |
action | string | block | block or escalate |
fuzzy_matching | boolean | false | Also matches terms within max_distance edits. Input only. |
max_distance | integer | 1 | Maximum Levenshtein distance for a fuzzy match. Lint accepts 0 to 8. |
max_age | integer | 0 | Age of the audience. From 1 to 17, adds the adult content check. Input only. |
Lint accepts these modes: critical_infrastructure, automotive, education, law_enforcement, government, military, hr, justice, healthcare, finance, legal, and defense.
Built-in terms
| Mode | Terms used when block_if is empty |
|---|---|
critical_infrastructure | explosive, weapon, attack |
automotive | disable airbags, bypass brakes, tamper |
education | self-harm, suicide |
law_enforcement | doxx, target |
| Every other mode | explosive, weapon, attack |
How it works
- The gateway joins the content of all request messages into one lowercase string.
- The control uses
block_ifif it is not empty, otherwise the built-in terms ofmode. - It matches each term at word boundaries. If none matched and
fuzzy_matchingistrue, it compares each term with word windows, withinmax_distance. - With
max_agefrom1to17, it also looks for the substringsalcohol,tobacco,gambling,explicit,pornograph,drug use,substance abuse,violence,gore, andprofanity. - No match: the input verdict is
allowwithsafety.clean. - A match: the input verdict is
blockorescalate, followingaction. The reason code issafety.age_inappropriate.<mode>for an age match, orsafety.triggered.<mode>for a term. - When the provider responds, the control matches the same terms against the response at word boundaries. Fuzzy and age checks do not run on the response.
- A response match gives an output verdict of
blockorescalatewithsafety.triggered.<mode>.
The input details contain mode, action, matched, fuzzy_matching, max_distance, max_age, and age_inappropriate. The output details contain mode, action, and matched.
Phase and verdicts
| Item | Value |
|---|---|
| Phase | input and output |
| Request families | Chat Completions, Completions, Responses, Messages |
| Verdicts | allow, block, escalate |
| Reason codes | safety.clean, safety.triggered.<mode>, safety.age_inappropriate.<mode> |
A request blocked on input gets HTTP 400 with error code content_policy_violation. An escalation returns HTTP 200 with content: null, finish_reason: content_filter, and a verdictan.escalation object with the escalation ID and status queued. With gateway 0.1.26 or later, a buffered escalation of a Responses request gets a response object with status: incomplete, the content_filter reason, and no output. A Messages request gets a message with no content and the refusal stop reason. On an escalation, the client does not get the model text. Gateway 0.1.25 and earlier record a buffered output escalation but still return the model text.
Limits and failure behavior
- Only the listed words match. A request that describes a dangerous action in other words passes. Do not rely on this control alone for safety.
- The built-in lists are short, and
targetorattackmatch ordinary business text. Setblock_iffor production traffic. block_ifreplaces the built-in terms. To keep one, add it to your list.- Only
critical_infrastructure,automotive,education, andlaw_enforcementhave their own terms. Other modes use thecritical_infrastructureterms with a different reason code. - The
max_agecheck is a substring match on ten fixed words, not a classifier.violencein a history question triggers it. - Output matching runs on streamed and buffered Chat Completions, Responses, and Messages responses. With gateway 0.1.26 or later, a buffered Responses block returns the same HTTP
400error as Chat Completions. The input check runs on every listed family. - With
stream: trueandaction: block, the gateway sends the text while it arrives. With gateway 0.1.26 or later, it holds back the text that can be the start of a term. Thus no part of a matched term gets to the client. Text before the term can already be with the client. See Output blocks in a stream. - With gateway 0.1.26 or later and
stream: true,action: escalateexamines streamed Chat Completions, Responses, and Messages output. The gateway holds back the text that can be the start of a term. Thus no part of a matched term gets to the client. Text before the term can already be with the client. On a match, the stream stops with the error event of the route. The event has the reason code, the verdictESCALATE, and averdictan.escalationobject with statusqueued. See Output escalations in a stream. - When the control blocks or escalates a response, the request records the usage that the provider reports.
- Set up an escalation route before you use
action: escalatein production.
Next steps
- External Moderation: a classifier when a term list is not enough.
- Escalation Routing Configuration: route the escalations that
action: escalatecreates. - Resolve an Escalation: handle an escalated request in the Inbox.