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]>
Semantic Changes
34 symbols
Files Changed
~7
1073 in snapshot
0 comments
muse hub commit comment sha256:208cbdc4dd3500efe16cf0dd2e067fc0696f0568ca078b47224050586e1488f0 --body "your comment"
No comments yet. Be the first to start the discussion.