feat: realtime WebSocket layer for live events and streaming logs

- Backend: ConnectionManager, Redis-backed bus with in-process fallback,
  channel authorization, and typed emit helpers wired into workers
  (ai_runtime, sandbox_gateway), workforce/sandbox/billing/audit/error services.
- Frontend: single shared WS client with backoff + heartbeat, RealtimeProvider,
  useLiveChannel/useLiveEvent hooks, live indicators on task detail, manager
  chat, dashboard, traces, sandbox terminal, admin errors, and billing.
- Contracts: WsEvent/WsServerFrame/WsClientFrame types mirroring the port.

All 255 backend tests pass; web typecheck and build pass.
This commit is contained in:
mjalmoudy94
2026-10-13 12:56:36 +03:30
parent afdef4b444
commit f1e5df29ce
31 changed files with 2981 additions and 28 deletions
+112
View File
@@ -0,0 +1,112 @@
"""Channel authorization — the only gate on what a socket may receive.
Authorization happens once, at subscribe time. After that the connection holds
plain channel names and the manager routes on them, so there is no code path
that can deliver an event to a connection that was never checked.
Every rule resolves the channel's tenant through the repositories and then the
caller's membership in it — exactly like the HTTP layer's ``get_org_context``.
A tenant that does not own the object gets a rejection, not an empty stream:
the difference matters when debugging "why is my page not live?".
The caller's identity arrives as two primitives (id, platform-owner flag) —
what an authenticated socket actually knows — rather than a ``User`` row, so a
subscribe costs one fewer database read.
"""
from __future__ import annotations
from sqlalchemy.ext.asyncio import AsyncSession
from app.infrastructure.repositories.identity import MembershipRepository
from app.infrastructure.repositories.sandbox import (
SandboxSessionRepository,
WorkspaceRepository,
)
from app.infrastructure.repositories.workforce import (
RunRepository,
TaskRepository,
ThreadRepository,
)
async def authorize_channel(
session: AsyncSession,
*,
user_id: str,
is_platform_owner: bool,
channel: str,
) -> bool:
"""May this identity subscribe to ``channel``? Deny by default."""
kind, _, identifier = channel.partition(":")
if not identifier and kind != "admin":
return False
if kind == "admin":
return is_platform_owner
if kind == "user":
return identifier == user_id
if kind == "org":
return await _is_member(
session=session, user_id=user_id, organization_id=identifier
)
if kind == "task":
return await _task_is_visible(
session=session, user_id=user_id, task_id=identifier
)
if kind == "run":
return await _run_is_visible(
session=session, user_id=user_id, run_id=identifier
)
if kind == "thread":
return await _thread_is_visible(
session=session, user_id=user_id, thread_id=identifier
)
if kind == "session":
return await _session_is_visible(
session=session, user_id=user_id, session_id=identifier
)
return False
async def _is_member(*, session: AsyncSession, user_id: str, organization_id: str) -> bool:
membership = await MembershipRepository(session).get(organization_id, user_id)
return membership is not None and membership.is_active
async def _task_is_visible(*, session: AsyncSession, user_id: str, task_id: str) -> bool:
task = await TaskRepository(session).get(task_id)
if task is None:
return False
return await _is_member(
session=session, user_id=user_id, organization_id=task.organization_id
)
async def _run_is_visible(*, session: AsyncSession, user_id: str, run_id: str) -> bool:
run = await RunRepository(session).get(run_id)
if run is None:
return False
return await _task_is_visible(session=session, user_id=user_id, task_id=run.task_id)
async def _thread_is_visible(*, session: AsyncSession, user_id: str, thread_id: str) -> bool:
thread = await ThreadRepository(session).get(thread_id)
if thread is None:
return False
return await _is_member(
session=session, user_id=user_id, organization_id=thread.organization_id
)
async def _session_is_visible(*, session: AsyncSession, user_id: str, session_id: str) -> bool:
"""A sandbox session belongs to a workspace, which belongs to a tenant."""
sandbox_session = await SandboxSessionRepository(session).get(session_id)
if sandbox_session is None:
return False
workspace = await WorkspaceRepository(session).get(sandbox_session.workspace_id)
if workspace is None:
return False
return await _is_member(
session=session, user_id=user_id, organization_id=workspace.organization_id
)