1use std::sync::Arc;
10
11use sacp::JrConnectionCx;
12use sacp::link::AgentToClient;
13use sacp::schema::{
14 PermissionOption, PermissionOptionId, PermissionOptionKind, RequestPermissionOutcome,
15 RequestPermissionRequest, SessionId, ToolCallUpdate, ToolCallUpdateFields,
16};
17
18use crate::types::AgentError;
19
20#[derive(Debug, Clone, PartialEq)]
22pub enum PermissionManagerDecision {
23 AllowOnce,
25 AllowAlways,
27 Rejected,
29 Cancelled,
31}
32
33pub struct PendingPermissionRequest {
35 pub tool_name: String,
36 pub tool_input: serde_json::Value,
37 pub tool_call_id: String,
38 pub session_id: String,
39 pub response_tx: tokio::sync::oneshot::Sender<PermissionManagerDecision>,
40}
41
42pub struct PermissionManager {
59 pending_requests: tokio::sync::mpsc::UnboundedSender<PendingPermissionRequest>,
61
62 #[allow(dead_code)]
66 connection_cx: Arc<JrConnectionCx<AgentToClient>>,
67}
68
69impl PermissionManager {
70 pub fn new(connection_cx: Arc<JrConnectionCx<AgentToClient>>) -> Self {
72 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
73
74 tokio::spawn(async move {
76 Self::handle_permission_requests(rx).await;
77 });
78
79 Self {
80 pending_requests: tx,
81 connection_cx,
82 }
83 }
84
85 pub fn request_permission(
92 &self,
93 tool_name: String,
94 tool_input: serde_json::Value,
95 tool_call_id: String,
96 session_id: String,
97 ) -> tokio::sync::oneshot::Receiver<PermissionManagerDecision> {
98 let (tx, rx) = tokio::sync::oneshot::channel();
99
100 let request = PendingPermissionRequest {
101 tool_name,
102 tool_input,
103 tool_call_id,
104 session_id,
105 response_tx: tx,
106 };
107
108 drop(self.pending_requests.send(request));
110
111 rx
112 }
113
114 async fn handle_permission_requests(
116 mut receiver: tokio::sync::mpsc::UnboundedReceiver<PendingPermissionRequest>,
117 ) {
118 while let Some(request) = receiver.recv().await {
119 tracing::info!(
120 tool_name = %request.tool_name,
121 tool_call_id = %request.tool_call_id,
122 "Processing permission request in background task"
123 );
124
125 let _ = request
136 .response_tx
137 .send(PermissionManagerDecision::Cancelled);
138
139 tracing::warn!(
140 tool_name = %request.tool_name,
141 "Permission request sent but interactive dialog not yet implemented"
142 );
143 }
144 }
145
146 #[allow(dead_code)]
151 async fn send_permission_request_to_client(
152 &self,
153 tool_name: &str,
154 tool_input: &serde_json::Value,
155 tool_call_id: &str,
156 session_id: &str,
157 ) -> Result<PermissionManagerDecision, AgentError> {
158 let options = vec![
160 PermissionOption::new(
161 PermissionOptionId::new("allow_always"),
162 "Always Allow",
163 PermissionOptionKind::AllowAlways,
164 ),
165 PermissionOption::new(
166 PermissionOptionId::new("allow_once"),
167 "Allow",
168 PermissionOptionKind::AllowOnce,
169 ),
170 PermissionOption::new(
171 PermissionOptionId::new("reject_once"),
172 "Reject",
173 PermissionOptionKind::RejectOnce,
174 ),
175 ];
176
177 let tool_call_update = ToolCallUpdate::new(
179 tool_call_id.to_string(),
180 ToolCallUpdateFields::new()
181 .title(&format_tool_title(tool_name, tool_input))
182 .raw_input(tool_input.clone()),
183 );
184
185 let request =
187 RequestPermissionRequest::new(SessionId::new(session_id), tool_call_update, options);
188
189 let response = self
191 .connection_cx
192 .send_request(request)
193 .block_task()
194 .await
195 .map_err(|e| AgentError::Internal(format!("Permission request failed: {}", e)))?;
196
197 Ok(parse_permission_response(response.outcome))
199 }
200}
201
202#[allow(dead_code)]
206fn parse_permission_response(outcome: RequestPermissionOutcome) -> PermissionManagerDecision {
207 match outcome {
208 RequestPermissionOutcome::Selected(selected) => {
209 match selected.option_id.0.as_ref() {
210 "allow_always" => PermissionManagerDecision::AllowAlways,
211 "allow_once" => PermissionManagerDecision::AllowOnce,
212 "reject_once" => PermissionManagerDecision::Rejected,
213 _ => PermissionManagerDecision::Rejected, }
215 }
216 RequestPermissionOutcome::Cancelled => PermissionManagerDecision::Cancelled,
217 _ => PermissionManagerDecision::Cancelled,
219 }
220}
221
222#[allow(dead_code)]
226fn format_tool_title(tool_name: &str, input: &serde_json::Value) -> String {
227 let display_name = tool_name.strip_prefix("mcp__acp__").unwrap_or(tool_name);
229
230 match display_name {
231 "Read" => {
232 let path = input
233 .get("file_path")
234 .and_then(|v| v.as_str())
235 .unwrap_or("file");
236 format!("Read {}", path)
237 }
238 "Write" => {
239 let path = input
240 .get("file_path")
241 .and_then(|v| v.as_str())
242 .unwrap_or("file");
243 format!("Write to {}", path)
244 }
245 "Edit" => {
246 let path = input
247 .get("file_path")
248 .and_then(|v| v.as_str())
249 .unwrap_or("file");
250 format!("Edit {}", path)
251 }
252 "Bash" => {
253 let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
254 let desc = input.get("description").and_then(|v| v.as_str());
255 desc.map(String::from)
256 .unwrap_or_else(|| format!("Run: {}", truncate_string(cmd, 50)))
257 }
258 _ => display_name.to_string(),
259 }
260}
261
262#[allow(dead_code)]
266fn truncate_string(s: &str, max_len: usize) -> String {
267 if s.len() <= max_len {
268 s.to_string()
269 } else {
270 format!("{}...", &s[..max_len.saturating_sub(3)])
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
279 fn test_format_tool_title_read() {
280 let title = format_tool_title("Read", &serde_json::json!({"file_path": "/tmp/test.txt"}));
281 assert_eq!(title, "Read /tmp/test.txt");
282 }
283
284 #[test]
285 fn test_format_tool_title_edit() {
286 let title = format_tool_title("Edit", &serde_json::json!({"file_path": "/tmp/file.txt"}));
287 assert_eq!(title, "Edit /tmp/file.txt");
288 }
289
290 #[test]
291 fn test_format_tool_title_mcp_prefix() {
292 let title = format_tool_title(
293 "mcp__acp__Read",
294 &serde_json::json!({"file_path": "/tmp/test.txt"}),
295 );
296 assert_eq!(title, "Read /tmp/test.txt");
297 }
298
299 #[test]
300 fn test_truncate_string() {
301 assert_eq!(truncate_string("hello", 10), "hello");
302 assert_eq!(truncate_string("hello world", 8), "hello...");
303 assert_eq!(truncate_string("hi", 2), "hi");
304 }
305
306 #[test]
307 fn test_parse_permission_response_selected() {
308 let _ = parse_permission_response;
311 }
312}