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 connection_cx: Arc<JrConnectionCx<AgentToClient>>,
64}
65
66impl PermissionManager {
67 pub fn new(connection_cx: Arc<JrConnectionCx<AgentToClient>>) -> Self {
69 let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
70
71 tokio::spawn(async move {
73 Self::handle_permission_requests(rx).await;
74 });
75
76 Self {
77 pending_requests: tx,
78 connection_cx,
79 }
80 }
81
82 pub fn request_permission(
89 &self,
90 tool_name: String,
91 tool_input: serde_json::Value,
92 tool_call_id: String,
93 session_id: String,
94 ) -> tokio::sync::oneshot::Receiver<PermissionManagerDecision> {
95 let (tx, rx) = tokio::sync::oneshot::channel();
96
97 let request = PendingPermissionRequest {
98 tool_name,
99 tool_input,
100 tool_call_id,
101 session_id,
102 response_tx: tx,
103 };
104
105 drop(self.pending_requests.send(request));
107
108 rx
109 }
110
111 async fn handle_permission_requests(
113 mut receiver: tokio::sync::mpsc::UnboundedReceiver<PendingPermissionRequest>,
114 ) {
115 while let Some(request) = receiver.recv().await {
116 tracing::info!(
117 tool_name = %request.tool_name,
118 tool_call_id = %request.tool_call_id,
119 "Processing permission request in background task"
120 );
121
122 let _ = request
133 .response_tx
134 .send(PermissionManagerDecision::Cancelled);
135
136 tracing::warn!(
137 tool_name = %request.tool_name,
138 "Permission request sent but interactive dialog not yet implemented"
139 );
140 }
141 }
142
143 async fn send_permission_request_to_client(
145 &self,
146 tool_name: &str,
147 tool_input: &serde_json::Value,
148 tool_call_id: &str,
149 session_id: &str,
150 ) -> Result<PermissionManagerDecision, AgentError> {
151 let options = vec![
153 PermissionOption::new(
154 PermissionOptionId::new("allow_always"),
155 "Always Allow",
156 PermissionOptionKind::AllowAlways,
157 ),
158 PermissionOption::new(
159 PermissionOptionId::new("allow_once"),
160 "Allow",
161 PermissionOptionKind::AllowOnce,
162 ),
163 PermissionOption::new(
164 PermissionOptionId::new("reject_once"),
165 "Reject",
166 PermissionOptionKind::RejectOnce,
167 ),
168 ];
169
170 let tool_call_update = ToolCallUpdate::new(
172 tool_call_id.to_string(),
173 ToolCallUpdateFields::new()
174 .title(&format_tool_title(tool_name, tool_input))
175 .raw_input(tool_input.clone()),
176 );
177
178 let request =
180 RequestPermissionRequest::new(SessionId::new(session_id), tool_call_update, options);
181
182 let response = self
184 .connection_cx
185 .send_request(request)
186 .block_task()
187 .await
188 .map_err(|e| AgentError::Internal(format!("Permission request failed: {}", e)))?;
189
190 Ok(parse_permission_response(response.outcome))
192 }
193}
194
195fn parse_permission_response(outcome: RequestPermissionOutcome) -> PermissionManagerDecision {
197 match outcome {
198 RequestPermissionOutcome::Selected(selected) => {
199 match selected.option_id.0.as_ref() {
200 "allow_always" => PermissionManagerDecision::AllowAlways,
201 "allow_once" => PermissionManagerDecision::AllowOnce,
202 "reject_once" => PermissionManagerDecision::Rejected,
203 _ => PermissionManagerDecision::Rejected, }
205 }
206 RequestPermissionOutcome::Cancelled => PermissionManagerDecision::Cancelled,
207 _ => PermissionManagerDecision::Cancelled,
209 }
210}
211
212fn format_tool_title(tool_name: &str, input: &serde_json::Value) -> String {
214 let display_name = tool_name.strip_prefix("mcp__acp__").unwrap_or(tool_name);
216
217 match display_name {
218 "Read" => {
219 let path = input
220 .get("file_path")
221 .and_then(|v| v.as_str())
222 .unwrap_or("file");
223 format!("Read {}", path)
224 }
225 "Write" => {
226 let path = input
227 .get("file_path")
228 .and_then(|v| v.as_str())
229 .unwrap_or("file");
230 format!("Write to {}", path)
231 }
232 "Edit" => {
233 let path = input
234 .get("file_path")
235 .and_then(|v| v.as_str())
236 .unwrap_or("file");
237 format!("Edit {}", path)
238 }
239 "Bash" => {
240 let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
241 let desc = input.get("description").and_then(|v| v.as_str());
242 desc.map(String::from)
243 .unwrap_or_else(|| format!("Run: {}", truncate_string(cmd, 50)))
244 }
245 _ => display_name.to_string(),
246 }
247}
248
249fn truncate_string(s: &str, max_len: usize) -> String {
251 if s.len() <= max_len {
252 s.to_string()
253 } else {
254 format!("{}...", &s[..max_len.saturating_sub(3)])
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_format_tool_title_read() {
264 let title = format_tool_title("Read", &serde_json::json!({"file_path": "/tmp/test.txt"}));
265 assert_eq!(title, "Read /tmp/test.txt");
266 }
267
268 #[test]
269 fn test_format_tool_title_edit() {
270 let title = format_tool_title("Edit", &serde_json::json!({"file_path": "/tmp/file.txt"}));
271 assert_eq!(title, "Edit /tmp/file.txt");
272 }
273
274 #[test]
275 fn test_format_tool_title_mcp_prefix() {
276 let title = format_tool_title(
277 "mcp__acp__Read",
278 &serde_json::json!({"file_path": "/tmp/test.txt"}),
279 );
280 assert_eq!(title, "Read /tmp/test.txt");
281 }
282
283 #[test]
284 fn test_truncate_string() {
285 assert_eq!(truncate_string("hello", 10), "hello");
286 assert_eq!(truncate_string("hello world", 8), "hello...");
287 assert_eq!(truncate_string("hi", 2), "hi");
288 }
289
290 #[test]
291 fn test_parse_permission_response_selected() {
292 let _ = parse_permission_response;
295 }
296}