chore(sdd): handoff snapshot — ledger, briefs, reports, review packages + HANDOFF.md for cross-machine continuation

This commit is contained in:
m3ta-chiron committed 2026-10-18 21:03:24 +02:00
1 parent acfba19fc2
commit b934571b97
25 files changed
+17590

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
# Task 6 Report: CORS-Tür + MCP-Endpoint
**Status:** DONE
**Datum:** 2026-10-18
**Basis:** 24ceff3 (Tasks 1–5 verifiziert) → HEAD `30bd535`
**Commits (feat/v1, kein Push, .scratch/ unberührt):**
1. `cef6e6b` feat(config): CORS-Erlaubnisliste server.allowed_origins (TALK_ALLOWED_ORIGINS)
2. `156c20d` feat(api): CORS-Tür für POST /speak — OPTIONS-Preflight und ACAO aus Config
3. `a577597` feat(api): MCP-Endpoint POST /mcp — schlankes eigenes JSON-RPC-Subset mit speak-Tool
4. `150c374` test: Prozessgrenzen-Tests für CORS-Tür und MCP-Handshake
5. `30bd535` docs(readme): CORS-Konfiguration und MCP-Endpoint mit curl-Handshake dokumentiert
## Was geliefert wurde
### 1. CORS-Tür (nur POST /speak)
- **`internal/api/cors.go`** (neu): `OPTIONS /speak` beantwortet Browser-Preflights mit
`204 No Content` + `Access-Control-Allow-Origin` (exakt gematchte Origin bzw. literales
`*` bei Wildcard-Konfiguration), `Access-Control-Allow-Methods: POST, OPTIONS`,
`Access-Control-Allow-Headers: Content-Type`, `Access-Control-Max-Age: 600`.
Echte (non-preflight) `POST /speak`-Antworten tragen ACAO, wenn die Anfrage-Origin
gelistet ist. `Vary: Origin` steht immer auf /speak (Cache-Korrektheit).
- **Ungelistete Origin:** sauberer 204/202 **ohne** jegliche CORS-Header — die Blockade
macht der Browser; HTTP bleibt für Nicht-Browser-Clients sauber erreichbar.
- **Leere Liste (Default):** gar keine CORS-Header — Tür zu, bis konfiguriert.
- **Nur /speak:** `OPTIONS /status` → 405 (kein Preflight-Support), `GET /status` ohne
ACAO; kein anderer Endpunkt sendet CORS-Header (Prozessgrenzen-Test `TestCORSNurAufSpeak`).
- **`internal/config`:** `server.allowed_origins` (TOML-Liste) + Env-Override
`TALK_ALLOWED_ORIGINS` (kommagetrennt, Whitespace toleriert; `off` leert die Liste).
Wildcard `*` ist explizit erlaubt, NICHT Default. Leere Einträge → klarer Fehler.
Unit-Tests: `TestLoadAllowedOrigins` (Defaults, TOML, Env schlägt Datei, off,
Wildcard, Fehlerfälle).
- **`GET /status`:** additives Feld `cors: [origins]` — immer vorhanden, `[]` (nicht
`null`) bei geschlossener Tür (Compat-Anforderung des Briefs; Test `TestStatusCorsFeld`).
### 2. MCP-Endpoint (Streamable HTTP)
- **`internal/api/mcp.go`** (neu): `POST /mcp` — **eigene schlanke Implementierung des
JSON-RPC-2.0-Subsets, kein SDK-Dependency** (bindende Decision des Briefs; es wurde
auch keins in Betracht gezogen — der benötigte Umfang sind ~370 Zeilen inkl. Doku,
ein SDK wäre schwerer als der Payload). Methoden: `initialize`,
`notifications/initialized`, `ping`, `tools/list`, `tools/call`.
- **Ein Tool `speak`:** Input-Schema exakt nach Brief — `{text: string, priority?,
provider?, lang?, now?}`, `required: ["text"]`, enums für priority/lang (Test prüft
Exactly-those-5-Properties).
- **`tools/call` = `POST /speak`:** Beide Türen fahren denselben Aufnahme-Pfad
`admitSpeak` (Refactoring in `internal/api/server.go`): Validierung → Mute-Check →
Enqueue inkl. Gesetz (Rate-Limit-Budget, Ruhezeit; Rangordnung mute > now > quiet >
rate aus Task 05 bleibt unverändert in `internal/queue`). Kein Contract-Drift möglich.
- **Ergebnis-Texte:** Erfolg → `queued: <id>`; Mute → `muted: Ansage verworfen (Mute
ist aktiv)`; Verwurf → `discarded: rate_limit …` / `discarded: quiet_hours …`
(+ Toast-Hinweis). **Bewusst `isError: false`** bei muted/discarded (deliberate
Decision laut Brief: dokumentiertes Aufnahme-Ergebnis, kein Protokollfehler — der
Agent soll den Grund lesen, nicht crashen).
- **`GET /status`:** additives Feld `mcp: true` (Test `TestStatusMCPFeld`).
- Daemon-Log-Zeile nennt jetzt cors-Liste und mcp.
## MCP-Protokoll-Entscheidungen (dokumentationspflichtig)
1. **Version-Handling:** `initialize` **echot die angefragte `protocolVersion`**
(akzeptiert sie); fehlt sie, antwortet talkd mit `2025-06-18`. Inhaltlich
implementiert ist der 2025-06-18-Umfang: Initialize-Ergebnis mit
`protocolVersion`, `capabilities: {tools: {}}`, `serverInfo: {name: "talkd",
version: <daemon-version>}`; Streamable HTTP auf einem Endpoint; keine Batches
(seit 2025-06-18 entfernt → `-32600` mit klarer Meldung). Getestet: Echo von
`2025-06-18` und `2025-03-26`.
2. **GET /mcp:** talkd pusht nie (statische Tool-Liste, keine Server-Notifications,
kein Sampling) — server-initiierte SSE-Streams entfallen daher komplett. `GET /mcp`
antwortet **`405 Method Not Allowed` mit `Allow: POST, OPTIONS`** — exakt die im
Streamable-HTTP-Transport vorgesehene Antwort für Server ohne Server-Streams.
`DELETE /mcp` (Session-Terminierung) fällt auf den auto-405 des Routers.
3. **Content-Types:** Antworten auf Requests: `200 OK` + `application/json` — auch für
JSON-RPC-Fehlerobjekte (der HTTP-Teil war erfolgreich; getestet in
`TestMCPFehlerPfade`). Notifications: `202 Accepted` mit leerem Body (JSON-RPC 2.0:
nie eine Antwort). Requests: der Daemon erzwingt kein `Content-Type` (curl-freundlich,
strikter wäre SDK-Komfort ohne Vertragsgewinn).
4. **Zustandslos, keine Session-Enforcement:** Jeder POST ist unabhängig; `Mcp-Session-Id`
wird ignoriert (nie gesetzt), und die Initialize-vor-allem-Regel wird NICHT erzwungen —
ein curl kann `tools/call` direkt rufen. MCP-Clients fahren den Handshake freiwillig;
Tests fahren ihn als volle Sequenz.
5. **JSON-RPC-Subset-Details:** Notification = `id` fehlt oder ist explizit `null`
(→ 202; unbekannte Notifications werden still ignoriert). Fehlercodes: kaputtes
JSON `-32700`, falsche jsonrpc-Version/Batch `-32600`, unbekannte Methode `-32601`,
Validierung/unbekanntes Tool `-32602`, interner Fehler (enqueue, HTTP-Äquivalent
503) `-32603`. HTTP-400-Validierung mapped also auf `-32602` + deutscher Klartext
(identische Meldungen wie POST /speak). `id` wird roh geechot (number/string/
null-sicher via `json.RawMessage`). `ping` → leeres Result (billig, hilft Clients).
Body-Limit 64 KiB wie /speak.
6. **Kein CORS auf /mcp** (bindend: CORS nur auf POST /speak) — MCP-Clients sind keine
Browser. Sollte später ein Browser-MCP-Client entstehen, muss die Tür explizit
erweitert werden (bewusste Grenze, Spec: „LibreChat-Frontend-Anbindung out of scope").
## Tests (Prozessgrenze, primäre Seam — harness erweitert, nicht ersetzt)
`test/harness_test.go`: `statusBody` +`mcp`/`cors`; neue Helfer `doRequest` (rohe
HTTP-Requests mit Headern), `preflight`, `mcpPost` (literale JSON-RPC-Payloads, kein
Client-SDK), `mcpDecode`.
- **`test/cors_test.go`:** Preflight von gelisteter Origin → 204 + alle Header
(Abnahmekriterium); ungeliste Origin → kein ACAO; echter POST mit/ohne gelistete
Origin; leere Liste → gar keine CORS-Header (Default); Wildcard `*` → ACAO `*`;
CORS nur auf /speak (OPTIONS /status 405, GET /status ohne ACAO); `cors`-Feld in
/status (gelistet + `[]` bei leerer Liste).
- **`test/mcp_test.go`:** kompletter Handshake als literal JSON-RPC über HTTP —
initialize (Version-Echo 2025-06-18 und 2025-03-26, capabilities.tools, serverInfo)
→ notifications/initialized (202, leerer Body) → tools/list (genau speak, Schema mit
required=[text], genau die 5 Properties) → tools/call (`queued: <id>`, isError
falsch, **Ansage am Fake-Provider sichtbar** = echte Queue-Aufnahme, Abnahmekriterium).
Gesetze: muted → isError:false + „muted …", keine Synthese; rate_limit (30 s) →
zweite low verworfen als „discarded: rate_limit …"; quiet_hours → „discarded:
quiet_hours …". Fehlerpfade: text fehlt/leer, bad priority, unbekanntes Tool,
unbekannte Methode, kaputtes JSON, falsche jsonrpc-Version, Batch — je korrekter
Code + Meldungsanteil, Status 200; ping; GET /mcp → 405 + Allow; unbekannte
Notification still 202. Alle Daemons auf ephemeralen Ports (TALK_PORT), isoliertes
HOME/XDG, sink=null, Fake-Provider.
## Verifikation
- `gofmt -l .` leer; `go vet ./...` grün; `go build ./...` grün;
`go test ./...` komplett grün (inkl. aller Tasks 1–5-Tests, keine Regressionen —
die CLI dekodiert /status mit den additiven Feldern ohne Anpassung).
- **Cross-Compile-Smoke** (CGO_ENABLED=0): `GOOS=darwin GOARCH=amd64` und
`GOOS=windows GOARCH=amd64` für `talk` UND `talkd` gebaut — erfolgreich.
## Self-Review (git diff 24ceff3..HEAD)
- Ein Verantwortlicher pro Datei: cors.go (CORS), mcp.go (MCP), server.go (Routing +
admitSpeak-Refactoring), config.go (Liste + Env), Tests getrennt nach(cors|mcp).
- `admitSpeak` ist die einzige Aufnahme-Stelle — POST /speak-Verhalten unverändert
(alle Task-04/05-Prozesstests laufen unverändert grün).
- Keine Abnahme-Lücken gegenüber dem Brief: alle vier bindenden Testszenarien
(preflight gelistet / ungelistet / leere Liste / \*), Handshake + tools/call mit
Fake-Beobachtung, /status-Felder, curl-Sequenz in README.
## Concerns / Grenzen
- `notifications/initialized` MIT id (falsch benutzter Client) bekommt ein leeres
Result statt Fehler (tolerante Wahl, dokumentiert im Code).
- CORS-Preflight wird bewusst auch bei fehlendem `Origin`-Header mit 204 beantwortet
(kein CORS-Kontext, keine Header) — harmlos, dokumentiert.
- OS-Toast in Ruhezeit bleibt wie in Task 5 eine Log-Zeile (Notification-Anbindung
folgt später) — betrifft MCP-/CORS-Pfad nicht.