Skip to main content

agent_core_runtime/permissions/
registry.rs

1//! Permission registry for managing grants and permission requests.
2//!
3//! The registry tracks:
4//! - Active grants per session
5//! - Pending permission requests (both individual and batched)
6//! - Provides methods for checking, granting, and revoking permissions
7
8use std::collections::{HashMap, HashSet};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::{Duration, Instant};
11
12use tokio::sync::{mpsc, oneshot, Mutex};
13
14/// Maximum number of pending requests before triggering cleanup.
15const PENDING_CLEANUP_THRESHOLD: usize = 50;
16
17/// Maximum age for pending requests before they're considered stale (5 minutes).
18const 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/// Information about a pending permission request for UI display.
26#[derive(Debug, Clone)]
27pub struct PendingPermissionInfo {
28    /// Tool use ID for this permission request.
29    pub tool_use_id: String,
30    /// Session ID this permission belongs to.
31    pub session_id: i64,
32    /// The permission request details.
33    pub request: PermissionRequest,
34    /// Turn ID for this permission request.
35    pub turn_id: Option<TurnId>,
36}
37
38/// Response from the UI to a permission request.
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
40pub struct PermissionPanelResponse {
41    /// Whether permission was granted.
42    pub granted: bool,
43    /// Grant to add to session (None for "once" or "deny").
44    #[serde(skip)]
45    pub grant: Option<Grant>,
46    /// Optional message from user (e.g., reason for denial).
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub message: Option<String>,
49}
50
51/// Counter for generating unique batch IDs.
52static BATCH_COUNTER: AtomicU64 = AtomicU64::new(1);
53
54/// Generates a unique batch ID.
55pub fn generate_batch_id() -> String {
56    let id = BATCH_COUNTER.fetch_add(1, Ordering::SeqCst);
57    format!("batch-{}", id)
58}
59
60/// Error types for permission operations.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum PermissionError {
63    /// No pending permission request found.
64    NotFound,
65    /// The permission request was already responded to.
66    AlreadyResponded,
67    /// Failed to send response (channel closed).
68    SendFailed,
69    /// Failed to send event notification.
70    EventSendFailed,
71    /// The batch has already been processed.
72    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
89/// Internal state for a pending individual permission request.
90struct PendingRequest {
91    session_id: i64,
92    request: PermissionRequest,
93    turn_id: Option<TurnId>,
94    responder: oneshot::Sender<PermissionPanelResponse>,
95    created_at: Instant,
96}
97
98/// Internal state for a pending batch permission request.
99struct PendingBatch {
100    session_id: i64,
101    /// Stored for potential future UI display of pending batch details.
102    #[allow(dead_code)]
103    requests: Vec<PermissionRequest>,
104    /// Stored for potential future UI context display.
105    #[allow(dead_code)]
106    turn_id: Option<TurnId>,
107    responder: oneshot::Sender<BatchPermissionResponse>,
108    created_at: Instant,
109}
110
111/// Registry for managing permission grants and requests.
112///
113/// This registry implements the new grant-based permission system with:
114/// - Hierarchical permission levels (Admin > Execute > Write > Read > None)
115/// - Path-based grants with optional recursion
116/// - Domain and command pattern grants
117/// - Batch permission request handling
118///
119/// # Lock Ordering
120///
121/// This struct contains multiple async mutexes. To prevent deadlocks, all methods
122/// follow these rules:
123///
124/// 1. **Never hold multiple locks simultaneously** - each method acquires locks
125///    in separate scopes, releasing one before acquiring the next
126/// 2. **Release locks before async operations** - locks are released before
127///    sending to channels or awaiting other async calls
128/// 3. **Sequential ordering when multiple locks needed**:
129///    - `pending_requests` or `pending_batches` first (for request lookup)
130///    - `session_grants` second (for grant operations)
131///
132/// Example pattern used throughout:
133/// ```ignore
134/// // Good: sequential lock acquisition in separate scopes
135/// let request = {
136///     let mut pending = self.pending_requests.lock().await;
137///     pending.remove(id)?
138/// }; // lock released here
139/// self.add_grant(session_id, grant).await; // acquires session_grants
140/// ```
141pub struct PermissionRegistry {
142    /// Active grants per session (session_id -> list of grants).
143    session_grants: Mutex<HashMap<i64, Vec<Grant>>>,
144    /// Pending individual permission requests (request_id -> pending state).
145    pending_requests: Mutex<HashMap<String, PendingRequest>>,
146    /// Pending batch permission requests (batch_id -> pending state).
147    pending_batches: Mutex<HashMap<String, PendingBatch>>,
148    /// Channel to send controller events.
149    event_tx: mpsc::Sender<ControllerEvent>,
150}
151
152impl PermissionRegistry {
153    /// Creates a new PermissionRegistry.
154    ///
155    /// # Arguments
156    /// * `event_tx` - Channel to send events when permissions are requested.
157    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    // ========================================================================
167    // Grant Management
168    // ========================================================================
169
170    /// Adds a grant for a session.
171    ///
172    /// # Arguments
173    /// * `session_id` - Session to add the grant to.
174    /// * `grant` - The grant to add.
175    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    /// Adds multiple grants for a session.
182    ///
183    /// # Arguments
184    /// * `session_id` - Session to add the grants to.
185    /// * `new_grants` - The grants to add.
186    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    /// Removes expired grants from a session.
193    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    /// Revokes all grants matching a specific target for a session.
201    ///
202    /// # Arguments
203    /// * `session_id` - Session to revoke grants from.
204    /// * `target` - Target to match for revocation.
205    ///
206    /// # Returns
207    /// Number of grants revoked.
208    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    /// Gets all grants for a session.
220    ///
221    /// # Arguments
222    /// * `session_id` - Session to query.
223    ///
224    /// # Returns
225    /// List of grants for the session (empty if none).
226    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    /// Clears all grants for a session.
232    ///
233    /// # Arguments
234    /// * `session_id` - Session to clear.
235    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    // ========================================================================
241    // Permission Checking
242    // ========================================================================
243
244    /// Checks if a permission request is satisfied by existing grants.
245    ///
246    /// This checks if any grant in the session satisfies the request based on:
247    /// 1. Target coverage (grant target covers request target)
248    /// 2. Level hierarchy (grant level >= required level)
249    /// 3. Grant not expired
250    ///
251    /// # Arguments
252    /// * `session_id` - Session to check.
253    /// * `request` - The permission request to check.
254    ///
255    /// # Returns
256    /// `true` if permission is granted, `false` otherwise.
257    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    /// Checks multiple permission requests and returns which are already granted.
267    ///
268    /// # Arguments
269    /// * `session_id` - Session to check.
270    /// * `requests` - The permission requests to check.
271    ///
272    /// # Returns
273    /// Set of request IDs that are already granted.
274    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    /// Finds which grant (if any) satisfies a request.
294    ///
295    /// # Arguments
296    /// * `session_id` - Session to check.
297    /// * `request` - The permission request to check.
298    ///
299    /// # Returns
300    /// The grant that satisfies the request, if any.
301    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    // ========================================================================
318    // Individual Permission Requests
319    // ========================================================================
320
321    /// Registers an individual permission request.
322    ///
323    /// If the request is already satisfied by existing grants, returns an auto-approved response immediately.
324    /// Otherwise, registers the request, emits an event, and returns a receiver to await the response.
325    ///
326    /// # Arguments
327    /// * `session_id` - Session requesting permission.
328    /// * `request` - The permission request.
329    /// * `turn_id` - Optional turn ID for UI context.
330    ///
331    /// # Returns
332    /// A receiver that will receive a `PermissionPanelResponse`.
333    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        // Check if already granted - auto-approve if so
340        //
341        // Note: There's a theoretical TOCTOU race here - a grant could be revoked
342        // between check() and sending the auto-approval. We accept this because:
343        // 1. Grant revocation during active requests is extremely rare
344        // 2. The window is microseconds
345        // 3. Holding the lock during channel ops would block all permission checks
346        // 4. The worst case is honoring a just-revoked grant (not a security issue
347        //    since the user explicitly granted it moments ago)
348        if self.check(session_id, &request).await {
349            let (tx, rx) = oneshot::channel();
350            let _ = tx.send(PermissionPanelResponse {
351                granted: true,
352                grant: None, // Already have a grant covering this
353                message: None,
354            });
355            return Ok(rx);
356        }
357
358        let (tx, rx) = oneshot::channel();
359        let request_id = request.id.clone();
360
361        // Store pending request
362        {
363            let mut pending = self.pending_requests.lock().await;
364
365            // Cleanup stale entries if map is getting large
366            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        // Emit event
394        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    /// Responds to an individual permission request.
408    ///
409    /// # Arguments
410    /// * `request_id` - ID of the request to respond to.
411    /// * `response` - The user's response (grant/deny with optional persistent grant).
412    ///
413    /// # Returns
414    /// Ok(()) if successful.
415    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        // Add grant if provided and granted
428        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    /// Cancels a pending permission request.
441    ///
442    /// This is called by the UI when the user closes the permission dialog
443    /// without responding. Dropping the sender will cause the tool to receive a RecvError.
444    ///
445    /// # Arguments
446    /// * `request_id` - ID of the request to cancel.
447    ///
448    /// # Returns
449    /// Ok(()) if the request was found and cancelled.
450    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            // Dropping the sender will cause the tool to receive a RecvError
454            Ok(())
455        } else {
456            Err(PermissionError::NotFound)
457        }
458    }
459
460    /// Gets all pending permission requests for a session.
461    ///
462    /// # Arguments
463    /// * `session_id` - Session ID to query.
464    ///
465    /// # Returns
466    /// List of pending permission info for the session.
467    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    /// Check if permission is already granted for the session.
482    ///
483    /// This is a convenience method that wraps `check()`.
484    /// Uses the Grant::satisfies method from the permission system.
485    ///
486    /// # Arguments
487    /// * `session_id` - Session to check.
488    /// * `request` - The permission request to check.
489    ///
490    /// # Returns
491    /// True if permission was previously granted for the session.
492    pub async fn is_granted(&self, session_id: i64, request: &PermissionRequest) -> bool {
493        self.check(session_id, request).await
494    }
495
496    // ========================================================================
497    // Batch Permission Requests
498    // ========================================================================
499
500    /// Registers a batch of permission requests.
501    ///
502    /// Requests that are already satisfied by existing grants are auto-approved
503    /// and not included in the batch sent to the UI.
504    ///
505    /// # Arguments
506    /// * `session_id` - Session requesting permissions.
507    /// * `requests` - The permission requests.
508    /// * `turn_id` - Optional turn ID for UI context.
509    ///
510    /// # Returns
511    /// A receiver that will receive the batch response.
512    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        // Check which requests are already granted
519        let auto_approved = self.check_batch(session_id, &requests).await;
520
521        // Filter out auto-approved requests
522        let needs_approval: Vec<_> = requests
523            .iter()
524            .filter(|r| !auto_approved.contains(&r.id))
525            .cloned()
526            .collect();
527
528        // If all auto-approved, return immediately
529        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        // Create batch request with suggested grants
543        let batch = BatchPermissionRequest::new(batch_id.clone(), needs_approval.clone());
544
545        // Store pending batch
546        {
547            let mut pending = self.pending_batches.lock().await;
548
549            // Cleanup stale entries if map is getting large
550            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        // Emit event
578        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    /// Responds to a batch permission request.
591    ///
592    /// # Arguments
593    /// * `batch_id` - ID of the batch to respond to.
594    /// * `response` - The batch response with approved grants and denied requests.
595    ///
596    /// # Returns
597    /// Ok(()) if successful.
598    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        // Add approved grants to session
611        if !response.approved_grants.is_empty() {
612            self.add_grants(pending.session_id, response.approved_grants.clone())
613                .await;
614        }
615
616        // Ensure batch_id matches
617        response.batch_id = batch_id.to_string();
618
619        pending
620            .responder
621            .send(response)
622            .map_err(|_| PermissionError::SendFailed)
623    }
624
625    /// Cancels a pending batch permission request.
626    ///
627    /// This is called by the UI when the user closes the batch permission dialog
628    /// without responding. Dropping the sender will cause the tools to receive errors.
629    ///
630    /// # Arguments
631    /// * `batch_id` - ID of the batch to cancel.
632    ///
633    /// # Returns
634    /// Ok(()) if the batch was found and cancelled.
635    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            // Dropping the sender will cause the tools to receive errors
639            Ok(())
640        } else {
641            Err(PermissionError::NotFound)
642        }
643    }
644
645    // ========================================================================
646    // Session Management
647    // ========================================================================
648
649    /// Cancels all pending requests for a session.
650    ///
651    /// This drops the response channels, causing receivers to get errors.
652    ///
653    /// # Arguments
654    /// * `session_id` - Session to cancel.
655    pub async fn cancel_session(&self, session_id: i64) {
656        // Cancel individual requests
657        {
658            let mut pending = self.pending_requests.lock().await;
659            pending.retain(|_, p| p.session_id != session_id);
660        }
661
662        // Cancel batch requests
663        {
664            let mut pending = self.pending_batches.lock().await;
665            pending.retain(|_, p| p.session_id != session_id);
666        }
667    }
668
669    /// Clears all state for a session (grants and pending requests).
670    ///
671    /// # Arguments
672    /// * `session_id` - Session to clear.
673    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    /// Checks if there are any pending requests for a session.
679    ///
680    /// # Arguments
681    /// * `session_id` - Session to check.
682    ///
683    /// # Returns
684    /// `true` if there are pending requests.
685    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    /// Gets count of pending requests across all sessions.
700    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    /// Gets pending request IDs for a session.
707    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    /// Gets pending batch IDs for a session.
717    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        // Different session should not have grant
752        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        // Add write grant
761        let grant = Grant::write_path("/project", true);
762        registry.add_grant(1, grant).await;
763
764        // Read request should be satisfied (Write > Read)
765        let read_request = create_read_request("req-1", "/project/file.rs");
766        assert!(registry.check(1, &read_request).await);
767
768        // Write request should also be satisfied
769        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        // Add read grant
779        let grant = Grant::read_path("/project", true);
780        registry.add_grant(1, grant).await;
781
782        // Write request should NOT be satisfied (Read < Write)
783        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        // Add recursive grant
793        let grant = Grant::read_path("/project", true);
794        registry.add_grant(1, grant).await;
795
796        // Nested path should be covered
797        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        // Add non-recursive grant
807        let grant = Grant::read_path("/project/src", false);
808        registry.add_grant(1, grant).await;
809
810        // Direct child should be covered
811        let direct = create_read_request("req-1", "/project/src/main.rs");
812        assert!(registry.check(1, &direct).await);
813
814        // Nested path should NOT be covered
815        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        // Add grant
825        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"), // Not covered
831            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        // Add grant first
847        let grant = Grant::read_path("/project", true);
848        registry.add_grant(1, grant).await;
849
850        // Request should be auto-approved
851        let request = create_read_request("req-1", "/project/file.rs");
852        let result_rx = registry.request_permission(1, request, None).await.unwrap();
853
854        // Should receive response immediately (auto-approved)
855        let response = result_rx.await.unwrap();
856        assert!(response.granted);
857
858        // No event should be emitted for auto-approved
859        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        // No grant - should need approval
868        let request = create_read_request("req-1", "/project/file.rs");
869        let result_rx = registry.request_permission(1, request, None).await.unwrap();
870
871        // Event should be emitted
872        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        // Respond to request with session grant
880        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        // Should receive approval
889        let response = result_rx.await.unwrap();
890        assert!(response.granted);
891
892        // Future requests should be auto-approved
893        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        // Consume the event
906        let _ = rx.recv().await.unwrap();
907
908        // Deny the request
909        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        // Should receive denial
917        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        // Event should be emitted
934        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        // Respond with approval using the actual batch ID
944        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        // Should receive response
949        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        // Add grant for /project/src
959        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"), // Should be auto-approved
964            create_read_request("req-2", "/project/tests/test.rs"), // Needs approval
965        ];
966
967        let result_rx = registry.register_batch(1, requests, None).await.unwrap();
968
969        // Event should only contain non-auto-approved request
970        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        // Respond with approval for the remaining request
980        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        // Add grant covering all requests
993        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        // Should receive immediately with auto-approved
1004        let result = result_rx.await.unwrap();
1005        assert!(result.auto_approved.contains("req-1"));
1006        assert!(result.auto_approved.contains("req-2"));
1007
1008        // No event should be emitted
1009        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        // Revoke one grant
1023        let target = GrantTarget::path("/project/src", true);
1024        let revoked = registry.revoke_grants(1, &target).await;
1025        assert_eq!(revoked, 1);
1026
1027        // First grant should be gone
1028        let request1 = create_read_request("req-1", "/project/src/file.rs");
1029        assert!(!registry.check(1, &request1).await);
1030
1031        // Second grant should remain
1032        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        // Clear session
1045        registry.clear_session(1).await;
1046
1047        // Grant should be gone
1048        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        // Register a pending request
1058        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        // Cancel session
1064        registry.cancel_session(1).await;
1065
1066        // Should no longer have pending
1067        assert!(!registry.has_pending(1).await);
1068
1069        // Receiver should get error (channel dropped)
1070        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}