OGuardAI
Guides

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: true

A 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 last n characters, mask the rest. n is 1 to 64, mask_char defaults to "*".
  • reveal_first_n: show the first n characters, mask the rest, for a fixed leading prefix such as an IBAN country code or a postal-code prefix. Same n and mask_char bounds.
  • reveal_range: reveal a len-character window starting at 0-based start, 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. ranges is an ordered list of { max?, label }; the first range whose max is at least the value wins, and a range with no max is 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 band

A 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 authored action: allow is rejected at load; a deliberate raw pass-through is expressed via the policy whitelist, not an action.
  • Restore modes (full, formatted, partial, masked, abstract, none) are a closed set. Per-entity restore_strategies customize the partial reveal without code, but a brand-new mode is code. formatted restore is implemented for person and company names; other entity types under formatted fall back to masked, so use full/partial/masked/abstract/none for 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_labels are 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

  • redact for health_id, ssn, date_of_birth, with on_redact: reject on the identifiers that must hard-fail the request.
  • custom_patterns for mrn, npi, dea.
  • address at action: abstract so a location never reaches the model raw.
  • destination_permissions to allow ehr and deny external_llm for the most sensitive types.

Bank and PCI

  • credit_card and iban tokenized, restore_mode: masked by default.
  • restore_strategies.credit_card with reveal_last_n n: 4 for the receipt channel, so the partial restore shows only the last four.
  • A regulator_export channel with iban: "masked", person: "full".
  • custom_patterns for bank_account and routing_number.

Government and classified

  • rag.classification_levels set to your scheme (for example unclassified, cui, confidential, secret, top_secret).
  • caller_role conditions so a clearance level maps to a restore mode.
  • custom_patterns for clearance_id and facility_code.
  • Unknown classifications fail closed to most-restrictive automatically.

Ecommerce

  • order and customer_id tokenized and fully restored to internal channels.
  • A warehouse_label channel with address: "full", person: "full", email: "none" for shipping.
  • A support_reply path using caller_role: support_agent with override_restore_mode: partial.

CRM copilot

  • person, email, phone, company tokenized so drafts never carry raw contact data.
  • metadata_policy with expose_gender and expose_formality true so the model writes correctly gendered, correctly formal copy.
  • restore_templates.person_titles for the languages your reps write in.

Social

  • person, email, phone, ip, url tokenized.
  • A public_post channel restoring person: "abstract" and everything else none, so nothing identifying reaches a published surface.
  • Output guard left at the Mask floor for any custom publishing channel.
  • custom_patterns for matter_id.
  • caller_role conditions so privileged matter content reveals only to the assigned roles, partial or none to everyone else.
  • person and company tokenized, restored full only to an internal_summary channel.

HR

  • A salary custom_pattern, restored with a bucket strategy into bands so a copilot sees a range, not an exact figure.
  • caller_role: hr_manager with override_restore_mode: full for the people who are allowed the exact value.
  • custom_patterns for employee_id; date_of_birth tokenized with restore_mode: masked.