> ## Documentation Index
> Fetch the complete documentation index at: https://docs.augustin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Peripherals & Display (k8s-native design)

The i5 screen, camera, mic, and the "show me X on the screen" reflex, designed as **k3s
workloads with device passthrough** — not host processes. This is the turn-79 peripherals
plan (host `cage`+Chromium kiosk, host `ustreamer` sense-daemon, SSE→MQTT push, headless
Home Assistant) re-drawn so the fleet's hard invariant holds: **every box = Debian + k3s
only; k3s is the sole workload runtime.** Design only — nothing here is built. The
prerequisites in the phasing section gate any build, and one of them (verifying i5's DRM
and video devices) could not be checked read-only from where this was written; that gap is
called out honestly rather than papered over.

## Why this document exists

Turn 79 sketched the peripherals subsystem as **host-native processes on i5** — a `cage`
(Wayland kiosk compositor) running Chromium against the physical display, and a `ustreamer`
daemon owning the camera/mic — explicitly *"NOT a pod."* That was a deliberate escape hatch:
owning a physical display and a `/dev/dri` seat from inside a container is genuinely harder
than running the same binary on the host. But it directly violates the rule the whole
rewrite converged on — **OS + k3s, nothing else** — and a single host daemon on i5 is exactly
the "homeless layer" the k3s migration existed to kill. Augustin chose to redesign it
k8s-native. This is that design, plus an honest account of where the host-native path was
actually easier, so the exception decision can be reopened with eyes open rather than by
drift.

