From 8073a6aed93aef5f4137f95fe3a4faf95f4b6ea6 Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 14 Sep 2026 18:55:54 -0700 Subject: [PATCH] fix(esphome): bind the remote-build peer-link to loopback; finish the rsync fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ha-dev asked for the Device Builder 1.0.0 remote-build receiver to be turned off: one instance, builds run locally, so the feature has no role, and it was binding 0.0.0.0:6055 with mDNS advertisement on a privileged host-network container that writes firmware to devices. Reading the source first changed the framing. controllers/remote_build/ _state.py declares 'remote_builds_enabled: bool = True', so nobody enabled it — it arrived on by default with the rewrite. And the flag has no on-disk representation until it is changed: neither .device-builder.json nor .device-builder-preferences.json carries it, and the only writer is the app's own command API behind the UI. Setting it from a playbook would mean inventing a schema for a model I have not read. So this binds ESPHOME_REMOTE_BUILD_HOST=127.0.0.1 — a documented env var, no entrypoint override — which removes the LAN reachability now and is verifiable (ss reports 127.0.0.1:6055, was 0.0.0.0:6055). It is explicitly NOT the off switch ha-dev asked for and the compose comment says so; the Settings toggle is one UI click and the line can go once someone flips it. Also completes yesterday's deploy-stack.sh fix, which was half a fix. --no-o --no-g stopped rsync chgrp-ing a root:docker destination as a non-root identity, but the very next deploy failed the same way one layer along — 'failed to set times on ...' — because a non-root identity cannot utime() a root-owned directory either. Same exit 23 after a successful transfer. Added --omit-dir-times. Fixing only the group half looked fixed until the next run, which is the whole reason this is worth a line in the script's comment. --- scripts/deploy-stack.sh | 10 +++++++--- stacks/esphome/compose.yaml | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/deploy-stack.sh b/scripts/deploy-stack.sh index d50156b..2085c65 100755 --- a/scripts/deploy-stack.sh +++ b/scripts/deploy-stack.sh @@ -163,13 +163,17 @@ for entry in "${PAIRS[@]}"; do tmp_out=$(mktemp) tmp_err=$(mktemp) rc=0 - # --no-o --no-g: the deploy tree is root:docker 2775 (setgid) since the + # --no-o --no-g --omit-dir-times: the deploy tree is root:docker 2775 (setgid) since the # 2026-09-14 fleet normalisation, and plain -a makes rsync try to chgrp the # destination as the deploy identity, which is not root. It fails with # "chgrp ... Operation not permitted" and exits 23 AFTER transferring the # content — a loud error on a deploy that actually succeeded. The setgid bit # already assigns the right group, so rsync should not be fighting it. - rsync -az --no-o --no-g --delete --dry-run \ + # --omit-dir-times is the same problem one layer along: a non-root identity + # cannot utime() a root-owned directory either, which produced the identical + # "exit 23 after a successful transfer" on the next deploy. Both halves are + # needed; fixing only the group half looks fixed until the next run. + rsync -az --no-o --no-g --omit-dir-times --delete --dry-run \ "${RSYNC_REMOTE[@]}" \ --out-format='%i %n' \ "${EXCLUDES[@]}" "${extra[@]}" \ @@ -291,7 +295,7 @@ for entry in "${PAIRS[@]}"; do extra=() [ "$kind" = compose ] && extra+=(--exclude='conf/') printf 'pushing %s → %s\n' "$src" "$dest" - rsync -az --no-o --no-g --delete \ + rsync -az --no-o --no-g --omit-dir-times --delete \ "${RSYNC_REMOTE[@]}" \ "${EXCLUDES[@]}" "${extra[@]}" \ "$src" "$dest" diff --git a/stacks/esphome/compose.yaml b/stacks/esphome/compose.yaml index dcb7769..135cb5f 100644 --- a/stacks/esphome/compose.yaml +++ b/stacks/esphome/compose.yaml @@ -39,6 +39,23 @@ services: # the 2025.8.2 -> 2026.8.2 upgrade, 2026-09-14. - ESPHOME_USERNAME=${ESPHOME_USERNAME} - ESPHOME_PASSWORD=${ESPHOME_PASSWORD} + # + # Device Builder 1.0.0 added a remote-build peer-link receiver that binds + # 0.0.0.0:6055 and advertises itself over mDNS as _esphomebuilder._tcp. + # It is ON BY DEFAULT — controllers/remote_build/_state.py declares + # `remote_builds_enabled: bool = True` — so nobody enabled it; it arrived + # with the rewrite. There is exactly one Device Builder here and builds + # run locally in this container, so the feature has no role: it is + # unrequested LAN reachability on a privileged host-network container + # whose job is writing firmware to devices. + # + # ⚠ This binds it to loopback; it does NOT disable it. The real off + # switch is a Settings toggle in the UI, and its enabled flag has no + # on-disk representation until it is changed, so there is nothing to set + # from a playbook without inventing a schema. Flip it in Settings and + # this line can go. (ha-dev's stated preference, correctly, is not + # enabling a feature over restricting its bind address.) + - ESPHOME_REMOTE_BUILD_HOST=127.0.0.1 labels: - homepage.group=Apps - homepage.name=ESPHome