"""Fährt die Angriffsliste gegen auto_push.py, je Fall auf einem Wegwerf-Vault. Ersteller: Pascal Michel Erstellt: 2026-09-17 Jeder Fall bekommt einen eigenen Wegwerf-Vault: ein echtes Git-Repo mit einem zweiten Repo als bare-Remote daneben. Damit ist der Push echt — es wird wirklich übertragen, nur eben in ein Verzeichnis statt über das Netz. Ein Auto-Push, der nur gegen eine Attrappe geprüft wurde, belegt nichts über den Fall, der zählt: dass die Änderung nachher drüben liegt. Der Hook wird für jeden Fall in den Wegwerf-Vault kopiert, weil er seine Vault-Wurzel ausschließlich aus dem eigenen Skriptpfad bestimmt. Grundlinie zuerst: eine geänderte Datei im Geltungsbereich muss nach dem Lauf im Remote liegen. Läuft die Grundlinie nicht, misst der Rest nichts. Aufruf: python .vault-tools/auto-push/faelle_auto_push.py """ from __future__ import annotations import json import os import shutil import stat import subprocess import sys import tempfile from dataclasses import dataclass from pathlib import Path HIER = Path(__file__).resolve().parent HOOK = HIER / "auto_push.py" ASKPASS_PY = HIER / "askpass.py" # Die .gitignore des echten Vaults, gekürzt auf das, was die Fälle brauchen. GITIGNORE = """/* !/.gitignore !/CLAUDE.md !/AI/ /AI/* !/AI/Regelwerk/ !/.vault-tools/ .vault-tools/auto-push/.sperre __pycache__/ *.pyc """ def raeume(pfad: Path) -> None: """rmtree, das auch schreibgeschuetzte git-Objektdateien wegbekommt.""" def erzwinge(funktion, ziel, _fehler): try: os.chmod(ziel, stat.S_IWRITE) funktion(ziel) except OSError: pass shutil.rmtree(pfad, onexc=erzwinge) def git(wo: Path, *argumente: str) -> subprocess.CompletedProcess[bytes]: return subprocess.run( ["git", "-C", str(wo), *argumente], capture_output=True, stdin=subprocess.DEVNULL, check=False, ) def baue_vault(basis: Path) -> tuple[Path, Path]: """Legt Arbeits-Vault und bare-Remote an und macht den ersten Commit.""" vault = basis / "vault" remote = basis / "remote.git" (vault / "AI" / "Regelwerk").mkdir(parents=True) (vault / ".vault-tools" / "auto-push").mkdir(parents=True) (vault / "Inbox").mkdir() (vault / ".gitignore").write_text(GITIGNORE, encoding="utf-8") (vault / "CLAUDE.md").write_text("# Stamm\n", encoding="utf-8") (vault / "AI" / "Regelwerk" / "probe.md").write_text("# Probe\n", encoding="utf-8") # Eine Datei, die niemals herausgehen darf. (vault / "Inbox" / "geheim.md").write_text("Kundendaten\n", encoding="utf-8") shutil.copy2(HOOK, vault / ".vault-tools" / "auto-push" / "auto_push.py") shutil.copy2(ASKPASS_PY, vault / ".vault-tools" / "auto-push" / "askpass.py") shim = vault / ".vault-tools" / "auto-push" / "askpass.sh" shim.write_bytes(b"#!/bin/sh\necho unbenutzt\n") subprocess.run(["git", "init", "-q", "--bare", str(remote)], check=True) git(vault, "init", "-q") git(vault, "symbolic-ref", "HEAD", "refs/heads/main") git(vault, "config", "user.name", "Fall") git(vault, "config", "user.email", "fall@example.invalid") git(vault, "config", "commit.gpgsign", "false") git(vault, "remote", "add", "origin", remote.as_uri()) git(vault, "add", "-A") git(vault, "commit", "-q", "-m", "Grundstand") git(vault, "push", "-q", "-u", "origin", "main") return vault, remote def lauf_hook(vault: Path, eingabe: dict, umgebung_extra: dict | None = None) -> tuple[int, str, str]: umgebung = dict(os.environ, PYTHONIOENCODING="utf-8", GITEA_TOKEN="egal") umgebung.update(umgebung_extra or {}) lauf = subprocess.run( [sys.executable, str(vault / ".vault-tools" / "auto-push" / "auto_push.py")], input=json.dumps(eingabe).encode("utf-8"), capture_output=True, env=umgebung, timeout=90, check=False, ) return ( lauf.returncode, lauf.stdout.decode("utf-8", errors="replace"), lauf.stderr.decode("utf-8", errors="replace"), ) def remote_dateien(remote: Path) -> set[str]: lauf = git(remote, "ls-tree", "-r", "--name-only", "main") if lauf.returncode != 0: return set() return {z for z in lauf.stdout.decode("utf-8", errors="replace").splitlines() if z} def remote_commits(remote: Path) -> int: lauf = git(remote, "rev-list", "--count", "main") if lauf.returncode != 0: return -1 return int(lauf.stdout.decode().strip() or "0") @dataclass class Fall: name: str beschreibung: str def main() -> int: basis = Path(tempfile.mkdtemp(prefix="auto-push-faelle-")) gesamt = 0 bestanden = 0 def pruefe(name: str, ok: bool, text: str) -> None: nonlocal gesamt, bestanden gesamt += 1 bestanden += int(ok) print(f"{'BESTANDEN' if ok else 'FEHLER':10} {name:34} {text}") try: # --- Grundlinie ------------------------------------------------- vault, remote = baue_vault(basis / "grundlinie") vorher = remote_commits(remote) (vault / "AI" / "Regelwerk" / "probe.md").write_text("# Probe geaendert\n", encoding="utf-8") code, aus, fehler = lauf_hook(vault, {"tool_input": {"file_path": str(vault / "AI" / "Regelwerk" / "probe.md")}}) nachher = remote_commits(remote) pruefe( "grundlinie", code == 0 and nachher == vorher + 1 and not aus.strip(), f"Exit {code}, Commits {vorher}->{nachher}, Ausgabe {len(aus)} Z. {fehler[:80]}", ) # --- Ausgeschlossene Datei geht nie heraus ---------------------- drueben = remote_dateien(remote) pruefe( "positivliste haelt", not any(d.startswith("Inbox/") for d in drueben), f"Remote fuehrt {sorted(drueben)}", ) pruefe( "sperrdatei bleibt drinnen", not any(d.endswith(".sperre") for d in drueben), f"Remote fuehrt {sorted(drueben)}", ) # --- Lauf ohne Aenderung bleibt still --------------------------- vorher = remote_commits(remote) code, aus, _ = lauf_hook(vault, {"tool_input": {"file_path": str(vault / "CLAUDE.md")}}) pruefe( "nichts geaendert -> still", code == 0 and not aus.strip() and remote_commits(remote) == vorher, f"Exit {code}, Ausgabe {len(aus)} Z., Commits {remote_commits(remote)}", ) # --- Eingaben, die nichts ausloesen duerfen --------------------- stille_faelle = [ ("eingabe leer", b""), ("eingabe kein json", b"{kaputt"), ("eingabe kein objekt", b"[1,2,3]"), ("eingabe null", b"null"), ("kein tool_input", b'{"tool_name":"Edit"}'), ("tool_input keine map", b'{"tool_input":"x"}'), ("kein file_path", b'{"tool_input":{"content":"x"}}'), ("file_path keine zeichenkette", b'{"tool_input":{"file_path":42}}'), ("file_path leer", b'{"tool_input":{"file_path":" "}}'), ] for name, roh in stille_faelle: (vault / "CLAUDE.md").write_text(f"# Stamm {name}\n", encoding="utf-8") vorher = remote_commits(remote) lauf = subprocess.run( [sys.executable, str(vault / ".vault-tools" / "auto-push" / "auto_push.py")], input=roh, capture_output=True, env=dict(os.environ, PYTHONIOENCODING="utf-8", GITEA_TOKEN="egal"), timeout=60, check=False, ) aus = lauf.stdout.decode("utf-8", errors="replace") pruefe( name, lauf.returncode == 0 and not aus.strip() and remote_commits(remote) == vorher, f"Exit {lauf.returncode}, Ausgabe {len(aus)} Z.", ) git(vault, "checkout", "--", "CLAUDE.md") # --- Pfade ausserhalb des Geltungsbereichs ---------------------- aussen = [ ("pfad ausserhalb vault", str(basis / "woanders.md")), ("nachbarordner mit praefix", str(vault) + "-anders/CLAUDE.md"), ("inbox-notiz loest nicht aus", str(vault / "Inbox" / "geheim.md")), ] for name, pfad in aussen: (vault / "CLAUDE.md").write_text(f"# Stamm {name}\n", encoding="utf-8") vorher = remote_commits(remote) code, aus, _ = lauf_hook(vault, {"tool_input": {"file_path": pfad}}) pruefe( name, code == 0 and not aus.strip() and remote_commits(remote) == vorher, f"Exit {code}, Commits unveraendert: {remote_commits(remote) == vorher}", ) git(vault, "checkout", "--", "CLAUDE.md") # --- Pfadformen, die ausloesen muessen -------------------------- formen = [ ("relativer pfad", "AI/Regelwerk/probe.md"), ("backslash-pfad", str(vault / "AI" / "Regelwerk" / "probe.md").replace("/", "\\")), ("pfad mit punktsegment", str(vault / "AI" / ".." / "AI" / "Regelwerk" / "probe.md")), ("grossschreibung", str(vault / "AI" / "Regelwerk" / "probe.md").upper()), ("vault-tools zaehlt mit", str(vault / ".vault-tools" / "auto-push" / "notiz.md")), ] for nummer, (name, pfad) in enumerate(formen): ziel = vault / "AI" / "Regelwerk" / "probe.md" ziel.write_text(f"# Probe Form {nummer}\n", encoding="utf-8") vorher = remote_commits(remote) code, aus, fehler = lauf_hook(vault, {"tool_input": {"file_path": pfad}}) pruefe( name, code == 0 and remote_commits(remote) == vorher + 1, f"Exit {code}, Commits {vorher}->{remote_commits(remote)} {aus[:60]}", ) # --- Reihenfolge: erste und letzte Datei einer Sammeländerung --- for position in ("erste", "letzte"): vault2, remote2 = baue_vault(basis / f"reihenfolge-{position}") for i in range(5): (vault2 / "AI" / "Regelwerk" / f"a{i}.md").write_text(f"# {i}\n", encoding="utf-8") ausloeser = "a0.md" if position == "erste" else "a4.md" code, aus, _ = lauf_hook(vault2, {"tool_input": {"file_path": str(vault2 / "AI" / "Regelwerk" / ausloeser)}}) dateien = remote_dateien(remote2) fehlt = [f"AI/Regelwerk/a{i}.md" for i in range(5) if f"AI/Regelwerk/a{i}.md" not in dateien] pruefe( f"reihenfolge {position}", code == 0 and not fehlt, f"Exit {code}, fehlend im Remote: {fehlt or 'nichts'}", ) # --- Fehlende Voraussetzungen sind Befunde ---------------------- vault3, remote3 = baue_vault(basis / "ohne-token") (vault3 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8") code, aus, _ = lauf_hook(vault3, {"tool_input": {"file_path": str(vault3 / "CLAUDE.md")}}, {"GITEA_TOKEN": ""}) pruefe( "kein token -> befund", code == 0 and "GITEA_TOKEN" in aus and remote_commits(remote3) == 1, f"Exit {code}, meldet: {'ja' if 'GITEA_TOKEN' in aus else 'NEIN'}", ) vault4, remote4 = baue_vault(basis / "ohne-askpass") (vault4 / ".vault-tools" / "auto-push" / "askpass.sh").unlink() (vault4 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8") code, aus, _ = lauf_hook(vault4, {"tool_input": {"file_path": str(vault4 / "CLAUDE.md")}}) pruefe( "kein askpass -> befund", code == 0 and "askpass" in aus.lower(), f"Exit {code}, meldet: {'ja' if 'askpass' in aus.lower() else 'NEIN'}", ) vault5, _ = baue_vault(basis / "ohne-git") raeume(vault5 / ".git") (vault5 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8") code, aus, _ = lauf_hook(vault5, {"tool_input": {"file_path": str(vault5 / "CLAUDE.md")}}) pruefe( "kein repo -> befund", code == 0 and "Git-Repo" in aus, f"Exit {code}, meldet: {'ja' if 'Git-Repo' in aus else 'NEIN'}", ) vault6, remote6 = baue_vault(basis / "remote-weg") raeume(remote6) (vault6 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8") code, aus, _ = lauf_hook(vault6, {"tool_input": {"file_path": str(vault6 / "CLAUDE.md")}}) lokal = git(vault6, "rev-list", "--count", "HEAD").stdout.decode().strip() pruefe( "push scheitert -> befund, commit bleibt", code == 0 and "push" in aus.lower() and lokal == "2", f"Exit {code}, lokale Commits {lokal}, meldet: {'ja' if 'push' in aus.lower() else 'NEIN'}", ) # --- Belegte Sperre bleibt still -------------------------------- vault7, remote7 = baue_vault(basis / "sperre") sperre = vault7 / ".vault-tools" / "auto-push" / ".sperre" sperre.write_bytes(b"") (vault7 / "CLAUDE.md").write_text("# geaendert\n", encoding="utf-8") vorher = remote_commits(remote7) code, aus, _ = lauf_hook(vault7, {"tool_input": {"file_path": str(vault7 / "CLAUDE.md")}}) pruefe( "belegte sperre -> still", code == 0 and not aus.strip() and remote_commits(remote7) == vorher, f"Exit {code}, Ausgabe {len(aus)} Z.", ) print() print(f"{bestanden} von {gesamt} Faellen bestanden.") return 0 if bestanden == gesamt else 1 finally: shutil.rmtree(basis, ignore_errors=True) if __name__ == "__main__": try: sys.stdout.reconfigure(encoding="utf-8") except AttributeError: pass raise SystemExit(main())