r026: add dynamic agent toolbox integration
This commit is contained in:
1 parent
3c03924110
commit
63542bb29e
13 files changed
+184
-11
No files matched your search
+20
-1
@@ -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
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user