fix(security): deny tool calls requiring approval on remote channels

Remote channels (Telegram, Discord, Slack, etc.) previously either
auto-approved tool calls requiring approval (in the else-branch) or
bypassed the approval check entirely (by passing None for the
ApprovalManager). Both paths allowed unapproved tool execution.

This fix:
- Wires ApprovalManager into ChannelRuntimeContext so remote channels
  actually enter the approval check
- Changes the non-CLI branch from auto-approve to deny-by-default
- Adds a tracing::warn log and descriptive error message guiding users
  to configure auto_approve or set autonomy level to Full
- Updates stale doc comment on prompt_cli

Co-authored-by: xj <gh-xj@users.noreply.github.com>
This commit is contained in:
argenis de la rosa 2026-02-28 17:27:45 -05:00
parent 276c470c1f
commit 470af7051c
2 changed files with 19 additions and 3 deletions

View File

@ -1308,6 +1308,8 @@ pub(crate) async fn run_tool_call_loop(
arguments: tool_args.clone(),
};
// Only prompt interactively on CLI; deny on other channels
// (remote channels cannot provide interactive approval).
let decision = if channel_name == "cli" {
mgr.prompt_cli(&request)
} else if let Some(ctx) = non_cli_approval_context.as_ref() {
@ -1338,13 +1340,26 @@ pub(crate) async fn run_tool_call_loop(
)
.await
} else {
tracing::warn!(
tool = %tool_name,
channel = %channel_name,
"Tool requires approval but channel cannot prompt — denied"
);
ApprovalResponse::No
};
mgr.record_decision(&tool_name, &tool_args, decision, channel_name);
if decision == ApprovalResponse::No {
let denied = "Denied by user.".to_string();
let denied = if channel_name == "cli" {
"Denied by user.".to_string()
} else {
format!(
"Tool '{}' requires approval but channel '{}' cannot prompt interactively. \
Configure auto_approve or set autonomy level to Full to allow.",
tool_name, channel_name
)
};
runtime_trace::record_event(
"tool_call_result",
Some(channel_name),

View File

@ -581,8 +581,9 @@ impl ApprovalManager {
/// Prompt the user on the CLI and return their decision.
///
/// For non-CLI channels, returns `Yes` automatically (interactive
/// approval is only supported on CLI for now).
/// Only valid for the CLI channel. Non-CLI channels should not call
/// this method; the caller in `run_tool_call_loop` denies by default
/// when the channel cannot provide interactive approval.
pub fn prompt_cli(&self, request: &ApprovalRequest) -> ApprovalResponse {
prompt_cli_interactive(request)
}