1use sacp::JrConnectionCx;
7use sacp::link::AgentToClient;
8use sacp::schema::{
9 PermissionOption, PermissionOptionId, PermissionOptionKind, RequestPermissionOutcome,
10 RequestPermissionRequest, SessionId, ToolCallUpdate, ToolCallUpdateFields,
11};
12
13use crate::types::AgentError;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum PermissionOutcome {
18 AllowOnce,
20 AllowAlways,
22 Rejected,
24 Cancelled,
26}
27
28#[derive(Debug)]
30pub struct PermissionRequestBuilder {
31 session_id: String,
32 tool_call_id: String,
33 title: String,
34 tool_name: String,
35 tool_input: serde_json::Value,
36}
37
38impl PermissionRequestBuilder {
39 pub fn new(
41 session_id: impl Into<String>,
42 tool_call_id: impl Into<String>,
43 tool_name: impl Into<String>,
44 tool_input: serde_json::Value,
45 ) -> Self {
46 let tool_name_str: String = tool_name.into();
47 let title = format_tool_title(&tool_name_str, &tool_input);
48 Self {
49 session_id: session_id.into(),
50 tool_call_id: tool_call_id.into(),
51 title,
52 tool_name: tool_name_str,
53 tool_input,
54 }
55 }
56
57 pub fn title(mut self, title: impl Into<String>) -> Self {
59 self.title = title.into();
60 self
61 }
62
63 pub async fn request(
67 self,
68 connection_cx: &JrConnectionCx<AgentToClient>,
69 ) -> Result<PermissionOutcome, AgentError> {
70 let options = vec![
72 PermissionOption::new(
73 PermissionOptionId::new("allow_always"),
74 "Always Allow",
75 PermissionOptionKind::AllowAlways,
76 ),
77 PermissionOption::new(
78 PermissionOptionId::new("allow_once"),
79 "Allow",
80 PermissionOptionKind::AllowOnce,
81 ),
82 PermissionOption::new(
83 PermissionOptionId::new("reject_once"),
84 "Reject",
85 PermissionOptionKind::RejectOnce,
86 ),
87 ];
88
89 let tool_call_update = ToolCallUpdate::new(
91 self.tool_call_id.clone(),
92 ToolCallUpdateFields::new()
93 .title(&self.title)
94 .raw_input(self.tool_input.clone()),
95 );
96
97 tracing::debug!(
99 tool_call_id = %self.tool_call_id,
100 title = %self.title,
101 tool_name = %self.tool_name,
102 "Building permission request with ToolCallUpdate"
103 );
104
105 let request = RequestPermissionRequest::new(
107 SessionId::new(self.session_id.clone()),
108 tool_call_update,
109 options,
110 );
111
112 if let Ok(json) = serde_json::to_string_pretty(&request) {
114 tracing::trace!(
115 session_id = %self.session_id,
116 request_json = %json,
117 "Sending session/request_permission"
118 );
119 }
120
121 tracing::info!(
123 tool_call_id = %self.tool_call_id,
124 session_id = %self.session_id,
125 "Sending permission request, waiting for user response..."
126 );
127
128 let response = connection_cx
129 .send_request(request)
130 .block_task()
131 .await
132 .map_err(|e| {
133 tracing::error!(
134 tool_call_id = %self.tool_call_id,
135 error = %e,
136 "Permission request failed"
137 );
138 AgentError::Internal(format!("Permission request failed: {}", e))
139 })?;
140
141 tracing::info!(
142 tool_call_id = %self.tool_call_id,
143 "Received permission response"
144 );
145
146 Ok(parse_permission_response(response.outcome))
148 }
149
150 pub fn tool_name(&self) -> &str {
152 &self.tool_name
153 }
154}
155
156fn parse_permission_response(outcome: RequestPermissionOutcome) -> PermissionOutcome {
158 match outcome {
159 RequestPermissionOutcome::Selected(selected) => {
160 match selected.option_id.0.as_ref() {
161 "allow_always" => PermissionOutcome::AllowAlways,
162 "allow_once" => PermissionOutcome::AllowOnce,
163 "reject_once" => PermissionOutcome::Rejected,
164 _ => PermissionOutcome::Rejected, }
166 }
167 RequestPermissionOutcome::Cancelled => PermissionOutcome::Cancelled,
168 _ => PermissionOutcome::Cancelled,
170 }
171}
172
173fn format_tool_title(tool_name: &str, input: &serde_json::Value) -> String {
175 let stripped_name = tool_name.strip_prefix("mcp__acp__").unwrap_or(tool_name);
177
178 match stripped_name {
179 "Read" => {
180 let path = input
181 .get("file_path")
182 .and_then(|v| v.as_str())
183 .unwrap_or("file");
184 format!("Read {}", path)
185 }
186 "Write" => {
187 let path = input
188 .get("file_path")
189 .and_then(|v| v.as_str())
190 .unwrap_or("file");
191 format!("Write to {}", path)
192 }
193 "Edit" => {
194 let path = input
195 .get("file_path")
196 .and_then(|v| v.as_str())
197 .unwrap_or("file");
198 format!("Edit {}", path)
199 }
200 "Bash" => {
201 let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
202 let desc = input.get("description").and_then(|v| v.as_str());
203 desc.map(String::from)
204 .unwrap_or_else(|| format!("Run: {}", truncate_string(cmd, 50)))
205 }
206 "Grep" => {
207 let pattern = input.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
208 format!("Search: {}", pattern)
209 }
210 "Glob" => {
211 let pattern = input.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
212 format!("Find files: {}", pattern)
213 }
214 _ => stripped_name.to_string(),
215 }
216}
217
218fn truncate_string(s: &str, max_len: usize) -> String {
220 if s.len() <= max_len {
221 s.to_string()
222 } else {
223 format!("{}...", &s[..max_len.saturating_sub(3)])
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use sacp::schema::SelectedPermissionOutcome;
231 use serde_json::json;
232
233 #[test]
234 fn test_format_tool_title_read() {
235 let title = format_tool_title("Read", &json!({"file_path": "/tmp/test.txt"}));
236 assert_eq!(title, "Read /tmp/test.txt");
237 }
238
239 #[test]
240 fn test_format_tool_title_bash() {
241 let title = format_tool_title("Bash", &json!({"command": "ls -la"}));
242 assert_eq!(title, "Run: ls -la");
243
244 let title = format_tool_title(
245 "Bash",
246 &json!({"command": "ls -la", "description": "List files"}),
247 );
248 assert_eq!(title, "List files");
249 }
250
251 #[test]
252 fn test_format_tool_title_long_command() {
253 let long_cmd = "echo 'this is a very long command that should be truncated'";
254 let title = format_tool_title("Bash", &json!({"command": long_cmd}));
255 assert!(title.len() <= 60); assert!(title.ends_with("..."));
257 }
258
259 #[test]
260 fn test_truncate_string() {
261 assert_eq!(truncate_string("hello", 10), "hello");
262 assert_eq!(truncate_string("hello world", 8), "hello...");
263 assert_eq!(truncate_string("hi", 2), "hi");
264 }
265
266 #[test]
267 fn test_permission_outcome_selected() {
268 let selected_always = RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(
270 PermissionOptionId::new("allow_always"),
271 ));
272 assert_eq!(
273 parse_permission_response(selected_always),
274 PermissionOutcome::AllowAlways
275 );
276
277 let selected_once = RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(
278 PermissionOptionId::new("allow_once"),
279 ));
280 assert_eq!(
281 parse_permission_response(selected_once),
282 PermissionOutcome::AllowOnce
283 );
284
285 let selected_reject = RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(
286 PermissionOptionId::new("reject_once"),
287 ));
288 assert_eq!(
289 parse_permission_response(selected_reject),
290 PermissionOutcome::Rejected
291 );
292 }
293
294 #[test]
295 fn test_permission_outcome_cancelled() {
296 let cancelled = RequestPermissionOutcome::Cancelled;
297 assert_eq!(
298 parse_permission_response(cancelled),
299 PermissionOutcome::Cancelled
300 );
301 }
302
303 #[test]
304 fn test_permission_outcome_unknown() {
305 let unknown = RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(
307 PermissionOptionId::new("unknown_option"),
308 ));
309 assert_eq!(
310 parse_permission_response(unknown),
311 PermissionOutcome::Rejected
312 );
313 }
314}