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.
Files changed (126) hide show
  1. omnimancer/__init__.py +42 -0
  2. omnimancer/__main__.py +9 -0
  3. omnimancer/cli/__init__.py +8 -0
  4. omnimancer/cli/agent_manager.py +826 -0
  5. omnimancer/cli/agent_wizard.py +944 -0
  6. omnimancer/cli/approval_formatter.py +734 -0
  7. omnimancer/cli/approval_integration.py +734 -0
  8. omnimancer/cli/approval_prompt.py +1286 -0
  9. omnimancer/cli/batch_approval_display.py +617 -0
  10. omnimancer/cli/batch_approval_filters.py +729 -0
  11. omnimancer/cli/batch_operation_monitor.py +775 -0
  12. omnimancer/cli/commands/performance_cmd.py +624 -0
  13. omnimancer/cli/commands.py +757 -0
  14. omnimancer/cli/completion_handler.py +281 -0
  15. omnimancer/cli/config_setup_wizard.py +783 -0
  16. omnimancer/cli/dynamic_commands.py +371 -0
  17. omnimancer/cli/handlers/__init__.py +18 -0
  18. omnimancer/cli/handlers/agent_handler.py +865 -0
  19. omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
  20. omnimancer/cli/handlers/custom_agent_handler.py +352 -0
  21. omnimancer/cli/handlers/permissions_handler.py +1609 -0
  22. omnimancer/cli/interface.py +6695 -0
  23. omnimancer/cli/performance_dashboard.py +612 -0
  24. omnimancer/core/__init__.py +34 -0
  25. omnimancer/core/agent/__init__.py +256 -0
  26. omnimancer/core/agent/agent_config.py +715 -0
  27. omnimancer/core/agent/agent_switcher.py +668 -0
  28. omnimancer/core/agent/approval_context.py +460 -0
  29. omnimancer/core/agent/approval_dialog.py +902 -0
  30. omnimancer/core/agent/approval_interface.py +628 -0
  31. omnimancer/core/agent/approval_manager.py +760 -0
  32. omnimancer/core/agent/cli_integration.py +470 -0
  33. omnimancer/core/agent/config.py +845 -0
  34. omnimancer/core/agent/custom_agent_integration.py +654 -0
  35. omnimancer/core/agent/diff_renderer.py +706 -0
  36. omnimancer/core/agent/file_content_display.py +503 -0
  37. omnimancer/core/agent/file_modification_workflow.py +658 -0
  38. omnimancer/core/agent/file_system_manager.py +1790 -0
  39. omnimancer/core/agent/input_handler.py +614 -0
  40. omnimancer/core/agent/metrics_collector.py +771 -0
  41. omnimancer/core/agent/optimization_engine.py +729 -0
  42. omnimancer/core/agent/performance_monitor.py +717 -0
  43. omnimancer/core/agent/persona.py +870 -0
  44. omnimancer/core/agent/persona_validator.py +672 -0
  45. omnimancer/core/agent/program_executor.py +748 -0
  46. omnimancer/core/agent/proposed_changes_integration.py +663 -0
  47. omnimancer/core/agent/read_before_write_errors.py +492 -0
  48. omnimancer/core/agent/read_before_write_ui.py +647 -0
  49. omnimancer/core/agent/rich_renderer.py +763 -0
  50. omnimancer/core/agent/status_config.py +238 -0
  51. omnimancer/core/agent/status_core.py +446 -0
  52. omnimancer/core/agent/status_integrated_engine.py +540 -0
  53. omnimancer/core/agent/status_manager.py +878 -0
  54. omnimancer/core/agent/status_ui_components.py +535 -0
  55. omnimancer/core/agent/template_applicator.py +548 -0
  56. omnimancer/core/agent/terminal_status_listener.py +394 -0
  57. omnimancer/core/agent/token_tracker.py +679 -0
  58. omnimancer/core/agent/types.py +60 -0
  59. omnimancer/core/agent/web_client.py +745 -0
  60. omnimancer/core/agent/workflow_orchestrator.py +826 -0
  61. omnimancer/core/agent_engine.py +1625 -0
  62. omnimancer/core/agent_mode_manager.py +706 -0
  63. omnimancer/core/agent_progress_ui.py +545 -0
  64. omnimancer/core/chat_manager.py +141 -0
  65. omnimancer/core/config_manager.py +2199 -0
  66. omnimancer/core/config_migration.py +772 -0
  67. omnimancer/core/config_provider.py +1974 -0
  68. omnimancer/core/config_security_validator.py +393 -0
  69. omnimancer/core/config_validator.py +768 -0
  70. omnimancer/core/conversation_manager.py +352 -0
  71. omnimancer/core/engine.py +787 -0
  72. omnimancer/core/enhanced_input.py +523 -0
  73. omnimancer/core/env_loader.py +83 -0
  74. omnimancer/core/fallback_manager.py +568 -0
  75. omnimancer/core/health_monitor.py +462 -0
  76. omnimancer/core/history_manager.py +462 -0
  77. omnimancer/core/mcp_integration_layer.py +878 -0
  78. omnimancer/core/models.py +1889 -0
  79. omnimancer/core/provider_capabilities.py +403 -0
  80. omnimancer/core/provider_initializer.py +450 -0
  81. omnimancer/core/provider_registry.py +432 -0
  82. omnimancer/core/security/__init__.py +16 -0
  83. omnimancer/core/security/approval_workflow.py +406 -0
  84. omnimancer/core/security/audit_logger.py +511 -0
  85. omnimancer/core/security/permission_controller.py +511 -0
  86. omnimancer/core/security/sandbox_manager.py +379 -0
  87. omnimancer/core/security/security_manager.py +530 -0
  88. omnimancer/core/setup_wizard.py +345 -0
  89. omnimancer/core/setup_wizard_core.py +449 -0
  90. omnimancer/core/setup_wizard_guide_generator.py +376 -0
  91. omnimancer/core/setup_wizard_provider_setup.py +454 -0
  92. omnimancer/core/setup_wizard_ui.py +317 -0
  93. omnimancer/core/setup_wizard_validation.py +371 -0
  94. omnimancer/core/signal_handler.py +174 -0
  95. omnimancer/mcp/__init__.py +11 -0
  96. omnimancer/mcp/client.py +725 -0
  97. omnimancer/mcp/manager.py +649 -0
  98. omnimancer/providers/__init__.py +24 -0
  99. omnimancer/providers/azure.py +608 -0
  100. omnimancer/providers/base.py +262 -0
  101. omnimancer/providers/bedrock.py +711 -0
  102. omnimancer/providers/claude.py +426 -0
  103. omnimancer/providers/claude_code.py +406 -0
  104. omnimancer/providers/cohere.py +412 -0
  105. omnimancer/providers/factory.py +988 -0
  106. omnimancer/providers/gemini.py +708 -0
  107. omnimancer/providers/mistral.py +554 -0
  108. omnimancer/providers/ollama.py +510 -0
  109. omnimancer/providers/openai.py +362 -0
  110. omnimancer/providers/openrouter.py +709 -0
  111. omnimancer/providers/perplexity.py +476 -0
  112. omnimancer/providers/vertex.py +617 -0
  113. omnimancer/providers/xai.py +484 -0
  114. omnimancer/ui/__init__.py +6 -0
  115. omnimancer/ui/cancellation_handler.py +235 -0
  116. omnimancer/ui/progress_indicator.py +318 -0
  117. omnimancer/utils/__init__.py +22 -0
  118. omnimancer/utils/error_handler.py +589 -0
  119. omnimancer/utils/errors.py +667 -0
  120. omnimancer/utils/retry.py +238 -0
  121. omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
  122. omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
  123. omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
  124. omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
  125. omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  126. omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,734 @@
