1use std::sync::Arc;
2
3use acp_thread::AcpThread;
4use agent_client_protocol as acp;
5use agent_settings::AgentSettings;
6use anyhow::{Context as _, Result};
7use context_server::{
8 listener::{McpServerTool, ToolResponse},
9 types::ToolResponseContent,
10};
11use gpui::{AsyncApp, WeakEntity};
12use project::Fs;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use settings::{Settings as _, update_settings_file};
16use util::debug_panic;
17
18use crate::tools::ClaudeTool;
19
20#[derive(Clone)]
21pub struct PermissionTool {
22 fs: Arc<dyn Fs>,
23 thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
24}
25
26/// Request permission for tool calls
27#[derive(Deserialize, JsonSchema, Debug)]
28pub struct PermissionToolParams {
29 tool_name: String,
30 input: serde_json::Value,
31 tool_use_id: Option<String>,
32}
33
34#[derive(Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct PermissionToolResponse {
37 behavior: PermissionToolBehavior,
38 updated_input: serde_json::Value,
39}
40
41#[derive(Serialize)]
42#[serde(rename_all = "snake_case")]
43enum PermissionToolBehavior {
44 Allow,
45 Deny,
46}
47
48impl PermissionTool {
49 pub fn new(fs: Arc<dyn Fs>, thread_rx: watch::Receiver<WeakEntity<AcpThread>>) -> Self {
50 Self { fs, thread_rx }
51 }
52}
53
54impl McpServerTool for PermissionTool {
55 type Input = PermissionToolParams;
56 type Output = ();
57
58 const NAME: &'static str = "Confirmation";
59
60 async fn run(
61 &self,
62 input: Self::Input,
63 cx: &mut AsyncApp,
64 ) -> Result<ToolResponse<Self::Output>> {
65 if agent_settings::AgentSettings::try_read_global(cx, |settings| {
66 settings.always_allow_tool_actions
67 })
68 .unwrap_or(false)
69 {
70 let response = PermissionToolResponse {
71 behavior: PermissionToolBehavior::Allow,
72 updated_input: input.input,
73 };
74
75 return Ok(ToolResponse {
76 content: vec![ToolResponseContent::Text {
77 text: serde_json::to_string(&response)?,
78 }],
79 structured_content: (),
80 });
81 }
82
83 let mut thread_rx = self.thread_rx.clone();
84 let Some(thread) = thread_rx.recv().await?.upgrade() else {
85 anyhow::bail!("Thread closed");
86 };
87
88 let claude_tool = ClaudeTool::infer(&input.tool_name, input.input.clone());
89 let tool_call_id = acp::ToolCallId(input.tool_use_id.context("Tool ID required")?.into());
90
91 const ALWAYS_ALLOW: &str = "always_allow";
92 const ALLOW: &str = "allow";
93 const REJECT: &str = "reject";
94
95 let chosen_option = thread
96 .update(cx, |thread, cx| {
97 thread.request_tool_call_authorization(
98 claude_tool.as_acp(tool_call_id).into(),
99 vec![
100 acp::PermissionOption {
101 id: acp::PermissionOptionId(ALWAYS_ALLOW.into()),
102 name: "Always Allow".into(),
103 kind: acp::PermissionOptionKind::AllowAlways,
104 },
105 acp::PermissionOption {
106 id: acp::PermissionOptionId(ALLOW.into()),
107 name: "Allow".into(),
108 kind: acp::PermissionOptionKind::AllowOnce,
109 },
110 acp::PermissionOption {
111 id: acp::PermissionOptionId(REJECT.into()),
112 name: "Reject".into(),
113 kind: acp::PermissionOptionKind::RejectOnce,
114 },
115 ],
116 cx,
117 )
118 })??
119 .await?;
120
121 let response = match chosen_option.0.as_ref() {
122 ALWAYS_ALLOW => {
123 cx.update(|cx| {
124 update_settings_file::<AgentSettings>(self.fs.clone(), cx, |settings, _| {
125 settings.set_always_allow_tool_actions(true);
126 });
127 })?;
128
129 PermissionToolResponse {
130 behavior: PermissionToolBehavior::Allow,
131 updated_input: input.input,
132 }
133 }
134 ALLOW => PermissionToolResponse {
135 behavior: PermissionToolBehavior::Allow,
136 updated_input: input.input,
137 },
138 REJECT => PermissionToolResponse {
139 behavior: PermissionToolBehavior::Deny,
140 updated_input: input.input,
141 },
142 opt => {
143 debug_panic!("Unexpected option: {}", opt);
144 PermissionToolResponse {
145 behavior: PermissionToolBehavior::Deny,
146 updated_input: input.input,
147 }
148 }
149 };
150
151 Ok(ToolResponse {
152 content: vec![ToolResponseContent::Text {
153 text: serde_json::to_string(&response)?,
154 }],
155 structured_content: (),
156 })
157 }
158}