feat: agregar write_file, list_directory, append_file, delete_file a tools/file/
Expande el paquete tools/file/ con 4 operaciones nuevas para que los agentes puedan interactuar con carpetas de trabajo (workspaces, outputs). Cambios: - Extraer validatePath() y resolveReal() a validate.go para reutilizarlos - Agregar validateWritePath() que verifica ReadOnly == false - write_file: crea/sobreescribe archivos, crea dirs padre, limite 1MB - list_directory: lista archivos con metadata, modo recursivo, limite 500 entries - append_file: agrega contenido al final, crea si no existe, limite 10MB total - delete_file: borra solo archivos (nunca directorios), previene rm -rf accidental - Registrar las 4 tools nuevas en runtime.go condicionalmente: - list_directory: siempre (no requiere escritura) - write/append/delete: solo si ReadOnly == false Seguridad: todas las tools reutilizan validatePath() con deny-by-default, resolucion de symlinks y proteccion contra path traversal.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validatePath checks that absPath is under one of the allowed paths.
|
||||
// Deny-by-default: if allowedPaths is empty, no paths are allowed.
|
||||
// Resolves symlinks to prevent traversal via ../ or symlink escapes.
|
||||
func validatePath(absPath string, allowedPaths []string) error {
|
||||
if len(allowedPaths) == 0 {
|
||||
return fmt.Errorf("file: no allowed paths configured, all operations denied")
|
||||
}
|
||||
|
||||
// Resolve symlinks on the requested path to get the real path.
|
||||
// If the file doesn't exist yet, resolve the parent directory.
|
||||
realPath, err := resolveReal(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file: cannot resolve path %q: %w", absPath, err)
|
||||
}
|
||||
|
||||
for _, allowed := range allowedPaths {
|
||||
a, err := filepath.Abs(allowed)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Resolve symlinks on the allowed path too.
|
||||
realAllowed, err := resolveReal(a)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Ensure the real path is strictly under the allowed directory.
|
||||
// Add trailing separator to prevent /opt matching /opt1234.
|
||||
if strings.HasPrefix(realPath, realAllowed+string(filepath.Separator)) || realPath == realAllowed {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("path %q not under any allowed path", absPath)
|
||||
}
|
||||
|
||||
// validateWritePath checks path validity AND that writing is allowed.
|
||||
func validateWritePath(absPath string, allowedPaths []string, readOnly bool) error {
|
||||
if readOnly {
|
||||
return fmt.Errorf("file: write operations denied (read_only mode)")
|
||||
}
|
||||
return validatePath(absPath, allowedPaths)
|
||||
}
|
||||
|
||||
// resolveReal resolves symlinks for a path.
|
||||
// If the exact path doesn't exist, it resolves the deepest existing ancestor
|
||||
// and appends the remaining segments, preventing partial traversal.
|
||||
func resolveReal(path string) (string, error) {
|
||||
real, err := filepath.EvalSymlinks(path)
|
||||
if err == nil {
|
||||
return filepath.Clean(real), nil
|
||||
}
|
||||
// Path doesn't exist — resolve parent and append base.
|
||||
parent := filepath.Dir(path)
|
||||
base := filepath.Base(path)
|
||||
realParent, err := filepath.EvalSymlinks(parent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Clean(filepath.Join(realParent, base)), nil
|
||||
}
|
||||
Reference in New Issue
Block a user