Extensibility
Express any vertical or use case in policy and config alone, with no fork and no code change
OGuardAI is built so a deployment can express its own entities, roles, audiences, classification scheme, and restore formats without forking the runtime. This guide collects every dynamic surface in one place and maps the common verticals to the exact knobs that express them.
The Line
There is one rule that governs all of it. The built-in detection and restore behavior is a fail-closed floor. Configuration may only add detectors, context, languages, and audiences, or tighten what is restored. It can never weaken a built-in or silently disable detection. Invalid config does not degrade quietly: it aborts startup. Where a dynamic value cannot be resolved (an unknown classification, a value too short for a partial format, a typo in a channel name), the runtime fails closed rather than over-exposing.
Read this alongside Policy Authoring and the Configuration Reference, which document the full field set. This page is the cross-cutting "no code required" view.
Custom entity types
A policy can declare its own regex detectors under custom_patterns. They are
additive: they run alongside the built-ins and never suppress one. Each match
becomes a custom:<type> entity and tokenizes to {{custom:<type>:id}}. Patterns
are validated and compiled at startup, fail-closed.
custom_patterns:
- entity_type: "employee_id" # a-z, 0-9, _ and - only; becomes custom:employee_id
pattern: '\bEMP-\d{6}\b'
confidence: 0.9 # 0.0 to 1.0, default 0.8
context_words: ["employee", "staff"]Use a built-in entity name (for example address) instead of a new label to
extend that built-in type rather than create a new one. Custom entities are then
referenced everywhere a built-in is: in rules, in channel_rules, and in the
output guard's entity_actions as custom:<type>.
Caller-role RBAC
Policy rules carry conditions on the authenticated caller. The fields
caller_role and caller_purpose are free strings: your own role and purpose
names, not a fixed enum. A matching condition sets override_restore_mode, so the
same entity restores at a different level depending on who is asking.
rules:
- entity_type: "person"
protection_level: 2
action: "tokenize"
conditions:
- field: "caller_role"
operator: "eq"
value: "support_agent"
override_restore_mode: "partial"
- field: "caller_role"
operator: "in"
value: ["admin", "supervisor"]
override_restore_mode: "full"In production, bind the role and purpose to the credential, not the request. An
API key carries them; a self-asserted context.caller_role is ignored outside dev
mode.
auth:
mode: api_key
api_keys:
- key: "${GUARDAI_API_KEY}"
identity: "support-desk"
caller_role: "support_agent"
caller_purpose: "support"
scopes: ["transform", "rehydrate"]When no role is available, role conditions do not match and the base rule action applies. A condition can only change the restore mode, never the action: an entity that tokenizes stays tokenized.
Entity roles
Label a detected entity with a semantic role from context, so the model sees "the patient's name" vs "the doctor's name" without either value. This is the deployment's OWN taxonomy (no built-in role floor) and is distinct from caller-role RBAC above: caller roles say who is CALLING; entity roles say who the DETECTED entity is.
roles:
- name: "patient" # emitted as metadata.role; any vertical names its own
applies_to: ["person"]
priority: 100 # higher wins when several roles match one entity
context_words: ["patient", "admitted"]
anchored_regex: # the entity is the {{ENTITY}} sentinel before matching
- '(?i)\bpatient\b[^.\n]{0,40}\{\{ENTITY\}\}'
- name: "doctor"
applies_to: ["person"]
priority: 90
context_words: ["Dr.", "physician"]The role is always a declared name literal, never the raw value: a cue only ever sees the
{{ENTITY}} sentinel. No matching role, or an ambiguous tie at the top priority, yields no role
(fail-safe, never guessed). Roles are additive metadata gated by metadata_policy.expose_role (define
roles: AND set expose_role: true), and flow through every ingress: transform, structured/chat,
RAG, file, image, and as response metadata on /v1/detect and /v1/batch/detect. Invalid role config
(a bad name, an unknown applies_to type, an invalid regex, a duplicate name) aborts startup.
Output channels and egress destinations
Restoration is resolved per audience. channel_rules maps a channel to the
restore mode for each entity type, and destination_permissions on a rule decides
whether a tokenized value may travel to a given egress target. Both the channel
name and the destination name are free strings matching [a-z0-9_-]+, so you
define your own audiences and sinks beyond the built-ins.
channel_rules:
regulator_export: # your own channel
person: "full"
iban: "masked"
warehouse_label:
person: "full"
address: "full"
email: "none"
rules:
- entity_type: "iban"
protection_level: 1
action: "tokenize"
restore_mode: "masked"
destination_permissions:
siem: false # your own destination; deny IBAN egress to the SIEM
ehr: trueA custom channel is fail-closed: with no rule it restores nothing, so a typo in
output_channel cannot over-expose. Built-in channels carry sensible defaults; a
custom channel you have not declared restores nothing until you opt in. For the
output guard's second-pass scanning, a custom output channel defaults to the Mask
floor unless you list it as trusted:
GUARDAI_TRUSTED_OUTPUT_CHANNELS="internal_analytics,debug_console"The value is comma-separated. Listing a channel relaxes the second-pass floor for that channel only; everything else keeps the Mask floor.
RAG classification lattice
For retrieval, OGuardAI filters chunks by comparing a caller's access_level
against each chunk's classification. The lattice is yours to define, ordered
least to most restrictive.
rag:
classification_levels: ["unclassified", "cui", "confidential", "secret", "top_secret"]An access level grants a chunk when its rank is greater than or equal to the chunk's rank. Any label not in the lattice ranks most-restrictive (fail closed), so an unknown or misspelled classification is filtered out rather than leaked. The list must be non-empty, unique, and at most 255 entries.
Restore strategies
When the six built-in restore modes are not enough, a per-entity strategy
customizes the partial restore with no code. Set it policy-side under
restore_strategies, or config-side under restore_templates.partial_formats.
These kinds ship:
reveal_last_n: show the lastncharacters, mask the rest.nis 1 to 64,mask_chardefaults to"*".reveal_first_n: show the firstncharacters, mask the rest, for a fixed leading prefix such as an IBAN country code or a postal-code prefix. Samenandmask_charbounds.reveal_range: reveal alen-character window starting at 0-basedstart, mask the rest, for an interior span such as a phone area code.mask: mask the whole value.bucket: generalize a numeric value into a coarse band.rangesis an ordered list of{ max?, label }; the first range whosemaxis at least the value wins, and a range with nomaxis the open-ended top band.
restore_strategies:
credit_card:
kind: reveal_last_n
n: 4
mask_char: "*"
salary:
kind: bucket
ranges:
- { max: 50000, label: "<50k" }
- { max: 100000, label: "50k-100k" }
- { label: "100k+" } # no max: open-ended top bandA strategy applies only when the channel-resolved mode is partial. The engine
clamps to the channel ceiling first, so a channel whose ceiling is masked or
none never reaches a strategy, and a strategy can never reveal more than the
channel already permits. If a strategy cannot produce a bounded result (a value
with fewer characters than n, an unparseable number) it masks the whole value, it
never falls back to the raw value. Invalid parameters abort startup.
With restore_mode: partial, 4111 1111 1111 4821 restores as
**** **** **** 4821, and a salary of 72000 restores as 50k-100k.
A restore strategy is per entity type and deployment-global: the partial-reveal
ALGORITHM does not vary per output channel. Per-channel control adjusts the restore
MODE (full, formatted, partial, masked, abstract, none, tighten-only), not which
partial algorithm runs. So credit_card uses the same strategy wherever it resolves
to partial, and a stricter channel can only mask more, never switch to a different
partial shape. This is deliberate; raise it if a deployment needs a different partial
shape per channel for the same entity.
Detector vocabulary overlay
detector.vocab in oguardai.yaml tunes the built-in detector for your domain
and locale without recompiling. Everything here is additive: it strengthens
detection and the base floor stays.
detector:
vocab:
context_words:
money: ["honorar", "rechnungsbetrag"]
person: ["mandant", "ansprechpartner"]
currency_codes: ["XBT"]
extra_patterns:
address: ['〒\d{3}-\d{4}[^\n]{0,40}'] # a non-DE/US postal address
date_of_birth: ['\b\d{4}年\d{1,2}月\d{1,2}日\b']
"custom:vat_id": ['\bVAT-\d{6}\b']extra_patterns extends detection of any entity type for any locale. A built-in
label yields the built-in entity; custom:<name> yields a new type.
For patterns that need more control, extra_pattern_specs takes structured entries
per label: a pattern plus optional confidence, context_words, context_required
(only match near a context word), context_window_chars, and value_group (tokenize
only one capture group). Use it for broad locale shapes, such as addresses, that would
over-detect without a context gate. Both surfaces are additive and fail closed:
detector:
vocab:
extra_pattern_specs:
address:
- pattern: '〒\d{3}-\d{4}' # JP postal, distinctive marker
- pattern: '\d{2}-\d{3}' # PL postal, gated so it does not over-detect
context_required: true
context_words: ["adres", "kod pocztowy"]There is no built-in locale ceiling for addresses: see the Address Detection guide and the ready-to-use examples/address-locale-pack.yaml.
The only control that reduces detection is suppressing a money-colliding currency code, and that is a gated break-glass requiring an explicit acknowledgement and a reason, logged at startup:
detector:
vocab:
money_colliding_codes: ["TRY"]
allow_money_detection_suppression: true
money_suppression_reason: "TRY collides with the verb 'try' in our corpus"The proxy reads the same overlay from GUARDAI_DETECTOR_VOCAB as JSON.
Language packs and restore templates
OGuardAI ships enrichment for about 30 languages as the immutable floor. Two surfaces extend it from data, with no source change.
Language packs (the Python detector's GUARDAI_LANGUAGE_PACKS, a JSON file) add a
brand-new language, extend a built-in (the overlay only widens it, the built-in
stays authoritative), or fully replace one with _mode: "replace".
{
"yue": { "female_titles": ["女士"], "male_titles": ["先生"] },
"de": { "informal_markers": ["servus "] },
"fr": { "_mode": "replace", "female_titles": ["Mme "], "male_titles": ["M. "] }
}Restore templates localize the restored output (honorifics, abstract labels), merged over the built-in defaults. Override only what you need; everything else keeps its default.
restore_templates:
person_titles:
de:
formal_female: "Sehr geehrte Frau {name}"
formal_male: "Sehr geehrter Herr {name}"
tr:
formal_female: "Sayın {name}"
company_abstract_labels:
ja: "(会社名)"A language with no built-in and no override falls back to a safe generic label,
never the raw value and never an error. The proxy reads the same overrides from
GUARDAI_RESTORE_TEMPLATES as JSON. Coverage is a convenience floor, not a hard
limit: any language is addable.
Restoration is also bidi-safe. A restored value that contains right-to-left or bidi-control characters is wrapped in a directional isolate and its inner bidi controls are stripped, so a restored value cannot reorder or spoof the surrounding text. Left-to-right values are unchanged. Partial restore masks family-first names (for example zh, ja, ko, vi, hu) rather than reveal a misidentified initial, while given-first scripts such as Arabic and Hebrew keep their reveal.
What requires code (and why)
Everything a vertical needs to express its data is dynamic: entity types, entity roles, caller roles, output channels, egress destinations, the classification lattice, NER labels, languages, and the partial-restore algorithm. A small core is fixed by design, and that is deliberate, not a missing feature.
- Policy actions (
redact,tokenize,abstract) are a closed set. What can happen to a detected entity is a security-reviewed contract, so a novel action such as route, quarantine, or notify is a code change, not a config knob. An authoredaction: allowis rejected at load; a deliberate raw pass-through is expressed via the policywhitelist, not an action. - Restore modes (
full,formatted,partial,masked,abstract,none) are a closed set. Per-entityrestore_strategiescustomize thepartialreveal without code, but a brand-new mode is code.formattedrestore is implemented for person and company names; other entity types underformattedfall back tomasked, so usefull/partial/masked/abstract/nonefor them. - Detector backends (built-in regex, GLiNER, spaCy) are a fixed set, but the
vertical NER labels are fully dynamic: a policy's
detection.ner_labelsare sent to GLiNER as zero-shot labels with no code change. A proprietary detection engine is the only backend case that requires code.
This boundary is the point of the trust model. The things that are fixed (the action and restore vocabulary, the token format, the crypto, the decompression and size caps) are exactly the things a deployment should not be able to weaken from a config file. Everything domain-specific is yours to define in YAML.
Per-vertical recipes
Each vertical is expressed by composing the surfaces above. The shipped policies
under policies/ already use custom_patterns for their domain identifiers.
Hospital and HIPAA
redactforhealth_id,ssn,date_of_birth, withon_redact: rejecton the identifiers that must hard-fail the request.custom_patternsformrn,npi,dea.addressataction: abstractso a location never reaches the model raw.destination_permissionsto allowehrand denyexternal_llmfor the most sensitive types.
Bank and PCI
credit_cardandibantokenized,restore_mode: maskedby default.restore_strategies.credit_cardwithreveal_last_nn: 4for the receipt channel, so the partial restore shows only the last four.- A
regulator_exportchannel withiban: "masked",person: "full". custom_patternsforbank_accountandrouting_number.
Government and classified
rag.classification_levelsset to your scheme (for exampleunclassified,cui,confidential,secret,top_secret).caller_roleconditions so a clearance level maps to a restore mode.custom_patternsforclearance_idandfacility_code.- Unknown classifications fail closed to most-restrictive automatically.
Ecommerce
orderandcustomer_idtokenized and fully restored to internal channels.- A
warehouse_labelchannel withaddress: "full",person: "full",email: "none"for shipping. - A
support_replypath usingcaller_role: support_agentwithoverride_restore_mode: partial.
CRM copilot
person,email,phone,companytokenized so drafts never carry raw contact data.metadata_policywithexpose_genderandexpose_formalitytrue so the model writes correctly gendered, correctly formal copy.restore_templates.person_titlesfor the languages your reps write in.
Social
person,email,phone,ip,urltokenized.- A
public_postchannel restoringperson: "abstract"and everything elsenone, so nothing identifying reaches a published surface. - Output guard left at the Mask floor for any custom publishing channel.
Legal
custom_patternsformatter_id.caller_roleconditions so privileged matter content reveals only to the assigned roles, partial or none to everyone else.personandcompanytokenized, restoredfullonly to aninternal_summarychannel.
HR
- A
salarycustom_pattern, restored with abucketstrategy into bands so a copilot sees a range, not an exact figure. caller_role: hr_managerwithoverride_restore_mode: fullfor the people who are allowed the exact value.custom_patternsforemployee_id;date_of_birthtokenized withrestore_mode: masked.