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