OGuardAI
Guides

Policy Authoring

Write declarative YAML policies that control how OGuardAI handles each entity type

Policies are declarative YAML documents that define how OGuardAI handles each entity type. They control detection, transformation, and restoration behavior.

Policy Evaluation Flow

Policy YAML Structure

name: "my-policy"
version: "1.0.0"
description: "What this policy does and when to use it"

rules:
  - entity_type: "person"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "email"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "ssn"
    protection_level: 1
    action: "redact"
    conditions: []

defaults:
  protection_level: 2
  action: "tokenize"
  restore_mode: "full"
  on_redact: "continue"

metadata_policy:
  expose_gender: true
  expose_formality: true
  expose_language: true
  expose_role: true

Top-Level Fields

FieldRequiredDescription
nameYesUnique policy identifier (referenced in API requests)
versionYesSemantic version for change tracking
descriptionNoHuman-readable purpose description
rulesYesList of entity-type rules
defaultsYesFallback values for entity types without explicit rules
metadata_policyYesControls which metadata fields are exposed to the LLM (each flag defaults to false)
custom_patternsNoPolicy-scoped regex detectors (additive; never suppress a built-in)
detectionNoDetection controls: require NER for some entity types (fail closed), set a confidence floor

Per-Policy Detection (custom_patterns, detection)

A policy can add its own detectors and tighten detection, for any tenant, domain, or locale, without code. These controls are additive and fail closed: a policy can only add detection or require more, never silently weaken it.

custom_patterns:
  - entity_type: "employee_id"     # a custom:<type> entity; use a built-in name to extend that type
    pattern: '\bEMP-\d{6}\b'
    confidence: 0.9
    context_words: ["employee", "staff"]

detection:
  required_for: ["person", "company", "location"]  # fail closed if NER did not run for these
  min_confidence: 0.6                                # drop detections below this confidence

required_for makes the policy refuse a request rather than fall back to weaker regex-only detection when the NER detector did not actually run for an NER-dependent type, so "the model never sees a real name" holds even on a transient NER outage. min_confidence raises the detection floor uniformly.

min_confidence is a precision knob, not a calibrated probability. Confidence is per-backend and the scales are not comparable: built-in regex assigns fixed pattern confidences, GLiNER returns raw model scores, and spaCy stamps a default. A single min_confidence therefore means different things per backend, so tune it empirically against your own data rather than treating it as a probability. To trade recall against precision at the source, adjust the sidecar's GLINER_CONFIDENCE_THRESHOLD. min_confidence only ever tightens: it can drop low-confidence non-required spans, never admit one a stricter floor would reject.

This is how a deployment supports its own domain with no code change. The shipped policies show it: the opt-in healthcare-hipaa template (under policies/templates/, requires NER) defines mrn/npi/dea, financial defines bank_account/routing_number, hr defines employee_id (full only for hr_manager), government defines clearance_id/facility_code, and legal-privilege defines matter_id.

Every entity_type a policy references, in a rule, a channel override, a restore_strategies key, or detection.required_for, must be a built-in type or a custom type the same policy declares in custom_patterns. An unknown label aborts startup instead of silently becoming a rule that never matches, so a mistyped type (e.g. sssn for ssn, or persn in required_for) cannot quietly drop protection. To target a custom entity detected by a deployment-level detector rather than this policy, write it as custom:<name> to confirm intent and opt out of the typo check.

Entity Rules

Each rule defines how one entity type is handled:

rules:
  - entity_type: "person"        # Which entity type this rule applies to
    protection_level: 2           # 1 = hard mask/redact, 2 = reversible tokenization, 3 = semantic abstraction
    action: "tokenize"            # What action to take (redact | abstract | tokenize)
    conditions: []                # Optional conditional overrides

Protection Levels

LevelApplies ToDescription
1passport, vat_id, iban, credit_card, health_id, ssnHard masking or redaction. Raw values never stored in reversible form.
2person, email, phone, company, customer_id, order, address, location, date_of_birth, ip, url, customReversible tokenization. Values can be fully restored during rehydration.
3Metadata-only attributesSemantic abstraction. No raw value stored.

