"""Workspace API — browse server directories to pick a tool workspace folder.""" import os from fastapi import APIRouter, Request, HTTPException, Query from src.auth_helpers import get_current_user from src.tool_security import owner_is_admin_or_single_user def setup_workspace_routes(): router = APIRouter(prefix="/api/workspace", tags=["workspace"]) @router.get("/browse") def browse(request: Request, path: str = Query(default="")): """List subdirectories of `path` (default: home) so the UI can navigate the server filesystem and pick a workspace folder. Directories only. ADMIN-ONLY: this enumerates the server filesystem, so it is gated the same way the file/shell tools are (read_file/write_file/bash are in NON_ADMIN_BLOCKED_TOOLS). A non-admin who can't use those tools must not be able to map the host's directory tree either. """ owner = get_current_user(request) if not owner_is_admin_or_single_user(owner): raise HTTPException(status_code=403, detail="Workspace browsing is admin-only") # Resolve symlinks so the reported path is canonical and the UI navigates # real directories (defends against symlink games in displayed paths). target = os.path.realpath(os.path.expanduser(path.strip() or "~")) if not os.path.isdir(target): target = os.path.realpath(os.path.expanduser("~")) dirs = [] try: with os.scandir(target) as it: for entry in it: try: # Don't follow symlinks when classifying — a symlinked # dir is skipped rather than letting the browser wander # off via a link. Hidden entries are omitted. if entry.is_dir(follow_symlinks=False) and not entry.name.startswith("."): # Build the child path server-side with os.path.join # so it's correct on Windows (backslashes) and Linux. dirs.append({"name": entry.name, "path": os.path.join(target, entry.name)}) except OSError: continue except (PermissionError, OSError): dirs = [] parent = os.path.dirname(target) return { "path": target, "parent": parent if parent and parent != target else None, "dirs": sorted(dirs, key=lambda d: d["name"].lower()), } return router