wiki-vault/Sources/Dev/2026-06-25-android-client-split-surface-migration-map-contract-investigatio.md

6.2 KiB
Raw Blame History

created path project tags type
2026-06-25 Sources/Dev petal-dispatch-phase-4-2-ingress-split
dispatch
authentik
cloudflare
lovebug
mcp
session-notes

Planning + recon session (no code changes). Mapped how the petal-dispatch-android client must change to ride the split surface (ingress LXC) instead of the fused service, and verified the app↔ingress API contract against the live phase-4.2/ingress-split branch (4002b17). Travis added a major direction change mid-session: at cutover the CF Access front-end is dropped in favor of his self-hosted Authentik layer.

Outcome

The app-side change to reach the split is smaller than feared. Verified findings:

  • Transport is already split-compatible. SseConsumer already uses ?from_seq=N with a live-only first connect and ignores Last-Event-ID — exactly the ingress contract (harness_routes reuse). Host coupling is centralized in one constant (DispatchHttp.BASE_URL).
  • Contract investigation — four suspected gaps resolved (source-authoritative; route table grepped from src/petal_ingress/app.py):
    1. POST /api/tasks/{session_id}/actionMISSING. The one real functional gap. No /action handler exists in petal_ingress. The app's worker pause/stop/delete (DispatchApi.taskAction) has no backend. Matches thread-605's deferred "small admin endpoint, not tonight" note. Fix = DB air-gap pattern (ingress writes action to a queue, executor consumes).
    2. GET /api/threads?hide_archived=false — param ignored, non-breaking. Split derives threads from thread_state BIGSERIAL (no archived concept). "Show archived" toggle is inert, not an error.
    3. POST /api/threads/unassigned/activatePRESENT (app.py:523). Not a gap.
    4. dispatch_session_id in POST /api/chat — hardcoded null (app.py:658), but the app never reads it (ChatProjection.sendOne branches on targetThreadId). Benign.
  • Design decision (Travis): the dispatch host becomes the app's first-ever setting, which dissolves the earlier Path A (DNS repoint) vs Path B (point app at split) fork.
  • Current-state drift caught: loopback curls to 127.0.0.1:8001 returned 401 on /api/* — the ingress app itself is auth-gating now (CF-oriented middleware live), NOT the _auth_noop the checkpoints described.

Topics Covered

  • Recon of plan 300 (security rationale: ingress/executor split, DB as air-gap) + thread 605 (Phases 14 complete, parallel-run live, Phase 5 cutover gated on Travis).
  • Read the Android client end-to-end: DispatchHttp, DispatchApi, SseConsumer, AuthManager, AndroidManifest.
  • Live + source contract verification against the running ingress on this host.
  • The Authentik-replaces-CF-Access cascade and its (simplifying) effect on AuthManager.

Key Learnings

  • The URL-setting dissolves the host fork but NOT the auth/manifest coupling. A runtime BASE_URL cleanly drives REST/SSE and derives REDIRECT_URI + RESOURCE. But Android App Link <data android:host> is compile-time — autoVerify links are fixed in the APK and verified at install. So auth-capable hosts remain a bounded, pre-declared set. Recommended shape: make BASE_URL the setting; declare both dispatch.herbylab.dev and split.herbylab.dev as App Links (each origin serves assetlinks.json); register both redirect URIs at DCR (currently buildDcrRequestBody sends a single-element array). Host change ⇒ re-auth (token is audience-scoped) + SSE reconnect (SseConsumer.restart() exists).
  • Dropping CF Access for Authentik DELETES app code rather than adding it. Most of AuthManager's complexity exists only to paper over CF's non-standard OAuth: the hand-rolled parseDiscoveryConfig (CF omits jwks_uri), the runtime DCR path (ensureClientId/buildDcrRequestBody/parseDcrClientId), the RFC 8707 resource indicator, and the oauth: bearer-prefix uncertainty. Authentik is a full OIDC provider → revert to AppAuth standard discovery, ship a static pre-registered client_id, drop the resource indicator (also removes one host-coupled auth constant). Unchanged: auth-code+PKCE flow, https redirect, manifest App Link + assetlinks, the Chromium Custom-Tab handoff workaround. Server side: ingress middleware swaps CF-assertion validation → Authentik JWT validation (issuer/audience/jwks).
  • Keep IdP discovery/client_id pinned in config, NOT free-text user settings — only the dispatch host should be user-facing. Pointing auth discovery at an arbitrary URL is a security smell.
  • Code answers "what is"; vault/trellis answer "what we decided and why." (Self-correction from earlier in session: should run list_projects/list_threads scoped to a subsystem before reconstructing intent from source.)

Follow-ons

  • OPEN DECISION (blocks sequencing the auth work): Authentik flow topology — does the app do auth-code+PKCE directly with Authentik (keeps a slimmed AuthManager), or does Authentik sit as a Traefik forward-auth outpost and the app rides an edge-managed session (removes most of AuthManager)? This fork decides how much auth code survives.
  • Build POST /api/tasks/{session_id}/action on ingress (DB air-gap queue → executor consumer) to restore worker lifecycle.
  • Add the dispatch-host setting (app's first settings screen): BASE_URL const → runtime read; DispatchApi.BASE dynamic; derive REDIRECT_URI.
  • Manifest: declare both hosts as App Links; serve assetlinks.json on each origin with the app's signing fingerprint.
  • Rewrite auth for Authentik: static client_id, standard AppAuth OIDC discovery, drop RFC 8707 resource, simplify token handling.
  • Server: swap ingress in-app middleware from CF Access assertion → Authentik JWT validation (or forward-auth header trust), depending on topology decision above.
  • Confirm before public exposure: ingress CF→Authentik cutover removes the _auth_noop history entirely; the loopback 401 shows in-app gating is already active.
  • Optional context-source improvement: there is almost certainly a dedicated vault plan + trellis thread for the Android client (it has phase-4.1-cfauth versioning, docs/RECON.md, docs/HANDOFF-4b.md) — locate it before the build to avoid re-deriving intent. Thread 588 (android + web build, long) is the likely home.