The precedents already in the repo say this is *plausible*: [jellyfin](/media/jellyfin)
passes `/dev/dri` into a pod for QuickSync; the [downloads stack](#) runs a `NET_ADMIN` +
`/dev/net/tun` pod for VPN; beszel's DaemonSet takes `SYS_ADMIN`/`SYS_RAWIO` and hostPath
`/proc`+`/sys`. Device-passthrough pods are a solved, blessed pattern here. What's *new* is
DRM **master** (owning a display output, not just decoding on the render node) and a
Wayland seat inside a container. That's the whole ballgame, and section [Risks](#risks-where-k8s-native-is-genuinely-harder)
does not pretend otherwise.

## The GPU reality (read this before anything else)

The two GPUs are not interchangeable, and the display is on the *wrong* one for the existing
precedent:

* **i3** (`kind=storage`, 192.168.1.100) has the **working `/dev/dri`** — Intel HD 4400,
  `card0` + `renderD128`, used by jellyfin for QuickSync. It has **no attached screen** that
  matters.
* **i5** (`kind=compute`, 192.168.1.25) has the **physical screen** — the Dell Inspiron 5490
  All-in-One (model **W24C**). It has its own iGPU (10th-gen-class, QuickSync-capable per the
  turn-13 spec recon), but **no one has verified its `/dev/dri` / `/dev/video*` / `/dev/input`
  device layout** in the k3s era. The jellyfin passthrough is on i3 and tells us **nothing**
  about i5's device nodes.

The kiosk must render to **i5's physical panel**, so it must pin to **i5** and use **i5's**
DRM device and seat. You cannot borrow i3's GPU for this — DRM scanout is local to the card
wired to the panel. Every kiosk manifest below therefore carries
`nodeSelector: { kind: compute }` (i5) and mounts **i5's** device nodes, whose exact paths
are a **hard prerequisite to verify** (see [Phasing](#phasing)).

One piece of good news for free: the fleet's OS is **headless** (Debian + k3s, no desktop, no
display manager). That is exactly the condition a DRM-master container needs — nothing else on
i5 is holding the display. The "OS + k3s only" rule and "a pod owns the screen" are, on this
specific point, *aligned*: because there's no host `gdm`/`Xorg`/`cage` competing for
`/dev/dri/card*`, a pod can become DRM master cleanly. The invariant that makes this hard
elsewhere makes it *possible* here.

## Architecture at a glance

Five components, one new namespace `peripherals`, all on the GitOps tree:

| Component                | Kind                       | Node                | The hard part                                                             |
| ------------------------ | -------------------------- | ------------------- | ------------------------------------------------------------------------- |
| **kiosk**                | Deployment, privileged     | i5 (`kind=compute`) | owns the physical display (DRM master + Wayland seat in-pod)              |
| **senses**               | Deployment, device-mounted | i5 (`kind=compute`) | `/dev/video*` + `/dev/snd` passthrough; MJPEG/audio out                   |
| **mosquitto**            | Deployment + PVC           | i3 (`kind=storage`) | in-cluster MQTT broker; the push bus                                      |
| **home-assistant**       | Deployment + PVC           | i3 (`kind=storage`) | headless / API-only state hub                                             |
| *(no new agent tooling)* | —                          | —                   | the agent drives the kiosk via existing `deploy-web-app` + `call-service` |

Data flow for "show me X on the screen":

```
agent  --deploy-web-app-->  apps.augustin.ai/<x>   (Caddy, existing webapps box)
agent  --call-service (MQTT-bridge / HA) -->  mosquitto  topic: kiosk/show  {url:".../<x>"}
kiosk  <--subscribe--  mosquitto        ->  Chromium navigates to the URL on i5's panel
senses --MJPEG/PCM-->  (HA / agent)     ->  camera + mic as a passive feed and a callable input
```

The design principle, carried from the agent's own evolution (turns 91–95): **do not invent
new agent tooling.** The kiosk shows a web page; the agent already publishes web pages
(`deploy-web-app` → `apps.augustin.ai/<x>`) and already speaks HTTP to any in-cluster service
(`call-service`). The display subsystem is therefore *almost entirely* a consumer of
capabilities that already exist. The only genuinely new infrastructure is the **push bus**
(so the screen learns what to show without polling) and the two **device-owning pods**.

***

## 1. Display / kiosk as a pod

**Goal:** a k3s pod renders Chromium full-screen to i5's physical panel, and switches what
it shows on command.

**Shape:** a single-container (or compositor+browser two-container) Deployment, pinned to i5,
running a **Wayland kiosk compositor inside the pod** (`cage`, or `weston --shell=kiosk`) that
takes DRM master on i5's card and launches Chromium as its only client in kiosk mode. This is
the host-native turn-79 design (`cage` + Chromium) moved *inside a container* — same binaries,
same idea, different runtime boundary.

Why the compositor lives **in the pod** and not on the host: putting `cage` on the host is
precisely the host-process exception we're removing. The compositor is the workload; the
workload runs in k3s. The container image is `cage` + `chromium` + `mesa`/`libva` +
`seatd`/`libseat` on a minimal Debian base (built under `images/kiosk/`, pushed to
`ghcr.io/amarcin/kiosk`, same CI matrix as the other custom images).

### The three hard problems and the proposed solutions

**(a) DRM master — only one process can own the display.**
KMS allows exactly one DRM master per card. If anything else holds it, the compositor's
`drmSetMaster` fails and you get a black screen. Solution, in order of preference:

1. **Rely on the headless OS.** i5 ships with no display manager (OS + k3s only), so on boot
   nothing owns `/dev/dri/card*`. The kiosk pod's `cage` becomes master unopposed. This is the
   clean path and it's a *consequence* of the k3s-only rule — verify at bring-up that i5 has no
   `getty` on the console VT grabbing DRM and no `plymouth`/`fbcon` splash holding it.
2. **VT/seat handoff.** Run the compositor on a dedicated VT and mount `/dev/tty0` +
   `/dev/tty2` so `cage` can `VT_SETMODE`/`KDSETMODE` to take the console. `seatd` (a tiny
   seat manager) in the pod brokers device access without a full `systemd-logind`; mount
   `/run/seatd.sock` from a `seatd` sidecar or run `seatd` as the container's init. `cage`
   supports `libseat` with the `seatd` backend, which is the lightest option that still does
   proper master arbitration.

**(b) Input devices.** Touch (the W24C is likely a touch SKU — unverified) and any keyboard/
mouse come in via `/dev/input/event*`. `cage`/`libinput` needs these mounted and needs
`seatd` to grant them. Mount `/dev/input` (hostPath) and let `seatd` own the seat. If touch
calibration is off, that's an evdev/libinput config problem inside the pod, not a k8s problem.

**(c) The GPU render + KMS device.** Mount **i5's** `/dev/dri` (both `card*` for KMS/scanout
and `renderD*` for GL/VA) exactly like jellyfin mounts i3's — but the device paths and the
`LIBVA_DRIVER_NAME` (i5 is 10th-gen → `iHD`, not jellyfin's `i965` for the 4400) are
i5-specific and **must be verified**, not copied from jellyfin.

### Pod spec (described)

* **`nodeSelector: { kind: compute }`** — pin to i5 (the panel is there).
* **`strategy: { type: Recreate }`** — a rolling second pod would fight for DRM master; only
  one kiosk may exist. Same reasoning as the `hostPort`/Recreate rule for ingress.
* **securityContext — the honest part.** This pod is **privileged** *or* runs with a specific
  cap set (`SYS_ADMIN` for DRM ioctls/mode-setting, plus `SYS_TTY_CONFIG` for VT switching)
  and `seLinuxOptions`/`seccompProfile: Unconfined` if the compositor trips the default
  seccomp filter (mode-setting ioctls sometimes do — the same class of exception
  [firefox](/firefox) needed `seccomp:unconfined` for). Start from the **minimal** set —
  `SYS_ADMIN` + `SYS_TTY_CONFIG` + the device mounts + `seccomp: Unconfined` — and only fall
  back to `privileged: true` if `seatd`-brokered access can't get DRM master. Document whichever
  lands; do not leave it at blanket `privileged` if a cap set works.
* **Device mounts (hostPath, all on i5, all VERIFY-FIRST):**
  * `/dev/dri` → the GPU (KMS `card*` + render `renderD*`).
  * `/dev/input` → keyboard/mouse/touch (`event*`).
  * `/dev/tty0` (+ a dedicated VT like `/dev/tty2`) → console/VT for master handoff.
  * `/run/udev` (readOnly) → `libinput` device discovery, if `seatd` alone isn't enough.
* **`/dev/shm` sizing** — Chromium crashes on the default 64 MB shm (the exact trap
  [firefox](/firefox) documents). Mount an `emptyDir` `{ medium: Memory, sizeLimit: 1Gi }`
  at `/dev/shm`.
* **No ingress, no Service.** The kiosk is an *output device*, not a network service — nothing
  connects *to* it. It only makes *outbound* connections (subscribe to MQTT, fetch the app URL).
* **Resources:** request \~512Mi/250m, limit \~2Gi (a full Chromium + compositor; comparable to
  the browserless limit).
* **Config:** subscribe target (MQTT broker Service DNS), a default/idle URL to show when no
  event is pending (e.g. a clock/dashboard app on `apps.augustin.ai`), and screen
  resolution/rotation for the W24C panel.

### The tradeoff vs. the shell-free / minimal-privilege ethos

The agent stack fought hard to be **shell-free** and least-privilege (turn 73; the mastra pod
runs `runAsUser: 1000` with a narrow RBAC role and no node shell). This kiosk pod is the
**opposite corner** of that ethos: privileged-ish, hostPath device mounts, likely
`seccomp: Unconfined`. That is not a contradiction to resolve away — it's a real cost. The
mitigations: it runs in its **own namespace** with **no serviceAccount privileges** (it never
touches the k8s API — it only owns hardware), it exposes **no listening port** (no inbound
attack surface), and its blast radius is "the screen and the local input devices on one AIO,"
not the cluster. It is a **device driver wearing a pod**, and we accept that a device driver
needs device access. The line we hold: privileged for **hardware**, never privileged for
**convenience**.

***

## 2. Camera / mic sense layer as a pod

**Goal:** replace the host `ustreamer` daemon with a pod that owns i5's camera and mic and
publishes them as a passive feed plus a callable input.

**Shape:** a Deployment pinned to i5, mounting `/dev/video*` and `/dev/snd`, running
`ustreamer` (MJPEG over HTTP) for the camera and, if audio is wanted, a small ALSA/PulseAudio
capture into the same pod or a PCM/RTP stream. Direct analog of jellyfin's `/dev/dri` mount —
this is the *device-mount* precedent applied to V4L2 + ALSA instead of DRM.

### Pod spec (described)

* **`nodeSelector: { kind: compute }`** — the camera/mic are physically on the i5 AIO.
* **securityContext:** far lighter than the kiosk. V4L2 + ALSA generally need only the device
  mounts and a matching group (`video`, `audio`), **not** `privileged` and **not**
  `SYS_ADMIN`. Use `supplementalGroups` for the host `video`/`audio` GIDs (the pattern
  jellyfin uses with `supplementalGroups: [992]` for `render`). No net caps.
* **Device mounts (hostPath, on i5, VERIFY-FIRST):**
  * `/dev/video0` (and any `/dev/video1..N` — UVC cameras expose two nodes, capture +
    metadata; mount the directory or enumerate) via `type: CharDevice`.
  * `/dev/snd` → the sound devices (directory).
* **Service + no public ingress:** expose the MJPEG stream on a ClusterIP Service
  (`senses.peripherals.svc`) so HA and the agent can read it **in-cluster only**. If a camera
  view ever needs to be *shown*, it's shown *through the kiosk* (which fetches an
  `apps.augustin.ai` page embedding the stream) — the camera feed does not get its own public
  hostname.
* **Device-plugin note:** the "proper" k8s way to hand out `/dev/video*`/`/dev/dri` is a
  **device plugin** (e.g. `intel-gpu-plugin`, or a generic
  [`k8s-device-plugin` for hostPath devices](https://github.com/squat/generic-device-plugin))
  advertising `intel.com/gpu` / a `video` resource so pods *request* the device instead of
  hostPath-mounting it. That's cleaner and schedules honestly, but it's **more moving parts**
  for a single-node, single-device situation. **Decision: start with hostPath** (matches
  jellyfin, zero new infra) and only adopt a device plugin if a second consumer or a second
  GPU ever makes contention real. Don't gold-plate a one-camera problem.

***

## 3. The push channel

**Goal:** the agent (and any service) can push a "show X" event to the screen without the
kiosk polling.

**Decision — reconsider SSE→MQTT, land on MQTT.** Turn 79 said "SSE→MQTT push channel," which
is really two things: MQTT as the bus, SSE as one possible edge for a browser client. For a
kiosk that is a native Chromium+compositor (not a thin browser tab hitting an SSE endpoint), a
**plain MQTT subscription is simpler and more robust** than SSE — the kiosk holds one long
MQTT connection and reacts to `kiosk/show` messages. Keep it **fully in-cluster and
k8s-native**: an in-cluster **Mosquitto broker as a pod**.

### mosquitto (broker)

* **Deployment + PVC**, `nodeSelector: { kind: storage }` (i3 — it's stateful-ish: retained
  messages + a persistence file; pin it with the rest of the stateful data), `Recreate`.
* ClusterIP Service on 1883, **in-cluster only** (no ingress, no LoadBalancer — unlike the
  [stalwart](#) mail-port exception, nothing external needs MQTT).
* Auth: a username/password from a `kubectl`-created secret (`secretKeyRef`), never in git.
* `runAsUser: 1000`, minimal config ConfigMap (listener 1883, persistence on, one ACL:
  publishers can write `kiosk/#`, the kiosk subscribes `kiosk/#`).

### The bridge — reuse `call-service`, don't build a tool

The agent already has **`call-service`** (turn 93): a shell-free authenticated HTTP primitive
that reaches any in-cluster service via a registry. MQTT is a TCP protocol, not HTTP, so the
agent can't publish to Mosquitto *directly* with `call-service`. Two ways to keep it
k8s-native without new agent code:

1. **HA as the bridge (preferred).** Home Assistant (§4) speaks **both** MQTT and a REST API.
   The agent calls HA's REST/webhook API via `call-service` (register HA in the service
   registry), and an HA automation republishes to the `kiosk/show` MQTT topic. One config
   entry, zero new agent tooling, and it makes "show X" a first-class HA action alongside every
   other device action.
2. **A tiny MQTT-HTTP bridge pod** (`mqtt/http` shim, e.g. a 20-line service exposing
   `POST /publish {topic,payload}` → MQTT) registered in `call-service`. Only build this if HA
   turns out to be the wrong hub. It's the fallback, not the default.

Either way the *interface the agent uses* is the one it already has: an HTTP call to an
in-cluster service. The push channel stays k8s-native (broker is a pod, bridge is a pod or an
HA automation) and adds **no new agent surface.**

***

## 4. Home Assistant headless / API-only

**Goal:** HA as the device/state hub (the thing that knows about the camera, the screen, and
any future smart-home devices) — its **API and MQTT**, not its frontend. Build our own UI (a
web app on `apps.augustin.ai`); don't adopt HA's Lovelace as the surface. This matches the
turn-36/38 senses framing: *the agent is the UI; the services are senses.*

### Pod spec (described)

* **Deployment + PVC** (`/config` — HA's SQLite/state store), `nodeSelector: { kind: storage }`
  (i3, with the stateful data), `Recreate`, `replicas: 1`.
* **Image:** `ghcr.io/home-assistant/home-assistant:stable`.
* **Network — the one wrinkle.** HA's device auto-discovery (mDNS/SSDP for smart-home gear)
  wants **host networking** or at least broadcast reach on the LAN. For an **API/MQTT-only**
  hub driving the screen, that discovery is **not needed** — so run it as a **normal ClusterIP
  Deployment** (no `hostNetwork`), talking to Mosquitto over the cluster network and to the
  agent over REST. **If and when** a real broadcast-discovered device is added, revisit
  `hostNetwork: true` (pinned to i3) as a scoped exception — but don't take that networking hit
  speculatively. Document it as a known future edge.
* **Config:** the MQTT integration pointed at `mosquitto.peripherals.svc`, a long-lived access
  token (secret) for the agent's `call-service` calls, and the `kiosk/show` automation from §3.
* **No public ingress by default** (agent reaches it in-cluster). If a debugging view of HA's
  own UI is ever wanted, add an ingress behind the existing CF Access policy — but the
  *product* UI is the custom `apps.augustin.ai` app, per the framing.

***

## 5. How the agent drives it (no new tooling)

The whole point of tying into existing capabilities: **the display subsystem is a client of
what the agent can already do.**

1. **Content:** the agent publishes a page with **`deploy-web-app`** → served by Caddy at
   `apps.augustin.ai/<x>` (turn 72, LOCKED). "Show me images of X" = generate a tiny gallery
   app, `deploy-web-app` it, done. No new content pipeline.
2. **Command:** the agent tells the screen what to show by **`call-service`** to HA (or the
   bridge), which publishes `kiosk/show {url: "https://apps.augustin.ai/<x>"}`. The kiosk pod,
   subscribed, navigates Chromium there.
3. **Proactive:** the agent can already reach out via **ntfy** (the mastra pod has `NTFY_URL`
   wired). "I put it on the screen" can also ping the phone with the same link — the surface is
   the screen *and* the notification, reusing the existing escalation path.
4. **Camera as a tool:** the senses pod's in-cluster MJPEG/PCM endpoints are just more
   in-cluster services — reachable by `call-service` (HTTP) for a snapshot, or surfaced through
   HA as a camera entity. No bespoke "camera tool" needed; it collapses into the existing broad
   primitive, which is exactly the turn-91→95 direction (few broad primitives, not a per-device
   tool pile).

Net new agent code across the whole subsystem: **effectively zero** — a service-registry entry
for HA and, optionally, one HA automation. That is the design working *with* the grain of the
agent's evolution instead of against it.

***

## Phasing

**Hard prerequisites first.** Nothing below is buildable until these are answered; several
require physical/console access to i5 that a read-only k8s session does not have.

**Phase 0 — verification & access (BLOCKS everything).**

* **P0.1 — Re-establish i5 bring-up access.** `ssh i5` is **unwired** (verified: from pentium,
  `ssh i5` → *"Could not resolve hostname i5"*). i5 is a live k3s node at **192.168.1.25**
  (verified: `kubectl get nodes` shows i5 Ready, `kind=compute`, Debian 13, kernel 6.12.94),
  but there is **no working shell path to it** from the bastion for device inspection or
  console work. Restore a reachable, authenticated path to i5 (fix the `~/.ssh/config` host
  entry / mesh name resolution) before touching hardware. **This document could not enumerate
  i5's devices for exactly this reason** — treat every device path below as *unconfirmed*.
* **P0.2 — Verify i5's `/dev/dri`.** Confirm `card*` (KMS/scanout) **and** `renderD*` exist and
  which VA driver the 10th-gen iGPU uses (`iHD` vs `i965`). The jellyfin manifest is on **i3**
  and tells us nothing about i5. *Without this, the kiosk manifest is a guess.*
* **P0.3 — Verify i5's `/dev/video*` and `/dev/snd`.** Confirm the AIO webcam enumerates
  (UVC → `/dev/video0`+`) and the mic/speakers appear under `/dev/snd\`.
* **P0.4 — Verify i5 holds no DRM master at boot.** Confirm the headless OS truly leaves
  `/dev/dri/card*` free — no `gdm`/`Xorg`/`getty`-on-console/`plymouth` grabbing it. This is
  the assumption the whole kiosk rests on; the "OS + k3s only" rule *should* guarantee it, but
  verify, don't assume.
* **P0.5 — W24C panel/touch/backlight verify** (the standing turn-79 TODO). Confirm the Dell
  W24C's touch SKU, panel native resolution/rotation, and that backlight control works
  headless (some AIO panels need a kernel `video=`/`i915` quirk). Touch drives whether we mount
  `/dev/input` for a touchscreen and whether calibration is needed.

**Phase 1 — the push bus (no hardware; safe, do first).** Deploy **mosquitto** (i3) + a
minimal ConfigMap + secret. This is a plain stateful pod with a PVC — the well-trodden path.
Prove publish/subscribe in-cluster. Zero device risk; unblocks the rest.

**Phase 2 — Home Assistant (no hardware).** Deploy **home-assistant** (i3, ClusterIP, PVC),
wire the MQTT integration to mosquitto, mint the agent's access token, add HA to the
`call-service` registry, author the `kiosk/show` automation. Now "publish a show-event" works
end-to-end **before** any screen exists — testable by watching the MQTT topic.

**Phase 3 — senses pod (needs P0.1/P0.3).** Build/deploy the **senses** Deployment on i5 with
`/dev/video*` + `/dev/snd` mounts and `supplementalGroups`. Lower-risk than the kiosk (no DRM
master). Verify the MJPEG stream on the ClusterIP Service and, optionally, as an HA camera
entity. This also **proves i5 device passthrough works at all** before betting the display on
it.

**Phase 4 — the kiosk (needs ALL of P0).** Build `images/kiosk/` (cage + Chromium + mesa +
seatd), deploy the privileged/carefully-capped Deployment on i5 with the DRM/input/tty mounts
and the `/dev/shm` sizing. Iterate the securityContext **down** from a known-working state
toward the minimal cap set. Wire it to subscribe `kiosk/show`. First success = an
`apps.augustin.ai` page appears on the physical panel on command. This is last because it is
the hardest and every earlier phase de-risks a piece of it.

**Phase 5 — glue & polish.** Default/idle screen app (clock/dashboard), the agent's
"show me X" flow end-to-end (generate → `deploy-web-app` → `call-service` HA → screen),
ntfy dual-surface, touch interactions if the W24C is a touch panel.

***

## Risks — where k8s-native is genuinely harder

This is the section that doesn't rubber-stamp the mandate.

* **DRM master in a container is the real risk.** On a headless host it *should* work (nothing
  competes), but the failure modes are nasty and remote-debuggable only up to a point: if
  `cage` can't take master you get a **black screen with no network symptom**, and diagnosing
  it means console access to i5 (which, per P0.1, isn't even wired yet). A host process hitting
  the same problem is debuggable over plain SSH with `journalctl`; a pod adds a container/seat
  layer between you and the DRM error. **This is strictly harder than host-native, and it's the
  single most likely place the build stalls.**
* **Seat management without `systemd-logind`.** Containers don't have the host's `logind` seat.
  `seatd` is the answer and it works, but it's one more in-pod daemon to get right, and
  `libseat` backend selection (`seatd` vs `logind` vs `builtin`) is fiddly. Host-native `cage`
  under the host's `seatd`/`logind` is a documented, common setup; in-pod is comparatively
  uncharted.
* **Single-display arbitration.** Only one DRM master, ever. The `Recreate` strategy enforces
  one pod, but a crash-looping kiosk that grabs-and-releases master can wedge the console; and
  if P0.4 is wrong (something *does* hold master at boot), the pod never starts cleanly. A host
  process has the same constraint but a simpler recovery (restart the unit).
* **securityContext cost.** This pod is the least-privilege ethos's worst citizen (privileged
  or `SYS_ADMIN`+`SYS_TTY_CONFIG`+`seccomp: Unconfined`+broad device hostPaths). We contain it
  (own namespace, no API access, no inbound port), but it's a real expansion of the fleet's
  privileged surface — from "beszel reads `/sys`" to "a pod owns the console."

**Does any component honestly want a host exception?** Four of the five are cleanly and
comfortably k8s-native — **mosquitto, home-assistant, and the senses pod** are ordinary
device-mount/stateful pods squarely in the repo's existing precedent, and driving them via
`deploy-web-app`/`call-service` is a *better* fit than the host design. **No exception wanted
there.**

The **kiosk compositor** is the honest maybe. DRM-master-in-a-container is a known-hard thing,
and the turn-79 host `cage` existed precisely to sidestep it. My recommendation: **build it
k8s-native as designed** — the headless OS removes the usual blocker, the precedents exist, and
a host `cage` daemon is exactly the homeless layer the k3s migration killed. **But** set a
concrete tripwire: if, after Phase 0 verification, the in-pod `cage` **cannot reliably take DRM
master** (crash-loops, black screen, or needs escalating hacks beyond a documented cap set),
that is the signal to reopen the host-exception decision **for the compositor only** — as a
narrow, documented carve-out (a single host `cage` unit that the k8s kiosk pod's Chromium
connects to as a Wayland client), not a reversal of the k3s-only rule. Everything else stays in
k3s regardless. The k8s-native path is the primary; the tripwire keeps us honest instead of
sinking unbounded effort into the one genuinely hard component. Augustin decides at the
tripwire, with real evidence instead of a guess.
