Documentation

Implementierungsplan: personal-agent Companion App (Android-first)

Update 2026-06: Die Backend-Seite dieses Plans ist grösstenteils ausgeliefert (Push-Token-Registry, FCM-Wake-and-Fetch, Phone-als-Entity, Phone-Commands, SPA-Bridge V1+V2), allerdings mit anderen konkreten Endpunkten/Tabellen als hier ursprünglich skizziert. Die unten beschriebenen API-Namen wurden an den real implementierten Stand angeglichen; das Phasen-/Architektur-Narrativ bleibt als historische Blaupause erhalten. Der konkrete Arbeitspaket-Plan für den weiteren Ausbau - nach Quellcode-Abgleich mit der HA-Android-App und Verifikation der Backend-Verträge - liegt in personal-agent-android-expansion-plan.md und ersetzt die Phasen P2-P7 unten.

Prinzip (von Home Assistant übernommen): Eine native Android-Shell wrappt die bestehende personal-agent-Quasar-SPA in einer WebView und ergänzt sie um native Integrationen (Push, Voice/Wake-Word, Sensoren, Deep-Links). Das ist exakt das Modell der HA-App: WebViewActivity hostet das HA-Frontend, ein External Bus (JS-Bridge) verbindet Web ↔ Native, der Rest (Sensoren, Notifications, Voice) lebt nativ. Wir kopieren die Seams, ersetzen aber HA-spezifische Mechanismen (OAuth/IndieAuth, Multi-Server) durch personal-agent-Äquivalente (Keycloak/OIDC, Multi-Org).


0. Zielbild & Leitentscheidungen

Thema HA-Muster (Quelle) personal-agent-Entscheidung
Shell Kotlin + WebView in Compose-Activity (WebViewActivity.kt, WebViewContentScreen.kt) Identisch: Kotlin, Jetpack Compose, eine WebView-Host-Activity, die https://pa.luebke.dev lädt
Web vs. Native UI/State/Logik bleiben im Frontend; nur OS-Capabilities nativ Identisch: Quasar-SPA bleibt vollständig die UI. Nativ nur: Auth-Storage, Push, Voice-Capture, Sensoren, Datei/Share, Deep-Links
Bridge External Bus V1 (addJavascriptInterface) + V2 (WebViewCompat.addWebMessageListener) V1 zuerst (addJavascriptInterface("personalAgentNative")), V2 als späterer Hardening-Schritt
Auth OAuth authorization_code, WebView-Redirect-Intercept, Token in Room+Keystore AppAuth/OIDC gegen Keycloak, Token in EncryptedSharedPreferences, Handoff in die SPA via Bridge
Multi-* Multi-Server (ServerManager, serverId) personal-agent = ein Server, mehrere Orgs. Kein Server-Picker; stattdessen Org-Auswahl bleibt in der SPA (X-Personal-Agent-Org). Multi-Server bewusst out-of-scope (siehe Risiken)
Push FCM (full) + WebSocket-Fallback (minimal/FOSS) (FirebaseCloudMessagingService.kt, WebsocketManager.kt) Identisches Dual-Path: FCM-Flavor + FOSS-Flavor über personal-agent-Control-WS /api/v1/ws + user_events_channel
Build Multi-Modul + Convention-Integrations + Full/Minimal-Flavors (build-logic/) Übernommen, verschlankt (3 Module statt 8)

Cross-Platform: Android zuerst. iOS später als analoge WKWebView-Shell (gleiche Bridge-Verträge, gleiche Server-Endpunkte). Wear/Automotive ausdrücklich Post-MVP.


1. Prinzip & Architektur

1.1 Was bleibt Web, was wird nativ

Bleibt Web (Quasar-SPA, unverändert bedient): komplette Chat-UI, Streaming-Rendering (SSE), Markstream, Settings, Entities/Workflows-UI, Drafts/Comms, Computer Services-UI, Org-Wahl. Die SPA ist bereits responsive — das ist die Voraussetzung, die HA ebenfalls nutzt (Frontend ist mobile-optimiert und wird 1:1 in die WebView geladen).

Wird nativ:

  • WebView-Hosting + TLS/Cookie/Header-Handling (vgl. TLSWebViewClient.kt, HAWebViewClient.kt)
  • OIDC-Login-Flow + verschlüsselte Token-Persistenz + Token-Handoff in die SPA (vgl. ServerSessionManager.kt / getExternalAuth)
  • Push-Empfang (FCM + WS-Fallback) und Rendering als native Notifications inkl. Action-Buttons (vgl. MessagingManager.kt, NotificationActionReceiver.kt)
  • Voice-Capture (Mikrofon, optionales On-Device-Wake-Word) (vgl. microwakeword/, WakeWordListener.kt, AssistViewModelBase.kt)
  • Phone-als-Entity-Sensoren (opt-in) (vgl. SensorManager.kt, SensorWorker.kt)
  • Datei-Upload/Kamera/Download/Share/Deep-Links (vgl. External-Bus File-Handling in WebViewActivity.kt)

