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
42impl std::fmt::Debug for PendingPermissionRequest {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("PendingPermissionRequest")
45 .field("tool_name", &self.tool_name)
46 .field("tool_input", &self.tool_input)
47 .field("tool_call_id", &self.tool_call_id)
48 .field("session_id", &self.session_id)
49 .field("response_tx", &"<oneshot::Sender>")
50 .finish()
51 }
52}
53
54pub struct PermissionManager {
71 pending_requests: tokio::sync::mpsc::UnboundedSender<PendingPermissionRequest>,
73
74 #[allow(dead_code)]
78 connection_cx: Arc<JrConnectionCx<AgentToClient>>,
79}
80
81impl std::fmt::Debug for PermissionManager {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("PermissionManager")
84 .field("pending_requests", &"<mpsc::UnboundedSender>")
85 .field("connection_cx", &"<JrConnectionCx>")
86 .finish()
87 }
88}
89
90impl PermissionManager {
91 pub fn new(connection_cx: Arc<JrConnectionCx<AgentToClient>>) -> Self {
93 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
94
95 tokio::spawn(async move {
97 Self::handle_permission_requests(rx).await;
98 });
99
100 Self {
101 pending_requests: tx,
102 connection_cx,
103 }
104 }
105
106 pub fn request_permission(
113 &self,
114 tool_name: String,
115 tool_input: serde_json::Value,
116 tool_call_id: String,
117 session_id: String,
118 ) -> tokio::sync::oneshot::Receiver<PermissionManagerDecision> {
119 let (tx, rx) = tokio::sync::oneshot::channel();
120
121 let request = PendingPermissionRequest {
122 tool_name,
123 tool_input,
124 tool_call_id,
125 session_id,
126 response_tx: tx,
127 };
128
129 drop(self.pending_requests.send(request));
131
132 rx
133 }
134
135 async fn handle_permission_requests(
137 mut receiver: tokio::sync::mpsc::UnboundedReceiver<PendingPermissionRequest>,
138 ) {
139 while let Some(request) = receiver.recv().await {
140 tracing::info!(
141 tool_name = %request.tool_name,
142 tool_call_id = %request.tool_call_id,
143 "Processing permission request in background task"
144 );
145
146 let _ = request
157 .response_tx
158 .send(PermissionManagerDecision::Cancelled);
159
160 tracing::warn!(
161 tool_name = %request.tool_name,
162 "Permission request sent but interactive dialog not yet implemented"
163 );
164 }
165 }
166
167 #[allow(dead_code)]
172 async fn send_permission_request_to_client(
173 &self,
174 tool_name: &str,
175 tool_input: &serde_json::Value,
176 tool_call_id: &str,
177 session_id: &str,
178 ) -> Result<PermissionManagerDecision, AgentError> {
179 let options = vec![
181 PermissionOption::new(
182 PermissionOptionId::new("allow_always"),
183 "Always Allow",
184 PermissionOptionKind::AllowAlways,
185 ),
186 PermissionOption::new(
187 PermissionOptionId::new("allow_once"),
188 "Allow",
189 PermissionOptionKind::AllowOnce,
190 ),
191 PermissionOption::new(
192 PermissionOptionId::new("reject_once"),
193 "Reject",
194 PermissionOptionKind::RejectOnce,
195 ),
196 ];
197
198 let tool_call_update = ToolCallUpdate::new(
200 tool_call_id.to_string(),
201 ToolCallUpdateFields::new()
202 .title(format_tool_title(tool_name, tool_input))
203 .raw_input(tool_input.clone()),
204 );
205
206 let request =
208 RequestPermissionRequest::new(SessionId::new(session_id), tool_call_update, options);
209
210 let response = self
212 .connection_cx
213 .send_request(request)
214 .block_task()
215 .await
216 .map_err(|e| AgentError::Internal(format!("Permission request failed: {}", e)))?;
217
218 Ok(parse_permission_response(response.outcome))
220 }
221}
222
223#[allow(dead_code)]
227fn parse_permission_response(outcome: RequestPermissionOutcome) -> PermissionManagerDecision {
228 match outcome {
229 RequestPermissionOutcome::Selected(selected) => {
230 match selected.option_id.0.as_ref() {
231 "allow_always" => PermissionManagerDecision::AllowAlways,
232 "allow_once" => PermissionManagerDecision::AllowOnce,
233 "reject_once" => PermissionManagerDecision::Rejected,
234 _ => PermissionManagerDecision::Rejected, }
236 }
237 RequestPermissionOutcome::Cancelled => PermissionManagerDecision::Cancelled,
238 _ => PermissionManagerDecision::Cancelled,
240 }
241}
242
243#[allow(dead_code)]
247fn format_tool_title(tool_name: &str, input: &serde_json::Value) -> String {
248 let display_name = tool_name.strip_prefix("mcp__acp__").unwrap_or(tool_name);
250
251 match display_name {
252 "Read" => {
253 let path = input
254 .get("file_path")
255 .and_then(|v| v.as_str())
256 .unwrap_or("file");
257 format!("Read {}", path)
258 }
259 "Write" => {
260 let path = input
261 .get("file_path")
262 .and_then(|v| v.as_str())
263 .unwrap_or("file");
264 format!("Write to {}", path)
265 }
266 "Edit" => {
267 let path = input
268 .get("file_path")
269 .and_then(|v| v.as_str())
270 .unwrap_or("file");
271 format!("Edit {}", path)
272 }
273 "Bash" => {
274 let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
275 let desc = input.get("description").and_then(|v| v.as_str());
276 desc.map(String::from)
277 .unwrap_or_else(|| format!("Run: {}", truncate_string(cmd, 50)))
278 }
279 _ => display_name.to_string(),
280 }
281}
282
283#[allow(dead_code)]
287fn truncate_string(s: &str, max_len: usize) -> String {
288 if s.len() <= max_len {
289 s.to_string()
290 } else {
291 format!("{}...", &s[..max_len.saturating_sub(3)])
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn test_format_tool_title_read() {
301 let title = format_tool_title("Read", &serde_json::json!({"file_path": "/tmp/test.txt"}));
302 assert_eq!(title, "Read /tmp/test.txt");
303 }
304
305 #[test]
306 fn test_format_tool_title_edit() {
307 let title = format_tool_title("Edit", &serde_json::json!({"file_path": "/tmp/file.txt"}));
308 assert_eq!(title, "Edit /tmp/file.txt");
309 }
310
311 #[test]
312 fn test_format_tool_title_mcp_prefix() {
313 let title = format_tool_title(
314 "mcp__acp__Read",
315 &serde_json::json!({"file_path": "/tmp/test.txt"}),
316 );
317 assert_eq!(title, "Read /tmp/test.txt");
318 }
319
320 #[test]
321 fn test_truncate_string() {
322 assert_eq!(truncate_string("hello", 10), "hello");
323 assert_eq!(truncate_string("hello world", 8), "hello...");
324 assert_eq!(truncate_string("hi", 2), "hi");
325 }
326
327 #[test]
328 fn test_parse_permission_response_selected() {
329 let _ = parse_permission_response;
332 }
333}