#!/usr/bin/env bash # Mutantenlauf auf einer Kopie der Kopie, gedacht fuer den Hilfsagenten der Plankritik # (.claude/workflows/plankritik.js). Nie in der uebergebenen Kopie selbst arbeiten: Die Linsen # fahren dort gleichzeitig Tests, und mutanten_lauf.py setzt Dateien zwischenzeitlich zurueck. # # Aufruf: bash mutanten_lauf_kopie.sh # modus ist "voll" oder "muster". Der ganze Lauf ist auf BUDGET_S Sekunden begrenzt (Vorgabe 540), # weil das Bash-Werkzeug eines Hilfsagenten nach hoechstens zehn Minuten endet; eine ueberschrittene # Grenze meldet die Zeile ZEITGRENZE, die bis dahin gefahrenen Mutanten stehen in der Ausgabe. # Beide Pfade sind Windows-Pfade (auch Kurznamen wie PASCAL~1), # die dieses Skript selbst mit wslpath in WSL-Pfade umwandelt. # # Nachbesserung 2026-09-28 (Pruefbericht vom selben Tag, Befunde 1 bis 4, 7): # - Netzsperre: mutanten_lauf.py laeuft in einem eigenen Netz-Namensraum # (unshare --map-current-user --net). Gemessen in WSL Debian als Benutzer claude # (util-linux 2.41.5): UID innen bleibt 1000 wie aussen, nur Schnittstelle lo, jeder # Verbindungsversuch nach aussen (etwa 127.0.0.1:8144, wo das Dashboard lauscht) scheitert. # Fehlt unshare ohne Netz, bricht das Skript vorher ab statt einen ungesperrten Mutantenlauf # still durchzulassen. # - Ungepufferte Ausgabe: python3 -u, damit bei einer Zeitgrenze die bis dahin gefahrenen # Mutanten wirklich in der Ausgabe stehen. # - Zeitlimit je Mutant: ZEITLIMIT_S (Vorgabe 120 s, kleiner als BUDGET_S) geht als --zeitlimit # an mutanten_lauf.py; B1 mit 74 Mutanten brauchte 168 s gesamt, rund 2 s je Mutant. # - Modus-Zeile vor dem Lauf ("modus: voll" / "modus: muster (...)"), damit die Linse weiss, ob # ueberhaupt ein Mutant gefahren ist. # - Aufraeumen bei SIGTERM: timeout -k 15 schickt nach der Frist noch ein SIGKILL hinterher, falls # mutanten_lauf.py nach dem SIGTERM nicht rechtzeitig endet; sein eigener SIGTERM-Handler setzt # die Zieldatei vorher zurueck und raeumt seine Temp-Ordner auf. # - chmod 700 auf die Kopie der Kopie direkt nach dem Kopieren. # # Nachbesserung 2026-10-09 (Paket RB3 der Trading Plattform, Plan # docs/plans/2026-10-09-mutanten-kopie-loopback.md): # - lo im Netz-Namensraum: Im frischen Namensraum war lo aus; jeder Test mit einem Server auf # 127.0.0.1 scheiterte mit Errno 101, auch ohne Mutant. Jetzt startet unshare zusaetzlich mit # --keep-caps, eine sh faehrt lo mit ip hoch, und setpriv wirft danach alle vererbbaren und # ambienten Faehigkeiten ab und geht per exec in python3 ueber. Gemessen vom Koordinator am # 2026-10-09 in WSL Debian als Benutzer claude: uid 1000 wie aussen, CapEff und CapAmb 0, # Verbindung ueber 127.0.0.1 gelingt. 127.0.0.1 ist das lo des Namensraums, nicht das des # Rechners; ein Dienst des Rechners auf 127.0.0.1 bleibt unerreichbar (Treiber-Fall W8). # - exec-Kette: unshare, sh, setpriv und python3 sind derselbe Prozess, das SIGTERM von timeout # trifft den Laeufer selbst; keine Zwischenschale bleibt stehen (Treiber-Fall W6). # - Vorpruefung vor dem Kopieren: Fehlt ip, setpriv oder python3, kommt lo in einer Probe im # eigenen Namensraum nicht hoch, gelingt dort keine Verbindung ueber 127.0.0.1 oder hat die # Probe nach setpriv noch CapEff oder CapAmb ungleich 0, endet das Skript mit # MUTANTEN_LAUF_FEHLER und Exit 2, ohne EXIT-Zeile. Die Faehigkeiten prueft das Skript nur an # dieser Probe und nur CapEff und CapAmb, nicht am Laeufer selbst; alle vier Mengen am Laeufer # prueft Treiber-Fall W3. Die Vorpruefung steht vor der Suche nach mutanten.toml: Scheitert # sie, endet das Skript auch fuer einen Planordner ohne mutanten.toml mit Exit 2 statt mit # KEINE_MUTANTEN_TOML. # - Hauptaufruf: Kommt lo dort nicht hoch, endet das Skript nach der Modus-Zeile mit # MUTANTEN_LAUF_FEHLER, Exit 2 und EXIT-Zeile, ohne den Laeufer zu starten. Ein Lauf ohne # Netzsperre oder ohne lo ist nie die Ausweichloesung. # - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W13 (W10 bis W13 seit Nachbesserung 1, # docs/plans/2026-10-09-mutanten-kopie-loopback-nachbesserung-1.md). set -u KOPIE_WIN="${1:-}" PLANORDNER_WIN="${2:-}" MODUS="${3:-}" if [ -z "$KOPIE_WIN" ] || [ -z "$PLANORDNER_WIN" ] || [ -z "$MODUS" ]; then echo "MUTANTEN_LAUF_FEHLER: Aufruf braucht " exit 2 fi if [ "$MODUS" != "voll" ] && [ "$MODUS" != "muster" ]; then echo "MUTANTEN_LAUF_FEHLER: modus muss voll oder muster sein, nicht $MODUS" exit 2 fi BUDGET_S="${BUDGET_S:-540}" ZEITLIMIT_S="${ZEITLIMIT_S:-120}" if ! [ "$ZEITLIMIT_S" -lt "$BUDGET_S" ] 2>/dev/null; then echo "MUTANTEN_LAUF_FEHLER: ZEITLIMIT_S ($ZEITLIMIT_S) muss kleiner sein als BUDGET_S ($BUDGET_S)" exit 2 fi if ! unshare --map-current-user --net true 2>/dev/null; then echo "MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich" exit 2 fi # Vorpruefung lo (seit 2026-10-09): Werkzeuge da, lo kommt im eigenen Namensraum hoch, eine # Verbindung ueber 127.0.0.1 gelingt, und nach setpriv sind CapEff und CapAmb 0. Scheitert # etwas davon, endet das Skript hier, vor dem Kopieren. for WERKZEUG in ip setpriv python3; do if ! command -v "$WERKZEUG" >/dev/null 2>&1; then echo "MUTANTEN_LAUF_FEHLER: $WERKZEUG fehlt (gebraucht fuer lo im Netz-Namensraum)" exit 2 fi done LO_PROBE='import socket server = socket.socket() server.bind(("127.0.0.1", 0)) server.listen(1) client = socket.create_connection(server.getsockname(), timeout=3) client.close() server.close() felder = {} for zeile in open("/proc/self/status", encoding="utf-8", errors="replace"): schluessel, _, wert = zeile.partition(":") felder[schluessel] = wert.strip() if int(felder["CapEff"], 16) != 0 or int(felder["CapAmb"], 16) != 0: raise SystemExit("Faehigkeiten nicht abgeworfen: CapEff " + felder["CapEff"] + ", CapAmb " + felder["CapAmb"]) print("lo-probe ok")' LO_AUSGABE=$(timeout 30 unshare --map-current-user --net --keep-caps \ sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 -c "$1"' sh "$LO_PROBE" 2>&1) LO_EXIT=$? if [ "$LO_EXIT" -ne 0 ] || [ "$LO_AUSGABE" != "lo-probe ok" ]; then echo "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar (Exit $LO_EXIT): $LO_AUSGABE" exit 2 fi KOPIE=$(wslpath -u "$KOPIE_WIN") PLANORDNER=$(wslpath -u "$PLANORDNER_WIN") if [ ! -d "$KOPIE" ]; then echo "MUTANTEN_LAUF_FEHLER: Kopie fehlt: $KOPIE" exit 2 fi if [ ! -d "$PLANORDNER" ]; then echo "MUTANTEN_LAUF_FEHLER: Planordner fehlt: $PLANORDNER" exit 2 fi MUTANTEN_TOML="$PLANORDNER/mutanten.toml" if [ ! -f "$MUTANTEN_TOML" ]; then echo "KEINE_MUTANTEN_TOML" exit 0 fi TMPKOPIE=$(mktemp -d /tmp/plankritik-mut-XXXXXX) trap 'rm -rf "$TMPKOPIE"' EXIT cp -a "$KOPIE/." "$TMPKOPIE/" chmod 700 "$TMPKOPIE" find "$TMPKOPIE" -name __pycache__ -type d -prune -exec rm -rf {} + MUSTER_SCHALTER="" if [ "$MODUS" = "muster" ]; then MUSTER_SCHALTER="--nur-muster" echo "modus: muster (nur Musterpruefung, kein Mutant gefahren)" else echo "modus: voll" fi # exec-Kette ohne Zwischenschale (seit 2026-10-09): unshare, sh, setpriv und python3 sind derselbe # Prozess, das SIGTERM von timeout trifft den Laeufer selbst (Treiber-Fall W6). timeout -k 15 "$BUDGET_S" unshare --map-current-user --net --keep-caps \ sh -c 'ip link set lo up || { echo "MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht hochgefahren"; exit 2; }; exec setpriv --inh-caps=-all --ambient-caps=-all -- "$@"' sh \ python3 -u /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py \ --wurzel "$TMPKOPIE" --mutanten "$MUTANTEN_TOML" --zeitlimit "$ZEITLIMIT_S" $MUSTER_SCHALTER EXITCODE=$? if [ "$EXITCODE" -eq 124 ]; then echo "ZEITGRENZE: Mutantenlauf nach $BUDGET_S s abgebrochen; Mutanten ohne Zeile oben sind nicht gefahren" fi echo "EXIT mutanten_lauf: $EXITCODE" exit $EXITCODE