1.2 Empfohlener Stack

  • Sprache/UI: Kotlin, Jetpack Compose (Material3), eine MainActivity/WebViewActivity als Host (analog HAs Trennung Activity = Lifecycle vs. Compose = UI in WebViewActivity.kt + WebViewContentScreen.kt).
  • DI: Hilt (wie HA), aber nur ein SingletonComponent-Graph. Kein Room nötig im MVP (HA braucht Room für Sensor/Notification-History; personal-agent hält Zustand serverseitig). Persistenz im MVP = EncryptedSharedPreferences (Tokens, Push-Token, Settings). Room nur einführen, falls Sensor-Historie/Outbox (Phase 6) es verlangt.
  • Networking: OkHttp + Retrofit (wie HA) — nur für native Calls (OIDC-Token-Endpoint, personal-agent Push-Register, FCM-loses WS). Die SPA macht ihre eigenen HTTP-Calls in der WebView.
  • WS: OkHttp-WebSocket für den FOSS-Push-Pfad (analog WebsocketManager.kt).
  • AppAuth: net.openid:appauth für den OIDC-Code-Flow gegen Keycloak.
  • Coroutines/Flow für Pipeline-/Event-Streams (wie HA).

1.3 Modul-Layout (verschlankt aus HAs build-logic-Ansatz)

HA hat 8 Module; personal-agent braucht im MVP 3, mit der gleichen Convention-Integration-Idee (AndroidApplicationConventionIntegration.kt, AndroidFullMinimalFlavorConventionIntegration.kt), um Flavor-Boilerplate zentral zu halten:

personal-agent-android/
  build-logic/                      # Convention-Integrations (Application, Flavor full/minimal)
  :app                              # WebView-Host, Bridge, Onboarding, Notifications-Rendering, Voice-UI
      src/main                      # gemeinsamer Code
      src/full     (FCM/GMS)        # FirebaseMessagingService, Play-Services
      src/minimal  (FOSS)           # MinimalApplicationModule: leerer Push-Token, WS-Push always-on
  :common                           # Auth (AppAuth), Bridge-Verträge, OkHttp/Retrofit, EncryptedPrefs, WS-Client
  :wakeword         (später)        # optional, C++/TFLite-Micro analog HAs :microwakeword

Flavors full vs minimal sind direkt aus HA übernommen (app/src/full vs app/src/minimal, FullApplicationModule.kt vs MinimalApplicationModule.kt): full = Google Play, mit FCM/GMS; minimal = F-Droid/FOSS, ohne GMS, Push ausschließlich über Personal Agents Control-WS. Beide teilen src/main.


2. WebView-Host + Native↔JS-Bridge

2.1 WebView-Host

