--- 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 1–4 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 `` 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.