#!/usr/bin/env python3 """pre_tool_call-Guard (Faden 8, 13.07.2026): sperrt Schreibzugriffe auf den LIVE-Checkout ~/mission-control-v2 und die Hermes-QUELLE ~/.hermes/hermes-agent HART. 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) -> diese Luecke. UNBEDINGT (kein Task-/Profil-Scope): KEIN legitimer Hermes-Agent-Flow schreibt je in diese zwei Pfade — der Werkstatt-Worker arbeitet im WORKSPACE-Klon (/mission-control-v2, NICHT home-verankert -> erlaubt), Lucy fasst MC2-Quelle nie an, und die Wartungs-Pipeline (deploy.sh, Annahme-Runner) laeuft als Shell ueber ssh, NICHT ueber Hermes-Tools. Ein Task-Scope waere hier sogar gefaehrlich: Worker tragen als task_id den Session-Zeitstempel (z.B. 20260713_224206_267304), NICHT die Kanban-t_-ID — ein "nur t_"-Check liesse also alle Worker-Schreibzugriffe durch (Bug 13.07., so entdeckt). WICHTIG: Gesperrt wird der home-verankerte Live-Checkout ($HOME/mission-control-v2); der blosse Name "mission-control-v2" zaehlt NUR, wenn er vom cwd aus in einen Tabu-Pfad aufloest (V16 unten) — der Workspace-Klon (~/.hermes/kanban/workspaces//mission-control-v2) bleibt frei, sonst waere die legitime Worker-Arbeit blockiert. Lesen ist erlaubt; geblockt werden nur Schreib-Operationen. Ausgabe {"action":"block","message":...} = Tool geblockt; {} = durchlassen. """ import json import os import re import sys def out(obj): sys.stdout.write(json.dumps(obj)) sys.exit(0) try: payload = json.loads(sys.stdin.read() or "{}") except Exception: 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") # R2 (Review 15.07.): systemd-User-Units. Die Nacht-Werkstatt hat am 14.07. einen Override # DIREKT scharf geschaltet — ging gut, verletzt aber "jede Code-Zeile ist Klick-pflichtig". # Unit-/Override-Dateien werden nur noch ueber eine angenommene Karte installiert. SYSD = os.path.join(home, ".config", "systemd", "user") MSG = ( "TABU (Faden 8/R2): Schreiben an dieser Stelle ist fuer Worker gesperrt. Der Live-Checkout " "~/mission-control-v2, die Hermes-Quelle ~/.hermes/hermes-agent und die systemd-User-Units " "~/.config/systemd/user duerfen NIE direkt veraendert werden. Arbeite im FRISCHEN Klon " "deines Task-Workspaces; MC2-Aenderungen kommen als Vorschlags-Branch, systemd-Unit-/" "Override-Dateien werden nur ueber eine angenommene Karte installiert, 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 any(rp == t or rp.startswith(t + os.sep) for t in (LIVE, SRC, SYSD)) # --- 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({}) cwd_raw = payload.get("cwd") or "" cwd = os.path.realpath(os.path.expanduser(cwd_raw)) if cwd_raw else "" # R5 (Review 15.07.): `git init` im Kanban-Workspace hart blocken. Drei Naechte in # Folge bauten Worker git-init-Orphans ohne gemeinsamen Ursprung mit main # (cleanse-skill-index 12.07., skill-kanten-generator 13.07., blogwatcher-logging # ~14.07.) — solche Vorschlaege sind bei der Annahme technisch tot, die Nacht-Arbeit # verpufft. Kein legitimer Worker-Flow braucht git init; MC2-/Lucy-Arbeit = VOLL # klonen (SOUL/GRENZEN). Scope: nur Kanban-Worker (Lucy/CLI duerfen z. B. fuer # projekt-start neue Repos initialisieren). ws_dir = os.path.join(home, ".hermes", "kanban", "workspaces") if cwd_raw.startswith(ws_dir) and re.search(r"(?:^|[^\w])git\s+init\b", cmd): out({"action": "block", "message": ( "TABU (R5): `git init` ist im Worker-Workspace gesperrt — Orphan-Branches ohne " "gemeinsamen Ursprung mit main sind bei der Annahme technisch tot (Vorfaelle " "12.–14.07.). Richtiger Weg: VOLL klonen, z. B. git clone " "http://192.168.178.153:3000/Hitonabi/mission-control-v2 " "(Credentials liegen in ~/.git-credentials), dann Branch von origin/main abzweigen.")}) # Schreib-Subkommandos von git (V16: + clone — `git clone URL ~/mission-control-v2` # rutschte vorher an (a) UND (d) vorbei). git_write = ( 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|clone|remote|config)\b" ) # V16 (Vorfall 16.07.2026): Werkstatt-Worker t_7a05992a ersetzte um 03:59 den Live- # Checkout mit cwd=$HOME und RELATIVEN Pfaden (`rm -rf mission-control-v2 && git # clone ...`) — keine home-verankerte Tabu-Form im Kommandotext, der Guard sah nur # Strings und liess durch (.venv weg -> Gateway im 203/EXEC-Crash-Loop). Drei Stopfen: # (1) cwd liegt IN einem Tabu-Pfad -> jede Schreib-Operation blocken (relative Ziele # treffen zwangslaeufig den Tabu-Baum). Lesen bleibt erlaubt. if cwd and under_tabu(cwd): if (re.search(git_write, cmd) or re.search(r"(?:^|[^\w])sed\s+-i", cmd) or re.search(r"(?:^|[^\w])(rm|rmdir|mv|cp|truncate|dd|chmod|chown|ln|install|mkdir|touch)\s", cmd) or re.search(r">>?\s*(?![/&])", cmd) or re.search(r"(?:^|[^\w])tee\s+(?:-a\s+)?(?!/)", cmd)): block() # (2) cwd == $HOME -> `git clone`/`git init`, deren Ziel direkt in $HOME landet, # blocken (exakt der Vorfalls-Hergang). Worker klonen im Task-Workspace; ein # explizites Ziel AUSSERHALB von $HOME (z.B. Unterordner) bleibt erlaubt. m = re.search(r"(?:^|[^\w])git(?:\s+-C\s+\S+)?\s+(?:-[\w-]+\s+)*(?:clone|init)\b([^|&;]*)", cmd) if cwd == home and m: toks = m.group(1).split() dest = toks[-1] if toks else "" # kein brauchbares Ziel-Argument (leer, Option oder URL) -> implizit direkt in $HOME implicit = (not dest or dest.startswith("-") or re.search(r"://|^git@|^[\w.-]+@[\w.-]+:|\.git$", dest)) if (implicit or under_tabu(os.path.join(home, dest)) or os.path.dirname(os.path.realpath(os.path.join(home, dest))) == home): out({"action": "block", "message": ( "TABU (V16): `git clone`/`git init` direkt in $HOME ist gesperrt — so wurde " "am 16.07. der Live-Checkout ~/mission-control-v2 durch einen frischen Klon " "ersetzt (.venv weg, Gateway tot). Klone/initialisiere in deinem Task-" "Workspace (~/.hermes/kanban/workspaces//...) oder gib ein Ziel " "ausserhalb von $HOME an.")}) # 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", SYSD, "~/.config/systemd/user", "$HOME/.config/systemd/user", "${HOME}/.config/systemd/user", ] # (3) cwd == Vorfahr eines Tabu-Pfads (z.B. $HOME) -> auch die RELATIVE Nennung des # Tabu-Ziels (mission-control-v2, .hermes/hermes-agent, .config/systemd/user) # zaehlt als Treffer fuer die Schreib-Checks unten. Die Lookarounds stellen # sicher, dass der Name ein eigenes Argument beginnt — als Teil eines laengeren # Pfades (z.B. .hermes/kanban/workspaces//mission-control-v2) matcht er # NICHT, die legitime Workspace-Arbeit bleibt frei. rel_pats = [] if cwd: for t in (LIVE, SRC, SYSD): if t.startswith(cwd + os.sep): rel = os.path.relpath(t, cwd).replace(os.sep, "/") rel_pats.append(r"(? Workspace-Arbeit, durchlassen # (a) git-Schreib-Subkommando (Repo-Kontext ist der referenzierte Tabu-Pfad). if re.search(git_write, 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({})