fix(esphome): actually disable remote-build — two switches, only one closes the port

ha-dev found the WS API and tested the read half; this runs the write. But the
command they identified is the wrong half, which is worth recording because the
naming actively misleads.

  remote_build/set_offloader_settings {remote_builds_enabled: false}
      the OUTBOUND half — this dashboard sending builds to peers.
      Persists, reads back false, and leaves the receiver listening.

  remote_build/set_settings {enabled: false}
      the receiver-side master switch, per ReceiverController.set_settings's
      own docstring. Tears the listener down live, no restart needed.

Set both. Verified across a restart: 6055 absent, zero peer-link bind lines,
zero mDNS advertisements, both switches read back false. Persisted at
_remote_build.enabled in /config/.device-builder.json — which did not exist
until the flag was first changed, so 'no on-disk representation' was true only
of the default state.

ESPHOME_REMOTE_BUILD_HOST=127.0.0.1 is KEPT as a backstop rather than removed.
The off state now lives in one JSON file whose in-code default is enabled:True
(controllers/remote_build/_state.py) and whose module's stores soft-recover to
an empty model on a malformed blob rather than erroring — so a lost or corrupt
settings file silently re-enables remote-build. With the env var set, that
regression binds loopback instead of 0.0.0.0.

Also finishes deploy-stack.sh properly. This was patched three times in one
session because -a is -rlptgoD and a non-root identity cannot apply owner,
group, permissions OR times to a root-owned directory; each patch fixed one
letter and the next deploy failed on the next one, every time exiting 23 AFTER
a successful transfer. The rule is now written into the script: the deploy
syncs content, the conventions own metadata. --no-o --no-g --no-perms
--omit-dir-times. Verified: clean run, destination keeps 2775 root:docker with
setgid intact.
This commit is contained in:
2026-09-14 19:05:11 -07:00
parent 8073a6aed9
commit 687c6999f3
3 changed files with 58 additions and 13 deletions
+21 -7
View File
@@ -163,17 +163,31 @@ for entry in "${PAIRS[@]}"; do
tmp_out=$(mktemp) tmp_err=$(mktemp)
rc=0
# --no-o --no-g --omit-dir-times: the deploy tree is root:docker 2775 (setgid) since the
# THE RULE: the deploy syncs CONTENT; the conventions own METADATA.
#
# --no-o --no-g --no-perms --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.
# --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 \
# This was fixed three times in one session before the rule above was
# written down, because `-a` is `-rlptgoD` and a non-root identity cannot
# apply ANY of owner, group, permissions or times to a root-owned directory.
# Each patch fixed one letter and the next deploy failed on the next one:
# chgrp failed -> --no-o --no-g
# failed to set times -> --omit-dir-times
# failed to set perms -> --no-perms
# Every one of them exited 23 AFTER transferring the content — a loud error
# on a deploy that had succeeded — so each partial fix looked complete until
# the next run. Enumerate the flag set, don't chase the symptom.
#
# ⚠ --no-perms means a NEW file lands with the remote umask rather than the
# source's mode, so a stack shipping an executable script needs its +x set by
# the convention (playbooks/normalize-docker-tree.yaml preserves exec bits),
# not by the deploy. Existing files keep their modes.
rsync -az --no-o --no-g --no-perms --omit-dir-times --delete --dry-run \
"${RSYNC_REMOTE[@]}" \
--out-format='%i %n' \
"${EXCLUDES[@]}" "${extra[@]}" \
@@ -295,7 +309,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 --omit-dir-times --delete \
rsync -az --no-o --no-g --no-perms --omit-dir-times --delete \
"${RSYNC_REMOTE[@]}" \
"${EXCLUDES[@]}" "${extra[@]}" \
"$src" "$dest"
+17
View File
@@ -18,6 +18,23 @@ ssh infra-ops@esh-docker-vm 'cd /opt/docker/compose/esphome && sudo -n docker co
The `.env` is never synced in either direction (see the root `CLAUDE.md`
exclusion list). It is created on the host once, from the vault.
## Remote build is off
Device Builder 1.0.0 ships remote-build **on by default** and it binds
`0.0.0.0:6055` with mDNS advertisement. Disabled 2026-09-14 via the dashboard's
own `/ws` API — `remote_build/set_settings {"enabled": false}` — persisted at
`_remote_build.enabled` in `/config/.device-builder.json`.
**There are two switches and only one closes the port.**
`remote_build/set_offloader_settings {remote_builds_enabled}` is the *outbound*
half; setting it false persists, reads back false, and leaves the receiver
listening. `remote_build/set_settings {enabled}` is the receiver-side master
switch. The one whose name reads like the master switch is not.
`ESPHOME_REMOTE_BUILD_HOST=127.0.0.1` is kept as a backstop: the off state lives
in one JSON file whose in-code default is `True`, so a lost or corrupt settings
file re-enables the feature silently.
## Three things not to undo
- **`privileged: true` and `network_mode: host` are required.** USB flashing
+20 -6
View File
@@ -49,12 +49,26 @@ services:
# 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.)
# RESOLVED 2026-09-14: remote-build is now genuinely OFF —
# `remote_build/set_settings {enabled: false}` over the dashboard's /ws
# API, persisted at `_remote_build.enabled` in /config/.device-builder.json.
# Verified across a restart: no listener, no mDNS advertisement.
#
# ⚠ Two switches, and only one of them closes the port. The obvious
# candidate — `remote_build/set_offloader_settings {remote_builds_enabled}`
# — is the OUTBOUND half (this dashboard sending builds to peers). Setting
# it to false persists, reads back false, and leaves the receiver
# listening. The port belongs to `ReceiverController.set_settings`, whose
# own docstring calls it "the receiver-side `enabled` master switch".
# Turning off the one named `remote_builds_enabled` looks like the fix and
# is not.
#
# This line is now the BACKSTOP, kept deliberately. The off state lives in
# a single JSON file whose in-code default is `enabled: True`
# (controllers/remote_build/_state.py), and this module's stores
# soft-recover to an empty model on a malformed blob rather than erroring
# — so a lost or corrupt settings file silently re-enables remote-build.
# With this set, that regression binds loopback instead of 0.0.0.0.
- ESPHOME_REMOTE_BUILD_HOST=127.0.0.1
labels:
- homepage.group=Apps