{"record":{"id":"821d3d9758f10812","repo":"zeroclaw-labs/zeroclaw","slug":"approval-route-channel-channel-key-is-not-a-co","errorCode":null,"errorMessage":"approval route channel '{channel_key}' is not a configured channel (route '{route}')","messagePattern":"approval route channel '(.+?)' is not a configured channel \\(route '(.+?)'\\)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-runtime/src/sop/approval/channel_route.rs","lineNumber":377,"sourceCode":"        );\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\n            // operator error worth surfacing: return Err so the broker logs it. It\n            // still never affects the gate (the broker's deliver_* wrappers only log).\n            anyhow::bail!(\n                \"approval route channel '{channel_key}' is not a configured channel \\\n                 (route '{route}')\"\n            );\n        };\n        // An inbound-only channel's `send` is a no-op that returns `Ok`, so spawning it\n        // would report success without delivering anything. Refuse and surface it (the\n        // broker logs the Err) rather than silently dropping the notice.\n        if !channel.supports_outbound_send() {\n            anyhow::bail!(\n                \"approval route channel '{channel_key}' does not support outbound \\\n                 delivery (it is inbound-only); its approval notice cannot be sent \\\n                 (route '{route}')\"\n            );\n        }\n        // Fire-and-forget: hand the async send to the runtime and return. The gate is\n        // never blocked on channel I/O; a send failure is logged in the task.\n        // Native gate prompt first (buttons / keyboards, answered through the\n        // channel's inbound path); channels without one fall back to the text","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/sop/approval/channel_route.rs#L359-L395","documentation":"The approval route parsed fine, but its channel half names a channel key that is not present in the runtime's channel registry (self.channels). A misconfigured route is treated as a real operator error: deliver() returns Err so the broker logs it. It never affects the gate itself — the broker's deliver wrappers only log.","triggerScenarios":"deliver() with a route like \"discord.ops:123\" when no channel keyed \"discord.ops\" is configured — e.g. the channel section was renamed, disabled, or the route was copied from another deployment.","commonSituations":"Channel renamed in config but approval route left stale; channel section commented out for testing; environment-specific channel keys (discord-prod vs discord-ops) mismatched; typo in the channel half of the route.","solutions":["Fix the route's channel half to exactly match a configured channel key (list your channels config).","Or add/enable the channel under the key the route names.","Keep route and channel keys in one place — derive route strings from channel keys rather than retyping them.","Add a config lint that cross-checks every approval route channel against configured channel keys at startup."],"exampleFix":"# before\n[channels.discord]\ntoken = \"...\"\n[approval]\nroute = \"discord.ops:123456789\"   # no channel keyed 'discord.ops'\n\n# after\n[channels.discord.ops]\ntoken = \"...\"\n[approval]\nroute = \"discord.ops:123456789\"","handlingStrategy":"validation","validationCode":"let (channel_key, _) = route.split_once(':').expect(\"validated route\");\nassert!(\n    channels.contains_key(channel_key),\n    \"approval route channel '{channel_key}' is not configured\"\n);","typeGuard":"fn route_channel_configured(route: &str, channels: &ChannelMap) -> bool {\n    route.split_once(':').map(|(c, _)| channels.contains_key(c)).unwrap_or(false)\n}","tryCatchPattern":"match router.deliver(kind, route, &notice).await {\n    Err(e) if e.to_string().contains(\"is not a configured channel\") => {\n        log::warn!(\"operator error: route {route} names an unconfigured channel; fix config\");\n    }\n    rest => rest?,\n}","preventionTips":["Cross-check approval route channel halves against configured channel keys at config load time.","When renaming/removing a channel, grep config for routes referencing it in the same change.","The broker only logs this error — add alerting on deliver failures so misroutes are noticed."],"tags":["config","approval","routing","channel","not-configured"],"backgroundTag":"unknown-channel-config","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}