Scoped Mandate — Implementierungsstand

Fachliches Brief: SCOPED_MANDATE_EUDIPal.md. Deployment der Mocks: ../tools/DEPLOY.md. Branch feat/scoped-mandate, Stand 2026-10-10 (Build 747).

Ziel: Ein Shopping-Agent kauft im Namen des Nutzers ein, nach einer SCA. Die Bank haelt das Mandat und prueft den Payment-Scope, der Haendler prueft den Fach-Scope und signiert eine Attestation, der Agent hat eigene Identitaet und eigenen Key. Die Wallet entscheidet nie ueber Zahlungen.

Einordnung in die bestehende Architektur

Rolle Phase 1 (jetzt) Phase 2 (Tausch hinter derselben Schnittstelle)
Bank tools/mock-bank (Flask, Firestore) → https://bank.s-you.me PaSO-Referenzimplementierung
Haendler tools/mock-merchant (Flask, Secret-Manager-Key) → https://shop.s-you.me, Haendler-Sicht /dashboard echtes UCP-Endpoint
Agent ScopedAgentIdentity + Secure-Enclave-Key in der App EBW-Legal-Person-Credential
Wallet/SCA PaSO-Pfad (OID4VPCoordinator, PaSOPaymentConsentView) mit transaction_data Typ generic_mandate PaSO-URN

Die Swift-Schnittstellen liegen in Sources/ScopedMandate/Services/ScopedMandateProtocols.swift: MandateServiceProtocol (Bank), MerchantServiceProtocol (Haendler), AgentIdentityProtocol (Agent). Fehler sind immer ScopedMandateError mit rejectedBy (bank | merchant) und Code — jede Ablehnung zeigt Grund und Partei (Brief, Leitplanke).

