From b93cf5dacf0bdb7e816487152e44412f8bc83073 Mon Sep 17 00:00:00 2001 From: Alex Netsch Date: Tue, 25 Nov 2025 17:25:40 +0000 Subject: [PATCH] Parse tool_use_id in canusetool control request (#1370) --- .../executors/src/executors/claude/client.rs | 64 +++++-------------- .../src/executors/claude/protocol.rs | 3 +- .../executors/src/executors/claude/types.rs | 2 + 3 files changed, 20 insertions(+), 49 deletions(-) diff --git a/crates/executors/src/executors/claude/client.rs b/crates/executors/src/executors/claude/client.rs index 97a9340e..b9884ff0 100644 --- a/crates/executors/src/executors/claude/client.rs +++ b/crates/executors/src/executors/claude/client.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use tokio::sync::Mutex; use workspace_utils::approvals::ApprovalStatus; use super::types::PermissionMode; @@ -26,7 +25,6 @@ pub struct ClaudeAgentClient { log_writer: LogWriter, approvals: Option>, auto_approve: bool, // true when approvals is None - latest_unhandled_tool_use_id: Mutex>, } impl ClaudeAgentClient { @@ -40,24 +38,8 @@ impl ClaudeAgentClient { log_writer, approvals, auto_approve, - latest_unhandled_tool_use_id: Mutex::new(None), }) } - async fn set_latest_unhandled_tool_use_id(&self, tool_use_id: String) { - if self.latest_unhandled_tool_use_id.lock().await.is_some() { - tracing::warn!( - "Overwriting unhandled tool_use_id: {} with new tool_use_id: {}", - self.latest_unhandled_tool_use_id - .lock() - .await - .as_ref() - .unwrap(), - tool_use_id - ); - } - let mut guard = self.latest_unhandled_tool_use_id.lock().await; - guard.replace(tool_use_id); - } async fn handle_approval( &self, @@ -133,34 +115,27 @@ impl ClaudeAgentClient { tool_name: String, input: serde_json::Value, _permission_suggestions: Option>, + tool_use_id: Option, ) -> Result { if self.auto_approve { Ok(PermissionResult::Allow { updated_input: input, updated_permissions: None, }) + } else if let Some(latest_tool_use_id) = tool_use_id { + self.handle_approval(latest_tool_use_id, tool_name, input) + .await } else { - let latest_tool_use_id = { - let guard = self.latest_unhandled_tool_use_id.lock().await.take(); - guard.clone() - }; - - if let Some(latest_tool_use_id) = latest_tool_use_id { - self.handle_approval(latest_tool_use_id, tool_name, input) - .await - } else { - // Auto approve tools with no matching tool_use_id. - // This rare edge case happens if a tool call triggers no hook callback, - // so no tool_use_id is available to match the approval request to. - tracing::warn!( - "No unhandled tool_use_id available for tool '{}', cannot request approval", - tool_name - ); - Ok(PermissionResult::Allow { - updated_input: input, - updated_permissions: None, - }) - } + // Auto approve tools with no matching tool_use_id + // tool_use_id is undocumented so this may not be possible + tracing::warn!( + "No tool_use_id available for tool '{}', cannot request approval", + tool_name + ); + Ok(PermissionResult::Allow { + updated_input: input, + updated_permissions: None, + }) } } @@ -168,7 +143,7 @@ impl ClaudeAgentClient { &self, _callback_id: String, _input: serde_json::Value, - tool_use_id: Option, + _tool_use_id: Option, ) -> Result { if self.auto_approve { Ok(serde_json::json!({ @@ -179,16 +154,9 @@ impl ClaudeAgentClient { } })) } else { - // Hook callbacks is only used to store tool_use_id for later approval request - // Both hook callback and can_use_tool are needed. - // - Hook callbacks have a constant 60s timeout, so cannot be used for long approvals - // - can_use_tool does not provide tool_use_id, so cannot be used alone - // Together they allow matching approval requests to tool uses. + // Hook callbacks is only used to forward approval requests to can_use_tool. // This works because `ask` decision in hook callback triggers a can_use_tool request // https://docs.claude.com/en/api/agent-sdk/permissions#permission-flow-diagram - if let Some(tool_use_id) = tool_use_id.clone() { - self.set_latest_unhandled_tool_use_id(tool_use_id).await; - } Ok(serde_json::json!({ "hookSpecificOutput": { "hookEventName": "PreToolUse", diff --git a/crates/executors/src/executors/claude/protocol.rs b/crates/executors/src/executors/claude/protocol.rs index 678e5574..8789b7c5 100644 --- a/crates/executors/src/executors/claude/protocol.rs +++ b/crates/executors/src/executors/claude/protocol.rs @@ -96,9 +96,10 @@ impl ProtocolPeer { tool_name, input, permission_suggestions, + tool_use_id, } => { match client - .on_can_use_tool(tool_name, input, permission_suggestions) + .on_can_use_tool(tool_name, input, permission_suggestions, tool_use_id) .await { Ok(result) => { diff --git a/crates/executors/src/executors/claude/types.rs b/crates/executors/src/executors/claude/types.rs index c4972d12..1e5802bf 100644 --- a/crates/executors/src/executors/claude/types.rs +++ b/crates/executors/src/executors/claude/types.rs @@ -67,6 +67,8 @@ pub enum ControlRequestType { input: Value, #[serde(skip_serializing_if = "Option::is_none")] permission_suggestions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + tool_use_id: Option, }, HookCallback { #[serde(rename = "callback_id")]