Files
talk/.superpowers/sdd/plan/task-6-report.md
T

144 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.