diff --git a/abnahme_phase1.md b/abnahme_phase1.md index b57ac63..7bbcc35 100644 --- a/abnahme_phase1.md +++ b/abnahme_phase1.md @@ -46,3 +46,25 @@ Begriffe, Logik unverändert, Suite 27/27, Smoke 6/6 identisch zur Baseline. json.lua, Suite) - Baseline vor Phase 1: /root/harvest/phase1_baseline/ - Patch: /root/harvest/coder_maxbody_65536.patch; Deploy-Doku: /root/harvest/deploy_coder_65536.md + +## Nachtrag 2026-08-10 (Korrektur: Scan-Loch im German-Scan) + +Beim Review der Phase-2-Suite (konformitaet_dual.py) fiel auf: der German-Scan von +Phase 1 ("0 Treffer") war FEHLERHAFT — die dokumentierte Suchmaske lief +case-sensitiv, wodurch deutsche Kommentare mit grossem Anfangsbuchstaben +(`Fassung`, `Sitzung`, `Methode`, und ein ganzer Block L.66-70: `Erlaubnisliste`, +`Werkzeug`, `verhindern`, ...) durchrutschten. Betroffen: lmcp.lua (4 Stellen: +L.66-70 Block, L.1082, L.1418-1419). + +Korrektur (operator work, reine Kommentar-UEbersetzung, keine Logik): +- L.66-70: Erlaubnisliste-Block -> Englisch +- L.1082: `unbekannte Methode` -> `unknown method` +- L.1418-1419: SSE/405-Block -> Englisch +- Neuer Scan (case-insensitive + erweiterte Wortliste, nur Kommentare): alle + *.lua CLEAN. Einziger Treffer war ein False Positive ("die" = englisches Verb). +- lmcp.lua sha: 72253549ce1f1a45... -> 4a816a44474e1766f8023aadcce2c76a... +- Re-Attest: bullpen-attest --impl lmcp.lua --test konformitaet_2026_07_28.py -> + GRUEN, rueckgabe 0, Test-sha unveraendert c90450a6... + +Lehre fuer kuenftige Scans: German-Checks IMMER case-insensitive ausfuehren und +die Kommentar-Suche mit einer Funktionswort-Liste (und/oder/nicht/...) ergaenzen. diff --git a/kickoff_p2_suite.md b/kickoff_p2_suite.md new file mode 100644 index 0000000..a135a00 --- /dev/null +++ b/kickoff_p2_suite.md @@ -0,0 +1,79 @@ +KAMPAGNE (b) DUAL-PROTOKOLL — KICKOFF, SCHRITT 1 @TESTDESIGNER: ERWEITERTE SUITE + +PROBLEM +PO-Go für Phase 2 (Dual-Protokoll) ist erteilt. Die bestehende Suite +(konformitaet_2026_07_28.py, 27/27, sha256 c90450a6240f133c…, Vertrag) bleibt +UNVERAENDERT — sie ist der Regression-Net für die neue Fassung. Phase 2 braucht eine +ERWEITERTE, version-aware Suite als neuen Vertrag für den Dual-Modus: alte Fassung +(2025-06-18) und neue Fassung (2026-07-28) müssen beide funktionieren, version-routed. +Der Vertrag kommt zuerst: die erweiterte Suite wird geschrieben und verankert, BEVOR +irgendetwas implementiert wird. + +WHERE +Die Arbeitskopie lebt in /root/spec-2026-07-28/ (lmcp.lua, server.lua, json.lua, +envelope.lua, versions.lua, konformitaet_2026_07_28.py, SPEC-AUSZUG.md, UEBERGABE.md). +Die erweiterte Suite ist eine NEUE Datei neben der alten — die alte wird nicht angefasst. +Referenz für das Alt-Verhalten: /root/harvest/_lmcp_ref @ 3409a41 (initialize/ping/ +logging/setLevel, Session-Auflösung — die alte Fassung lebt heute auf dem Raum-Server). + +TRAPS (Verboten — die plausiblen Fehler) +- NICHT die bestehende Suite ändern oder schwächen: konformitaet_2026_07_28.py bleibt + byte-identisch (sha256 c90450a6…), der 27er-Bestand muss weiter grün sein. Die neue + Suite ist eine NEUE Datei (z.B. konformitaet_dual.py), die die alten 27 Fälle ÜBERNIMMT + (kopiert, nicht modifiziert) und die Alt-Fassungs-Fälle HINZUFÜGT. +- KEINE Self-Tests im Coder-Teil: die Suite ist der Test. +- NICHT nach "Header vorhanden?" routen: die ANGEKÜNDIGTE Version entscheidet + (MCP-Protocol-Version-Header ODER _meta["io.modelcontextprotocol/protocolVersion"]). +- KEINE Version angekündigt -> ALTE Fassung (2025-06-18), PO-Entscheidung E2 — die + Raum-Klienten sprechen 2025-06-18 ohne Header. +- Kein Zugriff auf den laufenden Raum-Server: die Suite testet die Arbeitskopie. + LMCP_HOST/LMCP_PORT/LMCP_TOKEN gegen einen LOKALEN Testserver aus server.lua. + +ANFORDERUNG (Zieldatei: konformitaet_dual.py im Verzeichnis der Arbeitskopie) +Schreibe eine version-aware Conformance-Suite in PYTHON (frisch geschriebener Code auf +Englisch). Sie testet gegen einen lokal gestarteten Server (server.lua) via +urllib/HTTP wie die bestehende Suite. Abdeckung: + +A) NEUE Fassung (2026-07-28) — alle 27 Verhalten der bestehenden Suite, unverändert + übernommen (die 27 Prüfungen der alten Suite als Teilmenge; bei Abweichung ist die + Übernahme falsch). Insbesondere: initialize -> -32601/404 (entfernt), + Mcp-Method/Header-Pflicht, _meta-Pflicht, Version-Gate unbekannt -> -32022 mit + data.supported=['2026-07-28'] (hier: mit BEIDEN Fassungen, s.u.), Keystone + "discover enthält 2026-07-28". + +B) ALTE Fassung (2025-06-18) — die Alt-Verhalten, version-routed: + 1. server/discover mit Header MCP-Protocol-Version: 2025-06-18 -> 200, + supportedVersions enthält BEIDE: "2026-07-28" UND "2025-06-18". + 2. initialize (2025-06-18) -> 200, NICHT -32601 (alte Methode wieder aktiv). + 3. ping (2025-06-18) -> 200. + 4. logging/setLevel (2025-06-18, level="debug") -> 200. + 5. KEINE Envelope-Pflicht für alte Klienten: POST ohne Mcp-Method-Header und ohne + MCP-Protocol-Version-Header -> NICHT -32020 (die alte Fassung kennt den + Envelope-Check nicht; Default-Routing = alt, E2). + 6. Unbekannte Version (z.B. 1900-01-01) -> 400, code -32022, data.supported enthält + BEIDE Fassungen, data.requested = '1900-01-01'. + +C) KONSISTENZ: derselbe Request mit derselben angekündigten Version muss dasselbe + Ergebnis geben, egal ob die Version im Header oder in _meta steht (wo beides + zulässig ist). + +Der Testserver-Aufruf erfolgt wie in der alten Suite (LMCP_PORT aus env mit Default, +LUA_PATH auf das Arbeitskopie-Verzeichnis, server.lua starten). Der Test ist ein +runnahes Skript: `python3 konformitaet_dual.py lmcp.lua` -> Exit 0 und Zähler +"X ok, 0 OFFEN" analog zur alten Suite. + +ABNAHME (@testdesigner selbst) +- Die Suite LÄUFT gegen die Arbeitskopie, sobald sie implementiert ist — jetzt noch + nicht: der Implementierungsstand ist alt (nur 2026-07-28). Lieferung = die neue + Datei konformitaet_dual.py (Rohtext, keine Fences) + sha256 + Erklärung, welche + Prüfungen übernommen (27) und welche neu (B-Teil) sind. +- WICHTIG: die alte Suite muss unverändert bleiben (sha256 c90450a6…) — mitliefern als + Beleg. + +CONSTRAINTS +- Nur die neue Datei konformitaet_dual.py entsteht. Keine Änderung an lmcp.lua, + versions.lua, envelope.lua, server.lua, konformitaet_2026_07_28.py. +- Neuer Code auf Englisch (PO-Ziel (a)). +- Lieferung als Rohtext im Raum, keine Fences. + +Dispatch, then END YOUR TURN. diff --git a/phase1_result/lmcp.lua b/phase1_result/lmcp.lua index 395af6d..f2a857f 100644 --- a/phase1_result/lmcp.lua +++ b/phase1_result/lmcp.lua @@ -63,11 +63,11 @@ function lmcp.new(name, opts) self.host = opts.host or "0.0.0.0" self.port = opts.port or 8080 self.tools = {} - -- Erlaubnisliste je Instanz (LMCP_TOOL_ALLOW, kommagetrennt). Ist sie - -- gesetzt, registriert `tool()` NUR diese Namen -- Built-ins wie Plugins. - -- Nicht gesetzt: alles wie bisher. Das ist die einzige Stelle, an der ein - -- Werkzeug entsteht, also die einzige, an der man es verhindern kann; - -- nachtraeglich loeschen muss jeden kuenftigen Eintrag kennen und veraltet. + -- Per-instance allowlist (LMCP_TOOL_ALLOW, comma-separated). When set, + -- `tool()` registers ONLY those names -- built-ins like plugins. + -- When unset: everything as before. This is the only place where a + -- tool is created, hence the only place where it can be prevented; + -- removing one later means every future entry must know and stale out. self.tool_allow = nil do local roh = os.getenv("LMCP_TOOL_ALLOW") @@ -1079,7 +1079,7 @@ local _finalise_dispatch -- following parts (header, _meta, version) get their 400s without anyone -- having to add to it again. -- --- -32601 unbekannte Methode -> 404 („MUST respond with 404 Not +-- -32601 unknown method -> 404 („MUST respond with 404 Not -- Found and … -32601") -- -32020 HeaderMismatch -> 400 -- -32021 MissingRequiredClientCapability -> 400 @@ -1414,10 +1414,10 @@ local function _conn_read(self, conn) conn.state = "writing" elseif conn.method == "GET" or conn.method == "DELETE" then -- MCP 2026-07-28: „HTTP GET or DELETE to the MCP endpoint: respond - -- with 405 Method Not Allowed." Beide trugen die Mechanik der - -- alten Fassung — GET oeffnete einen eigenstaendigen SSE-Strom, - -- DELETE beendete eine Sitzung. Zitat der Spec zu beidem: „None of - -- these mechanisms are part of this revision." + -- with 405 Method Not Allowed." Both carried the mechanics of the + -- old version — GET opened a separate SSE stream, DELETE ended + -- a session. Spec quote for both: „None of these mechanisms are + -- part of this revision." conn.write_buf = _build_http_response("405 Method Not Allowed", { ["Content-Type"] = "text/plain", ["Allow"] = "POST, OPTIONS", diff --git a/relay_suite_to_testdesigner.md b/relay_suite_to_testdesigner.md new file mode 100644 index 0000000..31c0d6c --- /dev/null +++ b/relay_suite_to_testdesigner.md @@ -0,0 +1,247 @@ +ENTSCHEIDUNG zu deiner Eskalation #872: OPTION B (Rohtext im Raum). + +Der Inhalt der alten Suite konformitaet_2026_07_28.py (sha256 c90450a6240f133c...) +steht unten VERBATIM (234 Zeilen, 10861 Bytes, unveraendert — nachpruefbar per sha256). +Re-dispatch an @testdesigner: unveraendert den Auftrag von ask 866, aber mit dieser +Quelle als Anlage (Block QUELLTEXT). Hinweis fuer @testdesigner: die 27 Faelle der alten +Suite in die neue konformitaet_dual.py UEBERNEHMEN (als Teilmenge, Verhalten exakt gleich; +nur Version-Gate-Erwartung data.supported auf BEIDE Fassungen erweitern, weil der +Implementierungsstand sich aendert). Zusaetzliche Referenz fuer Alt-Verhalten +(initialize/ping/logging/setLevel): die alte Fassung ist in /root/harvest/_lmcp_ref +gespiegelt, aber die B-Faelle sind im Auftragstext schon vollstaendig spezifiziert. + +QUELLTEXT (konformitaet_2026_07_28.py, verbatim): +#!/usr/bin/env python3 +"""Conformance suite for MCP 2026-07-28, cut from the specification itself. + +Sources fetched 2026-08-09 and quoted per check: + /specification/2026-07-28/ (index) + /specification/2026-07-28/basic (messages, _meta, error codes) + /specification/2026-07-28/basic/versioning (version negotiation) + /specification/2026-07-28/server/discover (DiscoverResult shape) + /specification/2026-07-28/basic/transports/streamable-http (headers, statuses) + +NOT derived from the changelog or from any second-hand delta: two field names in +the earlier draft were guessed wrong (`protocolVersions` instead of +`supportedVersions`, `serverInfo` at top level instead of inside `_meta`). + +The spec normalises BOTH the JSON-RPC error code and the HTTP status, so both +are checked. A check that only looks at the body would pass a server that +answers 200 where the spec demands 400. + +Expected result on an unmigrated server: almost everything OFFEN. That is the +measurement — the distance to the target — not a failure of the suite. + + python3 konf2026.py +""" +import json +import os +import socket +import subprocess +import sys +import time +import urllib.error +import urllib.request + +ZIEL = "2026-07-28" +KLON = (os.path.dirname(os.path.abspath(sys.argv[1])) if len(sys.argv) > 1 + else "/home/mfritsche/src/lmcp-work") + +_offen = 0 +def pruefe(punkt, was, ok, beleg=None, bekommen=None): + global _offen + if not ok: + _offen += 1 + print(f" [{'ok ' if ok else 'OFFEN '}] {punkt:<7} {was}") + if not ok and bekommen is not None: + print(f" bekommen: {bekommen}") + if not ok and beleg: + print(f" Spec: {beleg}") + + +META = {"io.modelcontextprotocol/protocolVersion": ZIEL, + "io.modelcontextprotocol/clientCapabilities": {}, + "io.modelcontextprotocol/clientInfo": {"name": "konf", "version": "1"}} + + +def freier_port(): + s = socket.socket(); s.bind(("127.0.0.1", 0)); p = s.getsockname()[1]; s.close(); return p + + +def ruf(port, token, methode, params=None, kopf=None, meta=META, art="POST"): + """Gibt (jsonrpc-objekt, http-status) zurueck. Beides zaehlt.""" + p = dict(params or {}) + if meta is not None: + p["_meta"] = meta + rumpf = {"jsonrpc": "2.0", "id": 1, "method": methode} + if p: + rumpf["params"] = p + k = {"Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + "Authorization": "Bearer " + token, + "MCP-Protocol-Version": ZIEL, + "Mcp-Method": methode} + if kopf is not None: + for name, wert in kopf.items(): + if wert is None: + k.pop(name, None) + else: + k[name] = wert + req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", + json.dumps(rumpf).encode(), k, method=art) + try: + a = urllib.request.urlopen(req, timeout=20) + roh, status = a.read().decode(), a.status + except urllib.error.HTTPError as e: + roh, status = e.read().decode(), e.code + except Exception as e: + return {"_transport": f"{type(e).__name__}: {e}"}, 0 + for z in roh.splitlines(): + if z.startswith("data: "): + roh = z[6:]; break + try: + return json.loads(roh), status + except Exception: + return {"_roh": roh[:160]}, status + + +def code(a): + return (a.get("error") or {}).get("code") + + +port, token = freier_port(), "konf-2026-07-28" +umg = dict(os.environ, LMCP_PORT=str(port), LMCP_TOKEN=token, LMCP_HOST="127.0.0.1", + LUA_PATH=f"{KLON}/?.lua;;") +srv = subprocess.Popen(["lua5.4", f"{KLON}/server.lua"], cwd=KLON, env=umg, + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) +try: + for _ in range(60): + time.sleep(0.5) + try: + socket.create_connection(("127.0.0.1", port), 1).close(); break + except OSError: + if srv.poll() is not None: + print(" Server startete nicht:", (srv.stderr.read() or b"").decode()[:300]) + sys.exit(2) + else: + print(" Server kam nicht hoch"); sys.exit(2) + + print(f" MCP {ZIEL} — Konformitaet von {KLON}\n") + + # ---- server/discover --------------------------------------------------- + a, st = ruf(port, token, "server/discover") + r = a.get("result") or {} + pruefe("discover", "server/discover wird beantwortet", "result" in a, + "server/discover: „Servers MUST implement it.\"", json.dumps(a)[:130]) + pruefe("discover", "result.supportedVersions ist eine nicht-leere Liste", + isinstance(r.get("supportedVersions"), list) and bool(r.get("supportedVersions")), + "DiscoverResult: supportedVersions", repr(r.get("supportedVersions"))) + pruefe("discover", "result.capabilities ist ein Objekt", + isinstance(r.get("capabilities"), dict), None, repr(r.get("capabilities"))) + pruefe("discover", "_meta traegt io.modelcontextprotocol/serverInfo", + isinstance((r.get("_meta") or {}).get("io.modelcontextprotocol/serverInfo"), dict), + "„Servers SHOULD include this field.\"", repr((r.get("_meta") or {}))[:90]) + pruefe("discover", f"supportedVersions enthaelt {ZIEL}", + ZIEL in (r.get("supportedVersions") or []), None, + repr(r.get("supportedVersions"))) + + # ---- resultType auf JEDEM Ergebnis ------------------------------------- + for m in ("server/discover", "tools/list"): + a, _ = ruf(port, token, m) + rt = (a.get("result") or {}).get("resultType") + pruefe("result", f"{m}: result.resultType == \"complete\"", rt == "complete", + "„The result MUST include a resultType field.\"", repr(rt)) + + # ---- _meta ist Pflicht -------------------------------------------------- + a, st = ruf(port, token, "tools/list", meta=None) + pruefe("_meta", "fehlendes _meta -> -32602", code(a) == -32602, + "„A request missing any required field is malformed; the server MUST " + "reject it with -32602.\"", f"code={code(a)}") + pruefe("_meta", "fehlendes _meta -> HTTP 400", st == 400, + "„On HTTP, the response status MUST be 400 Bad Request.\"", f"HTTP {st}") + + ohne_caps = {k: v for k, v in META.items() + if k != "io.modelcontextprotocol/clientCapabilities"} + a, st = ruf(port, token, "tools/list", meta=ohne_caps) + pruefe("_meta", "fehlende clientCapabilities -> -32602", code(a) == -32602, + "clientCapabilities: Required = Yes", f"code={code(a)}") + + # ---- Fassungsaushandlung ------------------------------------------------ + schlecht = dict(META, **{"io.modelcontextprotocol/protocolVersion": "1900-01-01"}) + a, st = ruf(port, token, "tools/list", meta=schlecht, + kopf={"MCP-Protocol-Version": "1900-01-01"}) + pruefe("version", "unbekannte Fassung -> -32022", code(a) == -32022, + "„it MUST respond with an UnsupportedProtocolVersionError\"", f"code={code(a)}") + pruefe("version", "unbekannte Fassung -> HTTP 400", st == 400, + "„MUST respond with 400 Bad Request and an UnsupportedProtocolVersionError\"", + f"HTTP {st}") + d = (a.get("error") or {}).get("data") or {} + pruefe("version", "Fehler nennt data.supported", isinstance(d.get("supported"), list), + None, repr(d)[:90]) + pruefe("version", "Fehler nennt data.requested == angefragte Fassung", + d.get("requested") == "1900-01-01", + "Beispiel der Spec: data.requested: \"1900-01-01\"", repr(d.get("requested"))) + + # ---- Koepfe ------------------------------------------------------------- + a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": None}) + pruefe("header", "fehlender Mcp-Method -> -32020", code(a) == -32020, + "„A required standard header … is missing.\" -> HeaderMismatch", f"code={code(a)}") + pruefe("header", "fehlender Mcp-Method -> HTTP 400", st == 400, + "„servers MUST return HTTP status 400 Bad Request\"", f"HTTP {st}") + + a, st = ruf(port, token, "tools/list", kopf={"Mcp-Method": "prompts/list"}) + pruefe("header", "Mcp-Method != method -> -32020", code(a) == -32020, + "„values specified in the headers do not match … MUST reject\"", f"code={code(a)}") + + a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": None}) + pruefe("header", "fehlender MCP-Protocol-Version -> -32020", code(a) == -32020, + "„Every POST request … MUST include an MCP-Protocol-Version header.\"", + f"code={code(a)}") + + a, st = ruf(port, token, "tools/list", kopf={"MCP-Protocol-Version": "2025-06-18"}) + pruefe("header", "Kopf-Fassung != _meta-Fassung -> -32020", code(a) == -32020, + "„If the values do not match, the server MUST reject … HeaderMismatch\"", + f"code={code(a)}") + + # ---- unbekannte Methode ------------------------------------------------- + a, st = ruf(port, token, "gibt/esnicht") + pruefe("method", "unbekannte Methode -> -32601", code(a) == -32601, None, f"code={code(a)}") + pruefe("method", "unbekannte Methode -> HTTP 404", st == 404, + "„it MUST respond with 404 Not Found and a JSON-RPC error with code -32601\"", + f"HTTP {st}") + + # ---- gestrichene Methoden ---------------------------------------------- + for m in ("initialize", "ping", "logging/setLevel"): + a, _ = ruf(port, token, m) + pruefe("entfernt", f"{m} existiert nicht mehr (-32601)", code(a) == -32601, + "SEP-2575: entfernt", f"code={code(a)}") + + # ---- verbotene Fehlercodes --------------------------------------------- + a, _ = ruf(port, token, "resources/read", {"uri": "file:///gibtsnicht"}, + kopf={"Mcp-Name": "file:///gibtsnicht"}) + pruefe("codes", "unbekannte Ressource -> -32602, NICHT -32002", code(a) == -32602, + "„Implementations of this protocol version MUST NOT emit … -32002\"", + f"code={code(a)}") + + # ---- GET/DELETE auf dem Endpunkt --------------------------------------- + for art in ("GET", "DELETE"): + try: + req = urllib.request.Request(f"http://127.0.0.1:{port}/mcp", method=art, + headers={"Authorization": "Bearer " + token}) + st = urllib.request.urlopen(req, timeout=10).status + except urllib.error.HTTPError as e: + st = e.code + except Exception: + st = 0 + pruefe("transport", f"HTTP {art} auf dem Endpunkt -> 405", st == 405, + "„respond with 405 Method Not Allowed\"", f"HTTP {st}") + + print() + print(f" {_offen} Punkte OFFEN") + sys.exit(1 if _offen else 0) +finally: + srv.terminate() + try: + srv.wait(5) + except Exception: + srv.kill()