r026: add dynamic agent toolbox integration

This commit is contained in:
Mikei386
2026-08-29 12:44:29 +02:00
parent 3c03924110
commit 63542bb29e
13 changed files with 184 additions and 11 deletions
+20
View File
@@ -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.
Binary file not shown.
+6
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
},
}); });
} }
+10
View File
@@ -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",