protection_level is a declarative tier annotation: it records intent and is surfaced in the policy-evaluation trace and policy diffs, but it does not enforce on its own. The effective handling of an entity is driven by action and restore_mode; keep protection_level consistent with them (e.g. a level-1 entity should use redact or a non-restorable restore_mode, not tokenize with restore_mode: full).

Policy Actions

The action field in each rule controls what happens to the entity during transformation. Valid values come from the PolicyAction enum:

ActionBehavior
redactValue is replaced with the sentinel [REDACTED:type] (e.g. [REDACTED:ssn]), purged from the session, and non-restorable. By default the request still returns 200; set on_redact: reject to fail the request with 403 instead.
tokenizeReplaced with a semantic token {{type:id:cap}} (reversible via rehydration). To mask on rehydration, set the rule's restore_mode: masked.
abstractReplaced with category label (e.g., [IBAN])

To send a specific value to the model unchanged, list it in the policy whitelist. The authorable actions are exactly redact, tokenize, and abstract: a policy that writes action: allow is rejected at load. allow exists only as an internal backstop, and the protection lattice clamps it to tokenize, so whitelist is the only pass-through path.

Redaction Disposition (on_redact)

redact removes the value but, by default, the request still succeeds with the value redacted in place. The on_redact field controls whether a redaction also fails the whole request:

ValueBehavior
continue (default)Redact the value to [REDACTED:type], purge it, return 200.
rejectReturn 403 with error code GUARDAI_POLICY_DENIED.

Set it as a policy-level default under defaults, or per rule. A per-rule value overrides the policy default. It only affects redact actions.

rules:
  - entity_type: "ssn"
    protection_level: 1
    action: "redact"
    on_redact: "reject"     # this entity hard-fails the request
    conditions: []

defaults:
  protection_level: 2
  action: "tokenize"
  restore_mode: "full"
  on_redact: "continue"     # other redactions redact-and-continue

Restore Modes

The restore_mode field (set in defaults or in channel overrides) controls how tokenized entities are restored during rehydration:

Restore ModeBehavior
fullComplete original value restored
partialDeterministic subset (e.g., J. Schneider)
maskedCharacter masking preserving length
formattedOriginal + contextual formatting (e.g., "Frau Julia Schneider")
abstractSemantic description (e.g., "(female customer)")
noneValue removed, shows [REDACTED]

Protect without restoring

Rehydration is optional. If your deployment should mask or tokenize but never bring the original value back, pick the pattern that fits:

  • Tokenize only. Call POST /v1/transform, send the tokenized safe_text to the model, and never call /v1/rehydrate. The {{type:id:cap}} tokens stay in the output and nothing is restored.
  • Irreversible redaction. Set action: redact on the rule. The value becomes [REDACTED:type], is purged from the session, and never reaches the model even as a token. Add on_redact: reject to fail the request instead. This is how cardholder data satisfies PCI-DSS Requirement 3.
  • Tokenize but never reveal. Keep action: tokenize for model context but set restore_mode: none, masked, or abstract, so a rehydrate call can never return the raw value.
  • Detect only. Call POST /v1/detect to find entities and spans without transforming anything.

action decides reversibility at transform time; restore_mode bounds what a rehydrate may reveal.

Language Coverage

formatted and abstract restoration are localized. The runtime ships built-in person titles and company/address/location/person abstract labels for 30 languages, and a deployment can override or add any language through restore_templates in oguardai.yaml (or GUARDAI_RESTORE_TEMPLATES for the proxy); the configured entries merge over the built-ins at startup. A language with no built-in or configured label falls back to a safe generic label; it is never the raw value and never an error. Coverage is a convenience floor, not a hard limit.

Custom Restore Strategies

