chore: sync from fn-registry agent
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
fsDefaultMaxBytes = 64 * 1024
|
||||
fsHardMaxBytes = 1024 * 1024
|
||||
)
|
||||
|
||||
// isPathAllowed valida path contra una lista de globs declarados en cap.PathsAllowed.
|
||||
// Aplica EvalSymlinks (best-effort) y filepath.Clean para neutralizar ../ + symlinks.
|
||||
// Soporta el sufijo "/**" para match recursivo bajo un prefijo.
|
||||
func isPathAllowed(path string, allowed []string) bool {
|
||||
if len(allowed) == 0 {
|
||||
return false
|
||||
}
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
abs = filepath.Clean(abs)
|
||||
|
||||
// Resolve symlinks si el target existe; si no existe (write a archivo nuevo)
|
||||
// resolvemos el directorio padre, no romper.
|
||||
resolved := abs
|
||||
if real, err := filepath.EvalSymlinks(abs); err == nil {
|
||||
resolved = real
|
||||
} else {
|
||||
// Para paths no existentes (write): intenta resolver el dir padre.
|
||||
dir := filepath.Dir(abs)
|
||||
base := filepath.Base(abs)
|
||||
if realDir, err := filepath.EvalSymlinks(dir); err == nil {
|
||||
resolved = filepath.Join(realDir, base)
|
||||
}
|
||||
}
|
||||
resolved = filepath.Clean(resolved)
|
||||
|
||||
for _, pat := range allowed {
|
||||
patClean := filepath.Clean(pat)
|
||||
// Soporte sufijo /** recursivo
|
||||
if strings.HasSuffix(patClean, string(filepath.Separator)+"**") || strings.HasSuffix(patClean, "/**") {
|
||||
prefix := strings.TrimSuffix(patClean, "/**")
|
||||
prefix = strings.TrimSuffix(prefix, string(filepath.Separator)+"**")
|
||||
prefix = filepath.Clean(prefix)
|
||||
if resolved == prefix || strings.HasPrefix(resolved, prefix+string(filepath.Separator)) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Si patron es un dir y resolved esta debajo, allow (caso /etc/)
|
||||
if strings.HasSuffix(pat, "/") {
|
||||
prefix := filepath.Clean(pat)
|
||||
if strings.HasPrefix(resolved, prefix+string(filepath.Separator)) || resolved == prefix {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Match exacto o glob simple
|
||||
if match, _ := filepath.Match(patClean, resolved); match {
|
||||
return true
|
||||
}
|
||||
if resolved == patClean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mapIntField extrae un int de map[string]any tolerando float64 (JSON default).
|
||||
func mapIntField(m map[string]any, key string, def int) int {
|
||||
if v, ok := m[key]; ok && v != nil {
|
||||
switch t := v.(type) {
|
||||
case float64:
|
||||
return int(t)
|
||||
case int:
|
||||
return t
|
||||
case int64:
|
||||
return int(t)
|
||||
case json.Number:
|
||||
n, err := t.Int64()
|
||||
if err == nil {
|
||||
return int(n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func mapStringField(m map[string]any, key string) string {
|
||||
if v, ok := m[key]; ok && v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// runFsRead lee un archivo y devuelve content_b64 + meta. Trunca a max_bytes.
|
||||
func runFsRead(cap *Capability, args map[string]any) (any, int, error) {
|
||||
path := mapStringField(args, "path")
|
||||
if path == "" {
|
||||
return nil, -1, fmt.Errorf("path required")
|
||||
}
|
||||
maxBytes := mapIntField(args, "max_bytes", fsDefaultMaxBytes)
|
||||
if maxBytes <= 0 {
|
||||
maxBytes = fsDefaultMaxBytes
|
||||
}
|
||||
if maxBytes > fsHardMaxBytes {
|
||||
maxBytes = fsHardMaxBytes
|
||||
}
|
||||
if !isPathAllowed(path, cap.PathsAllowed) {
|
||||
return nil, -1, fmt.Errorf("path not allowed by manifest: %s", path)
|
||||
}
|
||||
st, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
if st.IsDir() {
|
||||
return nil, -1, fmt.Errorf("path is a directory")
|
||||
}
|
||||
f, err := os.Open(path) // #nosec G304 — whitelisted above
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("open: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
buf := make([]byte, maxBytes)
|
||||
n, err := f.Read(buf)
|
||||
if err != nil && err.Error() != "EOF" && n == 0 {
|
||||
return nil, -1, fmt.Errorf("read: %w", err)
|
||||
}
|
||||
truncated := int64(n) < st.Size()
|
||||
return map[string]any{
|
||||
"content_b64": base64.StdEncoding.EncodeToString(buf[:n]),
|
||||
"size": st.Size(),
|
||||
"bytes_read": n,
|
||||
"mtime": st.ModTime().Unix(),
|
||||
"truncated": truncated,
|
||||
"path": path,
|
||||
}, 0, nil
|
||||
}
|
||||
|
||||
// runFsWrite escribe content_b64 a path. Mkdir parent, default mode 0644.
|
||||
func runFsWrite(cap *Capability, args map[string]any) (any, int, error) {
|
||||
path := mapStringField(args, "path")
|
||||
if path == "" {
|
||||
return nil, -1, fmt.Errorf("path required")
|
||||
}
|
||||
contentB64 := mapStringField(args, "content_b64")
|
||||
if contentB64 == "" {
|
||||
// Soporte fallback "content" plano
|
||||
if c := mapStringField(args, "content"); c != "" {
|
||||
contentB64 = base64.StdEncoding.EncodeToString([]byte(c))
|
||||
}
|
||||
}
|
||||
mode := mapIntField(args, "mode", 0644)
|
||||
if !isPathAllowed(path, cap.PathsAllowed) {
|
||||
return nil, -1, fmt.Errorf("path not allowed by manifest: %s", path)
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(contentB64)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("invalid content_b64: %w", err)
|
||||
}
|
||||
if int64(len(data)) > fsHardMaxBytes {
|
||||
return nil, -1, fmt.Errorf("content too large (>1MB)")
|
||||
}
|
||||
parent := filepath.Dir(path)
|
||||
if err := os.MkdirAll(parent, 0755); err != nil {
|
||||
return nil, -1, fmt.Errorf("mkdir parent: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, os.FileMode(mode)); err != nil { // #nosec G306
|
||||
return nil, -1, fmt.Errorf("write: %w", err)
|
||||
}
|
||||
return map[string]any{
|
||||
"path": path,
|
||||
"bytes_written": len(data),
|
||||
}, 0, nil
|
||||
}
|
||||
|
||||
// runFsList lista un directorio (no recursivo). glob opcional filtra entries.
|
||||
func runFsList(cap *Capability, args map[string]any) (any, int, error) {
|
||||
dir := mapStringField(args, "dir")
|
||||
if dir == "" {
|
||||
return nil, -1, fmt.Errorf("dir required")
|
||||
}
|
||||
glob := mapStringField(args, "glob")
|
||||
if !isPathAllowed(dir, cap.PathsAllowed) {
|
||||
return nil, -1, fmt.Errorf("dir not allowed by manifest: %s", dir)
|
||||
}
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("readdir: %w", err)
|
||||
}
|
||||
out := []map[string]any{}
|
||||
for _, e := range entries {
|
||||
if glob != "" {
|
||||
if m, _ := filepath.Match(glob, e.Name()); !m {
|
||||
continue
|
||||
}
|
||||
}
|
||||
info, ierr := e.Info()
|
||||
kind := "file"
|
||||
var size int64
|
||||
var mtime int64
|
||||
if ierr == nil {
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
kind = "symlink"
|
||||
} else if info.IsDir() {
|
||||
kind = "dir"
|
||||
}
|
||||
size = info.Size()
|
||||
mtime = info.ModTime().Unix()
|
||||
}
|
||||
out = append(out, map[string]any{
|
||||
"name": e.Name(),
|
||||
"kind": kind,
|
||||
"size": size,
|
||||
"mtime": mtime,
|
||||
})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
return out[i]["name"].(string) < out[j]["name"].(string)
|
||||
})
|
||||
return map[string]any{
|
||||
"dir": dir,
|
||||
"entries": out,
|
||||
"count": len(out),
|
||||
}, 0, nil
|
||||
}
|
||||
|
||||
// runFsStat devuelve metadata de un archivo o directorio.
|
||||
func runFsStat(cap *Capability, args map[string]any) (any, int, error) {
|
||||
path := mapStringField(args, "path")
|
||||
if path == "" {
|
||||
return nil, -1, fmt.Errorf("path required")
|
||||
}
|
||||
if !isPathAllowed(path, cap.PathsAllowed) {
|
||||
return nil, -1, fmt.Errorf("path not allowed by manifest: %s", path)
|
||||
}
|
||||
st, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("stat: %w", err)
|
||||
}
|
||||
kind := "file"
|
||||
if st.IsDir() {
|
||||
kind = "dir"
|
||||
} else if st.Mode()&os.ModeSymlink != 0 {
|
||||
kind = "symlink"
|
||||
}
|
||||
res := map[string]any{
|
||||
"path": path,
|
||||
"kind": kind,
|
||||
"size": st.Size(),
|
||||
"mode": fmt.Sprintf("%#o", st.Mode().Perm()),
|
||||
"mtime": st.ModTime().Unix(),
|
||||
}
|
||||
return res, 0, nil
|
||||
}
|
||||
Reference in New Issue
Block a user