Alibrown commited on
Commit
a1567a0
Β·
verified Β·
1 Parent(s): bf172ab

Update app/shellmaster.py

Browse files
Files changed (1) hide show
  1. app/shellmaster.py +316 -0
app/shellmaster.py CHANGED
@@ -0,0 +1,316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # app/shellmaster.py
3
+ # ShellMaster 2.0 β€” Modular Shell Tool
4
+ # Universal MCP Hub (Sandboxed) - based on PyFundaments Architecture
5
+ # Copyright 2026 - Volkan KΓΌcΓΌkbudak
6
+ # Apache License V. 2 + ESOL 1.1
7
+ # Repo: https://github.com/VolkanSah/Multi-LLM-API-Gateway
8
+ # =============================================================================
9
+ # ARCHITECTURE NOTE:
10
+ # Modular tool living in app/* only.
11
+ # NO low-level access, NO os.environ, NO fundaments access.
12
+ # Config comes from app/.pyfun [TOOL.shellmaster] via app/config.py.
13
+ # LLM (SmolLM2) generates commands β€” shellmaster validates + confirms + executes.
14
+ #
15
+ # FLOW:
16
+ # tools.py calls shellmaster.run(user_input)
17
+ # ↓
18
+ # LLM generates JSON {command, backup, recovery, risk}
19
+ # ↓
20
+ # validate against JSONL registry (local file or HF Dataset)
21
+ # ↓
22
+ # return confirmation request to user (NEVER auto-execute!)
23
+ # ↓
24
+ # user confirms β†’ send to agent β†’ return output
25
+ # ↓
26
+ # store recovery plan in db_sync
27
+ #
28
+ # CONFIRMATION FLOW:
29
+ # Every command requires explicit user confirmation.
30
+ # High-risk commands require backup + recovery plan from LLM.
31
+ # Recovery plan is stored in db_sync hub_state for later use.
32
+ # =============================================================================
33
+
34
+ import json
35
+ import logging
36
+ import httpx
37
+ from pathlib import Path
38
+ from typing import Optional
39
+
40
+ from . import config
41
+ from . import db_sync
42
+
43
+ logger = logging.getLogger("shellmaster")
44
+
45
+ # =============================================================================
46
+ # HARDCODED BLOCKS β€” cannot be overridden by registry or LLM
47
+ # Both Unix and Windows covered
48
+ # =============================================================================
49
+ _HARDCODED_BLOCKED = {
50
+ # ── Unix destructive ──────────────────────────────────────────────────────
51
+ "rm -rf", "rm -r /", "rm -f /",
52
+ "mkfs", "dd if=", "> /dev/sd", "> /dev/hd",
53
+ "shred /dev/", "wipefs",
54
+ # ── Windows destructive ───────────────────────────────────────────────────
55
+ "format c:", "format d:", "format /fs",
56
+ "rmdir /s", "rd /s", "del /f /s /q c:\\",
57
+ # ── Unix system destruction ───────────────────────────────────────────────
58
+ ":(){ :|:& };:",
59
+ "chmod -r 777 /", "chmod -r 000 /", "chmod 000 /",
60
+ "chown -r root /", "chown -r 0:0 /",
61
+ # ── Windows system destruction ────────────────────────────────────────────
62
+ "icacls c:\\ /grant", "icacls c:\\ /deny",
63
+ "takeown /f c:\\",
64
+ "reg delete hklm", "reg delete hkcu",
65
+ # ── Unix privilege escalation ─────────────────────────────────────────────
66
+ "sudo su", "sudo -i", "sudo -s",
67
+ "su root", "su -", "passwd root", "visudo",
68
+ # ── Windows privilege escalation ──────────────────────────────────────────
69
+ "runas /user:administrator",
70
+ "net user administrator",
71
+ "net localgroup administrators",
72
+ "psexec -s",
73
+ # ── Critical system files β€” Unix ─────────────────────────────────────────
74
+ "/etc/passwd", "/etc/shadow", "/etc/sudoers",
75
+ "/etc/crontab", "/boot/", "/proc/sysrq",
76
+ # ── Critical system files β€” Windows ──────────────────────────────────────
77
+ "c:\\windows\\system32\\", "c:\\windows\\system\\",
78
+ "hklm\\system", "hklm\\security",
79
+ # ── Shutdown/reboot β€” Unix ────────────────────────────────────────────────
80
+ "reboot", "halt", "poweroff",
81
+ "init 0", "init 6",
82
+ "systemctl poweroff", "systemctl reboot", "systemctl halt",
83
+ # ── Shutdown/reboot β€” Windows ─────────────────────────────────────────────
84
+ "shutdown /s", "shutdown /r", "shutdown /h",
85
+ # ── Network destruction ───────────────────────────────────────────────────
86
+ "iptables -f", "iptables --flush", "ufw disable",
87
+ "netsh firewall set opmode disable",
88
+ "netsh advfirewall set allprofiles state off",
89
+ }
90
+
91
+
92
+ def _is_hardblocked(cmd_str: str) -> bool:
93
+ """Check if command contains any hardcoded blocked pattern."""
94
+ cmd_lower = cmd_str.lower().strip()
95
+ return any(blocked in cmd_lower for blocked in _HARDCODED_BLOCKED)
96
+
97
+
98
+ # =============================================================================
99
+ # Module State β€” populated by initialize()
100
+ # =============================================================================
101
+ _cfg = {}
102
+ _agent_url = ""
103
+ _token = ""
104
+ _cmd_file = Path("shellmaster_commands.jsonl")
105
+ _timeout = 30
106
+ _initialized = False
107
+
108
+
109
+ # =============================================================================
110
+ # Initialization β€” called by app/app.py
111
+ # =============================================================================
112
+
113
+ async def initialize() -> None:
114
+ """
115
+ Initialize ShellMaster from app/.pyfun [TOOL.shellmaster].
116
+ Called by app/app.py β€” never reads os.environ directly.
117
+ """
118
+ global _cfg, _agent_url, _token, _cmd_file, _timeout, _initialized
119
+
120
+ tools_cfg = config.get_active_tools()
121
+ _cfg = tools_cfg.get("shellmaster", {})
122
+
123
+ if not _cfg or _cfg.get("active", "false").lower() != "true":
124
+ logger.info("ShellMaster not active in .pyfun β€” skipped")
125
+ return
126
+
127
+ _agent_url = _cfg.get("shellmaster_agent_url", "http://localhost:5004")
128
+ _token = _cfg.get("shellmaster_token", "")
129
+ _timeout = int(_cfg.get("timeout_sec", "30"))
130
+
131
+ # Command registry β€” local file first
132
+ cmd_file_path = _cfg.get("shellmaster_commands_file", "shellmaster_commands.jsonl")
133
+ _cmd_file = Path(cmd_file_path)
134
+
135
+ if not _token:
136
+ logger.warning("ShellMaster: shellmaster_token not set in .pyfun β€” disabled")
137
+ return
138
+
139
+ if not _cmd_file.exists():
140
+ logger.warning(f"ShellMaster: commands file not found: {_cmd_file} β€” disabled")
141
+ return
142
+
143
+ allowed = sum(
144
+ 1 for line in _cmd_file.open()
145
+ if line.strip() and json.loads(line).get("allowed", False)
146
+ )
147
+ logger.info(f"ShellMaster initialized | agent: {_agent_url} | allowed commands: {allowed}")
148
+ _initialized = True
149
+
150
+
151
+ # =============================================================================
152
+ # Command Registry
153
+ # =============================================================================
154
+
155
+ def _load_registry() -> dict:
156
+ """
157
+ Load command registry from local JSONL.
158
+ Hot-reloaded on each call β€” no restart needed for registry changes.
159
+ TODO: merge with HF Dataset registry if shellmaster_commands_dataset is set.
160
+ """
161
+ registry = {}
162
+ try:
163
+ with open(_cmd_file) as f:
164
+ for line in f:
165
+ line = line.strip()
166
+ if not line:
167
+ continue
168
+ cmd = json.loads(line)
169
+ registry[cmd["id"]] = cmd
170
+ except Exception as e:
171
+ logger.error(f"Registry load failed: {type(e).__name__}: {e}")
172
+ return registry
173
+
174
+
175
+ def _validate_command(cmd_str: str) -> tuple[bool, str]:
176
+ """
177
+ Validate a command string against registry + hardblock list.
178
+
179
+ Returns:
180
+ (True, command_id) if valid and allowed
181
+ (False, reason) if blocked
182
+ """
183
+ # Hardblock first β€” no exceptions
184
+ if _is_hardblocked(cmd_str):
185
+ return False, "blocked by system policy"
186
+
187
+ # Match against registry
188
+ registry = _load_registry()
189
+ cmd_lower = cmd_str.lower().strip()
190
+
191
+ for cmd_id, cmd in registry.items():
192
+ # Check both unix and win templates
193
+ for os_key in ("unix", "win"):
194
+ template_base = cmd.get(os_key, "").split()[0].lower()
195
+ if template_base and cmd_lower.startswith(template_base):
196
+ if not cmd.get("allowed", False):
197
+ return False, f"command '{cmd_id}' is blocked in registry"
198
+ return True, cmd_id
199
+
200
+ return False, f"command not found in registry: {cmd_str[:50]}"
201
+
202
+
203
+ def list_commands(category: str = None) -> list:
204
+ """List allowed commands, optionally filtered by category."""
205
+ cmds = [c for c in _load_registry().values() if c.get("allowed", False)]
206
+ if category:
207
+ cmds = [c for c in cmds if c.get("category") == category]
208
+ return cmds
209
+
210
+
211
+ # =============================================================================
212
+ # Confirmation Flow
213
+ # =============================================================================
214
+
215
+ def build_confirmation(llm_result: dict) -> str:
216
+ """
217
+ Build confirmation message from LLM-generated command plan.
218
+ Returns formatted string for user to confirm/deny.
219
+ """
220
+ command = llm_result.get("command", "")
221
+ backup = llm_result.get("backup", "")
222
+ recovery = llm_result.get("recovery", "")
223
+ risk = llm_result.get("risk", "unknown").upper()
224
+
225
+ lines = [
226
+ f"⚠️ ShellMaster β€” Confirmation Required",
227
+ f"",
228
+ f"Command: {command}",
229
+ f"Risk: {risk}",
230
+ ]
231
+
232
+ if backup:
233
+ lines.append(f"Backup: {backup}")
234
+ if recovery:
235
+ lines.append(f"Recovery: {recovery}")
236
+
237
+ lines += [
238
+ f"",
239
+ f"Type 'confirm shellmaster' to execute or 'cancel' to abort.",
240
+ ]
241
+ return "\n".join(lines)
242
+
243
+
244
+ async def store_recovery_plan(command: str, backup: str, recovery: str) -> None:
245
+ """Store recovery plan in db_sync hub_state for later use."""
246
+ try:
247
+ await db_sync.write("shellmaster.last_command", {
248
+ "command": command,
249
+ "backup": backup,
250
+ "recovery": recovery,
251
+ })
252
+ logger.info("ShellMaster: recovery plan stored in db_sync")
253
+ except Exception as e:
254
+ logger.warning(f"ShellMaster: recovery plan store failed: {type(e).__name__}")
255
+
256
+
257
+ async def get_recovery_plan() -> dict:
258
+ """Retrieve last recovery plan from db_sync."""
259
+ return await db_sync.read("shellmaster.last_command", default={})
260
+
261
+
262
+ # =============================================================================
263
+ # Agent Execution β€” only called after explicit user confirmation
264
+ # =============================================================================
265
+
266
+ async def execute_confirmed(command: str) -> str:
267
+ """
268
+ Execute a command on the local agent.
269
+ ONLY called after explicit user confirmation β€” never auto-execute!
270
+
271
+ Args:
272
+ command: Raw shell command string (already validated).
273
+
274
+ Returns:
275
+ Command output or error message.
276
+ """
277
+ if not _initialized:
278
+ return "ShellMaster not initialized."
279
+
280
+ # Final hardblock check before sending to agent
281
+ if _is_hardblocked(command):
282
+ logger.warning(f"HARDBLOCK on confirmed command: {command[:60]}")
283
+ return "Blocked by system policy β€” cannot execute."
284
+
285
+ try:
286
+ async with httpx.AsyncClient() as client:
287
+ r = await client.post(
288
+ f"{_agent_url}/command",
289
+ json={"command": command},
290
+ headers={
291
+ "Authorization": f"Bearer {_token}",
292
+ "Content-Type": "application/json",
293
+ },
294
+ timeout=_timeout,
295
+ )
296
+ r.raise_for_status()
297
+ data = r.json()
298
+ output = data.get("result", data.get("output", ""))
299
+ logger.info(f"ShellMaster executed: {command[:60]}")
300
+ return output
301
+
302
+ except httpx.ConnectError:
303
+ return f"ShellMaster Agent not reachable at {_agent_url} β€” is it running?"
304
+ except httpx.HTTPStatusError as e:
305
+ return f"Agent error: HTTP {e.response.status_code}"
306
+ except Exception as e:
307
+ logger.warning(f"ShellMaster execute failed: {type(e).__name__}: {e}")
308
+ return f"Error: {type(e).__name__}"
309
+
310
+
311
+ def is_ready() -> bool:
312
+ return _initialized
313
+
314
+
315
+ def get_config() -> dict:
316
+ return _cfg