Files
claude-regelwerk/.vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md
T
Pascal MichelandClaude Opus 5.5 8950af05e0 Mutantenlauf auf der Kopie: lo im Netz-Namensraum, fail-closed, Treiber W1 bis W13
mutanten_lauf_kopie.sh faehrt lo im eigenen Netz-Namensraum hoch (unshare --keep-caps, ip, setpriv,
exec in python3) und prueft das vorher fail-closed (ip, setpriv, python3, lo-Probe mit
Faehigkeitspruefung). Anlass: Im Paket RB3 der Trading Plattform scheiterten Tests mit Server auf
127.0.0.1 im Namensraum an Errno 101 (lo aus).
Neuer Treiber mutanten_lauf_kopie_treiber.py mit W1 bis W13; Regressionsnachweis gegen HEAD (W1, W2,
W4, W5, W9 rot) und neun kaputte Varianten v1 bis v9, je an ihren Faellen rot. Lauf mit echten Daten:
alle 73 RB3-Mutanten durch das neue Skript 73/73 OK, T1 und T12 jetzt im Namensraum. Ein pruefer
las Skript und Treiber (kein blockierender Befund; der wichtige zu fehlenden Faellen fuer die
Fehlerzweige ist mit Nachbesserung 1 behoben, W10 bis W13).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JU1LsUp4FdmPq8mvC368G3
2026-10-09 15:20:49 +02:00

56 KiB

type, title, status, summary, tags, created, updated, modell, effort, modell_begruendung
type title status summary tags created updated modell effort modell_begruendung
plan Mutantenlauf auf der Kopie: lo im Netz-Namensraum, fail-closed active Ein Task: mutanten_lauf_kopie.sh fährt lo im eigenen Netz-Namensraum hoch (unshare mit --keep-caps, ip, setpriv, exec in python3) und prüft das vorher fail-closed; dazu ein eigener Treiber mit den Fällen W1 bis W9, Regressionsnachweis gegen git show HEAD und fünf kaputte Varianten, Nachtrag in LIESMICH und Entwurf.
plan
vault-tools
pruefwerkzeuge
2026-10-09 2026-10-09 sonnet low Jeder Block steht wörtlich da, Zieldateien, Verbote und Haltepunkte sind genannt; LIESMICH- und Entwurf-Block tragen typografische Anführungszeichen und Auslassungspunkte, die Haiku still ersetzt, deshalb sonnet mit effort low.

Übersicht: LIESMICH der Planwerkzeuge · Anlass: Paket RB3 der Trading Plattform (D:/Claude/.arbeitsbaeume/rb-proben/rb3_mutanten.sh, Kopfkommentar)

Mutantenlauf auf der Kopie: lo im Netz-Namensraum, fail-closed

Ziel

mutanten_lauf_kopie.sh fährt den Mutantenläufer weiter in einem eigenen Netz-Namensraum, aber mit hochgefahrenem lo. Ein Test, der einen Server auf 127.0.0.1 startet, läuft dort grün, statt mit OSError: [Errno 101] Network is unreachable die Grundlinie rot zu machen. Die Netzsperre nach außen bleibt, der Läufer behält keine Fähigkeiten, und fehlt ein Baustein (ip, setpriv, python3, nutzbares lo), endet das Skript vor dem Kopieren mit MUTANTEN_LAUF_FEHLER und Exit 2. Die Plankritik bekommt damit für Projekte mit solchen Tests einen Mutantenlauf statt einer roten Grundlinie.

Nicht im Umfang

  • mutanten_lauf.py und sein Treiber mutanten_lauf_treiber.py bleiben unverändert.
  • .claude/workflows/plankritik.js bleibt unverändert, auch die Auswertung ohne EXIT-Zeile (siehe Offene Punkte).
  • rb3_mutanten.sh und die anderen Dateien unter D:/Claude/.arbeitsbaeume/rb-proben/ bleiben unverändert.
  • Begrenzungsmenge (CapBnd) und no_new_privs werden nicht angefasst.
  • Kein Lauf gegen ein echtes Projekt durch den Executer; den Lauf mit echten Daten fährt der Koordinator.

Warum so

Anlass

  • Das Skript fährt mutanten_lauf.py seit 2026-09-28 unter unshare --map-current-user --net.
  • Im frischen Netz-Namensraum ist lo aus. Jeder Test mit einem Server auf 127.0.0.1 scheitert dort mit Errno 101, auch ohne Mutant.
  • Gemessen am 2026-10-09 im Paket RB3: Die Mutanten T1 und T12 musste der Koordinator außerhalb des Namensraums fahren (rb3_mutanten.sh, Kopfkommentar).
  • plankritik.js ruft das Skript für jede mutanten.toml auf. Ein Projekt mit solchen Tests bekam dort eine rote Grundlinie statt eines Mutantenlaufs.

Was die Probe des Koordinators misst

D:/Claude/.arbeitsbaeume/rb-proben/lo_probe.py, gelesen am 2026-10-09:

  • Sie legt einen Server-Socket auf 127.0.0.1 an, den Port wählt der Kernel (bind mit Port 0), und ruft listen(1).
  • Ein zweiter Socket verbindet sich mit Zeitlimit 3 s dorthin. Ausgabe connect ok oder connect scheitert: <Fehler>.
  • Misst: lo ist oben, 127.0.0.1 ist im Namensraum adressierbar, der TCP-Verbindungsaufbau gelingt. Den Aufbau schließt der Kernel über die Warteschlange von listen ab, ohne accept.
  • Misst nicht: Datenaustausch (kein accept, kein send; das tut W1). uid und Fähigkeiten (hat der Koordinator getrennt gemessen; W3 prüft sie). Ob 127.0.0.1 innen vom lo des Rechners getrennt ist (W8).
  • Falle: Die Probe endet auch bei connect scheitert mit Exit 0. Die Vorprüfung im Skript vergleicht deshalb Exit-Code und Ausgabetext.

Entscheidungsliste

Frage Herkunft Antwort
Wie kommt lo im Namensraum hoch? Quelle: Messung des Koordinators am 2026-10-09 (WSL Debian, Benutzer claude, util-linux aus Debian 13) unshare --map-current-user --net --keep-caps sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- <befehl>'
Braucht es --keep-caps? Annahme (abgeleitet: ohne vererbte Fähigkeiten verliert sh beim exec die Fähigkeiten aus dem Benutzer-Namensraum und ip scheitert) → vom Executer zu messen: Vorprobe 2 und Variante v4 --keep-caps steht in Probe und Hauptaufruf
Welche Rechte hat der Läufer? Quelle: Messung (uid 1000 wie außen, CapEff und CapAmb 0). CapPrm und CapInh 0: Annahme nach der Umrechnung bei execve in capabilities(7), nicht nachgelesen → Projektvorgabe, W3 prüft alle vier W3
Erreicht das SIGTERM von timeout den Läufer? Projektvorgabe des Koordinators (exec-Kette). Dass unshare ohne --fork und setpriv per exec weitergeben und timeout ohne --foreground auch seine Prozessgruppe trifft, ist aus dem Gedächtnis, nicht nachgelesen → W6 misst den Elternprozess des Läufers (timeout) und die Zeile ABBRUCH: SIGTERM, v2 zeigt W6 rot sh geht per exec setpriv, setpriv per exec in python3 über
Was ist ein Fehlschlag, was folgt? Projektvorgabe des Koordinators Werkzeug fehlt, oder die Probe endet ungleich 0, oder ihre Ausgabe ist nicht genau lo-probe ok → MUTANTEN_LAUF_FEHLER: …, Exit 2. Nie ein Lauf ohne Netzsperre oder ohne lo
Wo steht die Vorprüfung? Projektvorgabe (Planer) direkt nach der bestehenden unshare-Prüfung, vor wslpath, mktemp und der Modus-Zeile; keine EXIT-Zeile, wie bei fehlendem unshare
Wird python3 mitgeprüft? Projektvorgabe (Planer), aus der Angriffsliste „fehlender Interpreter“ ja, Fall W9
Wie wandelt das Skript Pfade? Quelle: mutanten_lauf_kopie.sh, Zeilen 58 und 59: wslpath -u auf beide Argumente Der Treiber gibt ihm wslpath -w seiner /tmp-Ordner
Kommt wslpath -u von der Ausgabe von wslpath -w zurück? Annahme → vom Executer zu messen: Vorprobe 1; der Treiber prüft die Rundreise selbst vor dem ersten Fall und endet sonst mit TREIBER_FEHLER, Exit 2 Haltepunkt, falls nicht
Welche Befehle braucht das Skript über PATH? Quelle: Skripttext nach der Änderung unshare wslpath mktemp cp chmod find rm timeout sh true ip setpriv python3; W4, W5, W9 fahren erst einen Kontrolllauf mit allen als Attrappen, dann einen ohne den einen
Ist 127.0.0.1 im Namensraum das lo des Rechners? Annahme (eigenes Loopback je Netz-Namensraum, aus dem Gedächtnis) → W8 misst mit einem Dienst des Treibers, v5 zeigt W8 rot getrennt
Darf ein Test ins Netz? Projektvorgabe des Koordinators Nur 127.0.0.1. 192.0.2.1 (TEST-NET-1) nur im Namensraum; der Test prüft vorher, dass es keine Route außer über lo gibt, und scheitert ohne Namensraum dort, vor jedem connect
Zeiten für W6 Projektvorgabe (Planer), gerechnet BUDGET_S=10, ZEITLIMIT_S=6, zwei hängende Mutanten: Die Grenze trifft den ersten oder den zweiten, die Prüfungen hängen nicht davon ab

