Files
weichenandCursor 18ddd0f862 feat(workspace): add WorkspaceRef fencing and hide physical paths
Cross-module callers get an opaque ref plus rotating lease tokens so stale writers fail closed, and file I/O is refused inside a unit of work.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 15:59:14 +00:00

75 lines
1.9 KiB
Go

package workspace
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/jackc/pgx/v5/pgxpool"
)
// Resolver 将逻辑 ID 解析为受约束的工作区路径。物理路径不得进入 WorkspaceRef。
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)
}