From b723f820bfbca82d146feacaa8afa19571b81488 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Sun, 23 Aug 2026 21:52:17 +0200 Subject: [PATCH] Route automatic Unraid diagnostics through read-only MUA --- config/service-catalog.json | 4 +- dev/test_openwebui_filters.py | 6 +- docs/PLATFORM_OVERVIEW.md | 4 ++ docs/QWEN_OPERATOR_CONTEXT.md | 6 ++ .../openwebui/filters/auto_tool_selector.py | 4 +- platform/openwebui/install-filters.sh | 62 +++++++++++++++++++ 6 files changed, 79 insertions(+), 7 deletions(-) diff --git a/config/service-catalog.json b/config/service-catalog.json index 8e4e11f..f95e2e5 100644 --- a/config/service-catalog.json +++ b/config/service-catalog.json @@ -12,7 +12,7 @@ "port": 5001, "protocol": "https", "probe": "tcp", - "specialist_tool": "unraid-readonly-local", + "specialist_tool": "mua-readonly-local", "purpose": "Existing NAS, Docker host and storage platform. Inspect it before planning any replacement service on Athena." }, { @@ -37,7 +37,7 @@ "protocol": "http", "probe": "http-head", "probe_path": "/", - "specialist_tool": "unraid-readonly-local", + "specialist_tool": "mua-readonly-local", "purpose": "Existing Deemix backend and download location. An Athena MCP must integrate this instance over WireGuard; it must not create a second Deemix unless the user explicitly requests migration or replacement." } ] diff --git a/dev/test_openwebui_filters.py b/dev/test_openwebui_filters.py index ff23d2c..94ce7ef 100644 --- a/dev/test_openwebui_filters.py +++ b/dev/test_openwebui_filters.py @@ -146,7 +146,7 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): "Welche Docker-Container laufen aktuell auf Unraid?" ) self.assertEqual( - result["tool_ids"], ["server:mcp:unraid-readonly-local"] + result["tool_ids"], ["server:mcp:mua-readonly-local"] ) self.assertNotIn("server:mcp:mua", result["tool_ids"]) @@ -155,7 +155,7 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): "Welche Dacher Contäner laufen aktuell auf dem Anrate Server?" ) self.assertEqual( - result["tool_ids"], ["server:mcp:unraid-readonly-local"] + result["tool_ids"], ["server:mcp:mua-readonly-local"] ) async def test_navidrome_is_selected(self): @@ -193,7 +193,7 @@ class AutoToolSelectorTests(unittest.IsolatedAsyncioTestCase): ) self.assertEqual( result["tool_ids"], - ["server:mcp:github-local", "server:mcp:unraid-readonly-local"], + ["server:mcp:github-local", "server:mcp:mua-readonly-local"], ) self.assertIn("Never compensate", result["messages"][0]["content"]) diff --git a/docs/PLATFORM_OVERVIEW.md b/docs/PLATFORM_OVERVIEW.md index 65f20f4..6f4e486 100644 --- a/docs/PLATFORM_OVERVIEW.md +++ b/docs/PLATFORM_OVERVIEW.md @@ -68,6 +68,10 @@ Deemix läuft bereits als Container auf dem Unraid-HomeServer. Eine künftige Deemix-MCP-Integration auf Athena verwendet dieses Backend über WireGuard und erzeugt nicht ungefragt eine zweite Deemix-Instanz. +Automatische Unraid-Abfragen verwenden einen eigenen MUA-Read-only-Zugang, der +in Open WebUI ausschließlich Diagnosewerkzeuge sichtbar macht. Der vollständige +MUA-Verwaltungszugang bleibt davon getrennt und muss bewusst gewählt werden. + Open WebUI und Router veröffentlichen keinen normalen Host-Port. Der WireGuard-Gateway-Container stellt nur die vorgesehenen VPN-Endpunkte bereit. Anwendungscontainer erreichen Heimnetz und Internet fail-closed über WireGuard; diff --git a/docs/QWEN_OPERATOR_CONTEXT.md b/docs/QWEN_OPERATOR_CONTEXT.md index 4f3da76..2532e2c 100644 --- a/docs/QWEN_OPERATOR_CONTEXT.md +++ b/docs/QWEN_OPERATOR_CONTEXT.md @@ -242,6 +242,12 @@ Netzzugriff. Kein MCP-Port wird am Host veröffentlicht. | Unraid | Host-, Docker-, Array-, Netzwerk- und Logdiagnose | read-only Standard | | MUA/Admin | eng definierte Unraid-Verwaltung | bewusst aktivieren | +Für automatische Unraid-Diagnose existiert in Open WebUI zusätzlich +`mua-readonly-local`. Diese Verbindung nutzt denselben lokalen MUA-Endpunkt, +blendet aber Start/Stop, Installation, Änderungen und die freie Root-Shell +serverseitig in Open WebUI aus. Die vollständige Verbindung `mua` wird niemals +automatisch bereitgestellt. + Der vorhandene Deemix-Dienst läuft auf Unraid und ist als externe Abhängigkeit im Diensteverzeichnis eingetragen. Für ein Deemix-MCP wird standardmäßig nur ein Relay auf Athena gebaut; ein zweites Deemix-Backend erfordert einen diff --git a/platform/openwebui/filters/auto_tool_selector.py b/platform/openwebui/filters/auto_tool_selector.py index a595d36..963ab65 100644 --- a/platform/openwebui/filters/auto_tool_selector.py +++ b/platform/openwebui/filters/auto_tool_selector.py @@ -24,7 +24,7 @@ class Filter: "github": "server:mcp:github-local", "homeassistant": "server:mcp:homeassistant-local", "arr": "server:mcp:arr-local", - "unraid": "server:mcp:unraid-readonly-local", + "unraid": "server:mcp:mua-readonly-local", "navidrome": "server:mcp:navidrome-local", "platform": "server:mcp:athena-platform", "operator": "server:mcp:athena-operator-local", @@ -35,7 +35,7 @@ class Filter: "github": "GitHub", "homeassistant": "Home Assistant", "arr": "Sonarr/Radarr", - "unraid": "Unraid-Diagnose", + "unraid": "Unraid-Diagnose (MUA read-only)", "navidrome": "Navidrome", "platform": "Athena-Plattformwissen", "operator": "Athena Operator", diff --git a/platform/openwebui/install-filters.sh b/platform/openwebui/install-filters.sh index cd8cf0a..0e1cd98 100755 --- a/platform/openwebui/install-filters.sh +++ b/platform/openwebui/install-filters.sh @@ -44,6 +44,7 @@ if [[ -s /etc/mike-ai/github-mcp.env ]] && \ fi python3 - "$db" "$FILTER_DIR" "$ACTION_DIR" "${OPENWEBUI_FILTER_OWNER_ID:-}" "$navidrome_enabled" "$github_enabled" <<'PY' import json +import copy import pathlib import sqlite3 import sys @@ -204,6 +205,13 @@ with con: "Docker, Shares, Netzwerk, UPS und Logs. Für dieselbe Anfrage nicht zusätzlich " "MUA aufrufen; MUA nur für dessen spezielle oder freigegebene Verwaltungsaktionen.", ), + "mua-readonly-local": ( + "Unraid-Diagnose (MUA read-only)", + "Automatisch verwendbarer, serverseitig in Open WebUI auf reine Lese- und " + "Diagnosewerkzeuge begrenzter MUA-Zugang. Für Containerbestand, Logs, System, " + "Storage, Shares und Netzwerkstatus. Keine Start/Stop-, Installations-, " + "Änderungs- oder freie Shell-Funktion. Für bewusste Verwaltung MUA separat wählen.", + ), "mua": ( "MUA (Unraid-Verwaltung)", "Nur für ausdrücklich benötigte MUA-spezifische oder freigegebene Unraid-" @@ -268,6 +276,60 @@ with con: info["name"] = name info["description"] = description changed = True + # Clone the existing authenticated MUA connection into a second + # OpenWebUI connection whose exposed function list is strictly + # read-only. The bearer value remains in the database and is neither + # printed nor copied into Git. Automatic routing uses only this clone; + # the original MUA connection remains available for deliberate admin. + mua_source = next( + ( + connection for connection in connections + if isinstance(connection, dict) + and str((connection.get("info") or {}).get("id", "")).lower() == "mua" + ), + None, + ) + if mua_source is not None: + readonly_functions = ",".join(( + "unraid_docker_list", "unraid_docker_inspect", "unraid_docker_logs", + "unraid_docker_analyze_logs", "unraid_docker_processes", + "unraid_docker_stats", "unraid_docker_info", + "unraid_docker_update_status", "unraid_ca_search", + "unraid_network_inventory", "unraid_network_list", + "unraid_network_inspect", "unraid_network_host_state", + "unraid_network_audit_tcp", "unraid_network_lan_probe", + "unraid_system_health", "unraid_storage_status", + "unraid_disk_health", "unraid_notifications_list", + "unraid_shares_list", "unraid_share_inspect", + "unraid_system_connection_test", "unraid_system_shell_readonly", + )) + readonly = copy.deepcopy(mua_source) + readonly["config"] = { + "enable": True, + "function_name_filter_list": readonly_functions, + "access_grants": [], + } + name, description = descriptions["mua-readonly-local"] + readonly["info"] = { + "id": "mua-readonly-local", + "name": name, + "description": description, + } + existing_index = next( + ( + index for index, connection in enumerate(connections) + if isinstance(connection, dict) + and str((connection.get("info") or {}).get("id", "")).lower() + == "mua-readonly-local" + ), + None, + ) + if existing_index is None: + connections.append(readonly) + changed = True + elif connections[existing_index] != readonly: + connections[existing_index] = readonly + changed = True if navidrome_enabled and not any( isinstance(connection, dict) and (