Die aeltere Mandats-Schicht Sources/Mandate/ (M1/M2, PR #108: Wallet signiert Mandate selbst) widerspricht dem Brief und bleibt vorerst daneben stehen. Entscheidung dazu in AP6.

Dateien

BankingPal-Pocket/Sources/ScopedMandate/
├── Models/
│   ├── ScopedMandateTypes.swift     Cents, ScopedMandate, PaymentScope, FunctionalScope,
│   │                                MandateRequest (+ agent_public_key_jwk), GenericMandateTransactionData,
│   │                                MerchantAttestation, AgentPaymentRequest, PaymentReceipt,
│   │                                AgentIdentityCredential, ScopedMandateJSON (canonical)
│   └── ScopedMandateErrors.swift    DecidingParty, BankRejectionReason (13), MerchantRejectionReason (7)
├── Services/
│   ├── ScopedMandateProtocols.swift Bank-/Haendler-/Agent-Protokolle, MandateActivityEntry (Outcome im Bank-Wire-Format), Hashing
│   ├── ScopedMandateHTTP.swift      Endpunkte, JSON-Transport, Fehlermapping 422 → ScopedMandateError (Partei bleibt erhalten)
│   ├── HTTPMandateService.swift     MandateServiceProtocol gegen bank.s-you.me; prueft transaction_data_hash lokal nach
│   └── HTTPMerchantService.swift    MerchantServiceProtocol gegen shop.s-you.me; Discovery, prueft Attestation-Hash + Signatur
├── Agent/
│   ├── ECPublicKeyJWK.swift         P-256 JWK + base64url
│   ├── AgentSigningKey.swift        SecureEnclaveAgentKey, SoftwareAgentKey, Factory
│   ├── ScopedAgentIdentity.swift    Credential, agentRef, JWK, MandateRequest-Bau; signedPaymentRequest als
│   │                                Extension auf AgentIdentityProtocol
│   ├── ScopedMandateAgent.swift     Einkaufs-Agent (AP7): myMandates, activeMandate, requestMandate (Consent),
│   │                                purchase (Suche → Attestation → Zahlung), activity, revoke; ShoppingListParser
│   └── ScopedMandateTools.swift     Tool-Definitionen get_scoped_mandates, request_scoped_mandate,
│                                    shop_with_mandate, get_scoped_mandate_activity, revoke_scoped_mandate
Sources/AI/ToolRegistry+ScopedMandate.swift  Tool-Ausfuehrung: formatiert Ergebnisse und Ablehnungen (immer mit Partei)
Sources/AI/ContextBuilder.swift      Prompt-Abschnitt "Einkaufs-Agent mit Bank-Mandat"
└── Consent/
    ├── ScopedMandateConsentCoordinator.swift  Flow B: Anfrage → Consent → Geraete-SCA → lokaler Hash → authorize;
    │                                          MandateAuthenticator (LAContext / Fake), ScopedMandateConsentRequest
    └── MandateConsentView.swift     MandateConsentSheet (Zustaende) + MandateConsentView (PaSO-Muster)

Aufhaengung: WalletHomeView praesentiert das Sheet auf `Notification.Name.scopedMandateConsentRequested`
(Object: ScopedMandateConsentRequest mit Rueckruf). Demo-Trigger: Einstellungen → Labor → "Agent-Mandat erteilen".

Tests/ScopedMandate/
├── ScopedMandateTypesTests.swift    12 Tests (JSON-Form, Cents, Hashes, Fehler)
├── ScopedAgentIdentityTests.swift   13 Tests (Credential, JWK, agent_proof wie die Bank, Key-Trennung)
├── ScopedMandateWireTests.swift     9 Tests ohne Netz (URLProtocol-Stub): Bank-Log-Format, Fehlermapping,
│                                    Hash-Gegenpruefung, Attestation-Signatur, Katalog
├── ScopedMandateConsentCoordinatorTests.swift  8 Tests (Fake-Bank, Fake-SCA): Hash = Anzeige, kein Bank-Call
│                                    ohne SCA, Abbruch, Ablehnung mit Partei, run() wartet auf Entscheidung
├── ScopedMandateAgentTests.swift    9 Tests (Fake-Bank, Fake-Haendler): Parser, Einkauf mit gueltigem agent_proof,
│                                    Haendler-/Bank-Ablehnung mit Partei + betroffenen Artikeln, Mandatswahl, Widerruf
└── ScopedMandateLiveTests.swift     Flow A–D gegen die Live-Server; nur mit TEST_RUNNER_SCOPED_MANDATE_LIVE=1

tools/mock-bank/        app.py, dashboard.html, smoke_test.py (24), deploy.sh, allow-public.sh
tools/mock-merchant/    app.py, dashboard.html (Haendler-Sicht: Pruef-Log + Katalog, in-memory), smoke_test.py (23), deploy.sh
tools/setup-lb.sh       HTTPS-Load-Balancer fuer bank./shop.s-you.me

Konventionen (gelten auf allen Seiten)

Arbeitspakete

AP Inhalt Stand
1 Typen, Fehler, Protokolle ✓ Build 746
2 Mandate Service (Bank-Mock) live, Dashboard ✓
3 Merchant Service (Katalog, Fach-Scope, Attestation) live ✓
3b Bank-Dashboard (Mandate, Limits, Aktivitaetslog, Widerruf) ✓
— Domains bank.s-you.me / shop.s-you.me via Load Balancer ✓
4 Agent-Identitaet in Swift ✓ Build 747
5 Consent-Screen fuer generic_mandate (eigener Coordinator, PaSOPaymentConsentView-Muster, Geraete-SCA, lokaler Hash + MandateAuthorizationProof) ✓ Build 750
6 Mandats-Karte im Stapel (read-only, Spiegel der Bank) offen — Entscheidung zu Sources/Mandate/ siehe unten (Build 751)
7 Chat-Flow: Tools get_scoped_mandates, request_scoped_mandate, shop_with_mandate, get_scoped_mandate_activity, revoke_scoped_mandate ✓ Build 751
8 Demo-Skript, Firestore-Testdaten leeren offen
— HTTPMandateService / HTTPMerchantService gegen die Live-Server, Live-Test Flow A–D ✓ Build 749
— Interface-Dokument fuer den Google-PoC-Call: scoped-mandate-interfaces.md (EN) ✓

Chat-Flow (AP7, Build 751)

Demo-Dialog: „Kauf mir 2 Milch, Brot und Spülmittel" → get_scoped_mandates (keins) → request_scoped_mandate (Sheet, Face ID) → shop_with_mandate (Katalog → Attestation → Zahlung) → Bericht mit Positionen, Summe, SCT-Inst-Referenz, Rest. „Und ein Bier dazu" → Haendler lehnt ab (category_excluded), Antwort nennt Haendler, Grund und Artikel. „Was hat der Agent gekauft?" → get_scoped_mandate_activity. „Widerrufen" → revoke_scoped_mandate, danach lehnt die Bank ab.

Entscheidung zu Sources/Mandate/ (M1/M2, PR #108): Die alten Tools list_mandates / purchase_with_mandate (Wallet-signierte Mandate, PolicyEngine in der App) sind aus AgentTools.all genommen — zwei Mandats-Modelle im selben Prompt widersprechen sich. Code, Store, Views (Labor → Vollmachten) bleiben vorerst erhalten; endgueltiger Rueckbau nach dem Google-Call, wenn klar ist, dass Phase 2 auf dem Bank-Mandat aufsetzt.

HTTP-Schicht (Build 749)

Vorgaben Sparkasse (10.10.2026)

Vier Punkte aus der Abstimmung mit der Sparkasse und wie sie umgesetzt sind (Build 748):

# Vorgabe Umsetzung
1 Agent und Plattformanbieter weisen sich gegenueber der Sparkasse aus MandateRequest.agent_credential traegt das vollstaendige Credential (Agent-ID, Anzeigename, Betreiber, Registernummer, Key). Die Bank prueft Kurzform ↔ Credential, ebw_credential_hash und Key ↔ JWK; sonst agent_identity_invalid (rejected_by bank). Credential bleibt im Mandats-Spiegel, Dashboard zeigt Agent + Anbieter. Phase 2: zusaetzlich EBW-Signatur.
2 Mandat laeuft unter SPAA, Dynamic Recurring Payment PaymentScope.scheme = "spaa_drp" (Default in Swift und Bank), Discovery nennt Scheme, Dashboard zeigt es.
3 Kunde kann das Mandat jederzeit in der S-App widerrufen Bankseitig POST /mandates/{id}/revoke (idempotent) + Dashboard-Button — vorhanden. Walletseitig kommt der Widerruf mit der Mandatskarte in AP6. Die S-App ist im PoC durch das Bank-Dashboard vertreten.
4 Settlement ist eine SCT-Inst-Transaktion PaymentRail.sctInst ist Default (statt Wero aus dem Brief). Jeder PaymentReceipt traegt settlement { instrument: sct_inst, scheme, end_to_end_id, status: simulated }.

Abweichung vom Brief: Dort stand Wero als Rail. Wero bleibt als Wert erhalten, ist aber nicht mehr Default.

Entscheidungen in AP4

Sicherheit