home-assistant: placed directories, the build ace runs, its LAN listens #147

Open
mesh-admin wants to merge 9 commits from feat/home-assistant-for-ace into main
Contributor

Prepares home-assistant for ace (nothing deployed).

  • config dir and state are placed (${dir:config}, ${dir:state}), route binds into ${dir:state} (ADR 0112); config dir drops owner 1000:1000 (image runs as root).
  • sidecar no longer mounts HA's config dir (unused env var; it exposed the auth store and secrets.yaml).
  • new listens 1400 (Sonos event callback) and 18555 (go2rtc WebRTC): HA opens them by default and LAN devices dial in; host network so machine port = software port.
  • image pinned to the 2026.9.3 build ace runs (recorder schema migrates; never older than running).

Verified: go test ./internal/catalogue/ -run Catalogue with MESH_CATALOGUE on this tree; pinned image boots on a fresh root-owned 0700 config dir (manifest 200, /api 401 without token); refuses X-Forwarded-For from an untrusted proxy (400) — the route source must be in HA's trusted_proxies.

Operator-side notes: the sidecar's token must be a long-lived access token created in HA and secret accepted (none exists today); see ACE-MODULE-PLANS.md.

Prepares home-assistant for ace (nothing deployed). - config dir and state are placed (`${dir:config}`, `${dir:state}`), route binds into `${dir:state}` (ADR 0112); config dir drops owner 1000:1000 (image runs as root). - sidecar no longer mounts HA's config dir (unused env var; it exposed the auth store and secrets.yaml). - new listens 1400 (Sonos event callback) and 18555 (go2rtc WebRTC): HA opens them by default and LAN devices dial in; host network so machine port = software port. - image pinned to the 2026.9.3 build ace runs (recorder schema migrates; never older than running). Verified: `go test ./internal/catalogue/ -run Catalogue` with MESH_CATALOGUE on this tree; pinned image boots on a fresh root-owned 0700 config dir (manifest 200, /api 401 without token); refuses X-Forwarded-For from an untrusted proxy (400) — the route source must be in HA's trusted_proxies. Operator-side notes: the sidecar's `token` must be a long-lived access token created in HA and `secret accept`ed (none exists today); see ACE-MODULE-PLANS.md.
mesh-admin added 1 commit 2026-09-29 21:39:51 +00:00
The module stated /services/home-assistant/config and bound its route under
/var/lib/mesh — novox's layout, a path no definition may carry (ADR 0112).
The config dir and the module's state are now placed (${dir:config},
${dir:state}); the route binds into ${dir:state}.

The sidecar no longer mounts Home Assistant's config dir: nothing reads
MESH_HOMEASSISTANT_CONFIG_DIR, and the mount handed it the auth store and
secrets.yaml for nothing. The config dir loses owner 1000:1000 — the image
runs as root, the uid was the predecessor's host-user convention.

Two more listens that the software opens by default and LAN devices dial in
on, which a converged filter would otherwise close: 1400 (Sonos event
callback) and 18555 (bundled go2rtc WebRTC). Host network, so the machine
port is the software's.

Image pinned to the 2026.9.3 build ace's predecessor runs (2026-09-18);
Home Assistant migrates its recorder schema, so older than running is unsafe.

Verified: catalogue tests pass with MESH_CATALOGUE on this tree; the pinned
image boots on a fresh root-owned 0700 config dir (manifest 200, API 401
without a token), and refuses X-Forwarded-For from an untrusted proxy (400).
jschoubben added 8 commits 2026-09-30 11:11:58 +00:00
ombi's definition named /services/ombi/config (a HAL machine path) in three
places and pinned an image older than the one ace runs. Ombi migrates its
own SQLite schema, so a take onto the older pin (v4.53.10-ls267) would start
it on a database the newer build (ls269) already touched.

- config is a pathless placed directory, mounted as ${dir:config}
- a state directory placed at the assignment root carries route.json
- image pinned to the digest ace runs today (v4.53.10-ls269)
- the sidecar reaches ombi on the machine port the mesh assigns
  (${port:3579}) rather than assuming 3579 is free
- the sidecar no longer mounts ombi's data directory: MESH_OMBI_CONFIG_DIR
  is read by no code, and the mount exposed the databases for nothing

