UPL Filter
Use upl-filter to examine specified phrases in supported model output. The policy
works only with non-streaming output. It can block a response or add constant text.
UPL means unauthorized practice of law. This policy controls text. It does not make a legal determination. It does not identify jurisdictions, attorney status, client relationships, correct legal content, or disclaimer adequacy.
Outcome
| Condition | Verdict | Cause code | Body behavior |
|---|---|---|---|
| Blocked phrase, default behavior | block | upl.blocked | The gateway replaces the provider body with an HTTP 400 policy-violation response. |
Blocked phrase and rewrite_to_educational: true | allow | upl.rewritten_to_educational | Constant educational text is prepended. |
| Broad legal-advice cue and necessary disclaimer | allow | upl.disclaimer_injected | The policy prepends This is not legal advice. |
| No applicable mutation | allow | upl.clean | Body is unchanged. |
The policy details contain these values:
- A blocked result contains
{ "blocked": true }. - An educational rewrite contains
{ "rewritten": true, "original_would_block": true }. - A disclaimer or clean result contains
require_disclaimer,looks_like_legal_advice, anddisclaimer_injected.
The details do not identify which blocked phrase matched.
Prerequisites
- Add
upl-filterto the effective output chain. - Use a non-streaming OpenAI-compatible JSON response path.
- Identify if the policy must block or add an educational prefix.
- Get approval for the phrase set and disclaimer from legal and compliance owners.
The built-in strings are not legal advice for a specified jurisdiction.
Configuration
pack:
name: upl-filter-example-1
version: "1.0.0"
enabled: true
policies:
chain:
- upl-filter
policy:
upl-filter:
blocked_patterns:
- you should sue
- file this motion
- sign here
require_disclaimer: true
rewrite_to_educational: false
Supported fields
| Field | Type | Default | Notes |
|---|---|---|---|
blocked_patterns | string[] (items non-empty) | field not specified | If you do not specify this field, the runtime uses you should sue, file this motion, and sign here. A specified array replaces these defaults. |
require_disclaimer | boolean | true | Prepends This is not legal advice. for content that looks like legal advice. |
rewrite_to_educational | boolean | false | If a blocked pattern matches, the policy adds an educational notice. It does not block the response. |
blocked_patterns: [] is correct and gives the runtime an empty blocked-phrase
list. It does not restore the built-in phrases. The policy continues to examine
the broad disclaimer cues. Do not specify the field to use the built-in phrases.
Specified evaluation sequence
1. Blocked phrases
The policy checks each blocked pattern as a case-insensitive substring. It does not use a regex or word boundary. A custom array replaces the defaults. If an item matches, the policy does one of these actions:
-
By default, the policy returns
upl.blocked. -
With
rewrite_to_educational: true, the policy returnsallowand prepends this specified text:Note: The following is for educational purposes only and does not constitute legal advice. Please consult a qualified attorney for advice specific to your situation.
The prefix does not replace the source response. The evaluator does not change the response to an educational summary.
2. Broad disclaimer cues
If no blocked phrase matches, the policy checks for these legal-advice cues:
- The output contains
you should. - The output contains
fileandcourt. - The output contains
legal advice. - The output contains
retain counsel.
When require_disclaimer: true, the policy prepends:
This is not legal advice.
The policy does not add the text if the lowercase response starts with that specified sentence. The same sentence in a subsequent position does not prevent a new prefix.
rewrite_to_educational changes only blocked-phrase matches. It does not change
the broad disclaimer path.
Supported response shapes
The evaluator gets text from these fields:
- Chat Completions
choices[*].message.content. - A Responses top-level
outputstring. - Responses
output[*].content[*].textitems that have theoutput_texttype.
The prefix operation changes these fields:
- Each string in
choices[*].message.content. - Each string in Responses
output[*].content[*].textthat has theoutput_texttype.
A match in one segment sets the result for the full response. For a prefix result, the helper adds the prefix to each supported segment. It does not change only the segment that contains the cue.
The active prefix helper can examine a top-level output string but cannot
change it. Use the standard Responses output array when the policy must add
text.
The evaluator cannot get text from unsupported or invalid JSON. Thus, it cannot apply the UPL control to this JSON.
Streaming boundary
Do not use upl-filter for stream: true SSE traffic. The active streaming
policy loop does not run the UPL evaluator. Thus, it does not block streamed
output or add a prefix. Use stream: false for this control. Test the client
setting after each related change.
Safer configuration patterns
Keep the built-in hard blocks
policy:
upl-filter:
require_disclaimer: true
Extend the effective phrase set
Custom patterns replace the defaults. Thus, include the necessary built-in patterns in the list:
policy:
upl-filter:
blocked_patterns:
- you should sue
- file this motion
- sign here
- execute this affidavit
require_disclaimer: true
rewrite_to_educational: false
Prefix, not block
policy:
upl-filter:
blocked_patterns:
- you should sue
- file this motion
- sign here
require_disclaimer: true
rewrite_to_educational: true
This configuration delivers the source text after the educational prefix. Use it only when an authorized person approves this behavior.
Validate and verify
verdictan policy test does not run this gateway output handler. Use a controlled
upstream. Send non-streaming integration requests:
verdictan policy lint --file policy-config.yaml
verdictan gateway run \
--listen 127.0.0.1:41002 \
--agent upl-verification \
--policy-config policy-config.yaml
Send stream: false requests that cause each specified test result:
| Upstream text | Specified result with default config |
|---|---|
You should sue immediately. | HTTP 400, upl.blocked |
You should retain counsel for this matter. | 200, disclaimer prefix, upl.disclaimer_injected |
Courts have different jurisdictions. | 200, unchanged, upl.clean |
This is not legal advice. You should retain counsel. | 200, no duplicate prefix, upl.clean |
Run the blocked case again with rewrite_to_educational: true. Verify HTTP 200,
the educational prefix, and upl.rewritten_to_educational.
Correlate each request ID with:
verdictan events tail --since 10m --json
Verify the nested policy cause. Verify the body that the caller receives. A policy result does not prove that the prefix helper supports the returned JSON shape.
Rollout guidance
- Get positive and negative examples from legal owners for the specified jurisdictions and use cases.
- Start with non-streaming traffic on a limited route.
- Test false positives from blocked text in quotations, historical text, and educational text.
- Make sure that custom patterns keep each built-in phrase that is necessary.
- Measure block, disclaimer, and educational rewrite volumes for each configuration version.
- Review the list again when the product scope, jurisdictions, or legal workflows change.
Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
| Built-in phrase does not cause a block | blocked_patterns replaced the defaults. The value can be []. | Remove the field to use the defaults. Alternatively, include each necessary phrase. |
| Text matched unexpectedly | Matching is case-insensitive substring matching. | Narrow the phrase and add negative fixtures for quotations and educational content. |
| The policy adds a duplicate disclaimer | The response does not start with the specified built-in sentence. | Use the approved prefix in the same form. Alternatively, remove the subsequent copy from the upstream response. |
Event shows an injected disclaimer, but top-level output is unchanged | The evaluator can examine this shape, but the prefix helper cannot change it. | Use a supported output array shape. Verify the body. |
| Streamed response is unchanged | The active SSE policy loop does not run the UPL evaluation. | Use stream: false. |
| Educational mode delivers dangerous content | The policy adds constant text and does not change the source response. | Use the default block when the gateway must not deliver the source text. |
| Team uses the policy as legal compliance certification | Phrase checks and disclaimers cannot show legal adequacy. | Complete a legal review. Apply the necessary controls external to this policy. |
Next steps
- Legal Privilege — block supported privilege markers
- Response Rewriter — apply general deterministic output transformations
- Safety Filter — compare broader output blocking behavior
- Config Testing — document deterministic fixture coverage
- Investigate a Blocked Request — examine a live
upl.blockeddecision