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,406 @@
|
|
|
1
|
+
"""Approval workflow for high-risk agent operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApprovalStatus(Enum):
|
|
12
|
+
"""Status of approval requests."""
|
|
13
|
+
|
|
14
|
+
PENDING = "pending"
|
|
15
|
+
APPROVED = "approved"
|
|
16
|
+
DENIED = "denied"
|
|
17
|
+
EXPIRED = "expired"
|
|
18
|
+
CANCELLED = "cancelled"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RiskLevel(Enum):
|
|
22
|
+
"""Risk levels for operations."""
|
|
23
|
+
|
|
24
|
+
LOW = "low"
|
|
25
|
+
MEDIUM = "medium"
|
|
26
|
+
HIGH = "high"
|
|
27
|
+
CRITICAL = "critical"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ApprovalRequest:
|
|
32
|
+
"""Represents an approval request for a risky operation."""
|
|
33
|
+
|
|
34
|
+
id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
35
|
+
operation_type: str = ""
|
|
36
|
+
description: str = ""
|
|
37
|
+
risk_level: RiskLevel = RiskLevel.MEDIUM
|
|
38
|
+
requested_by: str = "agent"
|
|
39
|
+
requested_at: datetime = field(default_factory=datetime.now)
|
|
40
|
+
expires_at: Optional[datetime] = None
|
|
41
|
+
status: ApprovalStatus = ApprovalStatus.PENDING
|
|
42
|
+
approver: Optional[str] = None
|
|
43
|
+
approved_at: Optional[datetime] = None
|
|
44
|
+
denial_reason: Optional[str] = None
|
|
45
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
|
|
47
|
+
def is_expired(self) -> bool:
|
|
48
|
+
"""Check if the approval request has expired."""
|
|
49
|
+
if self.expires_at and datetime.now() > self.expires_at:
|
|
50
|
+
return True
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def time_remaining(self) -> Optional[timedelta]:
|
|
54
|
+
"""Get time remaining before expiration."""
|
|
55
|
+
if self.expires_at:
|
|
56
|
+
remaining = self.expires_at - datetime.now()
|
|
57
|
+
return remaining if remaining.total_seconds() > 0 else timedelta(0)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ApprovalWorkflow:
|
|
62
|
+
"""Manages approval workflows for high-risk operations."""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
default_expiry_minutes: int = 30,
|
|
67
|
+
auto_approve_low_risk: bool = True,
|
|
68
|
+
):
|
|
69
|
+
self.pending_requests: Dict[str, ApprovalRequest] = {}
|
|
70
|
+
self.completed_requests: Dict[str, ApprovalRequest] = {}
|
|
71
|
+
self.default_expiry_minutes = default_expiry_minutes
|
|
72
|
+
self.auto_approve_low_risk = auto_approve_low_risk
|
|
73
|
+
self.approval_handlers: Dict[RiskLevel, List[Callable]] = {
|
|
74
|
+
level: [] for level in RiskLevel
|
|
75
|
+
}
|
|
76
|
+
self.risk_assessment_rules = self._get_default_risk_rules()
|
|
77
|
+
|
|
78
|
+
def _get_default_risk_rules(self) -> Dict[str, RiskLevel]:
|
|
79
|
+
"""Get default risk assessment rules for operation types."""
|
|
80
|
+
return {
|
|
81
|
+
# Low risk operations
|
|
82
|
+
"file_read": RiskLevel.LOW,
|
|
83
|
+
"file_list": RiskLevel.LOW,
|
|
84
|
+
"system_info": RiskLevel.LOW,
|
|
85
|
+
"network_get": RiskLevel.LOW,
|
|
86
|
+
# Medium risk operations
|
|
87
|
+
"file_write": RiskLevel.MEDIUM,
|
|
88
|
+
"file_create": RiskLevel.MEDIUM,
|
|
89
|
+
"network_post": RiskLevel.MEDIUM,
|
|
90
|
+
"command_execute": RiskLevel.MEDIUM,
|
|
91
|
+
# High risk operations
|
|
92
|
+
"file_delete": RiskLevel.HIGH,
|
|
93
|
+
"system_config_change": RiskLevel.HIGH,
|
|
94
|
+
"package_install": RiskLevel.HIGH,
|
|
95
|
+
"service_control": RiskLevel.HIGH,
|
|
96
|
+
# Critical risk operations
|
|
97
|
+
"system_admin": RiskLevel.CRITICAL,
|
|
98
|
+
"credential_access": RiskLevel.CRITICAL,
|
|
99
|
+
"network_server": RiskLevel.CRITICAL,
|
|
100
|
+
"data_export": RiskLevel.CRITICAL,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def assess_risk_level(
|
|
104
|
+
self, operation_type: str, metadata: Optional[Dict[str, Any]] = None
|
|
105
|
+
) -> RiskLevel:
|
|
106
|
+
"""Assess the risk level of an operation."""
|
|
107
|
+
|
|
108
|
+
# Base risk from operation type
|
|
109
|
+
base_risk = self.risk_assessment_rules.get(operation_type, RiskLevel.MEDIUM)
|
|
110
|
+
|
|
111
|
+
if not metadata:
|
|
112
|
+
return base_risk
|
|
113
|
+
|
|
114
|
+
# Risk escalation factors
|
|
115
|
+
risk_factors = []
|
|
116
|
+
|
|
117
|
+
# Check for sensitive paths
|
|
118
|
+
if "path" in metadata:
|
|
119
|
+
path = metadata["path"].lower()
|
|
120
|
+
sensitive_patterns = [
|
|
121
|
+
".ssh",
|
|
122
|
+
".env",
|
|
123
|
+
"password",
|
|
124
|
+
"secret",
|
|
125
|
+
"credential",
|
|
126
|
+
"/etc",
|
|
127
|
+
"/system",
|
|
128
|
+
"config",
|
|
129
|
+
"settings",
|
|
130
|
+
]
|
|
131
|
+
if any(pattern in path for pattern in sensitive_patterns):
|
|
132
|
+
risk_factors.append("sensitive_path")
|
|
133
|
+
|
|
134
|
+
# Check for dangerous commands
|
|
135
|
+
if "command" in metadata:
|
|
136
|
+
command = metadata["command"].lower()
|
|
137
|
+
dangerous_commands = [
|
|
138
|
+
"rm -rf",
|
|
139
|
+
"sudo",
|
|
140
|
+
"chmod 777",
|
|
141
|
+
"curl",
|
|
142
|
+
"wget",
|
|
143
|
+
"nc",
|
|
144
|
+
"netcat",
|
|
145
|
+
"ssh",
|
|
146
|
+
"scp",
|
|
147
|
+
]
|
|
148
|
+
if any(cmd in command for cmd in dangerous_commands):
|
|
149
|
+
risk_factors.append("dangerous_command")
|
|
150
|
+
|
|
151
|
+
# Check for network operations
|
|
152
|
+
if "network" in metadata and metadata["network"]:
|
|
153
|
+
risk_factors.append("network_operation")
|
|
154
|
+
|
|
155
|
+
# Check for external URLs
|
|
156
|
+
if "url" in metadata:
|
|
157
|
+
url = metadata["url"]
|
|
158
|
+
if not any(
|
|
159
|
+
domain in url for domain in ["localhost", "127.0.0.1", "0.0.0.0"]
|
|
160
|
+
):
|
|
161
|
+
risk_factors.append("external_network")
|
|
162
|
+
|
|
163
|
+
# Escalate risk based on factors
|
|
164
|
+
if len(risk_factors) >= 3:
|
|
165
|
+
return RiskLevel.CRITICAL
|
|
166
|
+
elif len(risk_factors) >= 2:
|
|
167
|
+
return RiskLevel.HIGH
|
|
168
|
+
elif len(risk_factors) >= 1:
|
|
169
|
+
# Escalate by one level
|
|
170
|
+
levels = [
|
|
171
|
+
RiskLevel.LOW,
|
|
172
|
+
RiskLevel.MEDIUM,
|
|
173
|
+
RiskLevel.HIGH,
|
|
174
|
+
RiskLevel.CRITICAL,
|
|
175
|
+
]
|
|
176
|
+
current_index = levels.index(base_risk)
|
|
177
|
+
return levels[min(current_index + 1, len(levels) - 1)]
|
|
178
|
+
|
|
179
|
+
return base_risk
|
|
180
|
+
|
|
181
|
+
async def request_approval(
|
|
182
|
+
self,
|
|
183
|
+
operation_type: str,
|
|
184
|
+
description: str,
|
|
185
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
186
|
+
timeout_minutes: Optional[int] = None,
|
|
187
|
+
) -> ApprovalRequest:
|
|
188
|
+
"""Request approval for an operation."""
|
|
189
|
+
|
|
190
|
+
# Assess risk level
|
|
191
|
+
risk_level = self.assess_risk_level(operation_type, metadata)
|
|
192
|
+
|
|
193
|
+
# Create approval request
|
|
194
|
+
request = ApprovalRequest(
|
|
195
|
+
operation_type=operation_type,
|
|
196
|
+
description=description,
|
|
197
|
+
risk_level=risk_level,
|
|
198
|
+
metadata=metadata or {},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Set expiration
|
|
202
|
+
expiry_minutes = timeout_minutes or self.default_expiry_minutes
|
|
203
|
+
request.expires_at = datetime.now() + timedelta(minutes=expiry_minutes)
|
|
204
|
+
|
|
205
|
+
# Auto-approve low risk operations if configured
|
|
206
|
+
if self.auto_approve_low_risk and risk_level == RiskLevel.LOW:
|
|
207
|
+
request.status = ApprovalStatus.APPROVED
|
|
208
|
+
request.approver = "auto_approval"
|
|
209
|
+
request.approved_at = datetime.now()
|
|
210
|
+
self.completed_requests[request.id] = request
|
|
211
|
+
return request
|
|
212
|
+
|
|
213
|
+
# Add to pending requests
|
|
214
|
+
self.pending_requests[request.id] = request
|
|
215
|
+
|
|
216
|
+
# Trigger approval handlers
|
|
217
|
+
await self._trigger_approval_handlers(request)
|
|
218
|
+
|
|
219
|
+
return request
|
|
220
|
+
|
|
221
|
+
async def _trigger_approval_handlers(self, request: ApprovalRequest) -> None:
|
|
222
|
+
"""Trigger approval handlers for a request."""
|
|
223
|
+
|
|
224
|
+
handlers = self.approval_handlers.get(request.risk_level, [])
|
|
225
|
+
for handler in handlers:
|
|
226
|
+
try:
|
|
227
|
+
if asyncio.iscoroutinefunction(handler):
|
|
228
|
+
await handler(request)
|
|
229
|
+
else:
|
|
230
|
+
handler(request)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
print(f"Error in approval handler: {e}")
|
|
233
|
+
|
|
234
|
+
def approve_request(
|
|
235
|
+
self, request_id: str, approver: str, reason: Optional[str] = None
|
|
236
|
+
) -> bool:
|
|
237
|
+
"""Approve a pending request."""
|
|
238
|
+
|
|
239
|
+
if request_id not in self.pending_requests:
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
request = self.pending_requests[request_id]
|
|
243
|
+
|
|
244
|
+
# Check if expired
|
|
245
|
+
if request.is_expired():
|
|
246
|
+
request.status = ApprovalStatus.EXPIRED
|
|
247
|
+
self.completed_requests[request_id] = request
|
|
248
|
+
del self.pending_requests[request_id]
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
# Approve the request
|
|
252
|
+
request.status = ApprovalStatus.APPROVED
|
|
253
|
+
request.approver = approver
|
|
254
|
+
request.approved_at = datetime.now()
|
|
255
|
+
if reason:
|
|
256
|
+
request.metadata["approval_reason"] = reason
|
|
257
|
+
|
|
258
|
+
# Move to completed
|
|
259
|
+
self.completed_requests[request_id] = request
|
|
260
|
+
del self.pending_requests[request_id]
|
|
261
|
+
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
def deny_request(self, request_id: str, approver: str, reason: str) -> bool:
|
|
265
|
+
"""Deny a pending request."""
|
|
266
|
+
|
|
267
|
+
if request_id not in self.pending_requests:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
request = self.pending_requests[request_id]
|
|
271
|
+
|
|
272
|
+
# Deny the request
|
|
273
|
+
request.status = ApprovalStatus.DENIED
|
|
274
|
+
request.approver = approver
|
|
275
|
+
request.approved_at = datetime.now()
|
|
276
|
+
request.denial_reason = reason
|
|
277
|
+
|
|
278
|
+
# Move to completed
|
|
279
|
+
self.completed_requests[request_id] = request
|
|
280
|
+
del self.pending_requests[request_id]
|
|
281
|
+
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
def cancel_request(self, request_id: str) -> bool:
|
|
285
|
+
"""Cancel a pending request."""
|
|
286
|
+
|
|
287
|
+
if request_id not in self.pending_requests:
|
|
288
|
+
return False
|
|
289
|
+
|
|
290
|
+
request = self.pending_requests[request_id]
|
|
291
|
+
request.status = ApprovalStatus.CANCELLED
|
|
292
|
+
|
|
293
|
+
# Move to completed
|
|
294
|
+
self.completed_requests[request_id] = request
|
|
295
|
+
del self.pending_requests[request_id]
|
|
296
|
+
|
|
297
|
+
return True
|
|
298
|
+
|
|
299
|
+
async def wait_for_approval(
|
|
300
|
+
self, request_id: str, check_interval: float = 1.0
|
|
301
|
+
) -> ApprovalStatus:
|
|
302
|
+
"""Wait for an approval request to be resolved."""
|
|
303
|
+
|
|
304
|
+
while request_id in self.pending_requests:
|
|
305
|
+
request = self.pending_requests[request_id]
|
|
306
|
+
|
|
307
|
+
# Check if expired
|
|
308
|
+
if request.is_expired():
|
|
309
|
+
request.status = ApprovalStatus.EXPIRED
|
|
310
|
+
self.completed_requests[request_id] = request
|
|
311
|
+
del self.pending_requests[request_id]
|
|
312
|
+
return ApprovalStatus.EXPIRED
|
|
313
|
+
|
|
314
|
+
# Wait before checking again
|
|
315
|
+
await asyncio.sleep(check_interval)
|
|
316
|
+
|
|
317
|
+
# Request was resolved
|
|
318
|
+
if request_id in self.completed_requests:
|
|
319
|
+
return self.completed_requests[request_id].status
|
|
320
|
+
|
|
321
|
+
return ApprovalStatus.CANCELLED
|
|
322
|
+
|
|
323
|
+
def get_pending_requests(
|
|
324
|
+
self, risk_level: Optional[RiskLevel] = None
|
|
325
|
+
) -> List[ApprovalRequest]:
|
|
326
|
+
"""Get all pending approval requests."""
|
|
327
|
+
|
|
328
|
+
requests = list(self.pending_requests.values())
|
|
329
|
+
|
|
330
|
+
if risk_level:
|
|
331
|
+
requests = [r for r in requests if r.risk_level == risk_level]
|
|
332
|
+
|
|
333
|
+
return sorted(requests, key=lambda r: r.requested_at)
|
|
334
|
+
|
|
335
|
+
def get_request(self, request_id: str) -> Optional[ApprovalRequest]:
|
|
336
|
+
"""Get an approval request by ID."""
|
|
337
|
+
|
|
338
|
+
if request_id in self.pending_requests:
|
|
339
|
+
return self.pending_requests[request_id]
|
|
340
|
+
elif request_id in self.completed_requests:
|
|
341
|
+
return self.completed_requests[request_id]
|
|
342
|
+
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
def cleanup_expired_requests(self) -> List[str]:
|
|
346
|
+
"""Clean up expired requests and return their IDs."""
|
|
347
|
+
|
|
348
|
+
expired_ids = []
|
|
349
|
+
|
|
350
|
+
for request_id, request in list(self.pending_requests.items()):
|
|
351
|
+
if request.is_expired():
|
|
352
|
+
request.status = ApprovalStatus.EXPIRED
|
|
353
|
+
self.completed_requests[request_id] = request
|
|
354
|
+
del self.pending_requests[request_id]
|
|
355
|
+
expired_ids.append(request_id)
|
|
356
|
+
|
|
357
|
+
return expired_ids
|
|
358
|
+
|
|
359
|
+
def add_approval_handler(
|
|
360
|
+
self, risk_level: RiskLevel, handler: Callable[[ApprovalRequest], Any]
|
|
361
|
+
) -> None:
|
|
362
|
+
"""Add an approval handler for a specific risk level."""
|
|
363
|
+
|
|
364
|
+
self.approval_handlers[risk_level].append(handler)
|
|
365
|
+
|
|
366
|
+
def remove_approval_handler(
|
|
367
|
+
self, risk_level: RiskLevel, handler: Callable[[ApprovalRequest], Any]
|
|
368
|
+
) -> bool:
|
|
369
|
+
"""Remove an approval handler."""
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
self.approval_handlers[risk_level].remove(handler)
|
|
373
|
+
return True
|
|
374
|
+
except ValueError:
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
def get_approval_stats(self) -> Dict[str, Any]:
|
|
378
|
+
"""Get approval workflow statistics."""
|
|
379
|
+
|
|
380
|
+
pending_count = len(self.pending_requests)
|
|
381
|
+
completed_count = len(self.completed_requests)
|
|
382
|
+
|
|
383
|
+
# Count by status
|
|
384
|
+
status_counts = {}
|
|
385
|
+
for request in self.completed_requests.values():
|
|
386
|
+
status = request.status.value
|
|
387
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
388
|
+
|
|
389
|
+
# Count by risk level
|
|
390
|
+
risk_counts = {}
|
|
391
|
+
all_requests = list(self.pending_requests.values()) + list(
|
|
392
|
+
self.completed_requests.values()
|
|
393
|
+
)
|
|
394
|
+
for request in all_requests:
|
|
395
|
+
risk = request.risk_level.value
|
|
396
|
+
risk_counts[risk] = risk_counts.get(risk, 0) + 1
|
|
397
|
+
|
|
398
|
+
return {
|
|
399
|
+
"pending_requests": pending_count,
|
|
400
|
+
"completed_requests": completed_count,
|
|
401
|
+
"total_requests": pending_count + completed_count,
|
|
402
|
+
"status_breakdown": status_counts,
|
|
403
|
+
"risk_level_breakdown": risk_counts,
|
|
404
|
+
"auto_approve_enabled": self.auto_approve_low_risk,
|
|
405
|
+
"default_expiry_minutes": self.default_expiry_minutes,
|
|
406
|
+
}
|