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,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")
|