Add compact Athena MCP release workflow

This commit is contained in:
Mikei386
2026-08-25 08:51:46 +02:00
parent ac725416b8
commit 662913f8ab
9 changed files with 311 additions and 74 deletions
+39
View File
@@ -85,6 +85,45 @@ class OperatorTests(unittest.TestCase):
with self.assertRaises(RuntimeError):
self.module.execute({"ticket": proposal["ticket"], "confirmation": proposal["required_confirmation"]})
def test_patch_update_is_compact_and_updates_both_trees(self):
before = self.module.sha((self.repo / "docs" / "test.md").read_bytes())
proposal = self.module.prepare({
"operation": "patch_update",
"payload": {"files": [{
"path": "docs/test.md", "expected_sha256": before,
"patch": "--- a/docs/test.md\n+++ b/docs/test.md\n@@ -1 +1 @@\n-before\n+after\n",
}]},
})
self.assertIn("+after", proposal["preview"])
result = self.module.execute({"ticket": proposal["ticket"], "confirmation": proposal["required_confirmation"]})
self.assertEqual(result["operation"], "patch_update")
self.assertEqual((self.repo / "docs" / "test.md").read_text(), "after\n")
self.assertEqual((self.stack / "docs" / "test.md").read_text(), "after\n")
def test_patch_update_rejects_wrong_context_and_drift(self):
before = self.module.sha((self.repo / "docs" / "test.md").read_bytes())
with self.assertRaises(RuntimeError):
self.module.normalise_operation("patch_update", {"files": [{
"path": "docs/test.md", "expected_sha256": before,
"patch": "@@ -1 +1 @@\n-wrong\n+after\n",
}]})
with self.assertRaises(RuntimeError):
self.module.normalise_operation("patch_update", {"files": [{
"path": "docs/test.md", "expected_sha256": "0" * 64,
"patch": "@@ -1 +1 @@\n-before\n+after\n",
}]})
def test_mcp_release_normalises_one_complete_workflow(self):
before = self.module.sha((self.repo / "docs" / "test.md").read_bytes())
payload, preview = self.module.normalise_operation("mcp_release", {
"files": [{"path": "docs/test.md", "expected_sha256": before, "patch": "@@ -1 +1 @@\n-before\n+after\n"}],
"services": ["mcp-example"], "message": "Add example MCP service",
"checks": ["operator-tests"], "create_recovery": False,
})
self.assertEqual(payload["services"], ["mcp-example"])
self.assertEqual(payload["paths"], ["docs/test.md"])
self.assertIn("ONE MCP RELEASE", preview)
def test_structured_power_operations_do_not_exist(self):
self.assertNotIn("shell", self.module.ALLOWED_OPERATIONS)
for operation in ("shutdown", "reboot", "ssh", "network", "command"):
+6
View File
@@ -358,6 +358,12 @@ sind serverseitig blockiert.
Ein separater allgemeiner Shell-MCP wird nicht benötigt; die breite Fähigkeit
ist portabel im Athena Operator auf VPN-Port 8202 enthalten.
Seit Operator 2.2 werden kleine Änderungen als SHA-geschützte Unified Diffs
über `patch_update` übertragen. `mcp_release` fasst den üblichen vollständigen
MCP-Ablauf in einem bestätigten Auftrag zusammen: Patch, Tests, benannter
Deploy, OpenWebUI-Sync, selektiver Git-Publish und Recovery. Damit muss das
Modell keine kompletten Compose- oder Installationsdateien rekonstruieren.
## Bekannte Probleme des alten Hosts
- Systempartition vollständig gefüllt
+10 -10
View File
@@ -91,16 +91,16 @@ 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.
1. Kleine Änderungen mit `patch_update` als SHA-geschützten Unified Diff
vorbereiten. `file_update` ist neuen oder vollständig ersetzten Dateien
vorbehalten.
2. Für einen normalen MCP-Lifecycle bevorzugt ein einziges `mcp_release`
vorbereiten und nach separater Benutzerfreigabe ausführen. Es bündelt
Prüfungen, benannten Compose-Deploy, OpenWebUI-Sync, selektiven Git-Publish
und Recovery.
3. Einzeloperationen `run_checks`, `compose_deploy`, `git_publish` und
`recovery` nur für Diagnose oder bewusst partielle Wartung verwenden.
Fremde Dirty-Worktree-Dateien bleiben unberührt.
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
+5
View File
@@ -166,5 +166,10 @@ geändert. Die Abweichung wird benannt und zuerst geklärt.
/var/lib/docker/volumes Docker-Volumes, darunter OpenWebUI-Daten
```
Kleine Quelländerungen erfolgen über `patch_update` statt als vollständiger
Dateiersatz. Ein normaler MCP-Release erfolgt über `mcp_release`, das den
versionierten Gesamtweg von Patch und Tests bis Deploy, Client-Sync, selektivem
Git-Publish und Recovery kapselt.
Secrets unter `/etc/mike-ai` werden ausschließlich verschlüsselt gesichert und
gehören nie in Git, ein Wissensdokument oder einen Modellkontext.
+7 -2
View File
@@ -103,8 +103,13 @@ Operator verwaltete Working Tree liegt unter
Tree; `.mike-ai-source-commit` bezeichnet den ausgerollten Stand. Der
offizielle GitHub-MCP ist strikt read-only und kann Gitea nicht pflegen. Für
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`
Arbeitsweg vorgesehen: `patch_update` ändert kleine Stellen als SHA-geschützten
Unified Diff im kanonischen Working Tree und in der ausgerollten Kopie;
`file_update` ist neuen oder vollständig ersetzten Dateien vorbehalten. Für
einen vollständigen MCP-Lifecycle bündelt `mcp_release` Patch, Tests, benannten
Deploy, OpenWebUI-Sync, selektiven Git-Publish und Recovery in einem bestätigten
Ablauf. Vollständige Compose-Dateien oder Base64-Kopien sind dafür unnötig.
`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
@@ -65,7 +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
Prefer `patch_update` with a small unified diff and the current file SHA for
ordinary edits. Use `file_update` only for new files or intentional complete
replacements. 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.
@@ -75,7 +77,11 @@ 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 only the
8. **Close the maintenance loop.** For a normal MCP delivery, prefer one
confirmed `mcp_release`; it applies the reviewed patches, runs checks,
deploys only named services, synchronizes OpenWebUI, publishes selected
paths and creates recovery. Use separate operations only for diagnosis or a
deliberately partial workflow. Otherwise 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.
@@ -106,8 +112,9 @@ 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 Operator operations `file_update`,
`run_checks`, `compose_deploy`, `git_publish`, and `recovery`.
worktree. Use `patch_update` for compact edits and `mcp_release` for the
complete MCP lifecycle. Do not reconstruct whole Compose or installer files
for a small change.
- New skills are loaded at the next Hermes session; absence in the current
session is expected.
+10 -4
View File
@@ -10,7 +10,7 @@ import sys
from typing import Any
VERSION = "2.0.0"
VERSION = "2.2.0"
SOCKET_PATH = os.environ.get("ATHENA_OPERATOR_SOCKET", "/operator/operator.sock")
if hasattr(sys.stdin, "reconfigure"):
@@ -84,12 +84,18 @@ TOOLS = [
"description": (
"PREPARE a state-changing Athena operation. This never changes state. It returns a "
"content-bound ticket, exact preview and confirmation phrase. Supported operations: "
"file_update (write repository and deployed stack files), run_checks, compose_deploy, "
"patch_update (preferred for small changes), file_update (only for new or fully replaced files), "
"mcp_release (preferred one-ticket end-to-end MCP delivery), 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. 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: "
"patch_update payload: {files:[{path,patch,expected_sha256?}]} using standard unified diffs; "
"file_update payload: {files:[{path,content,expected_sha256?}]}; "
"mcp_release payload: {files:[patch entries],services,checks?,message,paths?,build?,"
"openwebui_sync?,create_recovery?,recovery_label?}. It applies drift-protected patches, runs checks, "
"deploys only named MCP services, syncs OpenWebUI, publishes only selected paths and creates recovery. "
"Use mcp_release instead of manually chaining all those operations. 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: {}; "
@@ -99,7 +105,7 @@ TOOLS = [
"inputSchema": {
"type": "object",
"properties": {
"operation": {"type": "string", "enum": ["file_update", "run_checks", "compose_deploy", "container_action", "openwebui_sync", "git_publish", "model_download", "benchmark", "recovery"]},
"operation": {"type": "string", "enum": ["patch_update", "file_update", "mcp_release", "run_checks", "compose_deploy", "container_action", "openwebui_sync", "git_publish", "model_download", "benchmark", "recovery"]},
"payload": {"type": "object"},
},
"required": ["operation", "payload"], "additionalProperties": False,
+1 -1
View File
@@ -191,7 +191,7 @@ services:
build:
context: .
dockerfile: Dockerfile.athena-operator
image: mike-ai/mcp-athena-operator:2.1.0
image: mike-ai/mcp-athena-operator:2.2.0
container_name: mike-ai-mcp-athena-operator
environment:
ATHENA_OPERATOR_SOCKET: /operator/operator.sock
+222 -53
View File
@@ -28,7 +28,7 @@ from pathlib import Path
from typing import Any
VERSION = "2.0.0"
VERSION = "2.2.0"
STACK = Path(os.environ.get("ATHENA_OPERATOR_STACK", "/opt/mike-ai/stack")).resolve()
REPOSITORY = Path(os.environ.get("ATHENA_OPERATOR_REPOSITORY", "/data/mike-ai-operator/repository")).resolve()
STATE = Path(os.environ.get("ATHENA_OPERATOR_STATE", "/data/mike-ai-operator/state")).resolve()
@@ -48,9 +48,11 @@ PROTECTED_CONTAINERS = {
"mike-ai-wireguard-gateway",
}
ALLOWED_OPERATIONS = {
"file_update", "run_checks", "compose_deploy", "container_action",
"file_update", "patch_update", "mcp_release", "run_checks", "compose_deploy", "container_action",
"openwebui_sync", "git_publish", "model_download", "benchmark", "recovery",
}
HUNK_HEADER = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
ALLOWED_CHECKS = {
"operator-tests": ["python3", "dev/test_athena_operator.py"],
"openwebui-filter-tests": ["python3", "dev/test_openwebui_filters.py"],
@@ -134,6 +136,85 @@ def sha(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def apply_unified_patch(before: str, patch: str) -> str:
"""Apply one ordinary unified diff without invoking a shell command."""
if not patch or len(patch.encode()) > MAX_FILE_BYTES:
raise ValueError("patch is empty or exceeds limit")
source = before.splitlines(keepends=True)
lines = patch.splitlines(keepends=True)
position = 0
output: list[str] = []
index = 0
if index < len(lines) and lines[index].startswith("--- "):
index += 1
if index < len(lines) and lines[index].startswith("+++ "):
index += 1
hunks = 0
while index < len(lines):
header = lines[index].rstrip("\r\n")
match = HUNK_HEADER.match(header)
if not match:
raise ValueError("patch must contain only standard unified-diff hunks")
old_start = int(match.group(1))
old_count = int(match.group(2) or "1")
new_count = int(match.group(4) or "1")
target = max(0, old_start - 1)
if target < position or target > len(source):
raise RuntimeError("patch hunk is outside the source file")
output.extend(source[position:target])
position = target
consumed = produced = 0
index += 1
while index < len(lines) and not lines[index].startswith("@@ "):
line = lines[index]
if line.startswith("\\ No newline at end of file"):
index += 1
continue
if not line or line[0] not in " +-":
raise ValueError("invalid unified-diff line")
marker, value = line[0], line[1:]
if marker in " -":
if position >= len(source) or source[position] != value:
raise RuntimeError("patch context does not match source")
if marker == " ":
output.append(source[position]); produced += 1
position += 1; consumed += 1
else:
output.append(value); produced += 1
index += 1
if consumed != old_count or produced != new_count:
raise RuntimeError("patch hunk line counts do not match its header")
hunks += 1
if not hunks:
raise ValueError("patch contains no hunks")
output.extend(source[position:])
return "".join(output)
def normalise_patches(files: Any) -> tuple[list[dict[str, Any]], str]:
if not isinstance(files, list) or not 1 <= len(files) <= MAX_FILES:
raise ValueError("files must contain 1..24 patch entries")
normal: list[dict[str, Any]] = []
previews: list[str] = []
for item in files:
if not isinstance(item, dict):
raise ValueError("each patch entry must be an object")
relative = safe_relative(str(item.get("path", "")))
target = source_file(REPOSITORY, relative)
before = target.read_text(encoding="utf-8", errors="strict") if target.is_file() else ""
before_sha = sha(before.encode())
expected = str(item.get("expected_sha256", ""))
if expected and expected != before_sha:
raise RuntimeError(f"source drift for {relative}")
patch = str(item.get("patch", ""))
after = apply_unified_patch(before, patch)
if len(after.encode()) > MAX_FILE_BYTES:
raise ValueError("patched file exceeds limit")
normal.append({"path": str(relative), "content": after, "before_sha256": before_sha, "after_sha256": sha(after.encode())})
previews.append(f"### {relative}\n{compact(patch)}")
return normal, "\n\n".join(previews)
def json_write(path: Path, value: Any) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
@@ -277,6 +358,44 @@ def normalise_operation(operation: str, payload: dict[str, Any]) -> tuple[dict[s
normal.append({"path": str(relative), "content": content, "before_sha256": before_sha, "after_sha256": sha(raw)})
previews.append(compact(diff))
return {"files": normal}, "\n".join(previews)
if operation == "patch_update":
files, preview = normalise_patches(payload.get("files"))
return {"files": files}, preview
if operation == "mcp_release":
files, patch_preview = normalise_patches(payload.get("files"))
checks = payload.get("checks") or ["operator-tests", "compose-mcp"]
if not isinstance(checks, list) or not checks or any(name not in ALLOWED_CHECKS for name in checks):
raise ValueError("unknown release check suite")
compose_file = str(payload.get("compose_file", "platform/mcp/compose.yaml"))
if compose_file != "platform/mcp/compose.yaml":
raise ValueError("MCP releases must use platform/mcp/compose.yaml")
services = payload.get("services") or []
if not isinstance(services, list) or not 1 <= len(services) <= 12 or any(not SAFE_NAME.fullmatch(str(x)) for x in services):
raise ValueError("invalid MCP service list")
message = str(payload.get("message", ""))
if not SAFE_COMMIT.fullmatch(message):
raise ValueError("invalid commit message")
paths = payload.get("paths") or [item["path"] for item in files]
selected = [str(safe_relative(str(path))) for path in paths]
if len(set(selected)) != len(selected) or not set(item["path"] for item in files).issubset(set(selected)):
raise ValueError("release paths must be unique and include every patched file")
label = str(payload.get("recovery_label", time.strftime("%Y%m%d-%H%M")))
if not SAFE_NAME.fullmatch(label):
raise ValueError("invalid recovery label")
normal = {
"files": files, "checks": checks, "compose_file": compose_file,
"services": [str(x) for x in services], "build": bool(payload.get("build", True)),
"openwebui_sync": bool(payload.get("openwebui_sync", True)),
"message": message, "paths": selected,
"create_recovery": bool(payload.get("create_recovery", True)), "recovery_label": label,
}
preview = (
f"ONE MCP RELEASE\nServices: {', '.join(normal['services'])}\n"
f"Checks: {', '.join(checks)}\nOpenWebUI sync: {normal['openwebui_sync']}\n"
f"Selective commit: {message}\nPaths: {', '.join(selected)}\n"
f"Recovery: {normal['create_recovery']} ({label})\n\n{patch_preview}"
)
return normal, preview
if operation == "run_checks":
checks = payload.get("checks") or []
if not isinstance(checks, list) or not checks or any(name not in ALLOWED_CHECKS for name in checks):
@@ -390,18 +509,107 @@ def start_job(ticket: str, operation: str, worker) -> dict[str, Any]:
return {"job_id": job_id, "status": "running", "instruction": "Poll athena_operator_job until completed or failed."}
def apply_files(files: list[dict[str, Any]], backup: Path) -> list[str]:
for item in files:
relative = safe_relative(item["path"])
current = source_file(REPOSITORY, relative)
current_sha = sha(current.read_bytes()) if current.is_file() else sha(b"")
if current_sha != item["before_sha256"]:
raise RuntimeError(f"source drift after preview: {relative}")
for item in files:
sync_file(safe_relative(item["path"]), item["content"], backup)
return [item["path"] for item in files]
def restore_files(files: list[dict[str, Any]], backup: Path) -> None:
for item in files:
relative = safe_relative(item["path"])
for root in (REPOSITORY, STACK):
target = source_file(root, relative)
saved = backup / root.name / relative
if saved.is_file():
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(saved, target)
elif target.exists():
target.unlink()
def publish_paths(message: str, selected: list[str]) -> dict[str, Any]:
run(["git", "add", "--", *selected], cwd=REPOSITORY, check=True)
run(["git", "diff", "--cached", "--check", "--", *selected], cwd=REPOSITORY, check=True)
commit = run(["git", "commit", "-m", 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")
return {"commit": head, "commit_output": commit, "push_output": pushed}
def perform_recovery(label: str) -> dict[str, Any]:
dirty = run(["git", "status", "--porcelain"], cwd=REPOSITORY, check=True)["output"].strip()
if dirty:
raise RuntimeError("publish repository changes before creating a recovery kit")
head = run(["git", "rev-parse", "HEAD"], cwd=REPOSITORY, check=True)["output"].strip()
marker = (STACK / ".mike-ai-source-commit").read_text().strip()
if marker != head:
raise RuntimeError("deployed source marker and operator repository HEAD differ")
encrypted = Path(f"/data/athena-recovery-{label}.tar.age")
source_bundle = Path(f"/data/athena-source-{label}.git.bundle")
release = Path(f"/data/mike-ai-recovery-kit-{label}")
for target in (encrypted, source_bundle, release):
if target.exists():
raise FileExistsError(target)
git_bundle = run(["git", "bundle", "create", str(source_bundle), "--all"], cwd=REPOSITORY, timeout=1800, check=True)
encrypted_result = run([str(STACK / "platform/recovery/create-recovery-bundle.sh"), str(encrypted)], timeout=7200, check=True)
identity = Path("/data/mike-ai-recovery-kit/recovery.agekey")
if not identity.is_file():
raise RuntimeError("existing recovery identity is unavailable")
kit_result = run([str(STACK / "platform/recovery/create-self-contained-data-kit.sh"), str(encrypted), str(identity), str(source_bundle), str(release)], timeout=7200, check=True)
verify = run(["sha256sum", "-c", "SHA256SUMS"], cwd=release, timeout=1800, check=True)
return {"commit": head, "recovery_bundle": str(encrypted), "source_bundle": str(source_bundle), "self_contained_kit": str(release), "git_bundle": git_bundle, "encrypted_bundle": encrypted_result, "kit": kit_result, "verification": verify}
def execute_operation(ticket: str, operation: str, payload: dict[str, Any]) -> dict[str, Any]:
if operation == "file_update":
if operation in {"file_update", "patch_update"}:
backup = STATE / "backups" / f"{now()}-{ticket}"
for item in payload["files"]:
relative = safe_relative(item["path"])
current = source_file(REPOSITORY, relative)
current_sha = sha(current.read_bytes()) if current.is_file() else sha(b"")
if current_sha != item["before_sha256"]:
raise RuntimeError(f"source drift after preview: {relative}")
for item in payload["files"]:
sync_file(safe_relative(item["path"]), item["content"], backup)
return {"changed": [item["path"] for item in payload["files"]], "backup": str(backup), "git_diff": run(["git", "diff", "--stat"], cwd=REPOSITORY)}
changed = apply_files(payload["files"], backup)
return {"changed": changed, "backup": str(backup), "git_diff": run(["git", "diff", "--stat"], cwd=REPOSITORY)}
if operation == "mcp_release":
def release():
backup = STATE / "backups" / f"{now()}-{ticket}"
published = False
deployed = False
synced = False
try:
changed = apply_files(payload["files"], backup)
checks = []
for name in payload["checks"]:
result = run(ALLOWED_CHECKS[name], cwd=REPOSITORY, timeout=1200, check=True)
checks.append({"name": name, **result})
run(["docker", "compose", "-f", payload["compose_file"], "config", "-q"], cwd=STACK, check=True)
argv = ["docker", "compose", "-f", payload["compose_file"], "up", "-d"]
if payload["build"]: argv.append("--build")
argv.extend(payload["services"])
deploy = run(argv, cwd=STACK, timeout=3600, check=True)
deployed = True
sync = None
if payload["openwebui_sync"]:
sync = run(["bash", str(STACK / "platform/openwebui/install-filters.sh")], cwd=STACK, timeout=1800, check=True)
synced = True
publication = publish_paths(payload["message"], payload["paths"])
published = True
recovery = perform_recovery(payload["recovery_label"]) if payload["create_recovery"] else None
return {"changed": changed, "checks": checks, "deploy": deploy, "openwebui_sync": sync, "publication": publication, "recovery": recovery, "containers": run(["docker", "ps", "--format", "{{.Names}}\t{{.Status}}"])}
except Exception:
if not published:
restore_files(payload["files"], backup)
run(["git", "reset", "--", *payload["paths"]], cwd=REPOSITORY)
if deployed:
rollback_argv = ["docker", "compose", "-f", payload["compose_file"], "up", "-d", "--build", *payload["services"]]
run(rollback_argv, cwd=STACK, timeout=3600)
if synced:
run(["bash", str(STACK / "platform/openwebui/install-filters.sh")], cwd=STACK, timeout=1800)
raise
return start_job(ticket, operation, release)
if operation == "run_checks":
return {"checks": [{"name": name, **run(ALLOWED_CHECKS[name], cwd=REPOSITORY, timeout=1200)} for name in payload["checks"]]}
if operation == "compose_deploy":
@@ -424,13 +632,7 @@ def execute_operation(ticket: str, operation: str, payload: dict[str, Any]) -> d
current_status = run(["git", "status", "--short", "--", *selected], cwd=REPOSITORY, check=True)["output"].strip()
if current_status != payload["reviewed_status"]:
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")
return {"commit": head, "commit_output": commit, "push_output": pushed}
return publish_paths(payload["message"], selected)
if operation == "model_download":
def download():
destination = source_file(MODELS, Path(payload["destination"]))
@@ -456,40 +658,7 @@ def execute_operation(ticket: str, operation: str, payload: dict[str, Any]) -> d
return start_job(ticket, operation, benchmark)
if operation == "recovery":
def recovery():
label = payload["label"]
dirty = run(["git", "status", "--porcelain"], cwd=REPOSITORY, check=True)["output"].strip()
if dirty:
raise RuntimeError("publish repository changes before creating a recovery kit")
head = run(["git", "rev-parse", "HEAD"], cwd=REPOSITORY, check=True)["output"].strip()
marker = (STACK / ".mike-ai-source-commit").read_text().strip()
if marker != head:
raise RuntimeError("deployed source marker and operator repository HEAD differ")
encrypted = Path(f"/data/athena-recovery-{label}.tar.age")
source_bundle = Path(f"/data/athena-source-{label}.git.bundle")
release = Path(f"/data/mike-ai-recovery-kit-{label}")
for target in (encrypted, source_bundle, release):
if target.exists():
raise FileExistsError(target)
git_bundle = run(["git", "bundle", "create", str(source_bundle), "--all"], cwd=REPOSITORY, timeout=1800, check=True)
encrypted_result = run([str(STACK / "platform/recovery/create-recovery-bundle.sh"), str(encrypted)], timeout=7200, check=True)
identity = Path("/data/mike-ai-recovery-kit/recovery.agekey")
if not identity.is_file():
raise RuntimeError("existing recovery identity is unavailable")
kit_result = run([
str(STACK / "platform/recovery/create-self-contained-data-kit.sh"),
str(encrypted), str(identity), str(source_bundle), str(release),
], timeout=7200, check=True)
verify = run(["sha256sum", "-c", "SHA256SUMS"], cwd=release, timeout=1800, check=True)
return {
"commit": head,
"recovery_bundle": str(encrypted),
"source_bundle": str(source_bundle),
"self_contained_kit": str(release),
"git_bundle": git_bundle,
"encrypted_bundle": encrypted_result,
"kit": kit_result,
"verification": verify,
}
return perform_recovery(payload["label"])
return start_job(ticket, operation, recovery)
raise AssertionError(operation)