From d728bb26d59bb2e97396c2a4921787ca3b0360a0 Mon Sep 17 00:00:00 2001 From: atian8179 Date: Sun, 1 Mar 2026 22:08:07 +0800 Subject: [PATCH] fix: use shell fallback instead of hardcoded /bin/zsh in LocalSandbox (#939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: use shell fallback instead of hardcoded /bin/zsh in LocalSandbox Replace hardcoded /bin/zsh executable with dynamic shell detection that falls back through /bin/zsh → /bin/bash → /bin/sh. This fixes skill execution failures in Docker containers (python:3.12-slim) where zsh is not available. Closes #935 * Update backend/src/sandbox/local/local_sandbox.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: atian8179 Co-authored-by: Willem Jiang Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- backend/src/sandbox/local/local_sandbox.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/backend/src/sandbox/local/local_sandbox.py b/backend/src/sandbox/local/local_sandbox.py index 22a43f0..d69aa42 100644 --- a/backend/src/sandbox/local/local_sandbox.py +++ b/backend/src/sandbox/local/local_sandbox.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from pathlib import Path @@ -132,13 +133,32 @@ class LocalSandbox(Sandbox): return pattern.sub(replace_match, command) + @staticmethod + def _get_shell() -> str: + """Detect available shell executable with fallback. + + Returns the first available shell in order of preference: + /bin/zsh → /bin/bash → /bin/sh → first `sh` found on PATH. + Raises a RuntimeError if no suitable shell is found. + """ + for shell in ("/bin/zsh", "/bin/bash", "/bin/sh"): + if os.path.isfile(shell) and os.access(shell, os.X_OK): + return shell + shell_from_path = shutil.which("sh") + if shell_from_path is not None: + return shell_from_path + raise RuntimeError( + "No suitable shell executable found. Tried /bin/zsh, /bin/bash, " + "/bin/sh, and `sh` on PATH." + ) + def execute_command(self, command: str) -> str: # Resolve container paths in command before execution resolved_command = self._resolve_paths_in_command(command) result = subprocess.run( resolved_command, - executable="/bin/zsh", + executable=self._get_shell(), shell=True, capture_output=True, text=True,