# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Workspace Feature Container - Main Module. Handles feature initialization and RBAC catalog registration. Unified AI Workspace feature. """ import logging from typing import Dict, List, Any logger = logging.getLogger(__name__) FEATURE_CODE = "workspace" FEATURE_LABEL = {"en": "AI Workspace", "de": "AI Workspace", "fr": "AI Workspace"} FEATURE_ICON = "mdi-brain" UI_OBJECTS = [ { "objectKey": "ui.feature.workspace.dashboard", "label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"}, "meta": {"area": "dashboard"} }, { "objectKey": "ui.feature.workspace.editor", "label": {"en": "Editor", "de": "Editor", "fr": "Editeur"}, "meta": {"area": "editor"} }, { "objectKey": "ui.feature.workspace.settings", "label": {"en": "Settings", "de": "Einstellungen", "fr": "Parametres"}, "meta": {"area": "settings"} }, ] RESOURCE_OBJECTS = [ { "objectKey": "resource.feature.workspace.start", "label": {"en": "Start Agent", "de": "Agent starten", "fr": "Demarrer agent"}, "meta": {"endpoint": "/api/workspace/{instanceId}/start/stream", "method": "POST"} }, { "objectKey": "resource.feature.workspace.stop", "label": {"en": "Stop Agent", "de": "Agent stoppen", "fr": "Arreter agent"}, "meta": {"endpoint": "/api/workspace/{instanceId}/{workflowId}/stop", "method": "POST"} }, { "objectKey": "resource.feature.workspace.files", "label": {"en": "Manage Files", "de": "Dateien verwalten", "fr": "Gerer fichiers"}, "meta": {"endpoint": "/api/workspace/{instanceId}/files", "method": "GET"} }, { "objectKey": "resource.feature.workspace.folders", "label": {"en": "Manage Folders", "de": "Ordner verwalten", "fr": "Gerer dossiers"}, "meta": {"endpoint": "/api/workspace/{instanceId}/folders", "method": "GET"} }, { "objectKey": "resource.feature.workspace.datasources", "label": {"en": "Data Sources", "de": "Datenquellen", "fr": "Sources de donnees"}, "meta": {"endpoint": "/api/workspace/{instanceId}/datasources", "method": "GET"} }, { "objectKey": "resource.feature.workspace.voice", "label": {"en": "Voice Input/Output", "de": "Spracheingabe/-ausgabe", "fr": "Entree/sortie vocale"}, "meta": {"endpoint": "/api/workspace/{instanceId}/voice/*", "method": "POST"} }, { "objectKey": "resource.feature.workspace.edits", "label": {"en": "Review File Edits", "de": "Datei-Aenderungen pruefen", "fr": "Verifier les modifications de fichiers"}, "meta": {"endpoint": "/api/workspace/{instanceId}/edit/*", "method": "POST"} }, ] TEMPLATE_ROLES = [ { "roleLabel": "workspace-viewer", "description": { "en": "Workspace Viewer - View workspace (read-only)", "de": "Workspace Betrachter - Workspace ansehen (nur lesen)", "fr": "Visualiseur Workspace - Consulter le workspace (lecture seule)" }, "accessRules": [ {"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True}, {"context": "UI", "item": "ui.feature.workspace.editor", "view": True}, {"context": "UI", "item": "ui.feature.workspace.settings", "view": True}, {"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"}, ] }, { "roleLabel": "workspace-user", "description": { "en": "Workspace User - Use AI workspace and tools", "de": "Workspace Benutzer - AI Workspace und Tools nutzen", "fr": "Utilisateur Workspace - Utiliser l'espace de travail AI et les outils" }, "accessRules": [ {"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True}, {"context": "UI", "item": "ui.feature.workspace.editor", "view": True}, {"context": "UI", "item": "ui.feature.workspace.settings", "view": True}, {"context": "RESOURCE", "item": "resource.feature.workspace.start", "view": True}, {"context": "RESOURCE", "item": "resource.feature.workspace.stop", "view": True}, {"context": "RESOURCE", "item": "resource.feature.workspace.files", "view": True}, {"context": "RESOURCE", "item": "resource.feature.workspace.folders", "view": True}, {"context": "RESOURCE", "item": "resource.feature.workspace.datasources", "view": True}, {"context": "RESOURCE", "item": "resource.feature.workspace.voice", "view": True}, {"context": "RESOURCE", "item": "resource.feature.workspace.edits", "view": True}, {"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"}, ] }, { "roleLabel": "workspace-admin", "description": { "en": "Workspace Admin - All UI and API actions; data is always scoped to own records (same privacy as users)", "de": "Workspace Admin - Alle UI- und API-Aktionen; Daten immer nur eigene Datensätze (gleiche Privatsphäre wie User)", "fr": "Administrateur Workspace - Toute l'UI et les API; donnees limitees a ses propres enregistrements" }, "accessRules": [ {"context": "UI", "item": None, "view": True}, {"context": "RESOURCE", "item": None, "view": True}, # DATA: never ALL in shared instances — every role (including admin) sees only _createdBy = self {"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"}, ] }, ] def getFeatureDefinition() -> Dict[str, Any]: """Return the feature definition for registration.""" return { "code": FEATURE_CODE, "label": FEATURE_LABEL, "icon": FEATURE_ICON, "autoCreateInstance": True, } def getUiObjects() -> List[Dict[str, Any]]: """Return UI objects for RBAC catalog registration.""" return UI_OBJECTS def getResourceObjects() -> List[Dict[str, Any]]: """Return resource objects for RBAC catalog registration.""" return RESOURCE_OBJECTS def getTemplateRoles() -> List[Dict[str, Any]]: """Return template roles for this feature.""" return TEMPLATE_ROLES def registerFeature(catalogService) -> bool: """Register this feature's RBAC objects in the catalog.""" try: for uiObj in UI_OBJECTS: catalogService.registerUiObject( featureCode=FEATURE_CODE, objectKey=uiObj["objectKey"], label=uiObj["label"], meta=uiObj.get("meta") ) for resObj in RESOURCE_OBJECTS: catalogService.registerResourceObject( featureCode=FEATURE_CODE, objectKey=resObj["objectKey"], label=resObj["label"], meta=resObj.get("meta") ) _syncTemplateRolesToDb() logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI objects and {len(RESOURCE_OBJECTS)} resource objects") return True except Exception as e: logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}") return False def _syncTemplateRolesToDb() -> int: """Sync template roles and their AccessRules to the database.""" try: from modules.interfaces.interfaceDbApp import getRootInterface from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext rootInterface = getRootInterface() existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE) templateRoles = [r for r in existingRoles if r.mandateId is None] existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles} createdCount = 0 for roleTemplate in TEMPLATE_ROLES: roleLabel = roleTemplate["roleLabel"] if roleLabel in existingRoleLabels: roleId = existingRoleLabels[roleLabel] _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) else: newRole = Role( roleLabel=roleLabel, description=roleTemplate.get("description", {}), featureCode=FEATURE_CODE, mandateId=None, featureInstanceId=None, isSystemRole=False ) createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump()) roleId = createdRole.get("id") _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) logger.info(f"Created template role '{roleLabel}' with ID {roleId}") createdCount += 1 if createdCount > 0: logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles") return createdCount except Exception as e: logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}") return 0 def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int: """Ensure AccessRules exist for a role based on templates.""" from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext existingRules = rootInterface.getAccessRulesByRole(roleId) existingSignatures = set() for rule in existingRules: sig = (rule.context.value if rule.context else None, rule.item) existingSignatures.add(sig) createdCount = 0 for template in ruleTemplates: context = template.get("context", "UI") item = template.get("item") sig = (context, item) if sig in existingSignatures: continue if context == "UI": contextEnum = AccessRuleContext.UI elif context == "DATA": contextEnum = AccessRuleContext.DATA elif context == "RESOURCE": contextEnum = AccessRuleContext.RESOURCE else: contextEnum = context newRule = AccessRule( roleId=roleId, context=contextEnum, item=item, view=template.get("view", False), read=template.get("read"), create=template.get("create"), update=template.get("update"), delete=template.get("delete"), ) rootInterface.db.recordCreate(AccessRule, newRule.model_dump()) createdCount += 1 if createdCount > 0: logger.debug(f"Created {createdCount} AccessRules for role {roleId}") return createdCount