diff --git a/README.md b/README.md index e66bd50..9e63425 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ mcp_servers: url: http://192.168.1.2:3002/mcp headers: Authorization: Bearer - timeout: 300 + timeout: 1800 connect_timeout: 30 enabled: true ``` @@ -231,6 +231,13 @@ Zusätzlich kann jedes Werkzeug einzeln nach Risikostufe freigegeben werden: Deaktivierte Tools werden vom MCP-Server gefiltert — sie erscheinen nicht in `tools/list` und können nicht aufgerufen werden (→ `ERROR: Tool disabled`). +Bei freigegebenem Vollzugriff ist `unraid_system_shell` das direkte Terminal +auf dem Unraid-Host. Agenten sollen es für normale Docker-, Datei- und +Administrationsaufträge unmittelbar verwenden und weder den MUA-Quellcode +untersuchen noch Befehle über ein lokales Client-Terminal weiterreichen. Das +Werkzeug akzeptiert mehrzeilige Skripte bis 256 KiB und Laufzeiten bis 30 +Minuten. Nur noch längere Arbeiten gehören in `unraid_system_job_start`. + --- ## Service-Verwaltung diff --git a/dist/mua-2026.08.28.r025-x86_64-1.txz b/dist/mua-2026.08.28.r025-x86_64-1.txz new file mode 100644 index 0000000..17a903f Binary files /dev/null and b/dist/mua-2026.08.28.r025-x86_64-1.txz differ diff --git a/docs/HERMES.md b/docs/HERMES.md index c886421..844cab1 100644 --- a/docs/HERMES.md +++ b/docs/HERMES.md @@ -110,6 +110,14 @@ Abschließend genau einen unschädlichen Leseauftrag verwenden, beispielsweise: Erst wenn Verbindungstest, Werkzeugerkennung und dieser Leseaufruf erfolgreich sind, ist die Einbindung abgeschlossen. +Wenn der Benutzer MUA in der Unraid-WebGUI bewusst auf **Vollzugriff** gestellt +hat, darf Hermes das angebotene `unraid_system_shell` für ausdrücklich +beauftragte Änderungen direkt verwenden. Das ist bereits das Terminal des +Unraid-Hosts. Dafür weder MUA-Quellcode untersuchen noch das lokale +Hermes-Containerterminal als Zwischenstation verwenden. Normale Befehle und +mehrzeilige Skripte laufen synchron; nur Arbeiten über 30 Minuten verwenden +die asynchronen Job-Werkzeuge. + ## 5. Entfernen Nur die Hermes-Verbindung entfernen, nicht das MUA-Plugin auf Unraid: diff --git a/package.json b/package.json index 44313a3..c107c79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mua", - "version": "2026.08.25.r024", + "version": "2026.08.28.r025", "description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid", "type": "module", "main": "src/index.ts", diff --git a/plugin/mua.plg b/plugin/mua.plg index 8440923..4fc99d1 100644 --- a/plugin/mua.plg +++ b/plugin/mua.plg @@ -2,13 +2,13 @@ - + - - + + ]> +### 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. +- 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. + ### 2026.08.25.r024 - Container-Inspect liefert standardmäßig nur Status, Image, Netzwerkmodus und Ports; Umgebungsvariablennamen und Mounts sind nur noch über `detail=full` enthalten. - Reduziert große MCP-Ausgaben bei gezielten Containerdiagnosen und hält Secrets weiterhin vollständig aus den Antworten heraus. @@ -153,7 +157,7 @@ Das .txz enthält: install/doinst.sh (läuft nach Installation) =========================================== --> - + &txzURL; &txzSHA256; diff --git a/plugin/plugin.json b/plugin/plugin.json index 8710f39..31b549d 100644 --- a/plugin/plugin.json +++ b/plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "mua", "author": "Michael", - "version": "2026.08.25.r024", + "version": "2026.08.28.r025", "minver": "7.0.0", "pluginDirectory": "/usr/local/emhttp/plugins/mua", "configDirectory": "/boot/config/plugins/mua", diff --git a/src/helpers.ts b/src/helpers.ts index 4ca440f..8925306 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -15,7 +15,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } // ── Konstanten ────────────────────────────────────────────────────────── export const MUA_SERVER_NAME = "mua"; -export const MUA_VERSION = "2026.08.25.r024"; +export const MUA_VERSION = "2026.08.28.r025"; export const MUA_PROTOCOL_VERSION = "2025-03-26"; export const PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-helper.php"; export const STATUS_HELPER = "/usr/local/bin/unraid-mcp-status-helper.php"; diff --git a/src/security.test.ts b/src/security.test.ts index 827faef..27dd98e 100644 --- a/src/security.test.ts +++ b/src/security.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { parseConfig, SAFE_DEFAULT_TOOLS } from "./auth"; import { installCommunityApp, runReadOnlyCommand, sanitizeLogOutput } from "./helpers"; -import { getToolRisk } from "./tools"; +import { getToolRisk, toolByName } from "./tools"; describe("secure tool configuration", () => { test("new and incomplete configs use the read-only baseline", () => { @@ -81,6 +81,23 @@ describe("risk classification", () => { }); }); +describe("direct Unraid terminal", () => { + test("accepts a normal command with the extended timeout", async () => { + const tool = toolByName("unraid_system_shell"); + expect(tool).toBeDefined(); + const result = JSON.parse(await tool!.handler({ command: "printf ready", timeout_seconds: 301 })); + expect(result.exit_code).toBe(0); + expect(result.stdout).toBe("ready"); + }); + + test("rejects excessive timeouts and oversized scripts", async () => { + const tool = toolByName("unraid_system_shell"); + expect(tool).toBeDefined(); + expect(() => tool!.handler({ command: "true", timeout_seconds: 1801 })).toThrow(); + expect(() => tool!.handler({ command: "x".repeat(262_145) })).toThrow(); + }); +}); + describe("read-only shell", () => { test("executes an allowlisted program without a shell", async () => { const result = JSON.parse(await runReadOnlyCommand("ls", ["-ld", "/"], 5)); diff --git a/src/tools.ts b/src/tools.ts index 7571b39..ff1b18e 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -74,6 +74,22 @@ const int = (desc: string) => ({ type: "integer", description: desc }); const num = (desc: string) => ({ type: "number", description: desc }); const bool = (desc: string) => ({ type: "boolean", description: desc }); const empty = { type: "object", properties: {}, additionalProperties: false } as const; +const MAX_SHELL_COMMAND_CHARS = 262_144; +const shellCommand = (description: string) => ({ + type: "string", + minLength: 1, + maxLength: MAX_SHELL_COMMAND_CHARS, + description, +}); + +function validateShellCommand(value: unknown): string { + const command = typeof value === "string" ? value : ""; + if (command.trim() === "") throw new Error("command is required"); + if (command.length > MAX_SHELL_COMMAND_CHARS) { + throw new Error(`command must not exceed ${MAX_SHELL_COMMAND_CHARS} characters`); + } + return command; +} export const TOOLS: ToolDef[] = [ // ── Docker (14) ─────────────────────────────────────────────────────── @@ -634,23 +650,23 @@ export const TOOLS: ToolDef[] = [ { name: "unraid_system_shell", description: - "CRITICAL: Execute an unrestricted shell command on the Unraid host as root. Keep this tool disabled unless explicitly needed for a supervised maintenance session.", + "Direct terminal on the Unraid host as root. When the user explicitly asks to create or edit host files, run Docker commands, install software, or administer Unraid, use this tool directly. Do not inspect MUA internals and do not proxy the command through Hermes' local container terminal. Multi-line shell scripts are accepted. Commands may run for up to 30 minutes; use unraid_system_job_start only for work expected to take longer.", inputSchema: { type: "object", properties: { - command: str( - "Shell command to execute on the Unraid host (run via /bin/sh -c)", + command: shellCommand( + "Command or multi-line shell script to execute directly on the Unraid host via /bin/sh -c (maximum 262144 characters)", ), - timeout_seconds: int("Timeout in seconds (1-300, default 60)"), + timeout_seconds: int("Timeout in seconds (1-1800, default 300)"), }, required: ["command"], + additionalProperties: false, }, handler: (a) => { - const command = (a["command"] as string) ?? ""; - if (command.trim() === "") throw new Error("command is required"); - const timeout = Number(a["timeout_seconds"] ?? 60); - if (timeout < 1 || timeout > 300) { - throw new Error("timeout_seconds must be between 1 and 300"); + const command = validateShellCommand(a["command"]); + const timeout = Number(a["timeout_seconds"] ?? 300); + if (timeout < 1 || timeout > 1800) { + throw new Error("timeout_seconds must be between 1 and 1800"); } return runShell(command, timeout); }, @@ -658,16 +674,16 @@ export const TOOLS: ToolDef[] = [ { name: "unraid_system_job_start", description: - "CRITICAL: Start one explicitly authorized long-running unrestricted shell command on Unraid as an asynchronous job. Use this instead of unraid_system_shell when work may exceed an HTTP/tool timeout. Returns a job_id immediately; poll only with unraid_system_job_status and reserve calls for verification and cleanup.", + "Start a direct asynchronous terminal job on the Unraid host for work expected to exceed 30 minutes. For normal Docker, file and administration commands use unraid_system_shell instead. Accepts multi-line scripts and returns a job_id immediately.", inputSchema: { type: "object", properties: { - command: str("Long-running shell command to execute on Unraid via /bin/sh"), + command: shellCommand("Long-running command or multi-line shell script to execute on Unraid via /bin/sh (maximum 262144 characters)"), }, required: ["command"], additionalProperties: false, }, - handler: (a) => startShellJob(String(a["command"] ?? "")), + handler: (a) => startShellJob(validateShellCommand(a["command"])), }, { name: "unraid_system_job_status",