"""Terminal tool — run shell commands from an allowlist."""
import asyncio
import shlex
from navi.config import settings
from .base import Tool, ToolResult
_TIMEOUT = 30
class TerminalTool(Tool):
name = "terminal"
description = (
"Execute a shell command. Only commands whose first token is in the allowed list "
"can be run. Returns stdout and stderr."
)
parameters = {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Shell command to execute",
},
"working_dir": {
"type": "string",
"description": "Working directory (optional, defaults to /tmp)",
},
},
"required": ["command"],
}
async def execute(self, params: dict) -> ToolResult:
command = params["command"].strip()
working_dir = params.get("working_dir", "/tmp")
# Safety: check first token against allowlist
try:
tokens = shlex.split(command)
except ValueError as e:
return ToolResult(success=False, output=f"Invalid command syntax: {e}", error=str(e))
if not tokens:
return ToolResult(success=False, output="Empty command.", error="empty_command")
allowed = settings.terminal_allowed_commands_list
if tokens[0] not in allowed:
return ToolResult(
success=False,
output=f"Command '{tokens[0]}' is not in the allowed list: {allowed}",
error="not_allowed",
)
try:
proc = await asyncio.create_subprocess_exec(
*tokens,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=working_dir,
)
try:
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=_TIMEOUT)
except asyncio.TimeoutError:
proc.kill()
return ToolResult(
success=False,
output=f"Command timed out after {_TIMEOUT}s",
error="timeout",
)
output_parts = []
if stdout:
output_parts.append(stdout.decode(errors="replace"))
if stderr:
output_parts.append(f"[stderr]\n{stderr.decode(errors='replace')}")
success = proc.returncode == 0
output = "\n".join(output_parts) or "(no output)"
return ToolResult(
success=success,
output=output,
metadata={"returncode": proc.returncode},
error=None if success else f"Exit code {proc.returncode}",
)
except FileNotFoundError:
return ToolResult(
success=False,
output=f"Command not found: {tokens[0]}",
error="not_found",
)
except Exception as e:
return ToolResult(success=False, output=f"Execution error: {e}", error=str(e))