from __future__ import annotations from typing import Any from core.subprocess import run_command from core.tools.base import BaseTool, ToolContext from core.tools.registry import registry from core.safety import safety class SubprocessTool(BaseTool): name = "subprocess" description = "Run a subprocess command safely" def execute( self, payload: dict[str, Any], ctx: ToolContext ): cmd = payload.get("cmd") cwd = payload.get("cwd") timeout = payload.get("timeout", 60) # ------------------------- # Validate command # ------------------------- if not isinstance(cmd, list) or not all(isinstance(c, str) for c in cmd): raise ValueError("cmd must be list[str]") # Optional safety: block empty commands if not cmd: raise ValueError("cmd cannot be empty") # ------------------------- # Validate cwd # ------------------------- if cwd is not None: if not isinstance(cwd, str): raise ValueError("cwd must be string") cwd_path = safety.validate_path(cwd) cwd = str(cwd_path) # ------------------------- # Validate timeout # ------------------------- if not isinstance(timeout, int) or timeout <= 0: raise ValueError("timeout must be positive int") # ------------------------- # Dry-run support (future-proofing) # ------------------------- if getattr(ctx, "dry_run", False): return { "dry_run": True, "cmd": cmd, "cwd": cwd, "timeout": timeout, "message": "Would execute subprocess" } return run_command( cmd=cmd, cwd=cwd, timeout=timeout ) registry.register(SubprocessTool())