Skip to content

Commit b5d3f6f

Browse files
committed
shell executor
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
1 parent 85b8e95 commit b5d3f6f

4 files changed

Lines changed: 1224 additions & 1 deletion

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# pytest: unit, qualitative
2+
"""Example usage patterns for bash_executor and local_bash_executor tools.
3+
4+
Demonstrates three ways to use Mellea's bash execution capabilities:
5+
1. Direct execution for non-LLM tasks
6+
2. Wrapping as a MelleaTool for agent use
7+
3. Integration with requirements framework for rejection sampling
8+
9+
Safety note: bash_executor uses Docker isolation via llm-sandbox (recommended
10+
for production). local_bash_executor runs commands directly (for dev/testing only).
11+
Both enforce a conservative safety denylist: no sudo, no rm -rf, no destructive
12+
git operations, no writes to /etc, /sys, /proc, etc.
13+
"""
14+
15+
from mellea.backends.tools import MelleaTool
16+
from mellea.stdlib.tools.shell import bash_executor, local_bash_executor
17+
18+
19+
def example_1_direct_execution() -> None:
20+
"""Example 1: Execute bash commands directly."""
21+
print("=== Example 1: Direct Execution ===")
22+
23+
# Execute a simple command
24+
result = local_bash_executor("echo 'Hello from Bash'")
25+
print("Command: echo 'Hello from Bash'")
26+
print(f"Success: {result.success}")
27+
print(f"Output: {result.stdout}")
28+
print()
29+
30+
# Execute a command with pipes and redirects
31+
result = local_bash_executor("ls -la | wc -l")
32+
print("Command: ls -la | wc -l")
33+
print(f"Success: {result.success}")
34+
print(f"Output: {result.stdout}")
35+
print()
36+
37+
# Attempt a dangerous command (will be rejected)
38+
result = local_bash_executor("sudo echo unsafe")
39+
print("Command: sudo echo unsafe")
40+
print(f"Skipped: {result.skipped}")
41+
print(f"Reason: {result.skip_message}")
42+
print()
43+
44+
45+
def example_2_wrapped_as_tool() -> None:
46+
"""Example 2: Wrap bash executor as a MelleaTool for LLM use."""
47+
print("=== Example 2: Wrapped as MelleaTool ===")
48+
49+
# Create tool from bash executor
50+
bash_tool = MelleaTool.from_callable(local_bash_executor)
51+
print(f"Tool name: {bash_tool.name}")
52+
print(f"Tool schema keys: {bash_tool.as_json_tool.keys()}")
53+
print()
54+
55+
# Invoke the tool directly (normally LLM would call this)
56+
result = bash_tool.run("pwd")
57+
print("Tool invocation result:")
58+
print(f" Success: {result.success}")
59+
print(f" Output: {result.stdout}")
60+
print()
61+
62+
63+
def example_3_with_working_dir() -> None:
64+
"""Example 3: Restrict command execution to a specific directory."""
65+
print("=== Example 3: Working Directory Restriction ===")
66+
67+
import tempfile
68+
69+
with tempfile.TemporaryDirectory() as tmpdir:
70+
print(f"Working directory: {tmpdir}")
71+
72+
# Create a file in the working directory
73+
result = local_bash_executor(
74+
f"echo 'project content' > {tmpdir}/myfile.txt", working_dir=tmpdir
75+
)
76+
print(f"Command: echo 'project content' > {tmpdir}/myfile.txt")
77+
print(f"Success: {result.success}")
78+
print()
79+
80+
# Read it back
81+
result = local_bash_executor(f"cat {tmpdir}/myfile.txt", working_dir=tmpdir)
82+
print(f"Command: cat {tmpdir}/myfile.txt")
83+
print(f"Output: {result.stdout}")
84+
print()
85+
86+
# Attempt to write outside working directory (will be rejected)
87+
result = local_bash_executor(
88+
"echo 'bad' > /tmp/outside.txt", working_dir=tmpdir
89+
)
90+
print(f"Command: echo 'bad' > /tmp/outside.txt (with working_dir={tmpdir})")
91+
print(f"Skipped: {result.skipped}")
92+
print(f"Reason: {result.skip_message}")
93+
print()
94+
95+
96+
def example_4_safety_features() -> None:
97+
"""Example 4: Demonstrate safety features."""
98+
print("=== Example 4: Safety Features ===")
99+
100+
dangerous_commands = [
101+
("rm -rf /home", "Recursive force delete"),
102+
("git push --force", "Force git push"),
103+
("sudo whoami", "Privilege escalation"),
104+
("bash -i", "Interactive shell"),
105+
("touch /etc/config", "Write to system path"),
106+
]
107+
108+
for cmd, description in dangerous_commands:
109+
result = local_bash_executor(cmd)
110+
print(f"{description}: {cmd}")
111+
print(f" Rejected: {result.skipped}")
112+
print(f" Reason: {result.skip_message}")
113+
print()
114+
115+
116+
def example_5_error_handling() -> None:
117+
"""Example 5: Handle execution errors gracefully."""
118+
print("=== Example 5: Error Handling ===")
119+
120+
# Command that fails (returns non-zero exit code)
121+
result = local_bash_executor("exit 1")
122+
print("Command: exit 1")
123+
print(f"Success: {result.success}")
124+
print(f"Stderr: {result.stderr}")
125+
print()
126+
127+
# Command that doesn't exist
128+
result = local_bash_executor("nonexistent_command_xyz")
129+
print("Command: nonexistent_command_xyz")
130+
print(f"Success: {result.success}")
131+
if not result.success and result.stderr is not None:
132+
print(f"Error output: {result.stderr[:100]}")
133+
print()
134+
135+
136+
if __name__ == "__main__":
137+
example_1_direct_execution()
138+
example_2_wrapped_as_tool()
139+
example_3_with_working_dir()
140+
example_4_safety_features()
141+
example_5_error_handling()

mellea/stdlib/tools/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
"""Implementations of tools."""
22

33
from .interpreter import code_interpreter, local_code_interpreter
4+
from .shell import bash_executor, local_bash_executor
45

5-
__all__ = ["code_interpreter", "local_code_interpreter"]
6+
__all__ = [
7+
"bash_executor",
8+
"code_interpreter",
9+
"local_bash_executor",
10+
"local_code_interpreter",
11+
]

0 commit comments

Comments
 (0)