When the six built-in modes are not enough (PCI card last-4, MRN last-3, an account number's last digits, a salary band instead of an exact figure), a restore_strategies block customizes the partial algorithm per entity type, with no code change. It is keyed by entity type, including a custom:<type> entity from custom_patterns. See the Extending Entities guide for how custom types are defined.

There are five strategy kinds:

KindParametersWhat it reveals
reveal_last_nn (1 to 64), mask_char (optional, default "*")The last n characters, masking the rest. Whitespace grouping is preserved.
reveal_first_nn (1 to 64), mask_char (optional)The first n characters, masking the rest. For a fixed prefix such as an IBAN country code or a postal-code prefix.
reveal_rangestart (0 to 64), len (1 to 64), mask_char (optional)A len-character window starting at 0-based character start, masking the rest. For an interior span such as a phone area code.
masknoneNothing. Every character is masked. This is the fail-closed partial.
bucketranges (a non-empty list of { max?, label })A coarse range label only (e.g. a salary band). The raw number is never shown.
restore_strategies:
  credit_card:
    kind: reveal_last_n     # show only the last n characters, mask the rest
    n: 4
    mask_char: "*"          # optional, default "*"
  mrn:
    kind: reveal_last_n
    n: 3
  iban:
    kind: reveal_first_n    # show only the leading country code, mask the rest
    n: 2
  phone:
    kind: reveal_range      # reveal only characters start..start+len
    start: 4
    len: 3
  account_token:            # masks fully even at partial
    kind: mask
  salary:
    kind: bucket            # numeric range generalization, not the exact value
    ranges:
      - { max: 50000, label: "<50k" }
      - { max: 100000, label: "50k-100k" }
      - { label: "100k+" }   # omit max on the last range to catch everything above

A bucket maps a numeric value into the first range whose max it does not exceed; the final range may omit max to catch everything above. A value that is not a single number, or that matches no range, masks fully rather than revealing anything.

The mrn, account_token, and salary keys above are custom types: the same policy must declare them in custom_patterns (or write them as custom:<name> to target a deployment-level detector), otherwise the load aborts with an unknown entity-type error.

Channel-ceiling gating

A strategy applies only when the channel-resolved restore mode for that entity is exactly partial. The rehydrate engine clamps the restore mode to the channel ceiling first, so a channel whose resolved mode is masked, abstract, or none never reaches partial, and the strategy does not run there. A strategy can therefore add a bounded reveal within partial, never beyond what the channel already permits.

If the strategy cannot produce a bounded result it masks the whole value, never the raw value: reveal_last_n and reveal_first_n mask fully when the value has n or fewer characters (revealing n would disclose all of it), reveal_range masks fully when the window is anchored at the start and reaches the end (any start above 0 already masks the prefix, so it cannot disclose everything), and bucket masks an unparseable or unmatched value. Parameters are validated at policy load, so an invalid value (for example n or len outside 1 to 64, or a bucket with an empty ranges) aborts startup. The line is the same as everywhere in the policy: config may only add or tighten, and invalid config fails closed.

With restore_mode: partial, 4111 1111 1111 4821 restores as **** **** **** 4821 under the credit_card strategy above.

Fail-closed partial defaults

When an entity restores at partial and no restore_strategies entry covers it, the engine applies a built-in default:

  • High-sensitivity identifiers (iban, ssn, credit_card, passport, health_id) and every custom:<type> entity reveal nothing in partial: they mask fully unless a restore_strategies entry opts that type into a bounded reveal. This is why an unconfigured custom identifier is safe at partial out of the box.
  • person, email, phone, company, and address have built-in partial shapes (for example J. Schneider). Person partials mask instead of revealing F. Surname for CJK-script and family-first names (zh, ja, ko, vi, hu), where that initial would be a misidentified name part.
  • Any other entity type with no built-in shape and no strategy masks fully. The default is always to reveal less, never more.

Strategies follow the same inheritance rule as entity and channel rules: a child policy overrides the parent's strategy for an entity type it lists, but omitting the type does not remove an inherited strategy (re-declare it to change it).

Conditional Rules

Rules support conditions for context-dependent behavior. Conditions are evaluated against detection confidence and the context object in the API request.

Three condition types are fully enforced at runtime:

  • confidence: numeric threshold conditions on detection confidence scores
  • caller_role: role-based conditions evaluated against the caller's authenticated role
  • caller_purpose: purpose-based conditions evaluated against the caller's authenticated purpose (e.g., "support", "analytics", "compliance")

Production: bind caller_role to API key config or JWT claims. In non-dev auth modes, request-supplied context.caller_role is ignored unless the auth credential has a bound role. This prevents callers from self-asserting privileged roles.

When a condition includes caller_role and no role is available (neither auth-bound nor dev-mode request), the condition does not match and the base rule action applies.

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"

      - field: "caller_role"
        operator: "neq"
        value: "guest"
        override_restore_mode: "tokenize"

Production: bind caller_role to your API key in the server config:

auth:
  api_keys:
    - key: "${GUARDAI_API_KEY}"
      identity: "admin-service"
      caller_role: "admin"
      scopes: ["transform", "rehydrate"]

Dev/testing only: pass caller_role in the transform request context:

curl -X POST http://localhost:3000/v1/transform \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Contact Julia Schneider at julia@example.com",
    "policy": "enterprise",
    "context": {
      "caller_role": "admin",
      "caller_purpose": "customer escalation review"
    }
  }'

