diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index a570ac9ec..5f4103dcf 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -95,10 +95,12 @@ var ( regexp.MustCompile(`\bssh\b.*@`), regexp.MustCompile(`\beval\b`), regexp.MustCompile(`\bsource\s+.*\.sh\b`), + regexp.MustCompile(`\bfind\s+/\b`), // find / - traverse entire filesystem + regexp.MustCompile(`\bls\s+/\b`), // ls / - list root directory } // absolutePathPattern matches absolute file paths in commands (Unix and Windows). - absolutePathPattern = regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/[^\s\"']+`) + absolutePathPattern = regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/(?:[^\s\"']*)?`) // safePaths are kernel pseudo-devices that are always safe to reference in // commands, regardless of workspace restriction. They contain no user data @@ -111,6 +113,7 @@ var ( "/dev/stdin": true, "/dev/stdout": true, "/dev/stderr": true, + "/": true, // root is a path boundary, not a regular file } ) diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go index a8de2f4c9..fedc99c45 100644 --- a/pkg/tools/shell_test.go +++ b/pkg/tools/shell_test.go @@ -1613,3 +1613,56 @@ func TestEncodeKeyTokenWithPtyKeyMode(t *testing.T) { }) } } + +func TestShellTool_FindRootBlocked(t *testing.T) { + tmpDir := t.TempDir() + tool, err := NewExecTool(tmpDir, true) + if err != nil { + t.Fatalf("unable to configure exec tool: %s", err) + } + + blocked := []string{ + "find / -name 'private*' -type f 2>/dev/null", + "find /etc -name 'passwd'", + "find / -type f -name '*.key'", + "ls /", + "ls /etc", + } + + for _, cmd := range blocked { + result := tool.Execute(context.Background(), map[string]any{ + "action": "run", + "command": cmd, + }) + if !result.IsError { + t.Errorf("expected command to be blocked: %s", cmd) + } + if !strings.Contains(result.ForLLM, "blocked") { + t.Errorf("expected 'blocked' message for: %s\ngot: %s", cmd, result.ForLLM) + } + } +} + +func TestShellTool_FindInWorkspaceAllowed(t *testing.T) { + tmpDir := t.TempDir() + tool, err := NewExecTool(tmpDir, true) + if err != nil { + t.Fatalf("unable to configure exec tool: %s", err) + } + + allowed := []string{ + "find . -name '*.go'", + "find -name '*.txt'", + "echo hello", + } + + for _, cmd := range allowed { + result := tool.Execute(context.Background(), map[string]any{ + "action": "run", + "command": cmd, + }) + if result.IsError && strings.Contains(result.ForLLM, "blocked") { + t.Errorf("command should not be blocked: %s\n error: %s", cmd, result.ForLLM) + } + } +}