// file_get_metadata returns metadata about a saved file (name, mime, // size, created_at) WITHOUT loading the bytes. This is the v10 // agent-friendly companion to file_get — agents that just need to // reason about a file's properties (size, type, name) should use // file_get_metadata instead of pulling the full body into the context // window. // // Why a separate tool (vs adding a flag to file_get): the byte-vs- // reference principle is enforced statically — file_get_metadata's // return shape simply does not carry bytes, so agents and tool // authors can rely on the type signature. A flag-gated variant would // invite "what does include_content=false mean" confusion. package tools import ( "context" "encoding/json" "errors" "fmt" "time" "gitea.stevedudenhoeffer.com/steve/executus/tool" ) type fileGetMetadataArgs struct { FileID string `json:"file_id" description:"Opaque file ID returned by file_save or file_list."` } type fileGetMetadataResult struct { Name string `json:"name"` Mime string `json:"mime"` SizeBytes int64 `json:"size_bytes"` CreatedAt string `json:"created_at"` // RFC3339 Scope string `json:"scope"` } // NewFileGetMetadata constructs the file_get_metadata tool. storage // nil → "not configured" at execute time. func NewFileGetMetadata(storage FileStorage) tool.Tool { return tool.NewGatedTool[fileGetMetadataArgs]( "file_get_metadata", "Fetch metadata for a saved file by its file_id (name, mime, size_bytes, created_at, scope). Does NOT load the file bytes — use file_get_text for text content or send_attachments to ship binary content to Discord.", tool.Permission{ AuthoringRequirement: tool.RequirementAnyone, OperatesOn: tool.ScopeCaller, SafeForShare: true, Categories: []string{"storage", "read"}, }, func(ctx context.Context, inv tool.Invocation, args fileGetMetadataArgs) (string, error) { if storage == nil { return "", fmt.Errorf("file_get_metadata: not configured") } if args.FileID == "" { return "", fmt.Errorf("file_get_metadata: file_id required") } meta, _, err := storage.FileGet(ctx, args.FileID) if err != nil { if errors.Is(err, ErrFileNotFound) { return "", fmt.Errorf("file_get_metadata: not found") } return "", fmt.Errorf("file_get_metadata: %w", err) } // Descendant grant: see file_descendant_grant.go — covers // the scope check too (the file's scope is the worker's run). grantedViaDescendant := false if meta.SkillID != inv.SkillID { if !descendantFileGrant(ctx, storage, inv, meta.SkillID) { return "", fmt.Errorf("file_get_metadata: file does not belong to this skill") } grantedViaDescendant = true } if !grantedViaDescendant { if err := ValidateScope(inv, meta.Scope, inv.CallerIsAdmin); err != nil { return "", fmt.Errorf("file_get_metadata: %w", err) } } res := fileGetMetadataResult{ Name: meta.Name, Mime: meta.MimeType, SizeBytes: meta.SizeBytes, CreatedAt: meta.CreatedAt.UTC().Format(time.RFC3339), Scope: meta.Scope, } b, err := json.Marshal(res) if err != nil { return "", fmt.Errorf("file_get_metadata: marshal: %w", err) } return string(b), nil }, ) }