Retire `repository:connect` — the provider connection binds an installation to a WORKSPACE, so no project permission can govern it
The inventory maps six operations to repository:connect under reason R7, "connects a provider installation and triggers indexing". Read on origin/main, all six turn out to have no project in them at all:
| Operation | What it resolves |
|---|---|
/api/github/oauth/start · /api/github/oauth/callback | getSession(); redirects to /settings/workspace/github |
/api/github/setup | getSession() + workspacesService; binds the installation to a WORKSPACE |
/api/github/organizations | getWorkspaceContext() |
/api/gitlab/oauth/start · /api/gitlab/oauth/callback | getSession() + getWorkspaceContext(); the file's own header says "WORKSPACE-scoped, so we resolve the acting member's active workspace" |
A project permission cannot gate an operation that never names a project. And the catalog's opening rule is explicit that a key exists because there is an operation it governs — "letting a user invent one would put a switch on a settings page that controls nothing" — so leaving repository:connect in place to be rendered by the Roles & permissions page is precisely the lie the model was built to prevent. It goes.
This does not leave the repository domain empty or the concern ungoverned: attaching a repository row TO a project is /api/projects/[key]/repositories, which is repository:manage and is wired by its own card. The split matches both mirrors — Jira and Plane put the provider connection at the org/workspace level and repository linking at the project level.
No route behaviour changes here. The six keep exactly the session / workspace gates they have; this card removes a key that was never wired and records the correct answer for those rows.
Acceptance criteria
- Confirm before removing. Open all six route files on the branch and confirm none resolves a project id (
getActiveProject, a[key]param, or aprojectsService.getByKey). If ANY of them does, STOP and record what you found on this card instead of forcing the retirement — the finding is worth more than the edit. repository:connectis removed fromPERMISSIONSandPERMISSION_METAinlib/permissions/catalog.ts. Therepositorydomain still holdsrepository:manageandrepository:manage_access, so the no-empty-domain guard is satisfied.messages/en.jsonandmessages/zh.jsondroppermissions.repository_connect.label/.descriptionin the same change — the catalog guard asserts both catalogs are TOTAL over the key set, and an orphaned pair is dead copy in two locales.- The six rows in
docs/decisions/permission-inventory.md'srepositorysection become decisionworkspace-scopedwith permission—, citingR3(the workspace/org administration reason). Their gate-today cells record what was actually read. If no row citesR7afterwards, deleteR7rather than leaving a reason nothing points at. - The catalog's own count sentence and the inventory's "The resulting catalog" table are updated to 31 keys, and the
repository (3)row becomesrepository (2). - The guard's pending pin falls by exactly these six operations — re-derived by running
tests/permissions/noUngovernedOperation.test.tson the branch, not copied from here, since the guard-repair card moves the same number. pnpm testgreen;pnpm lintand the prettier check pass repo-wide.
Context refs
app/api/github/oauth/start/route.ts·app/api/github/oauth/callback/route.ts·app/api/github/setup/route.ts·app/api/github/organizations/route.ts·app/api/gitlab/oauth/start/route.ts·app/api/gitlab/oauth/callback/route.ts— the six.lib/permissions/catalog.ts—PERMISSIONS,PERMISSION_META, the domain-non-empty guard, and the header rule this card applies.docs/decisions/permission-inventory.md— therepositorysection, the Reasons list (R3,R7), and the resulting-catalog table.messages/en.json·messages/zh.json— thepermissions.*namespace.tests/permissions/catalog.test.ts— the duplicate-key, domain-non-empty and i18n-totality assertions.- The inventory that mapped them · the guard repair.