From ac725416b8aefcf7cf54e96362cc314698cc4eb0 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 25 Aug 2026 08:42:30 +0200 Subject: [PATCH] Simplify Athena operator maintenance workflow --- dev/test_athena_operator.py | 11 ++++++- docs/CURRENT_REFERENCE.md | 16 ++++++++++ docs/PLATFORM_CONTEXT_MCP.md | 30 +++++++++++++++---- docs/PLATFORM_OVERVIEW.md | 3 +- docs/QWEN_OPERATOR_CONTEXT.md | 14 +++++++-- platform/hermes/config.yaml | 23 +++++++++++++- .../hermes/skills/athena-operator/SKILL.md | 11 +++++-- platform/mcp/athena_operator_mcp.py | 10 +++++-- platform/mcp/compose.yaml | 2 +- platform/mcp/platform_context_mcp.py | 12 ++++---- platform/operator/athena_operatord.py | 27 ++++++++++------- 11 files changed, 126 insertions(+), 33 deletions(-) diff --git a/dev/test_athena_operator.py b/dev/test_athena_operator.py index a12a2cc..52b36f9 100644 --- a/dev/test_athena_operator.py +++ b/dev/test_athena_operator.py @@ -136,14 +136,23 @@ class OperatorTests(unittest.TestCase): return {"argv": argv, "exit_code": 0, "output": " compose.yaml | 2 +-"} return {"argv": argv, "exit_code": 0, "output": "ok"} self.module.run = fake_run - payload, preview = self.module.normalise_operation("git_publish", {"message": "Update Athena platform"}) + payload, preview = self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": ["compose.yaml"]}) self.assertEqual(payload["reviewed_status"], "M compose.yaml") + self.assertEqual(payload["paths"], ["compose.yaml"]) self.assertIn("compose.yaml | 2 +-", preview) def test_git_publish_rejects_empty_repository(self): self.module.run = lambda argv, **kwargs: {"argv": argv, "exit_code": 0, "output": ""} with self.assertRaises(RuntimeError): + self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": ["compose.yaml"]}) + + def test_git_publish_requires_explicit_safe_paths(self): + with self.assertRaises(ValueError): self.module.normalise_operation("git_publish", {"message": "Update Athena platform"}) + with self.assertRaises(ValueError): + self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": ["compose.yaml", "compose.yaml"]}) + with self.assertRaises((ValueError, PermissionError)): + self.module.normalise_operation("git_publish", {"message": "Update Athena platform", "paths": [".git/config"]}) def test_path_traversal_and_protected_paths_are_rejected(self): for path in ("../etc/passwd", ".git/config", "/etc/passwd", "secrets/key"): diff --git a/docs/CURRENT_REFERENCE.md b/docs/CURRENT_REFERENCE.md index 7c5e2de..f217fcd 100644 --- a/docs/CURRENT_REFERENCE.md +++ b/docs/CURRENT_REFERENCE.md @@ -89,6 +89,22 @@ Der Router übernimmt: ## Hermes Agent +- Kontextkompression läuft frühzeitig bei 65 Prozent des jeweiligen + Profilfensters. Alte große Werkzeugausgaben werden ab 50.000 Token zunächst + ohne Modellaufruf bereinigt; `tail_mode: lean` hält nach der Kompression einen + kleinen, zusammenhängenden jüngsten Abschnitt. Dadurch sollen keine + mehrfachen minutenlangen Zusammenfassungen eines bereits weit überfüllten + Threads mehr nötig werden. +- Die Kompressionszusammenfassung nutzt weiterhin dasselbe aktive Modell. Ein + kleineres Fast-Modell wäre zwar schneller, besitzt aber nicht genug Kontext, + um die vollständige Mitte einer Medium-, Large- oder Ultra-Sitzung sicher zu + verarbeiten. `reasoning_effort: none` vermeidet unnötiges Nachdenken beim + reinen Zusammenfassen. +- `Summarizing thread` ist eine echte zusätzliche Modellanfrage. Bei sehr alten + Sitzungen kann die Desktop-Anzeige nach abgeschlossener Kompression außerdem + veraltet stehen bleiben. Maßgeblich sind dann Sitzungsfortschritt und + Backend-Log, nicht das Label allein. + - Container: `mike-ai-hermes` - Standardsprache: Deutsch (`display.language`, deutsches `SOUL.md`) - Spracheingabe: lokales Faster-Whisper, Modell `base`, Sprachhinweis `de` diff --git a/docs/PLATFORM_CONTEXT_MCP.md b/docs/PLATFORM_CONTEXT_MCP.md index 73601b1..c4ba481 100644 --- a/docs/PLATFORM_CONTEXT_MCP.md +++ b/docs/PLATFORM_CONTEXT_MCP.md @@ -79,16 +79,36 @@ Code, Compose, Profile, Installer, Netzwerke, Services, Git und Secrets können ## Git und Recovery Die kanonische Quelle ist der private Gitea-Stand -`git@192.168.1.2:michael/AI-Profile-Router.git`, Branch `main`. Das +`ssh://git@192.168.1.2:33/michael/AI-Profile-Router.git`, Branch `main`, im +Working Tree `/data/mike-ai-operator/repository`. Das Installationsverzeichnis `/opt/mike-ai/stack` ist eine ausgerollte Kopie und kein Git-Working-Tree; `.mike-ai-source-commit` benennt den ausgerollten Commit. Der offizielle GitHub-MCP ist read-only und kann dieses private -Gitea-Repository weder ändern noch pushen. Dafür ist ein getrennt -autorisiertes Git-Werkzeug beziehungsweise ein administrativer Git-Workflow -notwendig. +Gitea-Repository weder ändern noch pushen. Dafür besitzt der Athena Operator +den autorisierten, strukturierten Arbeitsweg. Ein Modell darf weder im eigenen +Sandbox-Container einen weiteren Clone anlegen noch einen SSH-Schlüssel +anfordern oder kopieren. + +Der verbindliche Ablauf für dauerhafte Änderungen lautet: + +1. `athena_operator_prepare` und nach separater Benutzerfreigabe + `athena_operator_execute` mit `file_update`; dies schreibt dieselben + ausgewählten Dateien driftgeschützt in den kanonischen Working Tree und die + ausgerollte Kopie. +2. Prüfungen über die Operation `run_checks` ausführen. +3. Nur betroffene Dienste über `compose_deploy` ausrollen. +4. Ausschließlich die ausdrücklich angegebenen geänderten Pfade mit + `git_publish` committen und pushen. Fremde Dirty-Worktree-Dateien bleiben + unberührt. +5. Mit `recovery` einen neuen Recovery-Koffer erzeugen und prüfen. + +Das allgemeine Terminal ist weder Ersatz für diesen Ablauf noch ein Weg zu +Git-Schlüsseln. `/data/mike-ai-operator/repository` muss aus der +Modellsandbox nicht direkt erreichbar sein; der rootseitige Executor besitzt +den notwendigen Zugriff. Eine angewandte Dokumentationspflege ist erst vollständig abgeschlossen, wenn -getrennte, dafür autorisierte Werkzeuge Folgendes bestätigt haben: +die dafür vorgesehenen Athena-Operator-Operationen Folgendes bestätigt haben: 1. dieselbe Änderung ist im privaten Quellrepository geprüft, committed und gepusht; diff --git a/docs/PLATFORM_OVERVIEW.md b/docs/PLATFORM_OVERVIEW.md index a611c76..afa29c5 100644 --- a/docs/PLATFORM_OVERVIEW.md +++ b/docs/PLATFORM_OVERVIEW.md @@ -158,7 +158,8 @@ geändert. Die Abweichung wird benannt und zuerst geklärt. ## Wichtige Pfade ```text -/opt/mike-ai/stack installierte versionierte Plattformquelle +/opt/mike-ai/stack ausgerollte Plattformkopie; kein Git-Working-Tree +/data/mike-ai-operator/repository kanonischer Git-Working-Tree; nur über Athena Operator ändern /data/models produktive Modelle; für Inferenz read-only eingehängt /etc/mike-ai root-only Secrets und Standortkonfiguration /data persistente Daten- und Recovery-SSD diff --git a/docs/QWEN_OPERATOR_CONTEXT.md b/docs/QWEN_OPERATOR_CONTEXT.md index 7c8fe49..f0e1be9 100644 --- a/docs/QWEN_OPERATOR_CONTEXT.md +++ b/docs/QWEN_OPERATOR_CONTEXT.md @@ -97,10 +97,18 @@ Inferenzcontainer eingehängt. Dauerhafte nur in einen laufenden Container. Die Hauptbestandteile sind: Die kanonische Git-Quelle ist der private Gitea-Branch `main` unter -`git@192.168.1.2:michael/AI-Profile-Router.git`. `/opt/mike-ai/stack` ist kein -Working Tree; `.mike-ai-source-commit` bezeichnet den ausgerollten Stand. Der +`ssh://git@192.168.1.2:33/michael/AI-Profile-Router.git`; der vom Athena +Operator verwaltete Working Tree liegt unter +`/data/mike-ai-operator/repository`. `/opt/mike-ai/stack` ist kein Working +Tree; `.mike-ai-source-commit` bezeichnet den ausgerollten Stand. Der offizielle GitHub-MCP ist strikt read-only und kann Gitea nicht pflegen. Für -Commit und Push ist daher ein getrennt autorisierter Git-Arbeitsweg nötig. +dauerhafte Änderungen ist ausschließlich der strukturierte Athena-Operator- +Arbeitsweg vorgesehen: `file_update` ändert driftgeschützt den kanonischen +Working Tree und die ausgerollte Kopie, `run_checks` prüft, `compose_deploy` +rollt nur benannte Dienste aus, `git_publish` veröffentlicht nur ausdrücklich +ausgewählte Pfade und `recovery` erneuert den Recovery-Koffer. Lege niemals +einen zweiten Clone in der Sandbox an und fordere oder kopiere keinen +SSH-Schlüssel; der Operator besitzt bereits den autorisierten Hostzugriff. - Open WebUI als Benutzeroberfläche und Speicher für Arbeitsbereichsmodelle, Filter, Aktionen und Chats diff --git a/platform/hermes/config.yaml b/platform/hermes/config.yaml index ab62804..64b09d7 100644 --- a/platform/hermes/config.yaml +++ b/platform/hermes/config.yaml @@ -39,7 +39,7 @@ web: keyless_rescue: true agent: - max_turns: 100 + max_turns: 500 gateway_timeout: 3600 session_stall_timeout: 600 tool_loop_guardrails: @@ -57,6 +57,27 @@ agent: max_web_searches: 20 max_subagents: 8 +# Compact before a tool-heavy session can grow beyond the selected model's +# usable window. Large old tool results are pruned without an LLM call first; +# lean tail retention avoids several expensive back-to-back summary passes. +compression: + enabled: true + progress_notices: true + threshold: 0.65 + target_ratio: 0.15 + tail_mode: "lean" + protect_last_n: 20 + proactive_prune_tokens: 50000 + proactive_prune_min_result_chars: 4000 + proactive_prune_min_reclaim_tokens: 4096 + context_total_ceiling_seconds: 600 + +auxiliary: + compression: + provider: "main" + model: "" + reasoning_effort: "none" + # German is the platform default. The display setting localizes the static # messages Hermes currently supports; agent replies are governed by SOUL.md. display: diff --git a/platform/hermes/skills/athena-operator/SKILL.md b/platform/hermes/skills/athena-operator/SKILL.md index 6a9e463..1da1184 100644 --- a/platform/hermes/skills/athena-operator/SKILL.md +++ b/platform/hermes/skills/athena-operator/SKILL.md @@ -65,6 +65,9 @@ repository and use live measurements only as evidence of current state. 5. **Change the source of truth.** Modify repository sources, not only a live container. Prefer `athena_operator_prepare`; show its full preview and stop for the exact user confirmation before `athena_operator_execute`. + Use `file_update` for the exact source paths. Never clone the repository in + the Hermes sandbox and never request or copy an SSH key; the Operator owns + the canonical worktree and its deploy credentials. Completion: the approved ticket matches the intended content. 6. **Deploy narrowly.** Change only named services. Never restart the entire stack merely to activate one component. Completion: unrelated containers @@ -72,8 +75,9 @@ repository and use live measurements only as evidence of current state. 7. **Verify behavior.** Run syntax/config checks, focused tests, service health, and one bounded functional test. A running container alone is not proof. Completion: expected behavior and rollback path are both verified. -8. **Close the maintenance loop.** Update relevant docs, publish the Git - change, create a newer recovery bundle, then check maintenance status. +8. **Close the maintenance loop.** Update relevant docs, publish only the + explicitly selected changed paths with `git_publish`, create a newer + recovery bundle, then check maintenance status. Completion: source commit, deployed state, docs, and recovery agree. ## Persistent Versus Temporary Work @@ -102,7 +106,8 @@ repository and use live measurements only as evidence of current state. - A profile switch can terminate active generation and invalidate prompt cache. - A healthy container can still expose the wrong model, route, or tool set. - `/opt/mike-ai/stack` is deployed source, not automatically the canonical Git - worktree. Complete durable changes through the documented Git workflow. + worktree. Complete durable changes through Operator operations `file_update`, + `run_checks`, `compose_deploy`, `git_publish`, and `recovery`. - New skills are loaded at the next Hermes session; absence in the current session is expected. diff --git a/platform/mcp/athena_operator_mcp.py b/platform/mcp/athena_operator_mcp.py index 84779d0..7a3b7a3 100755 --- a/platform/mcp/athena_operator_mcp.py +++ b/platform/mcp/athena_operator_mcp.py @@ -63,7 +63,9 @@ TOOLS = [ "and SSH to configured remote systems. Prefer a single focused command and cap noisy output " "with the command itself. The server blocks power control and changes to Athena's SSH, LAN, " "WireGuard, firewall, boot, kernel, mounts and partitions so remote reachability cannot be " - "accidentally destroyed. Other commands execute immediately and must be verified afterwards." + "accidentally destroyed. Other commands execute immediately and must be verified afterwards. " + "Do not clone the canonical repository, request/copy an SSH key, or use Terminal as a substitute " + "for the durable file_update, git_publish and recovery workflow." ), "inputSchema": { "type": "object", @@ -84,12 +86,14 @@ TOOLS = [ "content-bound ticket, exact preview and confirmation phrase. Supported operations: " "file_update (write repository and deployed stack files), run_checks, compose_deploy, " "container_action, openwebui_sync, git_publish, model_download, benchmark, recovery. Show the complete " - "preview to the user and stop. Never execute in the same autonomous tool sequence. " + "preview to the user and stop. Never execute in the same autonomous tool sequence. This is the " + "supported durable source and Git path: never clone the repository inside a sandbox and never " + "request or copy an SSH key. " "file_update payload: {files:[{path,content,expected_sha256?}]}; run_checks: " "{checks:[operator-tests,openwebui-filter-tests,platform-verify,compose-main,compose-mcp]}; " "compose_deploy: {compose_file,services,build}; container_action: {action,containers}; " "openwebui_sync: {}; " - "git_publish: {message}; model_download: {url,destination,sha256?}; benchmark: " + "git_publish: {message,paths:[exact changed repository paths]}; model_download: {url,destination,sha256?}; benchmark: " "{script,arguments}; recovery: {label}." ), "inputSchema": { diff --git a/platform/mcp/compose.yaml b/platform/mcp/compose.yaml index eac8717..2a337cb 100644 --- a/platform/mcp/compose.yaml +++ b/platform/mcp/compose.yaml @@ -191,7 +191,7 @@ services: build: context: . dockerfile: Dockerfile.athena-operator - image: mike-ai/mcp-athena-operator:2.0.0 + image: mike-ai/mcp-athena-operator:2.1.0 container_name: mike-ai-mcp-athena-operator environment: ATHENA_OPERATOR_SOCKET: /operator/operator.sock diff --git a/platform/mcp/platform_context_mcp.py b/platform/mcp/platform_context_mcp.py index 16d49ca..42e0c13 100644 --- a/platform/mcp/platform_context_mcp.py +++ b/platform/mcp/platform_context_mcp.py @@ -428,7 +428,7 @@ def read_source(arguments: dict[str, Any]) -> dict[str, Any]: WORKFLOWS = { - "mcp": ["platform/mcp/compose.yaml", "platform/mcp/README.md", "compose.yaml", "docs/COMPONENTS.md", "docs/SECURITY.md", "docs/QWEN_OPERATOR_CONTEXT.md"], + "mcp": ["platform/mcp/compose.yaml", "platform/mcp/README.md", "platform/mcp/athena_operator_mcp.py", "platform/hermes/skills/athena-operator/SKILL.md", "compose.yaml", "docs/COMPONENTS.md", "docs/SECURITY.md", "docs/PLATFORM_CONTEXT_MCP.md", "docs/QWEN_OPERATOR_CONTEXT.md"], "model": ["config/install.env.example", "platform/models/manifest.example.yaml", "platform/profiles/", "docs/STANDARD_PROFILE_MATRIX.md", "docs/QWEN_OPERATOR_CONTEXT.md"], "profile": ["platform/profiles/", "router/router_profiles.json", "platform/openwebui/install-models.sh", "docs/STANDARD_PROFILE_MATRIX.md"], "tts": ["compose.yaml", "router/xtts_worker.py", "platform/scripts/rollback-tts-production.sh", "docs/XTTS_EVALUATION_2026-08-23.md"], @@ -453,15 +453,17 @@ def change_workflow(arguments: dict[str, Any]) -> dict[str, Any]: "Capture current state with the narrowest specialist tool.", "Read relevant versioned sources and identify documentation drift.", "Define rollback and protect SSH, LAN, WireGuard and the active inference path.", - "Change source-of-truth files, not only a running container.", - "Validate syntax/configuration and run a bounded synthetic test.", + "Prepare and apply source changes with Athena Operator operation file_update. Never clone the repository inside the sandbox and never request or copy an SSH key.", + "Validate syntax/configuration with Athena Operator operation run_checks and run a bounded synthetic test.", + "Deploy only named services with Athena Operator operation compose_deploy.", "Verify service health and remote reachability without reading chats or private payloads.", "Update PLATFORM_OVERVIEW/CURRENT_REFERENCE/QWEN_OPERATOR_CONTEXT and the affected runbook.", - "Commit and push the private Git repository using a separate authorized Git tool.", - "Create and verify a new encrypted recovery bundle and self-contained data-disk kit.", + "Commit and push only the explicitly selected changed paths with Athena Operator operation git_publish.", + "Create and verify a new encrypted recovery bundle and self-contained data-disk kit with Athena Operator operation recovery.", ], "hard_boundaries": [ "This context MCP does not modify services, Docker, networking, models or secrets.", + "The sandbox needs neither a Git clone nor an SSH key; Athena Operator owns the canonical repository and deploy credentials.", "No shutdown, reboot, kernel/driver, SSH, firewall or VPN change without exact user approval and rollback.", "Never claim Git or recovery is current until separately verified.", ], diff --git a/platform/operator/athena_operatord.py b/platform/operator/athena_operatord.py index 6fc8524..57ae0bc 100755 --- a/platform/operator/athena_operatord.py +++ b/platform/operator/athena_operatord.py @@ -310,12 +310,18 @@ def normalise_operation(operation: str, payload: dict[str, Any]) -> tuple[dict[s message = str(payload.get("message", "")) if not SAFE_COMMIT.fullmatch(message): raise ValueError("invalid commit message") - status = run(["git", "status", "--short"], cwd=REPOSITORY, check=True)["output"].strip() + paths = payload.get("paths") + if not isinstance(paths, list) or not 1 <= len(paths) <= MAX_FILES: + raise ValueError("paths must contain 1..24 repository files") + selected = [str(safe_relative(str(path))) for path in paths] + if len(set(selected)) != len(selected): + raise ValueError("paths must be unique") + status = run(["git", "status", "--short", "--", *selected], cwd=REPOSITORY, check=True)["output"].strip() if not status: - raise RuntimeError("repository has no changes to publish") - diff = run(["git", "diff", "--stat"], cwd=REPOSITORY, check=True)["output"] - preview = f"Commit message: {message}\n\nChanged and untracked files:\n{status}\n\nDiff summary:\n{diff}" - return {"message": message, "reviewed_status": status}, preview + raise RuntimeError("selected repository paths have no changes to publish") + diff = run(["git", "diff", "--stat", "--", *selected], cwd=REPOSITORY, check=True)["output"] + preview = f"Commit message: {message}\nSelected paths: {', '.join(selected)}\n\nSelected changes:\n{status}\n\nDiff summary:\n{diff}" + return {"message": message, "paths": selected, "reviewed_status": status}, preview if operation == "model_download": url = str(payload.get("url", "")) parsed = urllib.parse.urlparse(url) @@ -414,12 +420,13 @@ def execute_operation(ticket: str, operation: str, payload: dict[str, Any]) -> d raise FileNotFoundError("versioned Open WebUI synchronisation script is missing") return {"sync": run(["bash", str(installer)], cwd=STACK, timeout=1800, check=True)} if operation == "git_publish": - current_status = run(["git", "status", "--short"], cwd=REPOSITORY, check=True)["output"].strip() + selected = payload["paths"] + current_status = run(["git", "status", "--short", "--", *selected], cwd=REPOSITORY, check=True)["output"].strip() if current_status != payload["reviewed_status"]: - raise RuntimeError("repository changed after the Git publish preview") - run(["git", "add", "--all"], cwd=REPOSITORY, check=True) - run(["git", "diff", "--cached", "--check"], cwd=REPOSITORY, check=True) - commit = run(["git", "commit", "-m", payload["message"]], cwd=REPOSITORY, check=True) + raise RuntimeError("selected repository paths changed after the Git publish preview") + run(["git", "add", "--", *selected], cwd=REPOSITORY, check=True) + run(["git", "diff", "--cached", "--check", "--", *selected], cwd=REPOSITORY, check=True) + commit = run(["git", "commit", "-m", payload["message"], "--", *selected], cwd=REPOSITORY, check=True) pushed = run(["git", "push", "origin", "HEAD:main"], cwd=REPOSITORY, timeout=300, check=True) head = run(["git", "rev-parse", "HEAD"], cwd=REPOSITORY, check=True)["output"].strip() (STACK / ".mike-ai-source-commit").write_text(head + "\n")