1
+ """
2
+ Approval Manager CLI Integration for Omnimancer.
3
+
4
+ This module wires the EnhancedApprovalManager to the CLI workflow for
5
+ user-facing approval processes. It integrates approval dialog display
6
+ in terminal with clear formatting and captures approval decisions.
7
+ """
8
+
9
+ import logging
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from rich.console import Console
15
+
16
+ from ..core.agent.approval_manager import (
17
+ BatchApprovalRequest,
18
+ EnhancedApprovalManager,
19
+ )
20
+ from ..core.agent.types import Operation, OperationType
21
+ from ..core.security.approval_workflow import (
22
+ ApprovalRequest,
23
+ )
24
+ from ..core.security.permission_controller import PermissionController
25
+ from .approval_formatter import CLIApprovalFormatter
26
+ from .approval_prompt import (
27
+ ApprovalDecision,
28
+ CLIApprovalPrompt,
29
+ )
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class CLIApprovalIntegration:
35
+ """
36
+ Integration layer that connects EnhancedApprovalManager with CLI approval dialogs.
37
+
38
+ This class handles the flow from approval request through user interaction
39
+ to final approval decision, including permission storage for "remember" decisions.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ approval_manager: Optional[EnhancedApprovalManager] = None,
45
+ permission_controller: Optional[PermissionController] = None,
46
+ console: Optional[Console] = None,
47
+ enable_auto_approval: bool = True,
48
+ approval_timeout_seconds: int = 300,
49
+ ):
50
+ """
51
+ Initialize CLI approval integration.
52
+
53
+ Args:
54
+ approval_manager: Enhanced approval manager instance
55
+ permission_controller: Permission controller for storing decisions
56
+ console: Rich console for display
57
+ enable_auto_approval: Whether to enable auto-approval for remembered decisions
58
+ approval_timeout_seconds: Default timeout for approval prompts
59
+ """
60
+ self.approval_manager = approval_manager or EnhancedApprovalManager()
61
+ self.permission_controller = permission_controller or PermissionController()
62
+ self.console = console or Console()
63
+ self.enable_auto_approval = enable_auto_approval
64
+ self.approval_timeout_seconds = approval_timeout_seconds
65
+
66
+ # Initialize UI components
67
+ self.formatter = CLIApprovalFormatter(self.console)
68
+ self.prompt_handler = CLIApprovalPrompt(
69
+ console=self.console,
70
+ formatter=self.formatter,
71
+ default_timeout_seconds=approval_timeout_seconds,
72
+ )
73
+
74
+ # Set approval callback on the manager
75
+ self.approval_manager.set_approval_callback(self._handle_single_approval)
76
+ self.approval_manager.set_batch_approval_callback(self._handle_batch_approval)
77
+
78
+ # Track approval decisions for session
79
+ self.approval_session_log: List[Dict[str, Any]] = []
80
+
81
+ async def request_approval_for_operation(self, operation: Operation) -> bool:
82
+ """
83
+ Request approval for a single operation with CLI integration.
84
+
85
+ Args:
86
+ operation: Operation requiring approval
87
+
88
+ Returns:
89
+ True if approved, False if denied
90
+ """
91
+ try:
92
+ # Check for auto-approval first if enabled
93
+ if self.enable_auto_approval:
94
+ auto_approval = await self._check_auto_approval(operation)
95
+ if auto_approval is not None:
96
+ return auto_approval
97
+
98
+ # Use the enhanced approval manager for full workflow
99
+ approved = await self.approval_manager.request_single_approval(operation)
100
+
101
+ # Log the decision
102
+ self._log_approval_decision(operation, approved)
103
+
104
+ return approved
105
+
106
+ except Exception as e:
107
+ logger.error(f"Error in approval request: {e}")
108
+ self.console.print(f"[red]❌ Approval system error: {e}[/red]")
109
+ return False
110
+
111
+ async def request_batch_approval(
112
+ self, operations: List[Operation]
113
+ ) -> Dict[str, Any]:
114
+ """
115
+ Request approval for multiple operations with CLI integration.
116
+
117
+ Args:
118
+ operations: List of operations requiring approval
119
+
120
+ Returns:
121
+ Dictionary with approval results
122
+ """
123
+ try:
124
+ # Use the enhanced approval manager for batch workflow
125
+ batch_request = await self.approval_manager.request_batch_approval(
126
+ operations
127
+ )
128
+
129
+ # Convert to result format
130
+ summary = batch_request.get_approval_summary()
131
+
132
+ result = {
133
+ "approved_operations": list(batch_request.approved_operations),
134
+ "total_operations": len(operations),
135
+ "approval_summary": summary,
136
+ "batch_id": batch_request.id,
137
+ }
138
+
139
+ # Log batch decision
140
+ self._log_batch_approval_decision(operations, batch_request)
141
+
142
+ return result
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error in batch approval request: {e}")
146
+ self.console.print(f"[red]❌ Batch approval system error: {e}[/red]")
147
+ return {
148
+ "approved_operations": [],
149
+ "total_operations": len(operations),
150
+ "approval_summary": {
151
+ "all_approved": False,
152
+ "approval_rate": 0.0,
153
+ },
154
+ "error": str(e),
155
+ }
156
+
157
+ async def _handle_single_approval(self, approval_context: Dict[str, Any]) -> bool:
158
+ """
159
+ Handle single operation approval through CLI dialog.
160
+
161
+ Args:
162
+ approval_context: Context dict with operation, preview, approval_request, etc.
163
+
164
+ Returns:
165
+ True if approved, False if denied
166
+ """
167
+ try:
168
+ operation = approval_context["operation"]
169
+ preview = approval_context.get("preview")
170
+ approval_request = approval_context["approval_request"]
171
+
172
+ # Present approval dialog and get user decision
173
+ decision = await self.prompt_handler.prompt_for_approval(
174
+ approval_request,
175
+ preview,
176
+ operation.data,
177
+ self.approval_timeout_seconds,
178
+ )
179
+
180
+ # Handle "remember" decision
181
+ if decision.should_remember and decision.is_approved:
182
+ await self._store_approval_pattern(operation, approval_request)
183
+
184
+ # Record decision in session log
185
+ self._record_session_decision(operation, decision)
186
+
187
+ return decision.is_approved
188
+
189
+ except Exception as e:
190
+ logger.error(f"Error in single approval handler: {e}")
191
+ return False
192
+
193
+ async def _handle_batch_approval(
194
+ self, batch_request: BatchApprovalRequest
195
+ ) -> Dict[str, Any]:
196
+ """
197
+ Handle batch operation approval through CLI dialog.
198
+
199
+ Args:
200
+ batch_request: Batch approval request
201
+
202
+ Returns:
203
+ Dictionary with approval result information
204
+ """
205
+ try:
206
+ # Present batch approval dialog
207
+ batch_decision = await self.prompt_handler.prompt_for_batch_approval(
208
+ batch_request, self.approval_timeout_seconds
209
+ )
210
+
211
+ # Process the batch decision
212
+ if batch_decision.decision_type == "approve_all":
213
+ return {
214
+ "approve_all": True,
215
+ "approved_indices": list(range(len(batch_request.operations))),
216
+ }
217
+ elif batch_decision.decision_type == "deny_all":
218
+ return {
219
+ "deny_all": True,
220
+ "reason": batch_decision.user_notes or "User denied all operations",
221
+ }
222
+ elif batch_decision.decision_type == "selective":
223
+ return {"approved_indices": batch_decision.approved_indices}
224
+ elif batch_decision.decision_type == "individual":
225
+ # Handle individual decisions with potential "remember" actions
226
+ approved_indices = []
227
+ for i, decision in enumerate(batch_decision.individual_decisions):
228
+ if decision.is_approved:
229
+ approved_indices.append(i)
230
+
231
+ # Store remember patterns for individual decisions
232
+ if decision.should_remember:
233
+ operation = batch_request.operations[i]
234
+ # Create minimal approval request for pattern storage
235
+ temp_request = ApprovalRequest(
236
+ operation_type=operation.type.value,
237
+ description=operation.description,
238
+ metadata=operation.data,
239
+ )
240
+ await self._store_approval_pattern(operation, temp_request)
241
+
242
+ return {"approved_indices": approved_indices}
243
+ else:
244
+ # Cancelled or timeout
245
+ return {
246
+ "deny_all": True,
247
+ "reason": f"Batch approval {batch_decision.decision_type}: {batch_decision.user_notes}",
248
+ }
249
+
250
+ except Exception as e:
251
+ logger.error(f"Error in batch approval handler: {e}")
252
+ return {
253
+ "deny_all": True,
254
+ "reason": f"Error in batch approval: {str(e)}",
255
+ }
256
+
257
+ async def _check_auto_approval(self, operation: Operation) -> Optional[bool]:
258
+ """
259
+ Check if operation should be auto-approved based on stored patterns.
260
+
261
+ Args:
262
+ operation: Operation to check
263
+
264
+ Returns:
265
+ True if auto-approved, False if auto-denied, None if no match
266
+ """
267
+ if not self.enable_auto_approval:
268
+ return None
269
+
270
+ try:
271
+ # Generate operation signature for matching
272
+ operation_signature = self._generate_operation_signature(operation)
273
+
274
+ # Check permission controller for matching approvals
275
+ has_permission = (
276
+ await self.permission_controller.check_operation_permission(
277
+ operation.type.value, operation_signature, operation.data
278
+ )
279
+ )
280
+
281
+ if has_permission:
282
+ # Show auto-approval message
283
+ self.console.print(
284
+ f"[green]✅ Auto-approved: {operation.description} (remembered decision)[/green]"
285
+ )
286
+ return True
287
+
288
+ return None
289
+
290
+ except Exception as e:
291
+ logger.error(f"Error checking auto-approval: {e}")
292
+ return None
293
+
294
+ async def _store_approval_pattern(
295
+ self, operation: Operation, approval_request: ApprovalRequest
296
+ ):
297
+ """
298
+ Store approval pattern for future auto-approval.
299
+
300
+ Args:
301
+ operation: The approved operation
302
+ approval_request: The approval request context
303
+ """
304
+ try:
305
+ # Generate operation signature
306
+ operation_signature = self._generate_operation_signature(operation)
307
+
308
+ # Store in permission controller
309
+ await self.permission_controller.grant_operation_permission(
310
+ operation_type=operation.type.value,
311
+ operation_signature=operation_signature,
312
+ metadata={
313
+ "operation_data": operation.data,
314
+ "approval_request_id": approval_request.id,
315
+ "risk_level": approval_request.risk_level.value,
316
+ "stored_reason": "user_remember_decision",
317
+ },
318
+ expires_hours=24 * 30, # 30 days default
319
+ )
320
+
321
+ self.console.print(
322
+ f"[blue]🧠 Stored approval pattern for similar '{operation.type.value}' operations[/blue]"
323
+ )
324
+
325
+ except Exception as e:
326
+ logger.error(f"Error storing approval pattern: {e}")
327
+ self.console.print(
328
+ f"[yellow]⚠️ Failed to store approval pattern: {e}[/yellow]"
329
+ )
330
+
331
+ def _generate_operation_signature(self, operation: Operation) -> str:
332
+ """
333
+ Generate a signature for operation pattern matching.
334
+
335
+ Args:
336
+ operation: Operation to generate signature for
337
+
338
+ Returns:
339
+ String signature for pattern matching
340
+ """
341
+ # Create signature based on operation type and key parameters
342
+ signature_parts = [operation.type.value]
343
+
344
+ # Add type-specific signature components
345
+ if operation.type in [
346
+ OperationType.FILE_READ,
347
+ OperationType.FILE_WRITE,
348
+ OperationType.FILE_DELETE,
349
+ ]:
350
+ if "path" in operation.data:
351
+ path = Path(operation.data["path"])
352
+ # Use directory for pattern matching (allows similar files)
353
+ signature_parts.append(f"dir:{path.parent}")
354
+ # Also include file extension for type matching
355
+ if path.suffix:
356
+ signature_parts.append(f"ext:{path.suffix}")
357
+
358
+ elif operation.type == OperationType.COMMAND_EXECUTE:
359
+ if "command" in operation.data:
360
+ command = operation.data["command"]
361
+ # Use base command name for matching
362
+ base_command = command.split()[0] if command.split() else command
363
+ signature_parts.append(f"cmd:{base_command}")
364
+
365
+ elif operation.type == OperationType.WEB_REQUEST:
366
+ if "url" in operation.data:
367
+ from urllib.parse import urlparse
368
+
369
+ parsed_url = urlparse(operation.data["url"])
370
+ # Use domain for matching
371
+ signature_parts.append(f"domain:{parsed_url.netloc}")
372
+ # Include method
373
+ method = operation.data.get("method", "GET")
374
+ signature_parts.append(f"method:{method}")
375
+
376
+ return "|".join(signature_parts)
377
+
378
+ def _log_approval_decision(self, operation: Operation, approved: bool):
379
+ """Log approval decision for audit trail."""
380
+ log_entry = {
381
+ "timestamp": (
382
+ operation.created_at.isoformat() if operation.created_at else None
383
+ ),
384
+ "operation_type": operation.type.value,
385
+ "operation_description": operation.description,
386
+ "approved": approved,
387
+ "operation_signature": self._generate_operation_signature(operation),
388
+ }
389
+
390
+ self.approval_session_log.append(log_entry)
391
+
392
+ # Limit session log size
393
+ if len(self.approval_session_log) > 100:
394
+ self.approval_session_log = self.approval_session_log[-100:]
395
+
396
+ def _log_batch_approval_decision(
397
+ self, operations: List[Operation], batch_request: BatchApprovalRequest
398
+ ):
399
+ """Log batch approval decision for audit trail."""
400
+ summary = batch_request.get_approval_summary()
401
+
402
+ for i, operation in enumerate(operations):
403
+ approved = i in batch_request.approved_operations
404
+ self._log_approval_decision(operation, approved)
405
+
406
+ # Add batch summary log
407
+ batch_log_entry = {
408
+ "timestamp": batch_request.created_at.isoformat(),
409
+ "operation_type": "batch_approval",
410
+ "operation_description": f"Batch of {len(operations)} operations",
411
+ "approved": summary["all_approved"],
412
+ "batch_summary": summary,
413
+ "batch_id": batch_request.id,
414
+ }
415
+
416
+ self.approval_session_log.append(batch_log_entry)
417
+
418
+ def _record_session_decision(
419
+ self, operation: Operation, decision: ApprovalDecision
420
+ ):
421
+ """Record detailed session decision information."""
422
+ decision_record = {
423
+ "operation_type": operation.type.value,
424
+ "operation_description": operation.description,
425
+ "decision_type": decision.decision.value,
426
+ "approved": decision.is_approved,
427
+ "remember": decision.should_remember,
428
+ "response_time_seconds": decision.response_time_seconds,
429
+ "timeout_occurred": decision.timeout_occurred,
430
+ "user_notes": decision.user_notes,
431
+ "timestamp": decision.created_at.isoformat(),
432
+ }
433
+
434
+ # Store in session log
435
+ session_entry = {
436
+ **decision_record,
437
+ "operation_signature": self._generate_operation_signature(operation),
438
+ }
439
+
440
+ self.approval_session_log.append(session_entry)
441
+
442
+ def get_session_statistics(self) -> Dict[str, Any]:
443
+ """Get statistics about approval decisions in this session."""
444
+ if not self.approval_session_log:
445
+ return {"total_decisions": 0}
446
+
447
+ total = len(
448
+ [entry for entry in self.approval_session_log if "decision_type" in entry]
449
+ )
450
+ approved = len(
451
+ [
452
+ entry
453
+ for entry in self.approval_session_log
454
+ if entry.get("approved", False)
455
+ ]
456
+ )
457
+ remembered = len(
458
+ [
459
+ entry
460
+ for entry in self.approval_session_log
461
+ if entry.get("remember", False)
462
+ ]
463
+ )
464
+
465
+ # Calculate average response time
466
+ response_times = [
467
+ entry.get("response_time_seconds", 0)
468
+ for entry in self.approval_session_log
469
+ if "response_time_seconds" in entry
470
+ ]
471
+ avg_response_time = (
472
+ sum(response_times) / len(response_times) if response_times else 0
473
+ )
474
+
475
+ return {
476
+ "total_decisions": total,
477
+ "approved_decisions": approved,
478
+ "denied_decisions": total - approved,
479
+ "remembered_decisions": remembered,
480
+ "approval_rate": approved / total if total > 0 else 0,
481
+ "remember_rate": remembered / total if total > 0 else 0,
482
+ "average_response_time_seconds": avg_response_time,
483
+ "recent_decisions": (
484
+ self.approval_session_log[-10:] if self.approval_session_log else []
485
+ ),
486
+ }
487
+
488
+ def add_no_approval_flag_support(self, no_approval_enabled: bool = False):
489
+ """
490
+ Configure support for --no-approval flag.
491
+
492
+ Args:
493
+ no_approval_enabled: Whether --no-approval flag is active
494
+ """
495
+ if no_approval_enabled:
496
+ self.console.print(
497
+ "[yellow]⚠️ WARNING: Approval system bypassed with --no-approval flag[/yellow]"
498
+ )
499
+ self.console.print(
500
+ "[yellow] All operations will be executed without user confirmation[/yellow]"
501
+ )
502
+
503
+ # Set approval manager to auto-approve everything
504
+ self.approval_manager.approval_callback = lambda ctx: True
505
+ self.enable_auto_approval = True
506
+ else:
507
+ # Restore normal approval behavior
508
+ self.approval_manager.set_approval_callback(self._handle_single_approval)
509
+
510
+ async def cleanup(self):
511
+ """Clean up resources and save session information."""
512
+ try:
513
+ # Save session statistics if significant activity
514
+ if len(self.approval_session_log) > 0:
515
+ stats = self.get_session_statistics()
516
+ logger.info(
517
+ f"Approval session completed with {stats['total_decisions']} decisions"
518
+ )
519
+
520
+ # Clean up approval manager resources
521
+ if self.approval_manager:
522
+ await self._cleanup_approval_manager()
523
+
524
+ # Clean up expired approvals in permission controller
525
+ if self.permission_controller:
526
+ expired_count = self.permission_controller.cleanup_expired_approvals()
527
+ if expired_count > 0:
528
+ logger.info(
529
+ f"Cleaned up {expired_count} expired approvals during shutdown"
530
+ )
531
+
532
+ # Clean up prompt handler resources
533
+ if self.prompt_handler:
534
+ self.prompt_handler.cleanup_resources()
535
+
536
+ # Clear session log
537
+ self.approval_session_log.clear()
538
+
539
+ except Exception as e:
540
+ logger.error(f"Error during approval integration cleanup: {e}")
541
+
542
+ async def _cleanup_approval_manager(self):
543
+ """Clean up approval manager resources and pending requests."""
544
+ try:
545
+ # Cancel any pending approval requests
546
+ await self._cancel_pending_approvals()
547
+
548
+ # Clean up expired batch requests
549
+ if hasattr(self.approval_manager, "cleanup_expired_requests"):
550
+ expired_count = self.approval_manager.cleanup_expired_requests()
551
+ if expired_count > 0:
552
+ logger.info(f"Cleaned up {expired_count} expired batch requests")
553
+
554
+ # Log pending operations that will be cancelled
555
+ pending_operations = getattr(
556
+ self.approval_manager, "pending_operations", {}
557
+ )
558
+ if pending_operations:
559
+ logger.warning(
560
+ f"Cancelling {len(pending_operations)} pending approval operations"
561
+ )
562
+
563
+ except Exception as e:
564
+ logger.error(f"Error cleaning up approval manager: {e}")
565
+
566
+ async def _cancel_pending_approvals(self):
567
+ """Cancel any pending approval requests gracefully."""
568
+ try:
569
+ # Check if approval manager has pending requests
570
+ if hasattr(self.approval_manager, "pending_batches"):
571
+ pending_batches = self.approval_manager.pending_batches
572
+ if pending_batches:
573
+ logger.info(
574
+ f"Cancelling {len(pending_batches)} pending batch approvals"
575
+ )
576
+
577
+ # Cancel each pending batch
578
+ cancelled_batches = []
579
+ for batch_id, batch_request in pending_batches.items():
580
+ try:
581
+ # Mark as cancelled
582
+ batch_request.status = getattr(
583
+ batch_request, "status", None
584
+ )
585
+ if hasattr(batch_request, "cancel"):
586
+ batch_request.cancel("System shutdown")
587
+ cancelled_batches.append(batch_id)
588
+
589
+ # Log the cancellation
590
+ self._log_cancelled_batch(batch_request)
591
+
592
+ except Exception as batch_error:
593
+ logger.error(
594
+ f"Error cancelling batch {batch_id}: {batch_error}"
595
+ )
596
+
597
+ # Move cancelled batches to completed
598
+ if hasattr(self.approval_manager, "completed_batches"):
599
+ for batch_id in cancelled_batches:
600
+ batch_request = pending_batches[batch_id]
601
+ self.approval_manager.completed_batches[batch_id] = (
602
+ batch_request
603
+ )
604
+ del pending_batches[batch_id]
605
+
606
+ except Exception as e:
607
+ logger.error(f"Error cancelling pending approvals: {e}")
608
+
609
+ def _log_cancelled_batch(self, batch_request):
610
+ """Log information about a cancelled batch request."""
611
+ try:
612
+ # Add cancellation entry to session log
613
+ cancellation_log_entry = {
614
+ "timestamp": datetime.now().isoformat(),
615
+ "operation_type": "batch_cancellation",
616
+ "operation_description": f'Cancelled batch of {len(getattr(batch_request, "operations", []))} operations',
617
+ "approved": False,
618
+ "batch_id": getattr(batch_request, "id", "unknown"),
619
+ "cancellation_reason": "System shutdown",
620
+ }
621
+
622
+ self.approval_session_log.append(cancellation_log_entry)
623
+
624
+ except Exception as e:
625
+ logger.error(f"Error logging cancelled batch: {e}")
626
+
627
+ async def handle_operation_cancellation(
628
+ self, operation_id: str, reason: str = "User cancelled"
629
+ ):
630
+ """
631
+ Handle cancellation of a specific operation with cleanup.
632
+
633
+ Args:
634
+ operation_id: ID of operation to cancel
635
+ reason: Reason for cancellation
636
+ """
637
+ try:
638
+ # Log the cancellation
639
+ cancellation_entry = {
640
+ "timestamp": datetime.now().isoformat(),
641
+ "operation_type": "operation_cancellation",
642
+ "operation_description": f"Cancelled operation {operation_id}",
643
+ "approved": False,
644
+ "cancellation_reason": reason,
645
+ "operation_id": operation_id,
646
+ }
647
+
648
+ self.approval_session_log.append(cancellation_entry)
649
+
650
+ # Clean up any related approval state
651
+ await self._cleanup_operation_state(operation_id)
652
+
653
+ logger.info(f"Operation {operation_id} cancelled: {reason}")
654
+
655
+ except Exception as e:
656
+ logger.error(
657
+ f"Error handling operation cancellation for {operation_id}: {e}"
658
+ )
659
+
660
+ async def _cleanup_operation_state(self, operation_id: str):
661
+ """Clean up state related to a cancelled operation."""
662
+ try:
663
+ # Remove from any pending operation tracking
664
+ if hasattr(self.approval_manager, "pending_operations"):
665
+ pending_ops = self.approval_manager.pending_operations
666
+ if operation_id in pending_ops:
667
+ del pending_ops[operation_id]
668
+
669
+ # Clean up any temporary resources related to the operation
670
+ # This could include temporary files, locks, etc.
671
+
672
+ except Exception as e:
673
+ logger.error(f"Error cleaning up operation state for {operation_id}: {e}")
674
+
675
+
676
+ # Utility functions for easy integration
677
+
678
+
679
+ async def create_cli_approval_integration(
680
+ approval_manager: Optional[EnhancedApprovalManager] = None,
681
+ permission_controller: Optional[PermissionController] = None,
682
+ console: Optional[Console] = None,
683
+ config: Optional[Dict[str, Any]] = None,
684
+ ) -> CLIApprovalIntegration:
685
+ """
686
+ Create and configure CLI approval integration.
687
+
688
+ Args:
689
+ approval_manager: Enhanced approval manager instance
690
+ permission_controller: Permission controller instance
691
+ console: Rich console instance
692
+ config: Configuration dictionary
693
+
694
+ Returns:
695
+ Configured CLIApprovalIntegration instance
696
+ """
697
+ config = config or {}
698
+
699
+ integration = CLIApprovalIntegration(
700
+ approval_manager=approval_manager,
701
+ permission_controller=permission_controller,
702
+ console=console,
703
+ enable_auto_approval=config.get("enable_auto_approval", True),
704
+ approval_timeout_seconds=config.get("approval_timeout_seconds", 300),
705
+ )
706
+
707
+ return integration
708
+
709
+
710
+ def inject_approval_integration_into_agent_engine(
711
+ agent_engine, cli_approval_integration: CLIApprovalIntegration
712
+ ):
713
+ """
714
+ Inject CLI approval integration into an agent engine.
715
+
716
+ Args:
717
+ agent_engine: Agent engine to modify
718
+ cli_approval_integration: CLI approval integration instance
719
+ """
720
+ # Replace the approval manager's approval callback
721
+ if hasattr(agent_engine, "approval") and agent_engine.approval:
722
+ agent_engine.approval.set_approval_callback(
723
+ cli_approval_integration._handle_single_approval
724
+ )
725
+ agent_engine.approval.set_batch_approval_callback(
726
+ cli_approval_integration._handle_batch_approval
727
+ )
728
+
729
+ # Store reference for cleanup
730
+ agent_engine._cli_approval_integration = cli_approval_integration
731
+
732
+ logger.info("CLI approval integration injected into agent engine")
733
+ else:
734
+ logger.warning("Agent engine has no approval manager to integrate with")