#!/usr/bin/env python3 """pre_tool_call-Guard (Faden 8, 13.07.2026): sperrt Schreibzugriffe von Kanban-Workern auf den LIVE-Checkout ~/mission-control-v2 und die Hermes-QUELLE ~/.hermes/hermes-agent HART - egal welches Profil. Nur fuer echte Worker (task_id = t_); Lucy/Wartung/ interaktiv/CLI (UUID-task_id) = sofortiger No-op. Loest Fallstrick #2 (Worker wandert in die falsche Ebene / lag Patches im Live-Checkout ab). Die native is_write_denied deckt diese Pfade NICHT ab (nur Credentials/System) -> dieser Guard schliesst die Luecke. WICHTIG: Der Worker KLONT MC2 in seinen Workspace (/mission-control-v2) und arbeitet DORT legitim. Gesperrt wird deshalb NUR der home-verankerte Live-Checkout ($HOME/mission-control-v2), NIE der blosse Name "mission-control-v2". Lesen ist erlaubt - geblockt werden nur Schreib-Operationen. Ausgabe {"action":"block","message":...} = Tool geblockt; {} = durchlassen. """ import sys, json, os, re def out(obj): sys.stdout.write(json.dumps(obj)) sys.exit(0) try: payload = json.loads(sys.stdin.read() or "{}") except Exception: out({}) extra = payload.get("extra") or {} task_id = str(extra.get("task_id") or payload.get("task_id") or "") # Scope: NUR echte Kanban-Worker. Hermes setzt fuer Lucy/CLI eine UUID-task_id # (agent/turn_context.py) -> nur das t_-Muster ist ein Worker. if not re.match(r"^t_[0-9a-f]{4,}$", task_id): out({}) tool = payload.get("tool_name") or "" ti = payload.get("tool_input") or {} home = os.path.realpath(os.path.expanduser("~")) LIVE = os.path.join(home, "mission-control-v2") SRC = os.path.join(home, ".hermes", "hermes-agent") MSG = ( "TABU (Faden 8): Schreiben an dieser Stelle ist fuer Worker gesperrt. Der Live-Checkout " "~/mission-control-v2 und die Hermes-Quelle ~/.hermes/hermes-agent duerfen NIE veraendert " "werden. Arbeite im FRISCHEN Klon deines Task-Workspaces; MC2-Aenderungen kommen als " "Vorschlags-Branch (nie am Live-Checkout), Hermes-Verhalten nur ueber Config/SOUL/Skill/" "Hook/MCP. Siehe docs/wissen/GRENZEN.md." ) def block(): out({"action": "block", "message": MSG}) def under_tabu(path): rp = os.path.realpath(path) return rp == LIVE or rp == SRC or rp.startswith(LIVE + os.sep) or rp.startswith(SRC + os.sep) # --- Datei-Tools: Zielpfad kanonisch pruefen (zuverlaessig) ------------------------- if tool in ("write_file", "patch"): fp = ti.get("path") or ti.get("file_path") or ti.get("filepath") or "" if not fp: out({}) fp = os.path.expanduser(fp) cwd = payload.get("cwd") or "" if not os.path.isabs(fp) and cwd: fp = os.path.join(cwd, fp) if under_tabu(fp): block() out({}) # --- Terminal: nur bei Schreib-Operation, die auf einen Tabu-Pfad zielt ------------- if tool == "terminal": cmd = ti.get("command") or "" if not cmd: out({}) # Home-verankerte Tabu-Formen. Der Workspace-Klon # ($HOME/.hermes/kanban/.../mission-control-v2) enthaelt KEINE davon als Teilstring. tabu_forms = [ LIVE, "~/mission-control-v2", "$HOME/mission-control-v2", "${HOME}/mission-control-v2", SRC, "~/.hermes/hermes-agent", "$HOME/.hermes/hermes-agent", "${HOME}/.hermes/hermes-agent", ] tabu_alt = "(?:" + "|".join(re.escape(t) for t in tabu_forms) + ")" if not re.search(tabu_alt, cmd): out({}) # kein home-verankerter Tabu-Pfad referenziert -> Workspace-Arbeit, durchlassen # (a) git-Schreib-Subkommando (Repo-Kontext ist der referenzierte Tabu-Pfad). if re.search( r"(?:^|[^\w])git(?:\s+-C\s+\S+)?\s+(?:-[\w-]+\s+)*" r"(commit|push|reset|checkout|switch|merge|add|rm|mv|clean|stash|rebase|apply|restore|init|remote|config)\b", cmd, ): block() # (b) sed -i, das einen Tabu-Pfad anfasst. if re.search(r"(?:^|[^\w])sed\s+-i\S*\s+[^|&;]*" + tabu_alt, cmd): block() # (c) Umleitung > / >> / tee, die auf einen Tabu-Pfad ZIELT (nicht 2>/dev/null o.ae.). if re.search(r"(?:>>?|(?:^|[^\w])tee\s+(?:-a\s+)?)\s*" + tabu_alt, cmd): block() # (d) destruktiv/kopierend mit Tabu-Pfad im selben Kommando-Segment. if re.search( r"(?:^|[^\w])(rm|rmdir|mv|cp|truncate|dd|chmod|chown|ln|install|mkdir|touch)\s+[^|&;]*" + tabu_alt, cmd, ): block() out({}) out({})