.vault-tools (ohne trading-plattform-pruefung und watermarks-remover), .claude/agents, .claude/skills, .claude/workflows, .claude/settings.json. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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. |
|
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.pyund sein Treibermutanten_lauf_treiber.pybleiben unverändert..claude/workflows/plankritik.jsbleibt unverändert, auch die Auswertung ohneEXIT-Zeile (siehe Offene Punkte).rb3_mutanten.shund die anderen Dateien unterD:/Claude/.arbeitsbaeume/rb-proben/bleiben unverändert.- Begrenzungsmenge (
CapBnd) undno_new_privswerden 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.pyseit 2026-09-28 unterunshare --map-current-user --net. - Im frischen Netz-Namensraum ist
loaus. Jeder Test mit einem Server auf127.0.0.1scheitert dort mitErrno 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.jsruft das Skript für jedemutanten.tomlauf. 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.1an, den Port wählt der Kernel (bindmit Port 0), und ruftlisten(1). - Ein zweiter Socket verbindet sich mit Zeitlimit 3 s dorthin. Ausgabe
connect okoderconnect scheitert: <Fehler>. - Misst:
loist oben,127.0.0.1ist im Namensraum adressierbar, der TCP-Verbindungsaufbau gelingt. Den Aufbau schließt der Kernel über die Warteschlange vonlistenab, ohneaccept. - Misst nicht: Datenaustausch (kein
accept, keinsend; das tut W1). uid und Fähigkeiten (hat der Koordinator getrennt gemessen; W3 prüft sie). Ob127.0.0.1innen vomlodes Rechners getrennt ist (W8). - Falle: Die Probe endet auch bei
connect scheitertmit 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 unterD:/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, keingit commit. - Jeden Block byte-genau übernehmen: keine Umformulierung, keine Ersetzung typografischer Zeichen, keine zusätzlichen Leerzeilen. Aus-Block als
old_string, wird-Block alsnew_stringim 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_compilelegt__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: gleichundconnect oksind 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(beiloaus scheitert auch127.0.0.1:9mitENETUNREACH); W4, W5, W9 rot, weil das alte Skript nicht prüft; der Wertexit=127bei W9 (unsharefindetpython3nicht). 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)
- Die vier Zieldateien sind byte-gleich mit der Kopie, auf die dieser Plan per
plan_materialisiere.pyangewendet wurde (cmp), und sonst ist unter.vault-tools/nichts geändert. - Treiber in WSL gegen das neue Skript: Exit 0,
gerechnete Fehler: 0, selbst gefahren. - 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.
- Der Lauf mit echten Daten (unten) ist gefahren, T1 und T12 stehen auf
OK. - Ein
prueferhat 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, PlanordnerD:/Claude/.arbeitsbaeume/trading-plattform-rb3/docs/plans/2026-10-08-paket-rb3-totmann-ping(dieselben wie inrb3d_mutanten.sh). Die Mutanten dermutanten.tomlpassen nur zum Stand inrb3d-kopie, nicht zurb3b-kopieausrb3_mutanten.sh(berichtigt in Nachbesserung 1). Fehlt die Kopie, vorher neu bauen. - Befehl (PowerShell, im Hintergrund mit Zeitlimit des Werkzeugs bis 3600000;
BUDGET_Shoch, weil RB3 mit--zeitlimit 300lief):
# 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, ohneErrno 101in der Grundlinie. Die übrigen Mutanten haben denselben Status wie in Teil A vonrb3d_mutanten.sh. Keine ZeileMUTANTEN_LAUF_FEHLER, keine ZeileZEITGRENZE. 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.tomlaus 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
- Rundreise der Pfade. Gibt
wslpath -udie Ausgabe vonwslpath -wnicht zurück, endet der Treiber vor dem ersten Fall mitTREIBER_FEHLER: wslpath-Rundreise. Merkmal schon in Vorprobe 1. Dann muss der Treiber seine Ordner unter dem Windows-Temp anlegen; das ist eine neue Planfassung. - Attrappen-Ordner.
wslpathhängt über einen Verweis an/init,python3an/usr/bin/python3.<n>. Folgt einer davon dem Verweis nicht, scheitert der Kontrolllauf in W4, W5 und W9. Merkmal:kontrolle exit=ungleich 0. - Prozesse im Benutzer-Namensraum unlesbar. Liest der Treiber
/proc/<pid>/environder Prozesse im Namensraum nicht, findet W6 weder Läufer noch pytest. Merkmal:eltern des laeufers=None, pytest gesehen=False; dann wäre auchreste=ohne Aussage, deshalb verlangt W6 beides. - 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. - 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.
- Fail-closed trifft bisher laufende Projekte. Fehlt auf einem Rechner
iproute2odersetpriv, bekam die Plankritik bisher einen Mutantenlauf, jetztmutanten_status: ohne_ergebnis. Merkmal: Log der Plankritik „Hilfsagent ohne verwertbares Ergebnis“. Das ist gewollt. - 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
/tmpan und übergibtwslpath -wdieser Ordner (UNC-Pfad); das Skript wandelt mitwslpath -uzurück. Die Rundreise ist ungemessen und deshalb Vorprobe 1 und Startprüfung des Treibers. python3steht zusätzlich zuipundsetprivin der Werkzeugprüfung (W9), aus der Angriffsliste.- Über die Mindestliste hinaus gibt es W8 (Dienst des Rechners unerreichbar) und W9 (
python3fehlt). - 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
CapEffundCapAmbauchCapPrmundCapInh(gerechnet 0). - Die Vorprüfung gibt wie die bestehende
unshare-Prüfung keineEXIT-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.mdbeschreibt 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.jsverliert die Meldung. Endet das Skript mitMUTANTEN_LAUF_FEHLERohneEXIT-Zeile, setztplankritik.jsmutanten_statusaufohne_ergebnisund gibt die Rohausgabe nicht weiter; die Linse sieht den Grund nicht. Das gilt schon heute bei fehlendemunshare. Entscheidung beim Koordinator: im Skript eineEXIT-Zeile ausgeben oderplankritik.jsanpassen, in einem eigenen Plan.- Gerechnete Werte. Exit-Codes hinter
FEHLER:im Lauf gegen HEAD, Wortlaut vonipohne--keep-caps,CapPrmundCapInhgleich 0, Elternprozesstimeout: alles gerechnet; der Executer misst es in den Schritten 0, 2 und 4. rb3_mutanten.shfährt T1 und T12 weiter außerhalb des Namensraums. Ob es nach dem Lauf mit echten Daten weg kann, entscheidet der Koordinator.