release: r019 idempotent batch container updates

This commit is contained in:
Mikei386
2026-08-24 11:21:19 +02:00
parent 47c27b06c4
commit d90874ed18
11 changed files with 174 additions and 14 deletions
+4 -4
View File
@@ -76,10 +76,10 @@ Oder manuell:
```bash
# .txz von Gitea laden
curl -O http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.24.r018-x86_64-1.txz
curl -O http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.24.r019-x86_64-1.txz
# Installieren
upgradepkg --install-new mua-2026.08.24.r018-x86_64-1.txz
upgradepkg --install-new mua-2026.08.24.r019-x86_64-1.txz
```
### 2. Service starten
@@ -94,7 +94,7 @@ Der Service startet automatisch bei jedem Boot (SysVinit).
```bash
curl http://192.168.1.2:3002/health
# → {"status":"ok","version":"2026.08.24.r018","auth":"required"}
# → {"status":"ok","version":"2026.08.24.r019","auth":"required"}
```
---
@@ -212,7 +212,7 @@ Zusätzlich kann jedes Werkzeug einzeln nach Risikostufe freigegeben werden:
| Gruppe | Tools |
|--------|-------|
| **Docker (15)** | `unraid_docker_list`, `unraid_docker_inspect`, `unraid_docker_logs`, `unraid_docker_analyze_logs`, `unraid_docker_processes`, `unraid_docker_stats`, `unraid_docker_info`, `unraid_docker_update_status`, `unraid_docker_start`, `unraid_docker_stop`, `unraid_docker_restart`, `unraid_docker_create`, `unraid_docker_modify`, `unraid_docker_update`, `unraid_docker_rebuild` |
| **Docker (16)** | `unraid_docker_list`, `unraid_docker_inspect`, `unraid_docker_logs`, `unraid_docker_analyze_logs`, `unraid_docker_processes`, `unraid_docker_stats`, `unraid_docker_info`, `unraid_docker_update_status`, `unraid_docker_start`, `unraid_docker_stop`, `unraid_docker_restart`, `unraid_docker_create`, `unraid_docker_modify`, `unraid_docker_update`, `unraid_docker_update_verified_batch`, `unraid_docker_rebuild` |
| **Community Applications (3)** | `unraid_ca_search`, `unraid_ca_install_preview`, `unraid_ca_install` |
| **Netzwerk (6)** | `unraid_network_inventory`, `unraid_network_list`, `unraid_network_inspect`, `unraid_network_host_state`, `unraid_network_audit_tcp`, `unraid_network_lan_probe` |
| **System (9)** | `unraid_system_health`, `unraid_storage_status`, `unraid_disk_health`, `unraid_notifications_list`, `unraid_shares_list`, `unraid_share_inspect`, `unraid_system_connection_test`, `unraid_system_shell_readonly`, `unraid_system_shell` |
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mua",
"version": "2026.08.24.r018",
"version": "2026.08.24.r019",
"description": "Mikes Unraid Agent - MCP over HTTP (Streamable HTTP) for Unraid",
"type": "module",
"main": "src/index.ts",
+9 -4
View File
@@ -2,13 +2,13 @@
<!DOCTYPE PLUGIN [
<!ENTITY name "mua">
<!ENTITY author "Michael">
<!ENTITY version "2026.08.24.r018">
<!ENTITY version "2026.08.24.r019">
<!ENTITY launch "Settings/mua">
<!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 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.24.r018-x86_64-1.txz">
<!ENTITY txzSHA256 "174603af715b3f875a875563b6a67045c6af6b8166b6943780712d8074bc2071">
<!ENTITY txzURL "http://192.168.1.2:4000/michael/MUA-Mikes-Unraid-Agent/raw/branch/main/dist/mua-2026.08.24.r019-x86_64-1.txz">
<!ENTITY txzSHA256 "382435dbb98ae4818c4aced563f4247c6fca6b552d4a9de5695f9e4e98764d6c">
]>
<PLUGIN name="&name;"
@@ -23,6 +23,11 @@
>
<CHANGES>
### 2026.08.24.r019
- Ein gebündelter Docker-Updateablauf prüft echte Image-IDs, überspringt bereits aktuelle Images trotz veraltetem Unraid-Cache und erhält den ursprünglichen Laufzustand.
- Bis zu 25 ausdrücklich benannte Container werden in einem Werkzeugaufruf aktualisiert und anschließend kompakt auf Container-ID, Zustand, RestartCount und Health geprüft.
- Eine bestehende Freigabe des bisherigen Einzel-Update-Werkzeugs schaltet automatisch dessen sicherere Batch-Variante frei.
### 2026.08.24.r018
- Added focused, batched container-log analysis via `focus_terms` to avoid repeated shell/grep loops.
- Repaired the repository self-test command.
@@ -126,7 +131,7 @@ Das .txz enthält:
install/doinst.sh (läuft nach Installation)
===========================================
-->
<FILE Name="/boot/config/plugins/&name;/mua-2026.08.24.r018-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0">
<FILE Name="/boot/config/plugins/&name;/mua-2026.08.24.r019-x86_64-1.txz" Run="upgradepkg --install-new" Mode="755" Min="7.0.0">
<URL>&txzURL;</URL>
<SHA256>&txzSHA256;</SHA256>
</FILE>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mua",
"author": "Michael",
"version": "2026.08.24.r018",
"version": "2026.08.24.r019",
"minver": "7.0.0",
"pluginDirectory": "/usr/local/emhttp/plugins/mua",
"configDirectory": "/boot/config/plugins/mua",
+1 -1
View File
@@ -104,7 +104,7 @@ cat > "${BUILD_DIR}/install/slack-desc" << DESCEOF
|
|${PKG_NAME} - Mikes Unraid Agent
|MCP over HTTP (Streamable HTTP) Server für Unraid.
|33 Tools: Docker, Community Applications, Netzwerk und Unraid-System.
|34 Tools: Docker, Community Applications, Netzwerk und Unraid-System.
|Port: 3002, Endpunkt: /mcp
|
|Runtime: TypeScript (Bun Runtime, kompiliertes Binary)
+110 -1
View File
@@ -8,6 +8,7 @@
* create <template_name>
* modify <container> <field> <value>
* update <container>
* update-verified-batch <container1|container2|...>
* rebuild <container>
* ca-install <template_url> <container_name> <overrides_json> <start:true|false>
*
@@ -116,6 +117,109 @@ function rebuild_container(string $name, bool $pull_image = false, bool $force_s
out("Done: $Name");
}
function docker_inspect_value(string $format, string $name): string {
return docker_exec('inspect --format ' . escapeshellarg($format) . ' ' . escapeshellarg($name));
}
/**
* Pull and update several exact containers without trusting Unraid's cached
* update flag. The immutable image ID is the authority. A stale cache can
* therefore never cause a needless container recreation.
*/
function update_verified_batch(string $raw_names): void {
$names = array_values(array_unique(array_filter(array_map('trim', explode('|', $raw_names)))));
if (count($names) < 1 || count($names) > 25) {
fail('Expected 1-25 container names separated by |');
}
$results = [];
foreach ($names as $name) {
if (!preg_match('/^[a-zA-Z0-9._-]{1,128}$/', $name)) {
$results[] = ['container' => $name, 'result' => 'error', 'error' => 'invalid container name'];
continue;
}
try {
$tmpl = get_template_path($name);
[, $template_name, $repository] = xmlToCommand($tmpl);
if ($template_name !== $name) {
throw new RuntimeException("Template name mismatch: $template_name");
}
$before_id = docker_inspect_value('{{.Id}}', $name);
$before_image_id = docker_inspect_value('{{.Image}}', $name);
$before_state = docker_inspect_value('{{.State.Status}}', $name);
$was_running = docker_inspect_value('{{.State.Running}}', $name) === 'true';
$pull_output = docker_exec('pull ' . escapeshellarg($repository));
if (preg_match('/(?:^|\n)(?:Error response|error:|failed)/i', $pull_output)) {
throw new RuntimeException('Image pull failed: ' . substr($pull_output, 0, 500));
}
$pulled_image_id = docker_exec('image inspect --format ' . escapeshellarg('{{.Id}}') . ' ' . escapeshellarg($repository));
if ($pulled_image_id === '') {
throw new RuntimeException('Pulled image ID could not be resolved');
}
if ($before_image_id === $pulled_image_id) {
$results[] = [
'container' => $name,
'result' => 'already-current',
'recreated' => false,
'original_state' => $before_state,
'final_state' => $before_state,
'container_id_changed' => false,
'image_id_changed' => false,
];
continue;
}
ob_start();
rebuild_container($name, false, false);
ob_end_clean();
$after_id = docker_inspect_value('{{.Id}}', $name);
$after_image_id = docker_inspect_value('{{.Image}}', $name);
$after_state = docker_inspect_value('{{.State.Status}}', $name);
$restart_count = (int)docker_inspect_value('{{.RestartCount}}', $name);
$health = docker_inspect_value('{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}', $name);
$state_preserved = $was_running ? $after_state === 'running' : $after_state !== 'running';
$results[] = [
'container' => $name,
'result' => ($after_image_id === $pulled_image_id && $state_preserved) ? 'updated' : 'verification-failed',
'recreated' => true,
'original_state' => $before_state,
'final_state' => $after_state,
'state_preserved' => $state_preserved,
'container_id_changed' => $before_id !== $after_id,
'image_id_changed' => $before_image_id !== $after_image_id,
'restart_count' => $restart_count,
'health' => $health,
];
} catch (Throwable $error) {
if (ob_get_level() > 0) ob_end_clean();
$results[] = [
'container' => $name,
'result' => 'error',
'error' => substr($error->getMessage(), 0, 600),
];
}
}
$counts = [];
foreach ($results as $result) {
$key = (string)$result['result'];
$counts[$key] = ($counts[$key] ?? 0) + 1;
}
echo json_encode([
'schema_version' => '1.0',
'requested_count' => count($names),
'result_counts' => $counts,
'all_verified' => !isset($counts['error']) && !isset($counts['verification-failed']),
'containers' => $results,
], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
}
/**
* Set the text content of a DOM element.
* This is the actual value Unraid uses (not the Default attribute).
@@ -354,6 +458,11 @@ switch ($action) {
rebuild_container($arg1, true);
break;
case 'update-verified-batch':
if (!$arg1) fail('Missing container names');
update_verified_batch($arg1);
break;
case 'rebuild':
if (!$arg1) fail("Missing container name");
out("Rebuilding container: $arg1");
@@ -366,5 +475,5 @@ switch ($action) {
break;
default:
fail("Unknown action: $action. Usage: create|modify|update|rebuild|ca-install");
fail("Unknown action: $action. Usage: create|modify|update|update-verified-batch|rebuild|ca-install");
}
+11
View File
@@ -93,6 +93,17 @@ export function parseConfig(content: string): MUAConfig {
}
}
}
// r019 replaces repeated single-container update calls with an idempotent
// batch variant. If an administrator already enabled the older update
// capability, expose the safer equivalent automatically; this does not
// grant a new class of operation.
if (
!cfg.allToolsEnabled
&& cfg.enabledTools.includes("unraid_docker_update")
&& !cfg.enabledTools.includes("unraid_docker_update_verified_batch")
) {
cfg.enabledTools.push("unraid_docker_update_verified_batch");
}
return cfg;
}
+1 -1
View File
@@ -14,7 +14,7 @@ import { createHash, randomUUID } from "node:crypto";
// ── Konstanten ──────────────────────────────────────────────────────────
export const MUA_SERVER_NAME = "mua";
export const MUA_VERSION = "2026.08.24.r018";
export const MUA_VERSION = "2026.08.24.r019";
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";
+9
View File
@@ -22,6 +22,14 @@ describe("secure tool configuration", () => {
expect(cfg.allToolsEnabled).toBe(true);
expect(cfg.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",
);
expect(cfg.enabledTools).toContain("unraid_docker_update");
expect(cfg.enabledTools).toContain("unraid_docker_update_verified_batch");
});
});
describe("secret handling", () => {
@@ -40,6 +48,7 @@ describe("risk classification", () => {
test("classifies root shell and container changes as critical", () => {
expect(getToolRisk("unraid_system_shell")).toBe("critical");
expect(getToolRisk("unraid_docker_modify")).toBe("critical");
expect(getToolRisk("unraid_docker_update_verified_batch")).toBe("critical");
expect(getToolRisk("unraid_docker_restart")).toBe("write");
expect(getToolRisk("unraid_network_lan_probe")).toBe("active");
expect(getToolRisk("unraid_docker_list")).toBe("read");
+27 -1
View File
@@ -40,6 +40,7 @@ const CRITICAL_TOOLS = new Set([
"unraid_docker_create",
"unraid_docker_modify",
"unraid_docker_update",
"unraid_docker_update_verified_batch",
"unraid_docker_rebuild",
"unraid_system_shell",
"unraid_ca_install",
@@ -281,7 +282,8 @@ export const TOOLS: ToolDef[] = [
},
{
name: "unraid_docker_update",
description: "Update a container (pull latest image, rebuild).",
description:
"Update one explicitly named container (pull latest image and rebuild). For two or more containers, or when update status may be stale, use unraid_docker_update_verified_batch instead.",
inputSchema: {
type: "object",
properties: { container: str("Container/template name") },
@@ -289,6 +291,30 @@ export const TOOLS: ToolDef[] = [
},
handler: (a) => runPhpHelper("update", validateName(a["container"], "container")),
},
{
name: "unraid_docker_update_verified_batch",
description:
"Authoritative one-call Docker image update workflow for 1-25 explicitly named Unraid containers. Pulls each configured image, compares immutable image IDs, skips already-current containers even if Unraid's cached status is stale, rebuilds only when the image actually changed, preserves every original running/stopped state, and returns compact post-verification (container ID change, state, restart count and health). Use this after read-only update discovery when the user explicitly requested the updates.",
inputSchema: {
type: "object",
properties: {
containers: str(
"One to 25 exact container names separated by |, as returned by Unraid inventory/update status",
),
},
required: ["containers"],
additionalProperties: false,
},
handler: (a) => {
const raw = String(a["containers"] ?? "");
const containers = raw.split("|").map((name) => name.trim()).filter(Boolean);
if (containers.length < 1 || containers.length > 25) {
throw new Error("containers must contain 1-25 names separated by |");
}
const unique = [...new Set(containers.map((name) => validateName(name, "container")))];
return runPhpHelper("update-verified-batch", unique.join("|"));
},
},
{
name: "unraid_docker_rebuild",
description: