// file_list returns metadata for files in a scope. Blob bytes are NOT // loaded — listing is a hot path that must stay light, and the LLM // would burn tokens for no benefit. package tools import ( "context" "encoding/json" "fmt" "strings" "time" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) type fileListArgs struct { Scope string `json:"scope" description:"Storage scope: 'skill', 'user:', or 'run:'."` } type fileListEntry struct { FileID string `json:"file_id"` Name string `json:"name"` Mime string `json:"mime"` SizeBytes int64 `json:"size_bytes"` CreatedAt string `json:"created_at"` } // NewFileList constructs the file_list tool. storage nil → "not // configured" at execute time. func NewFileList(storage FileStorage) tool.Tool { return tool.NewGatedTool[fileListArgs]( "file_list", "List files in a scope. Returns a JSON array of {file_id, name, mime, size_bytes, created_at}. Does NOT include bytes — call file_get with a file_id to fetch content.", tool.Permission{ AuthoringRequirement: tool.RequirementAnyone, OperatesOn: tool.ScopeCaller, SafeForShare: true, Categories: []string{"storage", "read"}, }, func(ctx context.Context, inv tool.Invocation, args fileListArgs) (string, error) { if storage == nil { return "", fmt.Errorf("file_list: not configured") } if err := ValidateScope(inv, args.Scope, false); err != nil { return "", fmt.Errorf("file_list: %w", err) } // root_run is a KV-only scope (v1) — see file_save's guard. if strings.HasPrefix(args.Scope, "root_run:") { return "", fmt.Errorf("file_list: root_run scope is KV-only") } rows, err := storage.FileList(ctx, inv.SkillID, args.Scope) if err != nil { return "", fmt.Errorf("file_list: %w", err) } out := make([]fileListEntry, 0, len(rows)) for _, r := range rows { out = append(out, fileListEntry{ FileID: r.ID, Name: r.Name, Mime: r.MimeType, SizeBytes: r.SizeBytes, CreatedAt: r.CreatedAt.UTC().Format(time.RFC3339), }) } b, err := json.Marshal(out) if err != nil { return "", fmt.Errorf("file_list: marshal: %w", err) } return string(b), nil }, ) }