Skip to main content

agent_air_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::{Mutex, mpsc, oneshot};
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                && sg.iter().any(|grant| grant.satisfies(request))
286            {
287                granted.insert(request.id.clone());
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            && let Some(ref g) = response.grant
430        {
431            self.add_grant(pending.session_id, g.clone()).await;
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 =
532                BatchPermissionResponse::with_auto_approved(generate_batch_id(), auto_approved);
533            let _ = tx.send(response);
534            return Ok(rx);
535        }
536
537        let batch_id = generate_batch_id();
538        let (tx, rx) = oneshot::channel();
539
540        // Create batch request with suggested grants
541        let batch = BatchPermissionRequest::new(batch_id.clone(), needs_approval.clone());
542
543        // Store pending batch
544        {
545            let mut pending = self.pending_batches.lock().await;
546
547            // Cleanup stale entries if map is getting large
548            if pending.len() >= PENDING_CLEANUP_THRESHOLD {
549                let now = Instant::now();
550                pending.retain(|id, batch| {
551                    let keep = now.duration_since(batch.created_at) < PENDING_MAX_AGE;
552                    if !keep {
553                        tracing::warn!(
554                            batch_id = %id,
555                            age_secs = now.duration_since(batch.created_at).as_secs(),
556                            "Cleaning up stale pending batch permission request"
557                        );
558                    }
559                    keep
560                });
561            }
562
563            pending.insert(
564                batch_id.clone(),
565                PendingBatch {
566                    session_id,
567                    requests: needs_approval,
568                    turn_id: turn_id.clone(),
569                    responder: tx,
570                    created_at: Instant::now(),
571                },
572            );
573        }
574
575        // Emit event
576        self.event_tx
577            .send(ControllerEvent::BatchPermissionRequired {
578                session_id,
579                batch,
580                turn_id,
581            })
582            .await
583            .map_err(|_| PermissionError::EventSendFailed)?;
584
585        Ok(rx)
586    }
587
588    /// Responds to a batch permission request.
589    ///
590    /// # Arguments
591    /// * `batch_id` - ID of the batch to respond to.
592    /// * `response` - The batch response with approved grants and denied requests.
593    ///
594    /// # Returns
595    /// Ok(()) if successful.
596    pub async fn respond_to_batch(
597        &self,
598        batch_id: &str,
599        mut response: BatchPermissionResponse,
600    ) -> Result<(), PermissionError> {
601        let pending = {
602            let mut pending = self.pending_batches.lock().await;
603            pending.remove(batch_id).ok_or(PermissionError::NotFound)?
604        };
605
606        // Add approved grants to session
607        if !response.approved_grants.is_empty() {
608            self.add_grants(pending.session_id, response.approved_grants.clone())
609                .await;
610        }
611
612        // Ensure batch_id matches
613        response.batch_id = batch_id.to_string();
614
615        pending
616            .responder
617            .send(response)
618            .map_err(|_| PermissionError::SendFailed)
619    }
620
621    /// Cancels a pending batch permission request.
622    ///
623    /// This is called by the UI when the user closes the batch permission dialog
624    /// without responding. Dropping the sender will cause the tools to receive errors.
625    ///
626    /// # Arguments
627    /// * `batch_id` - ID of the batch to cancel.
628    ///
629    /// # Returns
630    /// Ok(()) if the batch was found and cancelled.
631    pub async fn cancel_batch(&self, batch_id: &str) -> Result<(), PermissionError> {
632        let mut pending = self.pending_batches.lock().await;
633        if pending.remove(batch_id).is_some() {
634            // Dropping the sender will cause the tools to receive errors
635            Ok(())
636        } else {
637            Err(PermissionError::NotFound)
638        }
639    }
640
641    // ========================================================================
642    // Session Management
643    // ========================================================================
644
645    /// Cancels all pending requests for a session.
646    ///
647    /// This drops the response channels, causing receivers to get errors.
648    ///
649    /// # Arguments
650    /// * `session_id` - Session to cancel.
651    pub async fn cancel_session(&self, session_id: i64) {
652        // Cancel individual requests
653        {
654            let mut pending = self.pending_requests.lock().await;
655            pending.retain(|_, p| p.session_id != session_id);
656        }
657
658        // Cancel batch requests
659        {
660            let mut pending = self.pending_batches.lock().await;
661            pending.retain(|_, p| p.session_id != session_id);
662        }
663    }
664
665    /// Clears all state for a session (grants and pending requests).
666    ///
667    /// # Arguments
668    /// * `session_id` - Session to clear.
669    pub async fn clear_session(&self, session_id: i64) {
670        self.cancel_session(session_id).await;
671        self.clear_grants(session_id).await;
672    }
673
674    /// Checks if there are any pending requests for a session.
675    ///
676    /// # Arguments
677    /// * `session_id` - Session to check.
678    ///
679    /// # Returns
680    /// `true` if there are pending requests.
681    pub async fn has_pending(&self, session_id: i64) -> bool {
682        let individual_pending = {
683            let pending = self.pending_requests.lock().await;
684            pending.values().any(|p| p.session_id == session_id)
685        };
686
687        if individual_pending {
688            return true;
689        }
690
691        let pending = self.pending_batches.lock().await;
692        pending.values().any(|p| p.session_id == session_id)
693    }
694
695    /// Gets count of pending requests across all sessions.
696    pub async fn pending_count(&self) -> usize {
697        let individual = self.pending_requests.lock().await.len();
698        let batch = self.pending_batches.lock().await.len();
699        individual + batch
700    }
701
702    /// Gets pending request IDs for a session.
703    pub async fn pending_request_ids(&self, session_id: i64) -> Vec<String> {
704        let pending = self.pending_requests.lock().await;
705        pending
706            .iter()
707            .filter(|(_, p)| p.session_id == session_id)
708            .map(|(id, _)| id.clone())
709            .collect()
710    }
711
712    /// Gets pending batch IDs for a session.
713    pub async fn pending_batch_ids(&self, session_id: i64) -> Vec<String> {
714        let pending = self.pending_batches.lock().await;
715        pending
716            .iter()
717            .filter(|(_, p)| p.session_id == session_id)
718            .map(|(id, _)| id.clone())
719            .collect()
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726    use crate::permissions::PermissionLevel;
727
728    fn create_read_request(id: &str, path: &str) -> PermissionRequest {
729        PermissionRequest::file_read(id, path)
730    }
731
732    fn create_write_request(id: &str, path: &str) -> PermissionRequest {
733        PermissionRequest::file_write(id, path)
734    }
735
736    #[tokio::test]
737    async fn test_add_and_check_grant() {
738        let (tx, _rx) = mpsc::channel(10);
739        let registry = PermissionRegistry::new(tx);
740
741        let grant = Grant::read_path("/project/src", true);
742        registry.add_grant(1, grant).await;
743
744        let request = create_read_request("req-1", "/project/src/main.rs");
745        assert!(registry.check(1, &request).await);
746
747        // Different session should not have grant
748        assert!(!registry.check(2, &request).await);
749    }
750
751    #[tokio::test]
752    async fn test_level_hierarchy() {
753        let (tx, _rx) = mpsc::channel(10);
754        let registry = PermissionRegistry::new(tx);
755
756        // Add write grant
757        let grant = Grant::write_path("/project", true);
758        registry.add_grant(1, grant).await;
759
760        // Read request should be satisfied (Write > Read)
761        let read_request = create_read_request("req-1", "/project/file.rs");
762        assert!(registry.check(1, &read_request).await);
763
764        // Write request should also be satisfied
765        let write_request = create_write_request("req-2", "/project/file.rs");
766        assert!(registry.check(1, &write_request).await);
767    }
768
769    #[tokio::test]
770    async fn test_level_hierarchy_insufficient() {
771        let (tx, _rx) = mpsc::channel(10);
772        let registry = PermissionRegistry::new(tx);
773
774        // Add read grant
775        let grant = Grant::read_path("/project", true);
776        registry.add_grant(1, grant).await;
777
778        // Write request should NOT be satisfied (Read < Write)
779        let write_request = create_write_request("req-1", "/project/file.rs");
780        assert!(!registry.check(1, &write_request).await);
781    }
782
783    #[tokio::test]
784    async fn test_recursive_path_grant() {
785        let (tx, _rx) = mpsc::channel(10);
786        let registry = PermissionRegistry::new(tx);
787
788        // Add recursive grant
789        let grant = Grant::read_path("/project", true);
790        registry.add_grant(1, grant).await;
791
792        // Nested path should be covered
793        let request = create_read_request("req-1", "/project/src/utils/mod.rs");
794        assert!(registry.check(1, &request).await);
795    }
796
797    #[tokio::test]
798    async fn test_non_recursive_path_grant() {
799        let (tx, _rx) = mpsc::channel(10);
800        let registry = PermissionRegistry::new(tx);
801
802        // Add non-recursive grant
803        let grant = Grant::read_path("/project/src", false);
804        registry.add_grant(1, grant).await;
805
806        // Direct child should be covered
807        let direct = create_read_request("req-1", "/project/src/main.rs");
808        assert!(registry.check(1, &direct).await);
809
810        // Nested path should NOT be covered
811        let nested = create_read_request("req-2", "/project/src/utils/mod.rs");
812        assert!(!registry.check(1, &nested).await);
813    }
814
815    #[tokio::test]
816    async fn test_check_batch() {
817        let (tx, _rx) = mpsc::channel(10);
818        let registry = PermissionRegistry::new(tx);
819
820        // Add grant
821        let grant = Grant::read_path("/project/src", true);
822        registry.add_grant(1, grant).await;
823
824        let requests = vec![
825            create_read_request("req-1", "/project/src/main.rs"),
826            create_read_request("req-2", "/project/tests/test.rs"), // Not covered
827            create_read_request("req-3", "/project/src/lib.rs"),
828        ];
829
830        let granted = registry.check_batch(1, &requests).await;
831
832        assert!(granted.contains("req-1"));
833        assert!(!granted.contains("req-2"));
834        assert!(granted.contains("req-3"));
835    }
836
837    #[tokio::test]
838    async fn test_request_permission_auto_approve() {
839        let (tx, mut rx) = mpsc::channel(10);
840        let registry = PermissionRegistry::new(tx);
841
842        // Add grant first
843        let grant = Grant::read_path("/project", true);
844        registry.add_grant(1, grant).await;
845
846        // Request should be auto-approved
847        let request = create_read_request("req-1", "/project/file.rs");
848        let result_rx = registry.request_permission(1, request, None).await.unwrap();
849
850        // Should receive response immediately (auto-approved)
851        let response = result_rx.await.unwrap();
852        assert!(response.granted);
853
854        // No event should be emitted for auto-approved
855        assert!(rx.try_recv().is_err());
856    }
857
858    #[tokio::test]
859    async fn test_request_permission_needs_approval() {
860        let (tx, mut rx) = mpsc::channel(10);
861        let registry = PermissionRegistry::new(tx);
862
863        // No grant - should need approval
864        let request = create_read_request("req-1", "/project/file.rs");
865        let result_rx = registry.request_permission(1, request, None).await.unwrap();
866
867        // Event should be emitted
868        let event = rx.recv().await.unwrap();
869        if let ControllerEvent::PermissionRequired { tool_use_id, .. } = event {
870            assert_eq!(tool_use_id, "req-1");
871        } else {
872            panic!("Expected PermissionRequired event");
873        }
874
875        // Respond to request with session grant
876        let grant = Grant::read_path("/project", true);
877        let response = PermissionPanelResponse {
878            granted: true,
879            grant: Some(grant),
880            message: None,
881        };
882        registry
883            .respond_to_request("req-1", response)
884            .await
885            .unwrap();
886
887        // Should receive approval
888        let response = result_rx.await.unwrap();
889        assert!(response.granted);
890
891        // Future requests should be auto-approved
892        let new_request = create_read_request("req-2", "/project/other.rs");
893        assert!(registry.check(1, &new_request).await);
894    }
895
896    #[tokio::test]
897    async fn test_request_permission_denied() {
898        let (tx, mut rx) = mpsc::channel(10);
899        let registry = PermissionRegistry::new(tx);
900
901        let request = create_read_request("req-1", "/project/file.rs");
902        let result_rx = registry.request_permission(1, request, None).await.unwrap();
903
904        // Consume the event
905        let _ = rx.recv().await.unwrap();
906
907        // Deny the request
908        let response = PermissionPanelResponse {
909            granted: false,
910            grant: None,
911            message: None,
912        };
913        registry
914            .respond_to_request("req-1", response)
915            .await
916            .unwrap();
917
918        // Should receive denial
919        let response = result_rx.await.unwrap();
920        assert!(!response.granted);
921    }
922
923    #[tokio::test]
924    async fn test_register_batch() {
925        let (tx, mut rx) = mpsc::channel(10);
926        let registry = PermissionRegistry::new(tx);
927
928        let requests = vec![
929            create_read_request("req-1", "/project/src/main.rs"),
930            create_read_request("req-2", "/project/src/lib.rs"),
931        ];
932
933        let result_rx = registry.register_batch(1, requests, None).await.unwrap();
934
935        // Event should be emitted
936        let event = rx.recv().await.unwrap();
937        let batch_id = if let ControllerEvent::BatchPermissionRequired { batch, .. } = event {
938            assert_eq!(batch.requests.len(), 2);
939            assert!(!batch.suggested_grants.is_empty());
940            batch.batch_id.clone()
941        } else {
942            panic!("Expected BatchPermissionRequired event");
943        };
944
945        // Respond with approval using the actual batch ID
946        let grant = Grant::read_path("/project/src", true);
947        let response = BatchPermissionResponse::all_granted(&batch_id, vec![grant]);
948        registry
949            .respond_to_batch(&batch_id, response)
950            .await
951            .unwrap();
952
953        // Should receive response
954        let result = result_rx.await.unwrap();
955        assert!(!result.approved_grants.is_empty());
956    }
957
958    #[tokio::test]
959    async fn test_register_batch_partial_auto_approve() {
960        let (tx, mut rx) = mpsc::channel(10);
961        let registry = PermissionRegistry::new(tx);
962
963        // Add grant for /project/src
964        let grant = Grant::read_path("/project/src", true);
965        registry.add_grant(1, grant).await;
966
967        let requests = vec![
968            create_read_request("req-1", "/project/src/main.rs"), // Should be auto-approved
969            create_read_request("req-2", "/project/tests/test.rs"), // Needs approval
970        ];
971
972        let result_rx = registry.register_batch(1, requests, None).await.unwrap();
973
974        // Event should only contain non-auto-approved request
975        let event = rx.recv().await.unwrap();
976        let batch_id = if let ControllerEvent::BatchPermissionRequired { batch, .. } = event {
977            assert_eq!(batch.requests.len(), 1);
978            assert_eq!(batch.requests[0].id, "req-2");
979            batch.batch_id.clone()
980        } else {
981            panic!("Expected BatchPermissionRequired event");
982        };
983
984        // Respond with approval for the remaining request
985        let grant = Grant::read_path("/project/tests", true);
986        let response = BatchPermissionResponse::all_granted(&batch_id, vec![grant]);
987        registry
988            .respond_to_batch(&batch_id, response)
989            .await
990            .unwrap();
991
992        let _ = result_rx.await.unwrap();
993    }
994
995    #[tokio::test]
996    async fn test_register_batch_all_auto_approved() {
997        let (tx, mut rx) = mpsc::channel(10);
998        let registry = PermissionRegistry::new(tx);
999
1000        // Add grant covering all requests
1001        let grant = Grant::read_path("/project", true);
1002        registry.add_grant(1, grant).await;
1003
1004        let requests = vec![
1005            create_read_request("req-1", "/project/src/main.rs"),
1006            create_read_request("req-2", "/project/tests/test.rs"),
1007        ];
1008
1009        let result_rx = registry.register_batch(1, requests, None).await.unwrap();
1010
1011        // Should receive immediately with auto-approved
1012        let result = result_rx.await.unwrap();
1013        assert!(result.auto_approved.contains("req-1"));
1014        assert!(result.auto_approved.contains("req-2"));
1015
1016        // No event should be emitted
1017        assert!(rx.try_recv().is_err());
1018    }
1019
1020    #[tokio::test]
1021    async fn test_revoke_grants() {
1022        let (tx, _rx) = mpsc::channel(10);
1023        let registry = PermissionRegistry::new(tx);
1024
1025        let grant1 = Grant::read_path("/project/src", true);
1026        let grant2 = Grant::read_path("/project/tests", true);
1027        registry.add_grant(1, grant1).await;
1028        registry.add_grant(1, grant2).await;
1029
1030        // Revoke one grant
1031        let target = GrantTarget::path("/project/src", true);
1032        let revoked = registry.revoke_grants(1, &target).await;
1033        assert_eq!(revoked, 1);
1034
1035        // First grant should be gone
1036        let request1 = create_read_request("req-1", "/project/src/file.rs");
1037        assert!(!registry.check(1, &request1).await);
1038
1039        // Second grant should remain
1040        let request2 = create_read_request("req-2", "/project/tests/test.rs");
1041        assert!(registry.check(1, &request2).await);
1042    }
1043
1044    #[tokio::test]
1045    async fn test_clear_session() {
1046        let (tx, _rx) = mpsc::channel(10);
1047        let registry = PermissionRegistry::new(tx);
1048
1049        let grant = Grant::read_path("/project", true);
1050        registry.add_grant(1, grant).await;
1051
1052        // Clear session
1053        registry.clear_session(1).await;
1054
1055        // Grant should be gone
1056        let request = create_read_request("req-1", "/project/file.rs");
1057        assert!(!registry.check(1, &request).await);
1058    }
1059
1060    #[tokio::test]
1061    async fn test_cancel_session() {
1062        let (tx, _rx) = mpsc::channel(10);
1063        let registry = PermissionRegistry::new(tx);
1064
1065        // Register a pending request
1066        let request = create_read_request("req-1", "/project/file.rs");
1067        let result_rx = registry.request_permission(1, request, None).await.unwrap();
1068
1069        assert!(registry.has_pending(1).await);
1070
1071        // Cancel session
1072        registry.cancel_session(1).await;
1073
1074        // Should no longer have pending
1075        assert!(!registry.has_pending(1).await);
1076
1077        // Receiver should get error (channel dropped)
1078        assert!(result_rx.await.is_err());
1079    }
1080
1081    #[tokio::test]
1082    async fn test_domain_grant() {
1083        let (tx, _rx) = mpsc::channel(10);
1084        let registry = PermissionRegistry::new(tx);
1085
1086        let grant = Grant::domain("*.github.com", PermissionLevel::Read);
1087        registry.add_grant(1, grant).await;
1088
1089        let request =
1090            PermissionRequest::network_access("req-1", "api.github.com", PermissionLevel::Read);
1091        assert!(registry.check(1, &request).await);
1092
1093        let other_domain =
1094            PermissionRequest::network_access("req-2", "api.gitlab.com", PermissionLevel::Read);
1095        assert!(!registry.check(1, &other_domain).await);
1096    }
1097
1098    #[tokio::test]
1099    async fn test_command_grant() {
1100        let (tx, _rx) = mpsc::channel(10);
1101        let registry = PermissionRegistry::new(tx);
1102
1103        let grant = Grant::command("git *", PermissionLevel::Execute);
1104        registry.add_grant(1, grant).await;
1105
1106        let request = PermissionRequest::command_execute("req-1", "git status");
1107        assert!(registry.check(1, &request).await);
1108
1109        let other_cmd = PermissionRequest::command_execute("req-2", "docker run nginx");
1110        assert!(!registry.check(1, &other_cmd).await);
1111    }
1112
1113    #[tokio::test]
1114    async fn test_find_satisfying_grant() {
1115        let (tx, _rx) = mpsc::channel(10);
1116        let registry = PermissionRegistry::new(tx);
1117
1118        let grant = Grant::write_path("/project", true);
1119        registry.add_grant(1, grant.clone()).await;
1120
1121        let request = create_read_request("req-1", "/project/file.rs");
1122        let found = registry.find_satisfying_grant(1, &request).await;
1123        assert!(found.is_some());
1124        assert_eq!(found.unwrap().target, grant.target);
1125    }
1126
1127    #[tokio::test]
1128    async fn test_pending_counts() {
1129        let (tx, _rx) = mpsc::channel(10);
1130        let registry = PermissionRegistry::new(tx);
1131
1132        assert_eq!(registry.pending_count().await, 0);
1133
1134        let request = create_read_request("req-1", "/project/file.rs");
1135        let _ = registry.request_permission(1, request, None).await;
1136
1137        assert_eq!(registry.pending_count().await, 1);
1138
1139        let ids = registry.pending_request_ids(1).await;
1140        assert_eq!(ids.len(), 1);
1141        assert_eq!(ids[0], "req-1");
1142    }
1143}