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,748 @@
1
+ """
2
+ Enhanced Program Execution and Command Runner for Omnimancer Agent.
3
+
4
+ This module provides secure command execution with whitelisting, sandboxing,
5
+ real-time output streaming, resource limits, and comprehensive error handling.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import os
11
+ import shlex
12
+ import time
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from typing import (
16
+ Any,
17
+ AsyncIterator,
18
+ Callable,
19
+ Dict,
20
+ List,
21
+ Optional,
22
+ )
23
+
24
+
25
+ from ...utils.errors import SecurityError
26
+ from ..security.approval_workflow import ApprovalWorkflow, RiskLevel
27
+ from ..security.sandbox_manager import (
28
+ ResourceLimits,
29
+ SandboxedProcess,
30
+ SandboxManager,
31
+ )
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ class CommandCategory(Enum):
37
+ """Categories of commands for security classification."""
38
+
39
+ SAFE_READ = "safe_read" # ls, cat, grep, find
40
+ DEVELOPMENT = "development" # git, npm, pip, make
41
+ SYSTEM_INFO = "system_info" # ps, df, uptime, which
42
+ BUILD_TOOLS = "build_tools" # docker, docker-compose
43
+ PACKAGE_MANAGERS = "package_mgmt" # npm, pip, yarn, composer
44
+ DANGEROUS = "dangerous" # rm, sudo, chmod, dd
45
+ NETWORK = "network" # curl, wget, ssh, nc
46
+
47
+
48
+ class ExecutionMode(Enum):
49
+ """Execution modes with different security levels."""
50
+
51
+ RESTRICTED = "restricted" # Only safe read commands
52
+ DEVELOPMENT = "development" # Safe + development tools
53
+ FULL_ACCESS = "full_access" # All whitelisted commands
54
+ SANDBOX = "sandbox" # Isolated execution environment
55
+
56
+
57
+ @dataclass
58
+ class CommandResult:
59
+ """Result of command execution with streaming support."""
60
+
61
+ success: bool
62
+ command: str
63
+ args: List[str]
64
+ exit_code: int
65
+ stdout: str = ""
66
+ stderr: str = ""
67
+ execution_time: float = 0.0
68
+ resource_usage: Dict[str, Any] = field(default_factory=dict)
69
+ sandbox_info: Optional[Dict[str, Any]] = None
70
+ error_message: Optional[str] = None
71
+ was_terminated: bool = False
72
+
73
+ @property
74
+ def full_command(self) -> str:
75
+ """Get the full command string."""
76
+ return f"{self.command} {' '.join(self.args)}" if self.args else self.command
77
+
78
+
79
+ @dataclass
80
+ class ExecutionConfig:
81
+ """Configuration for command execution."""
82
+
83
+ timeout_seconds: int = 30
84
+ max_memory_mb: int = 512
85
+ max_output_size: int = 10 * 1024 * 1024 # 10MB
86
+ working_directory: Optional[str] = None
87
+ environment_vars: Dict[str, str] = field(default_factory=dict)
88
+ execution_mode: ExecutionMode = ExecutionMode.DEVELOPMENT
89
+ enable_streaming: bool = True
90
+ stream_callback: Optional[Callable[[str, str], None]] = (
91
+ None # (stream_type, content)
92
+ )
93
+ require_approval: bool = True
94
+
95
+
96
+ class CommandValidator:
97
+ """Validates commands against security policies."""
98
+
99
+ def __init__(self):
100
+ self.command_categories = {
101
+ CommandCategory.SAFE_READ: {
102
+ "ls",
103
+ "cat",
104
+ "head",
105
+ "tail",
106
+ "grep",
107
+ "find",
108
+ "wc",
109
+ "sort",
110
+ "uniq",
111
+ "diff",
112
+ "less",
113
+ "more",
114
+ "file",
115
+ "stat",
116
+ "du",
117
+ "tree",
118
+ "echo",
119
+ },
120
+ CommandCategory.DEVELOPMENT: {
121
+ "git",
122
+ "python",
123
+ "python3",
124
+ "node",
125
+ "npm",
126
+ "yarn",
127
+ "pip",
128
+ "pip3",
129
+ "make",
130
+ "cmake",
131
+ "javac",
132
+ "java",
133
+ "gcc",
134
+ "g++",
135
+ "rustc",
136
+ "cargo",
137
+ "go",
138
+ "mvn",
139
+ "gradle",
140
+ "composer",
141
+ },
142
+ CommandCategory.SYSTEM_INFO: {
143
+ "ps",
144
+ "top",
145
+ "htop",
146
+ "df",
147
+ "free",
148
+ "uptime",
149
+ "whoami",
150
+ "id",
151
+ "uname",
152
+ "which",
153
+ "whereis",
154
+ "env",
155
+ "printenv",
156
+ "pwd",
157
+ "date",
158
+ "sleep",
159
+ },
160
+ CommandCategory.BUILD_TOOLS: {
161
+ "docker",
162
+ "docker-compose",
163
+ "podman",
164
+ "buildah",
165
+ "kubernetes",
166
+ "kubectl",
167
+ "helm",
168
+ "terraform",
169
+ "ansible",
170
+ },
171
+ CommandCategory.PACKAGE_MANAGERS: {
172
+ "apt",
173
+ "apt-get",
174
+ "yum",
175
+ "dnf",
176
+ "pacman",
177
+ "brew",
178
+ "choco",
179
+ "snap",
180
+ "flatpak",
181
+ "conda",
182
+ "mamba",
183
+ },
184
+ CommandCategory.DANGEROUS: {
185
+ "rm",
186
+ "rmdir",
187
+ "mv",
188
+ "cp",
189
+ "chmod",
190
+ "chown",
191
+ "chgrp",
192
+ "sudo",
193
+ "su",
194
+ "passwd",
195
+ "dd",
196
+ "fdisk",
197
+ "parted",
198
+ "mkfs",
199
+ "mount",
200
+ "umount",
201
+ "systemctl",
202
+ "service",
203
+ "kill",
204
+ "killall",
205
+ "pkill",
206
+ },
207
+ CommandCategory.NETWORK: {
208
+ "curl",
209
+ "wget",
210
+ "ssh",
211
+ "scp",
212
+ "rsync",
213
+ "nc",
214
+ "netcat",
215
+ "telnet",
216
+ "ftp",
217
+ "sftp",
218
+ "ping",
219
+ "traceroute",
220
+ "nslookup",
221
+ "dig",
222
+ },
223
+ }
224
+
225
+ # Commands allowed by execution mode
226
+ self.mode_permissions = {
227
+ ExecutionMode.RESTRICTED: {CommandCategory.SAFE_READ},
228
+ ExecutionMode.DEVELOPMENT: {
229
+ CommandCategory.SAFE_READ,
230
+ CommandCategory.DEVELOPMENT,
231
+ CommandCategory.SYSTEM_INFO,
232
+ },
233
+ ExecutionMode.FULL_ACCESS: {
234
+ CommandCategory.SAFE_READ,
235
+ CommandCategory.DEVELOPMENT,
236
+ CommandCategory.SYSTEM_INFO,
237
+ CommandCategory.BUILD_TOOLS,
238
+ CommandCategory.PACKAGE_MANAGERS,
239
+ CommandCategory.NETWORK,
240
+ },
241
+ ExecutionMode.SANDBOX: {
242
+ CommandCategory.SAFE_READ,
243
+ CommandCategory.DEVELOPMENT,
244
+ CommandCategory.SYSTEM_INFO,
245
+ },
246
+ }
247
+
248
+ # Dangerous patterns in command arguments
249
+ self.dangerous_patterns = [
250
+ r"rm\s+-rf\s*/", # rm -rf /
251
+ r"sudo\s+rm", # sudo rm
252
+ r"chmod\s+777", # chmod 777
253
+ r">\s*/dev/", # redirect to /dev
254
+ r"\|\s*nc\s", # pipe to netcat
255
+ r"eval\s*\(", # eval execution
256
+ r"exec\s*\(", # exec execution
257
+ r"system\s*\(", # system calls
258
+ r"`[^`]*`", # backtick execution
259
+ r"\$\([^)]*\)", # command substitution
260
+ ]
261
+
262
+ def get_command_category(self, command: str) -> Optional[CommandCategory]:
263
+ """Get the category of a command."""
264
+ command_lower = command.lower()
265
+ for category, commands in self.command_categories.items():
266
+ if command_lower in commands:
267
+ return category
268
+ return None
269
+
270
+ def is_command_allowed(self, command: str, mode: ExecutionMode) -> bool:
271
+ """Check if command is allowed in the given execution mode."""
272
+ category = self.get_command_category(command)
273
+ if not category:
274
+ return False # Unknown commands are not allowed
275
+
276
+ allowed_categories = self.mode_permissions.get(mode, set())
277
+ return category in allowed_categories
278
+
279
+ def assess_command_risk(self, command: str, args: List[str]) -> RiskLevel:
280
+ """Assess the risk level of a command."""
281
+ category = self.get_command_category(command)
282
+
283
+ if category == CommandCategory.DANGEROUS:
284
+ return RiskLevel.CRITICAL
285
+
286
+ full_command = f"{command} {' '.join(args)}"
287
+
288
+ # Check for dangerous patterns
289
+ import re
290
+
291
+ for pattern in self.dangerous_patterns:
292
+ if re.search(pattern, full_command, re.IGNORECASE):
293
+ return RiskLevel.CRITICAL
294
+
295
+ # Risk by category
296
+ risk_mapping = {
297
+ CommandCategory.SAFE_READ: RiskLevel.LOW,
298
+ CommandCategory.SYSTEM_INFO: RiskLevel.LOW,
299
+ CommandCategory.DEVELOPMENT: RiskLevel.MEDIUM,
300
+ CommandCategory.BUILD_TOOLS: RiskLevel.MEDIUM,
301
+ CommandCategory.PACKAGE_MANAGERS: RiskLevel.HIGH,
302
+ CommandCategory.NETWORK: RiskLevel.HIGH,
303
+ }
304
+
305
+ return risk_mapping.get(category, RiskLevel.HIGH)
306
+
307
+ def validate_command_args(self, command: str, args: List[str]) -> List[str]:
308
+ """Validate and sanitize command arguments."""
309
+
310
+ sanitized_args = []
311
+ for arg in args:
312
+ # Remove dangerous characters and patterns
313
+ if any(char in arg for char in [";", "|", "&", "`", "$", ">"]):
314
+ # For now, reject arguments with shell metacharacters
315
+ # In production, this could be more sophisticated
316
+ raise SecurityError(f"Argument contains dangerous characters: {arg}")
317
+
318
+ # Ensure proper escaping
319
+ sanitized_args.append(shlex.quote(arg))
320
+
321
+ return sanitized_args
322
+
323
+
324
+ class StreamingExecutor:
325
+ """Handles real-time output streaming from processes."""
326
+
327
+ def __init__(self, callback: Optional[Callable[[str, str], None]] = None):
328
+ self.callback = callback
329
+ self.stdout_buffer = []
330
+ self.stderr_buffer = []
331
+
332
+ async def stream_process_output(self, process: asyncio.subprocess.Process) -> None:
333
+ """Stream output from a running process."""
334
+
335
+ async def read_stream(
336
+ stream: asyncio.StreamReader, stream_type: str, buffer: List[str]
337
+ ):
338
+ try:
339
+ while True:
340
+ line = await stream.readline()
341
+ if not line:
342
+ break
343
+
344
+ line_str = line.decode("utf-8", errors="replace")
345
+ buffer.append(line_str)
346
+
347
+ if self.callback:
348
+ try:
349
+ self.callback(stream_type, line_str.rstrip())
350
+ except Exception as callback_error:
351
+ logger.debug(f"Stream callback error: {callback_error}")
352
+
353
+ except Exception as e:
354
+ logger.debug(f"Error reading {stream_type}: {e}")
355
+
356
+ # Start streaming tasks
357
+ tasks = []
358
+ if process.stdout:
359
+ tasks.append(read_stream(process.stdout, "stdout", self.stdout_buffer))
360
+ if process.stderr:
361
+ tasks.append(read_stream(process.stderr, "stderr", self.stderr_buffer))
362
+
363
+ if tasks:
364
+ await asyncio.gather(*tasks, return_exceptions=True)
365
+
366
+ def get_output(self) -> tuple[str, str]:
367
+ """Get accumulated output."""
368
+ stdout = "".join(self.stdout_buffer)
369
+ stderr = "".join(self.stderr_buffer)
370
+ return stdout, stderr
371
+
372
+
373
+ class EnhancedProgramExecutor:
374
+ """
375
+ Enhanced program executor with comprehensive security and monitoring.
376
+
377
+ Features:
378
+ - Command whitelisting and validation
379
+ - Sandboxed execution with resource limits
380
+ - Real-time output streaming
381
+ - Comprehensive error handling and cleanup
382
+ - Integration with approval workflow
383
+ """
384
+
385
+ def __init__(
386
+ self,
387
+ sandbox_manager: Optional[SandboxManager] = None,
388
+ approval_workflow: Optional[ApprovalWorkflow] = None,
389
+ ):
390
+ self.validator = CommandValidator()
391
+ self.sandbox_manager = sandbox_manager or SandboxManager()
392
+ self.approval_workflow = approval_workflow
393
+ self.active_processes: Dict[str, SandboxedProcess] = {}
394
+ self.execution_history: List[CommandResult] = []
395
+
396
+ async def execute_command(
397
+ self,
398
+ command: str,
399
+ args: List[str] = None,
400
+ config: ExecutionConfig = None,
401
+ ) -> CommandResult:
402
+ """
403
+ Execute a command with full security controls.
404
+
405
+ Args:
406
+ command: Command to execute
407
+ args: Command arguments
408
+ config: Execution configuration
409
+
410
+ Returns:
411
+ CommandResult with execution details
412
+ """
413
+ if args is None:
414
+ args = []
415
+ if config is None:
416
+ config = ExecutionConfig()
417
+
418
+ execution_id = f"exec_{int(time.time() * 1000)}"
419
+ start_time = time.time()
420
+
421
+ try:
422
+ # Validate command
423
+ if not self.validator.is_command_allowed(command, config.execution_mode):
424
+ raise SecurityError(
425
+ f"Command '{command}' not allowed in {config.execution_mode.value} mode"
426
+ )
427
+
428
+ # Assess risk and request approval if needed
429
+ risk_level = self.validator.assess_command_risk(command, args)
430
+ if config.require_approval and self.approval_workflow:
431
+ approved = await self._request_approval(
432
+ command, args, risk_level, config
433
+ )
434
+ if not approved:
435
+ return CommandResult(
436
+ success=False,
437
+ command=command,
438
+ args=args,
439
+ exit_code=-1,
440
+ error_message="Command execution denied by user",
441
+ )
442
+
443
+ # Sanitize arguments
444
+ sanitized_args = self.validator.validate_command_args(command, args)
445
+
446
+ # Execute based on mode
447
+ if config.execution_mode == ExecutionMode.SANDBOX:
448
+ result = await self._execute_sandboxed(
449
+ command, sanitized_args, config, execution_id
450
+ )
451
+ else:
452
+ result = await self._execute_direct(command, sanitized_args, config)
453
+
454
+ result.execution_time = time.time() - start_time
455
+ self.execution_history.append(result)
456
+
457
+ return result
458
+
459
+ except Exception as e:
460
+ logger.error(f"Command execution failed: {e}")
461
+ return CommandResult(
462
+ success=False,
463
+ command=command,
464
+ args=args,
465
+ exit_code=-1,
466
+ execution_time=time.time() - start_time,
467
+ error_message=str(e),
468
+ )
469
+ finally:
470
+ # Cleanup active process tracking
471
+ if execution_id in self.active_processes:
472
+ del self.active_processes[execution_id]
473
+
474
+ async def _execute_direct(
475
+ self, command: str, args: List[str], config: ExecutionConfig
476
+ ) -> CommandResult:
477
+ """Execute command directly with basic controls."""
478
+ try:
479
+ # Prepare environment
480
+ env = dict(os.environ)
481
+ env.update(config.environment_vars)
482
+
483
+ # Create process
484
+ process = await asyncio.create_subprocess_exec(
485
+ command,
486
+ *args,
487
+ stdout=asyncio.subprocess.PIPE,
488
+ stderr=asyncio.subprocess.PIPE,
489
+ cwd=config.working_directory,
490
+ env=env,
491
+ )
492
+
493
+ # Handle streaming if enabled
494
+ if config.enable_streaming:
495
+ streaming_executor = StreamingExecutor(config.stream_callback)
496
+ stream_task = asyncio.create_task(
497
+ streaming_executor.stream_process_output(process)
498
+ )
499
+
500
+ # Wait for completion with timeout
501
+ try:
502
+ await asyncio.wait_for(
503
+ process.wait(), timeout=config.timeout_seconds
504
+ )
505
+ await stream_task
506
+ stdout, stderr = streaming_executor.get_output()
507
+ except asyncio.TimeoutError:
508
+ # Cancel the streaming task first
509
+ stream_task.cancel()
510
+ process.terminate()
511
+ try:
512
+ await asyncio.wait_for(process.wait(), timeout=5)
513
+ except asyncio.TimeoutError:
514
+ process.kill()
515
+ await process.wait()
516
+
517
+ # Clean up the streaming task
518
+ try:
519
+ await stream_task
520
+ except asyncio.CancelledError:
521
+ pass
522
+
523
+ stdout, stderr = streaming_executor.get_output()
524
+ return CommandResult(
525
+ success=False,
526
+ command=command,
527
+ args=args,
528
+ exit_code=-1,
529
+ stdout=stdout,
530
+ stderr=stderr,
531
+ was_terminated=True,
532
+ error_message=f"Command timed out after {config.timeout_seconds} seconds",
533
+ )
534
+ else:
535
+ # Simple execution without streaming
536
+ try:
537
+ stdout, stderr = await asyncio.wait_for(
538
+ process.communicate(), timeout=config.timeout_seconds
539
+ )
540
+ stdout = stdout.decode("utf-8", errors="replace")
541
+ stderr = stderr.decode("utf-8", errors="replace")
542
+ except asyncio.TimeoutError:
543
+ process.terminate()
544
+ try:
545
+ await asyncio.wait_for(process.wait(), timeout=5)
546
+ except asyncio.TimeoutError:
547
+ process.kill()
548
+ await process.wait()
549
+
550
+ return CommandResult(
551
+ success=False,
552
+ command=command,
553
+ args=args,
554
+ exit_code=-1,
555
+ was_terminated=True,
556
+ error_message=f"Command timed out after {config.timeout_seconds} seconds",
557
+ )
558
+
559
+ return CommandResult(
560
+ success=process.returncode == 0,
561
+ command=command,
562
+ args=args,
563
+ exit_code=process.returncode,
564
+ stdout=stdout,
565
+ stderr=stderr,
566
+ error_message=stderr if process.returncode != 0 else None,
567
+ )
568
+
569
+ except Exception as e:
570
+ return CommandResult(
571
+ success=False,
572
+ command=command,
573
+ args=args,
574
+ exit_code=-1,
575
+ error_message=f"Execution failed: {str(e)}",
576
+ )
577
+
578
+ async def _execute_sandboxed(
579
+ self,
580
+ command: str,
581
+ args: List[str],
582
+ config: ExecutionConfig,
583
+ execution_id: str,
584
+ ) -> CommandResult:
585
+ """Execute command in sandbox with resource limits."""
586
+ resource_limits = ResourceLimits(
587
+ max_memory_mb=config.max_memory_mb,
588
+ timeout_seconds=config.timeout_seconds,
589
+ max_file_size_mb=100,
590
+ max_open_files=100,
591
+ max_processes=10,
592
+ )
593
+
594
+ try:
595
+ with self.sandbox_manager.create_sandbox(resource_limits) as sandbox:
596
+ # Execute in sandbox
597
+ sandboxed_process = await sandbox.execute_command(
598
+ command,
599
+ args,
600
+ working_dir=config.working_directory,
601
+ env=config.environment_vars,
602
+ enable_streaming=config.enable_streaming,
603
+ stream_callback=config.stream_callback,
604
+ )
605
+
606
+ self.active_processes[execution_id] = sandboxed_process
607
+
608
+ # Wait for completion
609
+ result = await sandboxed_process.wait_for_completion()
610
+
611
+ return CommandResult(
612
+ success=result.exit_code == 0,
613
+ command=command,
614
+ args=args,
615
+ exit_code=result.exit_code,
616
+ stdout=result.stdout,
617
+ stderr=result.stderr,
618
+ resource_usage=result.resource_usage,
619
+ sandbox_info=result.sandbox_info,
620
+ was_terminated=result.was_terminated,
621
+ error_message=result.error_message,
622
+ )
623
+
624
+ except Exception as e:
625
+ return CommandResult(
626
+ success=False,
627
+ command=command,
628
+ args=args,
629
+ exit_code=-1,
630
+ error_message=f"Sandboxed execution failed: {str(e)}",
631
+ )
632
+
633
+ async def _request_approval(
634
+ self,
635
+ command: str,
636
+ args: List[str],
637
+ risk_level: RiskLevel,
638
+ config: ExecutionConfig,
639
+ ) -> bool:
640
+ """Request approval for command execution."""
641
+ if not self.approval_workflow:
642
+ return True
643
+
644
+ full_command = f"{command} {' '.join(args)}"
645
+
646
+ request = await self.approval_workflow.request_approval(
647
+ operation_type="command_execute",
648
+ description=f"Execute command: {full_command}",
649
+ metadata={
650
+ "command": command,
651
+ "args": args,
652
+ "working_dir": config.working_directory,
653
+ "execution_mode": config.execution_mode.value,
654
+ "risk_level": risk_level.value,
655
+ },
656
+ )
657
+
658
+ return request.status.value == "approved"
659
+
660
+ async def terminate_command(self, execution_id: str) -> bool:
661
+ """Terminate a running command."""
662
+ if execution_id in self.active_processes:
663
+ process = self.active_processes[execution_id]
664
+ process.terminate()
665
+ return True
666
+ return False
667
+
668
+ def get_execution_history(self, limit: int = 50) -> List[CommandResult]:
669
+ """Get recent execution history."""
670
+ return self.execution_history[-limit:]
671
+
672
+ def get_active_processes(self) -> Dict[str, Dict[str, Any]]:
673
+ """Get information about currently running processes."""
674
+ active = {}
675
+ for exec_id, process in self.active_processes.items():
676
+ if process.is_running():
677
+ active[exec_id] = {
678
+ "command": f"{process.process.args[0]} {' '.join(process.process.args[1:])}",
679
+ "pid": process.process.pid,
680
+ "start_time": process.start_time,
681
+ "runtime": time.time() - process.start_time,
682
+ }
683
+ return active
684
+
685
+ async def stream_command_output(
686
+ self,
687
+ command: str,
688
+ args: List[str] = None,
689
+ config: ExecutionConfig = None,
690
+ ) -> AsyncIterator[tuple[str, str]]:
691
+ """
692
+ Execute command and yield real-time output.
693
+
694
+ Yields:
695
+ Tuples of (stream_type, content) where stream_type is 'stdout' or 'stderr'
696
+ """
697
+ if args is None:
698
+ args = []
699
+ if config is None:
700
+ config = ExecutionConfig(enable_streaming=True)
701
+
702
+ # Set up streaming callback to yield output
703
+ output_queue = asyncio.Queue()
704
+
705
+ def stream_callback(stream_type: str, content: str):
706
+ try:
707
+ # Use the current event loop to schedule the task safely
708
+ loop = asyncio.get_running_loop()
709
+ loop.create_task(output_queue.put((stream_type, content)))
710
+ except RuntimeError:
711
+ # Event loop might be closing, ignore this output
712
+ pass
713
+ except Exception:
714
+ # Ignore other exceptions to prevent callback from crashing the program
715
+ pass
716
+
717
+ config.stream_callback = stream_callback
718
+
719
+ # Start execution in background
720
+ execution_task = asyncio.create_task(
721
+ self.execute_command(command, args, config)
722
+ )
723
+
724
+ # Yield output as it becomes available
725
+ try:
726
+ while not execution_task.done():
727
+ try:
728
+ stream_type, content = await asyncio.wait_for(
729
+ output_queue.get(), timeout=0.1
730
+ )
731
+ yield stream_type, content
732
+ except asyncio.TimeoutError:
733
+ continue
734
+
735
+ # Get any remaining output
736
+ while not output_queue.empty():
737
+ stream_type, content = await output_queue.get()
738
+ yield stream_type, content
739
+
740
+ finally:
741
+ # Ensure execution completes
742
+ if not execution_task.done():
743
+ execution_task.cancel()
744
+ try:
745
+ await execution_task
746
+ except (asyncio.CancelledError, Exception):
747
+ # Ignore cancellation and other exceptions during cleanup
748
+ pass