"""Shared path resolution for thread virtual paths (e.g. mnt/user-data/outputs/...).""" from pathlib import Path from fastapi import HTTPException from deerflow.config.paths import get_paths from deerflow.runtime.thread_paths import aresolve_path_user_id, resolve_path_user_id def resolve_thread_virtual_path(thread_id: str, virtual_path: str, *, user_id: str | None = None) -> Path: """Resolve a virtual path to the actual filesystem path under thread user-data. Args: thread_id: The thread ID. virtual_path: The virtual path as seen inside the sandbox (e.g., /mnt/user-data/outputs/file.txt). user_id: Pre-resolved canonical user bucket. When ``None`` it is resolved synchronously via :func:`resolve_path_user_id` (the thread creator for task-shared threads, else the current login). Async callers should pass the result of :func:`aresolve_path_user_id` to avoid a blocking DB read. Returns: The resolved filesystem path. Raises: HTTPException: If the path is invalid or outside allowed directories. """ if user_id is None: user_id = resolve_path_user_id(thread_id) try: return get_paths().resolve_virtual_path(thread_id, virtual_path, user_id=user_id) except ValueError as e: status = 403 if "traversal" in str(e) else 400 raise HTTPException(status_code=status, detail=str(e)) async def aresolve_thread_virtual_path(thread_id: str, virtual_path: str) -> Path: """Async variant of :func:`resolve_thread_virtual_path` for FastAPI handlers. Resolves the canonical user bucket without blocking the event loop, so a task-shared thread's artifacts resolve to the thread creator's directory regardless of which account is currently logged in. """ user_id = await aresolve_path_user_id(thread_id) return resolve_thread_virtual_path(thread_id, virtual_path, user_id=user_id)