Condition Operators

OperatorDescriptionExampleStatus
eqEqualsfield: "caller_role", value: "admin"Enforced
neqNot equalsfield: "caller_role", value: "guest"Enforced
inIn listfield: "caller_role", value: ["admin", "support"]Enforced
gteGreater than or equalfield: "confidence", value: 0.85Enforced
lteLess than or equalfield: "confidence", value: 0.5Enforced
gtGreater thanfield: "confidence", value: 0.7Enforced
ltLess thanfield: "confidence", value: 0.3Enforced
not_inNot in listfield: "caller_role", value: ["guest", "public"]Planned
existsField is presentfield: "vip_flag"Planned

How Role-Based Conditions Work

  1. A policy rule defines conditions with field: "caller_role".
  2. The caller's role is determined from auth credentials (caller_role in API key config or JWT claims). In dev mode, context.caller_role in the request is also accepted.
  3. The policy engine evaluates each condition against the authenticated role.
  4. If a condition matches, its override_restore_mode overrides the restore mode (not the action, entities are still tokenized).
  5. If no role is available, role-based conditions are skipped and the base action applies.

Re-authorized at rehydrate. A caller-gated override_restore_mode is sealed into the session as provenance, not as a fixed mode, and re-resolved at rehydrate time against the authenticated rehydrate caller (API key or JWT, never a request-supplied field). Holding a session_state produced under a privileged role therefore does not grant elevated restoration: a rehydrate caller who lacks the role degrades to the policy floor (the base restore mode without any caller elevation), and a restrictive override (for example caller_role neq admin mapping to none) still tightens for an unauthorized or roleless caller. The output channel ceiling is then applied on top, so a safe sink (log_safe, export) clamps the result regardless of role.

This enables enterprise policies where different roles see different levels of protection for the same entity types, enforced at the moment of restoration rather than only at transform time.

Channel Overrides

Output channels control how entities are restored for different audiences:

channel_rules:
  customer_email:
    person: "formatted"      # "Frau Julia Schneider"
    email: "none"            # [REDACTED]
    customer_id: "full"      # 948221
    order: "full"            # ORD-2026-4892

  internal_summary:
    person: "full"           # Julia Schneider
    email: "full"            # julia@example.com
    customer_id: "full"      # 948221
    order: "full"            # ORD-2026-4892

  export:
    person: "abstract"       # (female customer)
    email: "none"            # [REDACTED]
    customer_id: "abstract"  # (customer ID)
    order: "abstract"        # (order reference)

  log_safe:
    person: "masked"         # J**** ********r (first and last char kept)
    email: "masked"          # j*******************e
    customer_id: "masked"    # 9****1
    order: "partial"         # ORD-****-****

The built-in channels (user_output, customer_email, internal_summary, tool_payload, export, log_safe) carry sensible default restore modes. You can also define your own channels for any workflow: any name matching [a-z0-9_-]+ becomes a custom channel.

channel_rules:
  regulator_export:        # bank: deliver to a regulator
    person: "full"
    iban: "masked"
  warehouse_label:         # ecommerce: shipping label
    person: "full"
    address: "full"
    email: "none"

A custom channel is fail-closed: with no rule it restores nothing, so a typo in the request's output_channel can never over-expose. Define the rule to opt into the restoration you want.

Metadata Policy

Controls which metadata fields are included in the entity_context sent to the LLM:

metadata_policy:
  expose_gender: true       # Needed for gendered salutations (Herr/Frau)
  expose_formality: true    # Needed for formal/informal register
  expose_language: true     # Needed for language-correct output
  expose_role: true         # Emits the policy-defined entity role in entity_context

