diff --git a/src/minisweagent/environments/local.py b/src/minisweagent/environments/local.py index 2b12d33ec..64ea4893c 100644 --- a/src/minisweagent/environments/local.py +++ b/src/minisweagent/environments/local.py @@ -14,6 +14,8 @@ class LocalEnvironmentConfig(BaseModel): cwd: str = "" env: dict[str, str] = {} timeout: int = 30 + interpreter: list[str] = ["bash", "-lc"] + """Interpreter to use to execute commands. The command is appended as the final argument.""" class LocalEnvironment: @@ -26,7 +28,9 @@ def execute(self, action: dict, cwd: str = "", *, timeout: int | None = None) -> command = action.get("command", "") cwd = cwd or self.config.cwd or os.getcwd() try: - result = _run(command, cwd, os.environ | self.config.env, timeout or self.config.timeout) + result = _run( + command, cwd, os.environ | self.config.env, timeout or self.config.timeout, self.config.interpreter + ) output = {"output": result.stdout, "returncode": result.returncode, "exception_info": ""} except Exception as e: raw_output = getattr(e, "output", None) @@ -69,11 +73,12 @@ def serialize(self) -> dict: } -def _run(command: str, cwd: str, env: dict[str, str], timeout: int) -> subprocess.CompletedProcess[str]: +def _run( + command: str, cwd: str, env: dict[str, str], timeout: int, interpreter: list[str] +) -> subprocess.CompletedProcess[str]: """Like subprocess.run, but kills the whole process group on timeout so no children are orphaned.""" process = subprocess.Popen( - command, - shell=True, + [*interpreter, command], text=True, cwd=cwd, env=env, diff --git a/tests/environments/test_local.py b/tests/environments/test_local.py index 705c19a20..a9b83f7cc 100644 --- a/tests/environments/test_local.py +++ b/tests/environments/test_local.py @@ -19,6 +19,7 @@ def test_local_environment_config_defaults(): assert config.cwd == "" assert config.env == {} assert config.timeout == 30 + assert config.interpreter == ["bash", "-lc"] def test_local_environment_basic_execution(): @@ -262,3 +263,13 @@ def test_local_environment_shell_features(): result = env.execute({"command": "echo $(echo 'nested')"}) assert result["returncode"] == 0 assert "nested" in result["output"] + + +@pytest.mark.skipif(os.name == "nt", reason="requires bash") +def test_local_environment_uses_bash_by_default(): + """Test that the local environment supports bash-only syntax by default.""" + env = LocalEnvironment() + + result = env.execute({"command": "cat <(printf 'bash-only')"}) + assert result["returncode"] == 0 + assert result["output"] == "bash-only"