Angriffsliste

Klasse Angriff Fall Erwartung mit dem neuen Skript
fehlt ip nicht im PATH W4 MUTANTEN_LAUF_FEHLER: ip fehlt, Exit 2, keine Modus-Zeile, keine EXIT-Zeile, keine Kopie unter /tmp
fehlt setpriv nicht im PATH W5 ebenso mit setpriv
fehlt (Interpreter) python3 nicht im PATH W9 ebenso mit python3
fehlt --keep-caps fehlt, lo kommt nicht hoch Variante v4 Vorprüfung meldet nicht nutzbar, alle Fälle rot
fehlt Vorprüfung fehlt Variante v3 W4, W5, W9 rot
zusätzlich Fähigkeiten bleiben beim Läufer Variante v1 W3 rot
zusätzlich Zwischenschale bleibt stehen Variante v2 W6 rot (Elternprozess sh statt timeout)
Verweis ins Leere Verbindung nach außen W2 ENETUNREACH, kein Zeitlimit-Fehler
Verweis ins Leere Dienst des Rechners auf 127.0.0.1 W8 unerreichbar, beim Dienst kommt keine Verbindung an
Umgebung kein Netz-Namensraum Variante v5 W2 und W8 rot, ohne Verbindung nach außen
Umgebung uid innen anders als außen W3 uid wie außen
Umgebung PATH ohne /usr/sbin und /sbin W4, W5, W9 Attrappen-Ordner; der Kontrolllauf mit allen Attrappen ist grün
Umgebung Windows-Pfade (UNC aus wslpath -w) alle Rundreise vor dem ersten Fall
Wertebereich BUDGET_S klein W6 ZEITGRENZE, Exit 124, ABBRUCH: SIGTERM, kein Prozess des Falls, keine Temp-Reste
Reihenfolge Die Grenze trifft den ersten oder den zweiten hängenden Mutanten W6 Prüfungen unabhängig davon, welcher
Reihenfolge Server-Test vorn oder hinten — nicht anwendbar: Der Namensraum gilt für den ganzen Läuferprozess, nicht je Test
leer, null, falscher Typ, doppelt — — nicht anwendbar: Das Skript liest keine neue Datenstruktur; Argumente und mutanten.toml prüfen Skript und Läufer wie bisher (Läufer-Treiber, Fälle 19 bis 27)
Rückwärtsverträglichkeit Grundlinie ohne Server, Modus voll und muster W7 wie bisher

Nach jedem Fall prüft der Treiber zusätzlich, dass die übergebene Kopie byte-gleich ist und kein Ordner /tmp/plankritik-mut-* liegen blieb.

Zieldateien

Pfade relativ zu D:/Claude/.vault-tools/plan-kopie/:

  • mutanten_lauf_kopie.sh (geändert)
  • mutanten_lauf_kopie_treiber.py (neu)
  • LIESMICH.md (geändert)
  • mutanten_lauf_ENTWURF.md (geändert)

