Sprint 2a Spec — m365 v0.3 + vf-vault
Spec fuer den ersten Bau-Schritt aus der VF-Vault-Architektur. Marvin reviewed dieses Spec, dann Code in den Repos.
Ziel
Christoph’s SharePoint-Vault /Workshop/VibeFactory/ ist von Claude in Open WebUI lesbar unter eigener User-Identitaet. Christoph (christoph@vibe-factory.de) loggt in Open WebUI ein, autorisiert m365 einmalig via OAuth, ab dann kann Claude in seinem Namen auf SharePoint zugreifen. Token persistent in DynamoDB.
Scope dieses Sprints: nur Read. Write-Tools (m365_send_email, m365_upload_drive_file etc.) sind Sprint 2a Teil 2 — separat.
Ausserhalb dieses Sprints: Andre/Felix/Julian onboarden (Phase 2 der Roadmap), Skills, Cron-Jobs.
Stand heute (mcp-m365 v0.2)
| Aspekt | v0.2 |
|---|---|
| Auth | azure.identity.ClientSecretCredential (App-only / Service-Principal) |
| Token-Lifecycle | Global Token-Cache in graph.py, ein Token fuer den ganzen Prozess |
| Permissions in Azure | Sites.Selected (whitelist) + Mail.ReadWrite + Mail.Send |
| Tools | list_sites, list_drive_items, search_files, get_drive_item, download_file + Excel + Lists + Mail |
| Per-User-State | nein — alle Calls unter App-Identitaet |
| Audit-Trail | nicht User-aufgeloest — mcp-m365 Service-Principal als Actor |
Problem fuer Open WebUI Multi-User: alle vier VF-User wuerden unter derselben App-Identitaet schreiben. Mail-Send waere Mail-FROM=App. SharePoint-Versions-History waere alles vom Service-Principal. Wegen DSGVO + Audit-Trail wollen wir Per-User-Identitaet.
Architektur-Vorschlag v0.3
Open WebUI Chat
│ User: christoph@vibe-factory.de
│ Header: X-OpenWebUI-User-Email = christoph@vibe-factory.de
▼
mcp-vf-hosted (Mono)
│ Scalekit-JWT, audit-middleware extrahiert sub=christoph@vibe-factory.de
│ FastMCP-Context propagiert User-Email an Sub-MCPs
▼
vf-vault Sub-MCP (NEU) m365 Sub-MCP (v0.3 delegated) papierkram/ticketpay (unveraendert)
│ │
│ ruft m365-Sub │ Mode = "delegated"
│ intern auf │ Token-Lookup pro Tool-Call:
│ │ user_email → SHA256-Hash → DynamoDB → Access-Token
│ ▼
│ Microsoft Graph (User-Identitaet Christoph)
OAuth-Callback Flow (einmalig pro User):
Open WebUI Chat → "ich muss authorisieren" → click → MS-Login
→ Redirect zu mcp-vf-hosted /m365/oauth/callback?code=...&state=user_hash:csrf
→ Token-Tausch + Refresh-Token speichern in DynamoDB (KMS-CMK)
→ Browser-Antwort "OK, du kannst zurueck in den Chat"
Tool-Surface
vf-vault Sub-MCP (neuer Sub-MCP, im mcp-vf-hosted gemountet)
Wrapped m365-Sub-MCP intern, abstrahiert SharePoint-Pfade weg.
| Tool | Args | Wozu |
|---|---|---|
vault_read_file(path) | path relativ zu Workshop/VibeFactory/ (z.B. 09_Standards/Regeln/Artifact-Design.md) | Liest File-Content als String. .md → text/markdown, .txt → text/plain, .docx → python-docx Plaintext, .xlsx → JSON via read_excel_used_range |
vault_list_dir(path, recursive=False) | path relativ, optional recursive | Listet Files/Subordner. Standard nicht-rekursiv. Recursive bei Suche nach „alle _context.md” |
vault_search_text(query, path=None, limit=20) | query, optional path-prefix, limit | Volltextsuche via Graph search_files innerhalb des Vaults |
vault_get_context(path=None) | optional path | Liest CLAUDE.md + ggf. _context.md und _index.md im Pfad. Convenience-Tool fuer Conv-Start |
vault_get_metadata(path) | path | Last-Modified, Author, Size — fuer Drift-Detection |
Kein Write in diesem Sprint. Sprint 2a Teil 2: vault_write_file, vault_append_to_file, vault_create_folder.
m365 v0.3 Sub-MCP (Erweiterung)
Bestehende Tools bleiben. Neuer Auth-Mode + zwei neue Helper-Tools:
| Tool | Aenderung |
|---|---|
| Alle existierenden Tools | unveraendertes API, aber Token-Lookup ueber M365_AUTH_MODE (delegated vs service-principal) |
m365_auth_status() | NEU. Gibt zurueck: hat der User valid Token? Wann laeuft er ab? Wenn nein, was ist die Authorize-URL? |
m365_auth_revoke() | NEU. Loescht den Token fuer den aktuellen User (Consent-Widerruf) |
Backward-Compat
Wenn M365_AUTH_MODE=service-principal (default heute): alles unveraendert. Sprint-2-Stack setzt M365_AUTH_MODE=delegated fuer den Open-WebUI-Path. Cron-Routinen die ohne User laufen koennen weiter Service-Principal nutzen.
Auth-Flow konkret
Initial-Authorize (einmalig pro User)
- Open WebUI sendet User-Email-Header an mcp-vf-hosted bei jedem Tool-Call
- vf-vault Sub-MCP ruft m365 Sub-MCP mit User-Email-Header
- m365 Sub-MCP: DynamoDB-Lookup → kein Token? → wirft
M365NotAuthorizedErrormit Body{ "auth_url": "https://mcp-vf.../m365/oauth/authorize?state=<hash>:<csrf>" } - Claude im Chat zeigt die URL als anklickbaren Link
- User klickt → Microsoft-Login → Consent („Claude darf in deinem Namen Files lesen, Mails lesen, …“) → Redirect zu
mcp-vf-hosted/m365/oauth/callback?code=<authcode>&state=<hash>:<csrf> - Callback-Handler: Token-Tausch bei
https://login.microsoftonline.com/<tenant>/oauth2/v2.0/tokenmit MSAL-Python. Validiert CSRF-Token. Speichert Access+Refresh+Expiry in DynamoDB. - Browser-Antwort: „Du kannst zurueck in den Chat, Claude hat jetzt Zugriff.”
- User retry-tippt die urspruengliche Anfrage. Klappt jetzt.
Refresh (automatisch pro Tool-Call)
Vor jedem Graph-Call: DynamoDB-Lookup → check expires_at < now+60s → wenn ja, mit Refresh-Token erneuern → DynamoDB-Update → Call.
Revoke
m365_auth_revoke() Tool: User kann im Chat „bitte vergiss meinen Microsoft-Zugriff” sagen. Loescht DynamoDB-Eintrag. Bei naechstem Tool-Call kommt wieder Authorize-URL.
DynamoDB-Schema
Tabelle: mcp-vf-m365-tokens in av-production (eu-central-1).
| Attribut | Typ | Wozu |
|---|---|---|
user_email_hash (PK) | String, SHA256 hex | Hash der User-Email — kein Klartext-Email in der DB |
tenant_id | String | Azure-Tenant der Authorize-Domain (vibefactorygbr) |
access_token | String, KMS-encrypted | OAuth Access-Token, ca 1h gueltig |
refresh_token | String, KMS-encrypted | OAuth Refresh-Token, lang gueltig |
expires_at | Number (Unix-Timestamp) | Wann access_token ablaeuft |
scopes_granted | String | Komma-getrennte Scope-Liste die der User consented hat |
created_at | Number | Erste Authorize |
updated_at | Number | Letzte Token-Refresh |
Encryption: KMS-CMK alias/mcp-vf-m365-tokens, separate Key vom Bucket-KMS.
Access: Task-Role hat dynamodb:GetItem, PutItem, UpdateItem, DeleteItem nur auf diese Tabelle. Keine Scans.
Azure App-Registration
Neue App-Registration in VF’s Entra-Tenant (parallel zur bestehenden Service-Principal-App):
| Punkt | Wert |
|---|---|
| Name | mcp-m365-delegated (Agentic Ventures) |
| Type | Web (Confidential Client) |
| Redirect URI | https://mcp-vf.agenticventures.de/m365/oauth/callback |
| Permissions (delegated) | Files.Read.All, Sites.Read.All, User.Read, offline_access (fuer Refresh-Token) |
| Permissions (App-only) | KEINE — getrennt von Service-Principal-App |
| Admin-Consent | nicht erforderlich fuer Read-Only-Delegated-Permissions, User-Consent reicht |
| Client-Secret | neuer Secret, in AWS Secrets Manager unter mcp-vf-hosted/m365-delegated-app |
WICHTIG: bestehende mcp-m365-App (Service-Principal mit Sites.Selected) bleibt unveraendert + parallel. Cron-Routinen nutzen die weiter.
Sicherheits-Modell
| Threat | Mitigation |
|---|---|
| Token-Theft aus DynamoDB | KMS-CMK-Encryption, IAM-restriktiv, kein DB-Dump in Logs |
| CSRF im OAuth-Callback | state=user_hash:random_csrf, csrf wird vor Authorize in Redis/DDB hinterlegt + nach Callback verglichen |
| User A spoofs als User B | Forward-User-Email-Header kommt von Open WebUI nach JWT-Validation. Mono-MCP validiert Header gegen JWT-sub-Claim — Header ohne JWT-Match → 401 |
| Refresh-Token-Leakage | Refresh-Token niemals in Logs (Audit-Middleware: PII-Scrubbing erweitern) |
| Replay-Attack auf state | csrf-Token nur 5 Min gueltig |
| Scope-Escalation | Authorize-URL hat hartkodierte Scope-Liste. User kann nicht mehr requesten als wir reinpacken |
Migration-Strategie
Co-Existenz, kein Cut-Over.
- mcp-m365 v0.3 ist Drop-in-Replacement mit
M365_AUTH_MODE-Toggle - Default-Mode bleibt
service-principal— bestehende Setups (lokale Marvin-Sessions, Cron-Jobs) unbeeindruckt - mcp-vf-hosted setzt fuer Open-WebUI-Path explizit
M365_AUTH_MODE=delegatedvia ENV - Sub-MCPs sind unabhaengig — Aenderung in m365 bricht nicht papierkram/ticketpay
- Rollback: wenn v0.3 spinnt, wieder auf v0.2 zurueck, Open WebUI verliert Multi-User-Read aber Cron-Jobs laufen
Folge-Aktionen (nach Spec-Approval)
| # | Aktion | Wo | Aufwand |
|---|---|---|---|
| 1 | Azure App-Reg fuer delegated anlegen | Marvin in Entra-Portal | 15 Min |
| 2 | DynamoDB-Tabelle + KMS-Key | CDK in mcp-vf-hosted/infra/ | 30 Min |
| 3 | mcp-m365 v0.3: delegated.py Auth-Modul + M365_AUTH_MODE Toggle | mcp-m365 Code | 2-3 Std |
| 4 | mcp-m365 v0.3: Token-Store Adapter (DynamoDB) | mcp-m365 Code | 1-2 Std |
| 5 | mcp-m365 v0.3: 2 neue Tools (auth_status, auth_revoke) | mcp-m365 Code | 30 Min |
| 6 | vf-vault Sub-MCP als neues Modul | mcp-m365 Code (oder eigenes Repo?) | 3-4 Std |
| 7 | mcp-vf-hosted: OAuth-Callback-Route /m365/oauth/callback | mcp-vf-hosted Code | 2 Std |
| 8 | mcp-vf-hosted: vf-vault als 4. Sub-MCP einbinden | mcp-vf-hosted Code | 30 Min |
| 9 | mcp-vf-hosted: CDK-Update (DynamoDB-Permissions, neuer Secret, ENV M365_AUTH_MODE) | mcp-vf-hosted/infra | 1 Std |
| 10 | End-to-End-Smoke mit Marvin als erstem Test-User | claude.ai + Open WebUI | 1 Std |
| Total | ~14-16 Std = ~2 Tage |
Decisions (approved 2026-05-14)
Marvin hat alle vier Defaults bestaetigt:
- vf-vault als eigenes Repo
mcp-vault(nicht Teil von mcp-m365). Wiederverwendbar fuer andere Kunden mit SharePoint-Vault. - JWT.sub als single source of truth fuer User-Identitaet. Kein extra
X-OpenWebUI-User-Email-Header noetig. - DynamoDB im av-production-Account, Tabellen-Prefix per Kunde (
vf-m365-tokens). M365_AUTH_MODE=service-principalbleibt Default. Marvin’s lokales Setup + Cron-Routinen aendern sich nicht. Nur der av-production-Container setztdelegated.
Bau-Status
- Block 1.1 done (2026-05-14): mcp-vault Repo-Skelett unter
~/source/mcps/mcp-vault/— pyproject, README, env, settings, m365_backend, server. 3 Read-Tools mit Code, 2 Stubs fuer Block 2. - Block 1.2 in_progress: lokaler Smoke-Test mit VF-Credentials (uv install + .env.local aus mcp-m365 + start + curl-Test) — naechster Schritt nach Session-Reset.
- Restliche Bloecke 2-5 pending wie unten.
Test-Strategie
- Unit: Token-Store hat Tests fuer encrypt/decrypt, expiry-handling, refresh-logic
- Integration in mcp-m365: lokaler Test mit Azure-App + Test-User „marvin@…” (zweiter Account)
- End-to-End in Open WebUI: Marvin als erster User, dann Christoph wenn er Lust hat
- Smoke nach Deploy:
m365_auth_status()fuer Marvin → kein Token → Authorize → wieder check → Token da →vault_list_dir('')→ File-Liste
Risiken
| Risiko | Mitigation |
|---|---|
| Azure-Admin-Consent erforderlich (wenn Org-Policy strikt ist) | Vor Bau: Christoph fragen ob Andre als Org-Admin Consent geben kann. Default ist „User-Consent reicht” fuer Read-Only-Delegated |
| MSAL-Python in Fargate-Container schwergewichtig | Image-Size pruefen — falls > 500MB → eigener Sidecar-Container |
| OAuth-Callback braucht Public-URL der bereits durch Cloudflare laeuft | /m365/oauth/callback Route in Mono-MCP, Cloudflare-Tunnel laesst durch (default * Pattern) |
| User-Email-Hash-Kollision | SHA256 mit Tenant-ID als Salt → praktisch unmoeglich |
Cross-Refs
- vf-vault-architektur — uebergeordnete Architektur
- sprint-2-master-plan — taktischer Sprint-Plan (wird angepasst nach diesem Spec)
- m365 — heutiges m365 capability
- mcp-vf-hosted — Mono-MCP der Sub-MCPs mountet
- mcp-m365 — m365 Source-Repo
- mcp-vf-hosted — Mono Source-Repo