Email-Agent
Cloudflare Worker der hello@marvinkuehlmann.com alle 15 Min triagiert, klassifiziert (Anthropic Haiku 4.5 + Tool-Use), Gmail-Labels setzt und Marvin via Telegram-Bot bei Wichtigem pingt. Mac-unabhaengig, laeuft am Edge.
Architektur-Pivot 2026-05-12: weg von Cloudflare Worker, hin zu AWS Lambda. Begruendung: IAM-Execution-Role am Lambda statt Long-Lived-Access-Key in CF-Secret-Store, AWS-First-Konsistenz, native Bedrock-Aufrufe, eine Bill. Detail-Migration: ~/source/agents-platform/docs/migration-cloudflare-zu-lambda.md. Plattform-Pattern: agents-platform.
Repo neu: ~/source/agents-platform/ (CDK + Lambda + Layer)
Repo alt (archiviert): ~/source/agents/email-agent/ (Cloudflare Worker, deprecated)
Warum AWS Lambda (Stand 2026-05-12)
| Aspekt | Cloudflare Worker (verworfen) | AWS Lambda (aktuell) |
|---|---|---|
| AWS-Credentials | Long-Lived Access Key in CF-Secret | Execution-Role am Function, rotierende Credentials |
| Bedrock-Aufruf | SigV4 mit aws4fetch | natives AWS SDK |
| Memory | 128 MB | 1024 MB+ (PDF-Buffer) |
| Provider-Bills | 2 | 1 |
| AWS-First-Memory | bricht es | passt |
Frueheres Argument „Cron + Edge + Free-Tier” stimmt zwar, aber das Security-Argument (kein Long-Lived AWS-Credential ausserhalb AWS) wiegt schwerer. Plus: viele weitere Agents sollen folgen (Daily-Planner, Mahnungs-Checker, Follow-up-Bot, …) und die laufen alle mit demselben Pattern auf der Plattform agents-platform — Cloudflare waere fuer jeden davon ein separates Setup mit eigenen Access-Keys.
Stand 2026-05-12 abend — Session 1 von 4 durch
Beleg-Pipeline Session 1 abgeschlossen (Layer-Code Foundations). Aktuell laeuft im av-production:
- Lambda
agent-beleg-pipelineHeartbeat alle 15 Min — Bedrock-Probe + Telegram-Push - Layer
agentic-commonmitlogging+secrets+telegram+bedrockmodules - 3 Secrets in AWS Secrets Manager mit echten Werten (telegram-bot-token, gmail-oauth-refresh als nested Bundle, github-pat)
Was noch fehlt (Sessions 2-4):
| Session | Scope | Status |
|---|---|---|
| 1 — Foundations | Layer-Code, IAM, Secrets, Heartbeat | ✅ 2026-05-12 |
| 2 — Gmail + PDF | Mail-Pull via OAuth-Refresh, PDF-Text-Extraktion via pypdfium2 im Layer | ☐ — Prompt: next-session-prompt |
| 3 — Pipeline-Logik | Klassifikator (Bedrock + 3-Signal-Heuristik), S3-Upload (av-finanzen + Cross-Account mk-finanzen), Papierkram-Voucher, Vault-Stub via GitHub-API | ☐ |
| 4 — Backfill + Pending | ~486 historische Belege durchschleifen, Telegram-Reply-Handler /b / /p fuer unklare Faelle | ☐ |
Referenzen:
- Run-Bericht Session 1: _index
- Verdichtete Lessons-Learned (IAM-Quirks, EventBridge-Timezone, urllib3-Pattern, Repo-Divergenz): aws-lambda-cron-fallstricke
- Skill der die Plattform-Routinen baut: SKILL
- Parallel-Projekt (erste Routine ueber den Skill): _index
Achtung Phase-1.5-Tabelle weiter unten: die alten Cloudflare-Worker-Schritte (1.0-1.7, 1.5.1-1.5.15) sind nach dem AWS-Lambda-Pivot teilweise obsolet bzw. ihre Implementation laeuft anders. Schritte 1.5.1-1.5.3 (IAM-Setup) sind in Lambda-Form als Execution-Role am Function umgesetzt (siehe
infra/lib/beleg-pipeline-stack.ts). Die alten Steps stehen als Referenz fuer den urspruenglichen Plan-Inhalt.
Architektur (Phase 1)
Cloudflare Cron (*/15 * * * *)
└─ Worker email-agent
├─ KV.get(last_check)
├─ Gmail.list(after:last_check) ← direkt Gmail API, kein hosted MCP yet
├─ pro Mail:
│ ├─ getMessageMeta (From/Subject/Date/Snippet)
│ ├─ vault.lookupContact(domain) ← hardcoded Map, 9 Eintraege
│ ├─ Anthropic.classify (Haiku 4.5 + Tool-Use)
│ ├─ Gmail.addLabel(triage/<klasse>)
│ └─ if should_push: Telegram.send (HTML, Gmail-Deep-Link)
├─ KV.put(last_check = now)
└─ Telegram.send (Sammel-Push: "12 gesichtet, 2 wichtig, 7 newsletter")
Strict-Mode Phase-1: nur labeln + pushen, kein Archivieren/Loeschen. Erst nach 1 Woche Feedback wird STRICT_MODE=false.
Klassen
| Klasse | Push | Wann |
|---|---|---|
triage/wichtig | ja | Lead, Kunde mit Aktion, Behoerde, Notar, Bank-mit-Aktion |
triage/backoffice | wenn Aktion | Rechnung, Bank, Versicherung, Steuer |
triage/newsletter | nein | Marketing, Tech-Newsletter |
triage/spam | nein | Spam, Phishing, generisches Cold |
triage/persoenlich | ja | Familie, Freunde |
triage/auto | nein | GitHub-Notif, Calendar, System |
Vault-Zugriff (selektiv)
Behavior-Rule 7 strikt: Worker laedt niemals das ganze Vault. Lookup ist Sender-Domain → bekannter Kontakt-Hint (Name + Typ + 1 Notiz). Dieser Hint geht ins Anthropic-Prompt, sonst nichts. visibility: internal-Inhalte sind im Code nicht referenziert.
Phase-2: Worker fetcht via GitHub-PAT genau eine Vault-Datei pro Treffer (Read-Path), niemals alles.
Architektur-Entscheidung: Hybrid Hosting (2026-05-11 abend, neu)
Nach Marvin-Pivot „MCP Remote First, Cloudflare Edge wo moeglich”:
| Komponente | Hosting | Begruendung |
|---|---|---|
email-agent Worker (Cron-Trigger) | Cloudflare Workers | Trigger-Schicht, kein MCP-Material — Cron ist aktiv, MCPs sind reaktiv |
telegram-hosted MCP (NEU) | Cloudflare Workers | HTTP-native, frisch gebaut, kein stdio. Pattern: Anthropic Remote-MCP-Template (Doku) |
vault-hosted MCP (NEU) | Cloudflare Workers | dito — frischer GitHub-API-Wrapper, HTTP-native |
gsuite-hosted MCP (existing Plan) | AWS ECS Express | bleibt — mcp-gsuite ist Python-stdio, Container-Sidecar nicht trivial. Phase 1A.4-1A.6 in _index |
papierkram-hosted / ticketpay-hosted | AWS ECS Express (mcp-vf-hosted) | dito — bestehend, stdio-Sub-MCPs |
| LLM (Klassifikation + Schreib-Agent) | AWS Bedrock direkt | KEIN MCP davor — Bedrock IST der Service, MCP waere sinnlose Indirection. AWS-First, EU-Region (eu-central-1), eine Bill |
Grundregel: MCP = wiederverwendbare Capability mit mehreren Konsumenten (Worker + claude.ai + zukuenftige Agents). Direkter API-Call wo nur 1 Konsument oder reine Service-Konsumption.
Pattern-Ref: cloudflare-capability-map Punkt A (Workers + Agents SDK statt ECS fuer NEUE MCPs).
Phasen + Steps
Phase 0: Triage v1 (gestartet 2026-05-11 abend, abschliessen morgen frueh)
Ziel: Worker laeuft mit Anthropic Direct API + Gmail direkt + Telegram direkt. Funktioniert sofort, ist Throwaway-Code-Pfad fuer LLM (wird in Phase 1 auf Bedrock migriert).
| Step | Was | Status |
|---|---|---|
| 0.1 | Repo skeleton + Worker-Code (7 TS-Files, ~580 LOC) | ✅ |
| 0.2 | Domain-Mapping aus Vault (9 Eintraege) | ✅ |
| 0.3 | TypeScript clean | ✅ |
| 0.4 | KV-Namespace email_agent_state (fc757cccffdf4af49c204dfd622313f6) | ✅ |
| 0.5 | Worker-Code uploaded | ✅ |
| 0.6 | 4/6 Secrets gepumpt: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN, TELEGRAM_BOT_TOKEN | ✅ |
| 0.7 | Marvin: workers.dev-Subdomain registrieren (Onboarding-Link) | ☐ |
| 0.8 | Marvin: Telegram Chat-ID via @userinfobot | ☐ |
| 0.9 | Marvin: Anthropic-API-Key (oder direkt Phase 1 starten mit Bedrock) | ☐ |
| 0.10 | Marvin: Bot t.me/email_1623794_bot /start | ☐ |
| 0.11 | Final deploy + /health smoke + /run Test | ☐ |
Entscheidungs-Punkt morgen 15:00: Phase 0 noch fertig fahren (15 Min) ODER direkt Phase 1 mit Bedrock-Migration starten (klassify.ts wird eh ersetzt)?
Phase 1: Bedrock + telegram-hosted MCP (morgen 2026-05-12, ~3h)
Ziel: LLM laeuft via Bedrock. Telegram laeuft als Cloudflare-Workers-MCP. Worker spricht den telegram-MCP statt Telegram-Bot-API direkt.
| Step | Was | Aufwand | Status |
|---|---|---|---|
| 1.1 | Marvin: aws sso login --profile av-prod (Browser) | 2 Min | ☐ |
| 1.2 | Bedrock Model Access pruefen + ggf. requesten fuer Haiku 4.5 in eu-central-1 | 5-15 Min | ☐ |
| 1.3 | IAM User email-agent-bedrock in av-production, minimal Permission (bedrock:InvokeModel auf das eine Modell) via CDK | 30 Min | ☐ |
| 1.4 | Access Key generieren, AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION als Cloudflare Secrets pumpen | 10 Min | ☐ |
| 1.5 | aws4fetch als Worker-Dep, src/classify.ts refactor: Anthropic API → Bedrock InvokeModel mit SigV4-Signing | 45 Min | ☐ |
| 1.6 | Re-deploy + /run Smoke — Klassifikation laeuft jetzt via Bedrock | 10 Min | ☐ |
| 1.7 | Neues Repo ~/source/mcps/telegram-hosted/ — Cloudflare Worker mit MCP-Server (Anthropic Remote-MCP-Template) | 60 Min | ☐ |
| 1.8 | Tools: send_message, get_chat_id, set_webhook | (in 1.7) | ☐ |
| 1.9 | Auth: Cloudflare Access mit Marvins Google-Account fuer Phase 1 (Scalekit kommt spaeter wenn Multi-Tenant) | 15 Min | ☐ |
| 1.10 | Deploy als telegram-mcp.<workers-subdomain>.workers.dev, smoke mit Custom Connector in claude.ai Pro | 15 Min | ☐ |
| 1.11 | email-agent Worker: src/telegram.ts → src/mcp-telegram-client.ts (HTTP-Call statt Telegram-Bot-API direkt) | 30 Min | ☐ |
| 1.12 | Re-deploy + Smoke — Push laeuft jetzt ueber Telegram-MCP | 10 Min | ☐ |
Wall-Clock: 3-4h aktiv. Bottleneck: Marvin-Setup-Schritte (1.1, 1.4 falls Werte gebraucht werden).
Phase 1.5: Beleg-Pipeline (parallel zu Phase 1, ~4-5h)
Ziel: Worker zieht aus Gmail-Label Belege PDFs, klassifiziert business/privat, schreibt nach S3, bucht in Papierkram, commitet Vault-Stub. Schliesst die seit 2026-05-12 dokumentierte Beleg-Konvention.
Vorbedingung: Buckets av-finanzen-eu-central-1 (av-production) + mk-finanzen-eu-central-1 (mk-privat) existieren mit Object-Lock Governance 2555d, KMS-Encryption, Lifecycle (angelegt 2026-05-12, siehe buckets).
| Step | Was | Aufwand | Status |
|---|---|---|---|
| 1.5.1 | IAM-User email-agent-beleg-writer in av-production mit Inline-Policy BelegWriterAvFinanzen (S3+KMS+AssumeRole) | 20 Min | ✅ 2026-05-12 |
| 1.5.2 | Cross-Account-Role email-agent-beleg-writer-cross-account in mk-privat mit Inline-Policy BelegWriterMkFinanzen (S3+KMS, Trust auf av-production-User) | 20 Min | ✅ 2026-05-12 |
| 1.5.3 | Access Key generiert, in ~/source/agents/email-agent/.env.beleg-secrets (gitignored). Smoke-Test 4× gruen (Identity, ListBucket av-finanzen, AssumeRole, ListBucket mk-finanzen). Marvin: wrangler secret put fuer 6 Variablen | 10 Min | ◐ Marvin |
| 1.5.4 | Klassifikator-Erweiterung in src/classify.ts: neue Klasse triage/beleg mit Sub-Label business oder privat. 3-Signal-Logik (Gmail-Account, PDF-Empfaenger via PDF-text-extract, Sender-Domain) + Confidence-Score | 60 Min | ☐ |
| 1.5.5 | PDF-Attachment-Extraktion aus Gmail (messages.attachments.get) → Worker-Memory Buffer | 30 Min | ☐ |
| 1.5.6 | src/s3-beleg.ts: aws4fetch SigV4 PutObject in den richtigen Bucket. Object-Key <jahr>/<monat>/<sender-slug>-<datum>-<betrag>.pdf. Metadata: gmail-message-id, sender, subject, amount. | 60 Min | ☐ |
| 1.5.7 | src/papierkram-beleg.ts: nur fuer business — papierkram_create_voucher via mcp-vf-hosted (existiert in papierkram). PDF als Voucher-Document hochladen | 45 Min | ☐ |
| 1.5.8 | src/vault-beleg.ts: GitHub-API commit extern/inbound/rechnungen/<datum>-<sender>-<betrag>.md mit Frontmatter (s3_uri, papierkram_voucher_id, gmail_message_id, `category: business | privat`) | 30 Min |
| 1.5.9 | Gmail-Labels setzen: processed + triage/beleg-business oder triage/beleg-privat (neue Labels, falls nicht existent in Gmail anlegen) | 15 Min | ☐ |
| 1.5.10 | Telegram-Sammel-Push Format: „3 Belege archiviert: Hetzner 12,49€ (UG → Papierkram), Apple 0,99€ (Privat → S3 only), Cloudflare 0,00€ (Pending — bitte klassifizieren)” + Deep-Links | 20 Min | ☐ |
| 1.5.11 | Pending-Workflow: bei Confidence <80% Upload nach <bucket>/_unklar/, Telegram-Reply-Handler /b <id> oder /p <id> verschiebt | 45 Min | ☐ |
| 1.5.12 | Smoke-Test mit 1 echtem Beleg pro Sphaere (1 UG, 1 Privat) — S3 + Papierkram + Vault-Stub verifizieren | 20 Min | ☐ |
| 1.5.13 | Aktivieren: Cron-Schedule auf Live, 1 Woche Beobachtung | passive | ☐ |
| 1.5.14 | Backfill historischer Belege (~486 in Gmail label:Belege): eigener Backfill-Skript-Modus im Worker (/backfill?from=2023-01-01&to=2026-05-12), laeuft einmal Stunde durch, archiviert in S3 + Papierkram + Vault | 90 Min | ☐ |
| 1.5.15 | Reminder fuer ~2026-06-12: Object-Lock-Mode auf Compliance umstellen wenn Pipeline stabil | 2 Min | ☐ |
Wall-Clock: 4-5h aktiv. Kann parallel zu Phase 1 laufen — IAM-Setup (1.5.1-1.5.3) hat keine Abhaengigkeit zu Bedrock-Migration.
Klassifikator-Heuristik (Step 1.5.4):
score_business = 0
score_privat = 0
# Signal 1: Gmail-Account
if account == "hello@marvinkuehlmann.com": score_business += 30
if account == "marvinkuehlmann@gmail.com": score_privat += 30
# Signal 2: Empfaenger im PDF
if pdf_text contains "Agentic Ventures UG": score_business += 40
if pdf_text contains "Marvin Kuehlmann\nWarendorfer Str. 63": score_privat += 40
# Signal 3: Sender-Domain (hardcoded Map)
business_senders = {hetzner.com, replicate.com, replicate.email, anthropic.com,
ionos.de, cloudflare.com, stripe.com, qonto.com, sipgate.de,
amazonaws.com, midjourney.com, scalekit.com}
privat_senders = {apple.com, email.apple.com, cannaleo.de, greenmedical.health,
cremare.de, viomedi.de, prawani.de, deutschebahn.com (Privat),
vattenfall.de, bvh.org, fraenk.de, lemniscus.de}
if sender_domain in business_senders: score_business += 30
if sender_domain in privat_senders: score_privat += 30
# Decision
if abs(score_business - score_privat) < 20: → Pending
elif score_business > score_privat: → business
else: → privat
Phase 2: vault-hosted MCP + gsuite-hosted live + Worker-Refactor (Mi/Do, 1-2 Tage)
Ziel: Worker spricht 3 MCPs (gsuite + telegram + vault), eigene Logik schrumpft auf Cron-Trigger + Orchestrierung. Schreib-Agent vorbereitet.
| Step | Was | Aufwand | Status |
|---|---|---|---|
| 2.1 | vault-hosted MCP auf Cloudflare Workers (~/source/mcps/vault-hosted/) | 90 Min | ☐ |
| 2.2 | Tools: lookup_contact_by_domain, fetch_vault_file_safe (filtert visibility: internal aus Outbound-Context) | (in 2.1) | ☐ |
| 2.3 | GitHub-PAT mit repo-Scope auf agentic-ventures als Worker Secret | 5 Min | ☐ |
| 2.4 | Deploy + Custom Connector in claude.ai Pro testen (Marvin kann von claude.ai aus den Vault selektiv lesen) | 20 Min | ☐ |
| 2.5 | mcp-pipeline-aws Phase 1A.4-1A.6: gsuite-hosted deployen (ECS Express in av-production, Cloudflare-DNS auf gmail.agenticventures.de) | 2-3h | ☐ |
| 2.6 | Service-to-Service Auth Scalekit Client-Credentials fuer email-agent Worker → gsuite-hosted MCP | 60 Min | ☐ |
| 2.7 | email-agent Worker: src/gmail.ts → src/mcp-gsuite-client.ts | 30 Min | ☐ |
| 2.8 | src/vault.ts → src/mcp-vault-client.ts (kein hardcoded Mapping mehr) | 20 Min | ☐ |
| 2.9 | Worker schrumpft auf ~150 LOC (von 580): Cron + 4 MCP-Tool-Calls + Telegram-Sammel-Push | (in 2.7+2.8) | ☐ |
| 2.10 | 1-Woche Strict-Mode-Beobachtung mit Refactored-Setup | passive | ☐ |
Wall-Clock: 1-2 Tage aktiv + 1 Woche Beobachtung.
Phase 3: Schreib-Agent + Two-Way + Multi-Tenant (mittel, 2-3 Wochen)
Ziel: echter agentic Loop mit mehreren Tools. Marvin triggert Drafts via Telegram-Reply. Vorbereitung fuer Productized Vertical (KMU-Kunden).
| Step | Was | Aufwand |
|---|---|---|
| 3.1 | Schreib-Agent: Multi-Turn-Loop mit Tools get_thread, lookup_vault_file_safe, search_calendar, create_draft | 1-2 Tage |
| 3.2 | Telegram-Webhook fuer Two-Way: /draft <msg-id> <was>, /archive <msg-id>, /snooze <msg-id> | 1 Tag |
| 3.3 | Custom Connector in claude.ai Pro fuer Marvin: triagiere/draft on-demand von claude.ai aus | 30 Min |
| 3.4 | Privat-Account marvinkuehlmann@gmail.com als zweiter gsuite-Account | 1h |
| 3.5 | Custom-Domain email-agent.agenticventures.de (nach NS-Migration auf Cloudflare aus cloudflare-migration-guide) | 30 Min |
| 3.6 | Multi-Tenant-Vorbereitung: Durable Object pro Tenant, Bedrock-Spend-Cap pro Kunde via AI Gateway | 2-3 Tage |
| 3.7 | Productized Vertical: Onboarding-Page, OAuth-Connect, AVV-Template — eigenes Mini-Projekt unter intern/projekte/email-agent-saas/ | mehrere Wochen |
Decision-Points
| Punkt | Was Marvin entscheidet | Wann |
|---|---|---|
| 1.6 vor Deploy | Cloudflare Free-Plan OK? | jetzt |
| 1.6 vor Deploy | Telegram-Bot Name | bei BotFather |
| Nach 1.7 | Smoke-Test grun? Klassifikator plausibel? | sofort nach Deploy |
| Nach 1 Woche | Strict-Mode aufheben (Auto-Archive)? | nach Feedback-Phase |
| Phase-2-Start | Schreib-Agent direkt oder erst weitere Triage-Iteration? | nach 1 Woche Triage |
| Phase-3-Start | Refactor auf gsuite-hosted MCP wenn der live ist? | nach Pipeline 1A.6 |
Was Claude (Agent) NICHT kann
- Cloudflare-Account anlegen /
wrangler login(Browser-OAuth) - BotFather konversieren in Telegram
- Eigene Telegram-Chat-ID rausfinden (passiert in der Telegram-App)
- Gmail-OAuth-Refresh-Token aus dem Browser holen (lokal in
~/.config/mcp-gsuite/.oauth2.*.jsonschon vorhanden, das ist OK) - Secrets ins Cloudflare pumpen (Marvin tippt sie via
wrangler secret put)
Cost
Phase 1 realistisch: ~2 €/Monat Anthropic-Anteil. Cloudflare Workers + KV + Telegram + Gmail = 0 € (Free-Tier).
Risiken & Mitigationen
| Risiko | Mitigation |
|---|---|
| Klassifikator verfehlt wichtige Mail | Strict-Mode: nur labeln + pushen, nichts loeschen. “Im Zweifel wichtig” als Prompt-Regel |
| Refresh-Token laeuft ab (selten, ~6 Monate idle) | Klare Anleitung im README: lokal mcp-gsuite reauth + neuen Token via wrangler secret put |
| Anthropic-Spend explodiert (Loop oder Bug) | Phase-2: Cloudflare AI Gateway davor mit Spend-Cap |
| Telegram-Push-Spam wenn Klassifikator broken | Sammel-Push am Ende ist immer 1 Nachricht, einzelne Pushes nur bei `wichtig |
| Gmail API Rate-Limit | 100 Requests pro Triage × 4/Stunde = 400/h, Quota ist 250/User/Sek. Kein Problem |
Cross-Refs
- cloudflare-capability-map — warum Workers statt ECS
- _index — Phase-3 Migration auf gsuite-hosted
- gsuite — der MCP der heute lokal Gmail spricht
- stack — Tool-Inventar
- Memory:
AWS-First Hosting,Email Accounts & Tooling
Verlauf
- 2026-05-11 nachmittag: Projekt angelegt, Worker-Code v0.1 komplett (~580 Zeilen TS), TypeScript clean.
- 2026-05-11 abend: KV-Namespace + Worker upload + 4/6 Secrets gepumpt (Gmail-Triple + Telegram-Token). Deploy blockiert auf workers.dev-Subdomain-Registrierung. Architektur-Pivot: Marvin entscheidet “MCPs Remote First on Edge” — Plan restrukturiert: Phase-1-Code (Anthropic Direct + Telegram-Bot-API direkt) wird in Phase-1 (morgen) auf Bedrock + telegram-hosted-MCP migriert. Phasen 0/1/2/3 mit klarer Hybrid-Hosting-Entscheidung dokumentiert. Naechster Slot: morgen 2026-05-12 15:00, Phase 1.