Verbote

  • Nur die vier Zieldateien anlegen oder ändern. mutanten_lauf.py, mutanten_lauf_treiber.py, .claude/workflows/* und alles unter D:/Claude/.arbeitsbaeume/ bleiben unberührt.
  • Hilfsdateien nur im Rot-Ordner C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot (Git-Bash: /c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot, WSL: /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot).
  • Kein git add, kein git commit.
  • Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung typografischer Zeichen, keine zusätzlichen Leerzeilen. Aus-Block als old_string, wird-Block als new_string im Edit-Werkzeug; die neue Datei mit dem Write-Werkzeug.
  • Kein sed, kein Suchen-und-Ersetzen über ganze Dateien.
  • WSL-Aufrufe nur aus der PowerShell. Aus der Git-Bash macht die Pfadumwandlung aus /mnt/... einen Windows-Pfad.
  • Kein Netz außer 127.0.0.1. Den Test aus W2 nie von Hand außerhalb des Treibers fahren.
  • Den Treiber nicht fahren, während eine Plankritik läuft; er zählt neue Ordner /tmp/plankritik-mut-*.
  • Steht ein Aus-Block nicht genau einmal in seiner Datei, weicht eine Ausgabe von der Erwartung ab oder widerspricht etwas im Plan einem anderen Teil: anhalten und melden. Den Treiber nie anpassen, damit ein Fall grün wird.
  • python -m py_compile legt __pycache__ an; den Ordner danach löschen.

Schritt 0 — Rot-Ordner und Vorproben

In der Git-Bash den Rot-Ordner anlegen und das Skript aus dem letzten Commit hineinlegen. git show schreibt dort die Bytes unverändert, die PowerShell würde Zeilenenden umschreiben:

# Rot-Ordner neu anlegen, Skript aus HEAD als Wegwerfdatei hineinlegen
R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot
rm -rf "$R" && mkdir -p "$R"
git -C /d/Claude show HEAD:.vault-tools/plan-kopie/mutanten_lauf_kopie.sh > "$R/mutanten_lauf_kopie_alt.sh"
# vor der Umsetzung muss die Wegwerfdatei gleich der Arbeitsdatei sein und ohne CR
cmp "$R/mutanten_lauf_kopie_alt.sh" /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh && echo gleich
tr -cd '\r' < "$R/mutanten_lauf_kopie_alt.sh" | wc -c

Erwartet: gleich und 0.

Leg mit dem Write-Werkzeug die Datei C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\vorprobe.sh an.

Inhalt:

#!/usr/bin/env bash
# Vorproben zum Plan 2026-10-09-mutanten-kopie-loopback; legt nur einen leeren Temp-Ordner an und loescht ihn
# 1: Rundreise eines /tmp-Ordners ueber wslpath -w und wslpath -u
d=$(mktemp -d /tmp/mkopie-vorprobe-XXXXXX)
w=$(wslpath -w "$d")
z=$(wslpath -u "$w")
if [ "$z" = "$d" ]; then echo "rundreise: gleich"; else echo "rundreise: anders: $d -> $w -> $z"; fi
rmdir "$d"
# 2: ohne --keep-caps darf ip lo nicht hochfahren
unshare --map-current-user --net sh -c 'ip link set lo up; echo "ohne keep-caps: rc=$?"'
# 3: Startkette wie vom Koordinator gemessen, mit seiner Probe
unshare --map-current-user --net --keep-caps sh -c 'ip link set lo up && exec setpriv --inh-caps=-all --ambient-caps=-all -- python3 "$1"' sh /mnt/d/Claude/.arbeitsbaeume/rb-proben/lo_probe.py
# 4: Fundorte der Werkzeuge
for n in ip setpriv python3 unshare timeout sh wslpath; do echo "$n: $(command -v "$n")"; done

Dann in der Git-Bash auf CR prüfen und in der PowerShell fahren:

# Vorprobe darf kein CR tragen
tr -cd '\r' < /c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/vorprobe.sh | wc -c
# Vorproben in WSL
wsl -d Debian -- bash /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/vorprobe.sh
"exit=$LASTEXITCODE"

Erwartet:

rundreise: gleich
RTNETLINK answers: Operation not permitted
ohne keep-caps: rc=2
connect ok
ip: <pfad>
setpriv: <pfad>
python3: <pfad>
unshare: <pfad>
timeout: <pfad>
sh: <pfad>
wslpath: <pfad>
exit=0
  • rundreise: gleich und connect ok sind Pflicht. Weicht eine der beiden ab: anhalten und melden.
  • Zeilen 2 und 3 sind gerechnet, nicht gemessen. Pflicht ist nur rc= ungleich 0; den Wortlaut beider Zeilen meldest du.
  • Jede Fundort-Zeile muss einen Pfad tragen; die Pfade meldest du.

Schritt 1 — Tests zuerst: Treiber

mutanten_lauf_kopie_treiber.py, ganze Datei

Die ganze Datei ist neu:

#!/usr/bin/env python3
"""Treiber fuer mutanten_lauf_kopie.sh: Faelle W1 bis W9 (lo im Netz-Namensraum).

Zweck: Baut je Fall unter /tmp ein kleines Projekt (kopie/) und einen
Planordner (plan/) mit mutanten.toml, ruft mutanten_lauf_kopie.sh mit beiden
Ordnern als Windows-Pfaden auf, wie plankritik.js es tut, und vergleicht
Exit-Code und Ausgabezeilen mit der Erwartung aus dem Plan
docs/plans/2026-10-09-mutanten-kopie-loopback.md. Jeder Fall raeumt seine
Ordner selbst auf. Nach jedem Fall prueft der Treiber, dass die uebergebene
Kopie byte-gleich ist und kein Ordner /tmp/plankritik-mut-* liegen blieb.

Aufruf (in WSL): python mutanten_lauf_kopie_treiber.py [--skript <pfad>]
Ohne --skript faehrt er mutanten_lauf_kopie.sh neben sich, mit --skript eine
andere Fassung (Regressionsnachweis: Stand aus git show HEAD, kaputte
Varianten). W7 ist die Grundlinie und laeuft zuerst; ist sie rot, messen die
anderen Faelle wenig.

Pfade: Das Skript wandelt seine Argumente mit wslpath -u um. Der Treiber gibt
ihm deshalb wslpath -w seiner /tmp-Ordner und prueft vor dem ersten Fall,
dass die Rundreise denselben Pfad ergibt.

Netz: Die Tests verbinden sich nur mit 127.0.0.1 und, nur im Netz-Namensraum
des Skripts, mit 192.0.2.1 (TEST-NET-1). Der Test dazu prueft zuerst, dass es
keine Route ausser ueber lo gibt; ohne Namensraum scheitert er dort, bevor er
eine Verbindung versucht.

Nicht im Treiber: ob plankritik.js das Skript so aufruft wie hier
(MSYS_NO_PATHCONV, cygpath -m -l).
"""

import argparse
import hashlib
import os
import shutil
import socket
import subprocess
import sys
import tempfile
import time
import uuid
from pathlib import Path

HIER = Path(__file__).resolve().parent
SKRIPT_STANDARD = HIER / "mutanten_lauf_kopie.sh"
INTERPRETER = "/home/claude/venvs/trading/bin/python"
BASH = shutil.which("bash")
# Was mutanten_lauf_kopie.sh ueber PATH aufruft (Stand 2026-10-09); daraus
# bauen W4, W5 und W9 ihre Attrappen-Ordner.
WERKZEUGE = (
    "unshare", "wslpath", "mktemp", "cp", "chmod", "find", "rm", "timeout",
    "sh", "true", "ip", "setpriv", "python3",
)

FEHLERLISTE = []


def melde(fallname, ok, zusatz=""):
    if ok:
        print("{0} ok".format(fallname))
    else:
        print("{0} FEHLER: {1}".format(fallname, zusatz))
        FEHLERLISTE.append(fallname)


def kurz(ergebnis):
    return "exit={0}\n{1}{2}".format(ergebnis.returncode, ergebnis.stdout, ergebnis.stderr)


def sha_von(pfad):
    return hashlib.sha256(pfad.read_bytes()).hexdigest()


def plankritik_ordner():
    return set(Path("/tmp").glob("plankritik-mut-*"))


def windows_pfad(pfad):
    ergebnis = subprocess.run(["wslpath", "-w", str(pfad)], capture_output=True, text=True, timeout=30)
    return ergebnis.stdout.strip()


def linux_pfad(pfad):
    ergebnis = subprocess.run(["wslpath", "-u", pfad], capture_output=True, text=True, timeout=30)
    return ergebnis.stdout.strip()


# --- Projektbausteine -------------------------------------------------------

GRUNDDATEIEN = {
    "pytest.ini": "[pytest]\n",
    "paket/__init__.py": "",
    "tests/__init__.py": "",
}

MODUL = (
    "def f1(x):\n"
    "    if x > 0:\n"
    "        return True\n"
    "    return False\n"
)

TEST_MODUL = (
    "from paket.modul import f1\n"
    "\n"
    "\n"
    "def test_f1_positiv():\n"
    "    assert f1(5) is True\n"
)

DIENST = (
    "import socket\n"
    "\n"
    "\n"
    "def antwort_ueber_lo(nachricht):\n"
    "    server = socket.socket()\n"
    "    server.bind((\"127.0.0.1\", 0))\n"
    "    server.listen(1)\n"
    "    client = socket.create_connection(server.getsockname(), timeout=5)\n"
    "    verbindung, _ = server.accept()\n"
    "    client.sendall(nachricht)\n"
    "    daten = verbindung.recv(1024)\n"
    "    for sock in (client, verbindung, server):\n"
    "        sock.close()\n"
    "    return daten\n"
)

TEST_DIENST = (
    "from paket.dienst import antwort_ueber_lo\n"
    "\n"
    "\n"
    "def test_echo_ueber_lo():\n"
    "    assert antwort_ueber_lo(b\"hallo\") == b\"hallo\"\n"
)

NETZ = (
    "AUSSEN = (\"192.0.2.1\", 9)\n"
    "\n"
    "\n"
    "def aussen_adresse():\n"
    "    return AUSSEN\n"
)

TEST_NETZ = (
    "import errno\n"
    "import socket\n"
    "\n"
    "import pytest\n"
    "\n"
    "from paket.netz import aussen_adresse\n"
    "\n"
    "\n"
    "def test_keine_route_nach_aussen():\n"
    "    with open(\"/proc/net/route\", encoding=\"ascii\") as datei:\n"
    "        zeilen = datei.read().splitlines()[1:]\n"
    "    schnittstellen = [zeile.split()[0] for zeile in zeilen if zeile.strip()]\n"
    "    assert all(name == \"lo\" for name in schnittstellen), \"Routen ausser lo: {0}\".format(schnittstellen)\n"
    "    sock = socket.socket()\n"
    "    sock.settimeout(5)\n"
    "    try:\n"
    "        sock.connect(aussen_adresse())\n"
    "    except OSError as exc:\n"
    "        assert exc.errno == errno.ENETUNREACH, \"erwartet ENETUNREACH: {0!r}\".format(exc)\n"
    "    else:\n"
    "        pytest.fail(\"Verbindung nach aussen gelungen\")\n"
    "    finally:\n"
    "        sock.close()\n"
)

RECHTE = (
    "def status_feld(name):\n"
    "    with open(\"/proc/self/status\", encoding=\"utf-8\", errors=\"replace\") as datei:\n"
    "        for zeile in datei:\n"
    "            schluessel, _, wert = zeile.partition(\":\")\n"
    "            if schluessel == name:\n"
    "                return wert.strip()\n"
    "    raise KeyError(name)\n"
)

HAENGEN = (
    "def schritt_a(n):\n"
    "    return n + 1\n"
    "\n"
    "\n"
    "def schritt_b(n):\n"
    "    return n + 2\n"
)

TEST_HAENGEN = (
    "from paket.haengen import schritt_a, schritt_b\n"
    "\n"
    "\n"
    "def test_schritt_a():\n"
    "    assert schritt_a(1) == 2\n"
    "\n"
    "\n"
    "def test_schritt_b():\n"
    "    assert schritt_b(1) == 3\n"
)

SCHLEIFE = "    while True:\n        pass\n"

PRUEFZEILE_AUSSEN = "    assert info.value.errno in (errno.ECONNREFUSED, errno.ENETUNREACH), repr(info.value)\n"


def text_test_rechte(uid):
    return (
        "from paket.rechte import status_feld\n"
        "\n"
        "UID_AUSSEN = \"" + str(uid) + "\"\n"
        "\n"
        "\n"
        "def test_keine_faehigkeiten_gleiche_uid():\n"
        "    for feld in (\"CapInh\", \"CapPrm\", \"CapEff\", \"CapAmb\"):\n"
        "        wert = status_feld(feld)\n"
        "        assert int(wert, 16) == 0, \"{0} ist {1}\".format(feld, wert)\n"
        "    assert status_feld(\"Uid\").split()[0] == UID_AUSSEN\n"
    )


def text_test_aussen(port):
    return (
        "import errno\n"
        "import socket\n"
        "\n"
        "import pytest\n"
        "\n"
        "PORT_AUSSEN = " + str(port) + "\n"
        "\n"
        "\n"
        "def test_dienst_des_rechners_unerreichbar():\n"
        "    sock = socket.socket()\n"
        "    sock.settimeout(5)\n"
        "    try:\n"
        "        with pytest.raises(OSError) as info:\n"
        "            sock.connect((\"127.0.0.1\", PORT_AUSSEN))\n"
        "    finally:\n"
        "        sock.close()\n"
        + PRUEFZEILE_AUSSEN
    )


def kopf():
    return "format = 1\npython = \"{0}\"\n\n".format(INTERPRETER)


def mutant(id_, datei, aus, wird, rot_bei):
    return (
        "[[mutant]]\n"
        "id = \"" + id_ + "\"\n"
        "datei = \"" + datei + "\"\n"
        "aus = '''\n" + aus + "'''\n"
        "wird = '''\n" + wird + "'''\n"
        "rot_bei = [\"" + rot_bei + "\"]\n"
        "\n"
    )


class Fall:
    """Ein Fall unter /tmp: kopie/ mit den Projektdateien, plan/ mit mutanten.toml."""

    def __init__(self, dateien, mutanten):
        self.basis = Path(tempfile.mkdtemp(prefix="mkopie_treiber_"))
        self.kopie = self.basis / "kopie"
        self.plan = self.basis / "plan"
        self.plan.mkdir()
        alle = dict(GRUNDDATEIEN)
        alle.update(dateien)
        self.snap = {}
        for relpfad, inhalt in alle.items():
            pfad = self.kopie / relpfad
            pfad.parent.mkdir(parents=True, exist_ok=True)
            pfad.write_text(inhalt, encoding="utf-8")
            self.snap[pfad] = sha_von(pfad)
        (self.plan / "mutanten.toml").write_text(kopf() + mutanten, encoding="utf-8")

    def pruefe_kopie(self, fallname):
        for pfad, sha in self.snap.items():
            if not pfad.exists() or sha_von(pfad) != sha:
                melde(fallname + " (kopie)", False, "uebergebene Kopie veraendert: {0}".format(pfad))
                return

    def aufraeumen(self):
        shutil.rmtree(self.basis, ignore_errors=True)


def starte_skript(skript, fall, modus="voll", pfad=None):
    env = dict(os.environ)
    if pfad is not None:
        env["PATH"] = pfad
    cmd = [BASH, str(skript), windows_pfad(fall.kopie), windows_pfad(fall.plan), modus]
    return subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=180)


# --- Faelle ----------------------------------------------------------------

def fahre_einfach(name, skript, dateien, mutanten, mutant_id):
    fall = Fall(dateien, mutanten)
    try:
        ergebnis = starte_skript(skript, fall)
        ok = (
            ergebnis.returncode == 0
            and "{0} OK".format(mutant_id) in ergebnis.stdout
            and "EXIT mutanten_lauf: 0" in ergebnis.stdout
        )
        melde(name, ok, kurz(ergebnis))
        fall.pruefe_kopie(name)
    finally:
        fall.aufraeumen()


def fall_einfach():
    return Fall(
        {"paket/modul.py": MODUL, "tests/test_modul.py": TEST_MODUL},
        mutant("MW7", "paket/modul.py", "        return True\n", "        return False\n",
               "tests/test_modul.py::test_f1_positiv"),
    )


def fall_w7(skript):
    # Grundlinie wie bisher: kein Server-Test, Modus voll und muster.
    fall = fall_einfach()
    try:
        voll = starte_skript(skript, fall)
        muster = starte_skript(skript, fall, modus="muster")
        ok = (
            voll.returncode == 0
            and "modus: voll" in voll.stdout
            and "MW7 OK" in voll.stdout
            and "EXIT mutanten_lauf: 0" in voll.stdout
            and muster.returncode == 0
            and "modus: muster" in muster.stdout
            and "EXIT mutanten_lauf: 0" in muster.stdout
        )
        melde("W7", ok, "voll " + kurz(voll) + "\nmuster " + kurz(muster))
        fall.pruefe_kopie("W7")
    finally:
        fall.aufraeumen()


def fall_w1(skript):
    # Ein Test startet einen Server auf 127.0.0.1 und tauscht Daten aus.
    fahre_einfach(
        "W1", skript,
        {"paket/dienst.py": DIENST, "tests/test_dienst.py": TEST_DIENST},
        mutant("MW1", "paket/dienst.py", "    return daten\n", "    return daten[::-1]\n",
               "tests/test_dienst.py::test_echo_ueber_lo"),
        "MW1",
    )


def fall_w2(skript):
    # Verbindung nach aussen scheitert mit ENETUNREACH; der Mutant zielt auf
    # 127.0.0.1:9 und faellt nur, wenn lo oben ist (ECONNREFUSED).
    fahre_einfach(
        "W2", skript,
        {"paket/netz.py": NETZ, "tests/test_netz.py": TEST_NETZ},
        mutant("MW2", "paket/netz.py", "AUSSEN = (\"192.0.2.1\", 9)\n", "AUSSEN = (\"127.0.0.1\", 9)\n",
               "tests/test_netz.py::test_keine_route_nach_aussen"),
        "MW2",
    )


def fall_w3(skript):
    # CapInh, CapPrm, CapEff, CapAmb gleich 0 und dieselbe uid wie aussen.
    fahre_einfach(
        "W3", skript,
        {"paket/rechte.py": RECHTE, "tests/test_rechte.py": text_test_rechte(os.getuid())},
        mutant("MW3", "paket/rechte.py", "                return wert.strip()\n", "                return \"1\"\n",
               "tests/test_rechte.py::test_keine_faehigkeiten_gleiche_uid"),
        "MW3",
    )


def baue_attrappen(ordner, ohne):
    ordner.mkdir()
    for name in WERKZEUGE:
        if name != ohne:
            os.symlink(shutil.which(name), ordner / name)


def fahre_ohne_werkzeug(name, skript, werkzeug):
    # Erst ein Kontrolllauf mit allen Attrappen (muss gruen sein), dann einer
    # ohne das eine Werkzeug (muss vor dem Kopieren mit Exit 2 enden).
    fall = fall_einfach()
    try:
        alle = fall.basis / "attrappen_alle"
        ohne = fall.basis / "attrappen_ohne"
        baue_attrappen(alle, None)
        baue_attrappen(ohne, werkzeug)
        kontrolle = starte_skript(skript, fall, pfad=str(alle))
        vorher = plankritik_ordner()
        ergebnis = starte_skript(skript, fall, pfad=str(ohne))
        neu = sorted(str(p) for p in plankritik_ordner() - vorher)
        ok = (
            kontrolle.returncode == 0
            and "MW7 OK" in kontrolle.stdout
            and ergebnis.returncode == 2
            and "MUTANTEN_LAUF_FEHLER: {0} fehlt".format(werkzeug) in ergebnis.stdout
            and "modus:" not in ergebnis.stdout
            and "EXIT mutanten_lauf" not in ergebnis.stdout
            and not neu
        )
        melde(name, ok, "kontrolle exit={0}, ohne {1} exit={2}, neue Kopien={3}\nkontrolle {4}\nohne {1} {5}".format(
            kontrolle.returncode, werkzeug, ergebnis.returncode, neu, kurz(kontrolle), kurz(ergebnis)))
        fall.pruefe_kopie(name)
    finally:
        fall.aufraeumen()


def fall_w4(skript):
    fahre_ohne_werkzeug("W4", skript, "ip")


def fall_w5(skript):
    fahre_ohne_werkzeug("W5", skript, "setpriv")


def fall_w9(skript):
    fahre_ohne_werkzeug("W9", skript, "python3")


def prozesse_mit_marke(marke):
    """pid -> argv aller Prozesse, deren Umgebung die Marke traegt."""
    gefunden = {}
    for envdatei in Path("/proc").glob("[0-9]*/environ"):
        try:
            if marke.encode("ascii") not in envdatei.read_bytes():
                continue
            argv = (envdatei.parent / "cmdline").read_bytes().split(b"\0")
        except OSError:
            continue
        gefunden[envdatei.parent.name] = [teil.decode("utf-8", "replace") for teil in argv if teil]
    return gefunden


