Simplify Athena operator maintenance workflow
This commit is contained in:
@@ -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"):
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
],
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user