omnimancer-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
"""Permission controller for managing agent access permissions."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PermissionLevel(Enum):
|
|
13
|
+
"""Permission levels for different operations."""
|
|
14
|
+
|
|
15
|
+
READ = "read"
|
|
16
|
+
WRITE = "write"
|
|
17
|
+
EXECUTE = "execute"
|
|
18
|
+
ADMIN = "admin"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PermissionOperation:
|
|
22
|
+
"""Represents an operation that needs permission validation."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
operation_type: str,
|
|
27
|
+
path: Optional[str] = None,
|
|
28
|
+
command: Optional[str] = None,
|
|
29
|
+
**kwargs,
|
|
30
|
+
):
|
|
31
|
+
self.operation_type = operation_type
|
|
32
|
+
self.path = path
|
|
33
|
+
self.command = command
|
|
34
|
+
self.metadata = kwargs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PermissionController:
|
|
38
|
+
"""Controls and validates permissions for agent operations."""
|
|
39
|
+
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self.restricted_paths = self._get_default_restricted_paths()
|
|
42
|
+
self.allowed_commands = self._get_default_allowed_commands()
|
|
43
|
+
self.permission_rules = self._get_default_permission_rules()
|
|
44
|
+
|
|
45
|
+
# Initialize approval memory storage
|
|
46
|
+
self._approval_memory: Dict[str, Dict[str, Any]] = {}
|
|
47
|
+
|
|
48
|
+
def _get_default_restricted_paths(self) -> Set[str]:
|
|
49
|
+
"""Get default set of restricted file paths."""
|
|
50
|
+
return {
|
|
51
|
+
# SSH and security keys
|
|
52
|
+
".ssh",
|
|
53
|
+
"~/.ssh",
|
|
54
|
+
"/home/*/.ssh",
|
|
55
|
+
"/Users/*/.ssh",
|
|
56
|
+
# Environment and config files
|
|
57
|
+
".env",
|
|
58
|
+
".env.local",
|
|
59
|
+
".env.production",
|
|
60
|
+
# System directories
|
|
61
|
+
"/etc",
|
|
62
|
+
"/System",
|
|
63
|
+
"/sys",
|
|
64
|
+
"/proc",
|
|
65
|
+
"/boot",
|
|
66
|
+
"/root",
|
|
67
|
+
# Package manager directories
|
|
68
|
+
"/usr/bin",
|
|
69
|
+
"/usr/sbin",
|
|
70
|
+
"/sbin",
|
|
71
|
+
# Database files
|
|
72
|
+
"*.db",
|
|
73
|
+
"*.sqlite",
|
|
74
|
+
"*.sqlite3",
|
|
75
|
+
# Credential files
|
|
76
|
+
"*credentials*",
|
|
77
|
+
"*password*",
|
|
78
|
+
"*secret*",
|
|
79
|
+
"*token*",
|
|
80
|
+
"*key*",
|
|
81
|
+
".aws/credentials",
|
|
82
|
+
".config/gcloud",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
def _get_default_allowed_commands(self) -> Set[str]:
|
|
86
|
+
"""Get default set of allowed shell commands."""
|
|
87
|
+
return {
|
|
88
|
+
# File operations
|
|
89
|
+
"ls",
|
|
90
|
+
"cat",
|
|
91
|
+
"head",
|
|
92
|
+
"tail",
|
|
93
|
+
"grep",
|
|
94
|
+
"find",
|
|
95
|
+
"locate",
|
|
96
|
+
"cp",
|
|
97
|
+
"mv",
|
|
98
|
+
"mkdir",
|
|
99
|
+
"rmdir",
|
|
100
|
+
"touch",
|
|
101
|
+
"chmod",
|
|
102
|
+
"chown",
|
|
103
|
+
# Text processing
|
|
104
|
+
"sed",
|
|
105
|
+
"awk",
|
|
106
|
+
"sort",
|
|
107
|
+
"uniq",
|
|
108
|
+
"wc",
|
|
109
|
+
"tr",
|
|
110
|
+
"cut",
|
|
111
|
+
# Basic commands
|
|
112
|
+
"echo",
|
|
113
|
+
"printf",
|
|
114
|
+
"true",
|
|
115
|
+
"false",
|
|
116
|
+
"sleep",
|
|
117
|
+
# Development tools
|
|
118
|
+
"git",
|
|
119
|
+
"npm",
|
|
120
|
+
"pip",
|
|
121
|
+
"python",
|
|
122
|
+
"node",
|
|
123
|
+
"go",
|
|
124
|
+
"cargo",
|
|
125
|
+
"make",
|
|
126
|
+
"cmake",
|
|
127
|
+
"gcc",
|
|
128
|
+
"clang",
|
|
129
|
+
# System info (read-only)
|
|
130
|
+
"ps",
|
|
131
|
+
"top",
|
|
132
|
+
"df",
|
|
133
|
+
"du",
|
|
134
|
+
"free",
|
|
135
|
+
"uptime",
|
|
136
|
+
"whoami",
|
|
137
|
+
"pwd",
|
|
138
|
+
"which",
|
|
139
|
+
"whereis",
|
|
140
|
+
"uname",
|
|
141
|
+
# Network (limited)
|
|
142
|
+
"curl",
|
|
143
|
+
"wget",
|
|
144
|
+
"ping",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def _get_default_permission_rules(self) -> Dict[str, PermissionLevel]:
|
|
148
|
+
"""Get default permission rules for operation types."""
|
|
149
|
+
return {
|
|
150
|
+
"file_read": PermissionLevel.READ,
|
|
151
|
+
"file_write": PermissionLevel.WRITE,
|
|
152
|
+
"file_delete": PermissionLevel.WRITE,
|
|
153
|
+
"file_execute": PermissionLevel.EXECUTE,
|
|
154
|
+
"command_execute": PermissionLevel.EXECUTE,
|
|
155
|
+
"network_request": PermissionLevel.READ,
|
|
156
|
+
"system_info": PermissionLevel.READ,
|
|
157
|
+
"package_install": PermissionLevel.ADMIN,
|
|
158
|
+
"service_control": PermissionLevel.ADMIN,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
def validate_path_access(self, path: str, operation: str = "read") -> bool:
|
|
162
|
+
"""Validate if path access is allowed."""
|
|
163
|
+
try:
|
|
164
|
+
# Normalize path
|
|
165
|
+
normalized_path = str(Path(path).resolve())
|
|
166
|
+
|
|
167
|
+
# Check against restricted paths
|
|
168
|
+
for restricted in self.restricted_paths:
|
|
169
|
+
if self._path_matches_pattern(normalized_path, restricted):
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
# Additional checks for write operations
|
|
173
|
+
if operation in ["write", "delete", "execute"]:
|
|
174
|
+
# Don't allow writes to system directories
|
|
175
|
+
system_prefixes = ["/usr", "/etc", "/System", "/sys", "/proc"]
|
|
176
|
+
if any(
|
|
177
|
+
normalized_path.startswith(prefix) for prefix in system_prefixes
|
|
178
|
+
):
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
# Allow writes to project directory and safe temporary directories
|
|
182
|
+
project_root = os.getcwd()
|
|
183
|
+
safe_temp_prefixes = [
|
|
184
|
+
"/tmp",
|
|
185
|
+
"/var/tmp",
|
|
186
|
+
tempfile.gettempdir(),
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
# Check if path is in project directory or safe temp directory
|
|
190
|
+
is_in_project = normalized_path.startswith(project_root)
|
|
191
|
+
is_in_safe_temp = any(
|
|
192
|
+
normalized_path.startswith(prefix) for prefix in safe_temp_prefixes
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if not (is_in_project or is_in_safe_temp):
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
except (OSError, ValueError):
|
|
201
|
+
# If path resolution fails, deny access
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
def validate_command(self, command: str) -> bool:
|
|
205
|
+
"""Validate if command execution is allowed."""
|
|
206
|
+
# Extract base command (first word)
|
|
207
|
+
base_command = command.strip().split()[0]
|
|
208
|
+
|
|
209
|
+
# Remove path prefixes
|
|
210
|
+
base_command = os.path.basename(base_command)
|
|
211
|
+
|
|
212
|
+
# Check if command is in allowed list
|
|
213
|
+
if base_command not in self.allowed_commands:
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
# Additional security checks
|
|
217
|
+
if self._contains_dangerous_patterns(command):
|
|
218
|
+
return False
|
|
219
|
+
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
def validate_operation(self, operation: PermissionOperation) -> bool:
|
|
223
|
+
"""Validate if an operation is allowed."""
|
|
224
|
+
op_type = operation.operation_type
|
|
225
|
+
|
|
226
|
+
# Check if operation type is allowed
|
|
227
|
+
if op_type not in self.permission_rules:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
# Validate path if provided
|
|
231
|
+
if operation.path:
|
|
232
|
+
required_level = self.permission_rules[op_type]
|
|
233
|
+
access_type = self._permission_level_to_access_type(required_level)
|
|
234
|
+
if not self.validate_path_access(operation.path, access_type):
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
# Validate command if provided
|
|
238
|
+
if operation.command:
|
|
239
|
+
if not self.validate_command(operation.command):
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
return True
|
|
243
|
+
|
|
244
|
+
def _path_matches_pattern(self, path: str, pattern: str) -> bool:
|
|
245
|
+
"""Check if path matches a pattern (supports wildcards)."""
|
|
246
|
+
# Expand user home directory
|
|
247
|
+
pattern = os.path.expanduser(pattern)
|
|
248
|
+
|
|
249
|
+
# Handle relative patterns (like .ssh, .env) by checking if they appear in the path
|
|
250
|
+
if not pattern.startswith("/") and not pattern.startswith("~"):
|
|
251
|
+
# For relative patterns, check if the pattern matches any part of the path
|
|
252
|
+
path_parts = path.split("/")
|
|
253
|
+
for i, part in enumerate(path_parts):
|
|
254
|
+
if "*" not in pattern:
|
|
255
|
+
# Exact match for directory/file name
|
|
256
|
+
if part == pattern:
|
|
257
|
+
return True
|
|
258
|
+
# Check if any part of the path ends with the pattern (for files in directories)
|
|
259
|
+
remaining_path = "/".join(path_parts[i:])
|
|
260
|
+
if (
|
|
261
|
+
remaining_path.startswith(pattern + "/")
|
|
262
|
+
or remaining_path == pattern
|
|
263
|
+
):
|
|
264
|
+
return True
|
|
265
|
+
else:
|
|
266
|
+
# Wildcard pattern matching
|
|
267
|
+
regex_pattern = pattern.replace("*", ".*")
|
|
268
|
+
if re.match(f"^{regex_pattern}$", part, re.IGNORECASE):
|
|
269
|
+
return True
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
# For absolute patterns, use the original logic
|
|
273
|
+
regex_pattern = pattern.replace("*", ".*")
|
|
274
|
+
|
|
275
|
+
# For exact matches (no wildcards), check if path starts with pattern
|
|
276
|
+
if "*" not in pattern:
|
|
277
|
+
# Check if path starts with the pattern (directory matching)
|
|
278
|
+
if path.startswith(pattern):
|
|
279
|
+
# Ensure it's a proper directory match (either exact or followed by /)
|
|
280
|
+
if len(path) == len(pattern) or path[len(pattern)] == "/":
|
|
281
|
+
return True
|
|
282
|
+
# Also check exact match
|
|
283
|
+
regex_pattern = f"^{re.escape(pattern)}$"
|
|
284
|
+
else:
|
|
285
|
+
# For wildcard patterns, use full regex matching
|
|
286
|
+
regex_pattern = f"^{regex_pattern}$"
|
|
287
|
+
|
|
288
|
+
return bool(re.match(regex_pattern, path, re.IGNORECASE))
|
|
289
|
+
|
|
290
|
+
def _contains_dangerous_patterns(self, command: str) -> bool:
|
|
291
|
+
"""Check if command contains dangerous patterns."""
|
|
292
|
+
dangerous_patterns = [
|
|
293
|
+
r"[;&|]", # Command chaining
|
|
294
|
+
r"`", # Command substitution
|
|
295
|
+
r"\$\(", # Command substitution
|
|
296
|
+
r">\s*/dev/", # Device access
|
|
297
|
+
r"rm\s+-rf", # Recursive deletion
|
|
298
|
+
r"sudo", # Privilege escalation
|
|
299
|
+
r"su\s", # User switching
|
|
300
|
+
r"chmod\s+[0-7]{3,4}", # Permission changes
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
for pattern in dangerous_patterns:
|
|
304
|
+
if re.search(pattern, command):
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def _permission_level_to_access_type(self, level: PermissionLevel) -> str:
|
|
310
|
+
"""Convert permission level to access type string."""
|
|
311
|
+
if level == PermissionLevel.READ:
|
|
312
|
+
return "read"
|
|
313
|
+
elif level == PermissionLevel.WRITE:
|
|
314
|
+
return "write"
|
|
315
|
+
elif level == PermissionLevel.EXECUTE:
|
|
316
|
+
return "execute"
|
|
317
|
+
else:
|
|
318
|
+
return "admin"
|
|
319
|
+
|
|
320
|
+
def add_restricted_path(self, path: str) -> None:
|
|
321
|
+
"""Add a path to the restricted list."""
|
|
322
|
+
self.restricted_paths.add(path)
|
|
323
|
+
|
|
324
|
+
def remove_restricted_path(self, path: str) -> None:
|
|
325
|
+
"""Remove a path from the restricted list."""
|
|
326
|
+
self.restricted_paths.discard(path)
|
|
327
|
+
|
|
328
|
+
def add_allowed_command(self, command: str) -> None:
|
|
329
|
+
"""Add a command to the allowed list."""
|
|
330
|
+
self.allowed_commands.add(command)
|
|
331
|
+
|
|
332
|
+
def remove_allowed_command(self, command: str) -> None:
|
|
333
|
+
"""Remove a command from the allowed list."""
|
|
334
|
+
self.allowed_commands.discard(command)
|
|
335
|
+
|
|
336
|
+
def get_restricted_paths(self) -> List[str]:
|
|
337
|
+
"""Get list of restricted paths."""
|
|
338
|
+
return list(self.restricted_paths)
|
|
339
|
+
|
|
340
|
+
def get_allowed_commands(self) -> List[str]:
|
|
341
|
+
"""Get list of allowed commands."""
|
|
342
|
+
return list(self.allowed_commands)
|
|
343
|
+
|
|
344
|
+
async def check_operation_permission(
|
|
345
|
+
self,
|
|
346
|
+
operation_type: str,
|
|
347
|
+
operation_signature: str,
|
|
348
|
+
operation_data: Dict[str, Any],
|
|
349
|
+
) -> bool:
|
|
350
|
+
"""
|
|
351
|
+
Check if operation has stored permission (remembered approval).
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
operation_type: Type of operation
|
|
355
|
+
operation_signature: Operation signature for matching
|
|
356
|
+
operation_data: Operation data for validation
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
True if operation has stored permission, False otherwise
|
|
360
|
+
"""
|
|
361
|
+
try:
|
|
362
|
+
# Check if we have stored approval for this signature
|
|
363
|
+
if operation_signature in self._approval_memory:
|
|
364
|
+
approval_data = self._approval_memory[operation_signature]
|
|
365
|
+
|
|
366
|
+
# Check if approval has expired
|
|
367
|
+
if "expires_at" in approval_data:
|
|
368
|
+
expires_at = datetime.fromisoformat(approval_data["expires_at"])
|
|
369
|
+
if datetime.now() > expires_at:
|
|
370
|
+
# Remove expired approval
|
|
371
|
+
del self._approval_memory[operation_signature]
|
|
372
|
+
return False
|
|
373
|
+
|
|
374
|
+
# Verify operation type matches
|
|
375
|
+
if approval_data.get("operation_type") == operation_type:
|
|
376
|
+
return True
|
|
377
|
+
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
except Exception:
|
|
381
|
+
# If there's any error in checking, don't auto-approve
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
async def grant_operation_permission(
|
|
385
|
+
self,
|
|
386
|
+
operation_type: str,
|
|
387
|
+
operation_signature: str,
|
|
388
|
+
metadata: Dict[str, Any],
|
|
389
|
+
expires_hours: int = 24,
|
|
390
|
+
) -> None:
|
|
391
|
+
"""
|
|
392
|
+
Store operation permission for future auto-approval.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
operation_type: Type of operation
|
|
396
|
+
operation_signature: Operation signature for matching
|
|
397
|
+
metadata: Additional metadata about the approval
|
|
398
|
+
expires_hours: Hours until approval expires (default 24)
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
expires_at = datetime.now() + timedelta(hours=expires_hours)
|
|
402
|
+
|
|
403
|
+
self._approval_memory[operation_signature] = {
|
|
404
|
+
"operation_type": operation_type,
|
|
405
|
+
"stored_at": datetime.now().isoformat(),
|
|
406
|
+
"expires_at": expires_at.isoformat(),
|
|
407
|
+
"metadata": metadata,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
except Exception:
|
|
411
|
+
# Log error but don't fail the operation
|
|
412
|
+
pass
|
|
413
|
+
|
|
414
|
+
def get_stored_approvals(self) -> Dict[str, Dict[str, Any]]:
|
|
415
|
+
"""Get all stored approvals with their metadata."""
|
|
416
|
+
return self._approval_memory.copy()
|
|
417
|
+
|
|
418
|
+
def get_permission_rules(self) -> List[Dict[str, Any]]:
|
|
419
|
+
"""Get current permission rules in dictionary format."""
|
|
420
|
+
rules = []
|
|
421
|
+
rule_id = 1
|
|
422
|
+
|
|
423
|
+
# Convert permission rules to display format
|
|
424
|
+
for op_type, level in self.permission_rules.items():
|
|
425
|
+
rule = {
|
|
426
|
+
"id": str(rule_id),
|
|
427
|
+
"pattern": op_type.replace("_", " ").title(),
|
|
428
|
+
"type": "operation",
|
|
429
|
+
"level": level.value,
|
|
430
|
+
}
|
|
431
|
+
rules.append(rule)
|
|
432
|
+
rule_id += 1
|
|
433
|
+
|
|
434
|
+
# Add path-based rules
|
|
435
|
+
for path in self.restricted_paths:
|
|
436
|
+
rule = {
|
|
437
|
+
"id": str(rule_id),
|
|
438
|
+
"pattern": path,
|
|
439
|
+
"type": "file",
|
|
440
|
+
"level": "blocked",
|
|
441
|
+
}
|
|
442
|
+
rules.append(rule)
|
|
443
|
+
rule_id += 1
|
|
444
|
+
|
|
445
|
+
# Add command rules
|
|
446
|
+
for command in list(self.allowed_commands)[:10]: # Limit for display
|
|
447
|
+
rule = {
|
|
448
|
+
"id": str(rule_id),
|
|
449
|
+
"pattern": command,
|
|
450
|
+
"type": "command",
|
|
451
|
+
"level": "allowed",
|
|
452
|
+
}
|
|
453
|
+
rules.append(rule)
|
|
454
|
+
rule_id += 1
|
|
455
|
+
|
|
456
|
+
return rules
|
|
457
|
+
|
|
458
|
+
def get_learned_permissions(self) -> List[Dict[str, Any]]:
|
|
459
|
+
"""Get learned permissions in display format."""
|
|
460
|
+
learned = []
|
|
461
|
+
for signature, approval_data in self._approval_memory.items():
|
|
462
|
+
learned.append(
|
|
463
|
+
{
|
|
464
|
+
"pattern": signature[:50] + ("..." if len(signature) > 50 else ""),
|
|
465
|
+
"decision": "approved",
|
|
466
|
+
"count": 1, # Could track actual usage count in the future
|
|
467
|
+
"last_used": approval_data.get("stored_at", ""),
|
|
468
|
+
}
|
|
469
|
+
)
|
|
470
|
+
return learned
|
|
471
|
+
|
|
472
|
+
def revoke_approval(self, operation_signature: str) -> bool:
|
|
473
|
+
"""
|
|
474
|
+
Revoke a stored approval.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
operation_signature: Signature of approval to revoke
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
True if approval was found and revoked, False otherwise
|
|
481
|
+
"""
|
|
482
|
+
if operation_signature in self._approval_memory:
|
|
483
|
+
del self._approval_memory[operation_signature]
|
|
484
|
+
return True
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
def cleanup_expired_approvals(self) -> int:
|
|
488
|
+
"""
|
|
489
|
+
Remove expired approvals from memory.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Number of approvals that were removed
|
|
493
|
+
"""
|
|
494
|
+
expired_signatures = []
|
|
495
|
+
now = datetime.now()
|
|
496
|
+
|
|
497
|
+
for signature, approval_data in self._approval_memory.items():
|
|
498
|
+
if "expires_at" in approval_data:
|
|
499
|
+
try:
|
|
500
|
+
expires_at = datetime.fromisoformat(approval_data["expires_at"])
|
|
501
|
+
if now > expires_at:
|
|
502
|
+
expired_signatures.append(signature)
|
|
503
|
+
except Exception:
|
|
504
|
+
# If we can't parse the date, consider it expired
|
|
505
|
+
expired_signatures.append(signature)
|
|
506
|
+
|
|
507
|
+
# Remove expired approvals
|
|
508
|
+
for signature in expired_signatures:
|
|
509
|
+
del self._approval_memory[signature]
|
|
510
|
+
|
|
511
|
+
return len(expired_signatures)
|