mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
Problem: AI-created documents could not be handed off as validated, durable file cards, and binary files could not be read through the text editor route. Root cause: delivery identity lived only in ordinary tool output, while file reading, view selection and auxiliary actions lacked a shared preview path. Fix: add a bounded root-confined reader, Controller and raw-byte preview APIs, and host-confirmed present_files results projected through live and stored history. Add lazy PDF, worker CSV and sandboxed HTML viewers, responsive file cards, type icons, current-file state and accessible shared tooltips. Preserve dirty drafts while surfacing deletion/revocation and avoid metadata reads per keystroke. Keep the current workbench request-consumption contract intact. Verification: root and desktop Go tests, go vet, make lint/check, frontend typecheck, 498 related tests and production build. Browser checks cover PDF navigation, CSV literals, menus, request consumption and responsive themes. The builtin schema inventory grows by 143 measured tokens; boot effect tests verify stable schemas within a turn for direct and capability-proxy calls.
235 lines
5.7 KiB
Go
235 lines
5.7 KiB
Go
// Package workspacefile provides bounded, root-confined document reads.
|
|
package workspacefile
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const (
|
|
TextLimit = 2 << 20
|
|
ImageLimit = 10 << 20
|
|
PDFLimit = 32 << 20
|
|
)
|
|
|
|
var (
|
|
ErrDenied = errors.New("file is outside authorized roots")
|
|
ErrMissing = errors.New("file does not exist")
|
|
ErrNotFile = errors.New("not a regular file")
|
|
ErrTooLarge = errors.New("file exceeds preview limit")
|
|
ErrNotText = errors.New("file is not UTF-8 text")
|
|
ErrUnsupported = errors.New("file format has no preview")
|
|
ErrChanged = errors.New("file changed during preview read")
|
|
)
|
|
|
|
type Ref struct {
|
|
Path string `json:"path"`
|
|
Root string `json:"root,omitempty"`
|
|
}
|
|
|
|
type Info struct {
|
|
Ref Ref `json:"ref"`
|
|
Kind string `json:"kind"`
|
|
Size int64 `json:"size"`
|
|
Version string `json:"version"`
|
|
Limit int64 `json:"limit"`
|
|
ReadOnly bool `json:"readOnly"`
|
|
}
|
|
|
|
// Service receives roots already authorized by its host.
|
|
type Service struct {
|
|
Primary string
|
|
Extra []string
|
|
}
|
|
|
|
func (s Service) Resolve(path string) (Ref, error) {
|
|
if !filepath.IsAbs(path) {
|
|
if !filepath.IsLocal(path) {
|
|
return Ref{}, ErrDenied
|
|
}
|
|
return Ref{Path: filepath.ToSlash(filepath.Clean(path))}, nil
|
|
}
|
|
for _, root := range append([]string{s.Primary}, s.Extra...) {
|
|
rel, err := filepath.Rel(root, path)
|
|
if err == nil && filepath.IsLocal(rel) {
|
|
ref := Ref{Path: filepath.ToSlash(rel)}
|
|
if filepath.Clean(root) != filepath.Clean(s.Primary) {
|
|
ref.Root = root
|
|
}
|
|
return ref, nil
|
|
}
|
|
}
|
|
return Ref{}, ErrDenied
|
|
}
|
|
|
|
func (s Service) open(ref Ref) (*os.File, error) {
|
|
path := filepath.FromSlash(ref.Path)
|
|
if !filepath.IsLocal(path) || path == "." {
|
|
return nil, ErrDenied
|
|
}
|
|
base := s.Primary
|
|
if ref.Root != "" && filepath.Clean(ref.Root) != filepath.Clean(base) {
|
|
found := false
|
|
for _, extra := range s.Extra {
|
|
if filepath.Clean(extra) == filepath.Clean(ref.Root) {
|
|
base, found = extra, true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, ErrDenied
|
|
}
|
|
}
|
|
root, err := os.OpenRoot(base)
|
|
if err != nil {
|
|
return nil, classify(err)
|
|
}
|
|
defer root.Close()
|
|
resolvedBase, err := filepath.EvalSymlinks(base)
|
|
if err != nil {
|
|
return nil, classify(err)
|
|
}
|
|
target, err := filepath.EvalSymlinks(filepath.Join(base, path))
|
|
if err != nil {
|
|
return nil, classify(err)
|
|
}
|
|
relative, err := filepath.Rel(resolvedBase, target)
|
|
if err != nil || !filepath.IsLocal(relative) {
|
|
return nil, ErrDenied
|
|
}
|
|
// Stat before opening excludes FIFOs and devices, which may block on open.
|
|
info, err := root.Stat(path)
|
|
if err != nil {
|
|
return nil, classify(err)
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return nil, ErrNotFile
|
|
}
|
|
file, err := root.OpenFile(path, os.O_RDONLY|nonblock, 0)
|
|
if err != nil {
|
|
return nil, classify(err)
|
|
}
|
|
info, err = file.Stat()
|
|
if err != nil || !info.Mode().IsRegular() {
|
|
file.Close()
|
|
if err != nil {
|
|
return nil, classify(err)
|
|
}
|
|
return nil, ErrNotFile
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
func classify(err error) error {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("%w: %w", ErrMissing, err)
|
|
}
|
|
if errors.Is(err, fs.ErrPermission) {
|
|
return fmt.Errorf("%w: %w", ErrDenied, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func metadata(ref Ref, info fs.FileInfo) Info {
|
|
kind, limit := Format(ref.Path)
|
|
return Info{Ref: ref, Kind: kind, Size: info.Size(), Limit: limit, ReadOnly: ref.Root != "",
|
|
Version: strconv.FormatInt(info.ModTime().UnixNano(), 10) + ":" + strconv.FormatInt(info.Size(), 10)}
|
|
}
|
|
|
|
func (s Service) Info(ctx context.Context, ref Ref) (Info, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return Info{}, err
|
|
}
|
|
f, err := s.open(ref)
|
|
if err != nil {
|
|
return Info{}, err
|
|
}
|
|
defer f.Close()
|
|
info, err := f.Stat()
|
|
if err != nil {
|
|
return Info{}, err
|
|
}
|
|
return metadata(ref, info), nil
|
|
}
|
|
|
|
func (s Service) Content(ctx context.Context, ref Ref) ([]byte, Info, error) {
|
|
return s.content(ctx, ref, false)
|
|
}
|
|
|
|
func (s Service) Text(ctx context.Context, ref Ref) ([]byte, Info, error) {
|
|
return s.content(ctx, ref, true)
|
|
}
|
|
|
|
func (s Service) content(ctx context.Context, ref Ref, textOnly bool) ([]byte, Info, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, Info{}, err
|
|
}
|
|
f, err := s.open(ref)
|
|
if err != nil {
|
|
return nil, Info{}, err
|
|
}
|
|
defer f.Close()
|
|
before, err := f.Stat()
|
|
if err != nil {
|
|
return nil, Info{}, err
|
|
}
|
|
info := metadata(ref, before)
|
|
if textOnly {
|
|
info.Kind, info.Limit = "text", TextLimit
|
|
}
|
|
if info.Kind == "unsupported" {
|
|
return nil, info, ErrUnsupported
|
|
}
|
|
if info.Size > info.Limit {
|
|
return nil, info, ErrTooLarge
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(f, info.Limit+1))
|
|
if err != nil {
|
|
return nil, info, err
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, info, err
|
|
}
|
|
if int64(len(data)) > info.Limit {
|
|
return nil, info, ErrTooLarge
|
|
}
|
|
after, err := f.Stat()
|
|
if err != nil {
|
|
return nil, info, err
|
|
}
|
|
if metadata(ref, after).Version != info.Version {
|
|
return nil, info, ErrChanged
|
|
}
|
|
if info.Kind != "pdf" && info.Kind != "image" && (!utf8.Valid(data) || strings.IndexByte(string(data), 0) >= 0) {
|
|
return nil, info, ErrNotText
|
|
}
|
|
return data, info, nil
|
|
}
|
|
|
|
func Format(path string) (string, int64) {
|
|
switch strings.ToLower(filepath.Ext(path)) {
|
|
case ".md", ".markdown", ".mdx":
|
|
return "markdown", TextLimit
|
|
case ".html", ".htm":
|
|
return "html", TextLimit
|
|
case ".csv", ".tsv":
|
|
return "table", TextLimit
|
|
case ".pdf":
|
|
return "pdf", PDFLimit
|
|
case ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".bmp", ".ico", ".svg":
|
|
return "image", ImageLimit
|
|
case ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".zip", ".gz", ".7z", ".rar", ".exe", ".dll", ".bin":
|
|
return "unsupported", 0
|
|
default:
|
|
return "text", TextLimit
|
|
}
|
|
}
|