@@ -297,6 +297,13 @@ def main():
297297 if tool_specs :
298298 tools = load_tools (tool_specs , search_paths )
299299 log ("Loaded %d tools: %s" % (len (tools ), list (tools .keys ())))
300+
301+ # Load shell_tools
302+ shell_tool_specs = meta .get ("shell_tools" , {})
303+ if shell_tool_specs :
304+ shell_tools = load_shell_tools (shell_tool_specs )
305+ tools .update (shell_tools )
306+ log ("Loaded %d shell tools: %s" % (len (shell_tools ), list (shell_tools .keys ())))
300307
301308 # Determine effective provider early for file reading
302309 base_url = get_conf ("base_url" ) or get_base_url ()
@@ -1384,6 +1391,90 @@ def build_tool_result_message(tool_call, result, error, provider):
13841391# === TOOL LOADING & SCHEMA ===
13851392
13861393
1394+ def load_shell_tools (shell_tool_specs ):
1395+ """Load shell script tools from shell_tools: frontmatter."""
1396+ tools = {}
1397+
1398+ for name , spec in shell_tool_specs .items ():
1399+ # Handle short form: name: "command"
1400+ if isinstance (spec , str ):
1401+ cmd = spec
1402+ safe = False
1403+ description = cmd
1404+ # Handle long form: name: {cmd: ..., safe: ..., description: ...}
1405+ elif isinstance (spec , dict ):
1406+ cmd = spec .get ("cmd" )
1407+ if not cmd :
1408+ print ("%sWarning: shell_tool '%s' missing 'cmd' field%s" %
1409+ (YELLOW , name , RESET ), file = sys .stderr )
1410+ continue
1411+ safe = spec .get ("safe" , False )
1412+ description = spec .get ("description" , cmd )
1413+ else :
1414+ print ("%sWarning: shell_tool '%s' has invalid spec%s" %
1415+ (YELLOW , name , RESET ), file = sys .stderr )
1416+ continue
1417+
1418+ # Create the tool function
1419+ func = create_shell_tool (name , cmd , description , safe )
1420+ schema = function_to_tool_schema (func )
1421+ if schema :
1422+ schema ['function' ]['name' ] = name
1423+ tools [name ] = {"schema" : schema , "func" : func }
1424+ log ("Loaded shell tool: %s" % name )
1425+
1426+ return tools
1427+
1428+
1429+ def create_shell_tool (name , cmd , description , safe ):
1430+ """Create a shell tool function with the given command."""
1431+ def shell_tool (args : str = "" , ** env_vars ):
1432+ """Execute shell command with optional args and environment variables."""
1433+ import subprocess
1434+
1435+ # Build environment with provided env vars
1436+ env = os .environ .copy ()
1437+ for key , value in env_vars .items ():
1438+ env [key ] = str (value )
1439+
1440+ # Build full command
1441+ full_cmd = cmd
1442+ if args :
1443+ full_cmd = "%s %s" % (cmd , args )
1444+
1445+ # Use user's configured shell, fallback to /bin/sh
1446+ shell = env .get ("SHELL" , "/bin/sh" )
1447+
1448+ log ("Running shell tool '%s': %s" % (name , full_cmd ))
1449+ try :
1450+ result = subprocess .run (
1451+ [shell , "-c" , full_cmd ],
1452+ capture_output = True ,
1453+ text = True ,
1454+ timeout = 30 ,
1455+ env = env ,
1456+ )
1457+ if result .returncode == 0 :
1458+ return result .stdout .rstrip ('\n ' )
1459+ else :
1460+ return {
1461+ "returncode" : result .returncode ,
1462+ "stdout" : result .stdout .rstrip ('\n ' ),
1463+ "stderr" : result .stderr .rstrip ('\n ' ),
1464+ }
1465+ except subprocess .TimeoutExpired :
1466+ raise RuntimeError ("Command timed out after 30 seconds" )
1467+ except Exception as e :
1468+ raise RuntimeError ("Command failed: %s" % str (e ))
1469+
1470+ # Set docstring and metadata
1471+ shell_tool .__doc__ = description
1472+ shell_tool .safe = safe
1473+ shell_tool .__name__ = name
1474+
1475+ return shell_tool
1476+
1477+
13871478def load_tools (tool_specs , search_paths ):
13881479 tools = {} # name -> {"schema": ..., "func": ...}
13891480 for spec in tool_specs :
0 commit comments