mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
feat(mcp): M1+M1.5 MCP gateway + tool-registry + shared-crypto
Foundation for autonomous Claude-driven testing. Plan:
docs/plans/mana-mcp-and-personas.md.
New packages
- @mana/tool-registry — schema-first ToolSpec<InputSchema, OutputSchema>
with zod generics, scope ('user-space' | 'admin') and policyHint
('read' | 'write' | 'destructive'). sync-client helpers speak the
mana-sync push/pull protocol directly so RLS and field-level LWW are
preserved. MasterKeyClient fetches per-user MKs via the existing
mana-auth GET /api/v1/me/encryption-vault/key endpoint (JWT-gated,
ZK-aware, already audited) — no new service-key endpoint built.
ZeroKnowledgeUserError surfaced as a typed throw.
- @mana/shared-crypto — AES-GCM-256 primitives extracted from the web
app's $lib/data/crypto/aes.ts so the server-side tool handlers and the
browser produce byte-for-byte identical wire format
(enc:1:{b64(iv)}.{b64(ct)}). Web app aes.ts now re-exports from
shared-crypto — 5 existing importers unchanged, svelte-check stays
green.
New service
- services/mana-mcp (:3069, Bun/Hono) — MCP Streamable HTTP gateway.
JWKS auth against mana-auth, per-user session isolation (session-id
belongs to the user who opened it — cross-user access returns 403),
admin-scoped tools filtered out before registration. MasterKeyClient
cached per process with a 5-minute TTL.
11 tools registered
- habits.{create,list,update,archive}, spaces.list (plaintext, M1)
- todo.{create,list,complete}, notes.{create,search}, journal.add
(encrypted — field lists match
apps/mana/apps/web/src/lib/data/crypto/registry.ts verbatim)
Infra
- Port 3069 added to docs/PORT_SCHEMA.md
- services/mana-mcp/CLAUDE.md with architecture, auth model,
tool-authoring recipe, local smoke-test steps
- Root CLAUDE.md services list updated
Type-check green across shared-crypto, mana-tool-registry, mana-mcp.
svelte-check on apps/mana/apps/web stays at 0 errors / 0 warnings.
Boot smoke verified: /health returns registry.loaded=true, unauthed
/mcp → 401, invalid-JWT /mcp → 401 with descriptive message.
Decisions locked in for later milestones (per plan D1–D10):
- Personas will be real mana-auth users (users.kind='persona'), no
service-key bypass (D1, D2)
- Tool-registry is the SSOT; mana-ai and the legacy
apps/api/src/mcp/server.ts get merged into it in M4 (three current
parallel tool catalogs collapse to one)
- Persona-runner (:3070) will be a separate service using the Claude
Agent SDK + MCP client (D5)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f719d1768f
commit
16c8818338
31 changed files with 2958 additions and 360 deletions
|
|
@ -27,7 +27,9 @@
|
|||
> - mana-sync `3050`
|
||||
> - mana-credits `3061`, mana-user `3062`, mana-subscriptions `3063`,
|
||||
> mana-analytics `3064`, mana-events `3065`, mana-research `3068`
|
||||
> (new 2026-04-17, Bun/Hono, public: `research.mana.how`)
|
||||
> (new 2026-04-17, Bun/Hono, public: `research.mana.how`),
|
||||
> mana-mcp `3069` (new 2026-04-22, Bun/Hono, MCP gateway over
|
||||
> Streamable HTTP — see `services/mana-mcp/CLAUDE.md`)
|
||||
>
|
||||
> **Not deployed:** `mana-voice-bot` (default port `3024`, no scheduled
|
||||
> task, no cloudflared route, no launchd plist).
|
||||
|
|
|
|||
474
docs/plans/mana-mcp-and-personas.md
Normal file
474
docs/plans/mana-mcp-and-personas.md
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
# Mana MCP + Personas + Visual Suite
|
||||
|
||||
_Started 2026-04-22._
|
||||
|
||||
Autonome Nutzung und Test von Mana durch Claude (und andere Agents) über ein einziges, sauberes Protokoll — plus eine Persona-Simulation, die Mana dauerhaft mit realistischem Content bespielt, und eine visuelle Regression-Suite, die das Ergebnis tatsächlich anschaut.
|
||||
|
||||
Voraussetzung: **nicht live, unbegrenzte Ressourcen, keine Migrations-Kompromisse.** Wir bauen die Endzustands-Architektur direkt, ohne Legacy-Reste.
|
||||
|
||||
## Ziel in einem Satz
|
||||
|
||||
Jeder Agent (Claude Desktop, Claude Code, ein interner Tick-Loop, ein MCP-fähiger Voice-Client) spricht **ein** Protokoll mit Mana — und dasselbe Protokoll wird von simulierten Personas genutzt, deren akkumulierender Content nachts durch eine Playwright-Suite visuell geprüft wird.
|
||||
|
||||
## Nicht-Ziele
|
||||
|
||||
- **Kein** Ersatz der Svelte-UI durch Agent-Flows. Die UI bleibt primäres Frontend für Menschen.
|
||||
- **Keine** MCP-Exposition von Admin- oder Auth-Endpoints. Personas registrieren sich nicht selbst.
|
||||
- **Keine** parallele Tool-Registry. mana-ai und mana-mcp konsumieren dasselbe Paket (siehe Entscheidung D3).
|
||||
- **Keine** Persona-Daten in Produktion-Spaces. Personas sind eigene User mit klarer Kennzeichnung, ihre Spaces sind isoliert.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────┐
|
||||
│ Claude Desktop / Claude Code / persona-runner │
|
||||
└───────────────┬────────────────────────────────┘
|
||||
│ MCP (Streamable HTTP + JWT)
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ services/mana-mcp │ :3069
|
||||
│ (Hono/Bun) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
mana-auth mana-api mana-sync
|
||||
(JWT/JWKS) (Module-REST) (SSE, RLS)
|
||||
│
|
||||
▼
|
||||
PostgreSQL
|
||||
(mana_platform, mana_sync)
|
||||
|
||||
|
||||
┌─────────────────────────────┐
|
||||
│ services/mana-persona-runner│ :3070
|
||||
│ Claude Agent SDK + MCP │
|
||||
│ Tick-Loop (daily per user) │
|
||||
└──────────┬──────────────────┘
|
||||
│ writes: persona_actions, persona_feedback
|
||||
▼
|
||||
Postgres
|
||||
|
||||
┌─────────────────────────────┐
|
||||
│ tests/personas/*.spec.ts │
|
||||
│ Playwright, nightly │
|
||||
│ Login als Persona → Tour │
|
||||
│ toHaveScreenshot Baselines │
|
||||
└──────────┬──────────────────┘
|
||||
▼
|
||||
Visual Diff Report
|
||||
```
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
Explizit und mit Begründung, damit wir bei späteren Zweifeln den Pfad kennen.
|
||||
|
||||
### D1 — Personas sind echte Mana-User
|
||||
|
||||
Kein Bypass, kein Service-Key-Trick. Jede Persona macht ein ganz normales `POST /api/v1/auth/register`, bekommt ein reguläres JWT, landet mit RLS-geschütztem Zugriff auf ihre `personal` Space.
|
||||
|
||||
**Warum:** Jeder andere Weg erzeugt zwei Code-Pfade — den normalen und den Persona-Pfad. Zwei Pfade bedeuten: Tests decken nur den Persona-Pfad ab, Bugs im echten Pfad bleiben unsichtbar. Wir wollen, dass Personas exakt dasselbe durchlaufen wie echte Nutzer.
|
||||
|
||||
**Abgrenzung zu echten Usern:** Neue Spalte `users.kind` mit Enum `'human' | 'persona' | 'system'`. Defaults auf `'human'`. Admin-UIs und Metriken filtern standardmäßig auf `kind = 'human'`. Email-Namespace `persona.<name>@mana.test` (nicht-existierende TLD, visuell sofort erkennbar).
|
||||
|
||||
### D2 — Auth via JWT-per-Agent, kein Service-Key
|
||||
|
||||
Der MCP-Server akzeptiert ausschließlich gültige mana-auth JWTs. Claude hält pro Persona ein Token (oder Refresh-Token) und authentifiziert sich wie ein Mensch.
|
||||
|
||||
**Warum:** Service-Key + `X-User-Id` (wie heute mana-ai → mana-research) bypassed RLS-Logik und erlaubt jedem Request, sich als jeden User auszugeben. Für interne service-to-service ok. Für einen offenen MCP-Endpoint inakzeptabel.
|
||||
|
||||
**Konsequenz:** MCP-Server ist JWKS-validiert über mana-auth. Standard `authMiddleware()` aus `@mana/shared-hono`. Jeder Tool-Call erbt User-Context aus Token, forwarded das an mana-api, mana-sync.
|
||||
|
||||
### D3 — Tool-Registry ist ein neues Shared Package
|
||||
|
||||
`packages/mana-tool-registry/` wird der einzige Ort, an dem Tools definiert werden. mana-ai (Mission-Runner) und mana-mcp (MCP-Server) konsumieren denselben Registry. Jedes Modul (`todo`, `journal`, `calendar`, …) liefert einen Tool-Spec (Zod-Schema + Description + Implementation-Stub) und registriert ihn.
|
||||
|
||||
**Warum:** mana-ai hat heute 67 Tools über 21 Module, großteils hartcodiert. Duplizieren im MCP-Server wäre der sofortige Rutsch in Legacy. Einmal zentral, beide konsumieren.
|
||||
|
||||
**Was das enthält:**
|
||||
- Zod-Schema pro Tool-Input/Output (generiert Json-Schema für MCP + TS-Types für mana-ai)
|
||||
- `scope`: `'per-user-space'` vs. `'global-admin'` — MCP exponiert nur ersteres
|
||||
- `policy-hint`: `'read' | 'write' | 'destructive'` — für mana-ai Policy-Layer UND MCP Consent-Flows
|
||||
- `implementation`: Funktion `(input, ctx) => result`, wobei `ctx` User+Space+JWT enthält
|
||||
|
||||
**Reihenfolge:** Wir ziehen mana-ai während M4 auf die neue Registry um. Nicht vorher. Sonst blockiert der Mission-Runner den MCP-MVP.
|
||||
|
||||
### D4 — MCP-Transport: Streamable HTTP
|
||||
|
||||
Nicht stdio. Der MCP-Server läuft als echter HTTP-Service, Multi-Client-fähig, durch dasselbe Cloudflare-Tunnel/nginx-Setup wie andere Services erreichbar.
|
||||
|
||||
**Warum:** stdio erzwingt 1:1 Child-Process-Modell. Ungeeignet für einen gemeinsamen Server, den Claude Desktop, der Persona-Runner und ad-hoc Agents gleichzeitig nutzen.
|
||||
|
||||
### D5 — Persona-Runner als eigener, minimaler Service
|
||||
|
||||
`services/mana-persona-runner/` (Bun/Hono, :3070). Uses `@anthropic-ai/claude-agent-sdk` direkt, konfiguriert mit dem MCP-Endpoint, iteriert pro Persona einmal pro Tag (configurable).
|
||||
|
||||
**Warum nicht in mana-ai mit reinpacken:** mana-ai ist für _unsere Nutzer_ und deren Missionen. Persona-Runner ist Test-Infrastruktur. Unterschiedliche Lifecycle, unterschiedliche Observability, unterschiedliche Risiko-Profile. Vermischt = später schwer zu trennen.
|
||||
|
||||
**Warum nicht Claude Code als Daemon:** Claude Code ist eine interaktive CLI. `claude -p` geht, aber Setup ist fragiler als 200 Zeilen Agent-SDK-Code mit klarem Tick-Loop.
|
||||
|
||||
### D6 — Persona-Metadata als eigene Tabelle
|
||||
|
||||
`platform.personas` in mana_platform:
|
||||
- `userId` (FK, PK zu `users.id`, 1:1)
|
||||
- `archetype` (z.B. `'adhd-student'`, `'ceo-busy'`, `'creative-parent'`)
|
||||
- `systemPrompt` (Text, das was die Persona charakterisiert)
|
||||
- `moduleMix` (jsonb, Gewichtungen — welche Module wie oft)
|
||||
- `tickCadence` (`'daily'` | `'weekdays'` | `'hourly'`)
|
||||
- `createdAt`, `lastActiveAt`
|
||||
|
||||
Nicht auf `users` geklatscht, damit die User-Tabelle pur bleibt — echte User und Personas teilen nur Auth/Identity, nicht ihre Charakterisierung.
|
||||
|
||||
### D7 — Spaces: Personas nutzen auto-erstellte `personal` Space plus Cross-Space-Testing
|
||||
|
||||
Bei Persona-Registrierung wird durch den normalen Signup-Flow bereits eine `personal` Space angelegt (Spaces-Foundation). Zusätzlich definieren wir im Persona-Katalog **Space-Rollen**: z.B. `Anna + Ben` sind Member einer `family` Space, `Marcus + Lena` einer `team` Space. Das testet Shared-Space-Mechanik ohne Extra-Framework.
|
||||
|
||||
**Warum:** Wenn alle Personas nur Personal-Spaces haben, testen wir Shared-Spaces nie. Die Kopplung im Persona-Katalog ist billig und deckt das ab.
|
||||
|
||||
### D8 — Visual Regression: Playwright built-in
|
||||
|
||||
`toHaveScreenshot()` mit Baselines in `tests/personas/__snapshots__/`. Konfiguration: 3 Viewports (Desktop 1440×900, iPad 768×1024, iPhone 390×844), threshold konservativ (0.2% pixel diff).
|
||||
|
||||
**Kein Chromatic, kein Percy.** Die Baselines werden ins Repo committed (nicht zu groß bei 3 Viewports × ~20 Screens × ~10 Personas = ~600 PNGs, overlap hoch durch WebP-Komprimierung).
|
||||
|
||||
**Warum:** Dritt-Services bedeuten Kosten, Accounts, Integration. Wir sind nicht live, brauchen das nicht. Wenn Baseline-Drift zum Problem wird, können wir später zu Chromatic wechseln.
|
||||
|
||||
### D9 — Ratings als strukturiertes Feedback
|
||||
|
||||
Jeder Persona-Tick endet mit einem Rating-Schritt: Claude generiert (als Persona) für jedes genutzte Modul eine 1–5 Bewertung plus Freitext-Notiz. Landet in `platform.persona_feedback`.
|
||||
|
||||
**Warum:** Der ganze Sinn von "Persona spielt Produkt" ist die qualitative Beobachtung. Ohne strukturierte Ausgabe erzeugen wir Logs niemand liest. Mit Ratings kannst du dir Montagfrüh ein Dashboard anschauen.
|
||||
|
||||
### D10 — Keine Exposition von destruktiven Admin-Tools via MCP
|
||||
|
||||
Tool-Registry markiert Operationen mit `policy-hint`. MCP exponiert nur `read` und `write`. `destructive` (User-Löschung, Space-Löschung, Tier-Änderung) bleibt interner Admin-API, erreichbar nur über Admin-UI mit echter 2FA-Sitzung.
|
||||
|
||||
**Warum:** Wenn wir jemals den MCP-Server extern zugänglich machen (Claude Desktop mit OAuth), soll kompromittiertes Token nicht bedeuten "User ist weg".
|
||||
|
||||
## Komponenten
|
||||
|
||||
### Komponente 1 — `packages/mana-tool-registry`
|
||||
|
||||
Neues Workspace-Paket. Zuerst gebaut, weil alles andere darauf aufbaut.
|
||||
|
||||
**Public API (Skizze):**
|
||||
```ts
|
||||
export interface ToolSpec<Input, Output> {
|
||||
name: string; // z.B. 'todo.create'
|
||||
description: string; // Für LLM/Doku
|
||||
module: ModuleId; // 'todo', 'journal', …
|
||||
scope: 'user-space' | 'admin';
|
||||
policyHint: 'read' | 'write' | 'destructive';
|
||||
input: ZodSchema<Input>;
|
||||
output: ZodSchema<Output>;
|
||||
handler: (input: Input, ctx: ToolContext) => Promise<Output>;
|
||||
}
|
||||
|
||||
export interface ToolContext {
|
||||
userId: string;
|
||||
spaceId: string;
|
||||
jwt: string;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export function registerTool(spec: ToolSpec<any, any>): void;
|
||||
export function getRegistry(): ToolSpec<any, any>[];
|
||||
export function getToolsByModule(module: ModuleId): ToolSpec<any, any>[];
|
||||
```
|
||||
|
||||
Implementation der Handler ruft `mana-api` und `mana-sync` HTTP-Endpoints (mit `ctx.jwt`). Das hält die Registry dünn und zwingt korrekte RLS-Durchgänge. Direkte DB-Zugriffe wären schneller, aber würden RLS umgehen — genau das wollen wir nicht.
|
||||
|
||||
**Module-Coverage M1–M4:**
|
||||
- **M1**: todo, journal, notes, calendar, contacts (5 Module, ~20 Tools)
|
||||
- **M4 expand**: articles, picture, cards, missions, tags, spaces, goals, mood, dreams, library (10 weitere, ~45 Tools)
|
||||
- **Nicht in scope initial**: voice-bot, video-gen, image-gen (die haben eigene Async-Flows, eigene Phase)
|
||||
|
||||
### Komponente 2 — `services/mana-mcp`
|
||||
|
||||
Hono/Bun, port 3069. Nutzt MCP TypeScript SDK (`@modelcontextprotocol/sdk`). Streamable HTTP Transport.
|
||||
|
||||
**Server-Struktur:**
|
||||
```
|
||||
services/mana-mcp/
|
||||
├── src/
|
||||
│ ├── index.ts # Server bootstrap
|
||||
│ ├── mcp-adapter.ts # tool-registry → MCP tool definitions
|
||||
│ ├── auth-middleware.ts # JWKS verify
|
||||
│ └── transport.ts # Streamable HTTP
|
||||
├── package.json
|
||||
└── CLAUDE.md
|
||||
```
|
||||
|
||||
Adapter-Logik: Für jeden `ToolSpec` aus `mana-tool-registry`, generiere eine MCP-Tool-Definition. Input-Schema aus `zod-to-json-schema`. Bei Invoke: User-Context aus JWT ziehen, `ctx` aufbauen, `handler` aufrufen, Output streamen.
|
||||
|
||||
**Auth-Flow:**
|
||||
1. Client öffnet MCP-Session mit `Authorization: Bearer <jwt>`.
|
||||
2. Middleware verifiziert gegen JWKS von mana-auth.
|
||||
3. User-ID und aktive Space-ID aus Token-Claims.
|
||||
4. Active Space wird aus `X-Mana-Space` Header überschrieben werden können (wichtig für Cross-Space-Tests).
|
||||
|
||||
**Logging:** Pro Tool-Call ein structured log mit `{persona, toolname, inputHash, latencyMs, result: 'ok'|'error'}`. Landet in `platform.persona_actions` falls `users.kind = 'persona'`, sonst nur stdout (echte User).
|
||||
|
||||
### Komponente 3 — `services/mana-persona-runner`
|
||||
|
||||
Hono/Bun, port 3070. Tick-Loop.
|
||||
|
||||
**Loop pro Tick (pro Persona):**
|
||||
|
||||
```
|
||||
1. Lade persona record + systemPrompt + tickCadence + recent actions
|
||||
2. Prüfe ob persona fällig ist (basierend auf tickCadence + lastActiveAt)
|
||||
3. Falls ja:
|
||||
a. Hole JWT für persona (via stored refresh token oder re-login)
|
||||
b. Starte Claude Agent SDK session mit:
|
||||
- system: persona.systemPrompt + "Heute ist {date}, du hast Zugriff
|
||||
auf deine persönliche Mana-App. Was würdest du heute hier tun?"
|
||||
- mcp: localhost:3069 mit persona JWT
|
||||
- max_turns: 15
|
||||
c. Claude ruft MCP-Tools auf, erzeugt Content in Mana
|
||||
d. Abschließend Rating-Prompt: "Bewerte jedes Modul, das du heute
|
||||
genutzt hast, 1-5 mit einer Begründung."
|
||||
e. Parse Rating, schreibe zu persona_feedback
|
||||
4. Update persona.lastActiveAt
|
||||
```
|
||||
|
||||
**Concurrency:** Default 2 parallele Personas (Claude API rate limits schonen). Konfigurierbar über `PERSONA_CONCURRENCY` env.
|
||||
|
||||
**Scheduling:** Interner `setInterval` + `tickCadence`-Check. Kein externes Cron — Service muss eh durchlaufen, Metriken zu haben. Health-Endpoint `/health`, Prometheus-Metriken `/metrics`.
|
||||
|
||||
**Fehlertoleranz:** Timeout pro Persona-Tick 10 Minuten. Bei Fehler: Eintrag in `persona_actions` mit `error` Field, nächster Tick fährt weiter.
|
||||
|
||||
### Komponente 4 — Persona-Katalog
|
||||
|
||||
10 Personas, handgeschrieben, committed als JSON in `scripts/personas/catalog.json`.
|
||||
|
||||
**Archetype-Verteilung (Entwurf):**
|
||||
|
||||
| Name | Archetype | Modul-Mix | Space-Setup |
|
||||
|------|-----------|-----------|-------------|
|
||||
| Anna | adhd-student | todo, journal, notes, mood | personal only |
|
||||
| Ben | adhd-student | todo, calendar, todos, journal | personal + family(mit Anna) |
|
||||
| Marcus | ceo-busy | contacts, calendar, tasks, articles | personal + team(mit Lena) |
|
||||
| Lena | ceo-busy | contacts, meetings, journal | personal + team(mit Marcus) |
|
||||
| Sofia | creative-parent | journal, picture, notes, dreams | personal + family(mit Tom) |
|
||||
| Tom | creative-parent | cards, calendar, todos | personal + family(mit Sofia) |
|
||||
| Kai | solo-dev | articles, notes, library, goals | personal + practice |
|
||||
| Julia | researcher | articles, news-research, notes | personal only |
|
||||
| Paul | freelancer | invoices, calendar, contacts | personal + brand |
|
||||
| Maya | overwhelmed-newbie | nur todo+journal, macht viele Fehler | personal only |
|
||||
|
||||
Die "overwhelmed-newbie" Persona ist bewusst dabei, um Onboarding-Bugs und Confusing-UX zu finden. Ihr System-Prompt erlaubt Claude, "verwirrt" zu sein und suboptimale Dinge zu tun.
|
||||
|
||||
### Komponente 5 — `tests/personas/`
|
||||
|
||||
Playwright-Suite. Läuft nachts gegen Staging (oder lokal gegen dev).
|
||||
|
||||
**Struktur:**
|
||||
```
|
||||
tests/personas/
|
||||
├── fixtures/
|
||||
│ └── persona-auth.ts # Login-Helper, produziert Auth-State
|
||||
├── flows/
|
||||
│ ├── home-tour.spec.ts # Alle Personas: Home + Navigation
|
||||
│ ├── todo.spec.ts # Todo-Personas: Liste + Detail
|
||||
│ ├── journal.spec.ts # Journal-Personas: Schreiben + Archive
|
||||
│ └── ...
|
||||
└── __snapshots__/
|
||||
└── <test>-<persona>-<viewport>.png
|
||||
```
|
||||
|
||||
**Jeder Test-Lauf:**
|
||||
1. Hole Persona-Liste aus `persona_catalog` (DB, gefüllt durch seed).
|
||||
2. Für jede Persona × Flow × Viewport:
|
||||
- Login via API (bypass Browser-Login, wir testen nicht Login hier)
|
||||
- Storage-State setzen
|
||||
- Flow ausführen
|
||||
- Screenshot nehmen, vergleichen
|
||||
3. Bei Diff: report generieren, bei `--update-snapshots` neue Baseline.
|
||||
|
||||
**CI-Integration:** GitHub Action nightly, oder Mac-Mini-Cron. Diff-HTML-Report in MinIO uploaded, Link in Slack gepostet.
|
||||
|
||||
## Datenmodell-Erweiterungen
|
||||
|
||||
Drei neue Tabellen in `platform.*`:
|
||||
|
||||
```sql
|
||||
-- 1. Persona-Metadaten (1:1 mit users.kind='persona')
|
||||
CREATE TABLE platform.personas (
|
||||
user_id UUID PRIMARY KEY REFERENCES platform.users(id) ON DELETE CASCADE,
|
||||
archetype TEXT NOT NULL,
|
||||
system_prompt TEXT NOT NULL,
|
||||
module_mix JSONB NOT NULL,
|
||||
tick_cadence TEXT NOT NULL DEFAULT 'daily',
|
||||
last_active_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 2. Was Personas tun (audit trail)
|
||||
CREATE TABLE platform.persona_actions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
persona_id UUID NOT NULL REFERENCES platform.personas(user_id),
|
||||
tick_id UUID NOT NULL, -- zusammenhängender Tick
|
||||
tool_name TEXT NOT NULL,
|
||||
input_hash TEXT, -- für dedup analytics
|
||||
result TEXT NOT NULL, -- 'ok' | 'error'
|
||||
error_message TEXT,
|
||||
latency_ms INT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX ON platform.persona_actions (persona_id, created_at DESC);
|
||||
|
||||
-- 3. Persona-Feedback pro Tick
|
||||
CREATE TABLE platform.persona_feedback (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
persona_id UUID NOT NULL REFERENCES platform.personas(user_id),
|
||||
tick_id UUID NOT NULL,
|
||||
module TEXT NOT NULL,
|
||||
rating INT NOT NULL CHECK (rating BETWEEN 1 AND 5),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX ON platform.persona_feedback (module, created_at DESC);
|
||||
```
|
||||
|
||||
Plus **ein** Feld auf existierender `platform.users`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE platform.users
|
||||
ADD COLUMN kind TEXT NOT NULL DEFAULT 'human'
|
||||
CHECK (kind IN ('human', 'persona', 'system'));
|
||||
```
|
||||
|
||||
## No-Legacy-Residues
|
||||
|
||||
Explizite Anti-Patterns, gegen die wir uns committen:
|
||||
|
||||
1. **Kein `isBot` Boolean.** `users.kind` Enum mit klaren Werten. Boolean-Flags wachsen immer ungeklärt.
|
||||
2. **Keine parallele Tool-Definition.** Wenn mana-ai und mana-mcp je eigene Tool-Listen hätten, bekommen wir Drift. D3 zwingt die gemeinsame Registry.
|
||||
3. **Kein Service-Key-Shortcut.** Auch nicht "nur für Personas, weil's einfacher wäre". D1+D2 machen klar: Personas gehen den Nutzer-Pfad.
|
||||
4. **Kein versteckter Persona-Endpoint.** Alles was MCP kann, könnte auch ein echter Nutzer. Wenn ein Tool "nur für Personas" wäre, ist es Admin-Tool und gehört nicht in MCP.
|
||||
5. **Keine Placeholder-Daten direkt in DB.** Persona-Content wird ausschließlich via MCP/REST erzeugt, durchläuft RLS, Encryption, Validation. Wenn wir DB-direkt seeden würden, testen wir nicht was wir testen wollen.
|
||||
6. **Keine `if (user.email.endsWith('mana.test'))` Special-Cases.** Wenn Logik nach Persona verzweigen muss, dann über `users.kind`, und Grund muss in Commit dokumentiert sein.
|
||||
7. **Keine Screenshot-Baselines ohne Review.** `--update-snapshots` ist kein Routine-Run. Jede Baseline-Änderung erzeugt einen Diff, und der wird menschlich angeschaut.
|
||||
|
||||
## Milestones
|
||||
|
||||
Jeder Milestone landet als klar erkennbares Commit-Set, ist standalone nützlich, typechecked + validate:all grün.
|
||||
|
||||
### M1 — Foundation (Tool-Registry + MCP-MVP, plaintext only)
|
||||
|
||||
**Scope-Refinement (gegenüber initialer Plan-Skizze):** Die ursprünglich gelisteten Tools (`todo.create`, `journal.add`, `notes.create`) zielen alle auf **encrypted** Tabellen. Server-side Encryption braucht eine MK-Unwrap-Infrastruktur, die noch nicht existiert. Statt M1 zu vergrößern, hält M1 sich strikt an plaintext-Tabellen — die MCP-Plumbing wird unabhängig von Encryption sauber bewiesen. Encryption-Path wird **M1.5**.
|
||||
|
||||
- [ ] `packages/mana-tool-registry/` scaffold: types, register/get, context type
|
||||
- [ ] 5 Tools implementieren — alle plaintext: `habits.create`, `habits.list`, `habits.log`, `habits.recent_logs`, `spaces.list`
|
||||
- [ ] `services/mana-mcp/` scaffold + Hono server + JWKS auth middleware
|
||||
- [ ] MCP-Adapter: Registry → MCP tool definitions
|
||||
- [ ] Streamable HTTP transport
|
||||
- [ ] Integration-Test: lokaler Claude Code verbindet sich, listet Tools, ruft `habits.create` auf, Row erscheint in `mana_sync.sync_changes`
|
||||
- [ ] Port 3069 in `docs/PORT_SCHEMA.md`
|
||||
- [ ] `services/mana-mcp/CLAUDE.md` mit Architektur + Ports
|
||||
|
||||
**Exit criteria:** Claude Code kann mit einem dev-user-JWT gegen lokalen MCP-Server Habits anlegen, loggen und listen. Persona-Browser-Login (M5) zeigt diese Habits später visuell.
|
||||
|
||||
### M1.5 — Server-side encryption capability — ✅ SHIPPED 2026-04-22
|
||||
|
||||
**Scope-Update gegenüber initialer Skizze:** Der geplante neue Service-Key-gated Endpoint `POST /api/v1/internal/users/:id/mk` wurde **nicht gebaut** — nicht nötig. Der existierende Endpoint `GET /api/v1/me/encryption-vault/key` (JWT-gated, ZK-aware, bereits mit Audit-Trail) erfüllt genau den Zweck: Agent hält das JWT seiner Persona → fragt die Vault-API → bekommt MK für Non-ZK-User, recovery-blob für ZK (wir rejecten das mit `ZeroKnowledgeUserError`). Vorteil: zero neue Angriffsfläche, zero neuer Admin-Pfad.
|
||||
|
||||
- [x] `packages/shared-crypto/` extracted — `aes.ts` zieht um, Web-App `$lib/data/crypto/aes.ts` re-exportiert. Identische Wire-Format-Garantie (same code).
|
||||
- [x] `@mana/tool-registry.MasterKeyClient` — cached Vault-Fetch per userId, 5-min TTL
|
||||
- [x] `ToolContext.getMasterKey()` lazy — tools that need crypto call it, plaintext tools never do
|
||||
- [x] `ToolSpec.encryptedFields?` declarative — `{table, fields}` pro tool, matches web-app registry verbatim (M4 audit script wird das cross-checken)
|
||||
- [x] 5 neue encrypted tools: `todo.create`, `todo.list`, `todo.complete`, `notes.create`, `notes.search`, `journal.add` (ended up being 6 — `todo.complete` landed gratis, reines plaintext-update)
|
||||
- [x] mana-mcp adapter baut `getMasterKey` in den ToolContext beim Session-Start ein, ein `MasterKeyClient` pro Prozess, cache teilen
|
||||
|
||||
**Exit criteria — erfüllt:** Encrypted Module sind über MCP für Non-ZK-User erreichbar. Type-check über alle 3 Pakete grün. Smoke-Boot: Service registriert 9 Tools total (4 habits + 1 spaces + 3 todo + 2 notes + 1 journal = 11, check-me), unauthed bleibt 401.
|
||||
|
||||
**Nicht gemacht (bewusst, M4 gehört das hin):**
|
||||
- Audit-Script der `encryptedFields` vs. Web-App-Registry cross-checkt
|
||||
- Einheitliche Registry-Daten zwischen Web-App und shared-crypto (heute: Web-App hält seine typed `entry<T>()` Version, tools halten ihre eigene Feldliste pro Spec — CI-Audit muss später drift fangen)
|
||||
|
||||
### M2 — Persona-Primitives
|
||||
|
||||
- [ ] Drizzle-Schema: `users.kind`, `platform.personas`, `platform.persona_actions`, `platform.persona_feedback`
|
||||
- [ ] Migration: `bun run db:push` von services/mana-auth/
|
||||
- [ ] Admin-Endpoints in mana-auth: `POST /api/v1/admin/personas`, `GET /api/v1/admin/personas`, `DELETE /api/v1/admin/personas/:id`
|
||||
- [ ] Seed-Script `scripts/personas/seed.ts`: lese `catalog.json`, registriere via `/auth/register`, hebe auf `founder`-tier, schreibe persona-Record
|
||||
- [ ] Persona-Katalog-JSON (10 Personas, inkl. Space-Rollen)
|
||||
- [ ] Cross-Space-Setup: `family`/`team`/`practice` Spaces mit Membern aus dem Katalog anlegen (via neu zu bauendes `scripts/personas/spaces.ts`)
|
||||
|
||||
**Exit criteria:** `pnpm seed:personas` erzeugt 10 User, 10 `personal`-Spaces, 3 shared Spaces, befüllt `platform.personas`.
|
||||
|
||||
### M3 — Persona-Runner
|
||||
|
||||
- [ ] `services/mana-persona-runner/` scaffold
|
||||
- [ ] Tick-Loop: liest Personas aus DB, Cadence-Check, pro fällige Persona → Claude Agent SDK Aufruf
|
||||
- [ ] JWT-Handling: Refresh-Token-Storage in `platform.personas` (encrypted field), auto-refresh
|
||||
- [ ] Concurrency-Control (default 2)
|
||||
- [ ] `persona_actions` + `persona_feedback` Writes
|
||||
- [ ] Prometheus metrics: `persona_ticks_total`, `persona_tool_calls_total`, `persona_errors_total`
|
||||
- [ ] Observability-Dashboard (Markdown in `docs/observability/personas.md`, evtl. später Grafana)
|
||||
|
||||
**Exit criteria:** Service läuft lokal, nach 24h haben alle 10 Personas mindestens einen Tick ausgeführt, in der App ist sichtbarer Content pro Persona.
|
||||
|
||||
### M4 — Full Tool Coverage + Three-Catalog Unification
|
||||
|
||||
**Aufgefundener Stand (2026-04-22):** Es existieren bereits drei Tool-Kataloge im Repo, die alle gemerged werden:
|
||||
|
||||
1. `packages/shared-ai/AI_TOOL_CATALOG` — konsumiert von `apps/api/src/mcp/server.ts`
|
||||
2. `services/mana-ai/` interne Tool-Definitionen (~67 Tools, 21 Module) — Mission-Runner
|
||||
3. `packages/mana-tool-registry/` — neu in M1, wird der SSOT
|
||||
|
||||
- [ ] Tool-Registry auf 15+ Module ausbauen (siehe Komponente 1)
|
||||
- [ ] mana-ai migrieren: alle heute hartcodierten Tools ziehen in `mana-tool-registry`, mana-ai konsumiert via `getRegistry()`
|
||||
- [ ] `packages/shared-ai/AI_TOOL_CATALOG` löschen, `apps/api/src/mcp/server.ts` löschen (mana-mcp übernimmt die Funktion)
|
||||
- [ ] Alte Tool-Definitionen in mana-ai löschen (harter Cut, keine Parallelität)
|
||||
- [ ] End-to-end-Test: sowohl eine mana-ai-Mission als auch ein Persona-Runner-Tick nutzen dieselbe `todo.create`
|
||||
|
||||
**Exit criteria:** Grep nach duplizierten Tool-Definitionen → leer. Beide Consumer grün.
|
||||
|
||||
### M5 — Visual Suite
|
||||
|
||||
- [ ] `tests/personas/` Struktur
|
||||
- [ ] Persona-Login-Fixture (API-Login → storageState)
|
||||
- [ ] Flow-Specs: `home-tour`, `todo`, `journal`, `notes`, `calendar`, `articles`, `contacts`, `cards`, `missions`, `settings` (10 Flows)
|
||||
- [ ] 3 Viewports: Desktop, iPad, iPhone
|
||||
- [ ] Baseline-Capture pro Persona × Flow × Viewport
|
||||
- [ ] Playwright-Config: threshold 0.2%, animation-handling, font-loading wait
|
||||
- [ ] Nightly-Run: GitHub Action oder Mac-Mini-Cron, Report zu MinIO, Slack-Notify bei Diff
|
||||
- [ ] `docs/testing/visual-regression.md`
|
||||
|
||||
**Exit criteria:** Nightly läuft, baseline committed, bei Code-Change sehen wir Diff-Report.
|
||||
|
||||
### M6 — Polish (optional, nach M1–M5)
|
||||
|
||||
- [ ] Admin-UI-Tab "Personas" in der Web-App: Liste, letzte Actions, Feedback-Dashboard pro Modul
|
||||
- [ ] Rating-Aggregations-View: `"Modul X bekommt seit 3 Wochen schlechtere Ratings als vorher"`
|
||||
- [ ] MCP-OAuth-Flow für externe Clients (wenn wir das je öffnen)
|
||||
- [ ] Chaos-Personas: gezielt fehlerhafte Inputs, Edge-Cases, Unicode-Chaos
|
||||
|
||||
## Risiken + Mitigation
|
||||
|
||||
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|
||||
|--------|-----|-----|------------|
|
||||
| Claude API Rate-Limits bei 10 Personas × täglich × 15 Turns | Mittel | Mittel | Concurrency-Config (default 2), Retry-with-backoff, Tier-Upgrade falls nötig |
|
||||
| Persona-Daten "leaken" in Produktion-Dashboards | Niedrig | Hoch | `users.kind` Filter standardmäßig in allen Admin-Queries, Review-Checklist bei neuen Dashboards |
|
||||
| Tool-Registry wird zu groß / unübersichtlich | Mittel | Niedrig | Pro Modul eine Datei, Index nur re-exportiert; CI-Check max Tools-pro-Modul |
|
||||
| Visual Baselines drift durch zufällige Daten (z.B. Zeitanzeige, IDs) | Hoch | Mittel | Deterministic persona seed (fixed dates), `data-testid` statt visueller Hashes, frozen clock in Playwright |
|
||||
| MCP-SDK API ändert sich | Niedrig | Mittel | Dünner Adapter-Layer in `mcp-adapter.ts`, Pin der SDK-Version |
|
||||
| Persona-Refresh-Token läuft ab | Hoch (über Wochen) | Niedrig | Auto-refresh in Runner, Alert wenn Refresh fehlschlägt |
|
||||
|
||||
## Offene Entscheidungen (später)
|
||||
|
||||
- **Tool-Versioning:** Wenn wir `todo.create` ändern, Breaking für MCP-Clients? Wir sind noch nicht live, brauchen das noch nicht. Nach M4 einmal reviewen.
|
||||
- **MCP für externe Nutzer:** Wenn/wann jemals Mana als Tool für Claude Desktop released werden soll → OAuth-Flow statt JWT-Bearer. Phase M6+.
|
||||
- **Persona-Content-Cleanup:** Nach 90 Tagen werden Persona-Spaces massiv, DB wächst. Brauchen wir `persona.maxHistoryDays`? Beobachten, M3 sammelt erst mal Daten.
|
||||
- **Langzeit-Konsistenz:** Wenn Anna 30 Tage lang Todos anlegt, wird Claude sich an eigene frühere Einträge erinnern können (durch MCP-List-Tools). Wir müssen prüfen, ob der System-Prompt + letzte N Actions genug ist oder ob wir eine explizite "Anna's story so far"-Zusammenfassung pflegen.
|
||||
|
||||
## Shipping Log
|
||||
|
||||
(Leer — wird befüllt, während M1 → M6 gehen.)
|
||||
|
||||
| Phase | Purpose | Commit |
|
||||
| --- | --- | --- |
|
||||
| — | — | — |
|
||||
Loading…
Add table
Add a link
Reference in a new issue