OGuardAI
ArchitectureDecision Records

ADR 0005: Dynamic Policy YAML Over Hardcoded Rules

A new vertical is a policy YAML file, not a code change, within the bounds of the built-in security contract

Status: Accepted

Context

An early critique was that OGuardAI hardcoded domain and language vocabulary and limits in Rust and Python, which would cap it at the verticals someone had thought to compile in. The promise is the opposite: any domain, any language, expressed as configuration. The tension is that not everything can be dynamic. The token format, the crypto, and the action and restore vocabularies are a security contract that a config file must not be able to rewrite. So the decision is not "everything is dynamic," it is "the domain surface is dynamic, the security contract is fixed."

Decision

Domain behavior is driven by policy YAML, evaluated by crates/policy (schema.rs, loader.rs, engine.rs). A deployment expresses its vertical without touching code through:

  • Custom entity types: EntityType::Custom(String) plus policy custom_patterns (regex, confidence, context words) and additive ner_labels.
  • Entity and caller roles: policy roles, with no built-in role taxonomy compiled in.
  • Custom output channels and destinations: OutputChannel::Custom and Destination::Custom, both fail-closed by default (a custom channel restores nothing unless a tighten-only override says otherwise; a custom destination is permission-gated).
  • RAG classification lattice: the process-global classification levels are set from config and treat an unknown label as most-restrictive.
  • Per-policy strictness: required detectors, a confidence floor, and per-channel restore-mode overrides.

Policies are validated fail-closed at load, and merge only in the tightening direction (see ADR 0001). What stays fixed in code by design is the security contract: the closed set of authored policy actions (redact, tokenize, abstract; an authored allow is rejected at load, and the internal allow decision exists only for whitelisted values, clamped to tokenize everywhere else), the closed set of restore modes, the closed set of detector backends (regex, GLiNER, spaCy), the token format, the crypto, and the size caps. A config file must not be able to introduce a new action or restore mode, because that would be redefining what protection means.

Consequences

  • A brand-new vertical (healthcare, financial, HR, government, legal) is authored as a YAML policy that declares its domain identifiers via custom_patterns and its labels via ner_labels, with zero code change. This was validated across those verticals in the dynamic-versus-hardcoded audit.
  • The dynamic surface is large enough to honestly support "unlimited use cases" for the policy-expressible parts: custom entities, channels, destinations, classification lattice, and strictness.
  • The line is explicit and reviewable: introducing a genuinely new action, restore mode, or detector backend is a deliberate code change, not an accident of configuration.
  • Language coverage follows the same shape: the detection floor is multilingual, and full enrichment for a given language is either built in for the shipped packs or added via an operator language pack, rather than being unlimited out of the box.

Reference

This decision records the audit, the per-category verdicts, and the confirmation that the audited verticals are expressible in policy YAML.