Input Controls
Request Rewriter
request-rewriter changes a request in predictable ways before the model receives it: it can add a system message, and run text rules that replace, prepend, or append text. For example, it can replace email addresses with [EMAIL] and add an instruction to follow company policy.
It works on string text in Chat Completions and Responses. It always allows the request and never judges whether a request is safe, so use it next to controls that block.
Configuration
Save this as policy-config.yaml:
pack:
name: request-rewriter-example-1
version: "1.0.0"
enabled: true
policies:
chain:
- request-rewriter
policy:
request-rewriter:
system_message:
content: Follow company policy.
rules:
- name: redact-email
pattern: '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
replacement: "[EMAIL]"
position: replace
- name: add-prefix
replacement: '[SAFE] '
position: prepend
condition: confidentialBefore and after
With this configuration, a request with these messages:
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "user",
"content": "Email alice@example.com about confidential pricing."
}
]
}reaches the provider with these messages:
{
"messages": [
{
"role": "system",
"content": "Follow company policy."
},
{
"role": "user",
"content": "[SAFE] Email [EMAIL] about confidential pricing."
}
]
}The model and every other field stay the same.
Fields
| Field | Type | Default | Description |
|---|---|---|---|
system_message | string or { content: string } | Text to add as a system message. Empty or whitespace-only values are ignored. | |
rules | list | [] | Rules in order. Each rule sees the text that earlier rules produced. |
rules[].name | string | Rule name. Required. | |
rules[].replacement | string | Replacement, prefix, or suffix text. Required. | |
rules[].pattern | string | "" | Regular expression for position: replace. Other positions ignore it. |
rules[].position | replace, prepend, or append | replace | Where the rule changes the text |
rules[].condition | string | Substring that the text must contain for the rule to run. Case does not matter. |
How it works
- The gateway parses the request.
- It adds
system_message, if set. See System message placement. - It runs the rules in order on each supported string field. See Rewritten fields and Rule positions.
- If anything changed, it forwards the changed request with
request_rewriter.applied. Otherwise, it forwards the original withrequest_rewriter.no_match. - The details record
rules_evaluated,rules_applied, andsystem_message_applied.
rules_applied counts each rule once for each field it changed: one rule that changes two messages adds 2. It does not name the rules.
System message placement
| Request | Placement |
|---|---|
Chat Completions with a string system message | Appended to the first one, after two newlines |
| Chat Completions without one | A new system message at messages[0] |
Responses with string input | input becomes a two-item array: a system item and a user item |
Responses with array input and a string system item | Appended to the first one, after two newlines |
Responses with array input and no system item | A new system item at input[0] |
If the system text already contains the configured text, it is not added again. A request with messages gets the message there and input is not changed.
Rewritten fields
| Request | Fields |
|---|---|
| Chat Completions | String messages[*].content values |
Responses with string input | input |
Responses with array input | String input[*] items and string input[*].content values |
Multimodal content arrays, nested content parts, tool definitions, tool arguments, and metadata are never changed.
Rule positions
| Position | Behavior |
|---|---|
replace | Replaces each match of pattern with replacement. Capture references such as $1 work. |
prepend | Adds replacement at the start, unless the text already starts with it |
append | Adds replacement at the end, unless the text already ends with it |
Phase and verdicts
| Item | Value |
|---|---|
| Input phase | Rewrites the request before dispatch |
| Output phase | Not checked |
| Verdicts | allow |
| Reason code, changed | request_rewriter.applied |
| Reason code, unchanged | request_rewriter.no_match |
Examples
Chain two rules
rules:
- name: preserve-ticket-number
pattern: 'ticket ([0-9]+)'
replacement: 'case $1'
- name: add-classification
replacement: '[INTERNAL] '
position: prepend
condition: caseReview ticket 4821 becomes [INTERNAL] Review case 4821. The first rule produces the word case, which the condition of the second rule then matches.
Verify a rewrite through the gateway
verdictan policy test does not run this control. Apply the file to a test agent with the config-first workflow and keep its gateway running. In the client terminal from the quickstart, with VERDICTAN_BASE_URL set to the agent URL ending in /v1:
curl -i "${VERDICTAN_BASE_URL}/chat/completions" \
-H "Authorization: Bearer ${VERDICTAN_CLIENT_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"stream": false,
"messages": [
{
"role": "user",
"content": "Email alice@example.com about confidential pricing."
}
]
}'Use a model that the agent serves. Then run verdictan events tail --since 10m --json and find the event with the request ID. Its nested request-rewriter result shows request_rewriter.applied, a rules_applied above zero, and the expected system_message_applied. The top-level reason code can stay ok.
Limits and failure behavior
- The control always allows and cannot reject a request.
- A
replacerule with invalid regular expression syntax is skipped silently. It counts as evaluated, not applied. - If an earlier input control blocks, nothing is rewritten or forwarded.
- The duplicate checks for
system_message,prepend, andappendcompare exact strings. - Rules apply to each field separately, so a
prependrule can add its prefix to several messages. - The rewrite is deterministic text processing, not semantic. It does not replace
prompt-injection,dlp-filter, orpii-detector. - To prove every change, point the gateway at an inspection upstream and read the forwarded JSON. The model output does not show them.
| Problem | Cause | Action |
|---|---|---|
request_rewriter.no_match | No supported string field changed: the condition or pattern did not match, or the pattern is invalid. | Check the request shape and test the pattern with your own text. |
| No system message | The value is empty, the system text already contains it, or the request shape is not supported. | Use non-empty text and check the JSON structure. |
| Prefix on several messages | Rules run on each field. | Use a narrower condition. |
| Multimodal content unchanged | Only string fields change. | Put the text in a supported string field. |
| Lint rejects a rule | name or replacement is missing. | Add both. |
| Pack tests pass, live request unchanged | policy test does not run this control. | Send a request through the gateway and read the nested result. |
Next steps
- Response Rewriter: the same rule format for responses.
- DLP Filter: block or redact sensitive patterns with a verdict.
- Investigate a Blocked Request: match results by request ID.