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)

AspektCloudflare Worker (verworfen)AWS Lambda (aktuell)
AWS-CredentialsLong-Lived Access Key in CF-SecretExecution-Role am Function, rotierende Credentials
Bedrock-AufrufSigV4 mit aws4fetchnatives AWS SDK
Memory128 MB1024 MB+ (PDF-Buffer)
Provider-Bills21
AWS-First-Memorybricht espasst

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-pipeline Heartbeat alle 15 Min — Bedrock-Probe + Telegram-Push
  • Layer agentic-common mit logging + secrets + telegram + bedrock modules
  • 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):

SessionScopeStatus
1 — FoundationsLayer-Code, IAM, Secrets, Heartbeat✅ 2026-05-12
2 — Gmail + PDFMail-Pull via OAuth-Refresh, PDF-Text-Extraktion via pypdfium2 im Layer☐ — Prompt: next-session-prompt
3 — Pipeline-LogikKlassifikator (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

KlassePushWann
triage/wichtigjaLead, Kunde mit Aktion, Behoerde, Notar, Bank-mit-Aktion
triage/backofficewenn AktionRechnung, Bank, Versicherung, Steuer
triage/newsletterneinMarketing, Tech-Newsletter
triage/spamneinSpam, Phishing, generisches Cold
triage/persoenlichjaFamilie, Freunde
triage/autoneinGitHub-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”:

KomponenteHostingBegruendung
email-agent Worker (Cron-Trigger)Cloudflare WorkersTrigger-Schicht, kein MCP-Material — Cron ist aktiv, MCPs sind reaktiv
telegram-hosted MCP (NEU)Cloudflare WorkersHTTP-native, frisch gebaut, kein stdio. Pattern: Anthropic Remote-MCP-Template (Doku)
vault-hosted MCP (NEU)Cloudflare Workersdito — frischer GitHub-API-Wrapper, HTTP-native
gsuite-hosted MCP (existing Plan)AWS ECS Expressbleibt — mcp-gsuite ist Python-stdio, Container-Sidecar nicht trivial. Phase 1A.4-1A.6 in _index
papierkram-hosted / ticketpay-hostedAWS ECS Express (mcp-vf-hosted)dito — bestehend, stdio-Sub-MCPs
LLM (Klassifikation + Schreib-Agent)AWS Bedrock direktKEIN 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).

StepWasStatus
0.1Repo skeleton + Worker-Code (7 TS-Files, ~580 LOC)
0.2Domain-Mapping aus Vault (9 Eintraege)
0.3TypeScript clean
0.4KV-Namespace email_agent_state (fc757cccffdf4af49c204dfd622313f6)
0.5Worker-Code uploaded
0.64/6 Secrets gepumpt: GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REFRESH_TOKEN, TELEGRAM_BOT_TOKEN
0.7Marvin: workers.dev-Subdomain registrieren (Onboarding-Link)
0.8Marvin: Telegram Chat-ID via @userinfobot
0.9Marvin: Anthropic-API-Key (oder direkt Phase 1 starten mit Bedrock)
0.10Marvin: Bot t.me/email_1623794_bot /start
0.11Final 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.

StepWasAufwandStatus
1.1Marvin: aws sso login --profile av-prod (Browser)2 Min
1.2Bedrock Model Access pruefen + ggf. requesten fuer Haiku 4.5 in eu-central-15-15 Min
1.3IAM User email-agent-bedrock in av-production, minimal Permission (bedrock:InvokeModel auf das eine Modell) via CDK30 Min
1.4Access Key generieren, AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION als Cloudflare Secrets pumpen10 Min
1.5aws4fetch als Worker-Dep, src/classify.ts refactor: Anthropic API → Bedrock InvokeModel mit SigV4-Signing45 Min
1.6Re-deploy + /run Smoke — Klassifikation laeuft jetzt via Bedrock10 Min
1.7Neues Repo ~/source/mcps/telegram-hosted/ — Cloudflare Worker mit MCP-Server (Anthropic Remote-MCP-Template)60 Min
1.8Tools: send_message, get_chat_id, set_webhook(in 1.7)
1.9Auth: Cloudflare Access mit Marvins Google-Account fuer Phase 1 (Scalekit kommt spaeter wenn Multi-Tenant)15 Min
1.10Deploy als telegram-mcp.<workers-subdomain>.workers.dev, smoke mit Custom Connector in claude.ai Pro15 Min
1.11email-agent Worker: src/telegram.tssrc/mcp-telegram-client.ts (HTTP-Call statt Telegram-Bot-API direkt)30 Min
1.12Re-deploy + Smoke — Push laeuft jetzt ueber Telegram-MCP10 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).

