From f02444a109bbfba263b2167c3b113024713c9be0 Mon Sep 17 00:00:00 2001 From: "Aryan Singh K." <70511529+aryansk@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:27:52 +0530 Subject: [PATCH] fix(tools): make execute_bash non-interactive-safe --- gcode/tools.py | 11 ++++++++--- tests/test_tools.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/gcode/tools.py b/gcode/tools.py index f00e15e..2054b38 100644 --- a/gcode/tools.py +++ b/gcode/tools.py @@ -30,11 +30,16 @@ def execute_bash(command: str) -> str: """Execute a bash command on the local machine and return its output. Requires interactive confirmation (y/n) before running unless auto-approve - is enabled. Returns combined stdout and stderr, and reports a non-zero exit - code if the command fails. + is enabled. In non-interactive environments (CI, Docker, pipes) the command + is rejected rather than hanging or crashing; enable auto-approve to run. + Returns combined stdout and stderr, and reports a non-zero exit code if the + command fails. """ if not AUTO_APPROVE: - confirm = input(f"GCode wants to run: {command}\nApprove? (y/n): ") + try: + confirm = input(f"GCode wants to run: {command}\nApprove? (y/n): ") + except (EOFError, KeyboardInterrupt): + return "Command execution cancelled by user." if confirm.strip().lower() != "y": return "Command execution cancelled by user." try: diff --git a/tests/test_tools.py b/tests/test_tools.py index f0f8852..1df1537 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -116,3 +116,39 @@ def test_grep_python_fallback_skips_binary_files(tmp_path): out = _grep_python("needle", str(tmp_path), "*") assert "text.txt" in out assert "bin.dat" not in out + + +def test_execute_bash_cancels_on_eof(): + from gcode.tools import execute_bash + + with patch("builtins.input", side_effect=EOFError): + out = execute_bash.invoke({"command": "echo hi"}) + assert out == "Command execution cancelled by user." + + +def test_execute_bash_cancels_on_keyboard_interrupt(): + from gcode.tools import execute_bash + + with patch("builtins.input", side_effect=KeyboardInterrupt): + out = execute_bash.invoke({"command": "echo hi"}) + assert out == "Command execution cancelled by user." + + +def test_execute_bash_rejects_non_yes_answer(): + from gcode.tools import execute_bash + + with patch("builtins.input", return_value="n"): + out = execute_bash.invoke({"command": "echo hi"}) + assert out == "Command execution cancelled by user." + + +def test_execute_bash_auto_approve_skips_prompt(tmp_path): + from gcode.tools import AUTO_APPROVE, execute_bash, set_auto_approve + + set_auto_approve(True) + try: + with patch("builtins.input", side_effect=AssertionError("must not prompt")): + out = execute_bash.invoke({"command": "echo auto-approved"}) + assert "auto-approved" in out + finally: + set_auto_approve(AUTO_APPROVE)