def elternname(pid):
    try:
        stat = Path("/proc/{0}/stat".format(pid)).read_text()
        ppid = stat.rsplit(")", 1)[1].split()[1]
        return Path("/proc/{0}/comm".format(ppid)).read_text().strip()
    except (OSError, IndexError):
        return None


def fall_w6(skript):
    # Zeitgrenze: zwei haengende Mutanten, BUDGET_S 10, ZEITLIMIT_S 6. Waehrend
    # des Laufs muss timeout der Elternprozess des Laeufers sein (keine
    # Zwischenschale); danach lebt kein Prozess des Falls mehr.
    fall = Fall(
        {"paket/haengen.py": HAENGEN, "tests/test_haengen.py": TEST_HAENGEN},
        mutant("MW6a", "paket/haengen.py", "    return n + 1\n", SCHLEIFE, "tests/test_haengen.py::test_schritt_a")
        + mutant("MW6b", "paket/haengen.py", "    return n + 2\n", SCHLEIFE, "tests/test_haengen.py::test_schritt_b"),
    )
    try:
        wert = uuid.uuid4().hex
        marke = "KOPIE_TREIBER_MARKE=" + wert
        eigenes_tmpdir = fall.basis / "tmpdir"
        eigenes_tmpdir.mkdir()
        env = dict(os.environ)
        env.update({
            "BUDGET_S": "10",
            "ZEITLIMIT_S": "6",
            "TMPDIR": str(eigenes_tmpdir),
            "KOPIE_TREIBER_MARKE": wert,
        })
        cmd = [BASH, str(skript), windows_pfad(fall.kopie), windows_pfad(fall.plan), "voll"]
        proc = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
        eltern = None
        pytest_gesehen = False
        for _ in range(12):
            time.sleep(0.5)
            for pid, argv in prozesse_mit_marke(marke).items():
                if (argv and Path(argv[0]).name.startswith("python")
                        and any(teil.endswith("mutanten_lauf.py") for teil in argv)):
                    eltern = elternname(pid)
                if "pytest" in argv:
                    pytest_gesehen = True
            if eltern is not None and pytest_gesehen:
                break
        try:
            stdout, stderr = proc.communicate(timeout=90)
        except subprocess.TimeoutExpired:
            proc.kill()
            stdout, stderr = proc.communicate()
            stderr += "\nTREIBER: Skript nach 90 s nicht beendet"
        reste = prozesse_mit_marke(marke)
        tmp_reste = sorted(p.name for p in eigenes_tmpdir.iterdir())
        ok = (
            proc.returncode == 124
            and "ZEITGRENZE" in stdout
            and "EXIT mutanten_lauf: 124" in stdout
            and "ABBRUCH: SIGTERM" in stdout
            and eltern == "timeout"
            and pytest_gesehen
            and not reste
            and not tmp_reste
        )
        melde("W6", ok, "exit={0}, eltern des laeufers={1}, pytest gesehen={2}, reste={3}, tmp={4}\n{5}{6}".format(
            proc.returncode, eltern, pytest_gesehen, reste, tmp_reste, stdout, stderr))
        fall.pruefe_kopie("W6")
    finally:
        fall.aufraeumen()


