Files
mission-control-v2/deploy/agent-hooks/tabu-pfade-guard.py
T
Hitonabi fa22435d7c tabu-pfade-guard: V16 — relative Pfade bei riskantem cwd blocken (Vorfall 16.07.)
Nacht-Worker t_7a05992a ersetzte 03:59 den Live-Checkout mit cwd=$HOME und
RELATIVEN Pfaden (rm -rf mission-control-v2 && git clone ...) — der Guard
prueft beim terminal-Tool nur den Kommandotext auf home-verankerte Formen
und liess das durch (.venv weg -> Gateway 203/EXEC-Crash-Loop).

Drei Stopfen:
(1) cwd IN einem Tabu-Pfad -> jede Schreib-Operation geblockt (Lesen frei).
(2) git clone/init mit Ziel direkt in $HOME geblockt (Worker klonen im
    Task-Workspace); explizites Ziel ausserhalb $HOME bleibt erlaubt.
(3) cwd == Vorfahr eines Tabu-Pfads -> relative Nennung des Tabu-Ziels
    zaehlt als Treffer; Lookarounds lassen den Workspace-Klon-Pfad
    (.hermes/kanban/workspaces/<id>/mission-control-v2) weiter durch.
Nebenbefund zu: clone fehlte in der git-Schreibliste (a) — auch
git clone URL ~/mission-control-v2 war vorher ungeblockt.

Lokal bewiesen: 35/35 Payload-Faelle (Vorfalls-Kommando geblockt,
Workspace-Arbeit/Lesen frei, Alt-Verhalten regressionsfrei).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 11:02:35 +02:00

178 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 (<workspace>/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/<id>/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 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({})
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 "
"https://git.tobisniceshomelab.ddnsfree.com/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/<task>/...) 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/<id>/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"(?<![\w./@-])(?:\./)?" + re.escape(rel) + r"(?![\w.-])")
tabu_alt = "(?:" + "|".join([re.escape(t) for t in tabu_forms] + rel_pats) + ")"
if not re.search(tabu_alt, cmd):
out({}) # kein Tabu-Pfad referenziert -> 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({})