package workspace import ( "context" "errors" "fmt" "os" "path/filepath" "strings" "github.com/jackc/pgx/v5/pgxpool" ) var ( ErrPathEscape = errors.New("workspace: path escapes data root") ErrWorkDirNotFound = errors.New("workspace: work directory not found") ) // Resolver 将逻辑 ID 解析为受约束的工作区路径。 type Resolver struct { dataRoot string pool *pgxpool.Pool } func NewResolver(dataRoot string, pool *pgxpool.Pool) *Resolver { return &Resolver{dataRoot: dataRoot, pool: pool} } // EnsureUnderDataRoot 校验绝对路径位于 dataRoot 内。 func (r *Resolver) EnsureUnderDataRoot(path string) (string, error) { if strings.TrimSpace(path) == "" { return "", fmt.Errorf("workspace: empty path") } abs, err := filepath.Abs(path) if err != nil { return "", err } root, err := filepath.Abs(r.dataRoot) if err != nil { return "", err } rel, err := filepath.Rel(root, abs) if err != nil { return "", ErrPathEscape } if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { return "", ErrPathEscape } return abs, nil } // ResolveDocWorkDir 从 document_records 读取 work_dir 并沙箱校验。 func (r *Resolver) ResolveDocWorkDir(ctx context.Context, docID string) (string, error) { docID = strings.TrimSpace(docID) if docID == "" { return "", fmt.Errorf("workspace: doc_id required") } if r.pool == nil { return "", ErrWorkDirNotFound } var workDir string err := r.pool.QueryRow(ctx, ` SELECT COALESCE(work_dir, '') FROM document_records WHERE doc_id = $1 `, docID).Scan(&workDir) if err != nil || strings.TrimSpace(workDir) == "" { return "", ErrWorkDirNotFound } return r.EnsureUnderDataRoot(workDir) } // ResolveStoredWorkDir 沙箱校验已持久化的工作区路径(如 task run workspace_id)。 func (r *Resolver) ResolveStoredWorkDir(storedPath string) (string, error) { storedPath = strings.TrimSpace(storedPath) if storedPath == "" { return "", ErrWorkDirNotFound } return r.EnsureUnderDataRoot(storedPath) }