feat: import agents_and_robots platform as unibots (Matrix-out, unibus transport)
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/enmanuel/agents/internal/config"
|
||||
"github.com/enmanuel/agents/tools"
|
||||
)
|
||||
|
||||
// maxListEntries is the maximum number of entries returned by list_directory.
|
||||
const maxListEntries = 500
|
||||
|
||||
// NewListDirectory creates a list_directory tool that lists files and directories.
|
||||
// Deny-by-default: if AllowedPaths is empty, all listings are rejected.
|
||||
// Does not follow symlinks that point outside of AllowedPaths.
|
||||
func NewListDirectory(cfg config.FileOpsCfg) tools.Tool {
|
||||
return tools.Tool{
|
||||
Def: tools.Def{
|
||||
Name: "list_directory",
|
||||
Description: "List files and directories at the given path. Returns name, size, type (file/dir), and modification date for each entry.",
|
||||
Parameters: []tools.Param{
|
||||
{Name: "path", Type: "string", Description: "Absolute path to the directory to list", Required: true},
|
||||
{Name: "recursive", Type: "boolean", Description: "List recursively (default: false)", Required: false},
|
||||
},
|
||||
},
|
||||
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||
path := tools.GetString(args, "path")
|
||||
if path == "" {
|
||||
return tools.Result{Err: fmt.Errorf("list_directory: path is required")}
|
||||
}
|
||||
|
||||
recursive := getBool(args, "recursive")
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return tools.Result{Err: fmt.Errorf("list_directory: %w", err)}
|
||||
}
|
||||
|
||||
if err := validatePath(absPath, cfg.AllowedPaths); err != nil {
|
||||
return tools.Result{Err: err}
|
||||
}
|
||||
|
||||
info, err := os.Stat(absPath)
|
||||
if err != nil {
|
||||
return tools.Result{Err: fmt.Errorf("list_directory: %w", err)}
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return tools.Result{Err: fmt.Errorf("list_directory: %q is not a directory", absPath)}
|
||||
}
|
||||
|
||||
var entries []string
|
||||
if recursive {
|
||||
entries, err = listRecursive(absPath, cfg.AllowedPaths)
|
||||
} else {
|
||||
entries, err = listFlat(absPath, cfg.AllowedPaths)
|
||||
}
|
||||
if err != nil {
|
||||
return tools.Result{Err: fmt.Errorf("list_directory: %w", err)}
|
||||
}
|
||||
|
||||
if len(entries) > maxListEntries {
|
||||
entries = entries[:maxListEntries]
|
||||
entries = append(entries, fmt.Sprintf("... (truncated, showing %d of more entries)", maxListEntries))
|
||||
}
|
||||
|
||||
return tools.Result{Output: strings.Join(entries, "\n")}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// listFlat lists immediate children of dir.
|
||||
func listFlat(dir string, allowedPaths []string) ([]string, error) {
|
||||
dirEntries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []string
|
||||
for _, e := range dirEntries {
|
||||
entryPath := filepath.Join(dir, e.Name())
|
||||
|
||||
// Skip symlinks that point outside allowed paths.
|
||||
if e.Type()&os.ModeSymlink != 0 {
|
||||
if err := validatePath(entryPath, allowedPaths); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
info, err := e.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, formatEntry("", e.Name(), info))
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// listRecursive lists all files under dir recursively.
|
||||
func listRecursive(root string, allowedPaths []string) ([]string, error) {
|
||||
var results []string
|
||||
count := 0
|
||||
|
||||
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil // skip entries with errors
|
||||
}
|
||||
if path == root {
|
||||
return nil // skip the root directory itself
|
||||
}
|
||||
if count >= maxListEntries {
|
||||
return filepath.SkipAll
|
||||
}
|
||||
|
||||
// Skip symlinks that point outside allowed paths.
|
||||
if d.Type()&os.ModeSymlink != 0 {
|
||||
if err := validatePath(path, allowedPaths); err != nil {
|
||||
if d.IsDir() {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
rel, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
results = append(results, formatEntry("", rel, info))
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
// formatEntry formats a single directory entry for output.
|
||||
func formatEntry(prefix, name string, info os.FileInfo) string {
|
||||
kind := "file"
|
||||
if info.IsDir() {
|
||||
kind = "dir"
|
||||
}
|
||||
mod := info.ModTime().Format(time.RFC3339)
|
||||
display := name
|
||||
if prefix != "" {
|
||||
display = prefix + "/" + name
|
||||
}
|
||||
return fmt.Sprintf("%s\t%s\t%d\t%s", display, kind, info.Size(), mod)
|
||||
}
|
||||
|
||||
// getBool extracts a boolean argument by name, returning false if missing or wrong type.
|
||||
func getBool(args map[string]any, key string) bool {
|
||||
v, ok := args[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
b, ok := v.(bool)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user