lollmsbot 0.0.1__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.
- lollmsbot/__init__.py +1 -0
- lollmsbot/agent.py +1682 -0
- lollmsbot/channels/__init__.py +22 -0
- lollmsbot/channels/discord.py +408 -0
- lollmsbot/channels/http_api.py +449 -0
- lollmsbot/channels/telegram.py +272 -0
- lollmsbot/cli.py +217 -0
- lollmsbot/config.py +90 -0
- lollmsbot/gateway.py +606 -0
- lollmsbot/guardian.py +692 -0
- lollmsbot/heartbeat.py +826 -0
- lollmsbot/lollms_client.py +37 -0
- lollmsbot/skills.py +1483 -0
- lollmsbot/soul.py +482 -0
- lollmsbot/storage/__init__.py +245 -0
- lollmsbot/storage/sqlite_store.py +332 -0
- lollmsbot/tools/__init__.py +151 -0
- lollmsbot/tools/calendar.py +717 -0
- lollmsbot/tools/filesystem.py +663 -0
- lollmsbot/tools/http.py +498 -0
- lollmsbot/tools/shell.py +519 -0
- lollmsbot/ui/__init__.py +11 -0
- lollmsbot/ui/__main__.py +121 -0
- lollmsbot/ui/app.py +1122 -0
- lollmsbot/ui/routes.py +39 -0
- lollmsbot/wizard.py +1493 -0
- lollmsbot-0.0.1.dist-info/METADATA +25 -0
- lollmsbot-0.0.1.dist-info/RECORD +32 -0
- lollmsbot-0.0.1.dist-info/WHEEL +5 -0
- lollmsbot-0.0.1.dist-info/entry_points.txt +2 -0
- lollmsbot-0.0.1.dist-info/licenses/LICENSE +201 -0
- lollmsbot-0.0.1.dist-info/top_level.txt +1 -0
lollmsbot/tools/shell.py
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell tool for LollmsBot.
|
|
3
|
+
|
|
4
|
+
This module provides the ShellTool class for safe shell command execution
|
|
5
|
+
with strict security controls including allowlist/denylist filtering,
|
|
6
|
+
timeout protection, and comprehensive output capture.
|
|
7
|
+
|
|
8
|
+
SECURITY WARNING:
|
|
9
|
+
This tool executes system shell commands which can be dangerous. It includes
|
|
10
|
+
multiple security layers to mitigate risks, but should still be used with
|
|
11
|
+
caution. Always review the allowlist/denylist configuration carefully.
|
|
12
|
+
|
|
13
|
+
Security features:
|
|
14
|
+
- Explicit allowlist for permitted commands (opt-in security)
|
|
15
|
+
- Denylist for known dangerous commands and patterns
|
|
16
|
+
- Timeout protection to prevent hanging processes
|
|
17
|
+
- Working directory restriction
|
|
18
|
+
- No shell=True to prevent injection attacks
|
|
19
|
+
- Command argument validation
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import re
|
|
24
|
+
import shlex
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, List, Optional, Pattern, Set, Union
|
|
28
|
+
|
|
29
|
+
from lollmsbot.agent import Tool, ToolResult
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class SecurityPolicy:
|
|
34
|
+
"""Security configuration for shell command execution.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
allowed_commands: Set of explicitly allowed command names (empty = all allowed).
|
|
38
|
+
denied_commands: Set of denied command names/patterns.
|
|
39
|
+
denied_patterns: List of regex patterns that will reject commands.
|
|
40
|
+
max_timeout: Maximum execution timeout in seconds.
|
|
41
|
+
allowed_working_dirs: Set of directories where commands can execute.
|
|
42
|
+
max_output_size: Maximum output size in bytes to prevent memory issues.
|
|
43
|
+
"""
|
|
44
|
+
allowed_commands: Set[str] = field(default_factory=lambda: {
|
|
45
|
+
# Safe network diagnostic tools
|
|
46
|
+
"ping", "ping6",
|
|
47
|
+
"tracert", "traceroute",
|
|
48
|
+
"nslookup", "dig",
|
|
49
|
+
"curl", "wget",
|
|
50
|
+
"netstat", "ss",
|
|
51
|
+
"ip", "ifconfig", "ipconfig",
|
|
52
|
+
"hostname",
|
|
53
|
+
# File operations (read-only)
|
|
54
|
+
"cat", "head", "tail", "less", "more",
|
|
55
|
+
"ls", "dir", "find",
|
|
56
|
+
"grep", "egrep", "fgrep",
|
|
57
|
+
"wc", "sort", "uniq",
|
|
58
|
+
# System info (read-only)
|
|
59
|
+
"ps", "top", "htop", "tasklist",
|
|
60
|
+
"df", "du", "free", "vmstat",
|
|
61
|
+
"uname", "whoami", "id",
|
|
62
|
+
"date", "uptime",
|
|
63
|
+
"echo", "printf",
|
|
64
|
+
# Python (restricted but useful)
|
|
65
|
+
"python", "python3", "py",
|
|
66
|
+
})
|
|
67
|
+
denied_commands: Set[str] = field(default_factory=lambda: {
|
|
68
|
+
"rm", "del", "format", "mkfs", "dd", "shred", "wipe",
|
|
69
|
+
"chmod", "chown", "sudo", "su", "passwd", "shadow",
|
|
70
|
+
"nc", "netcat", "ncat", "telnet",
|
|
71
|
+
"bash", "sh", "zsh", "fish", "cmd", "powershell", "pwsh",
|
|
72
|
+
"perl", "ruby", "node", "php",
|
|
73
|
+
# "python", "python3", # Moved to allowed
|
|
74
|
+
"ssh", "scp", "sftp", "ftp", "rsync",
|
|
75
|
+
"systemctl", "service", "init", "reboot", "shutdown", "halt",
|
|
76
|
+
"kill", "killall", "pkill", "xkill",
|
|
77
|
+
"iptables", "ufw", "firewalld",
|
|
78
|
+
"useradd", "userdel", "groupadd", "groupdel",
|
|
79
|
+
"mount", "umount", "losetup", "modprobe",
|
|
80
|
+
})
|
|
81
|
+
denied_patterns: List[Pattern[str]] = field(default_factory=lambda: [
|
|
82
|
+
re.compile(r"[;&|]\s*(?:rm|del|format|mkfs|dd|chmod|chown|sudo)\b"), # Command chaining
|
|
83
|
+
re.compile(r"`.*?`"), # Backtick substitution
|
|
84
|
+
re.compile(r"\$\(.*?\)"), # Command substitution
|
|
85
|
+
re.compile(r"[><|]\s*/(?:etc|bin|sbin|usr|var|root|proc|sys|dev)"), # Redirection to system paths
|
|
86
|
+
re.compile(r"-[a-zA-Z]*[rf]"), # Force/recursive flags often used destructively
|
|
87
|
+
re.compile(r"\.\./\.\."), # Path traversal attempts
|
|
88
|
+
re.compile(r"(?:https?|ftp|file|data):[/\\]{2}"), # URL-like patterns in commands
|
|
89
|
+
# Block output redirection to files (prevents file writes via shell)
|
|
90
|
+
re.compile(r"[>|]\s*\S+\.(exe|bat|cmd|sh|py|js)$"), # Writing to executable/script files
|
|
91
|
+
re.compile(r"[>|]\s*/"), # Any absolute path redirection
|
|
92
|
+
])
|
|
93
|
+
max_timeout: float = 30.0
|
|
94
|
+
allowed_working_dirs: Set[Path] = field(default_factory=lambda: {Path.cwd()})
|
|
95
|
+
max_output_size: int = 1024 * 1024 # 1 MB
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ShellTool(Tool):
|
|
99
|
+
"""Tool for safe shell command execution with strict security controls.
|
|
100
|
+
|
|
101
|
+
This tool provides controlled shell command execution with multiple
|
|
102
|
+
security layers including allowlist/denylist filtering, timeout
|
|
103
|
+
protection, and output capture. Commands are executed without shell
|
|
104
|
+
interpolation to prevent injection attacks.
|
|
105
|
+
|
|
106
|
+
SECURITY WARNING:
|
|
107
|
+
- Only commands in the allowlist are permitted (if configured)
|
|
108
|
+
- Commands in the denylist are always rejected
|
|
109
|
+
- Commands matching denied patterns are rejected
|
|
110
|
+
- Timeout prevents runaway processes
|
|
111
|
+
- Working directory is restricted
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
name: Unique identifier for the tool.
|
|
115
|
+
description: Human-readable description of what the tool does.
|
|
116
|
+
parameters: JSON Schema describing expected parameters.
|
|
117
|
+
security: SecurityPolicy instance controlling command validation.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
name: str = "shell"
|
|
121
|
+
description: str = (
|
|
122
|
+
"Execute safe shell commands with strict security controls. "
|
|
123
|
+
"Commands are validated against allowlist/denylist, executed "
|
|
124
|
+
"with timeout protection, and return stdout, stderr, and return code. "
|
|
125
|
+
"Use with caution - only pre-approved commands are allowed. "
|
|
126
|
+
"Available commands: ping, curl, wget, nslookup, dig, traceroute, "
|
|
127
|
+
"cat, ls, ps, top, df, date, echo, and other read-only diagnostic tools."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
parameters: dict[str, Any] = {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"operation": {
|
|
134
|
+
"type": "string",
|
|
135
|
+
"enum": ["execute", "check_allowed"],
|
|
136
|
+
"description": "Operation to perform",
|
|
137
|
+
},
|
|
138
|
+
"command": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"description": "Shell command to execute (for execute operation)",
|
|
141
|
+
},
|
|
142
|
+
"timeout": {
|
|
143
|
+
"type": "number",
|
|
144
|
+
"description": "Timeout in seconds (optional, max 300)",
|
|
145
|
+
"minimum": 1,
|
|
146
|
+
"maximum": 300,
|
|
147
|
+
},
|
|
148
|
+
"working_dir": {
|
|
149
|
+
"type": "string",
|
|
150
|
+
"description": "Working directory for command execution (optional)",
|
|
151
|
+
},
|
|
152
|
+
"env_vars": {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"description": "Environment variables to set (optional)",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
"required": ["operation"],
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
security: Optional[SecurityPolicy] = None,
|
|
163
|
+
default_timeout: float = 30.0,
|
|
164
|
+
) -> None:
|
|
165
|
+
"""Initialize the ShellTool.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
security: SecurityPolicy for command validation. Uses defaults if None.
|
|
169
|
+
default_timeout: Default timeout for command execution in seconds.
|
|
170
|
+
"""
|
|
171
|
+
self.security: SecurityPolicy = security or SecurityPolicy()
|
|
172
|
+
self.default_timeout: float = min(default_timeout, 300.0) # Cap at 5 minutes
|
|
173
|
+
|
|
174
|
+
# Compile any additional patterns if provided as strings
|
|
175
|
+
self._ensure_patterns_compiled()
|
|
176
|
+
|
|
177
|
+
def _ensure_patterns_compiled(self) -> None:
|
|
178
|
+
"""Ensure all denial patterns are compiled regex objects."""
|
|
179
|
+
compiled_patterns: List[Pattern[str]] = []
|
|
180
|
+
for pattern in self.security.denied_patterns:
|
|
181
|
+
if isinstance(pattern, str):
|
|
182
|
+
compiled_patterns.append(re.compile(pattern))
|
|
183
|
+
else:
|
|
184
|
+
compiled_patterns.append(pattern)
|
|
185
|
+
self.security.denied_patterns = compiled_patterns
|
|
186
|
+
|
|
187
|
+
def check_command_allowed(self, command: str) -> tuple[bool, Optional[str]]:
|
|
188
|
+
"""Check if a command is allowed under current security policy.
|
|
189
|
+
|
|
190
|
+
Performs multiple security checks:
|
|
191
|
+
1. Empty/whitespace check
|
|
192
|
+
2. Denied pattern matching
|
|
193
|
+
3. Explicit denylist check
|
|
194
|
+
4. Explicit allowlist check (if configured)
|
|
195
|
+
5. Basic injection attempt detection
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
command: The command string to validate.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Tuple of (is_allowed, reason_if_denied). reason is None if allowed.
|
|
202
|
+
"""
|
|
203
|
+
# Check for empty command
|
|
204
|
+
stripped = command.strip()
|
|
205
|
+
if not stripped:
|
|
206
|
+
return False, "Empty command not allowed"
|
|
207
|
+
|
|
208
|
+
# Check for denied patterns (injection attempts, dangerous sequences)
|
|
209
|
+
for pattern in self.security.denied_patterns:
|
|
210
|
+
if pattern.search(stripped):
|
|
211
|
+
return False, f"Command matches denied security pattern: {pattern.pattern}"
|
|
212
|
+
|
|
213
|
+
# Parse command to get base command name
|
|
214
|
+
try:
|
|
215
|
+
# Use shlex to properly parse without executing
|
|
216
|
+
parsed = shlex.split(stripped)
|
|
217
|
+
if not parsed:
|
|
218
|
+
return False, "Could not parse command"
|
|
219
|
+
|
|
220
|
+
base_command = parsed[0]
|
|
221
|
+
|
|
222
|
+
# Remove path prefix if present to get command name
|
|
223
|
+
base_name = Path(base_command).name
|
|
224
|
+
|
|
225
|
+
except ValueError as exc:
|
|
226
|
+
return False, f"Command parsing error: {str(exc)}"
|
|
227
|
+
|
|
228
|
+
# Check explicit denylist
|
|
229
|
+
if base_name in self.security.denied_commands:
|
|
230
|
+
return False, f"Command '{base_name}' is in denylist"
|
|
231
|
+
|
|
232
|
+
# Check if base command is in allowed list (if allowlist is configured)
|
|
233
|
+
if self.security.allowed_commands:
|
|
234
|
+
# Check both full path and base name
|
|
235
|
+
allowed = base_name in self.security.allowed_commands or base_command in self.security.allowed_commands
|
|
236
|
+
if not allowed:
|
|
237
|
+
allowed_list = ", ".join(sorted(self.security.allowed_commands))
|
|
238
|
+
return False, f"Command '{base_name}' not in allowlist. Allowed: {allowed_list}"
|
|
239
|
+
|
|
240
|
+
return True, None
|
|
241
|
+
|
|
242
|
+
def _validate_working_directory(self, working_dir: Optional[str]) -> tuple[Path, Optional[str]]:
|
|
243
|
+
"""Validate and resolve working directory.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
working_dir: Requested working directory or None for default.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Tuple of (resolved_path, error_message). error is None if valid.
|
|
250
|
+
"""
|
|
251
|
+
if working_dir is None:
|
|
252
|
+
# Use first allowed directory as default
|
|
253
|
+
return next(iter(self.security.allowed_working_dirs)), None
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
requested = Path(working_dir).resolve()
|
|
257
|
+
|
|
258
|
+
# Check if within allowed directories
|
|
259
|
+
for allowed in self.security.allowed_working_dirs:
|
|
260
|
+
try:
|
|
261
|
+
requested.relative_to(allowed)
|
|
262
|
+
return requested, None
|
|
263
|
+
except ValueError:
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
allowed_strs = [str(d) for d in self.security.allowed_working_dirs]
|
|
267
|
+
return Path.cwd(), f"Working directory '{working_dir}' outside allowed paths: {', '.join(allowed_strs)}"
|
|
268
|
+
|
|
269
|
+
except (OSError, ValueError) as exc:
|
|
270
|
+
return Path.cwd(), f"Invalid working directory '{working_dir}': {str(exc)}"
|
|
271
|
+
|
|
272
|
+
async def execute(
|
|
273
|
+
self,
|
|
274
|
+
command: str,
|
|
275
|
+
timeout: Optional[float] = None,
|
|
276
|
+
working_dir: Optional[str] = None,
|
|
277
|
+
env_vars: Optional[dict[str, str]] = None,
|
|
278
|
+
) -> ToolResult:
|
|
279
|
+
"""Execute a shell command with security checks and timeout protection.
|
|
280
|
+
|
|
281
|
+
SECURITY WARNING: This method executes system commands. All inputs
|
|
282
|
+
are validated against the security policy before execution.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
command: The command string to execute.
|
|
286
|
+
timeout: Maximum execution time in seconds. Uses default if None.
|
|
287
|
+
working_dir: Working directory for execution. Must be in allowed list.
|
|
288
|
+
env_vars: Additional environment variables for the process.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
ToolResult with stdout, stderr, return code, and execution metadata.
|
|
292
|
+
"""
|
|
293
|
+
# Validate command against security policy
|
|
294
|
+
allowed, reason = self.check_command_allowed(command)
|
|
295
|
+
if not allowed:
|
|
296
|
+
return ToolResult(
|
|
297
|
+
success=False,
|
|
298
|
+
output=None,
|
|
299
|
+
error=f"Security check failed: {reason}",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Validate working directory
|
|
303
|
+
work_dir, dir_error = self._validate_working_directory(working_dir)
|
|
304
|
+
if dir_error:
|
|
305
|
+
return ToolResult(
|
|
306
|
+
success=False,
|
|
307
|
+
output=None,
|
|
308
|
+
error=f"Directory validation failed: {dir_error}",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Validate timeout
|
|
312
|
+
exec_timeout = min(timeout or self.default_timeout, 300.0)
|
|
313
|
+
|
|
314
|
+
# Prepare environment
|
|
315
|
+
process_env: Optional[dict[str, str]] = None
|
|
316
|
+
if env_vars:
|
|
317
|
+
import os
|
|
318
|
+
process_env = {**os.environ, **env_vars}
|
|
319
|
+
|
|
320
|
+
# Parse command safely using shlex
|
|
321
|
+
try:
|
|
322
|
+
cmd_args = shlex.split(command)
|
|
323
|
+
except ValueError as exc:
|
|
324
|
+
return ToolResult(
|
|
325
|
+
success=False,
|
|
326
|
+
output=None,
|
|
327
|
+
error=f"Failed to parse command: {str(exc)}",
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Execute with timeout using asyncio
|
|
331
|
+
import time
|
|
332
|
+
start_time = time.time()
|
|
333
|
+
|
|
334
|
+
try:
|
|
335
|
+
# Create subprocess without shell=True for security
|
|
336
|
+
process = await asyncio.create_subprocess_exec(
|
|
337
|
+
*cmd_args,
|
|
338
|
+
stdout=asyncio.subprocess.PIPE,
|
|
339
|
+
stderr=asyncio.subprocess.PIPE,
|
|
340
|
+
cwd=work_dir,
|
|
341
|
+
env=process_env,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Wait for completion with timeout
|
|
345
|
+
try:
|
|
346
|
+
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
347
|
+
process.communicate(),
|
|
348
|
+
timeout=exec_timeout,
|
|
349
|
+
)
|
|
350
|
+
except asyncio.TimeoutError:
|
|
351
|
+
# Kill the process if it times out
|
|
352
|
+
try:
|
|
353
|
+
process.kill()
|
|
354
|
+
await process.wait()
|
|
355
|
+
except ProcessLookupError:
|
|
356
|
+
pass # Already exited
|
|
357
|
+
|
|
358
|
+
return ToolResult(
|
|
359
|
+
success=False,
|
|
360
|
+
output=None,
|
|
361
|
+
error=f"Command timed out after {exec_timeout} seconds",
|
|
362
|
+
execution_time=time.time() - start_time,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
execution_time = time.time() - start_time
|
|
366
|
+
|
|
367
|
+
# Decode output with size limit
|
|
368
|
+
def decode_limited(data: bytes, max_size: int) -> str:
|
|
369
|
+
if len(data) > max_size:
|
|
370
|
+
truncated = data[:max_size]
|
|
371
|
+
decoded = truncated.decode("utf-8", errors="replace")
|
|
372
|
+
return decoded + f"\n[TRUNCATED: {len(data) - max_size} bytes omitted]"
|
|
373
|
+
return data.decode("utf-8", errors="replace")
|
|
374
|
+
|
|
375
|
+
max_size = self.security.max_output_size
|
|
376
|
+
stdout = decode_limited(stdout_bytes, max_size // 2)
|
|
377
|
+
stderr = decode_limited(stderr_bytes, max_size // 2)
|
|
378
|
+
|
|
379
|
+
# Build result
|
|
380
|
+
result_data = {
|
|
381
|
+
"command": command,
|
|
382
|
+
"return_code": process.returncode,
|
|
383
|
+
"stdout": stdout,
|
|
384
|
+
"stderr": stderr,
|
|
385
|
+
"stdout_bytes": len(stdout_bytes),
|
|
386
|
+
"stderr_bytes": len(stderr_bytes),
|
|
387
|
+
"execution_time": execution_time,
|
|
388
|
+
"working_directory": str(work_dir),
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
# Consider non-zero return code as failure
|
|
392
|
+
success = process.returncode == 0
|
|
393
|
+
|
|
394
|
+
return ToolResult(
|
|
395
|
+
success=success,
|
|
396
|
+
output=result_data,
|
|
397
|
+
error=stderr if not success and stderr else None,
|
|
398
|
+
execution_time=execution_time,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
except FileNotFoundError as exc:
|
|
402
|
+
return ToolResult(
|
|
403
|
+
success=False,
|
|
404
|
+
output=None,
|
|
405
|
+
error=f"Command not found: {exc.filename}",
|
|
406
|
+
execution_time=time.time() - start_time,
|
|
407
|
+
)
|
|
408
|
+
except PermissionError as exc:
|
|
409
|
+
return ToolResult(
|
|
410
|
+
success=False,
|
|
411
|
+
output=None,
|
|
412
|
+
error=f"Permission denied executing command: {str(exc)}",
|
|
413
|
+
execution_time=time.time() - start_time,
|
|
414
|
+
)
|
|
415
|
+
except Exception as exc:
|
|
416
|
+
return ToolResult(
|
|
417
|
+
success=False,
|
|
418
|
+
output=None,
|
|
419
|
+
error=f"Execution error: {str(exc)}",
|
|
420
|
+
execution_time=time.time() - start_time,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
async def check_allowed(self, command: str) -> ToolResult:
|
|
424
|
+
"""Check if a command would be allowed without executing it.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
command: The command string to check.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
ToolResult with check results and security policy info.
|
|
431
|
+
"""
|
|
432
|
+
allowed, reason = self.check_command_allowed(command)
|
|
433
|
+
|
|
434
|
+
result = {
|
|
435
|
+
"command": command,
|
|
436
|
+
"allowed": allowed,
|
|
437
|
+
"reason": reason,
|
|
438
|
+
"security_policy": {
|
|
439
|
+
"allowlist_enabled": bool(self.security.allowed_commands),
|
|
440
|
+
"allowed_commands_count": len(self.security.allowed_commands),
|
|
441
|
+
"denied_commands_count": len(self.security.denied_commands),
|
|
442
|
+
"denied_patterns_count": len(self.security.denied_patterns),
|
|
443
|
+
"max_timeout": self.security.max_timeout,
|
|
444
|
+
"allowed_working_dirs": [str(d) for d in self.security.allowed_working_dirs],
|
|
445
|
+
},
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return ToolResult(
|
|
449
|
+
success=allowed,
|
|
450
|
+
output=result,
|
|
451
|
+
error=reason if not allowed else None,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
async def execute_tool(self, **params: Any) -> ToolResult:
|
|
455
|
+
"""Execute shell tool operation based on parameters.
|
|
456
|
+
|
|
457
|
+
Main entry point for Tool base class. Dispatches to appropriate
|
|
458
|
+
method based on the 'operation' parameter.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
**params: Parameters must include:
|
|
462
|
+
- operation: 'execute' or 'check_allowed'
|
|
463
|
+
- command: Required for both operations
|
|
464
|
+
- timeout: Optional for execute
|
|
465
|
+
- working_dir: Optional for execute
|
|
466
|
+
- env_vars: Optional for execute
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
ToolResult from the executed operation.
|
|
470
|
+
"""
|
|
471
|
+
operation = params.get("operation")
|
|
472
|
+
|
|
473
|
+
if not operation:
|
|
474
|
+
return ToolResult(
|
|
475
|
+
success=False,
|
|
476
|
+
output=None,
|
|
477
|
+
error="Missing required parameter: 'operation'",
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
command = params.get("command")
|
|
481
|
+
if not command:
|
|
482
|
+
return ToolResult(
|
|
483
|
+
success=False,
|
|
484
|
+
output=None,
|
|
485
|
+
error="Missing required parameter: 'command'",
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if operation == "execute":
|
|
489
|
+
return await self.execute(
|
|
490
|
+
command=command,
|
|
491
|
+
timeout=params.get("timeout"),
|
|
492
|
+
working_dir=params.get("working_dir"),
|
|
493
|
+
env_vars=params.get("env_vars"),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
elif operation == "check_allowed":
|
|
497
|
+
return await self.check_allowed(command)
|
|
498
|
+
|
|
499
|
+
else:
|
|
500
|
+
return ToolResult(
|
|
501
|
+
success=False,
|
|
502
|
+
output=None,
|
|
503
|
+
error=f"Unknown operation: '{operation}'. Valid operations: execute, check_allowed",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Alias for Tool base class compatibility
|
|
507
|
+
async def execute(self, **params: Any) -> ToolResult:
|
|
508
|
+
"""Compatibility method for Tool base class.
|
|
509
|
+
|
|
510
|
+
Delegates to execute_tool for actual implementation.
|
|
511
|
+
"""
|
|
512
|
+
return await self.execute_tool(**params)
|
|
513
|
+
|
|
514
|
+
def __repr__(self) -> str:
|
|
515
|
+
return (
|
|
516
|
+
f"ShellTool(allowed={len(self.security.allowed_commands)}, "
|
|
517
|
+
f"denied={len(self.security.denied_commands)}, "
|
|
518
|
+
f"timeout={self.default_timeout})"
|
|
519
|
+
)
|
lollmsbot/ui/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Web UI package for LollmsBot.
|
|
3
|
+
|
|
4
|
+
Provides a beautiful local web interface for interacting with the AI agent.
|
|
5
|
+
Includes real-time chat, conversation history, and tool execution visualization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from lollmsbot.ui.app import WebUI
|
|
9
|
+
from lollmsbot.ui.routes import ui_router
|
|
10
|
+
|
|
11
|
+
__all__ = ["WebUI", "ui_router"]
|
lollmsbot/ui/__main__.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Run the LollmsBot Web UI as a standalone module.
|
|
4
|
+
"""
|
|
5
|
+
import uvicorn
|
|
6
|
+
from lollmsbot.ui.app import WebUI
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.layout import Layout
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich import box
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
def create_startup_panel(host: str, port: int) -> Panel:
|
|
16
|
+
"""Create a beautiful startup information panel."""
|
|
17
|
+
|
|
18
|
+
# Create feature table
|
|
19
|
+
feature_table = Table(
|
|
20
|
+
show_header=False,
|
|
21
|
+
box=None,
|
|
22
|
+
padding=(0, 2),
|
|
23
|
+
collapse_padding=True
|
|
24
|
+
)
|
|
25
|
+
feature_table.add_column("Icon", style="cyan", justify="center")
|
|
26
|
+
feature_table.add_column("Feature", style="white")
|
|
27
|
+
|
|
28
|
+
features = [
|
|
29
|
+
("⚡", "Real-time WebSocket chat"),
|
|
30
|
+
("🎨", "Dark modern interface"),
|
|
31
|
+
("🔧", "4 Built-in tools (Files, HTTP, Calendar, Shell)"),
|
|
32
|
+
("⚙️", "In-browser settings"),
|
|
33
|
+
("📱", "Mobile-responsive design"),
|
|
34
|
+
("🔄", "Auto-reconnect on connection loss"),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
for icon, feature in features:
|
|
38
|
+
feature_table.add_row(icon, feature)
|
|
39
|
+
|
|
40
|
+
# Create access URLs table
|
|
41
|
+
urls_table = Table(
|
|
42
|
+
show_header=True,
|
|
43
|
+
header_style="bold magenta",
|
|
44
|
+
box=box.SIMPLE,
|
|
45
|
+
border_style="blue",
|
|
46
|
+
padding=(0, 1)
|
|
47
|
+
)
|
|
48
|
+
urls_table.add_column("Access From", style="cyan")
|
|
49
|
+
urls_table.add_column("URL", style="green link")
|
|
50
|
+
|
|
51
|
+
local_url = f"http://localhost:{port}"
|
|
52
|
+
network_url = f"http://{host}:{port}"
|
|
53
|
+
ws_url = f"ws://{host}:{port}/ws/chat"
|
|
54
|
+
|
|
55
|
+
urls_table.add_row("This Computer", local_url)
|
|
56
|
+
if host not in ("127.0.0.1", "localhost"):
|
|
57
|
+
urls_table.add_row("Network Devices", network_url)
|
|
58
|
+
urls_table.add_row("WebSocket Endpoint", ws_url)
|
|
59
|
+
|
|
60
|
+
# Combine everything
|
|
61
|
+
layout = Layout()
|
|
62
|
+
layout.split_column(
|
|
63
|
+
Layout(feature_table, name="features"),
|
|
64
|
+
Layout(urls_table, name="urls")
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Add tips at the bottom
|
|
68
|
+
tips = """
|
|
69
|
+
[dim]💡 Pro Tips:
|
|
70
|
+
• Press [bold yellow]Ctrl+C[/bold yellow] to stop the server gracefully
|
|
71
|
+
• The UI auto-creates CSS/JS files on first run if missing
|
|
72
|
+
• Open multiple browser tabs to test multi-user scenarios
|
|
73
|
+
• Use the ⚙️ icon in top-right to customize LoLLMS connection[/dim]
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
panel_content = f"""
|
|
77
|
+
[bold blue]🤖 LollmsBot Web Interface[/bold blue]
|
|
78
|
+
|
|
79
|
+
{layout.tree}
|
|
80
|
+
{tips}
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
return Panel(
|
|
84
|
+
panel_content.strip(),
|
|
85
|
+
box=box.DOUBLE_EDGE,
|
|
86
|
+
border_style="bright_green",
|
|
87
|
+
title="[bold bright_green]🚀 Starting Server[/bold bright_green]",
|
|
88
|
+
subtitle=f"[dim]v0.1.0 | Python 3.10+ | FastAPI + WebSocket[/dim]"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
import argparse
|
|
93
|
+
|
|
94
|
+
parser = argparse.ArgumentParser(description="LollmsBot Web UI")
|
|
95
|
+
parser.add_argument("--host", default="127.0.0.1", help="Host to bind to")
|
|
96
|
+
parser.add_argument("--port", type=int, default=8080, help="Port to listen on")
|
|
97
|
+
parser.add_argument("--quiet", "-q", action="store_true", help="Minimal output")
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
|
|
100
|
+
if not args.quiet:
|
|
101
|
+
console.print()
|
|
102
|
+
console.print(create_startup_panel(args.host, args.port))
|
|
103
|
+
console.print()
|
|
104
|
+
|
|
105
|
+
# Create UI instance (this prints its own banner)
|
|
106
|
+
ui = WebUI(verbose=not args.quiet)
|
|
107
|
+
|
|
108
|
+
# Print server ready message
|
|
109
|
+
if not args.quiet:
|
|
110
|
+
ui.print_server_ready(args.host, args.port)
|
|
111
|
+
|
|
112
|
+
# Run server
|
|
113
|
+
try:
|
|
114
|
+
uvicorn.run(
|
|
115
|
+
ui.app,
|
|
116
|
+
host=args.host,
|
|
117
|
+
port=args.port,
|
|
118
|
+
log_level="warning" if args.quiet else "info"
|
|
119
|
+
)
|
|
120
|
+
except KeyboardInterrupt:
|
|
121
|
+
ui._print_shutdown_message()
|