Coverage for session_buddy / core / permissions.py: 97.96%
82 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-04 00:43 -0800
1"""Session permissions management.
3This module provides the SessionPermissionsManager class for managing
4trusted operations and permission scopes during sessions.
5"""
7from __future__ import annotations
9import hashlib
10import json
11import logging
12from datetime import datetime
13from pathlib import Path
14from typing import TYPE_CHECKING, Any
16logger = logging.getLogger(__name__)
18if TYPE_CHECKING:
19 from typing import Self
22class SessionPermissionsManager:
23 """Manages session permissions to avoid repeated prompts for trusted operations."""
25 _instance: SessionPermissionsManager | None = None
26 _session_id: str | None = None
27 _initialized: bool = False
29 def __new__(cls, claude_dir: Path) -> Self: # type: ignore[misc]
30 """Singleton pattern to ensure consistent session ID across tool calls."""
31 if cls._instance is None:
32 cls._instance = super().__new__(cls)
33 cls._instance._initialized = False
34 # Type checker knows this is Self from the annotation above
35 return cls._instance # type: ignore[return-value]
37 def __init__(self, claude_dir: Path) -> None:
38 if self._initialized:
39 return
40 self.claude_dir = claude_dir
41 self.permissions_file = claude_dir / "sessions" / "trusted_permissions.json"
42 self.permissions_file.parent.mkdir(parents=True, exist_ok=True)
43 self.trusted_operations: set[str] = set()
44 self.auto_checkpoint = False # Add the required attribute for tests
45 self.checkpoint_frequency = 300 # Default frequency (5 minutes)
47 # Use class-level session ID to persist across instances
48 if self.__class__._session_id is None: 48 ↛ 50line 48 didn't jump to line 50 because the condition on line 48 was always true
49 self.__class__._session_id = self._generate_session_id()
50 self.session_id = self.__class__._session_id
51 self._load_permissions()
52 self._initialized = True
54 def _generate_session_id(self) -> str:
55 """Generate unique session ID based on current time and working directory."""
56 try:
57 cwd = Path.cwd()
58 except FileNotFoundError:
59 cwd = Path.home()
60 session_data = f"{datetime.now().isoformat()}_{cwd}"
61 return hashlib.md5(session_data.encode(), usedforsecurity=False).hexdigest()[
62 :12
63 ]
65 def _load_permissions(self) -> None:
66 """Load previously granted permissions."""
67 if self.permissions_file.exists():
68 try:
69 with self.permissions_file.open() as f:
70 data = json.load(f)
71 self.trusted_operations.update(data.get("trusted_operations", []))
72 except (json.JSONDecodeError, KeyError):
73 pass
75 def _save_permissions(self) -> None:
76 """Save current trusted permissions."""
77 data = {
78 "trusted_operations": list(self.trusted_operations),
79 "last_updated": datetime.now().isoformat(),
80 "session_id": self.session_id,
81 }
82 with self.permissions_file.open("w") as f:
83 json.dump(data, f, indent=2)
85 def is_operation_trusted(self, operation: str) -> bool:
86 """Check if an operation is already trusted."""
87 return operation in self.trusted_operations
89 def trust_operation(self, operation: str, description: str = "") -> bool:
90 """Mark an operation as trusted to avoid future prompts."""
91 if operation is None:
92 msg = "Operation cannot be None"
93 raise TypeError(msg)
94 self.trusted_operations.add(operation)
95 self._save_permissions()
96 return True # Indicate success
98 def get_permission_status(self) -> dict[str, Any]:
99 """Get current permission status."""
100 return {
101 "session_id": self.session_id,
102 "trusted_operations_count": len(self.trusted_operations),
103 "trusted_operations": list(self.trusted_operations),
104 "permissions_file": str(self.permissions_file),
105 }
107 def configure_auto_checkpoint(
108 self, enabled: bool = True, frequency: int = 300
109 ) -> bool:
110 """Configure auto-checkpoint settings with security validations."""
111 # Accept all positive frequencies but log security concerns for unsafe values
112 # For security considerations, log if frequency is dangerously low (< 30s)
113 if frequency <= 0:
114 return False # Still reject non-positive frequencies
116 self.auto_checkpoint = enabled
117 if enabled: 117 ↛ 119line 117 didn't jump to line 119 because the condition on line 117 was always true
118 self.checkpoint_frequency = frequency
119 return True
121 def should_auto_checkpoint(self) -> bool:
122 """Determine if auto checkpointing should occur based on configuration."""
123 # For now, return the current setting - in a full implementation
124 # this might check timing since last checkpoint, etc.
125 return self.auto_checkpoint
127 def revoke_all_permissions(self) -> None:
128 """Revoke all trusted permissions (for security reset)."""
129 self.trusted_operations.clear()
130 if self.permissions_file.exists():
131 self.permissions_file.unlink()
133 @classmethod
134 def reset_singleton(cls) -> None:
135 """Reset the singleton instance (for testing)."""
136 cls._instance = None
137 cls._session_id = None
138 cls._initialized = False
140 # Common trusted operations
141 TRUSTED_UV_OPERATIONS = "uv_package_management"
142 TRUSTED_GIT_OPERATIONS = "git_repository_access"
143 TRUSTED_FILE_OPERATIONS = "project_file_access"
144 TRUSTED_SUBPROCESS_OPERATIONS = "subprocess_execution"
145 TRUSTED_NETWORK_OPERATIONS = "network_access"
148# =====================================
149# Configuration Functions
150# =====================================