def fall_w8(skript):
    # Ein Dienst des Treibers auf 127.0.0.1 darf aus dem Namensraum nicht
    # erreichbar sein, und bei ihm darf keine Verbindung ankommen.
    dienst = socket.socket()
    dienst.bind(("127.0.0.1", 0))
    dienst.listen(5)
    dienst.setblocking(False)
    fall = None
    try:
        fall = Fall(
            {"tests/test_aussen.py": text_test_aussen(dienst.getsockname()[1])},
            mutant("MW8", "tests/test_aussen.py", PRUEFZEILE_AUSSEN, "    assert False\n",
                   "tests/test_aussen.py::test_dienst_des_rechners_unerreichbar"),
        )
        ergebnis = starte_skript(skript, fall)
        try:
            verbindung, _ = dienst.accept()
            verbindung.close()
            angekommen = True
        except BlockingIOError:
            angekommen = False
        ok = (
            ergebnis.returncode == 0
            and "MW8 OK" in ergebnis.stdout
            and "EXIT mutanten_lauf: 0" in ergebnis.stdout
            and not angekommen
        )
        melde("W8", ok, "beim Dienst des Rechners angekommen={0}, {1}".format(angekommen, kurz(ergebnis)))
        fall.pruefe_kopie("W8")
    finally:
        dienst.close()
        if fall is not None:
            fall.aufraeumen()


FAELLE = [
    ("W7", fall_w7),
    ("W1", fall_w1),
    ("W2", fall_w2),
    ("W3", fall_w3),
    ("W4", fall_w4),
    ("W5", fall_w5),
    ("W6", fall_w6),
    ("W8", fall_w8),
    ("W9", fall_w9),
]


def main():
    zerleger = argparse.ArgumentParser(description="Treiber fuer mutanten_lauf_kopie.sh (Faelle W1 bis W9)")
    zerleger.add_argument("--skript", default=str(SKRIPT_STANDARD), help="zu pruefende Fassung des Skripts")
    argumente = zerleger.parse_args()
    skript = Path(argumente.skript)
    print("skript: {0}".format(skript))

    fehlend = [name for name in WERKZEUGE if shutil.which(name) is None]
    if BASH is None:
        fehlend.append("bash")
    if fehlend:
        print("TREIBER_FEHLER: Werkzeuge fehlen: {0}".format(", ".join(fehlend)))
        return 2
    if not skript.is_file():
        print("TREIBER_FEHLER: Skript fehlt: {0}".format(skript))
        return 2
    if not Path(INTERPRETER).is_file():
        print("TREIBER_FEHLER: Interpreter fehlt: {0}".format(INTERPRETER))
        return 2

    probe = Path(tempfile.mkdtemp(prefix="mkopie_treiber_rundreise_"))
    try:
        windows = windows_pfad(probe)
        zurueck = linux_pfad(windows) if windows else ""
    finally:
        probe.rmdir()
    if not windows or zurueck != str(probe):
        print("TREIBER_FEHLER: wslpath-Rundreise: {0} -> {1} -> {2}".format(probe, windows, zurueck))
        return 2
    print("rundreise: {0} -> {1}".format(probe, windows))

    for name, funktion in FAELLE:
        vorher = plankritik_ordner()
        try:
            funktion(skript)
        except Exception as exc:
            melde(name, False, "Ausnahme: {0!r}".format(exc))
        liegen_geblieben = sorted(str(p) for p in plankritik_ordner() - vorher)
        if liegen_geblieben:
            melde(name + " (aufraeumen)", False, "Kopien unter /tmp blieben liegen: {0}".format(liegen_geblieben))

    print("gerechnete Fehler: {0}".format(len(FEHLERLISTE)))
    return 0 if not FEHLERLISTE else 1


if __name__ == "__main__":
    sys.exit(main())

