release: r019 idempotent batch container updates
This commit is contained in:
@@ -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` |
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user