`repository:manage` + `repository:manage_access` — the project's repository SET, and who may clone it (self-connect stays open)
A project has a repository SET — as many repos as its architecture needs, not one — and deciding what is in it is administrative. repository:manage gates the set: add a row, remove it, move it to another project, take it over, establish a real repo behind a proposed row. repository:manage_access gates the separate question of who on the team may CLONE the code.
Today /api/projects/[key]/repositories, .../[rowId]/move, .../[rowId]/takeover and .../establish reach their services with workspace membership alone, and .../[rowId] is on assertCanEdit — so a plain member can currently detach a project's repository. The code-access routes are ungated in the same way.
⚠️ Two boundaries this card must not cross.
- Self-connect stays open to a non-admin.
lib/settings/projectSettingsNav.tsrecords the reason on thecode-accessentry: connecting your OWN GitHub identity is the one action nobody can take on your behalf (project-repository-set ADR §3 Q3), which is why that page is browse-gated.repository:manage_accessgates granting or revoking access for other people; it must not gate a member connecting themselves. ReadprojectRepoAccessServicemethod by method and say, in the PR, which side each one is on. - A provisioning callback is not an actor.
.../[rowId]/statereachesprojectRepoSetService.attachRealizedRepo. If that path is driven by a job or a provider callback rather than a person, it is ano-gaterow in the inventory (theserviceAuthfamily), not arepository:manageone. Determine which before gating it — putting a project permission in front of a machine caller breaks provisioning silently, and it fails in the environment where nobody is watching.
Acceptance criteria
- Enumerate first: run the guard on the branch for both keys, and
git grep -ln "projectRepo" -- lib/servicesto reach every service in the family. The PR body carries operation / gate today / gate after for every row in the inventory'srepositorysection, including the ones this card decides NOT to move. - Every repository-SET write — add / remove / move / takeover / establish, wherever they live across
projectRepoSetService,projectRepoEstablishService,projectRepoTakeoverServiceand their siblings — assertsrepository:managethroughprojectAccessService.assertPermission, threadingtxwhere the method already runs inside one. - Granting or revoking ANOTHER member's code access asserts
repository:manage_access. A member connecting their own identity does not, and a test asserts that directly. - Reads — the repositories view, the establish view, the code-access table — keep a browse-level gate. The Repositories and Code access nav entries stay browse-gated; a member still SEES where the code lives.
.../[rowId]/stateis classified by who calls it, and its inventory row records the answer with the evidence.- Both keys flip to
enforcement: 'enforced'inlib/permissions/catalog.ts. (repository:connectis retired separately; if that card has not landed, do not touch its key here.) - The inventory's
repositoryrows carry corrected gate-today / gate-after cells and movenew→existingfor the ones this card wires. - Tests: an admin adds, moves, takes over and detaches a repository row and grants a teammate code access; a project member is refused 403 on each; a member can still view the set and still connect their own identity; a non-browser gets 404.
- The guard's pending pin is re-derived by running the suite on the branch.
pnpm testgreen;pnpm lintand the prettier check pass repo-wide.
Context refs
lib/services/projectRepoSetService.ts·projectRepoEstablishService.ts·projectRepoTakeoverService.ts·projectRepoAccessService.ts·projectRepoPinService.ts·projectRepoProvisioningService.ts·projectRepoProposalService.ts·projectRepoRoomService.ts— the family; the enumeration decides which of them this card actually touches.app/api/projects/[key]/repositories/**— the set routes plusaccessandaccess/team.lib/settings/projectSettingsNav.ts— therepositoriesandcode-accessentries and the ADR reasoning recorded in their comments (read only; this card does not edit the nav).docs/decisions/project-repository-set.md— §3 Q3 (self-connect) and the repo-SET model.docs/decisions/permission-inventory.md— therepositorysection; reasons R21, R22.- The seam this calls · the
repository:connectretirement.