"""Core AI agent tools for MiniApp output (Phase M6). ``send_miniapp`` lets an agent embed a MiniApp as an interactive output block in its chat room (block_type "miniapp", approval_request precedent from agent_loop). Permission is checked fail-closed against the CALLING user for the target app's own permission — the tool never widens access. """ from __future__ import annotations import logging import uuid from typing import Any from app.ai.tool_registry import get_tool_registry from app.core.permissions import check_permission, resolve_permissions from app.plugins.miniapp_registry import get_miniapp_registry logger = logging.getLogger(__name__) def _get_komm_contract() -> Any | None: """Resolve the kommunikation contract (None when plugin inactive).""" from app.plugins.builtins.contracts import get_contract_registry return get_contract_registry().get("kommunikation") async def _send_miniapp_handler(arguments: dict[str, Any], context: dict[str, Any]) -> str: """Send a MiniApp as an output block to the agent's chat room.""" app_id = str(arguments.get("app_id") or "") settings = arguments.get("settings") or {} if not isinstance(settings, dict): settings = {} app = get_miniapp_registry().get_app(app_id) if app is None: return f"Error: MiniApp '{app_id}' not found" db = context.get("db") tenant_id = context.get("tenant_id") user_id = context.get("user_id") if not tenant_id or not user_id or db is None: return "Error: Missing tenant/user context" try: tenant_uuid = uuid.UUID(str(tenant_id)) user_uuid = uuid.UUID(str(user_id)) except (ValueError, TypeError): return "Error: Invalid tenant/user context" # Fail-closed permission check against the calling user resolved = await resolve_permissions(db, user_uuid, tenant_uuid) if app.permission and not check_permission(resolved, app.permission): return f"Error: Permission '{app.permission}' required for MiniApp '{app.app_id}'" komm = _get_komm_contract() if komm is None: return "Error: Communication plugin not available" agent_name = str(context.get("agent_name") or "Agent") conv_id = await komm.find_locked_room_id( db=db, tenant_id=tenant_uuid, plugin_name="automation", title=f"Agent: {agent_name}", ) if conv_id is None: return f"Info: No agent chat room found for '{agent_name}' — MiniApp not posted" agent_id_raw = context.get("agent_id") try: sender_id = uuid.UUID(str(agent_id_raw)) if agent_id_raw else None except (ValueError, TypeError): sender_id = None await komm.send_message( db=db, tenant_id=tenant_uuid, conversation_id=conv_id, sender_id=sender_id, sender_type="agent", content=f"MiniApp: {app.name}", content_format="text", blocks=[ { "block_type": "miniapp", "block_data": {"app_id": app_id, "config": settings}, "sort_order": 0, } ], ) return f"MiniApp '{app_id}' sent to chat" # ─── Registration ─── def register_miniapp_tools() -> None: """Register the MiniApp agent tools in the global tool registry.""" registry = get_tool_registry() registry.register( name="send_miniapp", description=( "Eine MiniApp als interaktiven Ausgabe-Block in den Agent-Chat senden " "(z.B. ein Widget mit Einstellungen anzeigen). Verfügbare App-IDs " "stehen in /api/v1/miniapps." ), parameters={ "type": "object", "properties": { "app_id": { "type": "string", "description": "ID der MiniApp (z.B. recent_contacts, tasks_summary)", }, "settings": { "type": "object", "description": "Optionale Einstellungen für die MiniApp-Instanz", }, }, "required": ["app_id"], }, handler=_send_miniapp_handler, plugin_name="system", required_permission=None, # per-app check inside the handler (fail-closed) category="ui", )