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

1"""Session permissions management. 

2 

3This module provides the SessionPermissionsManager class for managing 

4trusted operations and permission scopes during sessions. 

5""" 

6 

7from __future__ import annotations 

8 

9import hashlib 

10import json 

11import logging 

12from datetime import datetime 

13from pathlib import Path 

14from typing import TYPE_CHECKING, Any 

15 

16logger = logging.getLogger(__name__) 

17 

18if TYPE_CHECKING: 

19 from typing import Self 

20 

21 

22class SessionPermissionsManager: 

23 """Manages session permissions to avoid repeated prompts for trusted operations.""" 

24 

25 _instance: SessionPermissionsManager | None = None 

26 _session_id: str | None = None 

27 _initialized: bool = False 

28 

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] 

36 

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) 

46 

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 

53 

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 ] 

64 

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 

74 

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) 

84 

85 def is_operation_trusted(self, operation: str) -> bool: 

86 """Check if an operation is already trusted.""" 

87 return operation in self.trusted_operations 

88 

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 

97 

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 } 

106 

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 

115 

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 

120 

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 

126 

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() 

132 

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 

139 

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" 

146 

147 

148# ===================================== 

149# Configuration Functions 

150# =====================================