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,729 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Batch Approval Filtering and Sorting Logic for Omnimancer CLI.
|
|
3
|
+
|
|
4
|
+
This module provides filtering and sorting capabilities to handle large
|
|
5
|
+
batches of approval requests efficiently with complex query support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import fnmatch
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
16
|
+
|
|
17
|
+
from ..core.agent.approval_manager import BatchApprovalRequest, ChangePreview
|
|
18
|
+
from ..core.agent.types import Operation, OperationType
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FilterOperator(Enum):
|
|
24
|
+
"""Filter operators for query expressions."""
|
|
25
|
+
|
|
26
|
+
AND = "AND"
|
|
27
|
+
OR = "OR"
|
|
28
|
+
NOT = "NOT"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SortDirection(Enum):
|
|
32
|
+
"""Sort direction options."""
|
|
33
|
+
|
|
34
|
+
ASCENDING = "asc"
|
|
35
|
+
DESCENDING = "desc"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class SortBy(Enum):
|
|
39
|
+
"""Sort criteria options."""
|
|
40
|
+
|
|
41
|
+
TIMESTAMP = "timestamp"
|
|
42
|
+
RISK_LEVEL = "risk"
|
|
43
|
+
ACTION_TYPE = "type"
|
|
44
|
+
TARGET = "target"
|
|
45
|
+
STATUS = "status"
|
|
46
|
+
ALPHABETICAL = "alpha"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class FilterCriteria:
|
|
51
|
+
"""Represents a single filter criterion."""
|
|
52
|
+
|
|
53
|
+
field: str
|
|
54
|
+
operator: str
|
|
55
|
+
value: str
|
|
56
|
+
negated: bool = False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class SortCriteria:
|
|
61
|
+
"""Represents sort configuration."""
|
|
62
|
+
|
|
63
|
+
sort_by: SortBy
|
|
64
|
+
direction: SortDirection = SortDirection.ASCENDING
|
|
65
|
+
secondary_sort: Optional["SortCriteria"] = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BatchFilter(ABC):
|
|
69
|
+
"""Abstract base class for batch filters."""
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def matches(
|
|
73
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
74
|
+
) -> bool:
|
|
75
|
+
"""Check if operation matches this filter."""
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def get_description(self) -> str:
|
|
80
|
+
"""Get human-readable description of this filter."""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ActionTypeFilter(BatchFilter):
|
|
85
|
+
"""Filter operations by action type."""
|
|
86
|
+
|
|
87
|
+
def __init__(self, operation_types: List[OperationType]):
|
|
88
|
+
"""
|
|
89
|
+
Initialize action type filter.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
operation_types: List of operation types to match
|
|
93
|
+
"""
|
|
94
|
+
self.operation_types = set(operation_types)
|
|
95
|
+
|
|
96
|
+
def matches(
|
|
97
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
98
|
+
) -> bool:
|
|
99
|
+
"""Check if operation type matches."""
|
|
100
|
+
return operation.type in self.operation_types
|
|
101
|
+
|
|
102
|
+
def get_description(self) -> str:
|
|
103
|
+
"""Get description of this filter."""
|
|
104
|
+
types = [op.value for op in self.operation_types]
|
|
105
|
+
return f"Type: {', '.join(types)}"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class RiskLevelFilter(BatchFilter):
|
|
109
|
+
"""Filter operations by risk level."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, risk_levels: List[str]):
|
|
112
|
+
"""
|
|
113
|
+
Initialize risk level filter.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
risk_levels: List of risk levels to match (low, medium, high, critical)
|
|
117
|
+
"""
|
|
118
|
+
self.risk_levels = set(level.lower() for level in risk_levels)
|
|
119
|
+
|
|
120
|
+
def matches(
|
|
121
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Check if risk level matches."""
|
|
124
|
+
if not preview or not preview.risk_assessment:
|
|
125
|
+
return "unknown" in self.risk_levels
|
|
126
|
+
|
|
127
|
+
risk_level = self._extract_risk_level(preview.risk_assessment)
|
|
128
|
+
return risk_level in self.risk_levels
|
|
129
|
+
|
|
130
|
+
def _extract_risk_level(self, risk_assessment: str) -> str:
|
|
131
|
+
"""Extract risk level from assessment string."""
|
|
132
|
+
risk_lower = risk_assessment.lower()
|
|
133
|
+
if "critical" in risk_lower:
|
|
134
|
+
return "critical"
|
|
135
|
+
elif "high" in risk_lower:
|
|
136
|
+
return "high"
|
|
137
|
+
elif "medium" in risk_lower:
|
|
138
|
+
return "medium"
|
|
139
|
+
elif "low" in risk_lower:
|
|
140
|
+
return "low"
|
|
141
|
+
else:
|
|
142
|
+
return "unknown"
|
|
143
|
+
|
|
144
|
+
def get_description(self) -> str:
|
|
145
|
+
"""Get description of this filter."""
|
|
146
|
+
return f"Risk: {', '.join(self.risk_levels)}"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TargetPatternFilter(BatchFilter):
|
|
150
|
+
"""Filter operations by target pattern using glob matching."""
|
|
151
|
+
|
|
152
|
+
def __init__(self, patterns: List[str]):
|
|
153
|
+
"""
|
|
154
|
+
Initialize target pattern filter.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
patterns: List of glob patterns to match against targets
|
|
158
|
+
"""
|
|
159
|
+
self.patterns = patterns
|
|
160
|
+
|
|
161
|
+
def matches(
|
|
162
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
163
|
+
) -> bool:
|
|
164
|
+
"""Check if operation target matches any pattern."""
|
|
165
|
+
target = self._extract_target(operation)
|
|
166
|
+
if not target:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
return any(
|
|
170
|
+
fnmatch.fnmatch(target.lower(), pattern.lower())
|
|
171
|
+
for pattern in self.patterns
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def _extract_target(self, operation: Operation) -> Optional[str]:
|
|
175
|
+
"""Extract target from operation data."""
|
|
176
|
+
if operation.data:
|
|
177
|
+
# Try common target fields
|
|
178
|
+
for field in ["path", "url", "command", "target"]:
|
|
179
|
+
if field in operation.data:
|
|
180
|
+
return str(operation.data[field])
|
|
181
|
+
|
|
182
|
+
return operation.description
|
|
183
|
+
|
|
184
|
+
def get_description(self) -> str:
|
|
185
|
+
"""Get description of this filter."""
|
|
186
|
+
return f"Target: {', '.join(self.patterns)}"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class StatusFilter(BatchFilter):
|
|
190
|
+
"""Filter operations by approval status."""
|
|
191
|
+
|
|
192
|
+
def __init__(self, statuses: List[str], batch_request: BatchApprovalRequest):
|
|
193
|
+
"""
|
|
194
|
+
Initialize status filter.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
statuses: List of statuses to match (pending, approved)
|
|
198
|
+
batch_request: Batch request for status lookup
|
|
199
|
+
"""
|
|
200
|
+
self.statuses = set(status.lower() for status in statuses)
|
|
201
|
+
self.batch_request = batch_request
|
|
202
|
+
|
|
203
|
+
def matches(
|
|
204
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
205
|
+
) -> bool:
|
|
206
|
+
"""Check if operation status matches."""
|
|
207
|
+
# Find operation index in batch
|
|
208
|
+
try:
|
|
209
|
+
op_index = self.batch_request.operations.index(operation)
|
|
210
|
+
is_approved = op_index in self.batch_request.approved_operations
|
|
211
|
+
|
|
212
|
+
current_status = "approved" if is_approved else "pending"
|
|
213
|
+
return current_status in self.statuses
|
|
214
|
+
except ValueError:
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
def get_description(self) -> str:
|
|
218
|
+
"""Get description of this filter."""
|
|
219
|
+
return f"Status: {', '.join(self.statuses)}"
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class CustomQueryFilter(BatchFilter):
|
|
223
|
+
"""Filter operations using custom query expressions."""
|
|
224
|
+
|
|
225
|
+
def __init__(self, query: str, batch_request: BatchApprovalRequest):
|
|
226
|
+
"""
|
|
227
|
+
Initialize custom query filter.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
query: Query expression (e.g., "risk:high AND type:file_write")
|
|
231
|
+
batch_request: Batch request for context
|
|
232
|
+
"""
|
|
233
|
+
self.query = query
|
|
234
|
+
self.batch_request = batch_request
|
|
235
|
+
self.parsed_filters = self._parse_query(query)
|
|
236
|
+
|
|
237
|
+
def matches(
|
|
238
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
239
|
+
) -> bool:
|
|
240
|
+
"""Check if operation matches the query."""
|
|
241
|
+
return self._evaluate_filters(self.parsed_filters, operation, preview)
|
|
242
|
+
|
|
243
|
+
def _parse_query(self, query: str) -> List[Any]:
|
|
244
|
+
"""Parse query into filter components."""
|
|
245
|
+
# Simple query parser - supports basic expressions like:
|
|
246
|
+
# "risk:high", "type:file_write", "target:*.py"
|
|
247
|
+
# "risk:high AND type:file_write"
|
|
248
|
+
# "NOT status:approved"
|
|
249
|
+
|
|
250
|
+
# Tokenize the query
|
|
251
|
+
tokens = re.findall(
|
|
252
|
+
r"\w+:\w+|\w+:\*?\.\w+|\w+:\*|AND|OR|NOT|\(|\)",
|
|
253
|
+
query,
|
|
254
|
+
re.IGNORECASE,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
filters = []
|
|
258
|
+
current_filter = None
|
|
259
|
+
operator = None
|
|
260
|
+
negated = False
|
|
261
|
+
|
|
262
|
+
for token in tokens:
|
|
263
|
+
token_upper = token.upper()
|
|
264
|
+
|
|
265
|
+
if token_upper == "AND":
|
|
266
|
+
if current_filter:
|
|
267
|
+
filters.append(current_filter)
|
|
268
|
+
operator = FilterOperator.AND
|
|
269
|
+
current_filter = None
|
|
270
|
+
elif token_upper == "OR":
|
|
271
|
+
if current_filter:
|
|
272
|
+
filters.append(current_filter)
|
|
273
|
+
operator = FilterOperator.OR
|
|
274
|
+
current_filter = None
|
|
275
|
+
elif token_upper == "NOT":
|
|
276
|
+
negated = True
|
|
277
|
+
elif ":" in token:
|
|
278
|
+
# Field:value expression
|
|
279
|
+
field, value = token.split(":", 1)
|
|
280
|
+
filter_obj = self._create_field_filter(field.lower(), value.lower())
|
|
281
|
+
if filter_obj:
|
|
282
|
+
if negated:
|
|
283
|
+
filter_obj = NegatedFilter(filter_obj)
|
|
284
|
+
negated = False
|
|
285
|
+
|
|
286
|
+
if operator:
|
|
287
|
+
filters.append((operator, filter_obj))
|
|
288
|
+
operator = None
|
|
289
|
+
else:
|
|
290
|
+
current_filter = filter_obj
|
|
291
|
+
|
|
292
|
+
if current_filter:
|
|
293
|
+
filters.append(current_filter)
|
|
294
|
+
|
|
295
|
+
return filters
|
|
296
|
+
|
|
297
|
+
def _create_field_filter(self, field: str, value: str) -> Optional[BatchFilter]:
|
|
298
|
+
"""Create appropriate filter based on field and value."""
|
|
299
|
+
if field == "risk":
|
|
300
|
+
return RiskLevelFilter([value])
|
|
301
|
+
elif field == "type":
|
|
302
|
+
# Map common type names to OperationType
|
|
303
|
+
type_map = {
|
|
304
|
+
"file_read": OperationType.FILE_READ,
|
|
305
|
+
"file_write": OperationType.FILE_WRITE,
|
|
306
|
+
"file_delete": OperationType.FILE_DELETE,
|
|
307
|
+
"command": OperationType.COMMAND_EXECUTE,
|
|
308
|
+
"web": OperationType.WEB_REQUEST,
|
|
309
|
+
"mcp": OperationType.MCP_TOOL_CALL,
|
|
310
|
+
}
|
|
311
|
+
op_type = type_map.get(value)
|
|
312
|
+
if op_type:
|
|
313
|
+
return ActionTypeFilter([op_type])
|
|
314
|
+
elif field == "target":
|
|
315
|
+
return TargetPatternFilter([value])
|
|
316
|
+
elif field == "status":
|
|
317
|
+
return StatusFilter([value], self.batch_request)
|
|
318
|
+
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
def _evaluate_filters(
|
|
322
|
+
self,
|
|
323
|
+
filters: List[Any],
|
|
324
|
+
operation: Operation,
|
|
325
|
+
preview: Optional[ChangePreview],
|
|
326
|
+
) -> bool:
|
|
327
|
+
"""Evaluate parsed filters against operation."""
|
|
328
|
+
if not filters:
|
|
329
|
+
return True
|
|
330
|
+
|
|
331
|
+
result = True
|
|
332
|
+
|
|
333
|
+
for item in filters:
|
|
334
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
335
|
+
# Operator and filter
|
|
336
|
+
op, filter_obj = item
|
|
337
|
+
filter_result = filter_obj.matches(operation, preview)
|
|
338
|
+
|
|
339
|
+
if op == FilterOperator.AND:
|
|
340
|
+
result = result and filter_result
|
|
341
|
+
elif op == FilterOperator.OR:
|
|
342
|
+
result = result or filter_result
|
|
343
|
+
elif isinstance(item, BatchFilter):
|
|
344
|
+
# Direct filter
|
|
345
|
+
filter_result = item.matches(operation, preview)
|
|
346
|
+
result = result and filter_result
|
|
347
|
+
|
|
348
|
+
return result
|
|
349
|
+
|
|
350
|
+
def get_description(self) -> str:
|
|
351
|
+
"""Get description of this filter."""
|
|
352
|
+
return f"Query: {self.query}"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class NegatedFilter(BatchFilter):
|
|
356
|
+
"""Wrapper for negated filters."""
|
|
357
|
+
|
|
358
|
+
def __init__(self, base_filter: BatchFilter):
|
|
359
|
+
"""Initialize negated filter."""
|
|
360
|
+
self.base_filter = base_filter
|
|
361
|
+
|
|
362
|
+
def matches(
|
|
363
|
+
self, operation: Operation, preview: Optional[ChangePreview] = None
|
|
364
|
+
) -> bool:
|
|
365
|
+
"""Return negated result of base filter."""
|
|
366
|
+
return not self.base_filter.matches(operation, preview)
|
|
367
|
+
|
|
368
|
+
def get_description(self) -> str:
|
|
369
|
+
"""Get description of negated filter."""
|
|
370
|
+
return f"NOT ({self.base_filter.get_description()})"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class BatchFilterManager:
|
|
374
|
+
"""Manages multiple filters and applies them to batch requests."""
|
|
375
|
+
|
|
376
|
+
def __init__(self):
|
|
377
|
+
"""Initialize batch filter manager."""
|
|
378
|
+
self.active_filters: List[BatchFilter] = []
|
|
379
|
+
|
|
380
|
+
def add_filter(self, batch_filter: BatchFilter):
|
|
381
|
+
"""Add a filter to the active filters."""
|
|
382
|
+
self.active_filters.append(batch_filter)
|
|
383
|
+
|
|
384
|
+
def remove_filter(self, index: int) -> bool:
|
|
385
|
+
"""Remove filter by index."""
|
|
386
|
+
if 0 <= index < len(self.active_filters):
|
|
387
|
+
del self.active_filters[index]
|
|
388
|
+
return True
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
def clear_filters(self):
|
|
392
|
+
"""Remove all active filters."""
|
|
393
|
+
self.active_filters.clear()
|
|
394
|
+
|
|
395
|
+
def apply_filters(
|
|
396
|
+
self, batch_request: BatchApprovalRequest
|
|
397
|
+
) -> Tuple[List[Operation], List[ChangePreview]]:
|
|
398
|
+
"""
|
|
399
|
+
Apply active filters to batch request.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
batch_request: Batch request to filter
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Tuple of filtered operations and previews
|
|
406
|
+
"""
|
|
407
|
+
if not self.active_filters:
|
|
408
|
+
return batch_request.operations, batch_request.previews
|
|
409
|
+
|
|
410
|
+
filtered_ops = []
|
|
411
|
+
filtered_previews = []
|
|
412
|
+
|
|
413
|
+
for i, (operation, preview) in enumerate(
|
|
414
|
+
zip(batch_request.operations, batch_request.previews)
|
|
415
|
+
):
|
|
416
|
+
if self._matches_all_filters(operation, preview):
|
|
417
|
+
filtered_ops.append(operation)
|
|
418
|
+
filtered_previews.append(preview)
|
|
419
|
+
|
|
420
|
+
return filtered_ops, filtered_previews
|
|
421
|
+
|
|
422
|
+
def _matches_all_filters(
|
|
423
|
+
self, operation: Operation, preview: Optional[ChangePreview]
|
|
424
|
+
) -> bool:
|
|
425
|
+
"""Check if operation matches all active filters."""
|
|
426
|
+
return all(f.matches(operation, preview) for f in self.active_filters)
|
|
427
|
+
|
|
428
|
+
def get_filter_descriptions(self) -> List[str]:
|
|
429
|
+
"""Get descriptions of all active filters."""
|
|
430
|
+
return [f.get_description() for f in self.active_filters]
|
|
431
|
+
|
|
432
|
+
def get_filter_count(self) -> int:
|
|
433
|
+
"""Get number of active filters."""
|
|
434
|
+
return len(self.active_filters)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
class BatchSorter:
|
|
438
|
+
"""Handles sorting of batch operations."""
|
|
439
|
+
|
|
440
|
+
def __init__(self):
|
|
441
|
+
"""Initialize batch sorter."""
|
|
442
|
+
self.sort_functions = {
|
|
443
|
+
SortBy.TIMESTAMP: self._sort_by_timestamp,
|
|
444
|
+
SortBy.RISK_LEVEL: self._sort_by_risk,
|
|
445
|
+
SortBy.ACTION_TYPE: self._sort_by_action_type,
|
|
446
|
+
SortBy.TARGET: self._sort_by_target,
|
|
447
|
+
SortBy.STATUS: self._sort_by_status,
|
|
448
|
+
SortBy.ALPHABETICAL: self._sort_by_alphabetical,
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
def sort_batch(
|
|
452
|
+
self,
|
|
453
|
+
operations: List[Operation],
|
|
454
|
+
previews: List[ChangePreview],
|
|
455
|
+
criteria: SortCriteria,
|
|
456
|
+
batch_request: Optional[BatchApprovalRequest] = None,
|
|
457
|
+
) -> Tuple[List[Operation], List[ChangePreview]]:
|
|
458
|
+
"""
|
|
459
|
+
Sort operations and previews according to criteria.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
operations: Operations to sort
|
|
463
|
+
previews: Corresponding previews
|
|
464
|
+
criteria: Sort criteria
|
|
465
|
+
batch_request: Optional batch request for additional context
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Tuple of sorted operations and previews
|
|
469
|
+
"""
|
|
470
|
+
if not operations:
|
|
471
|
+
return operations, previews
|
|
472
|
+
|
|
473
|
+
# Create list of tuples for sorting
|
|
474
|
+
items = list(zip(operations, previews))
|
|
475
|
+
|
|
476
|
+
# Get sort function
|
|
477
|
+
sort_func = self.sort_functions.get(criteria.sort_by)
|
|
478
|
+
if not sort_func:
|
|
479
|
+
logger.warning(f"Unknown sort criteria: {criteria.sort_by}")
|
|
480
|
+
return operations, previews
|
|
481
|
+
|
|
482
|
+
# Sort items
|
|
483
|
+
sorted_items = sort_func(items, criteria.direction, batch_request)
|
|
484
|
+
|
|
485
|
+
# Apply secondary sort if specified
|
|
486
|
+
if criteria.secondary_sort:
|
|
487
|
+
secondary_func = self.sort_functions.get(criteria.secondary_sort.sort_by)
|
|
488
|
+
if secondary_func:
|
|
489
|
+
# Stable sort to preserve primary ordering
|
|
490
|
+
sorted_items = secondary_func(
|
|
491
|
+
sorted_items,
|
|
492
|
+
criteria.secondary_sort.direction,
|
|
493
|
+
batch_request,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Unzip back to separate lists
|
|
497
|
+
if sorted_items:
|
|
498
|
+
sorted_ops, sorted_previews = zip(*sorted_items)
|
|
499
|
+
return list(sorted_ops), list(sorted_previews)
|
|
500
|
+
else:
|
|
501
|
+
return [], []
|
|
502
|
+
|
|
503
|
+
def _sort_by_timestamp(
|
|
504
|
+
self,
|
|
505
|
+
items: List[Tuple[Operation, ChangePreview]],
|
|
506
|
+
direction: SortDirection,
|
|
507
|
+
batch_request: Optional[BatchApprovalRequest],
|
|
508
|
+
) -> List[Tuple[Operation, ChangePreview]]:
|
|
509
|
+
"""Sort by timestamp (using batch creation time as proxy)."""
|
|
510
|
+
# Since operations don't have individual timestamps, use batch timestamp
|
|
511
|
+
# In practice, this would sort by when each operation was added
|
|
512
|
+
batch_request.created_at if batch_request else datetime.now()
|
|
513
|
+
|
|
514
|
+
# For now, maintain original order since all operations have same timestamp
|
|
515
|
+
return items if direction == SortDirection.ASCENDING else list(reversed(items))
|
|
516
|
+
|
|
517
|
+
def _sort_by_risk(
|
|
518
|
+
self,
|
|
519
|
+
items: List[Tuple[Operation, ChangePreview]],
|
|
520
|
+
direction: SortDirection,
|
|
521
|
+
batch_request: Optional[BatchApprovalRequest],
|
|
522
|
+
) -> List[Tuple[Operation, ChangePreview]]:
|
|
523
|
+
"""Sort by risk level."""
|
|
524
|
+
risk_order = {
|
|
525
|
+
"critical": 4,
|
|
526
|
+
"high": 3,
|
|
527
|
+
"medium": 2,
|
|
528
|
+
"low": 1,
|
|
529
|
+
"unknown": 0,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
def get_risk_score(item):
|
|
533
|
+
operation, preview = item
|
|
534
|
+
if preview and preview.risk_assessment:
|
|
535
|
+
risk_level = self._extract_risk_level(preview.risk_assessment)
|
|
536
|
+
return risk_order.get(risk_level, 0)
|
|
537
|
+
return 0
|
|
538
|
+
|
|
539
|
+
reverse = direction == SortDirection.DESCENDING
|
|
540
|
+
return sorted(items, key=get_risk_score, reverse=reverse)
|
|
541
|
+
|
|
542
|
+
def _sort_by_action_type(
|
|
543
|
+
self,
|
|
544
|
+
items: List[Tuple[Operation, ChangePreview]],
|
|
545
|
+
direction: SortDirection,
|
|
546
|
+
batch_request: Optional[BatchApprovalRequest],
|
|
547
|
+
) -> List[Tuple[Operation, ChangePreview]]:
|
|
548
|
+
"""Sort by action type."""
|
|
549
|
+
|
|
550
|
+
def get_type_name(item):
|
|
551
|
+
operation, preview = item
|
|
552
|
+
return operation.type.value
|
|
553
|
+
|
|
554
|
+
reverse = direction == SortDirection.DESCENDING
|
|
555
|
+
return sorted(items, key=get_type_name, reverse=reverse)
|
|
556
|
+
|
|
557
|
+
def _sort_by_target(
|
|
558
|
+
self,
|
|
559
|
+
items: List[Tuple[Operation, ChangePreview]],
|
|
560
|
+
direction: SortDirection,
|
|
561
|
+
batch_request: Optional[BatchApprovalRequest],
|
|
562
|
+
) -> List[Tuple[Operation, ChangePreview]]:
|
|
563
|
+
"""Sort by operation target."""
|
|
564
|
+
|
|
565
|
+
def get_target(item):
|
|
566
|
+
operation, preview = item
|
|
567
|
+
if operation.data:
|
|
568
|
+
for field in ["path", "url", "command", "target"]:
|
|
569
|
+
if field in operation.data:
|
|
570
|
+
return str(operation.data[field]).lower()
|
|
571
|
+
return operation.description.lower() if operation.description else ""
|
|
572
|
+
|
|
573
|
+
reverse = direction == SortDirection.DESCENDING
|
|
574
|
+
return sorted(items, key=get_target, reverse=reverse)
|
|
575
|
+
|
|
576
|
+
def _sort_by_status(
|
|
577
|
+
self,
|
|
578
|
+
items: List[Tuple[Operation, ChangePreview]],
|
|
579
|
+
direction: SortDirection,
|
|
580
|
+
batch_request: Optional[BatchApprovalRequest],
|
|
581
|
+
) -> List[Tuple[Operation, ChangePreview]]:
|
|
582
|
+
"""Sort by approval status."""
|
|
583
|
+
|
|
584
|
+
def get_status_score(item):
|
|
585
|
+
if not batch_request:
|
|
586
|
+
return 0
|
|
587
|
+
|
|
588
|
+
operation, preview = item
|
|
589
|
+
try:
|
|
590
|
+
op_index = batch_request.operations.index(operation)
|
|
591
|
+
is_approved = op_index in batch_request.approved_operations
|
|
592
|
+
return 1 if is_approved else 0
|
|
593
|
+
except ValueError:
|
|
594
|
+
return 0
|
|
595
|
+
|
|
596
|
+
reverse = direction == SortDirection.DESCENDING
|
|
597
|
+
return sorted(items, key=get_status_score, reverse=reverse)
|
|
598
|
+
|
|
599
|
+
def _sort_by_alphabetical(
|
|
600
|
+
self,
|
|
601
|
+
items: List[Tuple[Operation, ChangePreview]],
|
|
602
|
+
direction: SortDirection,
|
|
603
|
+
batch_request: Optional[BatchApprovalRequest],
|
|
604
|
+
) -> List[Tuple[Operation, ChangePreview]]:
|
|
605
|
+
"""Sort alphabetically by description."""
|
|
606
|
+
|
|
607
|
+
def get_description(item):
|
|
608
|
+
operation, preview = item
|
|
609
|
+
return operation.description.lower() if operation.description else ""
|
|
610
|
+
|
|
611
|
+
reverse = direction == SortDirection.DESCENDING
|
|
612
|
+
return sorted(items, key=get_description, reverse=reverse)
|
|
613
|
+
|
|
614
|
+
def _extract_risk_level(self, risk_assessment: str) -> str:
|
|
615
|
+
"""Extract risk level from assessment string."""
|
|
616
|
+
risk_lower = risk_assessment.lower()
|
|
617
|
+
if "critical" in risk_lower:
|
|
618
|
+
return "critical"
|
|
619
|
+
elif "high" in risk_lower:
|
|
620
|
+
return "high"
|
|
621
|
+
elif "medium" in risk_lower:
|
|
622
|
+
return "medium"
|
|
623
|
+
elif "low" in risk_lower:
|
|
624
|
+
return "low"
|
|
625
|
+
else:
|
|
626
|
+
return "unknown"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class BatchPaginator:
|
|
630
|
+
"""Handles pagination of filtered and sorted results."""
|
|
631
|
+
|
|
632
|
+
def __init__(self, page_size: int = 10):
|
|
633
|
+
"""
|
|
634
|
+
Initialize paginator.
|
|
635
|
+
|
|
636
|
+
Args:
|
|
637
|
+
page_size: Number of items per page
|
|
638
|
+
"""
|
|
639
|
+
self.page_size = page_size
|
|
640
|
+
|
|
641
|
+
def paginate(
|
|
642
|
+
self,
|
|
643
|
+
operations: List[Operation],
|
|
644
|
+
previews: List[ChangePreview],
|
|
645
|
+
page: int = 0,
|
|
646
|
+
) -> Tuple[List[Operation], List[ChangePreview], Dict[str, Any]]:
|
|
647
|
+
"""
|
|
648
|
+
Paginate operations and previews.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
operations: Operations to paginate
|
|
652
|
+
previews: Corresponding previews
|
|
653
|
+
page: Page number (0-based)
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
Tuple of (page_operations, page_previews, pagination_info)
|
|
657
|
+
"""
|
|
658
|
+
total_items = len(operations)
|
|
659
|
+
total_pages = (total_items + self.page_size - 1) // self.page_size
|
|
660
|
+
|
|
661
|
+
# Ensure page is within bounds
|
|
662
|
+
page = max(0, min(page, total_pages - 1))
|
|
663
|
+
|
|
664
|
+
start_idx = page * self.page_size
|
|
665
|
+
end_idx = min(start_idx + self.page_size, total_items)
|
|
666
|
+
|
|
667
|
+
page_operations = operations[start_idx:end_idx]
|
|
668
|
+
page_previews = previews[start_idx:end_idx]
|
|
669
|
+
|
|
670
|
+
pagination_info = {
|
|
671
|
+
"current_page": page,
|
|
672
|
+
"total_pages": total_pages,
|
|
673
|
+
"page_size": self.page_size,
|
|
674
|
+
"total_items": total_items,
|
|
675
|
+
"start_index": start_idx,
|
|
676
|
+
"end_index": end_idx,
|
|
677
|
+
"has_previous": page > 0,
|
|
678
|
+
"has_next": page < total_pages - 1,
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return page_operations, page_previews, pagination_info
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def create_quick_filters() -> Dict[str, BatchFilter]:
|
|
685
|
+
"""Create commonly used quick filters."""
|
|
686
|
+
return {
|
|
687
|
+
"high_risk": RiskLevelFilter(["high", "critical"]),
|
|
688
|
+
"file_ops": ActionTypeFilter(
|
|
689
|
+
[OperationType.FILE_WRITE, OperationType.FILE_DELETE]
|
|
690
|
+
),
|
|
691
|
+
"read_only": ActionTypeFilter([OperationType.FILE_READ]),
|
|
692
|
+
"commands": ActionTypeFilter([OperationType.COMMAND_EXECUTE]),
|
|
693
|
+
"web_requests": ActionTypeFilter([OperationType.WEB_REQUEST]),
|
|
694
|
+
"python_files": TargetPatternFilter(["*.py"]),
|
|
695
|
+
"config_files": TargetPatternFilter(
|
|
696
|
+
["*.conf", "*.config", "*.json", "*.yaml", "*.yml"]
|
|
697
|
+
),
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def parse_filter_expression(
|
|
702
|
+
expression: str, batch_request: BatchApprovalRequest
|
|
703
|
+
) -> Optional[BatchFilter]:
|
|
704
|
+
"""
|
|
705
|
+
Parse a filter expression into a BatchFilter.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
expression: Filter expression string
|
|
709
|
+
batch_request: Batch request for context
|
|
710
|
+
|
|
711
|
+
Returns:
|
|
712
|
+
BatchFilter instance or None if parsing failed
|
|
713
|
+
"""
|
|
714
|
+
try:
|
|
715
|
+
return CustomQueryFilter(expression, batch_request)
|
|
716
|
+
except Exception as e:
|
|
717
|
+
logger.error(f"Failed to parse filter expression '{expression}': {e}")
|
|
718
|
+
return None
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def create_default_sort_criteria() -> SortCriteria:
|
|
722
|
+
"""Create default sort criteria (by risk level, descending)."""
|
|
723
|
+
return SortCriteria(
|
|
724
|
+
sort_by=SortBy.RISK_LEVEL,
|
|
725
|
+
direction=SortDirection.DESCENDING,
|
|
726
|
+
secondary_sort=SortCriteria(
|
|
727
|
+
sort_by=SortBy.ACTION_TYPE, direction=SortDirection.ASCENDING
|
|
728
|
+
),
|
|
729
|
+
)
|