diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java index 2f24aed5..f77242b2 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/ShellExecuteTool.java @@ -11,9 +11,11 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.util.Locale; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; /** * 内置工具:本地命令执行(跨平台) @@ -130,7 +132,10 @@ public class ShellExecuteTool { * Windows: cmd.exe /D /S /C "command" * /D 禁用 AutoRun 注册表项,避免副作用 * /S 保留引号原样传递给命令 - * Unix: /bin/sh -c command + * Unix: $SHELL -c command (RFC-03 Lane A1, fixes QwenPaw #3767) + * honors the user's interactive shell so alias resolution / PATH + * from the calling environment still apply; falls back to /bin/sh + * when $SHELL is unset or points at a non-executable path. */ private static ProcessBuilder buildShellProcess(String command) { ProcessBuilder pb; @@ -138,7 +143,8 @@ public class ShellExecuteTool { String winCommand = sanitizeWindowsCommand(command); pb = new ProcessBuilder("cmd.exe", "/D", "/S", "/C", winCommand); } else { - pb = new ProcessBuilder("/bin/sh", "-c", command); + String shell = selectPosixShell(System.getenv("SHELL")); + pb = new ProcessBuilder(shell, "-c", command); } // 设置工作区活动目录 @@ -181,6 +187,49 @@ public class ShellExecuteTool { return command.replace("\r\n", " ").replace("\n", " "); } + /** + * RFC-03 Lane A1 — pick the POSIX shell binary to invoke for a non-Windows + * tool call (fixes QwenPaw #3767). + * + *
Returns {@code userShellEnv} verbatim when: + *
Otherwise falls back to {@code /bin/sh} — the legacy hardcoded
+ * default. Important: {@code /bin/sh} on Debian/Ubuntu is dash, which
+ * does NOT honor users' bash-isms; the whole point of this method is
+ * to prefer the user's actual interactive shell when one is configured.
+ */
+ static String selectPosixShell(String userShellEnv) {
+ return selectPosixShell(userShellEnv, Files::isExecutable);
+ }
+
+ /**
+ * Test seam — same logic as {@link #selectPosixShell(String)} but with
+ * an injectable executable check so unit tests can drive every branch
+ * without depending on which shells actually exist on the test runner
+ * (Windows CI has no {@code /bin/sh}, POSIX dev hosts have varying
+ * shells installed). Production callers go through the single-arg
+ * overload above.
+ */
+ static String selectPosixShell(String userShellEnv, Predicate