feat(B-WS): WebSocket Helpers + Redis Pub/Sub + Error-Handling
Check Cross-Plugin Imports / check (push) Has been cancelled

B-WS: app/core/ws_helpers.py (NEU) — gemeinsame WebSocket Helpers
- authenticate_ws: Session-Auth für WebSocket (Cookie/Token → User/Tenant)
- check_ws_origin: Origin-Check (delegiert auf verify_ws_origin)
- check_ws_tenant: User-Tenant-Membership-Check
- cleanup_ws_connection: Connection aus Registry entfernen + WS schließen
- start_heartbeat: Background Ping-Task
- send_ws_error: strukturierte Error-Message an Client
- handle_ws_message: Message-Dispatch mit Error-Handling

B-WS: app/core/ws_pubsub.py (NEU) — Redis Pub/Sub für Multi-Worker-Fanout
- publish_to_channel / subscribe_to_channel
- get_tenant_channel / broadcast_to_tenants

B-WS: WebSocketManager + AIUIControlWSManager angepasst
- connect() nutzt authenticate_ws + check_ws_origin + check_ws_tenant
- disconnect() nutzt cleanup_ws_connection + cancelt Heartbeat/PubSub
- broadcast() unterstützt Redis Pub/Sub Fanout

B-ERR-WS: WS Error-Handling in ws_helpers integriert
- send_ws_error für strukturierte Errors
- handle_ws_message fängt Handler-Exceptions

B-WS-TEST: 24 Tests in test_ws_helpers.py — alle grün
- Auth, Origin, Error, Dispatch, Cleanup, Heartbeat, Pub/Sub Roundtrip
- Keine Regression: 47/47 Resilience+Hooks Tests grün
This commit is contained in:
Agent Zero
2026-08-13 16:43:54 +02:00
parent a3a26d1f66
commit 7a81a5f072
7 changed files with 969 additions and 76 deletions
+12 -25
View File
@@ -205,33 +205,28 @@ async def ai_ui_control_ws(websocket: WebSocket):
Authentication: via session cookie (same pattern as kommunikation plugin).
"""
from app.config import get_settings
from app.core.auth import get_session_data, get_redis, verify_ws_origin
from app.core.db import async_session_maker
from app.core.service_container import get_container
settings = get_settings()
if not await verify_ws_origin(websocket):
await websocket.close(code=4003, reason="Origin not allowed")
container = get_container()
if not container.has("ai_ui_control_ws"):
await websocket.close(code=4003, reason="AI UI Control not available")
return
session_id = websocket.cookies.get(settings.session_cookie_name)
if not session_id:
await websocket.close(code=4001, reason="Not authenticated")
return
ws_manager = container.get("ai_ui_control_ws")
redis = get_redis()
session_data = await get_session_data(redis, session_id)
if session_data is None:
await websocket.close(code=4001, reason="Session expired")
return
# connect() performs origin check, session auth, tenant check
async with async_session_maker() as db:
auth = await ws_manager.connect(websocket, db)
if auth is None:
return # connection was rejected and closed by ws_helpers
user_id = session_data["user_id"]
tenant_id = session_data["tenant_id"]
user_id = auth["user_id"]
tenant_id = auth["tenant_id"]
# Plugin-Gate: check if ai_ui_control plugin is active (global + tenant)
from app.core.permission_registry import get_permission_registry
from sqlalchemy import text as sa_text
from app.core.db import async_session_maker
import uuid as _uuid
try:
registry = get_permission_registry()
@@ -251,14 +246,6 @@ async def ai_ui_control_ws(websocket: WebSocket):
await websocket.close(code=4003, reason="Plugin check failed")
return
container = get_container()
if not container.has("ai_ui_control_ws"):
await websocket.close(code=4003, reason="AI UI Control not available")
return
ws_manager = container.get("ai_ui_control_ws")
await ws_manager.connect(websocket, user_id)
try:
while True:
data = await websocket.receive_text()