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

Close the ungoverned-operation gap — the member-facing permissions, and the gates the guard cannot confirm

The permission MODEL shipped a catalog derived from the real operation surface, and its inventory proved why it was needed: the eleven predicates the product had been asking its access questions through were one corner of the enforcement surface, not the whole of it. Walking all 252 routes found operations that reach the database with no project gate at all — a signed-in workspace member, a project viewer included, can rank the backlog, run the planner, read a burndown and drive an importer.

The administrative split took twelve of those keys — the ones that fall out of project:administer — and it could promise the built-in roles' behaviour was unchanged wherever the umbrella already stood. This story takes the remaining eight, and it cannot make that promise: these operations are governed by nothing today, so wiring them REMOVES capability from real people. That is the right outcome and it is a decision, not a re-pointing, which is why the two are separate stories.

⚠️ Re-measured 2026-08-07 on origin/main (44e55eff) — every number below replaces one this card carried before

This story was written before the extractor repair and the name-whitelist repair landed, and it quoted 56 pending and 33 unconfirmed. Both are wrong now, in the direction that would have over-sized every card underneath: the guard's own walk was under-reporting gates, and MOTIR-2256's twelve keys have since been wired. Re-derived by running the guard's walk against origin/main:

thennow
Ungated operations awaiting a key (PENDING)95 → 81 → 5636
Operations the inventory calls existing that the walk cannot confirm38 → 3318

Both figures are context, not a specification. Every card below re-derives its own list by running pnpm test tests/permissions/noUngovernedOperation.test.ts on its branch, because the cards move them as they land — the lesson MOTIR-2305 was filed to record.

The eight keys

The right-hand columns are what the inventory MAPS to each key, and how many of those reach the database today with no gate at all. The two are different sizes of work and both are this story's: an ungated row needs a gate added; a mapped-but-gated row is governed by a generic assertCanBrowse / assertCanEdit and needs RE-POINTING at its specific key, which no count in the guard can see.

KeyWhat it governsMapped opsUngated today
ai:plansubmitting and watching a planning job — chat, expand, augment, replan, generate, sprint-plan, explanation, pre-plan, the coding-convention audit32 routes + 1 action file19
sprint:managethe sprint lifecycle, the backlog rank, and a work item's sprint assignment138
import:runthe external-tracker importers and their OAuth flows117
report:viewthe project-scoped analytics reads (roadmap, velocity, the six /api/reports)81
saved_filter:manageproject-scoped saved queries, their stars and subscriptions51
ai:view_planreading a generated plan and acting on it — approve, decline, edit a proposal40
work_item:triagethe triage queue — accept / decline / promote an inbound request20
work_item:deletearchive / delete cascades over a subtree, separable from editing a field20

The decision this story owes

Every key above needs an answer to which built-in role holds it, and for most of them the answer is not obvious from the shipped behaviour — because the shipped behaviour is "everyone". sprint:manage at member matches Plane's Contributor and Jira team-managed, and takes sprint control away from a viewer who has it today. ai:plan spends the workspace's AI credits, which argues against the implicit grant a workspace member holds with no project membership. report:view may be as wide as project:browse — Jira has no separate report permission at all. import:run is a destructive-scale bulk write and reads as admin in both mirrors. Each answer is a capability somebody loses on the day it lands, so it is settled once, in a decision subtask, against the mirror products and this project's own access levels — never per-route while wiring.

One row is a LOOSENING rather than a tightening and must be caught: the four /api/ai/coding-convention/* operations are gated by assertCanManage today (aiConventionService.getAudit / getConvention / reaudit each assert it), and the inventory maps them to ai:plan. Moving them from admin-only to member widens who may spend credits. Either the mapping is wrong or the widening is intended, and this story says which — on its own card, so the one change that gives capability away is not buried among fifteen that take it back.

Two findings this decomposition surfaced, and neither is a permission

A job is readable by its id alone, on both sides of the boundary. app/api/ai/jobs/[jobId]/route.ts resolves the actor's active project and then calls getJob(jobId) — the client sends the SERVICE token and nothing else, and motir-ai's GET /v1/jobs/:id answers getJobView(id) with no tenant filter, returning a JobView of { jobId, status, result, error } that carries no owning project for motir-core to check. The eight [jobId]/stream routes are the same passthrough. So the fix is not a permission at all: the binding motir-ai already holds (PlanJob.aiProjectIdAiProject.coreProjectId) has to be enforced on the read, exactly as GET /v1/usage already requires the core ids "so a foreign id can't widen the rollup". That is one motir-ai card and one motir-core card, wired.

Nine mapped rows resolve no project, so no project permission can govern them — the six importer OAuth legs (resolveWorkspaceContext, no project, same shape as the repository:connect retirement in MOTIR-2294), the two /api/idea-draft operations (pre-auth, cross-origin, deliberately session-less), and /api/canvas-layout (a per-user arrangement inside a project, not a planning act). They get a decided disposition first, before anything downstream is sized against them.

The 33 → 18 unconfirmed gates

The no-ungoverned-operation guard pins a second bucket: operations the inventory calls existing — already governed by a shipped predicate — that its static walk cannot confirm. Either the gate is real and reached by a hop the walk does not follow, or the operation is genuinely ungoverned; from outside those look identical, and the difference is a security hole. After both instrument repairs the bucket is 18, and every one is this story's: the work-item, watcher and attachment paths (9) and the public, ready-set and upload paths (9). Each needs a human read ending in one of two places — the gate is real (record HOW it is reached, and teach the walk to see it), or it is not (that is a hole, and the fix is a gate).

Scope boundary

In: the role assignment decision for the eight keys; the eight keys entering the built-in role sets; wiring or re-pointing every operation the inventory maps to them; the tenant-scoped job read on both sides of the boundary; the 18 unconfirmed-gate reads and whatever gates they turn out to need; the inventory rows and the guard's pinned counts reaching zero; the story's two test subtasks.

Ends at: the server, and at these eight keys. The twelve administrative keys are MOTIR-2256's. What the UI SHOWS an actor who lacks one of these is the permission-gated UI story's — nothing here hides a control, and a refused write surfaces as whatever the shipped client already does with a 403. Choosing a subset of these keys is custom roles. Rendering them is the Roles & permissions page. Nothing here touches the workspace MemberRole axis or API-token scopes.

Verification recipe

  • Pull the story branch, pnpm install, pnpm dev.
  • As a project viewer, open the backlog and try to move an item into the active sprint — it is refused. Today it succeeds.
  • As a project member, run a planning job from the planning workspace — it still works; the same call from an account with no project membership is refused.
  • As a project member, open the importer wizard — it is refused; as an admin it still runs.
  • As a project admin, every flow above works exactly as before.
  • pnpm test tests/permissions/PLANNED_PERMISSIONS is empty, the guard's PENDING arm is deleted rather than re-pinned, and the claimed-but-unverified bucket is zero.

This story has no user-visible surface of its own — it adds no page, panel or control, and the visible difference is a refusal the shipped client already renders — so it is exempt from the acceptance-video rule and accepts on its tests plus the walk above.