package file import ( "context" "fmt" "os" "path/filepath" "strings" "time" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/tools" ) // maxListEntries is the maximum number of entries returned by list_directory. const maxListEntries = 500 // NewListDirectory creates a list_directory tool that lists files and directories. // Deny-by-default: if AllowedPaths is empty, all listings are rejected. // Does not follow symlinks that point outside of AllowedPaths. func NewListDirectory(cfg config.FileOpsCfg) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "list_directory", Description: "List files and directories at the given path. Returns name, size, type (file/dir), and modification date for each entry.", Parameters: []tools.Param{ {Name: "path", Type: "string", Description: "Absolute path to the directory to list", Required: true}, {Name: "recursive", Type: "boolean", Description: "List recursively (default: false)", Required: false}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { path := tools.GetString(args, "path") if path == "" { return tools.Result{Err: fmt.Errorf("list_directory: path is required")} } recursive := getBool(args, "recursive") absPath, err := filepath.Abs(path) if err != nil { return tools.Result{Err: fmt.Errorf("list_directory: %w", err)} } if err := validatePath(absPath, cfg.AllowedPaths); err != nil { return tools.Result{Err: err} } info, err := os.Stat(absPath) if err != nil { return tools.Result{Err: fmt.Errorf("list_directory: %w", err)} } if !info.IsDir() { return tools.Result{Err: fmt.Errorf("list_directory: %q is not a directory", absPath)} } var entries []string if recursive { entries, err = listRecursive(absPath, cfg.AllowedPaths) } else { entries, err = listFlat(absPath, cfg.AllowedPaths) } if err != nil { return tools.Result{Err: fmt.Errorf("list_directory: %w", err)} } if len(entries) > maxListEntries { entries = entries[:maxListEntries] entries = append(entries, fmt.Sprintf("... (truncated, showing %d of more entries)", maxListEntries)) } return tools.Result{Output: strings.Join(entries, "\n")} }, } } // listFlat lists immediate children of dir. func listFlat(dir string, allowedPaths []string) ([]string, error) { dirEntries, err := os.ReadDir(dir) if err != nil { return nil, err } var results []string for _, e := range dirEntries { entryPath := filepath.Join(dir, e.Name()) // Skip symlinks that point outside allowed paths. if e.Type()&os.ModeSymlink != 0 { if err := validatePath(entryPath, allowedPaths); err != nil { continue } } info, err := e.Info() if err != nil { continue } results = append(results, formatEntry("", e.Name(), info)) } return results, nil } // listRecursive lists all files under dir recursively. func listRecursive(root string, allowedPaths []string) ([]string, error) { var results []string count := 0 err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return nil // skip entries with errors } if path == root { return nil // skip the root directory itself } if count >= maxListEntries { return filepath.SkipAll } // Skip symlinks that point outside allowed paths. if d.Type()&os.ModeSymlink != 0 { if err := validatePath(path, allowedPaths); err != nil { if d.IsDir() { return filepath.SkipDir } return nil } } rel, err := filepath.Rel(root, path) if err != nil { return nil } info, err := d.Info() if err != nil { return nil } results = append(results, formatEntry("", rel, info)) count++ return nil }) return results, err } // formatEntry formats a single directory entry for output. func formatEntry(prefix, name string, info os.FileInfo) string { kind := "file" if info.IsDir() { kind = "dir" } mod := info.ModTime().Format(time.RFC3339) display := name if prefix != "" { display = prefix + "/" + name } return fmt.Sprintf("%s\t%s\t%d\t%s", display, kind, info.Size(), mod) } // getBool extracts a boolean argument by name, returning false if missing or wrong type. func getBool(args map[string]any, key string) bool { v, ok := args[key] if !ok { return false } b, ok := v.(bool) if !ok { return false } return b }