r026: add dynamic agent toolbox integration
This commit is contained in:
@@ -231,6 +231,25 @@ Zusätzlich kann jedes Werkzeug einzeln nach Risikostufe freigegeben werden:
|
|||||||
Deaktivierte Tools werden vom MCP-Server gefiltert — sie erscheinen nicht in
|
Deaktivierte Tools werden vom MCP-Server gefiltert — sie erscheinen nicht in
|
||||||
`tools/list` und können nicht aufgerufen werden (→ `ERROR: Tool disabled`).
|
`tools/list` und können nicht aufgerufen werden (→ `ERROR: Tool disabled`).
|
||||||
|
|
||||||
|
### Optionaler Agent-Werkzeugcontainer
|
||||||
|
|
||||||
|
MUA kann einen allgemeinen Werkzeugcontainer dynamisch über das Docker-Label
|
||||||
|
`mike.ai.role=toolbox` erkennen. Sobald mindestens ein solcher Container
|
||||||
|
existiert, wird in der WebGUI der Schalter **„Werkzeugcontainer für
|
||||||
|
KI-Agenten bekanntgeben“** verfügbar. Fehlt das Label, bleibt er ausgegraut.
|
||||||
|
|
||||||
|
Bei aktiviertem Schalter nennt MUA den aktuell erkannten Containernamen und
|
||||||
|
die Nutzung über `docker exec` in seinen MCP-Anweisungen. Der Name ist nicht
|
||||||
|
fest in MUA eingebaut. Wird der Container ersetzt, genügt dasselbe Label am
|
||||||
|
Nachfolger. Die eigentliche Ausführung benötigt das separat freigegebene
|
||||||
|
Werkzeug `unraid_system_shell`.
|
||||||
|
|
||||||
|
Beispiel für Docker beziehungsweise DockerMan `ExtraParams`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--label=mike.ai.role=toolbox
|
||||||
|
```
|
||||||
|
|
||||||
Bei freigegebenem Vollzugriff ist `unraid_system_shell` das direkte Terminal
|
Bei freigegebenem Vollzugriff ist `unraid_system_shell` das direkte Terminal
|
||||||
auf dem Unraid-Host. Agenten sollen es für normale Docker-, Datei- und
|
auf dem Unraid-Host. Agenten sollen es für normale Docker-, Datei- und
|
||||||
Administrationsaufträge unmittelbar verwenden und weder den MUA-Quellcode
|
Administrationsaufträge unmittelbar verwenden und weder den MUA-Quellcode
|
||||||
@@ -273,6 +292,7 @@ Prozess startet automatisch. Zusätzlich führt der POST-INSTALL-Hook im
|
|||||||
```ini
|
```ini
|
||||||
MUA_API_KEY=a1b2c3d4...
|
MUA_API_KEY=a1b2c3d4...
|
||||||
MUA_ENABLED_TOOLS=unraid_docker_list,unraid_docker_inspect,unraid_network_list
|
MUA_ENABLED_TOOLS=unraid_docker_list,unraid_docker_inspect,unraid_network_list
|
||||||
|
MUA_TOOLBOX_ENABLED=false
|
||||||
```
|
```
|
||||||
|
|
||||||
Sonderwerte: `all` aktiviert ausdrücklich alles, `none` deaktiviert alles.
|
Sonderwerte: `all` aktiviert ausdrücklich alles, `none` deaktiviert alles.
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -118,6 +118,12 @@ Hermes-Containerterminal als Zwischenstation verwenden. Normale Befehle und
|
|||||||
mehrzeilige Skripte laufen synchron; nur Arbeiten über 30 Minuten verwenden
|
mehrzeilige Skripte laufen synchron; nur Arbeiten über 30 Minuten verwenden
|
||||||
die asynchronen Job-Werkzeuge.
|
die asynchronen Job-Werkzeuge.
|
||||||
|
|
||||||
|
Wenn in der MUA-WebGUI der optionale Agent-Werkzeugcontainer aktiviert ist,
|
||||||
|
liefert MUA dessen dynamisch per Label erkannten Namen bereits in den
|
||||||
|
MCP-Anweisungen. Hermes verwendet ihn dann über `unraid_system_shell` und
|
||||||
|
`docker exec`; im Hermes-Container selbst werden keine Hilfsprogramme
|
||||||
|
nachinstalliert. MUA setzt keinen festen Containernamen voraus.
|
||||||
|
|
||||||
## 5. Entfernen
|
## 5. Entfernen
|
||||||
|
|
||||||
Nur die Hermes-Verbindung entfernen, nicht das MUA-Plugin auf Unraid:
|
Nur die Hermes-Verbindung entfernen, nicht das MUA-Plugin auf Unraid:
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
ini_set('display_errors', 0); // Fehler werden als JSON-RPC-Error zurückgegeben, nicht als HTML
|
ini_set('display_errors', 0); // Fehler werden als JSON-RPC-Error zurückgegeben, nicht als HTML
|
||||||
|
|
||||||
const MUA_VERSION = '2026.08.24.r017';
|
const MUA_VERSION = '2026.08.29.r026';
|
||||||
const MUA_SERVER_NAME = 'mua';
|
const MUA_SERVER_NAME = 'mua';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ const MUA_PORT = 3002;
|
|||||||
const MUA_BIND = '0.0.0.0';
|
const MUA_BIND = '0.0.0.0';
|
||||||
const MUA_SESSION_TTL = 3600; // Sekunden
|
const MUA_SESSION_TTL = 3600; // Sekunden
|
||||||
const MUA_SERVER_NAME = 'mua';
|
const MUA_SERVER_NAME = 'mua';
|
||||||
const MUA_VERSION = '2026.08.24.r017';
|
const MUA_VERSION = '2026.08.29.r026';
|
||||||
|
|
||||||
// ── Session-Management (in-memory) ───────────────────────────────────────
|
// ── Session-Management (in-memory) ───────────────────────────────────────
|
||||||
$sessions = []; // session_id => ['created' => time, 'initialized' => bool]
|
$sessions = []; // session_id => ['created' => time, 'initialized' => bool]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mua",
|
"name": "mua",
|
||||||
"version": "2026.08.28.r025",
|
"version": "2026.08.29.r026",
|
||||||
"description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid",
|
"description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
+9
-4
@@ -2,13 +2,13 @@
|
|||||||
<!DOCTYPE PLUGIN [
|
<!DOCTYPE PLUGIN [
|
||||||
<!ENTITY name "mua">
|
<!ENTITY name "mua">
|
||||||
<!ENTITY author "Michael">
|
<!ENTITY author "Michael">
|
||||||
<!ENTITY version "2026.08.28.r025">
|
<!ENTITY version "2026.08.29.r026">
|
||||||
<!ENTITY launch "Settings/mua">
|
<!ENTITY launch "Settings/mua">
|
||||||
<!ENTITY pluginURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/plugin/mua.plg">
|
<!ENTITY pluginURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/plugin/mua.plg">
|
||||||
<!ENTITY pluginLOC "/boot/config/plugins/&name;">
|
<!ENTITY pluginLOC "/boot/config/plugins/&name;">
|
||||||
<!ENTITY emhttpLOC "/usr/local/emhttp/plugins/&name;">
|
<!ENTITY emhttpLOC "/usr/local/emhttp/plugins/&name;">
|
||||||
<!ENTITY txzURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.28.r025-x86_64-1.txz">
|
<!ENTITY txzURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.29.r026-x86_64-1.txz">
|
||||||
<!ENTITY txzSHA256 "fec9b849f9c5527ddad31c47d60fa3dd0950b6be1794b162dfec25e83207c870">
|
<!ENTITY txzSHA256 "c1c045ce32d41cfec4c985a3f9ff08d8f2150d2103a75146bd17323922d562bb">
|
||||||
]>
|
]>
|
||||||
|
|
||||||
<PLUGIN name="&name;"
|
<PLUGIN name="&name;"
|
||||||
@@ -23,6 +23,11 @@
|
|||||||
>
|
>
|
||||||
|
|
||||||
<CHANGES>
|
<CHANGES>
|
||||||
|
### 2026.08.29.r026
|
||||||
|
- Erkennt optionale Agent-Werkzeugcontainer dynamisch über `mike.ai.role=toolbox` statt über einen fest eingebauten Namen.
|
||||||
|
- Die WebGUI bietet den Toolbox-Schalter nur bei vorhandenem Label an und zeigt andernfalls eine konkrete Einrichtungsanleitung.
|
||||||
|
- Bei Aktivierung erhalten MCP-Clients den aktuellen Containernamen und die Nutzung über `unraid_system_shell`/`docker exec` als Serverhinweis.
|
||||||
|
|
||||||
### 2026.08.28.r025
|
### 2026.08.28.r025
|
||||||
- Die freie Unraid-Shell ist als direkter Host-Terminalweg beschrieben, damit Agenten sie statt lokaler Proxy-Skripte oder MUA-Quellcodeanalyse verwenden.
|
- Die freie Unraid-Shell ist als direkter Host-Terminalweg beschrieben, damit Agenten sie statt lokaler Proxy-Skripte oder MUA-Quellcodeanalyse verwenden.
|
||||||
- Mehrzeilige Skripte bis 256 KiB und synchrone Laufzeiten bis 30 Minuten werden unterstützt; nur noch längere Arbeiten benötigen die vorhandenen asynchronen Job-Werkzeuge.
|
- Mehrzeilige Skripte bis 256 KiB und synchrone Laufzeiten bis 30 Minuten werden unterstützt; nur noch längere Arbeiten benötigen die vorhandenen asynchronen Job-Werkzeuge.
|
||||||
@@ -157,7 +162,7 @@ Das .txz enthält:
|
|||||||
install/doinst.sh (läuft nach Installation)
|
install/doinst.sh (läuft nach Installation)
|
||||||
===========================================
|
===========================================
|
||||||
-->
|
-->
|
||||||
<FILE Name="/boot/config/plugins/&name;/mua-2026.08.28.r025-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0">
|
<FILE Name="/boot/config/plugins/&name;/mua-2026.08.29.r026-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0">
|
||||||
<URL>&txzURL;</URL>
|
<URL>&txzURL;</URL>
|
||||||
<SHA256>&txzSHA256;</SHA256>
|
<SHA256>&txzSHA256;</SHA256>
|
||||||
</FILE>
|
</FILE>
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mua",
|
"name": "mua",
|
||||||
"author": "Michael",
|
"author": "Michael",
|
||||||
"version": "2026.08.28.r025",
|
"version": "2026.08.29.r026",
|
||||||
"minver": "7.0.0",
|
"minver": "7.0.0",
|
||||||
"pluginDirectory": "/usr/local/emhttp/plugins/mua",
|
"pluginDirectory": "/usr/local/emhttp/plugins/mua",
|
||||||
"configDirectory": "/boot/config/plugins/mua",
|
"configDirectory": "/boot/config/plugins/mua",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if ($action === 'preset_operator') $payload = ['enabledTools' => $operator, 'allToolsEnabled' => false];
|
if ($action === 'preset_operator') $payload = ['enabledTools' => $operator, 'allToolsEnabled' => false];
|
||||||
if ($action === 'preset_none') $payload = ['enabledTools' => [], 'allToolsEnabled' => false];
|
if ($action === 'preset_none') $payload = ['enabledTools' => [], 'allToolsEnabled' => false];
|
||||||
if ($action === 'preset_all') $payload = ['enabledTools' => [], 'allToolsEnabled' => true];
|
if ($action === 'preset_all') $payload = ['enabledTools' => [], 'allToolsEnabled' => true];
|
||||||
|
if ($action === 'save_toolbox') $payload = ['toolboxEnabled' => isset($_POST['toolbox_enabled'])];
|
||||||
if ($action === 'save_tools') {
|
if ($action === 'save_tools') {
|
||||||
$selected = [];
|
$selected = [];
|
||||||
foreach (($config['allTools'] ?? []) as $tool) {
|
foreach (($config['allTools'] ?? []) as $tool) {
|
||||||
@@ -128,6 +129,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
$enabled_tools = $config['enabledTools'] ?? [];
|
$enabled_tools = $config['enabledTools'] ?? [];
|
||||||
$all_tools_enabled = (bool)($config['allToolsEnabled'] ?? false);
|
$all_tools_enabled = (bool)($config['allToolsEnabled'] ?? false);
|
||||||
$all_tools = $config['allTools'] ?? [];
|
$all_tools = $config['allTools'] ?? [];
|
||||||
|
$toolbox = is_array($config['toolbox'] ?? null) ? $config['toolbox'] : [];
|
||||||
|
$toolbox_available = (bool)($toolbox['available'] ?? false);
|
||||||
|
$toolbox_enabled = (bool)($toolbox['enabled'] ?? false);
|
||||||
|
$toolbox_containers = is_array($toolbox['containers'] ?? null) ? $toolbox['containers'] : [];
|
||||||
$risk_groups = ['read' => [], 'active' => [], 'write' => [], 'critical' => []];
|
$risk_groups = ['read' => [], 'active' => [], 'write' => [], 'critical' => []];
|
||||||
foreach ($all_tools as $tool) {
|
foreach ($all_tools as $tool) {
|
||||||
if (!is_array($tool)) $tool = ['name' => $tool, 'description' => '', 'risk' => 'read'];
|
if (!is_array($tool)) $tool = ['name' => $tool, 'description' => '', 'risk' => 'read'];
|
||||||
@@ -145,6 +150,7 @@ $active_count = $all_tools_enabled ? count($all_tools) : count($enabled_tools);
|
|||||||
?>
|
?>
|
||||||
<style>
|
<style>
|
||||||
.mua-wrap{max-width:1280px}.mua-hero{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin:12px 0 20px}.mua-title h2{margin:0 0 5px;font-size:1.55rem}.mua-muted{color:#777}.mua-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(285px,1fr));gap:14px;margin:14px 0;align-items:start}.mua-card{border:1px solid #d8d8d8;border-radius:9px;padding:16px;background:rgba(255,255,255,.03)}.mua-card h3{margin:0 0 8px}.mua-status{padding:11px 14px;border-radius:7px;margin:10px 0;border:1px solid}.mua-status.ok,.mua-risk-safe{border-color:#43a047;background:rgba(67,160,71,.11)}.mua-status.err,.mua-risk-danger{border-color:#e53935;background:rgba(229,57,53,.11)}.mua-status.warn,.mua-risk-warn{border-color:#fb8c00;background:rgba(251,140,0,.12)}.mua-risk-notice{border-color:#1e88e5;background:rgba(30,136,229,.10)}.mua-pill{display:inline-block;padding:3px 9px;border-radius:20px;font-size:.82em;font-weight:600;border:1px solid currentColor}.mua-endpoint{font-size:1.05em;word-break:break-all}.mua-key{display:block;padding:12px;border:1px solid #43a047;border-radius:6px;word-break:break-all;font-size:1.04em;background:rgba(67,160,71,.08)}.mua-actions{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}.mua-btn{padding:8px 14px;border-radius:5px;border:1px solid #aaa;cursor:pointer}.mua-btn-primary{background:#168bd2;color:#fff;border-color:#168bd2}.mua-btn-danger{background:#c62828;color:#fff;border-color:#c62828}.mua-tool{display:grid;grid-template-columns:22px minmax(0,1fr);gap:8px;padding:9px 0;border-top:1px solid rgba(128,128,128,.2);align-items:start}.mua-tool:first-of-type{border-top:0}.mua-tool code{font-size:.86em;overflow-wrap:anywhere}.mua-tool small{display:block;color:#777;margin-top:3px;line-height:1.35}.mua-section{margin-top:24px}.mua-summary{font-size:1.08em}.mua-preset{margin:0}.mua-details td:first-child{width:190px;font-weight:600}@media(max-width:700px){.mua-hero{display:block}.mua-actions{display:grid}.mua-btn{width:100%}}
|
.mua-wrap{max-width:1280px}.mua-hero{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;margin:12px 0 20px}.mua-title h2{margin:0 0 5px;font-size:1.55rem}.mua-muted{color:#777}.mua-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(285px,1fr));gap:14px;margin:14px 0;align-items:start}.mua-card{border:1px solid #d8d8d8;border-radius:9px;padding:16px;background:rgba(255,255,255,.03)}.mua-card h3{margin:0 0 8px}.mua-status{padding:11px 14px;border-radius:7px;margin:10px 0;border:1px solid}.mua-status.ok,.mua-risk-safe{border-color:#43a047;background:rgba(67,160,71,.11)}.mua-status.err,.mua-risk-danger{border-color:#e53935;background:rgba(229,57,53,.11)}.mua-status.warn,.mua-risk-warn{border-color:#fb8c00;background:rgba(251,140,0,.12)}.mua-risk-notice{border-color:#1e88e5;background:rgba(30,136,229,.10)}.mua-pill{display:inline-block;padding:3px 9px;border-radius:20px;font-size:.82em;font-weight:600;border:1px solid currentColor}.mua-endpoint{font-size:1.05em;word-break:break-all}.mua-key{display:block;padding:12px;border:1px solid #43a047;border-radius:6px;word-break:break-all;font-size:1.04em;background:rgba(67,160,71,.08)}.mua-actions{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}.mua-btn{padding:8px 14px;border-radius:5px;border:1px solid #aaa;cursor:pointer}.mua-btn-primary{background:#168bd2;color:#fff;border-color:#168bd2}.mua-btn-danger{background:#c62828;color:#fff;border-color:#c62828}.mua-tool{display:grid;grid-template-columns:22px minmax(0,1fr);gap:8px;padding:9px 0;border-top:1px solid rgba(128,128,128,.2);align-items:start}.mua-tool:first-of-type{border-top:0}.mua-tool code{font-size:.86em;overflow-wrap:anywhere}.mua-tool small{display:block;color:#777;margin-top:3px;line-height:1.35}.mua-section{margin-top:24px}.mua-summary{font-size:1.08em}.mua-preset{margin:0}.mua-details td:first-child{width:190px;font-weight:600}@media(max-width:700px){.mua-hero{display:block}.mua-actions{display:grid}.mua-btn{width:100%}}
|
||||||
|
.mua-btn:disabled{opacity:.45;cursor:not-allowed}.mua-switch{display:flex;align-items:flex-start;gap:10px;margin:12px 0}.mua-switch input{margin-top:4px}.mua-switch span{line-height:1.4}
|
||||||
</style>
|
</style>
|
||||||
<div class="mua-wrap">
|
<div class="mua-wrap">
|
||||||
<div class="mua-hero"><div class="mua-title"><h2>MUA · Mikes Unraid Agent</h2><div class="mua-muted">Sicherer MCP-Zugriff auf Docker, Netzwerk und Unraid-Systemfunktionen</div></div><div><span class="mua-pill <?= $running && $health_ok ? 'mua-risk-safe' : 'mua-risk-danger' ?>"><?= $running && $health_ok ? '● Dienst bereit' : '● Dienst gestört' ?></span></div></div>
|
<div class="mua-hero"><div class="mua-title"><h2>MUA · Mikes Unraid Agent</h2><div class="mua-muted">Sicherer MCP-Zugriff auf Docker, Netzwerk und Unraid-Systemfunktionen</div></div><div><span class="mua-pill <?= $running && $health_ok ? 'mua-risk-safe' : 'mua-risk-danger' ?>"><?= $running && $health_ok ? '● Dienst bereit' : '● Dienst gestört' ?></span></div></div>
|
||||||
@@ -160,6 +166,16 @@ $active_count = $all_tools_enabled ? count($all_tools) : count($enabled_tools);
|
|||||||
|
|
||||||
<div class="mua-card"><h3>MCP-Endpunkt</h3><code class="mua-endpoint"><?= htmlspecialchars($endpoint) ?></code><p class="mua-muted">Streamable HTTP · JSON-RPC 2.0 · Bearer-Authentifizierung erforderlich</p></div>
|
<div class="mua-card"><h3>MCP-Endpunkt</h3><code class="mua-endpoint"><?= htmlspecialchars($endpoint) ?></code><p class="mua-muted">Streamable HTTP · JSON-RPC 2.0 · Bearer-Authentifizierung erforderlich</p></div>
|
||||||
|
|
||||||
|
<div class="mua-section"><h2>Agent-Werkzeugcontainer</h2><div class="mua-card <?= !$toolbox_available ? '' : ($toolbox_enabled ? 'mua-risk-safe' : 'mua-risk-notice') ?>">
|
||||||
|
<?php if ($toolbox_available): ?>
|
||||||
|
<p class="mua-summary">✅ Gefunden: <?php foreach ($toolbox_containers as $i => $container): ?><?= $i ? ', ' : '' ?><strong><?= htmlspecialchars($container['name'] ?? 'unbekannt') ?></strong> <span class="mua-muted">(<?= htmlspecialchars($container['state'] ?? 'unbekannt') ?>)</span><?php endforeach; ?></p>
|
||||||
|
<form method="post"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="save_toolbox"><label class="mua-switch"><input type="checkbox" name="toolbox_enabled" <?= $toolbox_enabled ? 'checked' : '' ?>><span><strong>Werkzeugcontainer für KI-Agenten bekanntgeben</strong><br><span class="mua-muted">MUA nennt den dynamisch erkannten Container in seinen MCP-Anweisungen und erklärt die Nutzung über <code>docker exec</code>.</span></span></label><button type="submit" class="mua-btn mua-btn-primary">Einstellung speichern</button></form>
|
||||||
|
<?php else: ?>
|
||||||
|
<p class="mua-summary mua-muted">Kein Werkzeugcontainer erkannt</p>
|
||||||
|
<label class="mua-switch"><input type="checkbox" disabled><span><strong>Werkzeugcontainer für KI-Agenten bekanntgeben</strong><br><span class="mua-muted">Nicht verfügbar. Erstelle einen Docker-Container und versehe ihn mit dem Label <code>mike.ai.role=toolbox</code>. Danach diese Seite neu laden.</span></span></label><button type="button" class="mua-btn" disabled>Einstellung speichern</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div></div>
|
||||||
|
|
||||||
<div class="mua-section"><h2>Schnelle Sicherheitsprofile</h2><p class="mua-muted">„Nur Lesen“ ist die empfohlene Dauerstellung. Weitergehende Rechte nur bei konkretem Bedarf freigeben.</p><div class="mua-actions">
|
<div class="mua-section"><h2>Schnelle Sicherheitsprofile</h2><p class="mua-muted">„Nur Lesen“ ist die empfohlene Dauerstellung. Weitergehende Rechte nur bei konkretem Bedarf freigeben.</p><div class="mua-actions">
|
||||||
<?php foreach ([['preset_readonly','Nur Lesen (empfohlen)',''],['preset_operator','Betrieb + Diagnose',''],['preset_none','Alles sperren',''],['preset_all','Vollzugriff','danger']] as $preset): ?>
|
<?php foreach ([['preset_readonly','Nur Lesen (empfohlen)',''],['preset_operator','Betrieb + Diagnose',''],['preset_none','Alles sperren',''],['preset_all','Vollzugriff','danger']] as $preset): ?>
|
||||||
<form method="post" class="mua-preset" onsubmit="<?= $preset[0] === 'preset_all' ? "return confirm('Vollzugriff aktiviert auch Container-Umbau und uneingeschränkte Root-Shell. Wirklich freigeben?');" : '' ?>"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="<?= $preset[0] ?>"><button class="mua-btn <?= $preset[2] === 'danger' ? 'mua-btn-danger' : ($preset[0] === 'preset_readonly' ? 'mua-btn-primary' : '') ?>" type="submit"><?= $preset[1] ?></button></form>
|
<form method="post" class="mua-preset" onsubmit="<?= $preset[0] === 'preset_all' ? "return confirm('Vollzugriff aktiviert auch Container-Umbau und uneingeschränkte Root-Shell. Wirklich freigeben?');" : '' ?>"><input type="hidden" name="csrf_token" value="<?= htmlspecialchars($unraid_csrf) ?>"><input type="hidden" name="mua_csrf" value="<?= htmlspecialchars($csrf) ?>"><input type="hidden" name="action" value="<?= $preset[0] ?>"><button class="mua-btn <?= $preset[2] === 'danger' ? 'mua-btn-danger' : ($preset[0] === 'preset_readonly' ? 'mua-btn-primary' : '') ?>" type="submit"><?= $preset[1] ?></button></form>
|
||||||
|
|||||||
+20
-1
@@ -31,6 +31,7 @@ export interface MUAConfig {
|
|||||||
apiKey: string;
|
apiKey: string;
|
||||||
enabledTools: string[];
|
enabledTools: string[];
|
||||||
allToolsEnabled: boolean;
|
allToolsEnabled: boolean;
|
||||||
|
toolboxEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sichere Grundeinstellung für Neuinstallationen. Schreibzugriffe, aktive
|
// Sichere Grundeinstellung für Neuinstallationen. Schreibzugriffe, aktive
|
||||||
@@ -68,6 +69,7 @@ export function parseConfig(content: string): MUAConfig {
|
|||||||
apiKey: "",
|
apiKey: "",
|
||||||
enabledTools: [...SAFE_DEFAULT_TOOLS],
|
enabledTools: [...SAFE_DEFAULT_TOOLS],
|
||||||
allToolsEnabled: false,
|
allToolsEnabled: false,
|
||||||
|
toolboxEnabled: false,
|
||||||
};
|
};
|
||||||
for (const line of content.split("\n")) {
|
for (const line of content.split("\n")) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
@@ -92,6 +94,8 @@ export function parseConfig(content: string): MUAConfig {
|
|||||||
.filter((s) => s.length > 0);
|
.filter((s) => s.length > 0);
|
||||||
cfg.allToolsEnabled = false;
|
cfg.allToolsEnabled = false;
|
||||||
}
|
}
|
||||||
|
} else if (key === "MUA_TOOLBOX_ENABLED") {
|
||||||
|
cfg.toolboxEnabled = ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// r019 replaces repeated single-container update calls with an idempotent
|
// r019 replaces repeated single-container update calls with an idempotent
|
||||||
@@ -139,7 +143,9 @@ function loadConfig(): MUAConfig {
|
|||||||
if (envToken && envToken.length > 0) {
|
if (envToken && envToken.length > 0) {
|
||||||
const envTools = process.env["MUA_ENABLED_TOOLS"] ?? "";
|
const envTools = process.env["MUA_ENABLED_TOOLS"] ?? "";
|
||||||
cachedConfig = parseConfig(
|
cachedConfig = parseConfig(
|
||||||
`MUA_API_KEY=${envToken}\nMUA_ENABLED_TOOLS=${envTools || "none"}\n`,
|
`MUA_API_KEY=${envToken}\n` +
|
||||||
|
`MUA_ENABLED_TOOLS=${envTools || "none"}\n` +
|
||||||
|
`MUA_TOOLBOX_ENABLED=${process.env["MUA_TOOLBOX_ENABLED"] ?? "false"}\n`,
|
||||||
);
|
);
|
||||||
return cachedConfig;
|
return cachedConfig;
|
||||||
}
|
}
|
||||||
@@ -162,6 +168,7 @@ function loadConfig(): MUAConfig {
|
|||||||
apiKey: newToken,
|
apiKey: newToken,
|
||||||
enabledTools: [...SAFE_DEFAULT_TOOLS],
|
enabledTools: [...SAFE_DEFAULT_TOOLS],
|
||||||
allToolsEnabled: false,
|
allToolsEnabled: false,
|
||||||
|
toolboxEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -190,6 +197,8 @@ function writeConfigFile(cfg: MUAConfig): void {
|
|||||||
`MUA_API_KEY=${cfg.apiKey}`,
|
`MUA_API_KEY=${cfg.apiKey}`,
|
||||||
"# Aktive Tools (all = alle, none = keine, oder kommagetrennte Tool-Namen)",
|
"# Aktive Tools (all = alle, none = keine, oder kommagetrennte Tool-Namen)",
|
||||||
`MUA_ENABLED_TOOLS=${toolsValue}`,
|
`MUA_ENABLED_TOOLS=${toolsValue}`,
|
||||||
|
"# Optionalen, per Docker-Label erkannten Werkzeugcontainer bekanntgeben",
|
||||||
|
`MUA_TOOLBOX_ENABLED=${cfg.toolboxEnabled ? "true" : "false"}`,
|
||||||
"",
|
"",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
writeFileSync(CONFIG_FILE, content, { mode: 0o600 });
|
writeFileSync(CONFIG_FILE, content, { mode: 0o600 });
|
||||||
@@ -242,6 +251,16 @@ export function setEnabledTools(names: string[], allToolsEnabled = false): void
|
|||||||
saveConfig(cfg);
|
saveConfig(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getToolboxEnabled(): boolean {
|
||||||
|
return loadConfig().toolboxEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToolboxEnabled(enabled: boolean): void {
|
||||||
|
const cfg = loadConfig();
|
||||||
|
cfg.toolboxEnabled = enabled;
|
||||||
|
saveConfig(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prüft, ob ein Tool aktiv ist.
|
* Prüft, ob ein Tool aktiv ist.
|
||||||
* true = aktiv, false = deaktiviert.
|
* true = aktiv, false = deaktiviert.
|
||||||
|
|||||||
+35
-1
@@ -15,7 +15,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
|||||||
|
|
||||||
// ── Konstanten ──────────────────────────────────────────────────────────
|
// ── Konstanten ──────────────────────────────────────────────────────────
|
||||||
export const MUA_SERVER_NAME = "mua";
|
export const MUA_SERVER_NAME = "mua";
|
||||||
export const MUA_VERSION = "2026.08.28.r025";
|
export const MUA_VERSION = "2026.08.29.r026";
|
||||||
export const MUA_PROTOCOL_VERSION = "2025-03-26";
|
export const MUA_PROTOCOL_VERSION = "2025-03-26";
|
||||||
export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php";
|
export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php";
|
||||||
export const STATUS_HELPER = "/usr/local/bin/unraid-mcp-status-helper.php";
|
export const STATUS_HELPER = "/usr/local/bin/unraid-mcp-status-helper.php";
|
||||||
@@ -314,6 +314,40 @@ export async function dockerExec(cmd: string, timeoutSec = 60): Promise<string>
|
|||||||
return runLocal("docker", `/usr/bin/docker ${cmd} 2>&1`, timeoutSec);
|
return runLocal("docker", `/usr/bin/docker ${cmd} 2>&1`, timeoutSec);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ToolboxContainer {
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
state: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find self-declared agent toolboxes without relying on a container name. */
|
||||||
|
export async function discoverToolboxContainers(): Promise<ToolboxContainer[]> {
|
||||||
|
const raw = await dockerExec(
|
||||||
|
"ps -a --filter 'label=mike.ai.role=toolbox' --format '{{json .}}'",
|
||||||
|
30,
|
||||||
|
);
|
||||||
|
if (!raw.trim()) return [];
|
||||||
|
const rows: ToolboxContainer[] = [];
|
||||||
|
for (const line of raw.split("\n")) {
|
||||||
|
if (!line.trim().startsWith("{")) continue;
|
||||||
|
try {
|
||||||
|
const item = JSON.parse(line) as Record<string, unknown>;
|
||||||
|
const name = String(item["Names"] ?? "").trim();
|
||||||
|
if (!name) continue;
|
||||||
|
rows.push({
|
||||||
|
name,
|
||||||
|
image: String(item["Image"] ?? ""),
|
||||||
|
state: String(item["State"] ?? "").toLowerCase(),
|
||||||
|
status: String(item["Status"] ?? ""),
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// Ignore unrelated Docker diagnostics; absence remains a valid result.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delegiert eine Write-Operation an den PHP-Helper.
|
* Delegiert eine Write-Operation an den PHP-Helper.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+64
-1
@@ -17,6 +17,7 @@ import {
|
|||||||
MUA_SERVER_NAME,
|
MUA_SERVER_NAME,
|
||||||
MUA_VERSION,
|
MUA_VERSION,
|
||||||
MUA_PROTOCOL_VERSION,
|
MUA_PROTOCOL_VERSION,
|
||||||
|
discoverToolboxContainers,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import { TOOLS, toolByName, getToolRisk } from "./tools";
|
import { TOOLS, toolByName, getToolRisk } from "./tools";
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +28,8 @@ import {
|
|||||||
getMaskedApiKey,
|
getMaskedApiKey,
|
||||||
generateApiKey,
|
generateApiKey,
|
||||||
setEnabledTools,
|
setEnabledTools,
|
||||||
|
getToolboxEnabled,
|
||||||
|
setToolboxEnabled,
|
||||||
} from "./auth";
|
} from "./auth";
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { spawn } from "node:child_process";
|
import { spawn } from "node:child_process";
|
||||||
@@ -40,6 +43,40 @@ const MAX_CONCURRENT_TOOL_CALLS = Number(process.env["MUA_MAX_CONCURRENT_TOOL_CA
|
|||||||
const CORS_ORIGIN = process.env["MUA_CORS_ORIGIN"] ?? "";
|
const CORS_ORIGIN = process.env["MUA_CORS_ORIGIN"] ?? "";
|
||||||
const AUDIT_LOG = process.env["MUA_AUDIT_LOG"] ?? "/var/log/plugins/mua-audit.log";
|
const AUDIT_LOG = process.env["MUA_AUDIT_LOG"] ?? "/var/log/plugins/mua-audit.log";
|
||||||
|
|
||||||
|
async function toolboxGuidance(): Promise<{
|
||||||
|
available: boolean;
|
||||||
|
enabled: boolean;
|
||||||
|
containers: Awaited<ReturnType<typeof discoverToolboxContainers>>;
|
||||||
|
instruction: string;
|
||||||
|
}> {
|
||||||
|
let containers: Awaited<ReturnType<typeof discoverToolboxContainers>> = [];
|
||||||
|
try {
|
||||||
|
containers = await discoverToolboxContainers();
|
||||||
|
} catch {
|
||||||
|
// Docker can be temporarily unavailable during Unraid startup.
|
||||||
|
}
|
||||||
|
const available = containers.length > 0;
|
||||||
|
const enabled = getToolboxEnabled() && available;
|
||||||
|
let instruction = "";
|
||||||
|
if (enabled) {
|
||||||
|
const running = containers.filter((c) => c.state === "running");
|
||||||
|
if (running.length > 0) {
|
||||||
|
instruction = isToolEnabled("unraid_system_shell")
|
||||||
|
? `Optional agent toolbox available: ${running.map((c) => c.name).join(", ")}. ` +
|
||||||
|
"When a command-line utility is missing, use unraid_system_shell and run it with " +
|
||||||
|
"docker exec inside a labeled toolbox container. Install missing utilities there, " +
|
||||||
|
"not in the MCP client or Hermes container."
|
||||||
|
: `Agent toolbox detected (${running.map((c) => c.name).join(", ")}), but ` +
|
||||||
|
"unraid_system_shell is disabled, so the toolbox cannot currently be used through MUA.";
|
||||||
|
} else {
|
||||||
|
instruction =
|
||||||
|
`Agent toolbox configured but currently stopped: ${containers.map((c) => c.name).join(", ")}. ` +
|
||||||
|
"Do not assume its tools are available until the container is running.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { available, enabled, containers, instruction };
|
||||||
|
}
|
||||||
|
|
||||||
// ── JSON-RPC Helpers ────────────────────────────────────────────────────
|
// ── JSON-RPC Helpers ────────────────────────────────────────────────────
|
||||||
function rpcResult(id: number | string | null, result: unknown) {
|
function rpcResult(id: number | string | null, result: unknown) {
|
||||||
return { jsonrpc: "2.0", id, result };
|
return { jsonrpc: "2.0", id, result };
|
||||||
@@ -104,11 +141,13 @@ async function handleMcpRequest(
|
|||||||
if (method === "initialize") {
|
if (method === "initialize") {
|
||||||
const sid = newSessionId();
|
const sid = newSessionId();
|
||||||
touchSession(sid);
|
touchSession(sid);
|
||||||
|
const toolbox = await toolboxGuidance();
|
||||||
return {
|
return {
|
||||||
response: rpcResult(id, {
|
response: rpcResult(id, {
|
||||||
protocolVersion: MUA_PROTOCOL_VERSION,
|
protocolVersion: MUA_PROTOCOL_VERSION,
|
||||||
capabilities: { tools: {} },
|
capabilities: { tools: {} },
|
||||||
serverInfo: { name: MUA_SERVER_NAME, version: MUA_VERSION },
|
serverInfo: { name: MUA_SERVER_NAME, version: MUA_VERSION },
|
||||||
|
...(toolbox.instruction ? { instructions: toolbox.instruction } : {}),
|
||||||
}),
|
}),
|
||||||
sessionId: sid,
|
sessionId: sid,
|
||||||
};
|
};
|
||||||
@@ -123,13 +162,17 @@ async function handleMcpRequest(
|
|||||||
// ── tools/list ────────────────────────────────────────────────────────
|
// ── tools/list ────────────────────────────────────────────────────────
|
||||||
if (method === "tools/list") {
|
if (method === "tools/list") {
|
||||||
if (sessionId) touchSession(sessionId);
|
if (sessionId) touchSession(sessionId);
|
||||||
|
const toolbox = await toolboxGuidance();
|
||||||
// Tool-Filter: nur aktive Tools anzeigen
|
// Tool-Filter: nur aktive Tools anzeigen
|
||||||
const activeTools = TOOLS.filter((t) => isToolEnabled(t.name));
|
const activeTools = TOOLS.filter((t) => isToolEnabled(t.name));
|
||||||
return {
|
return {
|
||||||
response: rpcResult(id, {
|
response: rpcResult(id, {
|
||||||
tools: activeTools.map((t) => ({
|
tools: activeTools.map((t) => ({
|
||||||
name: t.name,
|
name: t.name,
|
||||||
description: t.description,
|
description:
|
||||||
|
t.name === "unraid_system_shell" && toolbox.instruction
|
||||||
|
? `${t.description} ${toolbox.instruction}`
|
||||||
|
: t.description,
|
||||||
inputSchema: t.inputSchema,
|
inputSchema: t.inputSchema,
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
@@ -421,6 +464,7 @@ try {
|
|||||||
|
|
||||||
// GET: Config lesen
|
// GET: Config lesen
|
||||||
if (method === "GET") {
|
if (method === "GET") {
|
||||||
|
const toolbox = await toolboxGuidance();
|
||||||
return Response.json({
|
return Response.json({
|
||||||
apiKeyConfigured: true,
|
apiKeyConfigured: true,
|
||||||
apiKeyMasked: getMaskedApiKey(),
|
apiKeyMasked: getMaskedApiKey(),
|
||||||
@@ -431,6 +475,13 @@ try {
|
|||||||
description: t.description,
|
description: t.description,
|
||||||
risk: getToolRisk(t.name),
|
risk: getToolRisk(t.name),
|
||||||
})),
|
})),
|
||||||
|
toolbox: {
|
||||||
|
configured: getToolboxEnabled(),
|
||||||
|
available: toolbox.available,
|
||||||
|
enabled: toolbox.enabled,
|
||||||
|
label: "mike.ai.role=toolbox",
|
||||||
|
containers: toolbox.containers,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -452,13 +503,25 @@ try {
|
|||||||
.filter((t): t is string => typeof t === "string" && known.has(t));
|
.filter((t): t is string => typeof t === "string" && known.has(t));
|
||||||
setEnabledTools(tools, body["allToolsEnabled"] === true);
|
setEnabledTools(tools, body["allToolsEnabled"] === true);
|
||||||
}
|
}
|
||||||
|
if (typeof body["toolboxEnabled"] === "boolean") {
|
||||||
|
const toolbox = await toolboxGuidance();
|
||||||
|
setToolboxEnabled(body["toolboxEnabled"] === true && toolbox.available);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolbox = await toolboxGuidance();
|
||||||
return Response.json({
|
return Response.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
generatedApiKey,
|
generatedApiKey,
|
||||||
apiKeyMasked: getMaskedApiKey(),
|
apiKeyMasked: getMaskedApiKey(),
|
||||||
enabledTools: getEnabledTools(),
|
enabledTools: getEnabledTools(),
|
||||||
allToolsEnabled: getAllToolsEnabled(),
|
allToolsEnabled: getAllToolsEnabled(),
|
||||||
|
toolbox: {
|
||||||
|
configured: getToolboxEnabled(),
|
||||||
|
available: toolbox.available,
|
||||||
|
enabled: getToolboxEnabled() && toolbox.available,
|
||||||
|
label: "mike.ai.role=toolbox",
|
||||||
|
containers: toolbox.containers,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ describe("secure tool configuration", () => {
|
|||||||
expect(cfg.enabledTools).toEqual([]);
|
expect(cfg.enabledTools).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("toolbox integration is opt-in and parsed independently", () => {
|
||||||
|
const off = parseConfig("MUA_API_KEY=test\nMUA_ENABLED_TOOLS=none\n");
|
||||||
|
const on = parseConfig(
|
||||||
|
"MUA_API_KEY=test\nMUA_ENABLED_TOOLS=none\nMUA_TOOLBOX_ENABLED=true\n",
|
||||||
|
);
|
||||||
|
expect(off.toolboxEnabled).toBe(false);
|
||||||
|
expect(on.toolboxEnabled).toBe(true);
|
||||||
|
expect(on.enabledTools).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test("an enabled legacy update also exposes the safer batch update", () => {
|
test("an enabled legacy update also exposes the safer batch update", () => {
|
||||||
const cfg = parseConfig(
|
const cfg = parseConfig(
|
||||||
"MUA_API_KEY=test\nMUA_ENABLED_TOOLS=unraid_docker_update\n",
|
"MUA_API_KEY=test\nMUA_ENABLED_TOOLS=unraid_docker_update\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user