package filetool import ( "context" "fmt" "os" "path/filepath" "strings" "laodingbot/internal/logger" ) type Tool struct { allowedDirs []string log *logger.Logger } func New(allowedDirs []string, log *logger.Logger) *Tool { 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 log != nil { log.Infof("file tool initialized allowed_dirs=%d", len(normalized)) } return &Tool{allowedDirs: normalized, log: log} } func (t *Tool) Name() string { return "file" } func (t *Tool) Description() string { return "File operations with command format: read | write \\n" } func (t *Tool) Call(_ context.Context, input string) (string, error) { input = strings.TrimSpace(input) if t.log != nil { t.log.Debugf("file tool call input_len=%d", len(input)) } 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 } 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)) } return string(b), nil } 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) { abs, err := filepath.Abs(path) if err != nil { return "", err } 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) }