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 ascrates/detector-builtins/data/context_words.txtand 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 thedetectionblock: it fails closed when a required NER detector did not run, and drops detections belowmin_confidence, while exemptingrequired_fortypes 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.