gabriel / musehub public
feat BREAKING #193 feat/193-collaborator-accept #1 / 1
AI Agent gabriel · 3 days ago · Sep 11, 2026 · Diff

feat(#193): accept-invitation mechanism for collaborators (REST + MCP)

Since the security/131-wire-write-authz merge, _guard_repo_owner (and the inline admin+ checks in this file) require accepted_at IS NOT NULL for any non-owner write -- but nothing anywhere could ever set it. invite_collaborator only writes invited_at. Every invited collaborator was permanently locked out of the very permission they were granted. This blocked aaronrene's entire workflow on both gabriel/musehub and gabriel/muse.

Added: - POST /repos/{repo_id}/collaborators/accept (REST) -- self-service, no target handle, accepts the invitation addressed to the caller's own authenticated identity. Idempotent: accepting an already-accepted invitation succeeds and returns the row unchanged. - musehub_accept_collaborator_invite (MCP tool + dispatcher branch + execute_accept_collaborator_invite executor) -- same semantics. - CollaboratorResponse and the MCP _collab_data() helper both gained an `accepted: bool` field (derived from accepted_at is not None) -- there was previously no way to see acceptance status in any collaborator response at all, not just no way to change it.

Fixed 3 pre-existing red tests in test_collaborators.py (test_admin_collab_can_invite, test_update_owner_permission_403, test_remove_owner_403) -- their fixtures gave the actor admin permission via _db_collab(..., permission="admin") but never accepted=True, so the accepted_at gate silently dropped the actor's permission before the test's real assertion. These were direct, executable proof of this exact gap; fixed by adding accepted=True to each fixture (they're testing invite/update/remove authorization, not accept itself).

Bumped the two hardcoded MCP tool-count regression guards in test_mcp_dispatcher.py (124 -> 125) for the new tool.

Tests (TDD, red confirmed before implementation): - tests/test_collaborators.py::TestE2EAcceptCollaboratorInvite (7 tests): sets accepted_at; the exact regression this ticket closes (admin invite unusable before accept, usable immediately after); idempotent re-accept; requires auth; 404 on no pending invite; 404 on unknown repo; owner never needs to accept anything (sanity guard). - tests/test_mcp_write_tools.py::TestIntegrationCollaborators (+5 tests): same coverage at the MCP executor layer.

68/68 in test_collaborators.py, 12/12 in test_musehub_collaborators.py, 250/250 across test_mcp_write_tools.py + test_mcp_dispatcher.py + test_protocol_introspection.py, 91/91 across test_musehub_openapi.py + test_api_contracts.py -- all green, no regressions.

Related: - musehub#131 -- the write-authorization fix that introduced this gate without a corresponding accept mechanism. - Client-side counterpart (`muse hub collaborator accept` CLI verb) shipped separately in the muse repo (dev @ f0e66c9b).

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:208cbdc4dd3500efe16cf0dd2e067fc0696f0568ca078b47224050586e1488f0 sha
+16 ~18 symbols
7 changed · 1073 in snapshot files
sha256:873a61ff377e69b7a66711e3dc8892a99d64188bc659b52ae9a4b644da0aa79f snapshot
+16
symbols added
~18
symbols modified
7
files changed
1073
files in snapshot
0
dead code introduced
Semantic Changes 34 symbols
~ musehub/api/routes/musehub/collaborators.py .py 1 symbol added, 2 symbols modified
+ accept_collaborator_invite function async_function accept_collaborator_invite L235–279
~ musehub/mcp/dispatcher.py .py 1 symbol modified
~ musehub/mcp/write_tools/collaborators.py .py 1 symbol added, 1 symbol modified
+ execute_accept_collaborator_invite function async_function execute_accept_collaborator_invite L391–460
~ tests/test_collaborators.py .py 8 symbols added, 6 symbols modified
+ TestE2EAcceptCollaboratorInvite class class TestE2EAcceptCollaboratorInvite L437–551
+ test_accept_is_idempotent method async_method test_accept_is_idempotent L495–506
+ test_accept_no_pending_invite_404 method async_method test_accept_no_pending_invite_404 L518–527
+ test_accept_pending_invite_sets_accepted_at method async_method test_accept_pending_invite_sets_accepted_at L440–463
+ test_accept_requires_auth method async_method test_accept_requires_auth L508–516
+ test_accept_unknown_repo_404 method async_method test_accept_unknown_repo_404 L529–535
+ test_accepting_grants_the_permission_immediately method async_method test_accepting_grants_the_permission_immediately L465–493
+ test_owner_row_is_not_created_or_needed_for_owner_actions method async_method test_owner_row_is_not_created_or_needed_for_owner_actions L537–551
~ tests/test_mcp_write_tools.py .py 6 symbols added, 5 symbols modified
+ test_accept_invite_is_idempotent method async_method test_accept_invite_is_idempotent L1606–1615
+ test_accept_invite_no_pending_invite method async_method test_accept_invite_no_pending_invite L1617–1623
+ test_accept_invite_requires_auth method async_method test_accept_invite_requires_auth L1625–1631
+ test_accept_invite_sets_accepted_true_and_unblocks_admin_actions method async_method test_accept_invite_sets_accepted_true_and_unblocks_admin_actions L1586–1604
+ test_accept_invite_unknown_repo method async_method test_accept_invite_unknown_repo L1633–1636
+ execute_accept_collaborator_invite import import execute_accept_collaborator_invite L115–115
Files Changed
~7
1073 in snapshot
← Older Oldest on feat/193-collaborator-accept
All commits
Newer → Latest on feat/193-collaborator-accept

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:208cbdc4dd3500efe16cf0dd2e067fc0696f0568ca078b47224050586e1488f0 --body "your comment"