{"record":{"id":"ed46b6caaf96b76e","repo":"zeroclaw-labs/zeroclaw","slug":"approval-route-route-is-not-channel-recipient","errorCode":null,"errorMessage":"approval route '{route}' is not 'channel:recipient' (e.g. 'discord.ops:123456789') - both halves must be non-empty","messagePattern":"approval route '(.+?)' is not 'channel:recipient' \\(e\\.g\\. 'discord\\.ops:123456789'\\) - both halves must be non-empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/approval/channel_route.rs","lineNumber":356,"sourceCode":"            }\n        },\n        description,\n        reference: gate_reference(notice),\n        choices,\n        resolved_description: Some(resolved_description),\n    }\n}\n\n/// Build the (channel_key, message) delivery pair from a route + run identity, or an\n/// error describing why it can't be built. PURE (no I/O, no spawn) so the parse +\n/// message-shaping is unit-testable without a runtime.\nfn build_delivery(\n    kind: ApprovalNoticeKind,\n    route: &str,\n    notice: &GateNotice<'_>,\n) -> anyhow::Result<(String, SendMessage)> {\n    let Some((channel_key, recipient)) = parse_approval_route(route) else {\n        anyhow::bail!(\n            \"approval route '{route}' is not 'channel:recipient' (e.g. \\\n             'discord.ops:123456789') - both halves must be non-empty\"\n        );\n    };\n    let msg = SendMessage::new(render_notice(kind, notice), recipient).suppress_voice();\n    Ok((channel_key.to_string(), msg))\n}\n\nimpl ApprovalRouteAdapter for ChannelRouteAdapter {\n    fn deliver(\n        &self,\n        kind: ApprovalNoticeKind,\n        route: &str,\n        notice: &GateNotice<'_>,\n    ) -> anyhow::Result<()> {\n        let (channel_key, msg) = build_delivery(kind, route, notice)?;\n        let Some(channel) = self.channels.get(&channel_key).cloned() else {\n            // A misconfigured route (names a channel that isn't configured) is a real","sourceCodeStart":338,"sourceCodeEnd":374,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/approval/channel_route.rs#L338-L374","documentation":"Approval notices are routed by a config string 'channel:recipient'. build_delivery() parses it and requires both halves non-empty (e.g. 'discord.ops:123456789'); a route without a colon, or with an empty channel or recipient half, fails before any message is built.","triggerScenarios":"A gate fires a notice with approval_route set to something like \"discord.ops\" (missing ':recipient'), \" :123\" or \"discord.ops:\" (empty half), or a bare recipient id. parse_approval_route returns None and build_delivery bails.","commonSituations":"Hand-edited config omitting the recipient after a colon; YAML quoting that drops or merges the colon; placeholder values never replaced; operators assuming the channel default recipient is implied.","solutions":["Set the route to 'channel:recipient' with both halves, e.g. approval_route = \"discord.ops:123456789\".","Quote the value in YAML/ TOML so the colon survives parsing intact.","Confirm the channel half exactly matches a configured channel key (see the next error) and the recipient is a real id for that channel.","Add a startup config test that validates every approval route matches ^[^:\\s]+:[^:\\s]+$."],"exampleFix":"# before\n[approval]\nroute = \"discord.ops\"\n\n# after\n[approval]\nroute = \"discord.ops:123456789\"","handlingStrategy":"validation","validationCode":"fn parse_route(route: &str) -> Option<(&str, &str)> {\n    let (ch, recv) = route.split_once(':')?;\n    let ok = |s: &str| !s.trim().is_empty() && !s.contains(':');\n    (ok(ch) && ok(recv)).then_some((ch.trim(), recv.trim()))\n}\nassert!(parse_route(&config.approval.route).is_some(), \"route must be 'channel:recipient'\");","typeGuard":"fn is_valid_route(route: &str) -> bool {\n    route.split_once(':')\n        .map(|(c, r)| !c.is_empty() && !r.is_empty())\n        .unwrap_or(false)\n}","tryCatchPattern":"match approval_router.deliver(kind, route, &notice).await {\n    Err(e) if e.to_string().contains(\"is not 'channel:recipient'\") => {\n        eprintln!(\"config error: approval route {route:?} must be 'channel:recipient'\");\n    }\n    rest => rest?,\n}","preventionTips":["Validate every approval route against ^[^:\\s]+:[^:\\s]+$ in a startup config check.","Quote route strings explicitly in YAML/TOML so the colon is preserved verbatim.","Never ship placeholder routes — fail config load on TODO/empty values."],"tags":["config","approval","routing","channel","format","validation"],"backgroundTag":"invalid-config-format","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}