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

`field:manage` + `component:manage` + `label:manage` + `estimation:manage` — the four project vocabularies, with their READS left where they are

⚠️ Re-scoped 2026-08-06 against the branch — "the ungated ones" were not ungated

The card claimed /api/fields/[fieldId], its two options routes and /api/projects/[key]/estimation-config reach their service "with workspace membership alone." Read on the branch, all four are gated; three of them at exactly the right level and one of them TIGHTER than project:administer:

VocabularyReal gate todayAdmitsEffect of the split
fieldcustomFieldsService's module-private assertCanManage (line ~141)workspace manager OR project adminNEUTRAL
componentcomponentsService's module-private assertCanManage (line ~109)workspace manager OR project adminNEUTRAL
labelprojectAccessService.assertCanManageproject:administerNEUTRAL
estimationassertEstimationAdmin (estimationService.ts:351)isOwnerRole(...)workspace OWNER onlyLOOSENS

The guard reported the estimation route ungoverned because its GATE pattern is a whitelist of names and assertEstimationAdmin is not one — MOTIR-2304 repairs it and this card is blocked_by it. The field and component helpers escaped only because they happen to be named assertCanManage; they are two more private copies of the access policy, and this card deletes them.

So three of the four keys are the neutral re-pointing the story describes, and estimation:manage is a deliberate LOOSENING — the estimation scheme moves from workspace-OWNER-only to a key a workspace admin and a project admin also hold. That is required, not optional: MOTIR-2293 already put estimation:manage in BUILTIN_ROLE_PERMISSIONS.admin, so leaving the gate where it is would have the catalog advertise a permission the code refuses.

Four keys, four services, one idea: the vocabularies a project DEFINES. Custom fields, components, labels and the estimation scheme are each a shared list that everyone's work items then draw from, and defining them is administrative. estimation:manage is its own catalog domain and rides here rather than in a card of its own because it is one route and one service — a separate pull request for it would be more overhead than work.

  • field:managecustomFieldsService (create / rename / reorder / delete a field, and its options)
  • component:managecomponentsService
  • label:manageprojectTagsService (the project's label / tag vocabulary)
  • estimation:manageestimationService.updateEstimationConfig

Three of the four are a re-pointing at an equivalent gate; estimation is the one that moves — see the re-scope above for the measured gate behind each.

⚠️ The reads are the trap, and getting them wrong breaks the issue editor for every member. GET /api/projects/[key]/fields, /labels, /tags, /components and /estimation-config are how the create/edit form knows what a member may CHOOSE. The inventory maps each of them to the domain's manage key, and applying that literally would make it impossible for a non-admin to set a component or pick a label. Every read in this domain stays where it isassertCanBrowse, or project:browse under the new vocabulary — and the inventory rows are corrected to say so.

⚠️ And the second trap, one level down: /api/work-items/[id]/labels and /api/work-items/[id]/components are not this card's. APPLYING a label to a work item is work_item:edit; DEFINING the label is label:manage. The inventory already has that right; do not let the file names blur it.

Acceptance criteria

  • Enumerate first, against the REPAIRED guard (MOTIR-2304 is blocked_by): run it for these four keys, and git grep -n assertCanManage -- lib/services/customFieldsService.ts lib/services/componentsService.ts lib/services/projectTagsService.ts lib/services/estimationService.ts. The PR body carries operation / gate today / gate after for every row in the field and estimation sections.
  • Every vocabulary-DEFINITION write asserts its domain key through projectAccessService.assertPermission, threading tx where the method already runs inside a transaction. The module-private assertCanManage in customFieldsService and in componentsService, and assertEstimationAdmin in estimationService, are all DELETED — three more private copies of the access policy, one of which decides a different answer.
  • The estimation WIDENING is asserted, not just performed. A test proves it: a project admin can now update the estimation scheme (they could not before this card), a workspace admin can too, and a project member is still refused. The other three keys assert NEUTRALITY instead — the actors who could define a field, a component or a label before can after, and no one else can.
  • Every vocabulary READ keeps a browse-level gate. The card's tests assert this directly: a project member can still list fields, labels, components and the estimation config, and can still apply a label and a component to a work item.
  • The four keys flip to enforcement: 'enforced' in lib/permissions/catalog.ts.
  • The inventory's field and estimation sections carry corrected gate-today / gate-after cells; every read row is marked project:browse with a one-line reason rather than the domain's manage key, and the rows this card wires move newexisting.
  • Tests: an admin defines and deletes a field, an option, a component, a label and updates the estimation scheme; a project member is refused 403 on each; a non-browser gets 404.
  • The guard's pending pin is re-derived by running the suite on the branch.
  • pnpm test green; pnpm lint and the prettier check pass repo-wide.

Context refs

  • lib/services/customFieldsService.ts · lib/services/componentsService.ts · lib/services/projectTagsService.ts — each already calls assertCanManage on some paths; the enumeration is which, and which are missing it.
  • lib/services/estimationService.tsgetEstimationConfig (read) and updateEstimationConfig (write).
  • lib/services/customFieldValuesService.ts — the VALUE writes, which are work_item:edit and out of scope; named here so the boundary is legible.
  • app/api/fields/** · app/api/projects/[key]/fields/route.ts · .../labels/route.ts · .../tags/route.ts · .../components/route.ts · .../estimation-config/route.ts.
  • docs/decisions/permission-inventory.md — the field and estimation sections; reasons R20, R24, R25, R26.
  • The seam this calls · the model story.