@@ -9,6 +9,7 @@ import (
99 "strings"
1010
1111 "github.com/charmbracelet/crush/internal/fsext"
12+ "github.com/charmbracelet/crush/internal/permission"
1213)
1314
1415type LSParams struct {
@@ -16,6 +17,11 @@ type LSParams struct {
1617 Ignore []string `json:"ignore"`
1718}
1819
20+type LSPermissionsParams struct {
21+ Path string `json:"path"`
22+ Ignore []string `json:"ignore"`
23+}
24+
1925type TreeNode struct {
2026 Name string `json:"name"`
2127 Path string `json:"path"`
@@ -29,7 +35,8 @@ type LSResponseMetadata struct {
2935}
3036
3137type lsTool struct {
32− workingDir string
38+ workingDir string
39+ permissions permission.Service
3340}
3441
3542const (
@@ -71,9 +78,10 @@ TIPS:
7178- Combine with other tools for more effective exploration`
7279)
7380
74−func NewLsTool(workingDir string) BaseTool {
81+func NewLsTool(permissions permission.Service, workingDir string) BaseTool {
7582 return &lsTool{
76− workingDir: workingDir,
83+ workingDir: workingDir,
84+ permissions: permissions,
7785 }
7886}
7987
@@ -117,20 +125,51 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
117125 searchPath = filepath.Join(l.workingDir, searchPath)
118126 }
119127
120− if _, err := os.Stat(searchPath); os.IsNotExist(err) {
121− return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
128+ // Check if directory is outside working directory and request permission if needed
129+ absWorkingDir, err := filepath.Abs(l.workingDir)
130+ if err != nil {
131+ return ToolResponse{}, fmt.Errorf("error resolving working directory: %w", err)
122132 }
123133
124− files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
134+ absSearchPath, err := filepath.Abs(searchPath)
125135 if err != nil {
126− return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
136+ return ToolResponse{}, fmt.Errorf("error resolving search path: %w", err)
127137 }
128138
129− tree := createFileTree(files)
130− output := printTree(tree, searchPath)
139+ relPath, err := filepath.Rel(absWorkingDir, absSearchPath)
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")
145+ }
131146
132− if truncated {
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)
147+ granted := l.permissions.Request(
148+ permission.CreatePermissionRequest{
149+ SessionID: sessionID,
150+ Path: absSearchPath,
151+ ToolCallID: call.ID,
152+ ToolName: LSToolName,
153+ Action: "list",
154+ Description: fmt.Sprintf("List directory outside working directory: %s", absSearchPath),
155+ Params: LSPermissionsParams(params),
156+ },
157+ )
158+
159+ if !granted {
160+ return ToolResponse{}, permission.ErrorPermissionDenied
161+ }
162+ }
163+
164+ output, err := ListDirectoryTree(searchPath, params.Ignore)
165+ if err != nil {
166+ return ToolResponse{}, err
167+ }
168+
169+ // Get file count for metadata
170+ files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
171+ if err != nil {
172+ return ToolResponse{}, fmt.Errorf("error listing directory for metadata: %w", err)
134173 }
135174
136175 return WithResponseMetadata(
@@ -142,6 +181,26 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
142181 ), nil
143182}
144183
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)
187+ }
188+
189+ files, truncated, err := fsext.ListDirectory(searchPath, ignore, MaxLSFiles)
190+ if err != nil {
191+ return "", fmt.Errorf("error listing directory: %w", err)
192+ }
193+
194+ tree := createFileTree(files)
195+ output := printTree(tree, searchPath)
196+
197+ if truncated {
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)
199+ }
200+
201+ return output, nil
202+}
203+
145204func createFileTree(sortedPaths []string) []*TreeNode {
146205 root := []*TreeNode{}
147206 pathMap := make(map[string]*TreeNode)