@@ -9,6 +9,7 @@ import (
11 "github.com/charmbracelet/crush/internal/fsext"
11 "github.com/charmbracelet/crush/internal/fsext"
12+ "github.com/charmbracelet/crush/internal/permission"
14type LSParams struct {
15type LSParams struct {
@@ -16,6 +17,11 @@ type LSParams struct {
16 Ignore []string `json:"ignore"`
17 Ignore []string `json:"ignore"`
20+type LSPermissionsParams struct {
21+ Path string `json:"path"`
22+ Ignore []string `json:"ignore"`
19type TreeNode struct {
25type TreeNode struct {
20 Name string `json:"name"`
26 Name string `json:"name"`
21 Path string `json:"path"`
27 Path string `json:"path"`
@@ -29,7 +35,8 @@ type LSResponseMetadata struct {
31type lsTool struct {
37type lsTool struct {
32− workingDir string
38+ workingDir string
39+ permissions permission.Service
@@ -71,9 +78,10 @@ TIPS:
71- Combine with other tools for more effective exploration`
78- Combine with other tools for more effective exploration`
74−func NewLsTool(workingDir string) BaseTool {
81+func NewLsTool(permissions permission.Service, workingDir string) BaseTool {
75 return &lsTool{
82 return &lsTool{
76− workingDir: workingDir,
83+ workingDir: workingDir,
84+ permissions: permissions,
@@ -117,20 +125,51 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
117 searchPath = filepath.Join(l.workingDir, searchPath)
125 searchPath = filepath.Join(l.workingDir, searchPath)
120− if _, err := os.Stat(searchPath); os.IsNotExist(err) {
128+ // Check if directory is outside working directory and request permission if needed
121− return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
129+ absWorkingDir, err := filepath.Abs(l.workingDir)
131+ return ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
124− files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
134+ absSearchPath, err := filepath.Abs(searchPath)
125 if err != nil {
135 if err != nil {
126− return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
136+ return ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
129− tree := createFileTree(files)
139+ relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
130− output := printTree(tree, searchPath)
140+ if err != nil || strings.HasPrefix(relPath, "..") {
141+ // Directory is outside working directory, request permission
142+ sessionID, messageID := GetContextValues(ctx)
143+ if sessionID == "" || messageID == "" {
144+ return ToolResponse{}, fmt.Errorf("session ID and message ID are required for accessing directories outside working directory")
132− if truncated {
147+ granted := l.permissions.Request(
133− output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %d files and directories are included below:\n\n%s", MaxLSFiles, MaxLSFiles, output)
148+ permission.CreatePermissionRequest{
149+ SessionID: sessionID,
150+ Path: absSearchPath,
151+ ToolCallID: call.ID,
152+ ToolName: LSToolName,
154+ Description: fmt.Sprintf("List directory outside working directory: %s", absSearchPath),
155+ Params: LSPermissionsParams(params),
160+ return ToolResponse{}, permission.ErrorPermissionDenied
164+ output, err := ListDirectoryTree(searchPath, params.Ignore)
166+ return ToolResponse{}, err
169+ // Get file count for metadata
170+ files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
172+ return ToolResponse{}, fmt.Errorf("error listing directory for metadata: %w", err)
136 return WithResponseMetadata(
175 return WithResponseMetadata(
@@ -142,6 +181,26 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
184+func ListDirectoryTree(searchPath string, ignore []string) (string, error) {
185+ if _, err := os.Stat(searchPath); os.IsNotExist(err) {
186+ return "", fmt.Errorf("path does not exist: %s", searchPath)
189+ files, truncated, err := fsext.ListDirectory(searchPath, ignore, MaxLSFiles)
191+ return "", fmt.Errorf("error listing directory: %w", err)
194+ tree := createFileTree(files)
195+ output := printTree(tree, searchPath)
198+ output = fmt.Sprintf("There are more than %d files in the directory. Use a more specific path or use the Glob tool to find specific files. The first %d files and directories are included below:\n\n%s", MaxLSFiles, MaxLSFiles, output)
145func createFileTree(sortedPaths []string) []*TreeNode {
204func createFileTree(sortedPaths []string) []*TreeNode {
146 root := []*TreeNode{}
205 root := []*TreeNode{}
147 pathMap := make(map[string]*TreeNode)
206 pathMap := make(map[string]*TreeNode)