PYAE1994 commited on
Commit
87fb933
Β·
verified Β·
1 Parent(s): 04b77b0

feat: GOD MODE+ v4.0 - tools/terminal_engine.py

Browse files
Files changed (1) hide show
  1. tools/terminal_engine.py +380 -0
tools/terminal_engine.py ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Terminal Execution Engine β€” Real Command Runner
3
+ Streaming logs, timeout control, session management, self-repair loop
4
+ """
5
+
6
+ import asyncio
7
+ import os
8
+ import pty
9
+ import select
10
+ import signal
11
+ import time
12
+ import uuid
13
+ from typing import AsyncGenerator, Callable, Dict, List, Optional
14
+
15
+ import structlog
16
+
17
+ log = structlog.get_logger()
18
+
19
+ WORKSPACE = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace")
20
+
21
+ BLOCKED_COMMANDS = [
22
+ "rm -rf /",
23
+ ":(){ :|:& };:",
24
+ "mkfs",
25
+ "shutdown",
26
+ "reboot",
27
+ "halt",
28
+ "dd if=/dev/zero",
29
+ "dd if=/dev/urandom of=/dev",
30
+ "> /dev/sda",
31
+ "chmod 777 /",
32
+ "chown root /",
33
+ ]
34
+
35
+
36
+ def _is_blocked(cmd: str) -> bool:
37
+ c = cmd.strip()
38
+ for b in BLOCKED_COMMANDS:
39
+ if b in c:
40
+ return True
41
+ return False
42
+
43
+
44
+ class ProcessSession:
45
+ """Manages a persistent shell session."""
46
+ def __init__(self, session_id: str, cwd: str = WORKSPACE):
47
+ self.session_id = session_id
48
+ self.cwd = cwd
49
+ self.history: List[Dict] = []
50
+ self.env = os.environ.copy()
51
+ self.env["TERM"] = "xterm-256color"
52
+ self.env["PYTHONUNBUFFERED"] = "1"
53
+ self.created_at = time.time()
54
+ self.last_used = time.time()
55
+
56
+ def record(self, cmd: str, output: str, exit_code: int, duration: float):
57
+ self.history.append({
58
+ "cmd": cmd,
59
+ "output": output[:2000],
60
+ "exit_code": exit_code,
61
+ "duration": round(duration, 3),
62
+ "timestamp": time.time(),
63
+ })
64
+ self.last_used = time.time()
65
+ if len(self.history) > 100:
66
+ self.history = self.history[-100:]
67
+
68
+
69
+ class TerminalEngine:
70
+ """
71
+ Real Terminal Execution Engine
72
+ - Async subprocess with streaming output
73
+ - Session management (persistent cwd + env)
74
+ - Timeout + kill control
75
+ - Self-repair loop (run β†’ error β†’ analyze β†’ patch β†’ retry)
76
+ """
77
+
78
+ def __init__(self, ws_manager=None):
79
+ self.ws = ws_manager
80
+ self._sessions: Dict[str, ProcessSession] = {}
81
+ self._running_procs: Dict[str, asyncio.subprocess.Process] = {}
82
+
83
+ # ─── Session Management ───────────────────────────────────────────────────
84
+
85
+ def get_or_create_session(self, session_id: str, cwd: str = WORKSPACE) -> ProcessSession:
86
+ if session_id not in self._sessions:
87
+ self._sessions[session_id] = ProcessSession(session_id, cwd)
88
+ os.makedirs(cwd, exist_ok=True)
89
+ return self._sessions[session_id]
90
+
91
+ def get_session_history(self, session_id: str) -> List[Dict]:
92
+ sess = self._sessions.get(session_id)
93
+ return sess.history if sess else []
94
+
95
+ def cleanup_old_sessions(self, max_age: int = 3600):
96
+ now = time.time()
97
+ to_remove = [sid for sid, s in self._sessions.items() if now - s.last_used > max_age]
98
+ for sid in to_remove:
99
+ del self._sessions[sid]
100
+
101
+ # ─── Execute Command ──────────────────────────────────────────────────────
102
+
103
+ async def execute(
104
+ self,
105
+ command: str,
106
+ session_id: str = "",
107
+ task_id: str = "",
108
+ cwd: str = "",
109
+ timeout: int = 120,
110
+ stream_callback: Optional[Callable] = None,
111
+ env_override: Optional[Dict] = None,
112
+ ) -> Dict:
113
+ """
114
+ Execute a shell command with real streaming output.
115
+ Returns: {success, output, exit_code, duration, command}
116
+ """
117
+ if _is_blocked(command):
118
+ result = {
119
+ "success": False,
120
+ "output": f"❌ BLOCKED: Dangerous command detected: {command[:60]}",
121
+ "exit_code": -1,
122
+ "command": command,
123
+ "duration": 0.0,
124
+ }
125
+ await self._emit(task_id, session_id, "terminal_blocked", result)
126
+ return result
127
+
128
+ sess = self.get_or_create_session(session_id or "default", cwd or WORKSPACE)
129
+ work_dir = cwd or sess.cwd
130
+ os.makedirs(work_dir, exist_ok=True)
131
+
132
+ env = sess.env.copy()
133
+ if env_override:
134
+ env.update(env_override)
135
+
136
+ await self._emit(task_id, session_id, "terminal_exec", {
137
+ "command": command[:300],
138
+ "cwd": work_dir,
139
+ "session_id": session_id,
140
+ })
141
+
142
+ start = time.time()
143
+ output_lines = []
144
+
145
+ try:
146
+ proc = await asyncio.create_subprocess_shell(
147
+ command,
148
+ stdout=asyncio.subprocess.PIPE,
149
+ stderr=asyncio.subprocess.STDOUT,
150
+ cwd=work_dir,
151
+ env=env,
152
+ )
153
+
154
+ if session_id:
155
+ self._running_procs[session_id] = proc
156
+
157
+ # Stream output line by line
158
+ async def read_output():
159
+ while True:
160
+ try:
161
+ line = await asyncio.wait_for(proc.stdout.readline(), timeout=5.0)
162
+ if not line:
163
+ break
164
+ decoded = line.decode("utf-8", errors="replace").rstrip()
165
+ output_lines.append(decoded)
166
+
167
+ # Stream via WebSocket
168
+ await self._emit(task_id, session_id, "terminal_output", {
169
+ "line": decoded,
170
+ "command": command[:80],
171
+ })
172
+
173
+ if stream_callback:
174
+ await stream_callback(decoded)
175
+
176
+ if len(output_lines) > 500:
177
+ output_lines.append("... [output truncated at 500 lines]")
178
+ proc.kill()
179
+ break
180
+ except asyncio.TimeoutError:
181
+ if proc.returncode is not None:
182
+ break
183
+ continue
184
+
185
+ await asyncio.wait_for(
186
+ asyncio.gather(read_output(), proc.wait()),
187
+ timeout=timeout,
188
+ )
189
+
190
+ except asyncio.TimeoutError:
191
+ try:
192
+ proc.kill()
193
+ except Exception:
194
+ pass
195
+ output_lines.append(f"\n⚠️ Command timed out after {timeout}s")
196
+ exit_code = -1
197
+ except Exception as e:
198
+ output_lines.append(f"\n❌ Execution error: {str(e)}")
199
+ exit_code = -1
200
+ else:
201
+ exit_code = proc.returncode or 0
202
+
203
+ finally:
204
+ if session_id in self._running_procs:
205
+ del self._running_procs[session_id]
206
+
207
+ duration = time.time() - start
208
+ full_output = "\n".join(output_lines)
209
+
210
+ if session_id:
211
+ sess.record(command, full_output, exit_code, duration)
212
+
213
+ result = {
214
+ "success": exit_code == 0,
215
+ "output": full_output,
216
+ "exit_code": exit_code,
217
+ "command": command,
218
+ "duration": round(duration, 3),
219
+ "cwd": work_dir,
220
+ }
221
+
222
+ await self._emit(task_id, session_id, "terminal_result", {
223
+ "command": command[:100],
224
+ "exit_code": exit_code,
225
+ "success": exit_code == 0,
226
+ "duration_ms": round(duration * 1000),
227
+ "output_lines": len(output_lines),
228
+ })
229
+
230
+ return result
231
+
232
+ # ─── Kill Running Process ─────────────────────────────────────────────────
233
+
234
+ async def kill(self, session_id: str) -> Dict:
235
+ proc = self._running_procs.get(session_id)
236
+ if proc:
237
+ try:
238
+ proc.kill()
239
+ del self._running_procs[session_id]
240
+ return {"success": True, "action": "killed", "session_id": session_id}
241
+ except Exception as e:
242
+ return {"success": False, "error": str(e)}
243
+ return {"success": False, "error": "No running process for session"}
244
+
245
+ # ─── Execute Multiple Commands in Sequence ────────────────────────────────
246
+
247
+ async def execute_chain(
248
+ self,
249
+ commands: List[str],
250
+ session_id: str = "",
251
+ task_id: str = "",
252
+ cwd: str = "",
253
+ stop_on_error: bool = True,
254
+ ) -> Dict:
255
+ """Run a chain of commands, passing cwd between them."""
256
+ results = []
257
+ current_cwd = cwd or WORKSPACE
258
+
259
+ for i, cmd in enumerate(commands):
260
+ # Handle 'cd' specially to persist directory
261
+ if cmd.strip().startswith("cd "):
262
+ new_dir = cmd.strip()[3:].strip().strip('"').strip("'")
263
+ if not os.path.isabs(new_dir):
264
+ new_dir = os.path.join(current_cwd, new_dir)
265
+ if os.path.isdir(new_dir):
266
+ current_cwd = new_dir
267
+ results.append({"command": cmd, "success": True, "output": f"Changed to {current_cwd}", "exit_code": 0})
268
+ else:
269
+ results.append({"command": cmd, "success": False, "output": f"Directory not found: {new_dir}", "exit_code": 1})
270
+ if stop_on_error:
271
+ break
272
+ continue
273
+
274
+ result = await self.execute(
275
+ cmd,
276
+ session_id=session_id,
277
+ task_id=task_id,
278
+ cwd=current_cwd,
279
+ )
280
+ results.append(result)
281
+
282
+ if not result["success"] and stop_on_error:
283
+ break
284
+
285
+ success = all(r.get("success", False) for r in results)
286
+ combined_output = "\n".join(f"$ {r['command']}\n{r['output']}" for r in results)
287
+
288
+ return {
289
+ "success": success,
290
+ "commands": len(commands),
291
+ "results": results,
292
+ "combined_output": combined_output[:5000],
293
+ "final_cwd": current_cwd,
294
+ }
295
+
296
+ # ─── Self-Repair Loop ─────────────────────────────────────────────────────
297
+
298
+ async def self_repair(
299
+ self,
300
+ command: str,
301
+ error_output: str,
302
+ ai_router=None,
303
+ session_id: str = "",
304
+ task_id: str = "",
305
+ max_attempts: int = 3,
306
+ ) -> Dict:
307
+ """
308
+ Real self-repair loop:
309
+ Run β†’ Error β†’ Analyze β†’ AI suggests fix β†’ Retry
310
+ """
311
+ await self._emit(task_id, session_id, "self_repair_start", {
312
+ "command": command[:100],
313
+ "error": error_output[:200],
314
+ })
315
+
316
+ for attempt in range(1, max_attempts + 1):
317
+ await self._emit(task_id, session_id, "self_repair_attempt", {
318
+ "attempt": attempt,
319
+ "max": max_attempts,
320
+ })
321
+
322
+ if ai_router:
323
+ fix_prompt = [
324
+ {"role": "system", "content": "You are an expert DevOps engineer. Analyze the error and provide the EXACT fixed command or commands. Return ONLY the command(s), one per line. No explanations."},
325
+ {"role": "user", "content": f"Original command: {command}\n\nError output:\n{error_output[:1000]}\n\nProvide the corrected command(s):"},
326
+ ]
327
+ try:
328
+ fixed_cmd = await ai_router.complete(fix_prompt, temperature=0.1, max_tokens=300)
329
+ fixed_cmd = fixed_cmd.strip().strip("`").strip()
330
+ # Remove markdown code blocks
331
+ if fixed_cmd.startswith("```"):
332
+ lines = fixed_cmd.split("\n")
333
+ fixed_cmd = "\n".join(lines[1:-1]) if len(lines) > 2 else fixed_cmd
334
+ except Exception:
335
+ fixed_cmd = command
336
+
337
+ await self._emit(task_id, session_id, "self_repair_fix", {
338
+ "attempt": attempt,
339
+ "fixed_command": fixed_cmd[:200],
340
+ })
341
+
342
+ result = await self.execute(fixed_cmd, session_id=session_id, task_id=task_id)
343
+ if result["success"]:
344
+ await self._emit(task_id, session_id, "self_repair_success", {
345
+ "attempt": attempt,
346
+ "fixed_command": fixed_cmd[:100],
347
+ })
348
+ return {
349
+ "success": True,
350
+ "fixed_command": fixed_cmd,
351
+ "result": result,
352
+ "attempts": attempt,
353
+ }
354
+ error_output = result["output"]
355
+ else:
356
+ break
357
+
358
+ await self._emit(task_id, session_id, "self_repair_failed", {
359
+ "max_attempts": max_attempts,
360
+ "last_error": error_output[:200],
361
+ })
362
+ return {
363
+ "success": False,
364
+ "error": "Self-repair exhausted all attempts",
365
+ "last_error": error_output,
366
+ "attempts": max_attempts,
367
+ }
368
+
369
+ # ─── Emit Helper ─────────────────────────────────────────────────────────
370
+
371
+ async def _emit(self, task_id: str, session_id: str, event: str, data: Dict):
372
+ if not self.ws:
373
+ return
374
+ try:
375
+ if task_id:
376
+ await self.ws.emit(task_id, event, data, session_id=session_id)
377
+ if session_id:
378
+ await self.ws.emit_chat(session_id, event, data)
379
+ except Exception:
380
+ pass