expose_gender, expose_formality, expose_language, and expose_role each gate one metadata field on the entity_context: when false, that field is withheld. expose_role surfaces the policy-defined entity role produced by the roles: block (see the Extensibility guide). With no roles: declared, no role is produced, so the field is simply absent; declaring a role taxonomy and setting expose_role: true makes it appear, gated exactly like the other three.

For the rest of the per-vertical dynamic surface not covered here, the roles: taxonomy, the RAG classification lattice, egress destination_permissions, detector vocab overlays, and language packs, see the Extensibility guide.


Example: Healthcare Policy

name: "healthcare"
version: "1.0.0"
description: "HIPAA-aligned healthcare policy: blocks PHI identifiers, tokenizes names, abstracts addresses"

rules:
  - entity_type: "health_id"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "ssn"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "date_of_birth"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "iban"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "credit_card"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "passport"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "person"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "email"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "phone"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "address"
    protection_level: 3
    action: "abstract"
    conditions: []

  - entity_type: "location"
    protection_level: 2
    action: "abstract"
    conditions: []

  - entity_type: "company"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "customer_id"
    protection_level: 2
    action: "tokenize"
    conditions: []

defaults:
  protection_level: 2
  action: "tokenize"
  restore_mode: "partial"

detection:
  required_for:
    - "person"

metadata_policy:
  expose_gender: false
  expose_formality: false
  expose_language: true
  expose_role: false

Key decisions:

  • Health IDs, SSNs, dates of birth, IBANs, credit cards, and passports are all blocked (HIPAA requirement)
  • Person names, emails, and phones are tokenized (reversible during rehydration)
  • Addresses are abstracted at protection level 3 (no raw value stored)
  • Default restore mode is partial: only subsets of values are restored
  • Person detection requires NER (detection.required_for: [person]), so the policy fails closed rather than falling back to regex-only if the NER detector is unavailable

Example: Financial Policy

name: "financial"
version: "1.0.0"
description: "Banking and financial services policy: blocks all financial identifiers, tokenizes names, no raw values in logs"

rules:
  - entity_type: "iban"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "credit_card"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "ssn"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "passport"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "health_id"
    protection_level: 1
    action: "redact"
    conditions: []

  - entity_type: "person"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "company"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "email"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "phone"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "address"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "location"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "customer_id"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "order"
    protection_level: 2
    action: "tokenize"
    conditions: []

  - entity_type: "date_of_birth"
    protection_level: 1
    action: "tokenize"
    restore_mode: "masked"
    conditions: []

  - entity_type: "ip"
    protection_level: 2
    action: "tokenize"
    conditions: []

defaults:
  protection_level: 2
  action: "tokenize"
  restore_mode: "masked"

metadata_policy:
  expose_gender: false
  expose_formality: false
  expose_language: true
  expose_role: true

Key decisions:

  • IBAN, credit card, SSN, passport, and health ID are always redacted (never reach the LLM)
  • Person names, companies, emails, and other identifiers are tokenized
  • Date of birth uses tokenize with restore_mode: masked: reversible per policy, shown masked at rehydration, not destroyed
  • Default restore mode is masked: character masking preserving length

Policy File Location

Policies are stored as YAML files in the configured policy directory. The shipped set below is representative, not exhaustive; read your deployment's policy directory for the authoritative list:

policies/
  customer-support-de/policy.yaml
  customer-support-en/policy.yaml
  default/policy.yaml
  enterprise/policy.yaml
  examples/
  financial/policy.yaml
  gdpr-strict/policy.yaml
  german-support/policy.yaml
  government/policy.yaml
  healthcare/policy.yaml
  hr/policy.yaml
  legal/policy.yaml
  legal-privilege/policy.yaml
  minimal/policy.yaml
  strict-pii/policy.yaml
  templates/
    healthcare-hipaa/policy.yaml   # opt-in, NER-required template

Policy changes are picked up without a server restart: on demand via POST /v1/admin/policy/reload (requires the admin scope; a reload that would load zero policies or fails validation is rejected and the running policy is kept), or automatically by enabling the policy-directory watcher with policy.watch: true. Use POST /v1/admin/policy/validate for a dry-run check before reloading.