feat: Unified Messaging System — kommunikation plugin, AI/Proactive/System participants, MessageSidebar, Rich Content Renderer

Phase 1: Backend plugin kommunikation (13 files, 10 tables, REST API, WebSocket, RBAC, DMS Bridge, Participant Registry, Mini-App Registry, Search Provider)
Phase 2: AI plugins as participants (ai_assistant + ai_proactive dock as participants, heartbeat job)
Phase 3: system_notif plugin (system events → chat messages, pinned System room)
Phase 4: Frontend MessageSidebar (replaces AISidebar, same design, comm API client, WebSocket hook, commStore)
Phase 5: Rich Content Block Renderer (11 components: Markdown, HTML, Image, Audio, Video, File, ActionCard, ContactCard, MiniApp, BlockRenderer)
BasePlugin: added services property + _container in on_activate
This commit is contained in:
Agent Zero
2026-07-22 01:22:15 +02:00
parent 5980d38c66
commit cc3ac9a43d
45 changed files with 8154 additions and 113 deletions
+136
View File
@@ -0,0 +1,136 @@
"""Chat-internal RBAC logic — two-level permission checks."""
from __future__ import annotations
import logging
import uuid
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.permissions import check_permission
from app.plugins.builtins.kommunikation.models import CommParticipant
logger = logging.getLogger(__name__)
class CommRBAC:
"""Chat-internal RBAC, integrated with the existing permission system."""
@staticmethod
async def get_user_role(
db: AsyncSession,
conversation_id: uuid.UUID,
user_id: uuid.UUID,
) -> str | None:
"""Get the user's role in a conversation, or None if not a participant."""
result = await db.execute(
select(CommParticipant).where(
CommParticipant.conversation_id == conversation_id,
CommParticipant.participant_id == user_id,
CommParticipant.participant_type == "user",
CommParticipant.left_at.is_(None),
)
)
participant = result.scalar_one_or_none()
return participant.role if participant else None
@staticmethod
async def is_participant(
db: AsyncSession,
conversation_id: uuid.UUID,
user_id: uuid.UUID,
) -> bool:
"""Check if a user is an active participant in a conversation."""
role = await CommRBAC.get_user_role(db, conversation_id, user_id)
return role is not None
@staticmethod
async def can_user_write(
db: AsyncSession,
conversation_id: uuid.UUID,
current_user: dict[str, Any],
) -> bool:
"""Check if user can write messages.
1. System permission 'comm:write' via check_permission
2. is_system_admin → always allowed
3. User must be participant with role 'admin' or 'member'
"""
if current_user.get("is_system_admin"):
return True
if not check_permission(current_user, "comm:write"):
return False
user_id = uuid.UUID(current_user["user_id"])
role = await CommRBAC.get_user_role(db, conversation_id, user_id)
return role in ("admin", "member")
@staticmethod
async def can_user_manage(
db: AsyncSession,
conversation_id: uuid.UUID,
current_user: dict[str, Any],
) -> bool:
"""Check if user can manage participants.
1. System permission 'comm:manage' via check_permission
2. is_system_admin → always allowed
3. User must be conversation admin
"""
if current_user.get("is_system_admin"):
return True
if not check_permission(current_user, "comm:manage"):
return False
user_id = uuid.UUID(current_user["user_id"])
role = await CommRBAC.get_user_role(db, conversation_id, user_id)
return role == "admin"
@staticmethod
async def can_user_delete(
db: AsyncSession,
conversation_id: uuid.UUID,
current_user: dict[str, Any],
is_own_message: bool = False,
) -> bool:
"""Check if user can delete messages or conversation.
1. is_system_admin → always allowed
2. For own messages: comm:write suffices
3. For others' messages: comm:delete + conversation admin
4. For conversation: comm:delete + admin role
"""
if current_user.get("is_system_admin"):
return True
if is_own_message:
return check_permission(current_user, "comm:write")
if not check_permission(current_user, "comm:delete"):
return False
user_id = uuid.UUID(current_user["user_id"])
role = await CommRBAC.get_user_role(db, conversation_id, user_id)
return role == "admin"
@staticmethod
async def can_user_create(current_user: dict[str, Any]) -> bool:
"""Check if user can create conversations."""
if current_user.get("is_system_admin"):
return True
return check_permission(current_user, "comm:create")
@staticmethod
async def can_user_change_role(
db: AsyncSession,
conversation_id: uuid.UUID,
current_user: dict[str, Any],
) -> bool:
"""Check if user can change participant roles.
Requires comm:admin permission + conversation admin role.
"""
if current_user.get("is_system_admin"):
return True
if not check_permission(current_user, "comm:admin"):
return False
user_id = uuid.UUID(current_user["user_id"])
role = await CommRBAC.get_user_role(db, conversation_id, user_id)
return role == "admin"