1"""Session permissions management.

2

3 This module provides the SessionPermissionsManager class for managing

4 trusted operations and permission scopes during sessions.

5"""

6

7 from __future__ import annotations

8

9 import hashlib

10 import json

11 import logging

12 from datetime import datetime

13 from pathlib import Path

14 from typing import TYPE_CHECKING, Any

15

16 logger = logging.getLogger(__name__)

17

18 if TYPE_CHECKING:

19 from typing import Self

20

21

22 class 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 ↛ 50 line 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 ↛ 119 line 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# =====================================