Dann in der Git-Bash prüfen:

# Treiber kompilieren, nur ASCII, keine CR; __pycache__ danach weg
cd /d/Claude/.vault-tools/plan-kopie
python -m py_compile mutanten_lauf_kopie_treiber.py && echo kompiliert
rm -rf __pycache__
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf_kopie_treiber.py
tr -cd '\r' < mutanten_lauf_kopie_treiber.py | wc -c

Erwartet: kompiliert, dann 0, dann 0.

Schritt 2 — Erwarteter Fehlschlag: Treiber gegen das Skript aus HEAD

# Treiber in WSL gegen die Wegwerfdatei aus git show HEAD
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py --skript /mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/mutanten_lauf_kopie_alt.sh *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-alt.txt"
"exit=$LASTEXITCODE"
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-alt.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER"

Erwartet: exit=1, und die Treffer von Select-String enden auf diese Zeilenanfänge, in dieser Reihenfolge:

W1 FEHLER: exit=2
W2 FEHLER: exit=1
W4 FEHLER: kontrolle exit=0, ohne ip exit=0, neue Kopien=[]
W5 FEHLER: kontrolle exit=0, ohne setpriv exit=0, neue Kopien=[]
W9 FEHLER: kontrolle exit=0, ohne python3 exit=127, neue Kopien=[]
gerechnete Fehler: 5
  • Pflicht ist W1 rot (Grundlinie scheitert an Errno 101, der Läufer endet mit 2). Fehlt W1 unter den roten Fällen: anhalten und melden.
  • Gerechnet, nicht gemessen: W2 rot über MW2 LUECKE (bei lo aus scheitert auch 127.0.0.1:9 mit ENETUNREACH); W4, W5, W9 rot, weil das alte Skript nicht prüft; der Wert exit=127 bei W9 (unshare findet python3 nicht). W3, W6, W7 und W8 grün.
  • Weicht die Menge der roten Fälle ab: anhalten und die Ausgabe melden. Weicht nur ein gerechneter Wert hinter FEHLER: ab, meldest du ihn und machst weiter.

Schritt 3 — Umsetzung: Skript

Wende alle Blöcke dieses Abschnitts in der Reihenfolge an.

./mutanten_lauf_kopie.sh, Stelle 1 von 3

Aus:

# - chmod 700 auf die Kopie der Kopie direkt nach dem Kopieren.
set -u

wird:

# - 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 nicht hoch oder
#   bleiben Faehigkeiten, endet das Skript mit MUTANTEN_LAUF_FEHLER und Exit 2. Ein Lauf ohne
#   Netzsperre oder ohne lo ist nie die Ausweichloesung.
# - Treiber: mutanten_lauf_kopie_treiber.py, Faelle W1 bis W9.
set -u

./mutanten_lauf_kopie.sh, Stelle 2 von 3

Aus:

    echo "MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich"
    exit 2
fi

wird:

    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

./mutanten_lauf_kopie.sh, Stelle 3 von 3

Aus:

timeout -k 15 "$BUDGET_S" unshare --map-current-user --net \
    python3 -u /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf.py \

wird:

# 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 \

Danach in der Git-Bash und der PowerShell prüfen:

# nur ASCII, keine CR im Skript
cd /d/Claude/.vault-tools/plan-kopie
LC_ALL=C grep -cP '[^\x00-\x7F]' mutanten_lauf_kopie.sh
tr -cd '\r' < mutanten_lauf_kopie.sh | wc -c
# Syntax des Skripts in WSL
wsl -d Debian -- bash -n /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh
"exit=$LASTEXITCODE"

Erwartet: 0, 0, dann nur exit=0.

Schritt 4 — Verifikation: Treiber gegen das neue Skript, dann gegen fünf Varianten

Neues Skript

# Treiber in WSL gegen das neue Skript
wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-neu.txt"
"exit=$LASTEXITCODE"
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-neu.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER|^rundreise"

Erwartet: exit=0 und genau zwei Treffer, einer beginnt mit rundreise: /tmp/mkopie_treiber_rundreise_, der andere endet auf gerechnete Fehler: 0. Sonst anhalten und die ganze Datei ausgabe-neu.txt melden.

Varianten bauen

Leg mit dem Write-Werkzeug die Datei C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\varianten.py an.

Inhalt:

# Baut fuenf kaputte Fassungen des neuen mutanten_lauf_kopie.sh fuer den Regressionsnachweis.
# Aufruf: python varianten.py <neues skript> <zielordner>
import sys
from pathlib import Path

quelle = Path(sys.argv[1])
ziel = Path(sys.argv[2])
text = quelle.read_bytes().decode("utf-8")
if "\r" in text:
    raise SystemExit("Skript hat CR")


def ersetze(text, aus, wird, anzahl):
    gefunden = text.count(aus)
    if gefunden != anzahl:
        raise SystemExit("{0!r}: {1} Treffer statt {2}".format(aus, gefunden, anzahl))
    return text.replace(aus, wird)


HAUPT_EXEC = "exec setpriv --inh-caps=-all --ambient-caps=-all -- \"$@\"'"
START = "# Vorpruefung lo (seit 2026-10-09)"
ENDE = "nicht nutzbar (Exit $LO_EXIT): $LO_AUSGABE\"\n    exit 2\nfi\n"

varianten = {}
# v1: python3 ohne setpriv, Faehigkeiten bleiben (erwartet rot: W3)
varianten["v1_ohne_setpriv"] = ersetze(text, HAUPT_EXEC, "exec \"$@\"'", 1)
# v2: setpriv ohne exec, sh bleibt als Zwischenschale stehen (erwartet rot: W6)
varianten["v2_zwischenschale"] = ersetze(
    text, HAUPT_EXEC, "setpriv --inh-caps=-all --ambient-caps=-all -- \"$@\"; RC=$?; exit \"$RC\"'", 1)
# v3: ohne Vorpruefung (erwartet rot: W4, W5, W9)
if text.count(START) != 1 or text.count(ENDE) != 1:
    raise SystemExit("Vorpruefung nicht eindeutig gefunden")
anfang = text.index(START)
ende = text.index(ENDE) + len(ENDE)
if ende <= anfang:
    raise SystemExit("Ende der Vorpruefung vor ihrem Anfang")
varianten["v3_ohne_vorpruefung"] = text[:anfang] + text[ende:]
# v4: ohne --keep-caps in Probe und Hauptaufruf (erwartet rot: alle)
varianten["v4_ohne_keep_caps"] = ersetze(text, "--net --keep-caps", "--net", 2)
# v5: Hauptaufruf ohne unshare und ohne sh, also ohne Netz-Namensraum (erwartet rot: W2, W8)
zeilen = text.split("\n")
treffer = [i for i, z in enumerate(zeilen) if z.startswith("timeout -k 15 \"$BUDGET_S\" unshare ")]
if len(treffer) != 1 or not zeilen[treffer[0] + 1].lstrip().startswith("sh -c "):
    raise SystemExit("Hauptaufruf nicht eindeutig gefunden")
i = treffer[0]
varianten["v5_ohne_namensraum"] = "\n".join(zeilen[:i] + ["timeout -k 15 \"$BUDGET_S\" " + chr(92)] + zeilen[i + 2:])

for name, inhalt in varianten.items():
    (ziel / (name + ".sh")).write_bytes(inhalt.encode("utf-8"))
    print(name)
# Varianten aus dem neuen Skript bauen
R=/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot
python "$R/varianten.py" /d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "$R"

Erwartet genau diese fünf Zeilen:

v1_ohne_setpriv
v2_zwischenschale
v3_ohne_vorpruefung
v4_ohne_keep_caps
v5_ohne_namensraum

Endet das Skript mit einer Meldung statt der fünf Zeilen: anhalten und melden.

Treiber gegen jede Variante

