1use std::collections::HashMap;
23use std::sync::Arc;
24use std::sync::Mutex as StdMutex;
25use std::time::Duration;
26use tokio::sync::oneshot;
27
28use async_trait::async_trait;
29use serde_json::Value;
30
31use crate::agent::PermissionDecision;
32use crate::tools::PermissionHook;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum PermissionOutcome {
37 Allowed,
39 Denied,
41 Timeout,
43}
44
45pub fn translate(outcome: PermissionOutcome) -> PermissionDecision {
47 match outcome {
48 PermissionOutcome::Allowed => PermissionDecision::Allow,
49 PermissionOutcome::Denied | PermissionOutcome::Timeout => {
50 PermissionDecision::Deny("permission denied".to_string())
51 }
52 }
53}
54
55#[derive(Clone, Default)]
65pub struct PendingPermissionStore {
66 inner: Arc<StdMutex<HashMap<String, oneshot::Sender<PermissionOutcome>>>>,
67}
68
69impl PendingPermissionStore {
70 pub fn new() -> Self {
72 Self {
73 inner: Arc::new(StdMutex::new(HashMap::new())),
74 }
75 }
76
77 pub fn insert(&self, id: String, tx: oneshot::Sender<PermissionOutcome>) {
79 if let Ok(mut guard) = self.inner.lock() {
80 guard.insert(id, tx);
81 }
82 }
83
84 pub fn remove(&self, id: &str) -> Option<oneshot::Sender<PermissionOutcome>> {
86 self.inner
87 .lock()
88 .ok()
89 .and_then(|mut guard| guard.remove(id))
90 }
91
92 #[allow(dead_code)]
94 pub fn contains(&self, id: &str) -> bool {
95 self.inner
96 .lock()
97 .ok()
98 .is_some_and(|guard| guard.contains_key(id))
99 }
100}
101
102pub struct AcpPermissionHook {
115 store: PendingPermissionStore,
117 session_id: String,
119 notif_tx: tokio::sync::mpsc::UnboundedSender<Value>,
121}
122
123impl AcpPermissionHook {
124 pub fn new(
126 store: PendingPermissionStore,
127 session_id: String,
128 notif_tx: tokio::sync::mpsc::UnboundedSender<Value>,
129 ) -> Self {
130 Self {
131 store,
132 session_id,
133 notif_tx,
134 }
135 }
136}
137
138#[async_trait]
139impl PermissionHook for AcpPermissionHook {
140 async fn check(&self, tool_name: &str, args: &Value) -> PermissionDecision {
141 let perm_id = uuid::Uuid::new_v4().to_string();
142 let (tx, rx) = oneshot::channel();
143
144 self.store.insert(perm_id.clone(), tx);
145
146 let notif = serde_json::json!({
148 "jsonrpc": "2.0",
149 "method": "session/update",
150 "params": {
151 "sessionId": self.session_id,
152 "update": {
153 "sessionUpdate": "request_permission",
154 "permissionId": perm_id,
155 "toolName": tool_name,
156 "arguments": args,
157 }
158 }
159 });
160
161 let _ = self.notif_tx.send(notif);
162
163 tokio::time::timeout(Duration::from_secs(30), rx)
165 .await
166 .ok()
167 .and_then(|r| r.ok())
168 .map(translate)
169 .unwrap_or(PermissionDecision::Deny("permission timeout".to_string()))
170 }
171}
172
173pub fn handle_request_permission(
185 id: &Value,
186 params: Option<&Value>,
187 store: &PendingPermissionStore,
188) -> Value {
189 let params = match params {
190 Some(p) => p,
191 None => {
192 return serde_json::json!({
193 "jsonrpc": "2.0",
194 "id": id,
195 "error": {
196 "code": -32602,
197 "message": "Missing params",
198 },
199 });
200 }
201 };
202
203 let perm_id = match params.get("permissionId").and_then(|v| v.as_str()) {
204 Some(s) => s,
205 None => {
206 return serde_json::json!({
207 "jsonrpc": "2.0",
208 "id": id,
209 "error": {
210 "code": -32602,
211 "message": "Missing required field 'permissionId'",
212 },
213 });
214 }
215 };
216
217 let outcome = match params.get("outcome").and_then(|v| v.as_str()) {
218 Some("allowed") => PermissionOutcome::Allowed,
219 Some("denied") | Some("rejected") => PermissionOutcome::Denied,
220 _ => {
221 return serde_json::json!({
222 "jsonrpc": "2.0",
223 "id": id,
224 "error": {
225 "code": -32602,
226 "message": "Invalid or missing 'outcome' field (expected 'allowed' or 'denied')",
227 },
228 });
229 }
230 };
231
232 match store.remove(perm_id) {
233 Some(tx) => {
234 let _ = tx.send(outcome);
235 serde_json::json!({
236 "jsonrpc": "2.0",
237 "id": id,
238 "result": {},
239 })
240 }
241 None => {
242 serde_json::json!({
243 "jsonrpc": "2.0",
244 "id": id,
245 "error": {
246 "code": -32000,
247 "message": format!("Permission request not found: {perm_id}"),
248 },
249 })
250 }
251 }
252}
253
254#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
265 fn translate_allowed_to_allow() {
266 assert_eq!(
267 translate(PermissionOutcome::Allowed),
268 PermissionDecision::Allow
269 );
270 }
271
272 #[test]
273 fn translate_denied_to_deny() {
274 assert_eq!(
275 translate(PermissionOutcome::Denied),
276 PermissionDecision::Deny("permission denied".to_string())
277 );
278 }
279
280 #[test]
281 fn translate_timeout_to_deny() {
282 assert_eq!(
283 translate(PermissionOutcome::Timeout),
284 PermissionDecision::Deny("permission denied".to_string())
285 );
286 }
287
288 #[test]
291 fn store_insert_and_remove() {
292 let store = PendingPermissionStore::new();
293 let (tx, _rx) = oneshot::channel();
294 store.insert("perm-1".into(), tx);
295 assert!(store.contains("perm-1"));
296 let removed = store.remove("perm-1");
297 assert!(removed.is_some());
298 assert!(!store.contains("perm-1"));
299 }
300
301 #[test]
302 fn store_remove_nonexistent_returns_none() {
303 let store = PendingPermissionStore::new();
304 assert!(store.remove("nonexistent").is_none());
305 }
306
307 #[test]
308 fn store_default_is_empty() {
309 let store = PendingPermissionStore::default();
310 assert!(!store.contains("anything"));
311 }
312
313 #[test]
316 fn handle_permission_missing_params() {
317 let store = PendingPermissionStore::new();
318 let result = handle_request_permission(&Value::from(1), None, &store);
319 assert!(result["error"].is_object());
320 assert_eq!(result["error"]["code"], -32602);
321 }
322
323 #[test]
324 fn handle_permission_missing_permission_id() {
325 let store = PendingPermissionStore::new();
326 let params = serde_json::json!({"outcome": "allowed"});
327 let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
328 assert!(result["error"].is_object());
329 assert_eq!(result["error"]["code"], -32602);
330 }
331
332 #[test]
333 fn handle_permission_invalid_outcome() {
334 let store = PendingPermissionStore::new();
335 let params = serde_json::json!({
336 "permissionId": "p1",
337 "outcome": "maybe"
338 });
339 let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
340 assert!(result["error"].is_object());
341 assert_eq!(result["error"]["code"], -32602);
342 }
343
344 #[test]
345 fn handle_permission_nonexistent_id() {
346 let store = PendingPermissionStore::new();
347 let params = serde_json::json!({
348 "permissionId": "nonexistent",
349 "outcome": "allowed"
350 });
351 let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
352 assert!(result["error"].is_object());
353 assert_eq!(result["error"]["code"], -32000);
354 }
355
356 #[test]
357 fn handle_permission_allowed_resolves() {
358 let store = PendingPermissionStore::new();
359 let (tx, mut rx) = oneshot::channel();
360 store.insert("p1".into(), tx);
361
362 let params = serde_json::json!({
363 "permissionId": "p1",
364 "outcome": "allowed"
365 });
366 let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
367 assert!(result["result"].is_object());
368 let outcome = rx.try_recv().unwrap();
370 assert_eq!(outcome, PermissionOutcome::Allowed);
371 }
372
373 #[test]
374 fn handle_permission_denied_resolves() {
375 let store = PendingPermissionStore::new();
376 let (tx, mut rx) = oneshot::channel();
377 store.insert("p2".into(), tx);
378
379 let params = serde_json::json!({
380 "permissionId": "p2",
381 "outcome": "denied"
382 });
383 let result = handle_request_permission(&Value::from(1), Some(¶ms), &store);
384 assert!(result["result"].is_object());
385 let outcome = rx.try_recv().unwrap();
386 assert_eq!(outcome, PermissionOutcome::Denied);
387 }
388}