diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index fac69445e..9c97a201f 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -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), diff --git a/src/approval/mod.rs b/src/approval/mod.rs index bb4076eed..3c49af902 100644 --- a/src/approval/mod.rs +++ b/src/approval/mod.rs @@ -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) }