Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2255Done

The permission MODEL — a catalog derived from the real operation surface, and a role becomes a permission SET

Introduce the permission catalog and re-express the shipped access policy over it, so the question the code asks is "does this actor hold work_item:edit?" rather than "is this actor a member?"with no change to what any actor can do. This is the foundation the rest of the Permissions epic stands on, and it earns its place by being provably behaviour-neutral: the built-in roles are defined as exactly the permission sets that reproduce today's eleven predicates, and a truth table proves the equivalence over every combination of access level and role.

⚠️ Re-sliced 2026-08-06 (Yue) — the page moved out

This story originally ended in the read-only Roles & permissions page, so that it finished in something a person could accept. That page is now its own story.

The bundling produced a straddle that surfaced three times: the page needs work that lands AFTER the per-domain split, while the split needs only the model — so this story kept having to sit on both sides of a sibling. Each time it looked like a wiring mistake and each time it was the slice. The epic's own shape is already layered (MOTIR-2256 is model-only, MOTIR-2258 is UI-only), so the model/page boundary is the honest one, and it is what lets the page ship once, against a fully enforced catalog, instead of against a model that is a quarter wired.

So this story is now the MODEL, end to end — and it is complete.

What it ships

  • The catalog is CODE, not data. lib/permissions/catalog.ts — a frozen, exhaustive list of keys in the mirror's resource:action form, each with a domain, i18n keys, and an enforcement marker. Permissions are never rows a user authors: a key nothing enforces would be a promise the product cannot keep.
  • The catalog is derived from the REAL operation surface. The inventory walked all 251 routes, the 22 Server Action files and 122 services and recorded a decided policy per operation — because the eleven shipped predicates turned out to be one corner of the enforcement surface, not the whole of it. 32 permissions across 16 domains; 11 wired today, 21 planned and awaiting the split.
  • A role is a SET over that catalog. BUILTIN_ROLE_PERMISSIONS maps admin / member / viewer to their sets; resolvePermissions turns { accessLevel, workspaceRole, projectRole } into the actor's effective set, with both shipped rails expressed INSIDE it.
  • The eleven predicates SURVIVE as their public API — same names, same signatures, now one-line membership tests. None of the ~150 assertCan* call sites moves; re-pointing them is MOTIR-2256's job.
  • The service readgetPermissions / getPermissionsDTO / getRoleCatalog on projectAccessService, keeping the 404-not-403 posture.
  • Both guards, closing the model in each direction — every key has a consumer, and every operation has an answer.

Scope boundary

In: the catalog + its domains + enforcement; the built-in role sets and the resolution; the re-expression of all eleven predicates; the service read and its DTO; the operation inventory; the no-ungoverned-operation guard.

Out: the page and its design; re-pointing any call site (MOTIR-2256); custom roles (MOTIR-2257); hiding surfaces by permission (MOTIR-2258).

Verification recipe

This story has no user-visible surface by design — that is what makes it safe, and it is why the page is a separate story. It is verified by its tests rather than by a click-path:

  • pnpm test tests/permissions/ — the 64-row parity truth table passes: for every (accessLevel × workspaceRole × projectRole), each of the eleven predicates returns the same answer as the pre-story policy. Green means no actor's capabilities changed.
  • The catalog guards: no duplicate key, resource:action shape, every domain non-empty, both i18n catalogs total over all 32 keys, the module importing no Prisma and doing no IO.
  • The inventory coverage guard: every route and Server Action named in docs/decisions/permission-inventory.md with a decided policy and a cited reason.
  • The no-ungoverned-operation guard: every actor-initiated operation either gated or carrying a justified inventory decision.
  • Read docs/decisions/permission-inventory.md — the map, and the product decisions it records (a dashboard is workspace-scoped; the v1 API inherits its in-app twin's permission; _test routes are a logged finding).