docs(ST-1108): document M11 question-channel — API + architecture + pattern
docs/API.md — twee nieuwe secties: - 'Notifications' met /api/realtime/notifications SSE-endpoint (event-shapes, filter-rules, voorbeeld) - 'Cron — Expire questions' met /api/cron/expire-questions (Bearer-auth, schedule, response-shape, manual curl) docs/scrum4me-architecture.md — nieuw hoofdstuk 'Vraag-antwoord-kanaal Claude ↔ user' tussen QR-pairing-flow en Projectstructuur: - Mermaid sequence-diagram (Claude → DB → trigger → SSE → user → answer → trigger → Claude polls) - Threat-model-tabel (race, demo-misbruik, cross-product leak, cron-misbruik, growth, log-leakage) - Subsectie 'Waarom hergebruik scrum4me_changes-kanaal' met trade-off vs M10's eigen-kanaal-aanpak docs/patterns/claude-question-channel.md — herbruikbaar pattern 'Bidirectionele async-comms tussen MCP-agent en interactieve user' met de vier eindpunten, vier security-uitgangspunten, channel-strategie-tabel, TTL-richtlijn, en sjabloon-bestanden per laag (DB / server / client / MCP-tools). CLAUDE.md — extra rij in Implementatiepatronen-tabel die naar het nieuwe pattern-doc verwijst. Acceptatie 6 scenario's: 1. Sync happy path (MCP wait_seconds + UI submit) — handmatig getest tijdens ST-1105 acceptance-loop met de q-test injection 2. Async happy path — gedekt door get_question_answer-tool in ST-1102 + list_open_questions 3. Demo-block — actions/questions.test.ts (case 2: demo-user) + AnswerModal tooltip (visueel) 4. Access-isolation — notifications-stream.test.ts (case 'access-isolation') 5. Expiry — cron-expire-questions.test.ts (case '200 met juiste secret') 6. Race — actions/questions.test.ts (case 'al-answered' via atomic updateMany) Quality gates: lint 0 errors, tsc clean, vitest 151/151 (19 files), npm run build groen. M11 is hiermee feature-compleet. feat/M11-claude-questions heeft 12 commits lokaal, klaar voor user-acceptatie en PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eeed5d7506
commit
e5819ee079
4 changed files with 282 additions and 0 deletions
144
docs/patterns/claude-question-channel.md
Normal file
144
docs/patterns/claude-question-channel.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
# Patroon: Bidirectionele async-comms tussen MCP-agent en interactieve user
|
||||
|
||||
Het M11 vraag-antwoord-kanaal is herbruikbaar voor elke feature waarbij een
|
||||
**autonome agent** (Claude Code via MCP, een Vercel-job, etc.) iets wil
|
||||
ophelderen bij de **actieve gebruiker** zonder zelf te blokkeren of te raden.
|
||||
|
||||
> "Agent stelt vraag → vraag wacht persistent → user beantwoordt op een
|
||||
> moment dat het hem uitkomt → agent leest het antwoord en gaat door."
|
||||
|
||||
Voorbeelden waar dit zou kunnen passen:
|
||||
- AI-codereview: Claude vraagt om bevestiging op een controversiële refactor
|
||||
- Background-import: queue-worker vraagt om een keuze (overschrijven/skippen)
|
||||
per ambigue rij
|
||||
- Multi-step migratie: scripted task pauseert op een handmatige bevestiging
|
||||
- Approval-flow voor LLM-actions: user keurt een tool-call goed vóór 't echt
|
||||
gebeurt
|
||||
|
||||
---
|
||||
|
||||
## Vier eindpunten
|
||||
|
||||
| Endpoint | Auth | Doel |
|
||||
|---|---|---|
|
||||
| MCP `ask_*` (write) | API-token + demo-blok | maakt rij in `*_questions`-tabel; optioneel `wait_seconds` voor sync polling |
|
||||
| MCP `get_*_answer` (read) | API-token | leest huidige status + antwoord (voor latere session-pickup) |
|
||||
| `POST /actions/answer` (Server Action) | iron-session + product-membership | atomic `updateMany WHERE status='open'` |
|
||||
| `GET /api/realtime/<channel>` (SSE) | iron-session | user-scoped stream, filter op `entity` + product-access |
|
||||
|
||||
Plus:
|
||||
- **Postgres-trigger** op de tabel die `pg_notify` doet op een gedeeld
|
||||
channel met een `{op, entity, id, ...}`-payload
|
||||
- **Cron-endpoint** dat verlopen rijen markeert (status='expired') zodat
|
||||
achterstallige queries niet blijven groeien
|
||||
|
||||
---
|
||||
|
||||
## Vier security-uitgangspunten
|
||||
|
||||
1. **Atomic state-transities.** Antwoord-actie doet één UPDATE met alle
|
||||
invarianten in de WHERE (status, expiry, owner-check). Concurrent dubbele
|
||||
submit: PostgreSQL row-locking laat één caller count=1 zien, de rest 0.
|
||||
2. **Demo-blok op writes.** Agent-side via `requireWriteAccess` (PERMISSION_
|
||||
DENIED voor demo-tokens), user-side via early-return op `session.isDemo`
|
||||
en disabled-submit-knop met tooltip in de UI.
|
||||
3. **Access-isolation in SSE-filter.** Bij connect: query alle accessible
|
||||
product-IDs voor deze user → Set. In notification-handler: drop alle
|
||||
payloads waarvan `product_id ∉ accessibleSet`. Dit is naast de DB-query-
|
||||
filter; redundant maar voorkomt lekkage als de DB-laag iets doorlaat dat
|
||||
niet zou moeten.
|
||||
4. **Geen gevoelige data in logs.** Payload bevat alleen IDs en status — de
|
||||
tekst van vraag en antwoord komt via een aparte authenticated query.
|
||||
`console.log` alleen `question_id`, nooit content.
|
||||
|
||||
---
|
||||
|
||||
## Channel-strategie: hergebruik vs. eigen kanaal
|
||||
|
||||
Twee opties bij meerdere realtime-features:
|
||||
|
||||
| Optie | Voordeel | Nadeel |
|
||||
|---|---|---|
|
||||
| **Eigen channel per feature** (M10 `scrum4me_pairing`) | Geen filter-leakage tussen features | 1 LISTEN-connectie per feature; meer DB-resources; meer routes |
|
||||
| **Gedeeld channel met `entity`-key** (M11 `scrum4me_changes`) | 1 LISTEN per route; nieuwe entity = filter-aanpassing | Vergeten te filteren = leak |
|
||||
|
||||
Voor M11 is gekozen voor **hergebruik**: één channel scaalt beter naar v2
|
||||
(comments, mentions, status-updates allemaal op zelfde stream) en de filter-
|
||||
discipline is enforceable in code-review. Mitigatie voor leak-risico: expliciet
|
||||
`if (payload.entity === 'X') return false` in elke route die feature X niet
|
||||
hoort te zien — zoals `app/api/realtime/solo/route.ts` die `entity:'question'`
|
||||
weert.
|
||||
|
||||
---
|
||||
|
||||
## TTL-richtlijn
|
||||
|
||||
- **Question lifetime**: 24 u — kort genoeg dat verlaten queries niet
|
||||
ophopen, lang genoeg dat een gebruiker die afwezig is een nacht heeft om
|
||||
te antwoorden
|
||||
- **MCP-tool wait_seconds**: max 600 s — Claude wacht maximaal 10 min op een
|
||||
antwoord; daarna `status: 'pending'` zodat hij later kan terugkomen via
|
||||
`get_question_answer`
|
||||
- **Cron schedule**: `0 */6 * * *` — 4× per dag genoeg om 'expired' op te
|
||||
ruimen zonder Vercel-cron-budget te belasten
|
||||
|
||||
---
|
||||
|
||||
## Sjabloon-bestanden
|
||||
|
||||
Specifiek voor M11. Kopieer en pas aan:
|
||||
|
||||
### Database
|
||||
- `prisma/schema.prisma`: model met `id`, `status`, `expires_at`, denormalized
|
||||
`product_id` voor SSE-filter, asker/answerer-FKs, json `options?`-veld
|
||||
voor multiple-choice
|
||||
- `prisma/migrations/<ts>/migration.sql`: tabel-DDL + `notify_*_change()`-
|
||||
functie + `AFTER INSERT/UPDATE`-trigger op gedeeld channel
|
||||
|
||||
### Server (Scrum4Me)
|
||||
- `actions/questions.ts`: Server Action met getSession + Zod + demo-blok +
|
||||
productAccessFilter + atomic updateMany
|
||||
- `app/api/realtime/notifications/route.ts`: user-scoped SSE met
|
||||
initial-state-event ná LISTEN actief (race-fix conform M10 ST-1004)
|
||||
- `app/api/cron/expire-questions/route.ts`: Bearer-auth via CRON_SECRET +
|
||||
updateMany WHERE expires_at<now
|
||||
|
||||
### Client (Scrum4Me)
|
||||
- `stores/notifications-store.ts`: Zustand store met init/upsert/remove +
|
||||
selectors voor count en for-you-count
|
||||
- `lib/realtime/use-notifications-realtime.ts`: EventSource hook met
|
||||
state/message-handlers + reconnect-backoff + Page Visibility pause
|
||||
- `components/notifications/notifications-bell.tsx` + `notifications-sheet.tsx`
|
||||
+ `answer-modal.tsx`: Bell met badge, slide-over met item-list, Dialog
|
||||
met free-text/options-radio
|
||||
|
||||
### MCP-tools (scrum4me-mcp)
|
||||
- `src/tools/ask-user-question.ts`: write-tool met optionele `wait_seconds`-
|
||||
polling (intern setInterval tot status verandert of timeout)
|
||||
- `src/tools/get-question-answer.ts`: read-tool voor latere session-pickup
|
||||
- `src/tools/list-open-questions.ts`: read-tool voor session-start-check
|
||||
- `src/tools/cancel-question.ts`: write-tool, asker-only via atomic
|
||||
`updateMany WHERE asked_by + status='open'`
|
||||
|
||||
---
|
||||
|
||||
## Wanneer dit patroon NIET gebruiken
|
||||
|
||||
- Wanneer beide kanten al synchroon kunnen werken — dan is een gewone
|
||||
fetch/Server-Action eenvoudiger
|
||||
- Wanneer realtime niet kritiek is — een korte poll-loop is simpeler dan een
|
||||
SSE-stream
|
||||
- Wanneer er één centrale beslisser is — gebruik dan een gewone form-flow;
|
||||
het patroon hier is voor situaties waar de agent **niet hoeft te wachten**
|
||||
op één specifieke gebruiker
|
||||
|
||||
---
|
||||
|
||||
## Referenties
|
||||
|
||||
- Volledige flow + threat-model: `docs/scrum4me-architecture.md` § Vraag-
|
||||
antwoord-kanaal Claude ↔ user
|
||||
- Endpoint-contract: `docs/API.md` § Notifications + Cron
|
||||
- LISTEN/NOTIFY-pattern: `app/api/realtime/solo/route.ts` (M8 ST-802) — zelfde
|
||||
ReadableStream + heartbeat + hard-close + abort-cleanup
|
||||
- M10 vs M11 keuze tussen eigen/gedeeld kanaal: zie threat-model-tabel
|
||||
Loading…
Add table
Add a link
Reference in a new issue