OGuardAI
ArchitectureDecision Records

ADR 0001: The Line

A built-in safe floor that policy may only add to or tighten, never silently weaken; invalid config or a missing required detector fails closed

Status: Accepted

Context

OGuardAI has two kinds of behavior competing for the same surface. Some things must be fixed so a deployment cannot weaken protection by editing a config file: the universal PII formats (email, phone, IBAN, SSN, card, IP, URL), the token protocol, the crypto parameters, and the decompression and size safety caps. Other things must be open so a deployment can cover its own domain: extra entity types, more context words, higher confidence thresholds, a hard NER requirement.

The failure mode to avoid is a config file that silently lowers the floor: disabling a built-in detector, dropping a threshold, or allowing raw passthrough without anyone noticing. If configuration can quietly weaken protection, every downstream guarantee ("the model never sees the real value") becomes unprovable.

Decision

We draw a line between the built-in floor and policy, and enforce a single direction of travel: policy may add detection or tighten it, never silently weaken it.

  • The floor is built in. Universal PII format detectors live in crates/detector-builtins, with the multilingual context-word set embedded as crates/detector-builtins/data/context_words.txt and the currency data as embedded immutable files, not customer-mutable config.
  • Policy only tightens. crates/policy (DetectionControl) can add custom regex patterns, add NER labels, raise the confidence floor, and require detectors, but its documented contract is add-or-tighten only.
  • The runtime enforces the direction. crates/runtime/src/detection.rs (apply_policy_detection) merges policy custom patterns over the base entity set (merge_custom_over_ner_ranked), then applies the detection block: it fails closed when a required NER detector did not run, and drops detections below min_confidence, while exempting required_for types from that floor so tightening one control cannot silently drop a required type.
  • Bad config fails closed at startup. Every policy's custom patterns are compiled up front; a bad regex, an invalid entity type, or too many patterns (MAX_POLICY_PATTERNS = 128) aborts rather than failing every request later.

What is deliberately fixed in code and not config: the token format {{type:id:cap}}, EntityType core variant names and id_prefix(), the AES-GCM nonce, tenant binding, the token-repair invariants, the decompression-bomb caps, and the closed action, restore-mode, and detector-backend vocabularies. These are exactly the surfaces a deployment must not be able to weaken.

Consequences

  • A new vertical is expressed as a policy YAML file, not a code change, because the open surface (custom patterns, NER labels, thresholds, channels, destinations) is broad enough to describe a domain.
  • Configuration errors surface at load time and fail closed, not as silent gaps at request time.
  • The floor is auditable and stable: reviewers can reason about protection from the built-in detectors plus a policy that can only be stricter.
  • The cost is rigidity where rigidity is the point: adding a genuinely new policy action, restore mode, or detector backend is a code change by design, because letting a config file introduce one would let it redefine the security contract.

Reference

The full audit and the per-category verdicts behind this line are captured across these Architecture Decision Records.