Skip to content
Open
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
28 changes: 24 additions & 4 deletions internal/security/shell_execute_stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io"
"os/exec"
"runtime"
"sync"

"github.com/cloudwego/eino/adk/filesystem"
Expand Down Expand Up @@ -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 == "" {
Expand All @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
13 changes: 13 additions & 0 deletions internal/security/shell_execute_stream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"io"
"runtime"
"strings"
"testing"
"time"
Expand All @@ -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})
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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")
}
Expand Down