Skip to main content

Input Controls

Dual-Use Filter

dual-use-filter blocks requests that mention dual-use items, such as centrifuge or maraging steel, before they reach the provider. You supply the terms, or use its built-in list.

A match is a text signal only. It does not classify an item legally, and it does not understand the intent of the request.

Configuration​

policy-config.yaml
pack:
  name: dual-use-filter-example
  version: 1.0.0
  enabled: true
policies:
  chain:
    - dual-use-filter
policy:
  dual-use-filter:
    blocked_terms:
      - centrifuge
      - precursor chemical
      - maraging steel
      - uranium enrichment
    action: block
    fuzzy_matching: false

Verify exact matches first​

Add these tests and run verdictan policy test --json:

Add to policy-config.yaml
testing:
  suites:
    - name: dual-use-filter
      cases:
        - name: blocks-centrifuge
          input: {messages: [{role: user, content: "Describe a centrifuge"}]}
          expected: {verdict: block, reason_code: dual_use.triggered}
        - name: allows-schedule
          input: {messages: [{role: user, content: "Summarize the public schedule"}]}
          expected: {verdict: allow, reason_code: ok}

Both cases pass. Matching ignores case and uses word boundaries, so a harmless discussion of a listed item is also blocked. Add real allowed and blocked examples from your users before rollout.

With gateway 0.1.26 or later, action: redact replaces each matched term with [REDACTED:ID] in the request and in the response. An earlier gateway does not remove the term, so keep action: block on it.

Fields​

FieldTypeDefaultDescription
blocked_termslist of stringsBuilt-in listTerms to match. Without the key, the built-in list applies. An empty list matches nothing.
actionblock or redactblockVerdict on a match
fuzzy_matchingbooleanfalseAlso matches words within max_distance edits of a term
max_distanceinteger1Maximum Levenshtein distance, from 0 to 8

Built-in reference terms​

Used when blocked_terms is missing:

weapon, explosive, bioweapon, nerve agent, dual-use, dual use, stanag, nato restricted, nato secret, nato confidential, nato unclassified, precursor chemical, cyber surveillance, intrusion software, nuclear material, centrifuge, uranium enrichment, plutonium, heavy water, maraging steel, carbon fibre, carbon fiber, frequency hopping, spread spectrum, quantum cryptography, steganography, scrambler, biological agent, toxin, pathogen, stanag 4586, stanag 4607, stanag 5516, stanag 6001

How it works​

  1. The gateway joins the request messages into one lowercase string.
  2. Each term must match a whole word or phrase: weapon matches Weapon and weapon. but not weaponry.
  3. If nothing matched and fuzzy_matching is on, the control compares each term with word windows of the same length, within max_distance.
  4. No match: allow with dual_use.clean.
  5. A match: block or redact, following action, with dual_use.triggered.

The details contain action, matched (the first matching term), fuzzy_matching, and max_distance.

Phase and verdicts​

ItemValue
Phaseinput, plus an output check on streamed and buffered responses. With action: redact and gateway 0.1.26 or later, also output redaction.
Request familiesChat Completions, Completions, Responses, Messages
Verdictsallow, redact, block
Reason codesdual_use.clean, dual_use.triggered

Examples​

The three export control filters together:

policy-config.yaml
pack:
  name: export-control
  version: 1.0.0
  enabled: true
policies:
  chain:
    - dual-use-filter
    - entity-list-filter
    - itar-ear-filter
policy:
  dual-use-filter:
    blocked_terms:
      - uranium hexafluoride
      - thermal imaging
      - night vision
    action: block
  entity-list-filter:
    blocked_entities:
      - replace-with-denied-party-name
  itar-ear-filter: {}

The built-in list with the default block action:

Inside policy-config.yaml
policy:
  dual-use-filter: {}

Limits and failure behavior​

  • A block returns HTTP 400 with error code content_policy_violation, and the provider never gets the request.
  • With action: redact and gateway 0.1.26 or later, the gateway masks each matched term in the request before the provider gets it. The match uses whole words and ignores case. With fuzzy_matching: true, the gateway also masks the words within max_distance edits of a term. The marker follows the redaction settings of pii-detector. The same pass also replaces the PII that the shared redactor finds, such as email addresses.
  • With action: block, the gateway also checks the response on Chat Completions, Responses, and Messages. This output block check uses exact matching only, and without blocked_terms it checks only weapon, explosive, bioweapon, and nerve agent.
  • With action: block, an output match blocks the response. A buffered block returns HTTP 400. A streamed block ends the stream with an error event.
  • With action: block and stream: true, 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. See Output blocks in a stream.
  • With action: redact and gateway 0.1.26 or later, the gateway redacts the terms in streamed and buffered Chat Completions, Responses, and Messages output. The decision has the verdict redact, the reason code redact.applied, and an output result from dual-use-filter with dual_use.triggered. When the chain has another redacting policy, the output result can name that policy. See Output redaction reason codes. Without blocked_terms, output redaction uses the full built-in list.
  • Fuzzy matching also hits unrelated words: with max_distance: 2, the built-in toxin matches token. Start with 1, and test.
  • Literal matching does not understand context, negation, or intent.

Next steps​