@@ -12,11 +12,26 @@ class ShellExecutor:
1212
1313 def __init__ (self ):
1414 """
15- Initialize the executor. The allowed commands are read from ALLOW_COMMANDS
16- environment variable during command validation, not at initialization.
15+ Initialize the executor.
1716 """
1817 pass
1918
19+ def _get_allowed_commands (self ) -> set [str ]:
20+ """Get the set of allowed commands from environment variables"""
21+ allow_commands = os .environ .get ("ALLOW_COMMANDS" , "" )
22+ allowed_commands = os .environ .get ("ALLOWED_COMMANDS" , "" )
23+ commands = allow_commands + "," + allowed_commands
24+ return {cmd .strip () for cmd in commands .split ("," ) if cmd .strip ()}
25+
26+ def get_allowed_commands (self ) -> list [str ]:
27+ """Get the list of allowed commands from environment variables"""
28+ return list (self ._get_allowed_commands ())
29+
30+ def is_command_allowed (self , command : str ) -> bool :
31+ """Check if a command is in the allowed list"""
32+ cmd = command .strip ()
33+ return cmd in self ._get_allowed_commands ()
34+
2035 def _validate_redirection_syntax (self , command : List [str ]) -> None :
2136 """
2237 Validate the syntax of redirection operators in the command.
@@ -155,24 +170,12 @@ async def _cleanup_handles(
155170 """
156171 for key in ["stdout" , "stderr" ]:
157172 handle = handles .get (key )
158- if isinstance (handle , IO ) and handle != asyncio . subprocess . PIPE :
173+ if handle and hasattr (handle , "close" ) and not isinstance ( handle , int ) :
159174 try :
160175 handle .close ()
161- except IOError :
176+ except ( IOError , ValueError ) :
162177 pass
163178
164- def _get_allowed_commands (self ) -> set :
165- """
166- Get the set of allowed commands from environment variables.
167- Checks both ALLOW_COMMANDS and ALLOWED_COMMANDS.
168- """
169- allow_commands = os .environ .get ("ALLOW_COMMANDS" , "" )
170- allowed_commands = os .environ .get ("ALLOWED_COMMANDS" , "" )
171-
172- # Combine and deduplicate commands from both environment variables
173- commands = allow_commands + "," + allowed_commands
174- return {cmd .strip () for cmd in commands .split ("," ) if cmd .strip ()}
175-
176179 def _clean_command (self , command : List [str ]) -> List [str ]:
177180 """
178181 Clean command by trimming whitespace from each part.
@@ -253,32 +256,36 @@ def _validate_directory(self, directory: Optional[str]) -> None:
253256 if not os .access (directory , os .R_OK | os .X_OK ):
254257 raise ValueError (f"Directory is not accessible: { directory } " )
255258
256- def get_allowed_commands (self ) -> list [str ]:
257- """Get the allowed commands"""
258- return list (self ._get_allowed_commands ())
259-
260259 def _validate_no_shell_operators (self , cmd : str ) -> None :
261260 """Validate that the command does not contain shell operators"""
262261 if cmd in [";" "&&" , "||" , "|" ]:
263262 raise ValueError (f"Unexpected shell operator: { cmd } " )
264263
265- def _validate_pipeline (self , commands : List [str ]) -> None :
266- """Validate pipeline command and ensure all parts are allowed"""
264+ def _validate_pipeline (self , commands : List [str ]) -> Dict [str , str ]:
265+ """Validate pipeline command and ensure all parts are allowed
266+
267+ Returns:
268+ Dict[str, str]: Error message if validation fails, empty dict if success
269+ """
267270 current_cmd : List [str ] = []
268271
269272 for token in commands :
270273 if token == "|" :
271274 if not current_cmd :
272275 raise ValueError ("Empty command before pipe operator" )
273- self ._validate_command (current_cmd )
276+ if not self .is_command_allowed (current_cmd [0 ]):
277+ raise ValueError (f"Command not allowed: { current_cmd [0 ]} " )
274278 current_cmd = []
275279 elif token in [";" , "&&" , "||" ]:
276280 raise ValueError (f"Unexpected shell operator in pipeline: { token } " )
277281 else :
278282 current_cmd .append (token )
279283
280284 if current_cmd :
281- self ._validate_command (current_cmd )
285+ if not self .is_command_allowed (current_cmd [0 ]):
286+ raise ValueError (f"Command not allowed: { current_cmd [0 ]} " )
287+
288+ return {}
282289
283290 def _split_pipe_commands (self , command : List [str ]) -> List [List [str ]]:
284291 """
@@ -379,6 +386,7 @@ async def execute(
379386 timeout : Optional [int ] = None ,
380387 ) -> Dict [str , Any ]:
381388 start_time = time .time ()
389+ process = None # Initialize process variable
382390
383391 try :
384392 # Validate directory if specified
@@ -393,33 +401,55 @@ async def execute(
393401 "execution_time" : time .time () - start_time ,
394402 }
395403
396- # Preprocess command to handle pipe operators
404+ # Process command
397405 preprocessed_command = self ._preprocess_command (command )
398406 cleaned_command = self ._clean_command (preprocessed_command )
399407 if not cleaned_command :
400- raise ValueError ("Empty command" )
408+ return {
409+ "error" : "Empty command" ,
410+ "status" : 1 ,
411+ "stdout" : "" ,
412+ "stderr" : "Empty command" ,
413+ "execution_time" : time .time () - start_time ,
414+ }
401415
402416 # First check for pipe operators and handle pipeline
403417 if "|" in cleaned_command :
404- commands : List [List [str ]] = []
405- current_cmd : List [str ] = []
406- for token in cleaned_command :
407- if token == "|" :
408- if current_cmd :
409- commands .append (current_cmd )
410- current_cmd = []
418+ try :
419+ # Validate pipeline first
420+ error = self ._validate_pipeline (cleaned_command )
421+ if error :
422+ return {
423+ ** error ,
424+ "status" : 1 ,
425+ "stdout" : "" ,
426+ "execution_time" : time .time () - start_time ,
427+ }
428+
429+ # Split commands
430+ commands : List [List [str ]] = []
431+ current_cmd : List [str ] = []
432+ for token in cleaned_command :
433+ if token == "|" :
434+ if current_cmd :
435+ commands .append (current_cmd )
436+ current_cmd = []
437+ else :
438+ raise ValueError ("Empty command before pipe operator" )
411439 else :
412- raise ValueError ("Empty command before pipe operator" )
413- else :
414- current_cmd .append (token )
415- if current_cmd :
416- commands .append (current_cmd )
417-
418- # Validate each command in pipeline
419- for cmd in commands :
420- self ._validate_command (cmd )
440+ current_cmd .append (token )
441+ if current_cmd :
442+ commands .append (current_cmd )
421443
422- return await self ._execute_pipeline (commands , directory , timeout )
444+ return await self ._execute_pipeline (commands , directory , timeout )
445+ except ValueError as e :
446+ return {
447+ "error" : str (e ),
448+ "status" : 1 ,
449+ "stdout" : "" ,
450+ "stderr" : str (e ),
451+ "execution_time" : time .time () - start_time ,
452+ }
423453
424454 # Then check for other shell operators
425455 for token in cleaned_command :
@@ -561,6 +591,11 @@ async def communicate_with_timeout():
561591 "stderr" : str (e ),
562592 "execution_time" : time .time () - start_time ,
563593 }
594+ finally :
595+ # Ensure process is terminated
596+ if process and process .returncode is None :
597+ process .kill ()
598+ await process .wait ()
564599
565600 async def _execute_pipeline (
566601 self ,
@@ -663,5 +698,10 @@ async def _execute_pipeline(
663698 }
664699
665700 finally :
701+ # Ensure all processes are terminated
702+ for process in processes :
703+ if process .returncode is None :
704+ process .kill ()
705+ await process .wait ()
666706 if isinstance (last_stdout , IO ):
667707 last_stdout .close ()
0 commit comments