2121import logging
2222import os
2323import pathlib
24+ import resource
2425import shlex
2526import signal
2627from typing import Any
3233from .base_tool import BaseTool
3334from .tool_context import ToolContext
3435
36+ logger = logging .getLogger ("google_adk." + __name__ )
37+
3538
3639@dataclasses .dataclass (frozen = True )
3740class BashToolPolicy :
38- """Configuration for allowed bash commands based on prefix matching .
41+ """Configuration for allowed bash commands and resource limits .
3942
4043 Set allowed_command_prefixes to ("*",) to allow all commands (default),
4144 or explicitly list allowed prefixes.
45+
46+ Values for max_memory_bytes, max_file_size_bytes, and max_child_processes
47+ will be enforced upon the spawned subprocess.
4248 """
4349
4450 allowed_command_prefixes : tuple [str , ...] = ("*" ,)
51+ timeout_seconds : Optional [int ] = 30
52+ max_memory_bytes : Optional [int ] = None
53+ max_file_size_bytes : Optional [int ] = None
54+ max_child_processes : Optional [int ] = None
4555
4656
4757def _validate_command (command : str , policy : BashToolPolicy ) -> Optional [str ]:
@@ -61,6 +71,29 @@ def _validate_command(command: str, policy: BashToolPolicy) -> Optional[str]:
6171 return f"Command blocked. Permitted prefixes are: { allowed } "
6272
6373
74+ def _set_resource_limits (policy : BashToolPolicy ) -> None :
75+ """Sets resource limits for the subprocess based on the provided policy."""
76+ try :
77+ resource .setrlimit (resource .RLIMIT_CORE , (0 , 0 ))
78+ if policy .max_memory_bytes :
79+ resource .setrlimit (
80+ resource .RLIMIT_AS ,
81+ (policy .max_memory_bytes , policy .max_memory_bytes ),
82+ )
83+ if policy .max_file_size_bytes :
84+ resource .setrlimit (
85+ resource .RLIMIT_FSIZE ,
86+ (policy .max_file_size_bytes , policy .max_file_size_bytes ),
87+ )
88+ if policy .max_child_processes :
89+ resource .setrlimit (
90+ resource .RLIMIT_NPROC ,
91+ (policy .max_child_processes , policy .max_child_processes ),
92+ )
93+ except (ValueError , OSError ) as e :
94+ logger .warning ("Failed to set resource limits: %s" , e )
95+
96+
6497@features .experimental (features .FeatureName .SKILL_TOOLSET )
6598class ExecuteBashTool (BaseTool ):
6699 """Tool to execute a validated bash command within a workspace directory."""
@@ -144,20 +177,25 @@ async def run_async(
144177 stdout = asyncio .subprocess .PIPE ,
145178 stderr = asyncio .subprocess .PIPE ,
146179 start_new_session = True ,
180+ preexec_fn = lambda : _set_resource_limits (self ._policy ),
147181 )
148182
149183 try :
150184 stdout , stderr = await asyncio .wait_for (
151- process .communicate (), timeout = 30
185+ process .communicate (), timeout = self . _policy . timeout_seconds
152186 )
153187 except asyncio .TimeoutError :
154188 try :
155- os .killpg (process .pid , signal .SIGKILL )
189+ if process .pid :
190+ os .killpg (process .pid , signal .SIGKILL )
156191 except ProcessLookupError :
157192 pass
158193 stdout , stderr = await process .communicate ()
159194 return {
160- "error" : "Command timed out after 30 seconds." ,
195+ "error" : (
196+ f"Command timed out after { self ._policy .timeout_seconds } "
197+ " seconds."
198+ ),
161199 "stdout" : (
162200 stdout .decode (errors = "replace" )
163201 if stdout
@@ -176,7 +214,6 @@ async def run_async(
176214 os .killpg (process .pid , signal .SIGKILL )
177215 except ProcessLookupError :
178216 pass
179-
180217 return {
181218 "stdout" : (
182219 stdout .decode (errors = "replace" )
@@ -191,7 +228,6 @@ async def run_async(
191228 "returncode" : process .returncode ,
192229 }
193230 except Exception as e : # pylint: disable=broad-except
194- logger = logging .getLogger ("google_adk." + __name__ )
195231 logger .exception ("ExecuteBashTool execution failed" )
196232
197233 stdout_res = (
0 commit comments