permission_tool.rs

  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}