zeroclaw-labs/zeroclaw · error · anyhow::Error
approval route channel '{channel_key}' does not support outb
Error message
approval route channel '{channel_key}' does not support outbound delivery (it is inbound-only); its approval notice cannot be sent (route '{route}') What it means
The route's channel is configured, but it is inbound-only (channel.supports_outbound_send() is false). Its send() is a no-op returning Ok, so spawning it would report success while delivering nothing; deliver() refuses and surfaces the error so the notice is not silently dropped. The broker only logs this — the gate is unaffected.
Source
Thrown at crates/zeroclaw-runtime/src/sop/approval/channel_route.rs:386
kind: ApprovalNoticeKind,
route: &str,
notice: &GateNotice<'_>,
) -> anyhow::Result<()> {
let (channel_key, msg) = build_delivery(kind, route, notice)?;
let Some(channel) = self.channels.get(&channel_key).cloned() else {
// A misconfigured route (names a channel that isn't configured) is a real
// operator error worth surfacing: return Err so the broker logs it. It
// still never affects the gate (the broker's deliver_* wrappers only log).
anyhow::bail!(
"approval route channel '{channel_key}' is not a configured channel \
(route '{route}')"
);
};
// An inbound-only channel's `send` is a no-op that returns `Ok`, so spawning it
// would report success without delivering anything. Refuse and surface it (the
// broker logs the Err) rather than silently dropping the notice.
if !channel.supports_outbound_send() {
anyhow::bail!(
"approval route channel '{channel_key}' does not support outbound \
delivery (it is inbound-only); its approval notice cannot be sent \
(route '{route}')"
);
}
// Fire-and-forget: hand the async send to the runtime and return. The gate is
// never blocked on channel I/O; a send failure is logged in the task.
// Native gate prompt first (buttons / keyboards, answered through the
// channel's inbound path); channels without one fall back to the text
// notice, whose `approve <run_id>` reply the orchestrator also resolves.
let prompt = build_gate_prompt(kind, notice);
let recipient = msg.recipient.clone();
let run_id = notice.run_id.to_string();
let route = route.to_string();
self.handle.spawn(async move {
let prompted = match channel.send_gate_prompt(&recipient, &prompt).await {
Ok(prompted) => prompted,
Err(e) => {View on GitHub (pinned to 88bb9c8533)
Solutions
- Point the approval route at a channel with outbound delivery (e.g. a discord/slack/telegram channel key).
- If the integration supports sending, enable/configure its outbound side so supports_outbound_send() is true.
- Keep a documented list of outbound-capable channel keys and validate routes against it in config review.
Example fix
# before [channels.webhook-ingest] # inbound-only url = "https://..." [approval] route = "webhook-ingest:ops" # after [channels.discord.ops] token = "..." [approval] route = "discord.ops:123456789"
Defensive patterns
Strategy: validation
Validate before calling
let (channel_key, _) = route.split_once(':').expect("validated route");
if let Some(ch) = channels.get(channel_key) {
assert!(ch.supports_outbound_send(), "channel '{channel_key}' is inbound-only; pick an outbound channel for approvals");
} Type guard
fn route_channel_can_send(route: &str, channels: &ChannelMap) -> bool {
route.split_once(':')
.and_then(|(c, _)| channels.get(c))
.map(|ch| ch.supports_outbound_send())
.unwrap_or(false)
} Try / catch
match router.deliver(kind, route, ¬ice).await {
Err(e) if e.to_string().contains("inbound-only") => {
log::warn!("route {route} targets an inbound-only channel; repoint at an outbound-capable one");
}
rest => rest?,
} Prevention
- Maintain a documented list of outbound-capable channel keys and review approval routes against it.
- Config lint: every approval route must resolve to a channel where supports_outbound_send() is true.
- Remember the gate still functions when delivery fails — do not treat notice failures as approval failures.
When it happens
Trigger: deliver() with a route whose channel half names an inbound-only channel (e.g. a webhook listener or feed-style channel) — any channel registered without outbound send support.
Common situations: Routing approvals to a webhook/ingest channel by mistake; channel type changed to inbound-only in an upgrade; reusing an existing inbound channel key for approvals instead of adding a two-way one; misreading which integrations can send messages.
Related errors
- approval route '{route}' is not 'channel:recipient' (e.g. 'd
- approval route channel '{channel_key}' is not a configured c
- discord send has no recipient: message.recipient is empty an
- Telegram sendMessage (approval) failed ({status}): {err}
- unsupported room visibility '{other}': expected private or p
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/35e1274da343a7c3.
Report an issue: GitHub.