Files
LaodingBot/internal/tools/filetool/filetool.go

177 lines
4.4 KiB
Go
Raw Normal View History

2026-02-21 23:01:39 +08:00
package filetool
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"laodingbot/internal/logger"
)
type Tool struct {
allowedDirs []string
maxOutputChars int
2026-02-21 23:01:39 +08:00
log *logger.Logger
}
func New(allowedDirs []string, maxOutputChars int, log *logger.Logger) *Tool {
2026-02-21 23:01:39 +08:00
normalized := make([]string, 0, len(allowedDirs))
for _, dir := range allowedDirs {
abs, err := filepath.Abs(strings.TrimSpace(dir))
if err == nil {
normalized = append(normalized, filepath.Clean(abs))
}
}
if maxOutputChars <= 0 {
maxOutputChars = 4000
}
2026-02-21 23:01:39 +08:00
if log != nil {
log.Infof("file tool initialized allowed_dirs=%d max_output_chars=%d", len(normalized), maxOutputChars)
2026-02-21 23:01:39 +08:00
}
return &Tool{allowedDirs: normalized, maxOutputChars: maxOutputChars, log: log}
2026-02-21 23:01:39 +08:00
}
func (t *Tool) Name() string { return "file" }
func (t *Tool) Description() string {
return "File operations with command format: read <path> | list <path> | write <path>\\n<content>"
2026-02-21 23:01:39 +08:00
}
func (t *Tool) Call(_ context.Context, input string) (string, error) {
input = strings.TrimSpace(input)
if t.log != nil {
t.log.Infof("file tool call input_len=%d input=%q", len(input), input)
2026-02-21 23:01:39 +08:00
}
if strings.HasPrefix(input, "read ") {
path := strings.TrimSpace(strings.TrimPrefix(input, "read "))
resolved, err := t.resolveAllowed(path)
if err != nil {
if t.log != nil {
t.log.Warnf("file read denied path=%s err=%v", path, err)
}
return "", err
}
info, err := os.Stat(resolved)
if err != nil {
if t.log != nil {
t.log.Errorf("file read stat failed path=%s err=%v", resolved, err)
}
return "", err
}
if info.IsDir() {
return "", fmt.Errorf("PATH_IS_DIRECTORY: %s (use 'list <path>' first)", resolved)
}
2026-02-21 23:01:39 +08:00
b, err := os.ReadFile(resolved)
if err != nil {
if t.log != nil {
t.log.Errorf("file read failed path=%s err=%v", resolved, err)
}
return "", err
}
if t.log != nil {
t.log.Infof("file read success path=%s bytes=%d", resolved, len(b))
}
out := string(b)
if len(out) > t.maxOutputChars {
out = out[:t.maxOutputChars]
}
return out, nil
}
if strings.HasPrefix(input, "list ") {
path := strings.TrimSpace(strings.TrimPrefix(input, "list "))
resolved, err := t.resolveAllowed(path)
if err != nil {
if t.log != nil {
t.log.Warnf("file list denied path=%s err=%v", path, err)
}
return "", err
}
entries, err := os.ReadDir(resolved)
if err != nil {
if t.log != nil {
t.log.Errorf("file list failed path=%s err=%v", resolved, err)
}
return "", err
}
b := strings.Builder{}
for _, e := range entries {
name := e.Name()
if e.IsDir() {
name += "/"
}
b.WriteString(name)
b.WriteString("\n")
if b.Len() >= t.maxOutputChars {
break
}
}
out := strings.TrimSpace(b.String())
if out == "" {
return "(empty)", nil
}
if len(out) > t.maxOutputChars {
out = out[:t.maxOutputChars]
}
return out, nil
2026-02-21 23:01:39 +08:00
}
if strings.HasPrefix(input, "write ") {
parts := strings.SplitN(input, "\n", 2)
if len(parts) < 2 {
return "", fmt.Errorf("write requires content in second line")
}
path := strings.TrimSpace(strings.TrimPrefix(parts[0], "write "))
resolved, err := t.resolveAllowed(path)
if err != nil {
if t.log != nil {
t.log.Warnf("file write denied path=%s err=%v", path, err)
}
return "", err
}
if err := os.MkdirAll(filepath.Dir(resolved), 0o755); err != nil {
if t.log != nil {
t.log.Errorf("file write mkdir failed path=%s err=%v", resolved, err)
}
return "", err
}
if err := os.WriteFile(resolved, []byte(parts[1]), 0o644); err != nil {
if t.log != nil {
t.log.Errorf("file write failed path=%s err=%v", resolved, err)
}
return "", err
}
if t.log != nil {
t.log.Infof("file write success path=%s bytes=%d", resolved, len(parts[1]))
}
return "ok", nil
}
return "", fmt.Errorf("unsupported file command")
}
func (t *Tool) resolveAllowed(path string) (string, error) {
base := strings.TrimSpace(os.Getenv("AGENT_WORKSPACE_DIR"))
var abs string
var err error
if filepath.IsAbs(path) {
abs = path
} else if base != "" {
abs = filepath.Join(base, path)
} else {
abs, err = filepath.Abs(path)
if err != nil {
return "", err
}
2026-02-21 23:01:39 +08:00
}
abs = filepath.Clean(abs)
for _, allowed := range t.allowedDirs {
if strings.HasPrefix(abs, allowed+string(filepath.Separator)) || abs == allowed {
return abs, nil
}
}
return "", fmt.Errorf("path not allowed: %s", path)
}