ADR-0006: Classifier seam and degradation semantics
Status
Accepted
Context
Classification (task, complexity, sensitivity / PII, injection risk) must be pluggable: heuristics before any model exists, ONNX models in-process later, external HTTP services for customers who bring their own. It must also fail safe: a timed-out classifier must never turn into a guess that loosens a constraint.
Decision
routed_classify::Classifieris a synchronous, CPU-bound trait:classify(&ClassifyInput) -> Result<Findings, ClassifyError>. The router runs it on a bounded blocking pool under a strict per-classifier timeout (RouterProfile.spec.classifier.timeoutMs); the engine itself never waits.- Implementations are selected by
RouterProfile.spec.classifier.type:heuristic(default, always available),stub(tests, goldens),http(phase 2),onnx(phase 4, cargo featureonnx). Unknown or unavailable types fail at profile load, not per request. - Input is the last user message, a truncated system prompt and every tool output (tool outputs are the main injection vector).
- A classifier error or timeout produces
Findings::degraded = [name]and nothing else. The engine treats any degraded finding as "classification failed" and applies the policyfallbackDecision, still subject to the data class (ADR-0003). No classifier ever reports a conservative value; the conservative behaviour lives in the engine where it is testable. - Every implementation must pass
routed_classify::conformance::assert_conformant.
Consequences
- Phases 1 to 3 run entirely on the heuristic classifier.
- A missing risk score is never permissive: when the policy sets
denyIfRiskScoreAboveor any candidate tier capsmaxRiskScorebelow 1, the engine recordsrisk:missingas a degraded classifier and takes the fallback path. A risk score that is present is always enforced (policy block and tier caps), even when other classifiers degraded. - The fallback tier is never exempt from the data class, request facts (tools, context window, required capabilities) or a known risk score; when classification succeeded and simply nothing survived, every hard constraint applies to it.
Alternatives considered
- Async trait: rejected; inference is CPU-bound and the blocking pool gives the same cancellation semantics with less ceremony.
- Returning "worst case" findings on timeout: rejected; it hides failures in telemetry and couples classifier and policy semantics.