package tools import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/enmanuel/agents/internal/config" ) // NewReadFile creates a read_file tool that reads local files. // Validates paths against cfg.AllowedPaths when non-empty. func NewReadFile(cfg config.FileOpsCfg) Tool { return Tool{ Def: Def{ Name: "read_file", Description: "Read the contents of a local file.", Parameters: []Param{ {Name: "path", Type: "string", Description: "Absolute path to the file to read", Required: true}, }, }, Exec: func(ctx context.Context, args map[string]any) Result { path := getString(args, "path") if path == "" { return Result{Err: fmt.Errorf("read_file: path is required")} } absPath, err := filepath.Abs(path) if err != nil { return Result{Err: fmt.Errorf("read_file: %w", err)} } if err := validatePath(absPath, cfg.AllowedPaths); err != nil { return Result{Err: err} } data, err := os.ReadFile(absPath) if err != nil { return Result{Err: fmt.Errorf("read_file: %w", err)} } // Limit output to 64 KB content := string(data) if len(content) > 64*1024 { content = content[:64*1024] + "\n... (truncated)" } return Result{Output: content} }, } } func validatePath(absPath string, allowedPaths []string) error { if len(allowedPaths) == 0 { return nil } for _, allowed := range allowedPaths { a, err := filepath.Abs(allowed) if err != nil { continue } if strings.HasPrefix(absPath, a) { return nil } } return fmt.Errorf("path %q not under any allowed path", absPath) }