Eine Compose-Activity lädt PERSONAL_AGENT_BASE_URL (https://pa.luebke.dev). Wir übernehmen aus WebViewActivity.kt:

  • Activity hält Lifecycle/Fehlerbehandlung, Compose rendert (WebViewContentScreen.kt).
  • Custom WebViewClient (analog HAWebViewClient.kt): Fehlertypisierung (SSL, AUTHENTICATION, TIMEOUT), gefiltert nach Host, damit Subresource-Fehler keine Fehlerseite triggern.
  • WebChromeClient für onShowFileChooser (Datei-Upload/Kamera), onPermissionRequest (Mikrofon-Permission für WebRTC/getUserMedia der SPA — wichtig, da Personal Agents STT im Browser läuft).
  • Cookie/Header-Injection statt mTLS: personal-agent nutzt Keycloak-Bearer-JWT, keine Client-Zertifikate. Wir injizieren den Bearer-Token nicht pauschal in jeden Request (die SPA/oidc-client macht das selbst), sondern liefern ihn on demand über die Bridge (getExternalAuth, s. u.) — exakt HAs Modell (ServerSessionManager antwortet auf getExternalAuth).
  • JS-Settings: domStorageEnabled = true (oidc-client braucht localStorage/sessionStorage), javaScriptEnabled, mediaPlaybackRequiresUserGesture = false (für TTS-Autoplay).

2.2 Bridge-Vertrag (External Bus, V1)

Native exponiert window.personalAgentNative via addJavascriptInterface (HAs externalApp-Muster, FrontendJsBridge.kt). Nachrichten folgen HAs Discriminator-Design (id + type + command/result, ExternalBusMessage.kt): die SPA sendet {id, type, payload}, Native antwortet asynchron via evaluateJavascript-Callback bzw. (V2) personalAgentNative.onmessage.

Real implementiert (src/services/nativeBridge.ts): die SPA unterstützt beide Transporte (V1 addJavascriptInterface + V2 postMessage/onmessage). Über die untenstehende Tabelle hinaus existieren bereits weitere Commands: app/ready, notification/show, location/request-permission, health/request-permission (Health-Metriken), app-settings/get/set, onboarding/show sowie saveBlob für Downloads.

SPA → Native (Commands):

Command Zweck HA-Analogon
config/get Native meldet Capabilities: hasMic, hasNotifications, hasWakeWord, hasDeviceSensors, pushType (fcm/ws), appVersion, safeArea HAs config/get (has-mic, has-play-stream, …)
getExternalAuth Native liefert gültigen Access-Token {access_token, expires_in} (Refresh falls abgelaufen) getExternalAuth / ServerSessionManager
revokeExternalAuth Logout, Token löschen revokeExternalAuth
theme-update SPA meldet aktuelle Theme-Farbe → Native färbt Statusleiste/Splash HA theme-color extraction
haptic Vibrationsfeedback HA haptic
notifications/request-permission nativen Permission-Dialog (Android 13+ POST_NOTIFICATIONS) zeigen HA notifications-permission
push/register Push-Token-Registrierung anstoßen (s. §4) HA FCM-Registration
voice/show Assist/Voice-Chat öffnen (Push-to-Talk) HA assist/show
share Text/URL über Android-Share-Sheet teilen — (neu)
open-external URL im externen Browser/CustomTab öffnen (z. B. Keycloak-Account, externe Links) HA external-URL handling
download Datei-Download (blob/http) (s. §2.4) HA download split
navigate (Native→SPA) Deep-Link-Navigation in SPA-Route HA NavigateTo

Aktionen, die in den user_events_channel zurückspielen (personal-agent/*-Custom-Messages, analog HAs custom external-bus-Events): personal-agent/tool-approval (approve/deny), personal-agent/agent-answer (Antwort auf ask_user), personal-agent/draft-action (approve/send). Diese werden nicht über die Bridge, sondern direkt von der SPA via bestehende WS/HTTP-APIs ausgelöst — die App muss nur die Deep-Links liefern, damit ein Notification-Tap die SPA in den richtigen Zustand bringt (s. §4.4).

2.3 SPA-seitige Ergänzungen (klein, additiv)

Die Quasar-SPA bekommt ein dünnes Bridge-Modul (real: src/services/nativeBridge.ts, geladen via Boot src/boot/native.ts), das prüft, ob window.personalAgentNative existiert:

  • Auth-Handoff: Wenn personalAgentNative vorhanden ist, überspringt die SPA den oidc-client-Redirect-Login und ruft stattdessen personalAgentNative.getExternalAuth() → speichert den Token im oidc-Store / setzt den Bearer für HTTP+WS. (Dies ist HAs getExternalAuth-Vertrag, übertragen auf oidc-client.) Bei Ablauf ruft die SPA erneut getExternalAuth(force_refresh).
  • Capability-gating: config/get steuert, ob native Mic-/Wake-Word-Buttons sichtbar sind (statt Browser-getUserMedia).
  • Notifications/Permissions/Haptics/Share/Theme: dünne Wrapper, die in der reinen Browser-Variante No-Ops/Web-APIs bleiben (Progressive Enhancement).
  • Safe-Area: SPA liest safeArea-Insets aus config/get und setzt CSS-Variablen (Notch/Statusbar).

Diese SPA-Additions sind bewusst minimal und rein additiv — die SPA läuft weiterhin unverändert im normalen Browser/PWA (window.personalAgentNative === undefined).

2.4 Datei-Upload / Download / Kamera

Übernommen aus HAs WebViewActivity-File-Handling:

  • Upload: onShowFileChooserActivityResultContracts (Datei/Kamera). Mikrofon/Kamera-Permissions über onPermissionRequest.
  • Download-Split (1:1 HA): blob: → in Data-URI konvertieren → lokal speichern; http(s): → Androids DownloadManager.Request mit injiziertem Authorization: Bearer-Header (Token aus dem Auth-Layer).

3. Onboarding + Auth (Keycloak/OIDC)

HA nutzt OAuth authorization_code mit WebView-Redirect-Intercept (ConnectionViewModel.kt, HAWebViewClient.kt fängt homeassistant://auth-callback). personal-agent übernimmt das Muster, ersetzt aber den Identity-Provider durch Keycloak und nutzt AppAuth (robuster als handgebaute Redirect-Interception).

3.1 Flow

  1. Kein Server-Picker (anders als HAs ServerDiscoveryViewModel/NSD): personal-agent hat genau einen Server (PERSONAL_AGENT_BASE_URL). Optional ein einzelnes „Server-URL"-Feld für Self-Hosting, default vorbelegt.
  2. OIDC Auth-Code-Flow + PKCE via AppAuth gegen Keycloak: Discovery via .well-known/openid-configuration, Redirect-URI dev.luebke.personalagent:/oauth/callback (Custom-Scheme, im Realm via ${ANDROID_REDIRECT_SCHEME} templatisiert; Custom Tab, nicht eingebettete WebView - Store-/Security-Best-Practice). Entspricht funktional HAs /auth/authorize + Redirect-Intercept, nur mit Keycloak-Endpunkten.
  3. Token-Exchange am Keycloak-Token-Endpoint (grant_type=authorization_code) — Pendant zu HAs AuthenticationService.getToken().
  4. Provisioning: Nach Login ruft die App (oder die SPA nach Handoff) GET /me → personal-agent lazy-provisioniert den User (bestehendes Verhalten). Org-Auswahl bleibt in der SPA (X-Personal-Agent-Org).

3.2 Token-Storage & Refresh

  • Storage: access_token, refresh_token, expiry, id_token in EncryptedSharedPreferences (Jetpack Security, Keystore-gebackt). HA legt Tokens in Room ab, das nur per Filesystem-Verschlüsselung geschützt ist — wir wählen die explizit verschlüsselte Variante.
  • Refresh: AuthRepository.ensureValidSession() (Pendant zu HAs ensureValidSession): vor Ablauf via grant_type=refresh_token gegen Keycloak erneuern. Bei Refresh-Fail → Re-Login-Flow.
  • Handoff in WebView: Beim getExternalAuth-Call der SPA liefert die App {access_token, expires_in} (Refresh bei Bedarf) — exakt HAs retrieveExternalAuthentication(force_refresh)-Vertrag.

3.3 Logout

revokeExternalAuth → Keycloak end_session_endpoint (Custom Tab) + lokale Tokens löschen + WebView-Cookies/Storage leeren.


4. Push (Dual-Path: FCM + FOSS-WS)

Wir übernehmen HAs Dual-Path wörtlich (FirebaseCloudMessagingService.kt für FCM, WebsocketManager.kt als FOSS-Fallback), gemappt auf Personal Agents bestehenden user_events_channel.

4.1 Path A — FCM (full-Flavor, Play-Store)

  • FirebaseMessagingService.onNewToken() → registriert das Token bei personal-agent (s. §9 Endpoint), persistiert lokal (PREF_PUSH_TOKEN). Re-Registration bei Fehler — HAs Resilienz-Muster (404/410 → registerDevice() neu).
  • Eingehende FCM-Data-Messages → flaches Key-Value-Schema (§4.3) → NotificationRenderer.handle(dataMap).

4.2 Path B — FOSS WS-Fallback (minimal-Flavor, kein GMS)

  • Statt FCM: ein Foreground-/WorkManager-gehaltener WebSocket auf Personal Agents /api/v1/ws (Bearer via Sec-WebSocket-Protocol-Subprotocol — identisch zu Personal Agents Web-Client und zu HAs WS-Auth).
  • personal-agent pusht ohnehin schon per-User-Events über user_events_channel an alle Fenster (Chat-Title-Fanout, Agent-Fragen, Tool-Approval-Cards, Background-Run-Resumptions, Drafts). Der FOSS-Client abonniert denselben Kanal und rendert die Events als native Notifications.
  • Konfigurierbarer Schedule wie HA (websocketSetting: „immer", „bei Bildschirm an", „zuhause im WLAN", „nie"). FOSS-Default = always-on; full-Default = WS aus (FCM bevorzugt). Direkt aus HA übernommen.
  • Ack: Falls personal-agent für mobile Zustellung ein Ack braucht (Vermeidung von Doppelzustellung über mehrere Geräte), spiegeln wir HAs ackNotification(confirmId)-Muster mit einem delivery_id im Event.

4.3 Notification-Payload-Schema (HA-kompatibel, personal-agent-spezifisch)

Wir standardisieren auf HAs flaches Schema (ein Renderer für beide Pfade):

{
  "title": "Agent fragt nach",
  "message": "Datei löschen bestätigen?",
  "tag": "run:<uuid7>",
  "group": "agent-questions",
  "channel": "agent_questions",
  "importance": "high",
  "deeplink": "personal-agent://chat/<chat_id>?run=<run_id>",
  "event_type": "agent_question",
  "actions": [
    {"action": "approve", "title": "Erlauben"},
    {"action": "deny", "title": "Ablehnen", "destructive": true},
    {"action": "reply", "title": "Antworten", "behavior": "textinput"}
  ]
}

4.4 Mapping Personal Agents Events → Notifications

personal-agent user_events_channel-Event Notification Actions Deep-Link
chat_reply (Antwort fertig; chat_title ist WS-only) Info/Normal „Öffnen" personal-agent://chat/<id>
agent_question (ask_user) High Optionen als Buttons + ggf. textinput personal-agent://chat/<id>?run=<rid>&q=<qid>
tool_approval (security-mode) High „Erlauben"/„Ablehnen" personal-agent://chat/<id>?approve=<call_id>
draft_pending (Comms/HITL) Normal „Ansehen"/„Senden" personal-agent://drafts/<draft_id>
phone_command (Geräte-Aktuation) / Background-Run resumed High/Default - personal-agent://chat/<id>

Real implementierte push-würdige Frame-Typen (realtime/fcm.py): push_notification, agent_question, tool_approval, phone_command (high), chat_reply, draft_pending (normal). Frames ausserhalb dieser Liste (z. B. chat_title, chat_run, entity_changed) bleiben WS-only und wecken das Telefon nicht.

Action-Callback (HAs NotificationActionReceiver-Muster): Tap auf „Erlauben"/„Ablehnen"/Reply → NotificationActionReceiver.onReceive() → POST an personal-agent (s. §9 POST /api/v1/mobile/actions), das die wartende Temporal-Workflow-Resumption auslöst — analog HAs fireEvent("mobile_app_notification_action", …). Für textinput/reply hängen wir wie HA ein RemoteInput an. Tap auf den Body → Deep-Link öffnet die App auf der richtigen SPA-Route (§2.2 navigate).


5. Voice & Wake-Word

personal-agent hat bereits STT (Whisper/faster-whisper) + streaming TTS (Piper) über die speaches-Container und OpenAI-kompatible Audio-Models. Im WebView-Modell läuft Voice grundsätzlich weiter in der SPA (getUserMedia + bestehende voice.ts-Player). Die App liefert nur das, was der Browser nicht gut kann:

5.1 Push-to-Talk (MVP-tauglich, P4)

  • Nativer Mic-Button / Notification-Action / App-Shortcut → voice/show öffnet die SPA-Voice-Route. Mikrofon-Permission wird nativ erteilt (onPermissionRequest), Capture + STT/TTS laufen in der SPA über die bestehenden Endpunkte. Minimaler nativer Aufwand, voller Reuse von Personal Agents Voice-Stack.

5.2 On-Device Wake-Word (optional, später, :wakeword-Modul)

Direkt an HAs microwakeword/ orientiert:

  • TFLite-Micro-Modell(e) (~60–100 KB, z. B. deutsches Keyword), 16 kHz Mono-PCM, JNI-Wrapper analog MicroWakeWord.kt (processAudio → bool).
  • WakeWordListener (analog WakeWordListener.kt): AudioRecord-Lifecycle, 2-Sekunden-Cooldown gegen Re-Trigger (HAs POST_DETECTION_COOLDOWN_CHUNKS).
  • Foreground-Service (analog AssistVoiceInteractionService.kt): lauscht im Hintergrund, broadcastet bei Detektion → öffnet die Voice-Route (voice/show).
  • Dedup auch serverseitig (HAs duplicate_wake_up-Muster): personal-agent verwirft Re-Trigger innerhalb eines Cooldowns pro User.

Wake-Word ist bewusst Post-MVP und optional/opt-in (Akku, Privacy, Store-Review für Always-Listening).


6. Phone-als-Entity (optional, opt-in)

Aus HAs SensorManager.kt/SensorWorker.kt/BatterySensorManager.kt adaptiert, gemappt auf Personal Agents bestehendes Entity-System (Integrations + EntityWriter.upsert, Automation-Triggers):

  • Sensor-Abstraktion (analog HAs SensorManager-Interface): requiredPermissions(), requestSensorUpdate(), getAvailableSensors(). MVP-Sensoren: battery_level, battery_state, charging, network_type, wifi_connection, screen_state, optional location.
  • Hybrid-Collection wie HA: BroadcastReceiver (z. B. ACTION_BATTERY_CHANGED, WIFI_STATE_CHANGED) + WorkManager-Periodik (15 min, NetworkType.CONNECTED).
  • Push in personal_agent: statt HAs /api/integration_device/update_sensor_states → ein mobile Entity-Ingest-Endpoint (s. §9), der serverseitig auf EntityWriter.upsert(EntityRecord) mit domain="device" mappt. Reiche Attribute (Spannung/Health/BSSID/Signal/lat-lon-accuracy) in Entity.attributes (JSONB) — wie im Dossier skizziert.
  • Dedup/Permission-Gating wie HA: lastSentState/lastSentIcon, enabled-Flag pro Sensor, checkPermission() gated. Opt-in pro Sensor im Settings-Tab.
  • Automations: greifen über bestehende Trigger (event = entity.state_changed, domain=device, numerische Conditions, z. B. Akku < 20 %).

7. Extra-Surfaces (später)

Aus HAs Surfaces (widgets/, qs/, wear/, AssistShortcutActivity.kt) priorisiert:

  • Share-to-Personal-Agent (Android Share-Target): Text/URL/Bild aus anderen Apps → öffnet personal-agent mit vorbefülltem Composer (personal-agent://share?...). Höchster Nutzen, geringster Aufwand → erste Extra-Surface.
  • App-Shortcuts / Quick-Settings-Tile: „Neuer Chat", „Voice starten" (analog HAs AssistShortcutActivity + Tile-Services).
  • Home-Screen-Widgets: „Main-Chat öffnen", „Letzte Drafts" (analog HAs Button/Template-Widgets).
  • Wear OS / iOS: eigene spätere Tracks; Wear analog HAs :wear-Modul (Voice-first), iOS als WKWebView mit identischem Bridge-Vertrag.

8. Phasenplan (jede Phase eigenständig auslieferbar)

P1 — WebView-Shell + OIDC + Basis-Push (MVP)

Inhalt: :app-WebView-Host (Compose), Custom WebViewClient (Fehlertypen, Host-Filter), :common mit AppAuth-OIDC gegen Keycloak + EncryptedSharedPreferences, Bridge V1 (personalAgentNative) mit config/get, getExternalAuth, revokeExternalAuth, theme-update, haptic, open-external, share. SPA-Bridge-Modul (bridge.ts) mit Auth-Handoff + Capability-Gating. Push: nur FOSS-WS-Pfad zuerst (kein FCM-Setup nötig) → schnellster Pfad zu echten Notifications über user_events_channel. Deep-Link-Routing (personal-agent://…). Verifikation: Login per Keycloak im Custom Tab; SPA lädt ohne zweiten Login (Handoff greift); WS-Push erzeugt native Notification bei Chat-Antwort; Deep-Link öffnet korrekte Chat-Route; Logout leert Tokens+Cookies. Manuell auf Gerät + Emulator (Android 13/14).

P2 — FCM-Flavor + Actionable Notifications

Inhalt: full-Flavor mit Firebase (FirebaseMessagingService), Push-Token-Registry-Endpoint (§9), NotificationRenderer für das flache Schema, Action-Buttons + RemoteInput/Reply, NotificationActionReceiverPOST /api/v1/mobile/actions. Mapping aller user_events_channel-Eventtypen (§4.4). minimal-Flavor behält WS-Pfad. Verifikation: Tool-Approval-Push mit „Erlauben/Ablehnen" → Tap resumed den Temporal-Workflow; Agent-Frage mit Reply-Textinput; Doppelzustellung (FCM + WS) wird über delivery_id/Ack vermieden; full vs minimal getrennt gebaut (Flavor-Convention-Integration).

P3 — Datei/Kamera/Download + Hardening

Inhalt: onShowFileChooser (Datei/Kamera), Download-Split (blob→Data-URI, http→DownloadManager mit Auth-Header), Safe-Area-Insets, Theme/Statusbar-Sync, Fehlerseiten/Retry, Bridge V2 (addWebMessageListener, Origin-Filter) als Härtung gegen addJavascriptInterface-Risiken. Verifikation: Datei-Upload im Chat; authentifizierter Download; Notch-Layout korrekt; Bridge funktioniert unter V1+V2.

P4 — Voice (Push-to-Talk)

Inhalt: nativer Mic-Permission-Flow, voice/show-Bridge-Command, App-Shortcut + QS-Tile „Voice". STT/TTS bleiben in der SPA (speaches). Verifikation: Push-to-Talk öffnet Voice-Route, Mikrofon-Permission, deutsche STT-Transkription + streaming TTS-Wiedergabe im WebView.

P5 — Phone-als-Entity (opt-in)

Inhalt: :common-SensorManager-Abstraktion, Battery/Network/Screen-Sensoren, BroadcastReceiver + WorkManager-Periodik, mobiler Entity-Ingest-Endpoint → EntityWriter.upsert, Settings-Tab zum Aktivieren pro Sensor. Verifikation: Akkustand erscheint als device-Entity in personal-agent; Automation „Akku < 20 %" feuert; deaktivierter Sensor sendet nicht.

P6 — Wake-Word + Extra-Surfaces

Inhalt: :wakeword-Modul (TFLite-Micro JNI, Cooldown), Foreground-Listener; Share-to-Personal-Agent, Widgets, Quick-Tiles. Verifikation: Wake-Word öffnet Voice; Re-Trigger-Cooldown greift; Share aus Fremd-App landet im Composer.

P7 (Track) — iOS + Wear

Eigene Shell(s) gegen denselben Bridge-Vertrag und dieselben personal-agent-Endpunkte.


9. Backend-Änderungen an personal-agent

Bewusst minimal-invasiv — wir bauen auf user_events_channel, Control-WS und Temporal auf.

Stand-Hinweis: Punkte 1-5 sind ausgeliefert, aber mit anderen konkreten Endpunkten/Tabellen als ursprünglich skizziert. Die folgenden Beschreibungen wurden an den realen Code angeglichen.

  1. Push-Token-Registry (ausgeliefert) - services/api api/routers/push.py, Modell db/models/push_token.py

    • Tabelle push_tokens (RLS, Owner = owner_sub): id (uuid7), owner_sub, org_id, platform (android/ios/web), push_type (fcm/ws), token (Push-Token, Unique), label (freitext, z. B. “Pixel 8”), last_seen_at, created_at/updated_at. Kein separates device_id/flavor/app_version/enabled-Feld - der token ist der Identitäts-Schlüssel.
    • POST /api/v1/push/tokens (register/upsert), DELETE /api/v1/push/tokens/{token} (Unregister), GET /api/v1/push/tokens (Liste, nur Last-4 des Tokens). Re-Registration-Resilienz: tote Tokens (FCM UNREGISTERED/404/410) werden beim Senden gelöscht; ein täglicher Maintenance-Job (worker/push_maintenance_workflow.py) reapt seit token_ttl_days (default 90) stille Tokens.
  2. FCM-Sender (ausgeliefert) - realtime/fcm.py

    • FcmSender (process-global, via configure_fcm in API-Lifespan und Worker-Startup): bei jedem publish_user_event-Fanout prüft maybe_send, ob der User aktive FCM-Tokens hat, und sendet best-effort. FCM HTTP v1, OAuth2-Token aus dem Service-Account selbst gemintet (PyJWT RS256 → token endpoint via httpx, gecacht), kein Google-SDK. WS-Geräte erhalten dasselbe Event über den bestehenden Control-WS-Push.
    • Wake-and-Fetch (Privacy): die FCM-Data-Message trägt nur {type, payload_id} (kein Inhalt). Der volle Frame liegt kurzlebig in Redis (push_payload-Key, TTL 3600 s) und wird von der App über GET /api/v1/push/payload/{payload_id} (owner-scoped) nachgeladen - so transitiert Benachrichtigungsinhalt nie über Google. Nur push-würdige Frame-Typen wecken das Telefon: push_notification, agent_question, tool_approval, phone_command (alle high), chat_reply
      • draft_pending (normal).
    • Konfiguration via PERSONAL_AGENT__PUSH__FCM_CREDENTIALS_FILE (Service-Account-JSON, konventionell aus /run/secrets) + PERSONAL_AGENT__PUSH__TOKEN_TTL_DAYS. FOSS-Deployments ohne FCM-Credentials fallen automatisch auf den WS-Only-Pfad zurück (Sender = No-Op).
  3. Ein Frame, ein Renderer (ausgeliefert)

    • Es gibt keinen separaten serverseitigen Payload-Mapper: der Frame reist sowohl über WS als auch (per Wake-and-Fetch) über FCM as-is; die App rendert WS- und FCM-Frames durch einen NotificationRenderer. Das per-User-Fanout (realtime/bus/user_events.py:publish_user_event) ist der einzige Funnel; die FCM-Spiegelung ist zusätzlich per Notify-Prefs gegated (Master-Toggle
      • Per-Event-Toggle + Quiet-Hours), der WS-Publish wird nie unterdrückt. phone_command umgeht das Gate (funktionale Geräte-Aktuation, kein Notification).
  4. Action-Callback (ausgeliefert, kein dedizierter Mobile-Endpoint)

    • Es gibt kein POST /api/v1/mobile/actions. Notification-Actions routen direkt auf die bestehenden Resumption-Endpunkte: Tool-Approval → POST /api/v1/approvals/{approval_id}/approve bzw. /reject; ask_user-Antwort → POST /api/v1/chats/{chat_id}/questions/{question_id}/answer; Draft-Send → Drafts-Router. Diese setzen die wartenden Temporal-Workflows wie gehabt fort.
  5. Phone-als-Entity / Sensor-Ingest (P5, ausgeliefert) - me.py + integrations/phone/

    • PUT /api/v1/me/phone/state (kompletter On-Device-Sensor-Snapshot, ~alle 15 min) → gespeichert verbatim in phone_states (gelesen von read_phone_status) und projiziert in das Entity-Subsystem über die phone-Integration (EntityStateRecord, eine Entity pro Sensor, gruppiert per EntityStateDevice). Damit sind Phone-Sensoren erst-klassig: search_entities, historisiert (Logbook), Automation-Trigger (z. B. “Akku < 15 %”). Push-only, keine Pull-Sync, kein Config-Flow (Eintrag wird beim ersten Report auto-provisioniert).
    • Begleitend: POST /api/v1/me/phone/command-result (App-Ack für phone_command), GET /api/v1/me/phones, GET /api/v1/me/phone/sensor-catalog, PATCH /api/v1/me/phones/{device_id}/sensors (Per-Sensor opt-in), DELETE /api/v1/me/phones/{device_id}.
  6. Phone-Commands (ausgeliefert, zusätzlich zum Originalplan) - agent/phone_toolset.py

    • Der Agent kann das Companion-Telefon nativ steuern (DnD, Klingelton, Taschenlampe, Media, Sprechen/speak, Benachrichtigen, Alarm setzen, navigieren). Tool → dispatch_phone_command publiziert einen phone_command-Frame und wartet kurz (~5 s) auf den Ausführungs-Ack des Telefons (Redis-Ack-Channel), damit der Agent das echte Ergebnis statt eines angenommenen Erfolgs erfährt.
  7. Deep-Link-Vertrag (Doku, evtl. minimale Route-Aliase)

    • personal-agent://chat/{id}?run=&q=&approve=, personal-agent://drafts/{id}, personal-agent://voice, personal-agent://share. Die SPA muss diese Query-Parameter beim Laden auswerten (kleine Router-Erweiterung) - die App übersetzt den personal-agent://-Intent in die passende SPA-URL/navigate-Bridge-Message.
  8. OIDC-Client-Registrierung in Keycloak (ausgeliefert)

    • Public-Client (PKCE) “Personal Agent Android app” mit Redirect-URI ${ANDROID_REDIRECT_SCHEME}:/oauth/callback (default dev.luebke.personalagent) + Logout-Redirect, im Realm-as-code (keycloak/realm-personal-agent.json, Scheme aus den Helm-Values).
  9. CORS/CSP/Origin: WebView lädt dieselbe Origin wie der Browser - keine CORS-Änderung. Falls Bridge V2 Origin-Filter nutzt, SPA-Origin whitelisten.


10. Risiken & Entscheidungen

  1. WebView-Session vs. native API: Entscheidung — WebView-first (HAs Prinzip). Die SPA bleibt die Wahrheit für UI/State; Native ergänzt nur Capabilities. Risiko: WebView-Inkonsistenzen (ältere System-WebViews). Mitigation: Host-gefilterte Fehlerbehandlung wie HAWebViewClient.kt, Min-SDK so wählen, dass moderne WebView verfügbar ist; domStorage für oidc-client zwingend aktiv.
  2. addJavascriptInterface (V1) Sicherheit: Risiko der JS-Exposition. Mitigation: nur auf der eigenen, TLS-gesicherten Origin aktiv; V2-Migration (addWebMessageListener + Origin-Filter) in P3 — exakt HAs Begründung für den V2-Bus.
  3. FCM vs. FOSS: Beide über Flavors (HAs full/minimal). Risiko: Doppelzustellung → delivery_id/Ack (HAs ackNotification-Muster). FOSS-WS im Hintergrund kostet Akku/erfordert Foreground-Service-Notification (Android-Policy) — als „immer/bei WLAN/nie"-Setting konfigurierbar wie HA.
  4. Auth: eingebettete WebView vs. Custom Tab: Custom Tab/AppAuth statt HAs eingebetteter Login-WebView — Store- und Security-Best-Practice (kein Credential-Sniffing, System-Cookie-Reuse). Trade-off: weniger UI-Kontrolle, aber robuster gegen Keycloak-Theme/Flows.
  5. Token-Sicherheit: EncryptedSharedPreferences (Keystore) statt HAs filesystem-encryptem Room. Risiko: Refresh-Token-Lifetime/Rotation in Keycloak korrekt konfigurieren; bei Diebstahl Logout via end_session.
  6. Multi-Server bewusst out-of-scope: anders als HA (ServerManager/serverId) hat personal-agent einen Server, mehrere Orgs. Spart erheblichen Komplexitätsaufwand. Risiko: Self-Hosting-User wollen mehrere Instanzen → später nachrüstbar (Repository-per-Server-Muster von HA ist die Blaupause).
  7. Wake-Word/Always-Listening: Privacy + Akku + Play-Store-Review (Foreground-Mic). Daher opt-in, Post-MVP, on-device (kein Cloud-Audio im Idle), Cooldown wie HA.
  8. Store-Policies: Play verlangt klare Begründung für Mic/Location/Foreground-Service + Datenschutzerklärung. minimal-Flavor für F-Droid muss GMS-frei bleiben (keine Firebase-Artefakte) — über Flavor-Convention-Integration erzwingen (HAs Trennung FullApplicationModule vs. MinimalApplicationModule).
  9. iOS-Parität: Bridge-Verträge und Endpunkte sind plattformneutral gehalten, damit die iOS-WKWebView-Shell ohne Backend-Änderungen andocken kann. Risiko: APNs statt FCM → push_provider="apns" ist im Registry-Schema bereits vorgesehen.
  10. SPA-Kopplung: Die App hängt am Bridge-Vertrag der SPA. Mitigation: Versionierung über config/get (appVersion/bridgeVersion), additive, abwärtskompatible SPA-Wrapper (No-Op im reinen Browser), damit Web-PWA und App aus demselben SPA-Build laufen.

Relevante HA-Referenzdateien (Blaupausen, die dieser Plan adaptiert): app/.../webview/WebViewActivity.kt, WebViewContentScreen.kt, util/HAWebViewClient.kt, util/TLSWebViewClient.kt, frontend/js/FrontendJsBridge.kt, webview/externalbus/ExternalBusMessage.kt, frontend/session/ServerSessionManager.kt, onboarding/connection/ConnectionViewModel.kt, common/.../authentication/impl/AuthenticationService.kt, notifications/FirebaseCloudMessagingService.kt (full), notifications/MessagingManager.kt, notifications/NotificationActionReceiver.kt, websocket/WebsocketManager.kt, app/src/minimal/.../MinimalApplicationModule.kt, common/.../sensors/SensorManager.kt + SensorWorker.kt, microwakeword/.../MicroWakeWord.kt, assist/wakeword/WakeWordListener.kt, build-logic/convention/.../AndroidFullMinimalFlavorConventionIntegration.kt.