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-2304Done

The guard's gate pattern is a NAME WHITELIST — three privately-named gates are invisible, and three domain cards are sized against the opposite of the truth

MOTIR-2292 fixed the guard's BODY EXTRACTOR and left its GATE PATTERN alone. That pattern is a whitelist of NAMES:

const GATE = /assertCan[A-Za-z]+|get[A-Za-z]*Capabilities|hasPermission\(|canManageProject\(|canBrowse\(|canEdit\(/;

A gate whose function is named anything else is invisible to it. Read on the branch, lib/services holds three such gates, and every operation behind them is reported UNGOVERNED:

HelperFileWhat it actually decides
assertBoardConfigAdminboardsService.ts:1138isOwnerRole(workspaceMembership.role)workspace OWNER only
assertProjectAdminworkflowsService.ts:47isOwnerRole(...)workspace OWNER only
assertEstimationAdminestimationService.ts:351isOwnerRole(...)workspace OWNER only

(sprintsService.assertSprintAdmin is a fourth, same shape, but its domain is MOTIR-2291's, not this story's.)

This is the MOTIR-2292 failure recurring one level up, and it is worse this time. There, the instrument under-reported gates and would have sent cards adding a second assertPermission in front of code that already asserted. Here it under-reports gates AND misdescribes their POLICY — so three cards under this story were written claiming "no project gate at all today… any signed-in workspace member can add or delete a column on any board they can reach… closing that is most of this card's value." The truth is the exact opposite: those operations are gated more tightly than project:administer. Re-pointing them to the new keys is a LOOSENING — a project admin and a workspace admin GAIN board / workflow / estimation configuration they cannot perform today.

And that loosening is now REQUIRED, not optional. MOTIR-2293 already put board:configure, workflow:manage and estimation:manage into BUILTIN_ROLE_PERMISSIONS.admin. If the gates stay workspace-owner-only, the catalog says a project admin holds those keys while the code refuses them — precisely the lie lib/permissions/catalog.ts's opening rule exists to prevent. So the split must widen them, and the domain cards must SAY SO instead of claiming neutrality.

Acceptance criteria

  • The GATE pattern stops being a name whitelist over lib/services: serviceBodies()'s walk resolves a call to a MODULE-LOCAL function in the same file and tests that function's body too, so a gate reached through assertBoardConfigAdmin / assertProjectAdmin / assertEstimationAdmin — or any future private helper — is seen. The existing cross-SERVICE hop (serviceCalls + methodIsGated) is unchanged; this adds the same-file hop it never had.
  • The the guard can actually fail block gains a control for THIS shape, both halves: a fixture method whose only authorization is a call to a module-local assertSomethingAdmin that itself reads a membership role reports gated, and a fixture method calling a module-local helper that asserts NOTHING still reports ungated. Without both halves the fix is indistinguishable from making the walk answer true more often — the same control MOTIR-2292 owed and paid.
  • docs/decisions/permission-inventory.md gains a GATE TODAY, MEASURED subsection recording, per administrative domain, the gate that actually runs and the actor set it admits: board / workflow / estimation = workspace OWNER only; automation / component / field / ai = project:administer-equivalent; label / repository = assertCanEdit (project member). The board section's six rows and the workflow, field, estimation sections' rows have their Gate today cells corrected to name the real helper — they currently read session only, which is false.
  • The document records that project:administer is NOT the tightest administrative gate in the product today, so this story's split both TIGHTENS some domains and LOOSENS others, and the per-domain card is where each is argued.
  • Both pinned counts are re-derived from the run on this branch and the commit message states the before/after pair. The PENDING count is expected to FALL (operations that were never ungoverned stop being counted) — a fall here is the instrument being corrected, not gates being added. This card adds no gate, removes no gate, and changes no route.
  • A diff that touches lib/services/** or app/api/** handler logic is out of scope and belongs to the domain card for that area.
  • pnpm test green; pnpm lint and the prettier check pass repo-wide (not over a scoped glob — a path with a route group in it matches nothing and exits 0).

Context refs

  • tests/permissions/noUngovernedOperation.test.tsGATE, serviceBodies(), methodIsGated, both pins, and the can actually fail controls.
  • lib/services/boardsService.ts:1138 · lib/services/workflowsService.ts:47 · lib/services/estimationService.ts:351 — the three invisible gates.
  • lib/services/componentsService.ts:109 · lib/services/customFieldsService.ts:141 — two MORE private copies of the policy, visible to the guard only because they happen to be named assertCanManage.
  • docs/decisions/permission-inventory.md — the board, workflow, field and estimation sections.
  • The guard-repair card this recurs from · the seam the domains re-point to.