Source code for chia.base.tools.BashTool
from __future__ import annotations
import os
import signal
import subprocess
from typing import Optional, Dict
from chia.base.tools.ChiaTool import ChiaTool, ToolInfo
[docs]
class BashTool(ChiaTool):
"""MCP tool that executes bash commands in its deployed container.
When constructed with ``task_options``, the tools are setup on
machines using a setup function which is called remotely with those
task_options.
"""
def __init__(
self,
name: str,
work_dir: str = "/",
timeout_seconds: int = 120,
task_options: Optional[Dict] = None,
):
super().__init__(name, task_options=task_options)
self.work_dir = work_dir
self.timeout_seconds = timeout_seconds
self.mcp.add_tool(self.run_command, name=f"{name}_run_command")
super().__post_init__()
[docs]
def run_command(self, command: str) -> str:
"""Run a bash command and return combined stdout/stderr."""
# start_new_session=True puts the shell and all its descendants in a
# fresh process group, so on timeout we can kill the whole tree with
# killpg(). Without this, subprocess.run only kills the immediate
# ``sh`` and grandchildren keep the stdout/stderr pipes open, which
# stalls communicate() in cleanup and silently drops the response.
try:
proc = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
cwd=self.work_dir,
start_new_session=True,
)
except Exception as e:
return f"Error: {e}"
try:
stdout, stderr = proc.communicate(timeout=self.timeout_seconds)
except subprocess.TimeoutExpired:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
try:
proc.communicate(timeout=10)
except subprocess.TimeoutExpired:
pass
return f"Error: command timed out after {self.timeout_seconds}s"
except Exception as e:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
return f"Error: {e}"
output = ""
if stdout:
output += stdout
if stderr:
output += "\nSTDERR:\n" + stderr
if proc.returncode != 0:
output += f"\n[exit code: {proc.returncode}]"
return output or "(no output)"