11# pytest: e2e, qualitative
2- """Example usage patterns for bash_executor and local_bash_executor tools.
2+ """Example usage patterns for bash_executor and unsafe_local_bash_executor tools.
33
44Demonstrates multiple ways to use Mellea's bash execution capabilities:
551. Direct execution for non-LLM tasks
662. Wrapping as a MelleaTool for agent use
773. LLM-based tool calling with forced tool use
884. Integration with error handling
99
10- Safety note: bash_executor uses Docker isolation via llm-sandbox (recommended
11- for production). local_bash_executor runs commands directly (for dev/testing only).
10+ ⚠️ Security note: bash_executor uses Docker isolation via llm-sandbox (recommended
11+ for production and LLM-generated code). unsafe_local_bash_executor runs commands
12+ directly with no isolation (development/testing only with trusted code).
1213Both enforce a conservative safety denylist: no sudo, no rm -rf, no destructive
1314git operations, no writes to /etc, /sys, /proc, etc. Write operations can also
1415be constrained with ``working_dir`` and explicit ``allowed_paths``.
16+
17+ Note: Commands must use argv-friendly syntax (no pipes, redirects, or shell builtins).
18+ Use individual commands and compose them in Python instead.
1519"""
1620
1721from mellea import MelleaSession , start_session
1822from mellea .backends import ModelOption
1923from mellea .backends .tools import MelleaTool
2024from mellea .stdlib .requirements import uses_tool
21- from mellea .stdlib .tools .shell import bash_executor , local_bash_executor
25+ from mellea .stdlib .tools .shell import bash_executor , unsafe_local_bash_executor
2226
2327
2428def example_1_direct_execution () -> None :
2529 """Example 1: Execute bash commands directly."""
2630 print ("=== Example 1: Direct Execution ===" )
2731
2832 # Execute a simple command
29- result = local_bash_executor ("echo 'Hello from Bash'" )
33+ result = unsafe_local_bash_executor ("echo 'Hello from Bash'" )
3034 print ("Command: echo 'Hello from Bash'" )
3135 print (f"Success: { result .success } " )
3236 print (f"Output: { result .stdout } " )
3337 print ()
3438
35- # Execute a command with pipes and redirects
36- result = local_bash_executor ("ls -la | wc -l " )
37- print ("Command: ls -la | wc -l " )
39+ # Execute a command to list files (no pipes/ redirects)
40+ result = unsafe_local_bash_executor ("ls -la" )
41+ print ("Command: ls -la" )
3842 print (f"Success: { result .success } " )
39- print (f"Output: { result .stdout } " )
43+ if result .stdout :
44+ # Show first few lines
45+ lines = result .stdout .split ("\n " )[:3 ]
46+ print ("Output (first 3 lines):\n " + "\n " .join (lines ))
47+ print ()
48+
49+ # Demonstrate that pipes are blocked (for security)
50+ result = unsafe_local_bash_executor ("ls -la | wc -l" )
51+ print ("Command: ls -la | wc -l (pipe operator blocked)" )
52+ print (f"Rejected: { result .skipped } " )
53+ print (f"Reason: { result .skip_message } " )
4054 print ()
4155
4256 # Attempt a dangerous command (will be rejected)
43- result = local_bash_executor ("sudo echo unsafe" )
57+ result = unsafe_local_bash_executor ("sudo echo unsafe" )
4458 print ("Command: sudo echo unsafe" )
4559 print (f"Skipped: { result .skipped } " )
4660 print (f"Reason: { result .skip_message } " )
@@ -52,7 +66,7 @@ def example_2_wrapped_as_tool() -> None:
5266 print ("=== Example 2: Wrapped as MelleaTool ===" )
5367
5468 # Create tool from bash executor
55- bash_tool = MelleaTool .from_callable (local_bash_executor )
69+ bash_tool = MelleaTool .from_callable (unsafe_local_bash_executor )
5670 print (f"Tool name: { bash_tool .name } " )
5771 print (f"Tool schema keys: { bash_tool .as_json_tool .keys ()} " )
5872 print ()
@@ -75,9 +89,9 @@ def example_3_llm_with_forced_tool_use(m: MelleaSession) -> None:
7589
7690 result = m .instruct (
7791 description = "Use bash to count how many Python files are in the current directory." ,
78- requirements = [uses_tool (local_bash_executor )],
92+ requirements = [uses_tool (unsafe_local_bash_executor )],
7993 model_options = {
80- ModelOption .TOOLS : [MelleaTool .from_callable (local_bash_executor )]
94+ ModelOption .TOOLS : [MelleaTool .from_callable (unsafe_local_bash_executor )]
8195 },
8296 tool_calls = True ,
8397 )
@@ -86,11 +100,11 @@ def example_3_llm_with_forced_tool_use(m: MelleaSession) -> None:
86100 raise ValueError ("Expected tool_calls but got None" )
87101
88102 # Extract the bash command the LLM generated
89- command = result .tool_calls ["local_bash_executor " ].args ["command" ]
103+ command = result .tool_calls ["unsafe_local_bash_executor " ].args ["command" ]
90104 print (f"LLM generated bash command:\n { command } \n " )
91105
92106 # Execute the command
93- exec_result = result .tool_calls ["local_bash_executor " ].call_func ()
107+ exec_result = result .tool_calls ["unsafe_local_bash_executor " ].call_func ()
94108
95109 print ("Execution result:" )
96110 print (f" Success: { exec_result .success } " )
@@ -104,31 +118,42 @@ def example_3_with_working_dir() -> None:
104118 """Example 3: Restrict write validation and execution cwd to a directory."""
105119 print ("=== Example 3: Working Directory Restriction ===" )
106120
121+ import os
107122 import tempfile
108123
109124 with tempfile .TemporaryDirectory () as tmpdir :
110125 print (f"Working directory: { tmpdir } " )
111126
112- # Create a file in the working directory
113- result = local_bash_executor (
114- f"echo 'project content' > { tmpdir } /myfile.txt" , working_dir = tmpdir
115- )
116- print (f"Command: echo 'project content' > { tmpdir } /myfile.txt" )
127+ # Create a file using touch within the working directory (redirects blocked)
128+ result = unsafe_local_bash_executor ("touch myfile.txt" , working_dir = tmpdir )
129+ print (f"Command: touch myfile.txt (relative path, executed in { tmpdir } )" )
117130 print (f"Success: { result .success } " )
118131 print ()
119132
133+ # Verify the file was created
134+ file_path = os .path .join (tmpdir , "myfile.txt" )
135+ if os .path .exists (file_path ):
136+ print (f"✓ File created at: { file_path } " )
137+ print ()
138+
120139 # Read it back
121- result = local_bash_executor ( f "cat { tmpdir } / myfile.txt" , working_dir = tmpdir )
122- print (f "Command: cat { tmpdir } / myfile.txt" )
140+ result = unsafe_local_bash_executor ( "cat myfile.txt" , working_dir = tmpdir )
141+ print ("Command: cat myfile.txt" )
123142 print (f"Output: { result .stdout } " )
124143 print ()
125144
126- # Attempt to write outside the restricted working directory (will be rejected )
127- result = local_bash_executor (
128- "echo 'bad' > /tmp/outside .txt" , working_dir = tmpdir
145+ # Writing to /tmp is always allowed (temp directory exception )
146+ result = unsafe_local_bash_executor (
147+ "touch /tmp/tmpfile .txt" , working_dir = tmpdir
129148 )
130- print (f"Command: echo 'bad' > /tmp/outside.txt (with working_dir={ tmpdir } )" )
131- print (f"Skipped: { result .skipped } " )
149+ print (f"Command: touch /tmp/tmpfile.txt (with working_dir={ tmpdir } )" )
150+ print (f"Success: { result .success } (note: /tmp is always allowed)" )
151+ print ()
152+
153+ # Attempt to write to system paths (will be rejected)
154+ result = unsafe_local_bash_executor ("touch /etc/config.txt" , working_dir = tmpdir )
155+ print (f"Command: touch /etc/config.txt (with working_dir={ tmpdir } )" )
156+ print (f"Rejected: { result .skipped } " )
132157 print (f"Reason: { result .skip_message } " )
133158 print ()
134159
@@ -146,7 +171,7 @@ def example_4_safety_features() -> None:
146171 ]
147172
148173 for cmd , description in dangerous_commands :
149- result = local_bash_executor (cmd )
174+ result = unsafe_local_bash_executor (cmd )
150175 print (f"{ description } : { cmd } " )
151176 print (f" Rejected: { result .skipped } " )
152177 print (f" Reason: { result .skip_message } " )
@@ -158,14 +183,14 @@ def example_5_error_handling() -> None:
158183 print ("=== Example 5: Error Handling ===" )
159184
160185 # Command that fails (returns non-zero exit code)
161- result = local_bash_executor ( "exit 1 " )
162- print ("Command: exit 1 " )
186+ result = unsafe_local_bash_executor ( "false " )
187+ print ("Command: false (POSIX command that returns exit code 1) " )
163188 print (f"Success: { result .success } " )
164- print (f"Stderr : { result .stderr } " )
189+ print (f"Return code indicates failure : { not result .success } " )
165190 print ()
166191
167192 # Command that doesn't exist
168- result = local_bash_executor ("nonexistent_command_xyz" )
193+ result = unsafe_local_bash_executor ("nonexistent_command_xyz" )
169194 print ("Command: nonexistent_command_xyz" )
170195 print (f"Success: { result .success } " )
171196 if not result .success and result .stderr is not None :
0 commit comments