Phase1 scan hole fixed: 4 German comment sites in lmcp.lua translated (case-sensitive scan missed them); re-attested GRUEN; dual suite contract anchored (34df8841)

This commit is contained in:
user.email
2026-08-10 13:02:05 +00:00
parent 89fe21c424
commit 83e7f39665
4 changed files with 358 additions and 10 deletions
+22
View File
@@ -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.
+79
View File
@@ -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.
+10 -10
View File
@@ -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",
+247
View File
@@ -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 <pfad/zu/lmcp.lua>
"""
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()