Skip to main content

UPL Filter

Use upl-filter to apply a small, deterministic phrase check to supported non-streaming model output. It can block the response, prepend a fixed not-legal-advice disclaimer, or allow a blocked phrase with a fixed educational prefix.

UPL means unauthorized practice of law. This policy is a text control, not a legal determination. It does not identify jurisdiction, attorney status, client relationships, legal accuracy, or whether a disclaimer is legally sufficient.

Outcome

ConditionVerdictReason codeBody behavior
Blocked phrase, default behaviorblockupl.blockedProvider body is replaced by an HTTP 400 policy-violation response.
Blocked phrase and rewrite_to_educational: trueallowupl.rewritten_to_educationalFixed educational text is prepended.
Broad legal-advice cue and disclaimer requiredallowupl.disclaimer_injectedThis is not legal advice. is prepended.
No applicable mutationallowupl.cleanBody is unchanged.

Policy details are:

  • blocked: { "blocked": true };
  • educational rewrite: { "rewritten": true, "original_would_block": true }; or
  • disclaimer/clean: require_disclaimer, looks_like_legal_advice, and disclaimer_injected.

The details do not identify which blocked phrase matched.

Prerequisites

  • Add upl-filter to the effective output chain.
  • Use a non-streaming OpenAI-compatible JSON response path.
  • Decide whether a blocked phrase must stop delivery or may be delivered with an educational prefix.
  • Have qualified legal and compliance owners approve the phrase set and disclaimer. Built-in strings are not jurisdiction-specific legal advice.

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

FieldTypeDefaultNotes
blocked_patternsstring[] (items non-empty)field omittedWhen the field is omitted, runtime defaults are you should sue, file this motion, and sign here. A supplied array replaces the defaults.
require_disclaimerbooleantruePrepends This is not legal advice. for content that looks like legal advice.
rewrite_to_educationalbooleanfalseWhen a blocked pattern matches, prepends an educational notice instead of blocking.

blocked_patterns: [] is schema-valid and produces an empty blocked-phrase list in the current runtime. It does not restore the built-ins, but the broad disclaimer cues still run. Omit the field to use the built-ins.

Exact evaluation order

1. Blocked phrases

Each blocked pattern is a case-insensitive substring, not a regex or word-boundary expression. A custom array replaces rather than extends the defaults. If any item matches:

  • default behavior returns upl.blocked; or

  • rewrite_to_educational: true returns allow and prepends exactly:

    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 existing response remains after that prefix. The evaluator does not rewrite its substance into an educational summary.

2. Broad disclaimer cues

If no blocked phrase matched, output looks like legal advice when it contains any of:

  • you should;
  • both file and court, anywhere in the output;
  • legal advice; or
  • retain counsel.

When require_disclaimer: true, the policy prepends:

This is not legal advice.

It skips insertion only when the lowercased response already starts with that exact sentence. The same sentence later in the response does not prevent a new prefix.

rewrite_to_educational affects blocked-phrase matches only. It does not change the broad-cue disclaimer path.

Supported response shapes

The evaluator extracts text from:

  • Chat Completions choices[*].message.content;
  • a Responses top-level string output; and
  • Responses output[*].content[*].text items whose type is output_text.

Prefix insertion mutates:

  • every string choices[*].message.content; and
  • every string Responses output[*].content[*].text item with type output_text.

A match in any extracted segment makes the decision for the whole response. When the result is a prefix, the helper prepends it to every supported segment, not only the segment that contained the cue.

A top-level string output can be inspected but is not changed by the current prefix helper. Use the standard Responses output array when disclaimer or educational mutation is required.

Unsupported or invalid JSON produces no extracted text and therefore no effective UPL enforcement.

Streaming boundary

Do not rely on upl-filter for stream: true SSE traffic. The current streaming policy loop does not run the UPL evaluator over accumulated content, so it does not block or prefix streamed output. Require stream: false for this control and regression-test the client setting.

Safer configuration patterns

Keep the built-in hard blocks

policy:
upl-filter:
require_disclaimer: true

Extend the effective phrase set

Because custom patterns replace the defaults, repeat the built-ins explicitly:

policy:
upl-filter:
blocked_patterns:
- you should sue
- file this motion
- sign here
- execute this affidavit
require_disclaimer: true
rewrite_to_educational: false

Prefix instead of block

policy:
upl-filter:
blocked_patterns:
- you should sue
- file this motion
- sign here
require_disclaimer: true
rewrite_to_educational: true

This delivers the original text after the educational prefix. Use it only when that delivery behavior is explicitly approved.

Validate and verify

kt policy test does not execute this gateway output handler. Use a controlled upstream and non-streaming integration requests:

kt policy lint --file policy-config.yaml
kt gateway run \
--listen 127.0.0.1:41002 \
--agent upl-verification \
--policy-config policy-config.yaml

Send stream: false requests that produce each fixed fixture:

Upstream textExpected 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

Repeat the blocked case with rewrite_to_educational: true and expect HTTP 200, the educational prefix, and upl.rewritten_to_educational.

Correlate each request ID with:

kt events tail --since 10m --json

Verify the nested policy reason and the actual caller-visible body. A policy result alone does not prove that a prefix helper supported the returned JSON shape.

Rollout guidance

  1. Have legal owners provide positive and negative examples for the intended jurisdictions and use cases.
  2. Start with non-streaming traffic on a limited route.
  3. Test substring false positives such as blocked text inside quoted, historical, or educational discussion.
  4. Confirm custom patterns retain every built-in phrase you still require.
  5. Measure blocked, disclaimer, and educational-rewrite volumes by configuration version.
  6. Re-review the list when product scope, jurisdictions, or legal workflows change.

Troubleshooting

SymptomLikely causeResolution
Built-in phrase stopped blockingblocked_patterns was supplied and replaced the defaults, possibly with [].Remove the field for defaults or include every required phrase explicitly.
Text matched unexpectedlyMatching is case-insensitive substring matching.Narrow the phrase and add quoted/educational negative fixtures.
Disclaimer was duplicatedThe response did not start with the exact built-in sentence.Normalize the approved prefix or remove another later copy upstream.
Event says disclaimer injected but top-level output string is unchangedThe evaluator can inspect that shape, but the prefix helper does not mutate it.Use a supported output array shape and verify the body.
Streamed response was delivered unchangedUPL is not evaluated in the current SSE policy loop.Require stream: false.
Educational mode still delivered risky contentIt prepends fixed text and leaves the original response intact.Use default blocking when the original text must not be delivered.
Team expects legal compliance certificationPhrase matching and disclaimers cannot establish legal adequacy.Complete legal review and broader operational controls outside this policy.

Next steps