package file import ( "context" "fmt" "os" "path/filepath" "github.com/enmanuel/agents/internal/config" "github.com/enmanuel/agents/tools" ) // NewReadFile creates a read_file tool that reads local files. // Deny-by-default: if AllowedPaths is empty, all reads are rejected. // Resolves symlinks and normalizes paths to prevent traversal attacks. func NewReadFile(cfg config.FileOpsCfg) tools.Tool { return tools.Tool{ Def: tools.Def{ Name: "read_file", Description: "Read the contents of a local file.", Parameters: []tools.Param{ {Name: "path", Type: "string", Description: "Absolute path to the file to read", Required: true}, }, }, Exec: func(ctx context.Context, args map[string]any) tools.Result { path := tools.GetString(args, "path") if path == "" { return tools.Result{Err: fmt.Errorf("read_file: path is required")} } absPath, err := filepath.Abs(path) if err != nil { return tools.Result{Err: fmt.Errorf("read_file: %w", err)} } if err := validatePath(absPath, cfg.AllowedPaths); err != nil { return tools.Result{Err: err} } data, err := os.ReadFile(absPath) if err != nil { return tools.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 tools.Result{Output: content} }, } }