StepWasAufwandStatus
1.5.1IAM-User email-agent-beleg-writer in av-production mit Inline-Policy BelegWriterAvFinanzen (S3+KMS+AssumeRole)20 Min✅ 2026-05-12
1.5.2Cross-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.3Access 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 Variablen10 Min◐ Marvin
1.5.4Klassifikator-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-Score60 Min
1.5.5PDF-Attachment-Extraktion aus Gmail (messages.attachments.get) → Worker-Memory Buffer30 Min
1.5.6src/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.7src/papierkram-beleg.ts: nur fuer business — papierkram_create_voucher via mcp-vf-hosted (existiert in papierkram). PDF als Voucher-Document hochladen45 Min
1.5.8src/vault-beleg.ts: GitHub-API commit extern/inbound/rechnungen/<datum>-<sender>-<betrag>.md mit Frontmatter (s3_uri, papierkram_voucher_id, gmail_message_id, `category: businessprivat`)30 Min
1.5.9Gmail-Labels setzen: processed + triage/beleg-business oder triage/beleg-privat (neue Labels, falls nicht existent in Gmail anlegen)15 Min
1.5.10Telegram-Sammel-Push Format: „3 Belege archiviert: Hetzner 12,49€ (UG → Papierkram), Apple 0,99€ (Privat → S3 only), Cloudflare 0,00€ (Pending — bitte klassifizieren)” + Deep-Links20 Min
1.5.11Pending-Workflow: bei Confidence <80% Upload nach <bucket>/_unklar/, Telegram-Reply-Handler /b <id> oder /p <id> verschiebt45 Min
1.5.12Smoke-Test mit 1 echtem Beleg pro Sphaere (1 UG, 1 Privat) — S3 + Papierkram + Vault-Stub verifizieren20 Min
1.5.13Aktivieren: Cron-Schedule auf Live, 1 Woche Beobachtungpassive
1.5.14Backfill 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 + Vault90 Min
1.5.15Reminder fuer ~2026-06-12: Object-Lock-Mode auf Compliance umstellen wenn Pipeline stabil2 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.

StepWasAufwandStatus
2.1vault-hosted MCP auf Cloudflare Workers (~/source/mcps/vault-hosted/)90 Min
2.2Tools: lookup_contact_by_domain, fetch_vault_file_safe (filtert visibility: internal aus Outbound-Context)(in 2.1)
2.3GitHub-PAT mit repo-Scope auf agentic-ventures als Worker Secret5 Min
2.4Deploy + Custom Connector in claude.ai Pro testen (Marvin kann von claude.ai aus den Vault selektiv lesen)20 Min
2.5mcp-pipeline-aws Phase 1A.4-1A.6: gsuite-hosted deployen (ECS Express in av-production, Cloudflare-DNS auf gmail.agenticventures.de)2-3h
2.6Service-to-Service Auth Scalekit Client-Credentials fuer email-agent Worker → gsuite-hosted MCP60 Min
2.7email-agent Worker: src/gmail.tssrc/mcp-gsuite-client.ts30 Min
2.8src/vault.tssrc/mcp-vault-client.ts (kein hardcoded Mapping mehr)20 Min
2.9Worker schrumpft auf ~150 LOC (von 580): Cron + 4 MCP-Tool-Calls + Telegram-Sammel-Push(in 2.7+2.8)
2.101-Woche Strict-Mode-Beobachtung mit Refactored-Setuppassive

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).

StepWasAufwand
3.1Schreib-Agent: Multi-Turn-Loop mit Tools get_thread, lookup_vault_file_safe, search_calendar, create_draft1-2 Tage
3.2Telegram-Webhook fuer Two-Way: /draft <msg-id> <was>, /archive <msg-id>, /snooze <msg-id>1 Tag
3.3Custom Connector in claude.ai Pro fuer Marvin: triagiere/draft on-demand von claude.ai aus30 Min
3.4Privat-Account marvinkuehlmann@gmail.com als zweiter gsuite-Account1h
3.5Custom-Domain email-agent.agenticventures.de (nach NS-Migration auf Cloudflare aus cloudflare-migration-guide)30 Min
3.6Multi-Tenant-Vorbereitung: Durable Object pro Tenant, Bedrock-Spend-Cap pro Kunde via AI Gateway2-3 Tage
3.7Productized Vertical: Onboarding-Page, OAuth-Connect, AVV-Template — eigenes Mini-Projekt unter intern/projekte/email-agent-saas/mehrere Wochen

Decision-Points

PunktWas Marvin entscheidetWann
1.6 vor DeployCloudflare Free-Plan OK?jetzt
1.6 vor DeployTelegram-Bot Namebei BotFather
Nach 1.7Smoke-Test grun? Klassifikator plausibel?sofort nach Deploy
Nach 1 WocheStrict-Mode aufheben (Auto-Archive)?nach Feedback-Phase
Phase-2-StartSchreib-Agent direkt oder erst weitere Triage-Iteration?nach 1 Woche Triage
Phase-3-StartRefactor 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.*.json schon 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

RisikoMitigation
Klassifikator verfehlt wichtige MailStrict-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 brokenSammel-Push am Ende ist immer 1 Nachricht, einzelne Pushes nur bei `wichtig
Gmail API Rate-Limit100 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.

1 Datei in diesem Ordner.