r025: expose direct long-running Unraid terminal
This commit is contained in:
1 parent
b0070861d2
commit
3c03924110
9 files changed
+73
-21
No files matched your search
+1
-1
@@ -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";
|
||||
|
||||
+18
-1
@@ -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));
|
||||
|
||||
+28
-12
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user