1use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{Duration, Instant};
11
12use tokio::sync::{mpsc, oneshot, Mutex};
13
14const PENDING_CLEANUP_THRESHOLD: usize = 50;
16
17const PENDING_MAX_AGE: Duration = Duration::from_secs(300);
19
20use super::{
21 BatchPermissionRequest, BatchPermissionResponse, Grant, GrantTarget, PermissionRequest,
22};
23use crate::controller::types::{ControllerEvent, TurnId};
24
25#[derive(Debug, Clone)]
27pub struct PendingPermissionInfo {
28 pub tool_use_id: String,
30 pub session_id: i64,
32 pub request: PermissionRequest,
34 pub turn_id: Option<TurnId>,
36}
37
38#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
40pub struct PermissionPanelResponse {
41 pub granted: bool,
43 #[serde(skip)]
45 pub grant: Option<Grant>,
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub message: Option<String>,
49}
50
51static BATCH_COUNTER: AtomicU64 = AtomicU64::new(1);
53
54pub fn generate_batch_id() -> String {
56 let id = BATCH_COUNTER.fetch_add(1, Ordering::SeqCst);
57 format!("batch-{}", id)
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum PermissionError {
63 NotFound,
65 AlreadyResponded,
67 SendFailed,
69 EventSendFailed,
71 BatchAlreadyProcessed,
73}
74
75impl std::fmt::Display for PermissionError {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 PermissionError::NotFound => write!(f, "No pending permission request found"),
79 PermissionError::AlreadyResponded => write!(f, "Permission already responded to"),
80 PermissionError::SendFailed => write!(f, "Failed to send response"),
81 PermissionError::EventSendFailed => write!(f, "Failed to send event notification"),
82 PermissionError::BatchAlreadyProcessed => write!(f, "Batch has already been processed"),
83 }
84 }
85}
86
87impl std::error::Error for PermissionError {}
88
89struct PendingRequest {
91 session_id: i64,
92 request: PermissionRequest,
93 turn_id: Option<TurnId>,
94 responder: oneshot::Sender<PermissionPanelResponse>,
95 created_at: Instant,
96}
97
98struct PendingBatch {
100 session_id: i64,
101 #[allow(dead_code)]
103 requests: Vec<PermissionRequest>,
104 #[allow(dead_code)]
106 turn_id: Option<TurnId>,
107 responder: oneshot::Sender<BatchPermissionResponse>,
108 created_at: Instant,
109}
110
111pub struct PermissionRegistry {
142 session_grants: Mutex<HashMap<i64, Vec<Grant>>>,
144 pending_requests: Mutex<HashMap<String, PendingRequest>>,
146 pending_batches: Mutex<HashMap<String, PendingBatch>>,
148 event_tx: mpsc::Sender<ControllerEvent>,
150}
151
152impl PermissionRegistry {
153 pub fn new(event_tx: mpsc::Sender<ControllerEvent>) -> Self {
158 Self {
159 session_grants: Mutex::new(HashMap::new()),
160 pending_requests: Mutex::new(HashMap::new()),
161 pending_batches: Mutex::new(HashMap::new()),
162 event_tx,
163 }
164 }
165
166 pub async fn add_grant(&self, session_id: i64, grant: Grant) {
176 let mut grants = self.session_grants.lock().await;
177 let session_grants = grants.entry(session_id).or_insert_with(Vec::new);
178 session_grants.push(grant);
179 }
180
181 pub async fn add_grants(&self, session_id: i64, new_grants: Vec<Grant>) {
187 let mut grants = self.session_grants.lock().await;
188 let session_grants = grants.entry(session_id).or_insert_with(Vec::new);
189 session_grants.extend(new_grants);
190 }
191
192 pub async fn cleanup_expired(&self, session_id: i64) {
194 let mut grants = self.session_grants.lock().await;
195 if let Some(session_grants) = grants.get_mut(&session_id) {
196 session_grants.retain(|g| !g.is_expired());
197 }
198 }
199
200 pub async fn revoke_grants(&self, session_id: i64, target: &GrantTarget) -> usize {
209 let mut grants = self.session_grants.lock().await;
210 if let Some(session_grants) = grants.get_mut(&session_id) {
211 let original_len = session_grants.len();
212 session_grants.retain(|g| &g.target != target);
213 original_len - session_grants.len()
214 } else {
215 0
216 }
217 }
218
219 pub async fn get_grants(&self, session_id: i64) -> Vec<Grant> {
227 let grants = self.session_grants.lock().await;
228 grants.get(&session_id).cloned().unwrap_or_default()
229 }
230
231 pub async fn clear_grants(&self, session_id: i64) {
236 let mut grants = self.session_grants.lock().await;
237 grants.remove(&session_id);
238 }
239
240 pub async fn check(&self, session_id: i64, request: &PermissionRequest) -> bool {
258 let grants = self.session_grants.lock().await;
259 if let Some(session_grants) = grants.get(&session_id) {
260 session_grants.iter().any(|grant| grant.satisfies(request))
261 } else {
262 false
263 }
264 }
265
266 pub async fn check_batch(
275 &self,
276 session_id: i64,
277 requests: &[PermissionRequest],
278 ) -> HashSet<String> {
279 let grants = self.session_grants.lock().await;
280 let session_grants = grants.get(&session_id);
281
282 let mut granted = HashSet::new();
283 for request in requests {
284 if let Some(sg) = session_grants {
285 if sg.iter().any(|grant| grant.satisfies(request)) {
286 granted.insert(request.id.clone());
287 }
288 }
289 }
290 granted
291 }
292
293 pub async fn find_satisfying_grant(
302 &self,
303 session_id: i64,
304 request: &PermissionRequest,
305 ) -> Option<Grant> {
306 let grants = self.session_grants.lock().await;
307 if let Some(session_grants) = grants.get(&session_id) {
308 session_grants
309 .iter()
310 .find(|grant| grant.satisfies(request))
311 .cloned()
312 } else {
313 None
314 }
315 }
316
317 pub async fn request_permission(
334 &self,
335 session_id: i64,
336 request: PermissionRequest,
337 turn_id: Option<TurnId>,
338 ) -> Result<oneshot::Receiver<PermissionPanelResponse>, PermissionError> {
339 if self.check(session_id, &request).await {
349 let (tx, rx) = oneshot::channel();
350 let _ = tx.send(PermissionPanelResponse {
351 granted: true,
352 grant: None, message: None,
354 });
355 return Ok(rx);
356 }
357
358 let (tx, rx) = oneshot::channel();
359 let request_id = request.id.clone();
360
361 {
363 let mut pending = self.pending_requests.lock().await;
364
365 if pending.len() >= PENDING_CLEANUP_THRESHOLD {
367 let now = Instant::now();
368 pending.retain(|id, req| {
369 let keep = now.duration_since(req.created_at) < PENDING_MAX_AGE;
370 if !keep {
371 tracing::warn!(
372 request_id = %id,
373 age_secs = now.duration_since(req.created_at).as_secs(),
374 "Cleaning up stale pending permission request"
375 );
376 }
377 keep
378 });
379 }
380
381 pending.insert(
382 request_id.clone(),
383 PendingRequest {
384 session_id,
385 request: request.clone(),
386 turn_id: turn_id.clone(),
387 responder: tx,
388 created_at: Instant::now(),
389 },
390 );
391 }
392
393 self.event_tx
395 .send(ControllerEvent::PermissionRequired {
396 session_id,
397 tool_use_id: request_id,
398 request,
399 turn_id,
400 })
401 .await
402 .map_err(|_| PermissionError::EventSendFailed)?;
403
404 Ok(rx)
405 }
406
407 pub async fn respond_to_request(
416 &self,
417 request_id: &str,
418 response: PermissionPanelResponse,
419 ) -> Result<(), PermissionError> {
420 let pending = {
421 let mut pending = self.pending_requests.lock().await;
422 pending
423 .remove(request_id)
424 .ok_or(PermissionError::NotFound)?
425 };
426
427 if response.granted {
429 if let Some(ref g) = response.grant {
430 self.add_grant(pending.session_id, g.clone()).await;
431 }
432 }
433
434 pending
435 .responder
436 .send(response)
437 .map_err(|_| PermissionError::SendFailed)
438 }
439
440 pub async fn cancel(&self, request_id: &str) -> Result<(), PermissionError> {
451 let mut pending = self.pending_requests.lock().await;
452 if pending.remove(request_id).is_some() {
453 Ok(())
455 } else {
456 Err(PermissionError::NotFound)
457 }
458 }
459
460 pub async fn pending_for_session(&self, session_id: i64) -> Vec<PendingPermissionInfo> {
468 let pending = self.pending_requests.lock().await;
469 pending
470 .iter()
471 .filter(|(_, req)| req.session_id == session_id)
472 .map(|(tool_use_id, req)| PendingPermissionInfo {
473 tool_use_id: tool_use_id.clone(),
474 session_id: req.session_id,
475 request: req.request.clone(),
476 turn_id: req.turn_id.clone(),
477 })
478 .collect()
479 }
480
481 pub async fn is_granted(&self, session_id: i64, request: &PermissionRequest) -> bool {
493 self.check(session_id, request).await
494 }
495
496 pub async fn register_batch(
513 &self,
514 session_id: i64,
515 requests: Vec<PermissionRequest>,
516 turn_id: Option<TurnId>,
517 ) -> Result<oneshot::Receiver<BatchPermissionResponse>, PermissionError> {
518 let auto_approved = self.check_batch(session_id, &requests).await;
520
521 let needs_approval: Vec<_> = requests
523 .iter()
524 .filter(|r| !auto_approved.contains(&r.id))
525 .cloned()
526 .collect();
527
528 if needs_approval.is_empty() {
530 let (tx, rx) = oneshot::channel();
531 let response = BatchPermissionResponse::with_auto_approved(
532 generate_batch_id(),
533 auto_approved,
534 );
535 let _ = tx.send(response);
536 return Ok(rx);
537 }
538
539 let batch_id = generate_batch_id();
540 let (tx, rx) = oneshot::channel();
541
542 let batch = BatchPermissionRequest::new(batch_id.clone(), needs_approval.clone());
544
545 {
547 let mut pending = self.pending_batches.lock().await;
548
549 if pending.len() >= PENDING_CLEANUP_THRESHOLD {
551 let now = Instant::now();
552 pending.retain(|id, batch| {
553 let keep = now.duration_since(batch.created_at) < PENDING_MAX_AGE;
554 if !keep {
555 tracing::warn!(
556 batch_id = %id,
557 age_secs = now.duration_since(batch.created_at).as_secs(),
558 "Cleaning up stale pending batch permission request"
559 );
560 }
561 keep
562 });
563 }
564
565 pending.insert(
566 batch_id.clone(),
567 PendingBatch {
568 session_id,
569 requests: needs_approval,
570 turn_id: turn_id.clone(),
571 responder: tx,
572 created_at: Instant::now(),
573 },
574 );
575 }
576
577 self.event_tx
579 .send(ControllerEvent::BatchPermissionRequired {
580 session_id,
581 batch,
582 turn_id,
583 })
584 .await
585 .map_err(|_| PermissionError::EventSendFailed)?;
586
587 Ok(rx)
588 }
589
590 pub async fn respond_to_batch(
599 &self,
600 batch_id: &str,
601 mut response: BatchPermissionResponse,
602 ) -> Result<(), PermissionError> {
603 let pending = {
604 let mut pending = self.pending_batches.lock().await;
605 pending
606 .remove(batch_id)
607 .ok_or(PermissionError::NotFound)?
608 };
609
610 if !response.approved_grants.is_empty() {
612 self.add_grants(pending.session_id, response.approved_grants.clone())
613 .await;
614 }
615
616 response.batch_id = batch_id.to_string();
618
619 pending
620 .responder
621 .send(response)
622 .map_err(|_| PermissionError::SendFailed)
623 }
624
625 pub async fn cancel_batch(&self, batch_id: &str) -> Result<(), PermissionError> {
636 let mut pending = self.pending_batches.lock().await;
637 if pending.remove(batch_id).is_some() {
638 Ok(())
640 } else {
641 Err(PermissionError::NotFound)
642 }
643 }
644
645 pub async fn cancel_session(&self, session_id: i64) {
656 {
658 let mut pending = self.pending_requests.lock().await;
659 pending.retain(|_, p| p.session_id != session_id);
660 }
661
662 {
664 let mut pending = self.pending_batches.lock().await;
665 pending.retain(|_, p| p.session_id != session_id);
666 }
667 }
668
669 pub async fn clear_session(&self, session_id: i64) {
674 self.cancel_session(session_id).await;
675 self.clear_grants(session_id).await;
676 }
677
678 pub async fn has_pending(&self, session_id: i64) -> bool {
686 let individual_pending = {
687 let pending = self.pending_requests.lock().await;
688 pending.values().any(|p| p.session_id == session_id)
689 };
690
691 if individual_pending {
692 return true;
693 }
694
695 let pending = self.pending_batches.lock().await;
696 pending.values().any(|p| p.session_id == session_id)
697 }
698
699 pub async fn pending_count(&self) -> usize {
701 let individual = self.pending_requests.lock().await.len();
702 let batch = self.pending_batches.lock().await.len();
703 individual + batch
704 }
705
706 pub async fn pending_request_ids(&self, session_id: i64) -> Vec<String> {
708 let pending = self.pending_requests.lock().await;
709 pending
710 .iter()
711 .filter(|(_, p)| p.session_id == session_id)
712 .map(|(id, _)| id.clone())
713 .collect()
714 }
715
716 pub async fn pending_batch_ids(&self, session_id: i64) -> Vec<String> {
718 let pending = self.pending_batches.lock().await;
719 pending
720 .iter()
721 .filter(|(_, p)| p.session_id == session_id)
722 .map(|(id, _)| id.clone())
723 .collect()
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730 use crate::permissions::PermissionLevel;
731
732 fn create_read_request(id: &str, path: &str) -> PermissionRequest {
733 PermissionRequest::file_read(id, path)
734 }
735
736 fn create_write_request(id: &str, path: &str) -> PermissionRequest {
737 PermissionRequest::file_write(id, path)
738 }
739
740 #[tokio::test]
741 async fn test_add_and_check_grant() {
742 let (tx, _rx) = mpsc::channel(10);
743 let registry = PermissionRegistry::new(tx);
744
745 let grant = Grant::read_path("/project/src", true);
746 registry.add_grant(1, grant).await;
747
748 let request = create_read_request("req-1", "/project/src/main.rs");
749 assert!(registry.check(1, &request).await);
750
751 assert!(!registry.check(2, &request).await);
753 }
754
755 #[tokio::test]
756 async fn test_level_hierarchy() {
757 let (tx, _rx) = mpsc::channel(10);
758 let registry = PermissionRegistry::new(tx);
759
760 let grant = Grant::write_path("/project", true);
762 registry.add_grant(1, grant).await;
763
764 let read_request = create_read_request("req-1", "/project/file.rs");
766 assert!(registry.check(1, &read_request).await);
767
768 let write_request = create_write_request("req-2", "/project/file.rs");
770 assert!(registry.check(1, &write_request).await);
771 }
772
773 #[tokio::test]
774 async fn test_level_hierarchy_insufficient() {
775 let (tx, _rx) = mpsc::channel(10);
776 let registry = PermissionRegistry::new(tx);
777
778 let grant = Grant::read_path("/project", true);
780 registry.add_grant(1, grant).await;
781
782 let write_request = create_write_request("req-1", "/project/file.rs");
784 assert!(!registry.check(1, &write_request).await);
785 }
786
787 #[tokio::test]
788 async fn test_recursive_path_grant() {
789 let (tx, _rx) = mpsc::channel(10);
790 let registry = PermissionRegistry::new(tx);
791
792 let grant = Grant::read_path("/project", true);
794 registry.add_grant(1, grant).await;
795
796 let request = create_read_request("req-1", "/project/src/utils/mod.rs");
798 assert!(registry.check(1, &request).await);
799 }
800
801 #[tokio::test]
802 async fn test_non_recursive_path_grant() {
803 let (tx, _rx) = mpsc::channel(10);
804 let registry = PermissionRegistry::new(tx);
805
806 let grant = Grant::read_path("/project/src", false);
808 registry.add_grant(1, grant).await;
809
810 let direct = create_read_request("req-1", "/project/src/main.rs");
812 assert!(registry.check(1, &direct).await);
813
814 let nested = create_read_request("req-2", "/project/src/utils/mod.rs");
816 assert!(!registry.check(1, &nested).await);
817 }
818
819 #[tokio::test]
820 async fn test_check_batch() {
821 let (tx, _rx) = mpsc::channel(10);
822 let registry = PermissionRegistry::new(tx);
823
824 let grant = Grant::read_path("/project/src", true);
826 registry.add_grant(1, grant).await;
827
828 let requests = vec![
829 create_read_request("req-1", "/project/src/main.rs"),
830 create_read_request("req-2", "/project/tests/test.rs"), create_read_request("req-3", "/project/src/lib.rs"),
832 ];
833
834 let granted = registry.check_batch(1, &requests).await;
835
836 assert!(granted.contains("req-1"));
837 assert!(!granted.contains("req-2"));
838 assert!(granted.contains("req-3"));
839 }
840
841 #[tokio::test]
842 async fn test_request_permission_auto_approve() {
843 let (tx, mut rx) = mpsc::channel(10);
844 let registry = PermissionRegistry::new(tx);
845
846 let grant = Grant::read_path("/project", true);
848 registry.add_grant(1, grant).await;
849
850 let request = create_read_request("req-1", "/project/file.rs");
852 let result_rx = registry.request_permission(1, request, None).await.unwrap();
853
854 let response = result_rx.await.unwrap();
856 assert!(response.granted);
857
858 assert!(rx.try_recv().is_err());
860 }
861
862 #[tokio::test]
863 async fn test_request_permission_needs_approval() {
864 let (tx, mut rx) = mpsc::channel(10);
865 let registry = PermissionRegistry::new(tx);
866
867 let request = create_read_request("req-1", "/project/file.rs");
869 let result_rx = registry.request_permission(1, request, None).await.unwrap();
870
871 let event = rx.recv().await.unwrap();
873 if let ControllerEvent::PermissionRequired { tool_use_id, .. } = event {
874 assert_eq!(tool_use_id, "req-1");
875 } else {
876 panic!("Expected PermissionRequired event");
877 }
878
879 let grant = Grant::read_path("/project", true);
881 let response = PermissionPanelResponse {
882 granted: true,
883 grant: Some(grant),
884 message: None,
885 };
886 registry.respond_to_request("req-1", response).await.unwrap();
887
888 let response = result_rx.await.unwrap();
890 assert!(response.granted);
891
892 let new_request = create_read_request("req-2", "/project/other.rs");
894 assert!(registry.check(1, &new_request).await);
895 }
896
897 #[tokio::test]
898 async fn test_request_permission_denied() {
899 let (tx, mut rx) = mpsc::channel(10);
900 let registry = PermissionRegistry::new(tx);
901
902 let request = create_read_request("req-1", "/project/file.rs");
903 let result_rx = registry.request_permission(1, request, None).await.unwrap();
904
905 let _ = rx.recv().await.unwrap();
907
908 let response = PermissionPanelResponse {
910 granted: false,
911 grant: None,
912 message: None,
913 };
914 registry.respond_to_request("req-1", response).await.unwrap();
915
916 let response = result_rx.await.unwrap();
918 assert!(!response.granted);
919 }
920
921 #[tokio::test]
922 async fn test_register_batch() {
923 let (tx, mut rx) = mpsc::channel(10);
924 let registry = PermissionRegistry::new(tx);
925
926 let requests = vec![
927 create_read_request("req-1", "/project/src/main.rs"),
928 create_read_request("req-2", "/project/src/lib.rs"),
929 ];
930
931 let result_rx = registry.register_batch(1, requests, None).await.unwrap();
932
933 let event = rx.recv().await.unwrap();
935 let batch_id = if let ControllerEvent::BatchPermissionRequired { batch, .. } = event {
936 assert_eq!(batch.requests.len(), 2);
937 assert!(!batch.suggested_grants.is_empty());
938 batch.batch_id.clone()
939 } else {
940 panic!("Expected BatchPermissionRequired event");
941 };
942
943 let grant = Grant::read_path("/project/src", true);
945 let response = BatchPermissionResponse::all_granted(&batch_id, vec![grant]);
946 registry.respond_to_batch(&batch_id, response).await.unwrap();
947
948 let result = result_rx.await.unwrap();
950 assert!(!result.approved_grants.is_empty());
951 }
952
953 #[tokio::test]
954 async fn test_register_batch_partial_auto_approve() {
955 let (tx, mut rx) = mpsc::channel(10);
956 let registry = PermissionRegistry::new(tx);
957
958 let grant = Grant::read_path("/project/src", true);
960 registry.add_grant(1, grant).await;
961
962 let requests = vec![
963 create_read_request("req-1", "/project/src/main.rs"), create_read_request("req-2", "/project/tests/test.rs"), ];
966
967 let result_rx = registry.register_batch(1, requests, None).await.unwrap();
968
969 let event = rx.recv().await.unwrap();
971 let batch_id = if let ControllerEvent::BatchPermissionRequired { batch, .. } = event {
972 assert_eq!(batch.requests.len(), 1);
973 assert_eq!(batch.requests[0].id, "req-2");
974 batch.batch_id.clone()
975 } else {
976 panic!("Expected BatchPermissionRequired event");
977 };
978
979 let grant = Grant::read_path("/project/tests", true);
981 let response = BatchPermissionResponse::all_granted(&batch_id, vec![grant]);
982 registry.respond_to_batch(&batch_id, response).await.unwrap();
983
984 let _ = result_rx.await.unwrap();
985 }
986
987 #[tokio::test]
988 async fn test_register_batch_all_auto_approved() {
989 let (tx, mut rx) = mpsc::channel(10);
990 let registry = PermissionRegistry::new(tx);
991
992 let grant = Grant::read_path("/project", true);
994 registry.add_grant(1, grant).await;
995
996 let requests = vec![
997 create_read_request("req-1", "/project/src/main.rs"),
998 create_read_request("req-2", "/project/tests/test.rs"),
999 ];
1000
1001 let result_rx = registry.register_batch(1, requests, None).await.unwrap();
1002
1003 let result = result_rx.await.unwrap();
1005 assert!(result.auto_approved.contains("req-1"));
1006 assert!(result.auto_approved.contains("req-2"));
1007
1008 assert!(rx.try_recv().is_err());
1010 }
1011
1012 #[tokio::test]
1013 async fn test_revoke_grants() {
1014 let (tx, _rx) = mpsc::channel(10);
1015 let registry = PermissionRegistry::new(tx);
1016
1017 let grant1 = Grant::read_path("/project/src", true);
1018 let grant2 = Grant::read_path("/project/tests", true);
1019 registry.add_grant(1, grant1).await;
1020 registry.add_grant(1, grant2).await;
1021
1022 let target = GrantTarget::path("/project/src", true);
1024 let revoked = registry.revoke_grants(1, &target).await;
1025 assert_eq!(revoked, 1);
1026
1027 let request1 = create_read_request("req-1", "/project/src/file.rs");
1029 assert!(!registry.check(1, &request1).await);
1030
1031 let request2 = create_read_request("req-2", "/project/tests/test.rs");
1033 assert!(registry.check(1, &request2).await);
1034 }
1035
1036 #[tokio::test]
1037 async fn test_clear_session() {
1038 let (tx, _rx) = mpsc::channel(10);
1039 let registry = PermissionRegistry::new(tx);
1040
1041 let grant = Grant::read_path("/project", true);
1042 registry.add_grant(1, grant).await;
1043
1044 registry.clear_session(1).await;
1046
1047 let request = create_read_request("req-1", "/project/file.rs");
1049 assert!(!registry.check(1, &request).await);
1050 }
1051
1052 #[tokio::test]
1053 async fn test_cancel_session() {
1054 let (tx, _rx) = mpsc::channel(10);
1055 let registry = PermissionRegistry::new(tx);
1056
1057 let request = create_read_request("req-1", "/project/file.rs");
1059 let result_rx = registry.request_permission(1, request, None).await.unwrap();
1060
1061 assert!(registry.has_pending(1).await);
1062
1063 registry.cancel_session(1).await;
1065
1066 assert!(!registry.has_pending(1).await);
1068
1069 assert!(result_rx.await.is_err());
1071 }
1072
1073 #[tokio::test]
1074 async fn test_domain_grant() {
1075 let (tx, _rx) = mpsc::channel(10);
1076 let registry = PermissionRegistry::new(tx);
1077
1078 let grant = Grant::domain("*.github.com", PermissionLevel::Read);
1079 registry.add_grant(1, grant).await;
1080
1081 let request =
1082 PermissionRequest::network_access("req-1", "api.github.com", PermissionLevel::Read);
1083 assert!(registry.check(1, &request).await);
1084
1085 let other_domain =
1086 PermissionRequest::network_access("req-2", "api.gitlab.com", PermissionLevel::Read);
1087 assert!(!registry.check(1, &other_domain).await);
1088 }
1089
1090 #[tokio::test]
1091 async fn test_command_grant() {
1092 let (tx, _rx) = mpsc::channel(10);
1093 let registry = PermissionRegistry::new(tx);
1094
1095 let grant = Grant::command("git *", PermissionLevel::Execute);
1096 registry.add_grant(1, grant).await;
1097
1098 let request = PermissionRequest::command_execute("req-1", "git status");
1099 assert!(registry.check(1, &request).await);
1100
1101 let other_cmd = PermissionRequest::command_execute("req-2", "docker run nginx");
1102 assert!(!registry.check(1, &other_cmd).await);
1103 }
1104
1105 #[tokio::test]
1106 async fn test_find_satisfying_grant() {
1107 let (tx, _rx) = mpsc::channel(10);
1108 let registry = PermissionRegistry::new(tx);
1109
1110 let grant = Grant::write_path("/project", true);
1111 registry.add_grant(1, grant.clone()).await;
1112
1113 let request = create_read_request("req-1", "/project/file.rs");
1114 let found = registry.find_satisfying_grant(1, &request).await;
1115 assert!(found.is_some());
1116 assert_eq!(found.unwrap().target, grant.target);
1117 }
1118
1119 #[tokio::test]
1120 async fn test_pending_counts() {
1121 let (tx, _rx) = mpsc::channel(10);
1122 let registry = PermissionRegistry::new(tx);
1123
1124 assert_eq!(registry.pending_count().await, 0);
1125
1126 let request = create_read_request("req-1", "/project/file.rs");
1127 let _ = registry.request_permission(1, request, None).await;
1128
1129 assert_eq!(registry.pending_count().await, 1);
1130
1131 let ids = registry.pending_request_ids(1).await;
1132 assert_eq!(ids.len(), 1);
1133 assert_eq!(ids[0], "req-1");
1134 }
1135}