Permissions — a permission catalog, custom project roles, and permission-gated UI
Replace the fixed three-role model shipped by 6.4 Roles & permissions with a permission-based one: a named permission CATALOG the code enforces, built-in roles re-expressed as permission SETS, project admins able to author their own roles, and a UI that hides what the actor cannot reach instead of showing everything and refusing the write.
Where this starts from (rung 2 — verified on origin/main, 2026-08-05)
lib/projects/access.tsis already a pure policy module with 11 named predicates —canBrowse·canEdit·canComment·canModerateComments·canCreateAttachments·canDeleteAllAttachments·canManageWatchers·canManageProject·canSubmitToTriage·canUpvotePublicRequest·canCommentPublicRequest— each deciding over{ accessLevel, workspaceRole, projectRole }. Those predicate NAMES are already a permission vocabulary; what is missing is the indirection that lets a ROLE choose its own set.- The enforcement surface is coarse and wide:
git grep assertCanoverlib+appreturns 56assertCanBrowse· 51assertCanManage· 45assertCanEditcall sites.assertCanManagealone conflates every administrative domain — members, workflow, board, fields, components, estimation, automation, repositories, AI planning, code access — into one boolean. - The UI gate exists but is wide open:
lib/settings/projectSettingsNav.tsgives every settings entry anaccesspredicate, and 10 of its 11 entries arebrowse-gated (onlyautomationismanage-gated). Its own comment records the intent — "a member VIEWS every section". So a plain member does see Members, Code access, Workflow, Board, Fields, Components, AI planning; only the destination's writes are refused. The user's reading of the product is correct, and the mechanism to fix it is already in place — it is the POLICY that is missing. app/(authed)/_components/ProjectAccessProvider.tsxcarries exactly two booleans into the client tree —{ canEdit, canManage }. Every client-side affordance decision the product makes today is made from those two bits.
The model (rung 1 — CHECKED against the mirror + the field, not asserted)
- Jira team-managed (the model 6.4 already mirrors): project admins manage access through custom roles, over a catalog of ~27 granular permissions, with named BUNDLES ("Work on work items", "Collaborate on work items", "Manage work items") so the grid stays scannable. Default roles are Administrator / Member / Viewer. — https://support.atlassian.com/jira-software-cloud/docs/next-gen-permissions/
- Plane (the closest open-source Jira alternative) shipped custom roles + a permissions redesign in April 2026: permissions named
resource:actionwith a conditional form (workitem:delete+creator), grouped by resource, and roles composed from reusable named bundles. Roles live at workspace AND project scope. — https://plane.so/changelog/2026-04-25-custom-roles-granular-access-permissions-redesign - GitHub custom roles inherit a base role and add permissions on top — the pattern that keeps a new role comprehensible instead of starting from an empty grid. — https://docs.github.com/en/enterprise-cloud@latest/organizations/managing-user-access-to-your-organizations-repositories/managing-repository-roles/about-custom-repository-roles
- Linear deliberately ships NO custom roles (Admin / Member / Guest only). Recorded as the counter-example: it is why this epic keeps three OPINIONATED built-in roles that cover the common case, and treats custom roles as the bounded extension rather than the primary interface (the opinionated-defaults-plus-bounded-config stance).
What this epic therefore builds: permissions are a code-owned catalog, never user-authored rows — a permission the code does not enforce would be a lie on a settings screen. Built-in roles stay immutable and are seeded to reproduce today's behaviour exactly. Custom roles are project-scoped, authored by a project admin, and start from a built-in BASE. Access level (public/open/limited/private) is unchanged and keeps deciding who is in the project at all; the role decides what they may do once in.
Scope boundary
In: the permission catalog and its totality guards; built-in roles as permission sets; the lib/projects/access.ts re-expression; splitting project:administer into per-domain administrative permissions; project-scoped custom roles (schema, service, API, editor UI, assignment, delete-with-reassign); the actor's resolved permission set delivered to the client; the hide-versus-disable visibility rule and the surface sweep that applies it; the amendment of the 2026-06-09 gating directive recorded in design/projects/design-notes.md.
Out (named, with their owners): WORKSPACE/org-scoped custom roles — workspace MemberRole (owner/admin/member) is untouched by this epic, and the always-pass workspace-manager rail survives intact; API-token scopes (lib/mcp/scopes.ts) remain a separate, deliberately separate vocabulary that NARROWS a role and is not merged into the catalog; issue-level security schemes (Jira's per-issue security) — not planned, not needed by any shipped surface; permission INHERITANCE across projects, which Plane offers and Motir's flat workspace→project shape does not need.
Repo: every deliverable in this epic ships in motir-core — the policy module, the services, the settings surfaces and the tests all live there. Nothing here touches motir-ai or the gateway.