Estimate: 34m · Depends on: 6.4.2
The permission policy + its enforcement, at the service layer (the retrofit of the deferred TODO(6.4) seams). A projectAccessService (or a guard in projectsService) computes, for a (user, project): canBrowse — open/limited → any workspace member; private → a ProjectMembership exists (or the user is workspace owner/admin, who always pass); canEdit — limited → false for non-members (view+comment only); else gated by project role (viewer → false, member/admin → true). Thread canBrowse into the existing project read, the board projection (getBoard), and the issue list/detail reads so a non-member gets a typed ProjectAccessDeniedError (→ 403/404 at the route, the no-access state in the UI); thread canEdit into the write paths (create/move/assign/update) so a viewer’s writes are rejected. Workspace owner/admin bypass throughout.
Out of scope: the management API (6.4.4) + UI (6.4.5/6.4.6). This subtask is the policy + wiring it into reads/writes that already exist.
canBrowse(project, ctx) + canEdit(project, ctx) policy (open/limited/private × role), with workspace owner/admin always passing; a typed ProjectAccessDeniedError mapped to 403 (existing project) / 404 (hidden).canBrowse gates the project read + getBoard + the issue list/detail reads; canEdit gates the issue/board write paths (create/move/assign/update); no read or write path bypasses the gate.lib/services/projectsService.ts, boardsService.getBoard, workItemsService reads/writes — the seams to gate (search the TODO(6.4) notes, e.g. workflowsService 2.2.5, boardsService 3.3.3)motir-core/CLAUDE.md