The nine rows no project permission can govern — the importer OAuth legs, the pre-auth idea draft, and the canvas layout
Nine operations the inventory maps to import:run or ai:plan resolve no project, so no project permission can ever govern them. This is the repository:connect retirement one story over: a key pointed at an operation that never names a project is the lie the catalog's opening rule forbids, and a count that includes those rows sends every card underneath looking for gates that cannot exist. So this card runs first and gives each row a DECIDED disposition, before anything downstream is sized against it.
Read on origin/main (44e55eff), one file each:
| Rows | What the code does | Disposition |
|---|---|---|
/api/import/{jira,linear,plane}/oauth/{start,callback} (6) | resolveWorkspaceContext(req) and nothing else — the 3LO round trip binds the actor's stored provider credential to a WORKSPACE; app/api/import/jira/oauth/start/route.ts never mentions a project | workspace-scoped / R3, exactly as MOTIR-2294 decided for the GitHub and GitLab legs |
/api/idea-draft, /api/idea-draft/[id]/claim (2) | the PUBLIC cross-origin pre-auth receiver — origin-allowlisted, per-IP rate-limited, "deliberately no getSession() call" in its own header; the claim leg binds a draft to the signing-in user, still with no project | user-scoped (the claim) / no-gate (the anonymous POST), each argued in the Reasons section |
/api/canvas-layout (1) | getActiveProject() then a per-user, per-project node arrangement — the actor's own view state inside a project they already have open, not a planning act that spends anything | re-map to project:browse and ADD the assert, so it stops being ungated without pretending it is an AI operation |
Nothing here weakens a gate: eight rows are re-decided as operations a project role was never the right axis for, and the ninth gains a gate it does not have today. The importer's project-scoped operations stay import:run and are wired by their own card; attaching an imported project's work is not what the OAuth leg does.
Acceptance criteria
docs/decisions/permission-inventory.mdcarries the new decision for each of the nine rows, and each cites a Reason in the Reasons section — a new reason is added where none of the existing ones fits, rather than stretching one.- The six importer OAuth rows read the same decision + reason as the GitHub / GitLab legs MOTIR-2294 re-decided, and the document says why the two sets are the same shape.
canvasLayoutService.getLayoutandsavePositionseach assertprojectAccessService.assertPermission(projectId, ctx, 'project:browse'), and a unit test drives a non-browsing actor into the typed refusal.- The
import:runandai:planrows the inventory still maps are re-counted in the document's own totals, and the counts intests/permissions/noUngovernedOperation.test.tsare re-derived by RUNNING it on the branch, not by arithmetic on this card — the pinnedPENDINGfigure falls by exactly the rows this card moves plus the one it gates. pnpm test tests/permissions/is green, and the guard's "every pending operation names the permission that will govern it" assertion still holds for every remaining row.- No permission key is added, removed or re-labelled in
lib/permissions/catalog.ts.
Context refs
docs/decisions/permission-inventory.md— the rows and the Reasons section this card edits.tests/permissions/noUngovernedOperation.test.ts— the pinnedPENDINGcount and thePERMANENTLY_UNGATEDdecision set.app/api/import/jira/oauth/start/route.ts,app/api/import/jira/oauth/callback/route.ts— the workspace-context resolution to read before writing the disposition.app/api/idea-draft/route.ts— the pre-auth receiver's own header comment.app/api/canvas-layout/route.ts,lib/services/canvasLayoutService.ts— the one row that gains a gate.- MOTIR-2294 — the precedent: what a retirement-by-disposition looks like in this document.
- The operation inventory — where the original mapping came from.