Skip to content

feat(collaboration): wire the collaboration UI onto the real backend (#16443) - #16462

Merged
mrveiss merged 5 commits into
mainfrom
issue-16443-collaboration-ui-wiring
Sep 14, 2026
Merged

mrveiss merged 5 commits into
mainfrom
issue-16443-collaboration-ui-wiring

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

#16429's triage found ShareSecretDialog.vue depended on useSessionCollaboration.ts, whose other 5 sibling components in components/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.ts was talking entirely to an invented protocol — every message it sent ({type: "session_join", ...}) went over globalWebSocketService's generic /ws/live channel, 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 real session_collaborations table with owner/editor/viewer permissions) and websocket/presence.py's PresenceManager (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 on Dev_new_gui).

What Changed

  • autobot-frontend/src/composables/useSessionCollaboration.ts: rewritten onto the real backend. REST (via new apiService methods: inviteToSession, removeFromSession, shareSecretWithSession, getSessionPresence) for invite/remove/share; a real WebSocket to /ws/sessions/{id}/presence for 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 the await its 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_session now also calls presence_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 existing ChatFilePanel pattern exactly, hosting ParticipantList/ActivityFeed/SecretNotifications with InviteUserDialog as a modal. Wired into ChatInterface.vue's header + right-side panel slot, shown only when session.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.
  • i18n: added 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.
  • New 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 mocked apiService.
  • New ShareSecretDialog.test.ts: confirms the expiry selector is gone, confirms shareSecretWithSession is called with the selected participant ids, confirms the dialog stays open (no shared emit) on a failed share.
  • node_modules isn't installed in this environment (pre-push confirms: vue-tsc skipped) — could not run vitest/vue-tsc locally; reasoned through the test files carefully (traced MockWebSocket's connect/readyState timing, i18n string matches, detect-secrets false positives on test fixture data) but CI is the first real execution.
  • Found and fixed during implementation: every component calling useSessionCollaboration() registered its own onScopeDispose, 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

Model Used

Claude Sonnet 5

Refs #16429 (ShareSecretDialog AC)
Closes #16443

…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)
@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 9446420e-7547-4902-9c76-a1a70d3af87b


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

mrveiss added a commit that referenced this pull request Sep 12, 2026
…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.
@github-actions

Copy link
Copy Markdown
Contributor

Notice: 29 open PRs — past the runaway threshold (25)

There is no PR queue limit, and this is not a request to defer this PR. Work proceeds one issue at a time without a cap on open PRs; review capacity is the constraint.

This notice only means the count is high enough to be worth a glance for a runaway — something opening PRs in a loop, or a merge pipeline that has stalled so nothing is draining.

Currently open:

If the queue is draining normally, ignore this. Otherwise:

  1. Check whether CI is dispatching at all — see the ci-dispatch-watchdog status on these PRs
  2. Merge the ones whose CI has finished and review has passed: gh pr merge <number> --squash --delete-branch
  3. Look for a loop opening near-identical PRs

Warn-only runaway detector — .github/workflows/pr-queue-gate.yml. It never blocks a merge.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

@mrveiss

mrveiss commented Sep 14, 2026

Copy link
Copy Markdown
Owner Author

Bug in this head's own commit fixed in the 2d vehicle: #16702, commit 3a13531.

SecretsManager.vue:238 called shareSecret(secret) — secret isn't in scope in that block (the v-for binds item, and every sibling action button uses item.data). Caught by vue-tsc-baseline on the combined vehicle tree (introduced at 1c48732). Fixed to shareSecret(item.data); this PR's own branch was never updated. Per the merge-train plan this PR merges pinned to its current head and the vehicle lands immediately after with the fix — no action needed on this branch.

mrveiss added a commit that referenced this pull request Sep 14, 2026
- 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.
mrveiss added a commit that referenced this pull request Sep 14, 2026
#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.
@mrveiss
mrveiss merged commit 9737e2f into main Sep 14, 2026
4 checks passed
@mrveiss
mrveiss deleted the issue-16443-collaboration-ui-wiring branch September 14, 2026 12:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

impl gap: wire in the real-time collaborative-session UI (#608 phases 5-7 / #874) — 6 components + useSessionCollaboration, zero callers

1 participant