Verified: catalogue tests (MESH_CATALOGUE set, 6 pass, none skipped); the
pinned image starts as PUID 1000 in a 0700 dir and answers /api/v1/Status
200; data owned 1001:2000 (ace's media ids) under a 1000:1000 dir is
re-owned by the image's init and serves 200; a minted ApiKey is refused
(401) - the api-key secret must be accepted from ombi's own settings.
A host-network sidecar reaches its service over the machine's loopback, and
the mesh publishes that service on a machine port it assigns (ADR 0038) —
so dialling the software's port reaches whatever else holds it. On ace,
searxng's sidecar dialled 127.0.0.1:8080 and got unifi's inform port. The
same shape in bazarr, bookshelf, lidarr, nzbget, qbittorrent, radarr and
sonarr; each now asks with ${port:N} (hq 088). Found in review of ace's
module preparation.
A consumer on another machine (ombi first; jackett, bazarr and home-assistant
later) reached these by container name on HAL's shared network, which the mesh
does not have. Each app now provides <app>-api at mesh scope and serves the
software port, so the mesh tells a consumer where it is and redirects the
port to where the machine published it.

Named per app, not one servarr-api: a requirement is matched by name and
answered by exactly one provider per node, so a consumer cannot require one
name from three providers - and ombi's code is written against each app's
own API version (ADR 0027).

No grants and no provisioner: a Servarr instance has one API key, which the
mesh cannot mint. The operator accepts it as the pair credential for each
consumer (ADR 0092).
ombi keeps its Servarr connections in its own database, so the mesh has no
file to write them into. A run-once step reads the three bindings and pair
credentials and writes host, port, TLS, base path and key into ombi through
ombi's own API - only when they differ, and nothing else ombi keeps.

Until the operator accepts an app's key for this pair the mesh delivers a
value it minted, which no Servarr app accepts. The step tries the key against
the app first and, refused, writes nothing and fails naming the secret accept
that fixes it, so the old working key in ombi is never replaced by a dead one.

Declared last so its failing gates nothing else of ombi (ADR 0136), and
restart-on its six inputs so it runs again when a provider moves (ADR 0099).
home-assistant requires sonarr-api, radarr-api and lidarr-api, which #156 makes sonarr, radarr and
lidarr provide; this branch needs those providers to resolve.
Home Assistant reached mosquitto and the three Servarr apps at 127.0.0.1 and a port typed into its
own storage. It now requires mqtt-topic (asking for every topic: discovery and the devices' topics
are its job), sonarr-api, radarr-api and lidarr-api, and a run-once `provisions` step — declared
last, restarted when a binding or pair credential changes — makes Home Assistant's config entries
say what the mesh bound, through Home Assistant's own config flows and never its .storage:

- MQTT: the broker is asked first whether it takes the delivered login; then the integration's
  reconfigure flow sets broker, port, username and password, every other setting sent back as Home
  Assistant pre-filled it, and Home Assistant's own connection test must pass. A digest of what was
  written makes a rerun "already as the mesh says". Refused anywhere, nothing is written and Home
  Assistant keeps the login it has.
- Sonarr/Radarr/Lidarr: the bound key is tried against the app (a minted key is never written; the
  failure names the `secret accept`); no entry is made through the user flow; a reauth Home
  Assistant started is finished with the bound key (and URL where the integration asks); an entry
  already at the bound URL, or at another URL reaching the same running app, is left as it is.
  These integrations have no reconfigure flow, so a working entry elsewhere is refused loudly —
  the step never removes an entry.

The sidecar's URL now uses the port it was given.
Home Assistant's MQTT user flow shows no value for set_ca_cert and set_client_cert (a reconfigure
pre-fills them from the entry), and refuses a submit without them — found against the pinned
2026.9.3 in a throwaway instance.
Author
Contributor

