gabriel / musehub public
Closed #193 Bug
filed by gabriel human · 34 days ago

Critical: no accept-invitation mechanism — accepted_at gate from #131 permanently locks out all invited collaborators

0 Anchors
— Blast radius
— Churn 30d
0 Proposals

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_collaborator only writes invited_at, never accepted_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 route
  • muse 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).

  • #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."

◎ Activity1
●
gabriel opened this issue 34 days ago
✓
closed this issue 34 days ago
○
gabriel 34 days ago

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_invite MCP tool (same semantics).
  • CollaboratorResponse and the MCP collaborator payload both gained an accepted: bool field — there was previously no way to even see acceptance status.
  • Fixed 3 pre-existing red tests that were direct, executable proof of this exact gap (test_admin_collab_can_invite, test_update_owner_permission_403, test_remove_owner_403 — their fixtures granted admin permission but never accepted=True).
  • muse hub collaborator accept CLI verb shipped in the muse repo (client-side counterpart).

68/68 in test_collaborators.py, 12/12 in test_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 real hub collaborator list call.

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_at directly via SQL on both his collaborator rows (gabriel/musehub and gabriel/muse) on staging. Confirmed live: both now report "accepted": true, permission unchanged (admin on musehub, write on 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.