4ab879e461
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.
68 lines
2.1 KiB
Go
68 lines
2.1 KiB
Go
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
|
|
}
|