Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions src/minisweagent/environments/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions tests/environments/test_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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"