Critical: no accept-invitation mechanism — accepted_at gate from #131 permanently locks out all invited collaborators
Summary
Since the Sep 9 security/131-wire-write-authz merge (#131), every push from a non-owner collaborator returns 403 — including aaronrene's, on both gabriel/musehub and gabriel/muse. This is currently blocking all of aaronrene's work, including the fix for this exact ticket, since shipping it requires a push.
Root cause
accepted_at on collaborator rows gates all non-owner writes (per #131's fix). aaronrene's rows are admin, invited by gabriel — but accepted_at is NULL, and nothing in MuseHub can set it:
- No accept route (REST)
- No MCP tool
- No CLI verb
invite_collaboratoronly writesinvited_at, neveraccepted_at
So an invited collaborator has no way to ever transition to "accepted," and #131's new gate — correct in intent — permanently locks out every invited collaborator who was invited before an accept mechanism existed.
Immediate unblock
gabriel can set accepted_at directly via SQL on aaronrene's two collaborator rows (musehub, muse) to unblock him today while the real fix lands.
Real fix
Add an actual accept-invitation path — at minimum one of:
POST /repos/{repo}/collaborators/accept(or similar) REST routemuse hub collaborator accept <repo>CLI verb- Corresponding MCP tool
Should set accepted_at = now() on the invited row, scoped to the authenticated identity matching the invited handle (an identity must not be able to accept someone else's invitation).
Related
- #131 — the write-authorization fix that introduced this gate without a corresponding accept mechanism.
Source
Reported by aaronrene, 2026-09-10 (relayed via message to gabriel, processed 2026-09-11). Original message: "Could you set accepted_at on my collaborator rows for gabriel/musehub and gabriel/muse? ... The invite can't be accepted, and I can't ship the fix for that either, because it needs a push. Details on #131."
Fixed and deployed ✅
Real fix (REST + MCP), TDD, shipped to staging:
POST /repos/{repo_id}/collaborators/accept— self-service, no target handle, accepts the invitation addressed to the caller's own identity. Idempotent.musehub_accept_collaborator_inviteMCP tool (same semantics).CollaboratorResponseand the MCP collaborator payload both gained anaccepted: boolfield — there was previously no way to even see acceptance status.test_admin_collab_can_invite,test_update_owner_permission_403,test_remove_owner_403— their fixtures granted admin permission but neveraccepted=True).muse hub collaborator acceptCLI verb shipped in themuserepo (client-side counterpart).68/68 in
test_collaborators.py, 12/12 intest_musehub_collaborators.py, 250/250 across the MCP write-tools/dispatcher/introspection suites, 91/91 across OpenAPI/contract tests — all green, no regressions. Deployed to staging, verified live via a realhub collaborator listcall.Immediate unblock applied: since accept is deliberately self-service (only the invited identity can accept their own row — by design, not a bug), I couldn't accept on aaronrene's behalf through the new mechanism. Set
accepted_atdirectly via SQL on both his collaborator rows (gabriel/musehubandgabriel/muse) on staging. Confirmed live: both now report"accepted": true,permissionunchanged (adminon musehub,writeon muse).aaronrene should be unblocked now — pushes to both repos should work. Going forward, any newly invited collaborator can self-serve via
POST /repos/{repo_id}/collaborators/accept, the MCP tool, or (once a new muse release ships it)muse hub collaborator accept.