Nymbo commited on
Commit
bb89bb6
·
verified ·
1 Parent(s): 78d32d0

Create Shell_Exec.py

Browse files
Files changed (1) hide show
  1. Modules/Shell_Exec.py +141 -0
Modules/Shell_Exec.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import shlex
6
+ import subprocess
7
+ from typing import Annotated
8
+
9
+ import gradio as gr
10
+
11
+ from app import _log_call_end, _log_call_start, _truncate_for_log
12
+ from ._docstrings import autodoc
13
+ from .File_System import _resolve_path, ROOT_DIR, _display_path
14
+ import shutil
15
+
16
+
17
+ def _detect_shell(prefer_powershell: bool = True) -> tuple[list[str], str]:
18
+ """
19
+ Pick an appropriate shell for the host OS.
20
+ - Windows: use PowerShell by default, fall back to cmd.exe.
21
+ - POSIX: use /bin/bash if available, else /bin/sh.
22
+ Returns (shell_cmd_prefix, shell_name) where shell_cmd_prefix is the command list to launch the shell.
23
+ """
24
+ system = platform.system().lower()
25
+ if system == "windows":
26
+ if prefer_powershell:
27
+ pwsh = shutil.which("pwsh")
28
+ candidates = [pwsh, shutil.which("powershell"), shutil.which("powershell.exe")]
29
+ for cand in candidates:
30
+ if cand:
31
+ return [cand, "-NoLogo", "-NoProfile", "-Command"], "powershell"
32
+ # Fallback to cmd
33
+ comspec = os.environ.get("ComSpec", r"C:\\Windows\\System32\\cmd.exe")
34
+ return [comspec, "/C"], "cmd"
35
+ # POSIX
36
+ bash = shutil.which("bash")
37
+ if bash:
38
+ return [bash, "-lc"], "bash"
39
+ sh = os.environ.get("SHELL", "/bin/sh")
40
+ return [sh, "-lc"], "sh"
41
+
42
+
43
+ # Detect shell at import time for docs/UI purposes
44
+ _DETECTED_SHELL_PREFIX, _DETECTED_SHELL_NAME = _detect_shell()
45
+
46
+
47
+ # Clarify path semantics and expose detected shell in summary
48
+ TOOL_SUMMARY = (
49
+ "Execute a shell command within a safe working directory under the tool root ('/'). "
50
+ "Paths must be relative to '/' (which maps to Nymbo-Tools/Filesystem by default); "
51
+ "absolute paths are disabled unless UNSAFE_ALLOW_ABS_PATHS=1. "
52
+ f"Detected shell: {_DETECTED_SHELL_NAME}."
53
+ )
54
+
55
+
56
+ def _run_command(command: str, cwd: str, timeout: int) -> tuple[str, str, int]:
57
+ shell_prefix, shell_name = _detect_shell()
58
+ full_cmd = shell_prefix + [command]
59
+ try:
60
+ proc = subprocess.run(
61
+ full_cmd,
62
+ cwd=cwd,
63
+ stdout=subprocess.PIPE,
64
+ stderr=subprocess.PIPE,
65
+ text=True,
66
+ encoding="utf-8",
67
+ errors="replace",
68
+ timeout=timeout if timeout and timeout > 0 else None,
69
+ )
70
+ return proc.stdout, proc.stderr, proc.returncode
71
+ except subprocess.TimeoutExpired as exc:
72
+ return exc.stdout or "", (exc.stderr or "") + "\n[timeout]", 124
73
+ except Exception as exc:
74
+ return "", f"Execution failed: {exc}", 1
75
+
76
+
77
+ @autodoc(summary=TOOL_SUMMARY)
78
+ def Shell_Exec(
79
+ command: Annotated[str, "Shell command to execute. Accepts multi-part pipelines as a single string."],
80
+ workdir: Annotated[str, "Working directory (relative to root unless UNSAFE_ALLOW_ABS_PATHS=1)."] = ".",
81
+ timeout: Annotated[int, "Timeout in seconds (0 = no timeout, be careful on public hosting)."] = 60,
82
+ ) -> str:
83
+ _log_call_start("Shell_Exec", command=command, workdir=workdir, timeout=timeout)
84
+ if not command or not command.strip():
85
+ result = "No command provided."
86
+ _log_call_end("Shell_Exec", _truncate_for_log(result))
87
+ return result
88
+
89
+ abs_cwd, err = _resolve_path(workdir)
90
+ if err:
91
+ _log_call_end("Shell_Exec", _truncate_for_log(err))
92
+ return err
93
+ if not os.path.exists(abs_cwd):
94
+ result = f"Working directory not found: {abs_cwd}"
95
+ _log_call_end("Shell_Exec", _truncate_for_log(result))
96
+ return result
97
+
98
+ # Capture shell used for transparency
99
+ _, shell_name = _detect_shell()
100
+ stdout, stderr, code = _run_command(command, cwd=abs_cwd, timeout=timeout)
101
+ display_cwd = _display_path(abs_cwd)
102
+ header = (
103
+ f"Command: {command}\n"
104
+ f"CWD: {display_cwd}\n"
105
+ f"Root: /\n"
106
+ f"Shell: {shell_name}\n"
107
+ f"Exit code: {code}\n"
108
+ f"--- STDOUT ---\n"
109
+ )
110
+ output = header + (stdout or "<empty>") + "\n--- STDERR ---\n" + (stderr or "<empty>")
111
+ _log_call_end("Shell_Exec", _truncate_for_log(f"exit={code} stdout={len(stdout)} stderr={len(stderr)}"))
112
+ return output
113
+
114
+
115
+ def build_interface() -> gr.Interface:
116
+ return gr.Interface(
117
+ fn=Shell_Exec,
118
+ inputs=[
119
+ gr.Textbox(label="Command", placeholder="echo hello || dir", lines=2),
120
+ gr.Textbox(label="Workdir", value=".", max_lines=1),
121
+ gr.Slider(minimum=0, maximum=600, step=5, value=60, label="Timeout (seconds)"),
122
+ ],
123
+ outputs=gr.Textbox(label="Output", lines=20),
124
+ title="Shell Exec",
125
+ description=(
126
+ "Run a shell command under the same safe root as File System. "
127
+ "Paths are relative to '/' (maps to Nymbo-Tools/Filesystem by default); absolute paths are disabled "
128
+ "unless UNSAFE_ALLOW_ABS_PATHS=1. Tip: set workdir to '.' to use the root. "
129
+ f"Detected shell: {_DETECTED_SHELL_NAME}. "
130
+ "<br><br><b>Examples</b>:"
131
+ "<br>- Command: <code>dir</code> (Windows) or <code>ls -la</code> (Linux)"
132
+ "<br>- Command: <code>echo hello | tee out.txt</code>"
133
+ "<br>- Workdir: <code>notes</code>, Command: <code>ls</code> to list /notes"
134
+ ),
135
+ api_description=TOOL_SUMMARY,
136
+ flagging_mode="never",
137
+ submit_btn="Run",
138
+ )
139
+
140
+
141
+ __all__ = ["Shell_Exec", "build_interface"]