chore(sdd): handoff snapshot — ledger, briefs, reports, review packages + HANDOFF.md for cross-machine continuation
This commit is contained in:
1 parent
acfba19fc2
commit
b934571b97
25 files changed
+17590
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user