diff --git a/internal/security/shell_execute_stream.go b/internal/security/shell_execute_stream.go index 02c5cb741..080272cdb 100644 --- a/internal/security/shell_execute_stream.go +++ b/internal/security/shell_execute_stream.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "os/exec" + "runtime" "sync" "github.com/cloudwego/eino/adk/filesystem" @@ -42,6 +43,15 @@ func NewEinoStreamingShell() *EinoStreamingShell { return &EinoStreamingShell{} } +// newShellCommand 按平台选择 shell:Windows 使用 cmd.exe,类 Unix 使用 /bin/sh。 +// 修复 Windows 平台 execute 工具硬编码 /bin/sh 导致 exec: "/bin/sh": executable file not found。 +func newShellCommand(ctx context.Context, command string) *exec.Cmd { + if runtime.GOOS == "windows" { + return exec.CommandContext(ctx, "cmd", "/c", command) + } + return exec.CommandContext(ctx, "/bin/sh", "-c", command) +} + // ExecuteStreaming 实现 filesystem.StreamingShell。 func (s *EinoStreamingShell) ExecuteStreaming(ctx context.Context, input *filesystem.ExecuteRequest) (*schema.StreamReader[*filesystem.ExecuteResponse], error) { if input == nil || input.Command == "" { @@ -60,8 +70,8 @@ func (s *EinoStreamingShell) ExecuteStreaming(ctx context.Context, input *filesy func runShellInBackground(ctx context.Context, command string, w *schema.StreamWriter[*filesystem.ExecuteResponse]) { defer w.Close() - command = PrepareShellCommandForExecute(command) - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) + command = prepareShellCommandForPlatform(command) + cmd := newShellCommand(ctx, command) applyDefaultTerminalEnv(cmd) attachNonInteractiveStdin(cmd) stdout, err := cmd.StdoutPipe() @@ -120,8 +130,8 @@ func drainShellPipes(stdout, stderr io.Reader) { func streamShellForeground(ctx context.Context, command string, w *schema.StreamWriter[*filesystem.ExecuteResponse]) { defer w.Close() - command = PrepareShellCommandForExecute(command) - cmd := exec.CommandContext(ctx, "/bin/sh", "-c", command) + command = prepareShellCommandForPlatform(command) + cmd := newShellCommand(ctx, command) applyDefaultTerminalEnv(cmd) attachNonInteractiveStdin(cmd) @@ -209,3 +219,13 @@ func streamShellForeground(ctx context.Context, command string, w *schema.Stream } _ = w.Send(nil, fmt.Errorf("command failed: %w", waitErr)) } + +// prepareShellCommandForPlatform 按平台预处理命令: +// - Windows(cmd.exe):不注入 sh 专用指令,避免 /dev/null、export 等导致命令失败; +// - 类 Unix(/bin/sh):保持原有非交互 + 后台 IO 重定向包装,行为不变。 +func prepareShellCommandForPlatform(command string) string { + if runtime.GOOS == "windows" { + return command + } + return PrepareShellCommandForExecute(command) +} diff --git a/internal/security/shell_execute_stream_test.go b/internal/security/shell_execute_stream_test.go index 938f29942..4927a9f2b 100644 --- a/internal/security/shell_execute_stream_test.go +++ b/internal/security/shell_execute_stream_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "runtime" "strings" "testing" "time" @@ -12,6 +13,9 @@ import ( ) func TestEinoStreamingShell_StreamsStderrBeforeStdoutEOF(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sh syntax assertions, not applicable on cmd") + } shell := NewEinoStreamingShell() cmd := PrepareNonInteractiveShellCommand("echo err-only >&2; exit 1") sr, err := shell.ExecuteStreaming(context.Background(), &filesystem.ExecuteRequest{Command: cmd}) @@ -43,6 +47,9 @@ func TestEinoStreamingShell_StreamsStderrBeforeStdoutEOF(t *testing.T) { } func TestEinoStreamingShell_SudoFailsFast(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sudo is unix-only") + } shell := NewEinoStreamingShell() cmd := PrepareNonInteractiveShellCommand("sudo whoami && sudo cat /etc/os-release") sr, err := shell.ExecuteStreaming(context.Background(), &filesystem.ExecuteRequest{Command: cmd}) @@ -79,6 +86,9 @@ func TestEinoStreamingShell_SudoFailsFast(t *testing.T) { } func TestEinoStreamingShell_StderrWhileStdoutBlocks(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sh syntax assertions, not applicable on cmd") + } shell := NewEinoStreamingShell() // 模拟 sudo:stderr 先有输出,stdout 侧进程仍挂起;旧 eino local 在首包 stderr 前不会向流写任何内容。 cmd := PrepareNonInteractiveShellCommand(`echo "password prompt" >&2; sleep 30`) @@ -118,6 +128,9 @@ func TestEinoStreamingShell_StderrWhileStdoutBlocks(t *testing.T) { // TestEinoStreamingShell_BackgroundJobDoesNotHoldPipe 模拟 cmd & 后继续前台逻辑:重定向后应快速结束。 func TestEinoStreamingShell_BackgroundJobDoesNotHoldPipe(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("sh syntax assertions, not applicable on cmd") + } if testing.Short() { t.Skip("skipping shell integration in -short") }