r026: add dynamic agent toolbox integration

This commit is contained in:
Mikei386 committed 2026-08-29 12:44:29 +02:00
1 parent 3c03924110
commit 63542bb29e
13 files changed
+184 -11

No files matched your search

+20 -1
View File
@@ -31,6 +31,7 @@ export interface MUAConfig {
apiKey: string;
enabledTools: string[];
allToolsEnabled: boolean;
toolboxEnabled: boolean;
}
// Sichere Grundeinstellung für Neuinstallationen. Schreibzugriffe, aktive
@@ -68,6 +69,7 @@ export function parseConfig(content: string): MUAConfig {
apiKey: "",
enabledTools: [...SAFE_DEFAULT_TOOLS],
allToolsEnabled: false,
toolboxEnabled: false,
};
for (const line of content.split("\n")) {
const trimmed = line.trim();
@@ -92,6 +94,8 @@ export function parseConfig(content: string): MUAConfig {
.filter((s) => s.length > 0);
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
@@ -139,7 +143,9 @@ function loadConfig(): MUAConfig {
if (envToken && envToken.length > 0) {
const envTools = process.env["MUA_ENABLED_TOOLS"] ?? "";
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;
}
@@ -162,6 +168,7 @@ function loadConfig(): MUAConfig {
apiKey: newToken,
enabledTools: [...SAFE_DEFAULT_TOOLS],
allToolsEnabled: false,
toolboxEnabled: false,
};
try {
@@ -190,6 +197,8 @@ function writeConfigFile(cfg: MUAConfig): void {
`MUA_API_KEY=${cfg.apiKey}`,
"# Aktive Tools (all = alle, none = keine, oder kommagetrennte Tool-Namen)",
`MUA_ENABLED_TOOLS=${toolsValue}`,
"# Optionalen, per Docker-Label erkannten Werkzeugcontainer bekanntgeben",
`MUA_TOOLBOX_ENABLED=${cfg.toolboxEnabled ? "true" : "false"}`,
"",
].join("\n");
writeFileSync(CONFIG_FILE, content, { mode: 0o600 });
@@ -242,6 +251,16 @@ export function setEnabledTools(names: string[], allToolsEnabled = false): void
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.
* true = aktiv, false = deaktiviert.
+35 -1
View File
@@ -15,7 +15,7 @@ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync }
// ── Konstanten ──────────────────────────────────────────────────────────
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 PHP_HELPER = "/usr/local/bin/unraid-docker-mcp-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);
}
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.
*/
+64 -1
View File
@@ -17,6 +17,7 @@ import {
MUA_SERVER_NAME,
MUA_VERSION,
MUA_PROTOCOL_VERSION,
discoverToolboxContainers,
} from "./helpers";
import { TOOLS, toolByName, getToolRisk } from "./tools";
import {
@@ -27,6 +28,8 @@ import {
getMaskedApiKey,
generateApiKey,
setEnabledTools,
getToolboxEnabled,
setToolboxEnabled,
} from "./auth";
import { randomBytes } from "node:crypto";
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 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 ────────────────────────────────────────────────────
function rpcResult(id: number | string | null, result: unknown) {
return { jsonrpc: "2.0", id, result };
@@ -104,11 +141,13 @@ async function handleMcpRequest(
if (method === "initialize") {
const sid = newSessionId();
touchSession(sid);
const toolbox = await toolboxGuidance();
return {
response: rpcResult(id, {
protocolVersion: MUA_PROTOCOL_VERSION,
capabilities: { tools: {} },
serverInfo: { name: MUA_SERVER_NAME, version: MUA_VERSION },
...(toolbox.instruction ? { instructions: toolbox.instruction } : {}),
}),
sessionId: sid,
};
@@ -123,13 +162,17 @@ async function handleMcpRequest(
// ── tools/list ────────────────────────────────────────────────────────
if (method === "tools/list") {
if (sessionId) touchSession(sessionId);
const toolbox = await toolboxGuidance();
// Tool-Filter: nur aktive Tools anzeigen
const activeTools = TOOLS.filter((t) => isToolEnabled(t.name));
return {
response: rpcResult(id, {
tools: activeTools.map((t) => ({
name: t.name,
description: t.description,
description:
t.name === "unraid_system_shell" && toolbox.instruction
? `${t.description} ${toolbox.instruction}`
: t.description,
inputSchema: t.inputSchema,
})),
}),
@@ -421,6 +464,7 @@ try {
// GET: Config lesen
if (method === "GET") {
const toolbox = await toolboxGuidance();
return Response.json({
apiKeyConfigured: true,
apiKeyMasked: getMaskedApiKey(),
@@ -431,6 +475,13 @@ try {
description: t.description,
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));
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({
ok: true,
generatedApiKey,
apiKeyMasked: getMaskedApiKey(),
enabledTools: getEnabledTools(),
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([]);
});
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", () => {
const cfg = parseConfig(
"MUA_API_KEY=test\nMUA_ENABLED_TOOLS=unraid_docker_update\n",