zeroclaw-labs/zeroclaw · error
Unknown OTP domain category '{category}'. Known categories:
Error message
Unknown OTP domain category '{category}'. Known categories: {known} What it means
DomainMatcher::expand_categories maps named OTP domain categories to their built-in pattern lists. Exactly four categories exist in DOMAIN_CATEGORIES: banking, medical, government, identity_providers. Names are trimmed and lowercased before lookup, so casing never fails — only a genuine typo or unknown name does, and the error lists every valid name.
Source
Thrown at crates/zeroclaw-config/src/domain_matcher.rs:87
self.patterns
.iter()
.any(|pattern| domain_matches_pattern(pattern, &normalized_domain))
}
pub fn expand_categories(categories: &[String]) -> Result<Vec<String>> {
let mut expanded = Vec::new();
for category in categories {
let normalized = category.trim().to_ascii_lowercase();
let Some((_, domains)) = DOMAIN_CATEGORIES
.iter()
.find(|(name, _)| *name == normalized.as_str())
else {
let known = DOMAIN_CATEGORIES
.iter()
.map(|(name, _)| *name)
.collect::<Vec<_>>()
.join(", ");
bail!("Unknown OTP domain category '{category}'. Known categories: {known}");
};
expanded.extend(domains.iter().map(|domain| (*domain).to_string()));
}
Ok(expanded)
}
pub fn validate_pattern(pattern: &str) -> Result<()> {
let _ = normalize_pattern(pattern)?;
Ok(())
}
}
fn normalize_domain(raw: &str) -> Option<String> {
let mut domain = raw.trim().to_ascii_lowercase();
if domain.is_empty() {
return None;
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Use one of the exact names: banking, medical, government, identity_providers
- Pre-validate the config's category list against the four known names at load time
- If no built-in category fits, enumerate the concrete domain patterns in gated_domains instead of inventing a category
Example fix
# before categories = ["identity-provider"] # bails # after categories = ["identity_providers"]
Defensive patterns
Strategy: validation
Validate before calling
const KNOWN_CATEGORIES: &[&str] = &["banking", "medical", "government", "identity_providers"];
for c in &categories {
let n = c.trim().to_ascii_lowercase();
if !KNOWN_CATEGORIES.contains(&n.as_str()) {
anyhow::bail!("unknown OTP category {c}; expected one of {KNOWN_CATEGORIES:?}");
}
}
let matcher = DomainMatcher::new(&gated, &categories)?; Type guard
fn is_known_category(name: &str) -> bool {
let n = name.trim().to_ascii_lowercase();
matches!(n.as_str(), "banking" | "medical" | "government" | "identity_providers")
} Try / catch
match DomainMatcher::new(&gated, &categories) {
Ok(m) => m,
Err(e) if e.to_string().contains("Unknown OTP domain category") => {
anyhow::bail!("config error: {e}; fix the otp categories list")
}
Err(e) => return Err(e),
} Prevention
- Validate category names at config load, not at first OTP check
- Prefer copying the four canonical names from the docs table, not from memory
- If a category is missing, fall back to explicit gated_domains patterns
When it happens
Trigger: Calling `DomainMatcher::new(gated_domains, categories)` or `expand_categories(categories)` with any string outside {banking, medical, government, identity_providers} — e.g. "identity-provider" (hyphen instead of underscore) or "finance".
Common situations: Hand-editing the OTP-gated domains config and guessing at category names; copying a category from older docs or a different tool's vocabulary; hyphen/underscore confusion from slug-style naming conventions elsewhere in the config.
Related errors
- ACP request_permission failed: {} ({})
- ACP request_permission timed out after {timeout:?}
- ACP returned unknown optionId: {option_id}
- amqp.{}: dispatch = {:?} routes to the SOP engine but no SOP
- amqp channel '{}': client_cert is set but client_key is miss
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/294d7b53b8a81ab3.
Report an issue: GitHub.