# Treiber in WSL gegen jede kaputte Variante
foreach ($v in "v1_ohne_setpriv","v2_zwischenschale","v3_ohne_vorpruefung","v4_ohne_keep_caps","v5_ohne_namensraum") {
  wsl -d Debian -- timeout 900 /home/claude/venvs/trading/bin/python /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py --skript "/mnt/c/Users/PASCAL~1/AppData/Local/Temp/mkopie-rot/$v.sh" *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-$v.txt"
  "$v exit=$LASTEXITCODE"
  Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-$v.txt" -Pattern "^W\d.*FEHLER:|^gerechnete|^TREIBER_FEHLER"
}
Select-String -Path "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\ausgabe-v4_ohne_keep_caps.txt" -Pattern "nicht nutzbar" | Select-Object -First 1

Erwartet je Variante exit=1, und die FEHLER-Zeilen betreffen genau diese Fälle. Zeilen wie W6 (aufraeumen) FEHLER: oder W6 (kopie) FEHLER: zählen zu ihrem Fall:

Variante rote Fälle was es belegt
v1_ohne_setpriv W3 W3 sieht Fähigkeiten, die beim Läufer bleiben
v2_zwischenschale W6 W6 sieht eine Schale zwischen timeout und Läufer
v3_ohne_vorpruefung W4, W5, W9 die Vorprüfung ist der Grund für Exit 2 ohne Kopie
v4_ohne_keep_caps W1 bis W9, alle ohne --keep-caps kommt lo nicht hoch, und das Skript läuft nicht ungesperrt weiter
v5_ohne_namensraum W2, W8 W2 und W8 sehen eine fehlende Netzsperre, ohne Verbindung nach außen

Die letzte Zeile des Blocks zeigt eine Zeile MUTANTEN_LAUF_FEHLER: lo im Netz-Namensraum nicht nutzbar (Exit 2): RTNETLINK answers: Operation not permitted. Gerechnet ist der Wortlaut nach nicht nutzbar; Pflicht ist nur die Zeile selbst. Den Wortlaut meldest du, er ist die Messung zur Frage „Braucht es --keep-caps?“.

Weicht die Menge der roten Fälle einer Variante ab: anhalten und die ganze Ausgabedatei der Variante melden. Nicht weiter zu Schritt 5.

Schritt 5 — Doku

LIESMICH.md, Stelle 1 von 3

Aus:

created: 2026-09-17
updated: 2026-10-06
---

wird:

created: 2026-09-17
updated: 2026-10-09
---

LIESMICH.md, Stelle 2 von 3

Aus:

jede Verbindung nach außen scheitert) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt zu laufen.

wird:

jede Verbindung nach außen scheitert; seit 2026-10-09 mit `lo` oben, siehe Nachtrag unten) und gibt die Ausgabe an die Linse `arithmetik`. Fehlt `unshare` ohne Netz, fehlen `ip`, `setpriv` oder `python3` oder kommt `lo` im Namensraum nicht hoch, bricht das Skript vorher mit `MUTANTEN_LAUF_FEHLER` ab, statt ungesperrt oder ohne `lo` zu laufen.

LIESMICH.md, Stelle 3 von 3

Aus:

- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2).

wird:

- Rohdaten: `AI/secondbrain/linux-haertung-after-install-memory/2026-10-06-c1-abschluss/roh/` (`02-*`, `03-gegenprobe/`, `07-*`, `08-gegenprobe2/` mit dem nachgebesserten Läufer, `09-*` für Nachbesserung 2).