home-assistant now gets its broker and Sonarr/Radarr/Lidarr from the mesh (merge of #156 in 958a6f4, then 31af3f0 and 1e34ecc)

  • requires mqtt-topic (contributes topics: ["#"]), sonarr-api, radarr-api and lidarr-api, plus binds and secrets for each. The sidecar dials 127.0.0.1:${port:8123}.
  • This branch merges origin/feat/servarr-api-provision (#156), which provides the three Servarr APIs. Merge #156 first.
  • A new run-once provisions step is declared last. It restarts on any bound-* or secret-* change. It writes through Home Assistant's own config flows and never through .storage:
    • MQTT:
      • It first asks the broker whether it takes the delivered login (CONNACK), and warns if the login can't subscribe to homeassistant/#.
      • Then it runs the integration's reconfigure flow. Broker, port, username and password are set; every other field goes back exactly as HA pre-filled it. HA's own connection test must pass.
      • A digest of what it wrote makes a rerun a no-op. If anything refuses, nothing is written and HA keeps the login it has.
    • Servarr:
      • It tries the bound key against the app first. A minted key is never written, and the error names secret accept … home-assistant <app>-api --provider ….
      • If there is no entry, it runs the user flow.
      • If HA has started a reauth, it finishes it (key, plus the URL for Radarr and Lidarr).
      • An entry already at the bound URL is unchanged.
      • An entry at a different URL that reaches the same running app (same startTime, appData and version) is left alone, and the step says so. That is ace's case: 127.0.0.1:8989 vs ace.internal:8989.
      • These integrations have no reconfigure flow, so a working entry pointing at a different app is refused loudly. The step never removes an entry.
  • Tests: test/provisions.test.ts (13 passing). tsc typecheck and build are clean. The catalogue tests pass with #144/#147/#149 merged. A scratch resolution for ace fills every binding (mqtt-topic → ace.internal:1883 as mesh_ace_hass; sonarr, radarr and lidarr → ace.internal:8989/7878/8686) and every sealed secret, and the step's restart-on resolves.
  • E2E against throwaway 2026.9.3 HA, mosquitto, sonarr, radarr and lidarr (prov-*, 1938x, all removed):
    • HA was seeded like ace: MQTT at 127.0.0.1 as a carried legacy login, Sonarr and Lidarr at 127.0.0.1, no Radarr. Then Lidarr's key was rotated, so HA opened a reauth.
    • Run 1: MQTT was rewritten to mesh_ace_hass@ace.internal and passed HA's test. Sonarr was reported equivalent. Lidarr's reauth was finished with the bound URL and key. Radarr was refused with the minted key and nothing was written (exit 1).
    • Run 2 (key accepted): Radarr was created.
    • Run 3: everything was unchanged (exit 0).
    • A device publishing discovery and state with the legacy login still reaches HA over its new login (switch.e2e_office on/off). A fresh HA with no MQTT entry got one made.
    • Finding along the way: the MQTT user flow requires set_ca_cert and set_client_cert, and the step now sends them (fixed in 1e34ecc).
  • Window: accept the three Servarr keys for this pair before take. Until they are accepted, the step fails for those apps and leaves HA's entries alone.
**home-assistant now gets its broker and Sonarr/Radarr/Lidarr from the mesh** (merge of #156 in 958a6f4, then 31af3f0 and 1e34ecc) - **requires** `mqtt-topic` (contributes `topics: ["#"]`), `sonarr-api`, `radarr-api` and `lidarr-api`, plus `binds` and `secrets` for each. The sidecar dials `127.0.0.1:${port:8123}`. - This branch **merges origin/feat/servarr-api-provision (#156)**, which provides the three Servarr APIs. Merge #156 first. - A new **run-once `provisions` step** is declared last. It restarts on any `bound-*` or `secret-*` change. It writes through Home Assistant's own config flows and never through `.storage`: - **MQTT:** - It first asks the broker whether it takes the delivered login (CONNACK), and warns if the login can't subscribe to `homeassistant/#`. - Then it runs the integration's **reconfigure flow**. Broker, port, username and password are set; every other field goes back exactly as HA pre-filled it. HA's own connection test must pass. - A digest of what it wrote makes a rerun a no-op. If anything refuses, nothing is written and HA keeps the login it has. - **Servarr:** - It tries the bound key against the app first. A minted key is never written, and the error names `secret accept … home-assistant <app>-api --provider …`. - If there is no entry, it runs the user flow. - If HA has started a reauth, it finishes it (key, plus the URL for Radarr and Lidarr). - An entry already at the bound URL is unchanged. - An entry at a different URL that reaches **the same running app** (same startTime, appData and version) is left alone, and the step says so. That is ace's case: `127.0.0.1:8989` vs `ace.internal:8989`. - These integrations have **no reconfigure flow**, so a working entry pointing at a different app is refused loudly. The step never removes an entry. - Tests: `test/provisions.test.ts` (13 passing). tsc typecheck and build are clean. The catalogue tests pass with #144/#147/#149 merged. A scratch resolution for ace fills every binding (`mqtt-topic` → ace.internal:1883 as `mesh_ace_hass`; sonarr, radarr and lidarr → ace.internal:8989/7878/8686) and every sealed secret, and the step's `restart-on` resolves. - **E2E** against throwaway 2026.9.3 HA, mosquitto, sonarr, radarr and lidarr (`prov-*`, 1938x, all removed): - HA was seeded like ace: MQTT at 127.0.0.1 as a carried legacy login, Sonarr and Lidarr at 127.0.0.1, no Radarr. Then Lidarr's key was rotated, so HA opened a reauth. - **Run 1:** MQTT was rewritten to `mesh_ace_hass`@ace.internal and passed HA's test. Sonarr was reported equivalent. Lidarr's reauth was finished with the bound URL and key. Radarr was refused with the minted key and nothing was written (exit 1). - **Run 2** (key accepted): Radarr was created. - **Run 3:** everything was unchanged (exit 0). - A device publishing discovery and state with the legacy login still reaches HA over its new login (`switch.e2e_office` on/off). A fresh HA with no MQTT entry got one made. - Finding along the way: the MQTT user flow requires `set_ca_cert` and `set_client_cert`, and the step now sends them (fixed in 1e34ecc). - **Window:** accept the three Servarr keys for this pair before `take`. Until they are accepted, the step fails for those apps and leaves HA's entries alone.
You are not authorized to merge this pull request.
This pull request can be merged automatically.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/home-assistant-for-ace:feat/home-assistant-for-ace
git checkout feat/home-assistant-for-ace
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: novox/mesh-catalog#147