Skip to content

Commit e71cb54

Browse files
committed
review comments
Signed-off-by: Akihiko Kuroda <akihikokuroda2020@gmail.com>
1 parent b556d8f commit e71cb54

4 files changed

Lines changed: 470 additions & 111 deletions

File tree

docs/examples/tools/shell_example.py

Lines changed: 57 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,60 @@
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
44
Demonstrates multiple ways to use Mellea's bash execution capabilities:
55
1. Direct execution for non-LLM tasks
66
2. Wrapping as a MelleaTool for agent use
77
3. LLM-based tool calling with forced tool use
88
4. 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).
1213
Both enforce a conservative safety denylist: no sudo, no rm -rf, no destructive
1314
git operations, no writes to /etc, /sys, /proc, etc. Write operations can also
1415
be 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

1721
from mellea import MelleaSession, start_session
1822
from mellea.backends import ModelOption
1923
from mellea.backends.tools import MelleaTool
2024
from 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

2428
def 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:

mellea/stdlib/tools/__init__.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
"""Implementations of tools."""
22

33
from .interpreter import code_interpreter, local_code_interpreter
4-
from .shell import bash_executor, local_bash_executor
4+
from .shell import (
5+
BashEnvironment,
6+
LLMSandboxBashEnvironment,
7+
StaticBashEnvironment,
8+
bash_executor,
9+
unsafe_local_bash_executor,
10+
)
511

612
__all__ = [
13+
"BashEnvironment",
14+
"LLMSandboxBashEnvironment",
15+
"StaticBashEnvironment",
716
"bash_executor",
817
"code_interpreter",
9-
"local_bash_executor",
1018
"local_code_interpreter",
19+
"unsafe_local_bash_executor",
1120
]

0 commit comments

Comments
 (0)