Entelechy / tools /bash_tool.py
qa296
refactor: standardize type hints and improve null safety across codebase
6d49dc7
"""Bash tool - execute shell commands with safety checks."""
import asyncio
from pathlib import Path
from loguru import logger
DANGEROUS_PATTERNS = [
"rm -rf /",
"rm -rf /*",
"shutdown",
"reboot",
"> /dev/sd",
"mkfs.",
"dd if=",
":(){:|:&};:",
]
async def run_bash(command: str, workdir: Path | None = None, timeout: int = 120) -> str:
"""Execute a shell command.
Args:
command: The shell command to execute.
workdir: Working directory (defaults to cwd).
timeout: Timeout in seconds.
Returns:
Command output (stdout + stderr) or error message.
"""
# Safety check
cmd_lower = command.lower().strip()
for pattern in DANGEROUS_PATTERNS:
if pattern in cmd_lower:
return f"Error: Dangerous command blocked (matched: {pattern})"
logger.debug(f"Executing: {command}")
proc: asyncio.subprocess.Process | None = None
try:
proc = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(workdir) if workdir else None,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
output = (stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace")).strip()
if not output:
return f"(no output, exit code: {proc.returncode})"
# Truncate very long output
if len(output) > 50000:
output = output[:50000] + "\n\n... (output truncated at 50000 chars)"
return output
except asyncio.TimeoutError:
if proc is not None:
try:
proc.kill()
except Exception:
pass
return f"Error: Command timed out after {timeout}s"
except Exception as e:
return f"Error: {e}"