odysseus-dev/odysseus · warning · HTTPException

Workspace browsing is admin-only

Error message

Workspace browsing is admin-only

What it means

HTTP 403 from GET /browse (workspace routes): the authenticated user is neither an admin nor the single user of a single-user deployment, so filesystem browsing is denied. The endpoint enumerates server directories (same trust level as read_file/write_file/bash tools, which are in NON_ADMIN_BLOCKED_TOOLS), so any non-admin is blocked from mapping the host directory tree.

Source

Thrown at routes/workspace_routes.py:29

_MAX_BROWSE_DIRS = 500


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.

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Log in as an admin user (or the single user in single-user mode) to browse workspaces
  2. Have an admin grant the user admin rights if browsing is required for their role
  3. Non-admins: ask an admin to set the workspace path, or use per-user workspace settings if available
Defensive patterns

Strategy: type-guard

Validate before calling

user = get_current_user(request)
if not owner_is_admin_or_single_user(user):
    hide_workspace_picker()  # never call /browse

Type guard

def can_browse_workspace(user) -> bool:
    return owner_is_admin_or_single_user(user)

Try / catch

if resp.status_code == 403 and 'admin-only' in detail:
    hide_picker_and_show_notice('Ask an admin to set the workspace')

Prevention

When it happens

Trigger: A regular multi-user account calling /browse; a user whose admin flag was revoked but whose browser tab still has the workspace picker open; anonymous/session-cookie access without admin role.

Common situations: Multi-user installs where the workspace UI is visible to everyone but only functional for admins; permission changes not reflected until re-login.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/148ee9b0fea088ce. Report an issue: GitHub.