Repository navigation
feat(collaboration): wire the collaboration UI onto the real backend (#16443) - #16462
Conversation
…nce WebSocket (#16455) WS /ws/sessions/{session_id}/presence took user_id as a bare, unverified query parameter and handed it straight to presence_websocket_handler. Any caller could join any session's presence channel as any user, see who was genuinely online, and broadcast under a spoofed identity. Mounted live via feature_routers.py -- not hypothetical. Now authenticates via auth_middleware.authenticate_websocket (the same primitive api/live_events.py's /ws/live already uses -- this endpoint's own docstring claimed that extension didn't exist yet; it does, elsewhere). Authenticated is not authorized: the verified caller must also be the session's owner or a listed collaborator (any permission level) per models.session_collaboration.SessionCollaboration, checked via a new _authorized_participant helper that owns its own DB session (a single seam tests can patch wholesale without needing a real database to exercise the surrounding auth flow). No frontend code calls this endpoint today (useSessionCollaboration.ts talks to a different, non-functional protocol entirely -- see #16443) and the sweep for the same user_id-as-Query pattern across every other WebSocket route in the backend found no other instance -- this was isolated to one file.
…16443) useSessionCollaboration.ts sent invented message shapes over globalWebSocketService's /ws/live channel, whose protocol only understands {action: "subscribe"|"unsubscribe"|"command"|"ping"} -- every message was silently dropped server-side (api/live_events.py's _handle_message logs "Unknown live-events action" at DEBUG and moves on). Rewritten onto what actually exists: REST (api/collaboration.py: invite/remove/participants/ share-secret) and the real presence WebSocket (api/presence_ws.py + websocket/presence.py's PresenceManager, secured by #16455), using its generic {"type":"broadcast","payload"} relay for live activity and secret-share notifications. Kept the composable's public return shape unchanged wherever the real backend could support it, which is why ParticipantList.vue, PresenceIndicator.vue, ActivityFeed.vue and SecretNotifications.vue needed no code changes at all -- only InviteUserDialog.vue (await the now-async inviteCollaborator) and ShareSecretDialog.vue (participant-id-aware shareSecretWithSession; dropped the "expires in" selector, which Secret.share_with() has no concept of at all) changed. api/collaboration.py's share_secret_with_session now also calls presence_manager.broadcast_to_session() after it commits, so connected participants see the share live -- id/name/sharer only, never the secret value (pinned by test_share_secret_broadcasts_id_name_and_sharer_only). Fixed along the way: every component calling useSessionCollaboration() registered its own onScopeDispose -> leaveSession(), harmless under the old fake transport but a real bug now that the socket is real -- the first mounted consumer to unmount would close it out from under every other one still mounted. Reference-counted instead; the socket closes only when the last instance disposes. Retired useActivityTracking.ts (#608/#874): zero real callers, and its distinct value (local activity tracking + auto-broadcast) is only useful bundled with a caller that wants both, which none currently does -- useSessionActivityLogger (the local half) remains independently available. New: ChatCollaborationPanel.vue, a togglable right-side panel in ChatInterface.vue (mirrors the existing ChatFilePanel pattern), shown for session.mode === 'collaborative', hosting ParticipantList/ActivityFeed/ SecretNotifications with InviteUserDialog as a modal. Not in this PR (#16460, filed separately): activity/notification history is live-only -- nothing is persisted, so a reconnecting client sees nothing before it joined. pendingInvitations/respondToInvitation are dropped from the composable entirely rather than stubbed: there is no REST endpoint to list or respond to a pending invitation despite the model already supporting it (SessionCollaboration.add_invitation/remove_invitation). Refs #16429 (ShareSecretDialog AC)
…to issue-16443-collaboration-ui-wiring
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ain (#16464) session_collaborations, terminal_activities, file_activities, browser_activities, desktop_activities and secret_usage are all declared in real ORM models (models/session_collaboration.py, models/activities.py) but were never reachable via `alembic upgrade head` -- they only existed in autobot-backend/database/migrations/, which migrations/alembic.ini does not point at (script_location = migrations, version_locations = migrations/versions). migrations/schema_bootstrap.py confirms there is no silent create_all() fallback against a real database either. Same class of bug as #10750 A2, which found and fixed 8 LLC tables in exactly this state. Highest-impact finding: every one of the five activity/secret_usage tables carries ForeignKey("users.id", ondelete="CASCADE"), and users.org_id is itself ForeignKey("organizations.id", ondelete="CASCADE"). A missing CASCADE target doesn't just make the table unreachable -- it makes hard-deleting a user, or an organization (which cascades into its users), fail outright against any properly-migrated database. Not hypothetical: confirmed on a live install (37's probe entities). Two new tests (tests/migrations/test_cascade_delete_orphaned_tables_16464.py) exercise exactly that at the database level and pin it fixed. session_collaborations is what api/collaboration.py's entire REST API depends on (#16429/#16443/#16455/#16462) -- every one of those routes 500'd (UndefinedTableError) against a real database until this migration. Also added: - Both new concepts (session_collaboration, activity_audit_trail) declared in autobot_shared/store_authority.py, with a SYSTEM_OF_RECORD = system_of_record(...) call at each model file's module level. - repo_tests/orphaned_database_migrations_test.py: a general guard that fails the moment a NEW table lands in database/migrations/ without a matching CREATE TABLE in migrations/versions/, so this class of defect can't recur silently and get found by hand again. Deliberately excluded: database/migrations/001_create_conversation_files.py looks like the same bug at a glance (same orphaned directory) but manages its own dedicated SQLite file via a self-contained, live-invoked migration runner -- checked and confirmed unrelated. Follow-up filed: #16466 (3 of the 5 activity-tracking backend modules -- terminal/file/browser -- have zero real callers; wire or retire). Migration is fully additive (CREATE TABLE / CREATE INDEX only, nothing dropped) and idempotent (has_table()-guarded). Deliberately fully inlined per-table rather than a shared helper, matching #10750 A2's own style -- repo_tests/orphaned_database_migrations_test.py reads table names via AST from literal op.create_table("name", ...) calls, and a name passed through a helper's parameter is invisible to that scan.
✅ SSOT Configuration Compliance: Passing🎉 No new hardcoded values of either class — Known backlog in |
|
Bug in this head's own commit fixed in the 2d vehicle: #16702, commit 3a13531.
|
- SecretsManager.vue:238 shareSecret(secret) referenced an undeclared identifier; every sibling action button uses item.data (#16462's own reviewed head, reproducible standalone). - workflow.ts was stale for the mcp.external permission #16458 added to permissions.py without regenerating (#16458's own reviewed head, reproducible standalone). Regenerated via gen_frontend_types.py. - OrchestrationView.vue's redisSystemdService read systemd_service as a scalar; #16025 (already on main, not in #16352's base) made it a sequence. Redis owns exactly one unit, so take [0]. This one is a genuine cross-PR integration conflict, not a defect in either PR alone.
#16702) - SecretsManager.vue:238 shareSecret(secret) referenced an undeclared identifier; every sibling action button uses item.data (#16462's own reviewed head, reproducible standalone). - workflow.ts was stale for the mcp.external permission #16458 added to permissions.py without regenerating (#16458's own reviewed head, reproducible standalone). Regenerated via gen_frontend_types.py. - OrchestrationView.vue's redisSystemdService read systemd_service as a scalar; #16025 (already on main, not in #16352's base) made it a sequence. Redis owns exactly one unit, so take [0]. This one is a genuine cross-PR integration conflict, not a defect in either PR alone.
Thinking Path
#16429's triage found
ShareSecretDialog.vuedepended onuseSessionCollaboration.ts, whose other 5 sibling components incomponents/collaboration/were themselves unwired — a full real-time collaboration UI (#608 phases 5-7, closed / #874 phase 6) built and never connected. Investigating what backend each composable call actually needed turned up two things: first,useSessionCollaboration.tswas talking entirely to an invented protocol — every message it sent ({type: "session_join", ...}) went overglobalWebSocketService's generic/ws/livechannel, whose real protocol only understands{action: "subscribe"|"unsubscribe"|"command"|"ping"}(api/live_events.py), so every one of its messages was silently dropped, logged at DEBUG, never delivered. Second, and more surprising: a real, complete backend for this feature already existed and was simply never called —api/collaboration.py's REST API (invite/remove/list-participants/share-secret, backed by a realsession_collaborationstable with owner/editor/viewer permissions) andwebsocket/presence.py'sPresenceManager(a working join/leave/broadcast WebSocket handler). Tracing that WebSocket's route (api/presence_ws.py) surfaced a live, unauthenticated identity-spoofing vulnerability, fixed separately and urgently in #16455 (merged into this branch so it's self-consistent — GitHub will stop showing those commits here once #16456 lands onDev_new_gui).What Changed
autobot-frontend/src/composables/useSessionCollaboration.ts: rewritten onto the real backend. REST (via newapiServicemethods:inviteToSession,removeFromSession,shareSecretWithSession,getSessionPresence) for invite/remove/share; a realWebSocketto/ws/sessions/{id}/presencefor join/leave presence and a generic{"type":"broadcast","payload"}relay for live activity and secret-share notifications. The composable's public return shape was kept unchanged wherever the real backend could support it.autobot-frontend/src/components/collaboration/ParticipantList.vue,PresenceIndicator.vue,ActivityFeed.vue,SecretNotifications.vue: no code changes — each already consumed only the composable's public API, which is why keeping that shape stable mattered.autobot-frontend/src/components/collaboration/InviteUserDialog.vue:inviteCollaborator()is now async — added theawaitits own call site was missing.autobot-frontend/src/components/secrets/ShareSecretDialog.vue:shareSecretWithSession()now takes(secretId, participantIds?)and is awaited; the dialog only closes on real success. Dropped the "expires in" selector —Secret.share_with()has no expiry concept at all, so it had zero effect (same class of finding as security(secrets): Visibility/Organization/Team/Shared-With controls in SecretsManager.vue are not enforced server-side #16450).autobot-backend/api/collaboration.py:share_secret_with_sessionnow also callspresence_manager.broadcast_to_session()after it commits — connected participants see the share live. Payload carries id/name/sharer only, never the secret's value (pinned by a new test).autobot-frontend/src/components/chat/ChatCollaborationPanel.vue(new): a togglable right-side panel, mirroring the existingChatFilePanelpattern exactly, hostingParticipantList/ActivityFeed/SecretNotificationswithInviteUserDialogas a modal. Wired intoChatInterface.vue's header + right-side panel slot, shown only whensession.mode === 'collaborative'.autobot-frontend/src/composables/useActivityTracking.ts: retired. Zero real callers; its distinguishing feature (local activity tracking bundled with auto-broadcast) has no current consumer wanting that specific combination —useSessionActivityLogger(the local half) remains independently available to anyone who does.collaboration.panel.*(title, invite, closePanel, tabParticipants, tabActivity, tabNotifications) with real translations across all 11 locales.Verification
pytest api/collaboration_test.py api/presence_ws_auth_16455_test.py api/presence_ws_router_test.py -v→ 29 passed, including a new test that pins the secret-share broadcast payload never carries the secret's value.useSessionCollaboration.test.ts: connects to the real presence WS (not/ws/live); a dedicated test asserts nothing this composable sends is ever shaped like one of the 9 old fake message types; presence_sync/user_joined/user_left/user_message(secret_shared|activity) are each exercised against realistic backend payload shapes; invite/share REST calls verified with mockedapiService.ShareSecretDialog.test.ts: confirms the expiry selector is gone, confirmsshareSecretWithSessionis called with the selected participant ids, confirms the dialog stays open (nosharedemit) on a failed share.node_modulesisn't installed in this environment (pre-push confirms:vue-tscskipped) — could not runvitest/vue-tsclocally; reasoned through the test files carefully (tracedMockWebSocket's connect/readyState timing, i18n string matches,detect-secretsfalse positives on test fixture data) but CI is the first real execution.useSessionCollaboration()registered its ownonScopeDispose, unconditionally closing the shared presence socket — harmless while the transport was fake, a real bug now that several components share one real connection. Fixed with reference counting (socket closes only when the last mounted consumer disposes).Risks
UserPresence.usernamefalls back to the rawuser_id— neitherGET .../participantsnor the presence WebSocket returns a display name for anyone but the caller. Filed as part of feat(collaboration): persist activity/notification history and add invitation list+respond endpoints #16460.pendingInvitations/respondToInvitationare removed from the composable entirely (no REST endpoint exists to back them, despite the model already supporting invitations) rather than shipped as a stub. Restored once feat(collaboration): persist activity/notification history and add invitation list+respond endpoints #16460 adds the endpoints.Dev_new_guiand this branch is rebased/re-merges cleanly.Model Used
Claude Sonnet 5
Refs #16429 (ShareSecretDialog AC)
Closes #16443