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

52 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
created: '2026-06-25'
path: Sources/Dev
project: petal-dispatch-phase-4-2-ingress-split
tags:
- dispatch
- authentik
- cloudflare
- lovebug
- mcp
type: 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}/action`**MISSING. 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/activate`**PRESENT** (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.