Bug: ask_user tool is non-functional in Slack/gateway mode
Summary
When an agent running through a Slack channel (or any gateway channel) calls the ask_user tool, the call is broken in all approval modes:
- Auto-approved (AllowAll/Allowlist): The tool is approved and executed via the MCP server handler, which returns
"INTERACTIVE_REQUIRED" error — because the MCP handler explicitly does not support non-interactive execution. The agent receives an error result instead of user answers.
- Approval-gated (Ask/DenyAll with ask_user not allowlisted): The
ask_user call appears as a generic "Allow/Deny" approval prompt with a brief tool preview. The user can only approve or deny the tool — they never see the actual questions and cannot answer them.
In both cases, the ask_user tool is completely non-functional through gateway channels.
Root Cause
The gateway dispatcher (libs/gateway/src/dispatcher.rs) treats ask_user like any other tool — routing it through the tool_calls_proposed → approval → execute pipeline. But ask_user is fundamentally different from other tools:
- It requires user input as its result — not just an approve/deny binary decision
- Its arguments contain the questions that should be shown to the user
- The MCP server handler (
libs/mcp/server/src/local_tools.rs:3089) explicitly refuses non-interactive execution, returning an error
In contrast, the TUI interactive mode (cli/src/commands/agent/run/mode_interactive.rs) correctly intercepts ask_user before execution: it auto-approves the tool, shows a popup with the questions, collects answers, and sends the structured AskUserResult back as the tool result.
Expected Behavior
When ask_user is called through Slack, the channel should:
- Intercept the tool call before it reaches the approval/execution pipeline
- Render the questions as Slack Block Kit interactive elements — radio buttons for single-select, checkboxes for multi-select, with an optional text input for custom answers
- Collect user responses via Slack interactive callbacks
- Feed the structured
AskUserResult back as the tool result (resolving the tool call with the answer content, not just accept/reject)
Implementation Approach
The fix involves changes primarily in the gateway dispatcher and Slack channel:
1. Intercept ask_user in the dispatcher (libs/gateway/src/dispatcher.rs)
When tool_calls_proposed is received, check if any of the proposed tools is ask_user. If so:
- Auto-resolve all non-
ask_user tools normally (per approval policy)
- For
ask_user tools, parse the AskUserRequest from arguments and return a new RunOutcome::AskUserNeeded variant (similar to ApprovalNeeded)
- The dispatcher handler should call
channel.send_ask_user(...) on the Slack channel
2. Add send_ask_user to the Channel trait (libs/gateway/src/channels/mod.rs)
New trait method (with default no-op impl):
async fn send_ask_user(
&self,
reply: OutboundReply,
questions: Vec<AskUserQuestion>,
) -> Result<String> { ... }
3. Implement send_ask_user in Slack channel (libs/gateway/src/channels/slack.rs)
- Render each question as a Block Kit section with radio buttons (
input block type with radio_buttons element) or checkboxes for multi-select
- Add a plain_text_input element for custom answers when
allow_custom is true
- Use
action_id format: q:{question_label}:{option_index} for selection and q:{question_label}:custom for text input
- Handle interactive callback from Slack → parse responses → build
AskUserResult → resolve the tool call via client.resolve_tools() with the answer as content
4. Handle ask_user response callbacks in the dispatcher
When a Slack interactive callback arrives with type=ask_user_response:
- Parse the selected options per question
- Build
AskUserResult with AskUserAnswer entries
- Call
client.resolve_tools() with ToolDecisionAction::Accept and content = Some(serde_json::to_string(&result))
- Edit the original message to show the user's answers
5. Add ask_user to safe auto-approve allowlist
Since ask_user is now handled interactively by the channel, it should be in the default allowlist so it doesn't block on approval. The tool is inherently safe — it only asks questions and returns the user's answers.
Files to Modify
| File |
Change |
libs/gateway/src/dispatcher.rs |
Intercept ask_user in consume_run_events, add AskUserNeeded outcome, handle response callbacks |
libs/gateway/src/channels/mod.rs |
Add send_ask_user trait method |
libs/gateway/src/channels/slack.rs |
Implement Block Kit question rendering + callback parsing |
libs/gateway/src/channels/telegram.rs |
Implement inline keyboard question rendering |
libs/gateway/src/channels/discord.rs |
Implement component-based question rendering |
libs/gateway/src/types.rs |
Add InboundMessage metadata type for ask_user responses |
Fallback for channels without interactive support
For channels that don't implement send_ask_user (e.g., basic webhooks), the dispatcher should:
- Render the questions as formatted text in the channel
- Auto-reject the
ask_user tool with a message like "This channel does not support interactive questions. Please use a channel that supports buttons/interactive elements."
- Or: accept the tool with a default/cancelled result
Related Code References
- TUI ask_user handler:
tui/src/services/handlers/ask_user.rs
- TUI popup interception:
cli/src/commands/agent/run/mode_interactive.rs:145-180
- MCP handler (returns INTERACTIVE_REQUIRED):
libs/mcp/server/src/local_tools.rs:3089-3099
- Types:
libs/shared/src/models/tools/ask_user.rs
- Gateway approval flow:
libs/gateway/src/dispatcher.rs:1410-1490
- Slack approval buttons:
libs/gateway/src/channels/slack.rs:728-750
- Slack Block Kit:
libs/gateway/src/channels/slack.rs:1023+
Bug:
ask_usertool is non-functional in Slack/gateway modeSummary
When an agent running through a Slack channel (or any gateway channel) calls the
ask_usertool, the call is broken in all approval modes:"INTERACTIVE_REQUIRED"error — because the MCP handler explicitly does not support non-interactive execution. The agent receives an error result instead of user answers.ask_usercall appears as a generic "Allow/Deny" approval prompt with a brief tool preview. The user can only approve or deny the tool — they never see the actual questions and cannot answer them.In both cases, the
ask_usertool is completely non-functional through gateway channels.Root Cause
The gateway dispatcher (
libs/gateway/src/dispatcher.rs) treatsask_userlike any other tool — routing it through thetool_calls_proposed → approval → executepipeline. Butask_useris fundamentally different from other tools:libs/mcp/server/src/local_tools.rs:3089) explicitly refuses non-interactive execution, returning an errorIn contrast, the TUI interactive mode (
cli/src/commands/agent/run/mode_interactive.rs) correctly interceptsask_userbefore execution: it auto-approves the tool, shows a popup with the questions, collects answers, and sends the structuredAskUserResultback as the tool result.Expected Behavior
When
ask_useris called through Slack, the channel should:AskUserResultback as the tool result (resolving the tool call with the answer content, not just accept/reject)Implementation Approach
The fix involves changes primarily in the gateway dispatcher and Slack channel:
1. Intercept
ask_userin the dispatcher (libs/gateway/src/dispatcher.rs)When
tool_calls_proposedis received, check if any of the proposed tools isask_user. If so:ask_usertools normally (per approval policy)ask_usertools, parse theAskUserRequestfrom arguments and return a newRunOutcome::AskUserNeededvariant (similar toApprovalNeeded)channel.send_ask_user(...)on the Slack channel2. Add
send_ask_userto theChanneltrait (libs/gateway/src/channels/mod.rs)New trait method (with default no-op impl):
3. Implement
send_ask_userin Slack channel (libs/gateway/src/channels/slack.rs)inputblock type withradio_buttonselement) or checkboxes for multi-selectallow_customis trueaction_idformat:q:{question_label}:{option_index}for selection andq:{question_label}:customfor text inputAskUserResult→ resolve the tool call viaclient.resolve_tools()with the answer as content4. Handle
ask_userresponse callbacks in the dispatcherWhen a Slack interactive callback arrives with
type=ask_user_response:AskUserResultwithAskUserAnswerentriesclient.resolve_tools()withToolDecisionAction::Acceptandcontent = Some(serde_json::to_string(&result))5. Add
ask_userto safe auto-approve allowlistSince
ask_useris now handled interactively by the channel, it should be in the default allowlist so it doesn't block on approval. The tool is inherently safe — it only asks questions and returns the user's answers.Files to Modify
libs/gateway/src/dispatcher.rsask_userinconsume_run_events, addAskUserNeededoutcome, handle response callbackslibs/gateway/src/channels/mod.rssend_ask_usertrait methodlibs/gateway/src/channels/slack.rslibs/gateway/src/channels/telegram.rslibs/gateway/src/channels/discord.rslibs/gateway/src/types.rsInboundMessagemetadata type for ask_user responsesFallback for channels without interactive support
For channels that don't implement
send_ask_user(e.g., basic webhooks), the dispatcher should:ask_usertool with a message like "This channel does not support interactive questions. Please use a channel that supports buttons/interactive elements."Related Code References
tui/src/services/handlers/ask_user.rscli/src/commands/agent/run/mode_interactive.rs:145-180libs/mcp/server/src/local_tools.rs:3089-3099libs/shared/src/models/tools/ask_user.rslibs/gateway/src/dispatcher.rs:1410-1490libs/gateway/src/channels/slack.rs:728-750libs/gateway/src/channels/slack.rs:1023+