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.mdund 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:
WebViewActivityhostet 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/WebViewActivityals Host (analog HAs Trennung Activity = Lifecycle vs. Compose = UI inWebViewActivity.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:appauthfü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(analogHAWebViewClient.kt): Fehlertypisierung (SSL,AUTHENTICATION,TIMEOUT), gefiltert nach Host, damit Subresource-Fehler keine Fehlerseite triggern. WebChromeClientfüronShowFileChooser(Datei-Upload/Kamera),onPermissionRequest(Mikrofon-Permission für WebRTC/getUserMediader 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 (ServerSessionManagerantwortet aufgetExternalAuth). - 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 (V1addJavascriptInterface+ V2postMessage/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/showsowiesaveBlobfü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
personalAgentNativevorhanden ist, überspringt die SPA den oidc-client-Redirect-Login und ruft stattdessenpersonalAgentNative.getExternalAuth()→ speichert den Token im oidc-Store / setzt den Bearer für HTTP+WS. (Dies ist HAsgetExternalAuth-Vertrag, übertragen auf oidc-client.) Bei Ablauf ruft die SPA erneutgetExternalAuth(force_refresh). - Capability-gating:
config/getsteuert, 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 ausconfig/getund 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:
onShowFileChooser→ActivityResultContracts(Datei/Kamera). Mikrofon/Kamera-Permissions überonPermissionRequest. - Download-Split (1:1 HA):
blob:→ in Data-URI konvertieren → lokal speichern;http(s):→ AndroidsDownloadManager.Requestmit injiziertemAuthorization: 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
- 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. - OIDC Auth-Code-Flow + PKCE via AppAuth gegen Keycloak: Discovery via
.well-known/openid-configuration, Redirect-URIdev.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. - Token-Exchange am Keycloak-Token-Endpoint (
grant_type=authorization_code) — Pendant zu HAsAuthenticationService.getToken(). - 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_tokenin 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 HAsensureValidSession): vor Ablauf viagrant_type=refresh_tokengegen 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 HAsretrieveExternalAuthentication(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 viaSec-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_channelan 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 einemdelivery_idim 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(analogWakeWordListener.kt): AudioRecord-Lifecycle, 2-Sekunden-Cooldown gegen Re-Trigger (HAsPOST_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, optionallocation. - 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 aufEntityWriter.upsert(EntityRecord)mitdomain="device"mappt. Reiche Attribute (Spannung/Health/BSSID/Signal/lat-lon-accuracy) inEntity.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 alsWKWebViewmit 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, NotificationActionReceiver → POST /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.
-
Push-Token-Registry (ausgeliefert) -
services/apiapi/routers/push.py, Modelldb/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 separatesdevice_id/flavor/app_version/enabled-Feld - dertokenist 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 (FCMUNREGISTERED/404/410) werden beim Senden gelöscht; ein täglicher Maintenance-Job (worker/push_maintenance_workflow.py) reapt seittoken_ttl_days(default 90) stille Tokens.
- Tabelle
-
FCM-Sender (ausgeliefert) -
realtime/fcm.pyFcmSender(process-global, viaconfigure_fcmin API-Lifespan und Worker-Startup): bei jedempublish_user_event-Fanout prüftmaybe_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 überGET /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_replydraft_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).
-
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_commandumgeht das Gate (funktionale Geräte-Aktuation, kein Notification).
- Per-Event-Toggle + Quiet-Hours), der WS-Publish wird nie unterdrückt.
- 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
-
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}/approvebzw./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.
- Es gibt kein
-
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 inphone_states(gelesen vonread_phone_status) und projiziert in das Entity-Subsystem über diephone-Integration (EntityStateRecord, eine Entity pro Sensor, gruppiert perEntityStateDevice). 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ürphone_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}.
-
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_commandpubliziert einenphone_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.
- Der Agent kann das Companion-Telefon nativ steuern (DnD, Klingelton, Taschenlampe, Media,
Sprechen/
-
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 denpersonal-agent://-Intent in die passende SPA-URL/navigate-Bridge-Message.
-
OIDC-Client-Registrierung in Keycloak (ausgeliefert)
- Public-Client (PKCE) “Personal Agent Android app” mit Redirect-URI
${ANDROID_REDIRECT_SCHEME}:/oauth/callback(defaultdev.luebke.personalagent) + Logout-Redirect, im Realm-as-code (keycloak/realm-personal-agent.json, Scheme aus den Helm-Values).
- Public-Client (PKCE) “Personal Agent Android app” mit Redirect-URI
-
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
- 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;domStoragefür oidc-client zwingend aktiv. 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.- FCM vs. FOSS: Beide über Flavors (HAs full/minimal). Risiko: Doppelzustellung →
delivery_id/Ack (HAsackNotification-Muster). FOSS-WS im Hintergrund kostet Akku/erfordert Foreground-Service-Notification (Android-Policy) — als „immer/bei WLAN/nie"-Setting konfigurierbar wie HA. - 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.
- Token-Sicherheit: EncryptedSharedPreferences (Keystore) statt HAs filesystem-encryptem Room. Risiko: Refresh-Token-Lifetime/Rotation in Keycloak korrekt konfigurieren; bei Diebstahl Logout via
end_session. - 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). - 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.
- 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 TrennungFullApplicationModulevs.MinimalApplicationModule). - 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. - 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.