Nachbesserung vom 2026-10-09 (Paket RB3 der Trading Plattform, Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md`): Im frischen Netz-Namensraum war `lo` aus. Jeder Test mit einem Server auf `127.0.0.1` scheiterte dort mit „Network is unreachable“ (`Errno 101`), auch ohne Mutant; die Mutanten T1 und T12 des Pakets liefen deshalb von Hand außerhalb des Namensraums. `mutanten_lauf_kopie.sh` startet den Läufer jetzt mit `unshare --map-current-user --net --keep-caps`, fährt `lo` mit `ip` hoch, wirft mit `setpriv` alle vererbbaren und ambienten Fähigkeiten ab und geht per `exec` in `python3` über, ohne Zwischenschale. Eine Vorprüfung vor dem Kopieren verlangt `ip`, `setpriv` und `python3`, ein nutzbares `lo` mit einer Verbindung über `127.0.0.1` und `CapEff` und `CapAmb` gleich 0; sonst endet das Skript mit `MUTANTEN_LAUF_FEHLER` und Exit 2. `127.0.0.1` ist im Namensraum dessen eigenes `lo`, ein Dienst des Rechners bleibt unerreichbar. Belegt am selben Tag:
- `mutanten_lauf_kopie_treiber.py` ruft das Skript wie die Plankritik mit Windows-Pfaden auf und fährt neun Fälle: Server auf `127.0.0.1` (W1), keine Route nach außen und `ENETUNREACH` (W2), uid wie außen und keine Fähigkeiten (W3), `ip`, `setpriv` oder `python3` fehlen (W4, W5, W9), Zeitgrenze mit `timeout` als Elternprozess des Läufers und ohne Prozessreste (W6), Grundlinie ohne Server in beiden Modi (W7), Dienst des Rechners unerreichbar (W8). Neuer Stand ohne Fehler.
- Gegen das Skript vor der Änderung rot: W1, W2, W4, W5, W9. Gegen kaputte Varianten genau an ihrem Fall: ohne `setpriv` W3, `setpriv` ohne `exec` W6, ohne Vorprüfung W4, W5 und W9, ohne `--keep-caps` alle (die Vorprüfung meldet `Operation not permitted`), ohne Namensraum W2 und W8.
- Nicht abgeworfen und nicht geprüft: die Begrenzungsmenge (`CapBnd`) und `no_new_privs`. `plankritik.js` führt eine Vorprüfung mit `MUTANTEN_LAUF_FEHLER` wie bisher bei fehlendem `unshare` als `ohne_ergebnis`, ohne den Text der Meldung.

mutanten_lauf_ENTWURF.md, Stelle 1 von 2

Aus:

created: 2026-09-27
updated: 2026-10-06
---

wird:

created: 2026-09-27
updated: 2026-10-09
---

mutanten_lauf_ENTWURF.md, Stelle 2 von 2

Aus:

Fehlt `unshare` ohne Netz, bricht der Wrapper vorher mit `MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich` ab, statt einen ungesperrten Lauf durchzulassen.

wird:

Fehlt `unshare` ohne Netz, bricht der Wrapper vorher mit `MUTANTEN_LAUF_FEHLER: unshare ohne Netz nicht moeglich` ab, statt einen ungesperrten Lauf durchzulassen. **Seit 2026-10-09 (Paket RB3 der Trading Plattform):** `lo` ist im Namensraum oben. Der Wrapper startet `unshare … --keep-caps`, fährt `lo` mit `ip` hoch und geht über `setpriv` (alle vererbbaren und ambienten Fähigkeiten abgeworfen) per `exec` in den Läufer über. Eine Vorprüfung verlangt `ip`, `setpriv`, `python3`, ein nutzbares `lo` und `CapEff` und `CapAmb` gleich 0, sonst `MUTANTEN_LAUF_FEHLER` und Exit 2. Eine Verbindung auf `127.0.0.1` erreicht damit Dienste im Namensraum, nicht die des Rechners; nach außen bleibt es bei errno 101. Treiber: `mutanten_lauf_kopie_treiber.py` (W1 bis W9), Plan `docs/plans/2026-10-09-mutanten-kopie-loopback.md`.

Schritt 6 — Abschlussprüfung

# keine CR in den vier Zieldateien, nichts ausserhalb der Zieldateien geaendert
cd /d/Claude/.vault-tools/plan-kopie
for f in mutanten_lauf_kopie.sh mutanten_lauf_kopie_treiber.py LIESMICH.md mutanten_lauf_ENTWURF.md; do printf '%s %s\n' "$f" "$(tr -cd '\r' < "$f" | wc -c)"; done
git -C /d/Claude status --porcelain -- .vault-tools

Erwartet: je Datei die Zahl 0. git status zeigt genau diese fünf Zeilen, in beliebiger Reihenfolge:

 M .vault-tools/plan-kopie/LIESMICH.md
 M .vault-tools/plan-kopie/mutanten_lauf_ENTWURF.md
 M .vault-tools/plan-kopie/mutanten_lauf_kopie.sh
?? .vault-tools/plan-kopie/docs/plans/2026-10-09-mutanten-kopie-loopback.md
?? .vault-tools/plan-kopie/mutanten_lauf_kopie_treiber.py

Den Rot-Ordner lässt du stehen; der Koordinator legt die Ausgabedateien ab und löscht ihn.

Bericht

Melde:

  • je Zieldatei die Zahl der angewendeten Aus/wird-Paare und ob die neue Datei geschrieben ist;
  • die Ausgaben aus Schritt 0 (Vorproben im Wortlaut), Schritt 2, Schritt 3 und Schritt 4 (alle Select-String-Treffer im Wortlaut), Schritt 6;
  • jede Abweichung vom Plan, jeden gerechneten Wert, der anders ausfiel, und jede getroffene Annahme. „Keine Abweichungen“ nur, wenn es keine gab.

Fertig heißt (Koordinator)

  1. Die vier Zieldateien sind byte-gleich mit der Kopie, auf die dieser Plan per plan_materialisiere.py angewendet wurde (cmp), und sonst ist unter .vault-tools/ nichts geändert.
  2. Treiber in WSL gegen das neue Skript: Exit 0, gerechnete Fehler: 0, selbst gefahren.
  3. Rot-Nachweis selbst gesehen: Skript aus HEAD rot mindestens bei W1, gemeldete Menge W1, W2, W4, W5, W9; die fünf Varianten genau an ihren Fällen laut Tabelle in Schritt 4. Die Ausgabedateien aus dem Rot-Ordner liegen als Rohdaten abgelegt.
  4. Der Lauf mit echten Daten (unten) ist gefahren, T1 und T12 stehen auf OK.
  5. Ein pruefer hat Skript und Treiber gegen diesen Plan gelesen; kein offener blockierender oder wichtiger Befund.

Lauf mit echten Daten

  • Datensatz: Paket RB3 der Trading Plattform, Kopie D:/Claude/.arbeitsbaeume/rb3d-kopie, Planordner D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping (dieselben wie in rb3d_mutanten.sh). Die Mutanten der mutanten.toml passen nur zum Stand in rb3d-kopie, nicht zu rb3b-kopie aus rb3_mutanten.sh (berichtigt in Nachbesserung 1). Fehlt die Kopie, vorher neu bauen.
  • Befehl (PowerShell, im Hintergrund mit Zeitlimit des Werkzeugs bis 3600000; BUDGET_S hoch, weil RB3 mit --zeitlimit 300 lief):
# RB3 durch das neue Skript, Aufrufform wie in plankritik.js
wsl -d Debian -- env BUDGET_S=3000 ZEITLIMIT_S=300 timeout 3300 bash /mnt/d/Claude/.vault-tools/plan-kopie/mutanten_lauf_kopie.sh "D:/Claude/.arbeitsbaeume/rb3d-kopie" "D:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping" voll *> "C:\Users\PASCAL~1\AppData\Local\Temp\mkopie-rot\rb3.txt"
"exit=$LASTEXITCODE"
  • Auffallen müsste: T1 und T12 stehen auf OK, ohne Errno 101 in der Grundlinie. Die übrigen Mutanten haben denselben Status wie in Teil A von rb3d_mutanten.sh. Keine Zeile MUTANTEN_LAUF_FEHLER, keine Zeile ZEITGRENZE. Steht ein Mutant anders als in Teil A, hat die neue Startkette das Verhalten eines Tests verändert.
  • Gegenprobe Rückwärtsverträglichkeit: eine der sechs mutanten.toml aus der Gegenprobe vom 2026-10-06 (LIESMICH, 03-gegenprobe/) durch altes und neues Skript; gleicher Exit-Code und gleicher Status je Mutant. Welche, entscheidet der Koordinator.

Risiken

  1. Rundreise der Pfade. Gibt wslpath -u die Ausgabe von wslpath -w nicht zurück, endet der Treiber vor dem ersten Fall mit TREIBER_FEHLER: wslpath-Rundreise. Merkmal schon in Vorprobe 1. Dann muss der Treiber seine Ordner unter dem Windows-Temp anlegen; das ist eine neue Planfassung.
  2. Attrappen-Ordner. wslpath hängt über einen Verweis an /init, python3 an /usr/bin/python3.<n>. Folgt einer davon dem Verweis nicht, scheitert der Kontrolllauf in W4, W5 und W9. Merkmal: kontrolle exit= ungleich 0.
  3. Prozesse im Benutzer-Namensraum unlesbar. Liest der Treiber /proc/<pid>/environ der Prozesse im Namensraum nicht, findet W6 weder Läufer noch pytest. Merkmal: eltern des laeufers=None, pytest gesehen=False; dann wäre auch reste= ohne Aussage, deshalb verlangt W6 beides.
  4. Gleichzeitige Plankritik. Legt eine Plankritik während des Treiberlaufs /tmp/plankritik-mut-* an, meldet der Treiber (aufraeumen) für den gerade laufenden Fall. Der Treiber löscht fremde Ordner nicht.
  5. Langsamer Rechner in W6. Braucht die Grundlinie länger als etwa vier Sekunden, trifft die Grenze schon den ersten hängenden Mutanten. Die Prüfungen hängen nicht davon ab, welcher es ist.
  6. Fail-closed trifft bisher laufende Projekte. Fehlt auf einem Rechner iproute2 oder setpriv, bekam die Plankritik bisher einen Mutantenlauf, jetzt mutanten_status: ohne_ergebnis. Merkmal: Log der Plankritik „Hilfsagent ohne verwertbares Ergebnis“. Das ist gewollt.
  7. Begrenzungsmenge und no_new_privs. Nicht angefasst. Ein Programm mit Datei-Fähigkeiten bekäme im Benutzer-Namensraum wieder welche; die Netzsperre bleibt davon unberührt, geprüft ist das nicht.

Getroffene Annahmen

  • Der Treiber legt seine Ordner unter /tmp an und übergibt wslpath -w dieser Ordner (UNC-Pfad); das Skript wandelt mit wslpath -u zurück. Die Rundreise ist ungemessen und deshalb Vorprobe 1 und Startprüfung des Treibers.
  • python3 steht zusätzlich zu ip und setpriv in der Werkzeugprüfung (W9), aus der Angriffsliste.
  • Über die Mindestliste hinaus gibt es W8 (Dienst des Rechners unerreichbar) und W9 (python3 fehlt).
  • W2 prüft „keine Netzsperre“ über die Routingtabelle (/proc/net/route) statt über eine Verbindung. So ist W2 ohne Namensraum rot, ohne dass ein Paket hinausgeht; Variante v5 zeigt das.
  • W3 prüft neben CapEff und CapAmb auch CapPrm und CapInh (gerechnet 0).
  • Die Vorprüfung gibt wie die bestehende unshare-Prüfung keine EXIT-Zeile aus.
  • Der Regressionsnachweis umfasst neben dem Lauf gegen HEAD fünf kaputte Varianten, damit jeder neue Fall einmal rot gesehen ist.
  • Der Entwurf mutanten_lauf_ENTWURF.md beschreibt die Netzsperre des Wrappers und wird mitgezogen, obwohl der Auftrag nur LIESMICH und Kopfkommentar nennt.
  • Ein Plan mit einem Task, als Einzeldatei wie die Pläne daneben.

Offene Punkte

  • plankritik.js verliert die Meldung. Endet das Skript mit MUTANTEN_LAUF_FEHLER ohne EXIT-Zeile, setzt plankritik.js mutanten_status auf ohne_ergebnis und gibt die Rohausgabe nicht weiter; die Linse sieht den Grund nicht. Das gilt schon heute bei fehlendem unshare. Entscheidung beim Koordinator: im Skript eine EXIT-Zeile ausgeben oder plankritik.js anpassen, in einem eigenen Plan.
  • Gerechnete Werte. Exit-Codes hinter FEHLER: im Lauf gegen HEAD, Wortlaut von ip ohne --keep-caps, CapPrm und CapInh gleich 0, Elternprozess timeout: alles gerechnet; der Executer misst es in den Schritten 0, 2 und 4.
  • rb3_mutanten.sh fährt T1 und T12 weiter außerhalb des Namensraums. Ob es nach dem Lauf mit echten Daten weg kann, entscheidet der Koordinator.