Skip to content

fix: sync addon settings UI with Supervisor options end-to-end - #1420

Merged
kingpanther13 merged 7 commits into
homeassistant-ai:masterfrom
kingpanther13:fix/addon-ui-settings-sync
May 23, 2026
Merged

fix: sync addon settings UI with Supervisor options end-to-end#1420
kingpanther13 merged 7 commits into
homeassistant-ai:masterfrom
kingpanther13:fix/addon-ui-settings-sync

Conversation

@kingpanther13

@kingpanther13 kingpanther13 commented May 23, 2026

Copy link
Copy Markdown
Member

What does this PR do?

Fixes a cluster of related bugs in the addon-mode settings web UI and unifies the restart UX across every save endpoint (Tools, Server Settings, Backups). Originally a four-bug ticket; the review-toolkit + idiot-checker pass turned it into a structural rework of the save-and-restart flow.

Bugs fixed

  1. Server-settings toggles are editable in addon mode. They used to render "Managed by the add-on Configuration tab — open Settings → Add-ons → ha-mcp → Configuration to edit." and the checkboxes were disabled. Now each save POSTs through /addons/self/options (with the existing options merged so required schema keys like backup_hint survive Supervisor's full-replacement validation) and returns restart_required: True. Web UI ↔ Configuration tab stay in sync after the restart.

  2. No more spurious "Restart failed" alert / no more "addon is restarting" forever-message. The old code had save handlers schedule a supervisor self-restart in a background task; the restart raced the JSON response flush, HA ingress returned a 5xx Bad Gateway, the JS surfaced "Restart failed". The new architecture removes the auto-restart from every save handler entirely.

  3. ha_manage_custom_tool is listed in the tool catalog with the same "Beta — set enable_code_mode in the dev add-on config" hint as the other beta-gated tools, regardless of whether the toggle is on.

  4. Backup tab save no longer returns supervisor 400. _save_backup_config was POSTing only the auto-backup fields and dropping backup_hint (the addon schema's only required key). Fixed by reading current options, merging the new values, then posting — same pattern start.py::maybe_persist_secret_path already uses.

Unified restart UX (replaces every per-handler restart trigger)

Every save endpoint now returns the same {success, applied, mode, restart_required} shape. restart_required: True lights up a cross-tab restart-required banner; the global "Restart Add-on" button is the single restart trigger.

  • Save persists the change to the right backend (Supervisor options / override file / tool config JSON) and does NOT restart.
  • Restart button click captures the running process's instance_id (new field on /api/settings/info), POSTs /api/settings/restart, and broadcasts restart-initiated to every open settings tab. Each tab independently runs a poll-then-reload cycle.
  • Poll cycle waits a 3s grace period (lets supervisor actually kill the addon) then polls /api/settings/info every 2s, comparing instance_id against the captured baseline. Reloads the page on first instance_id flip; gives up after 60s with "Add-on did not come back online — reload manually" (button re-enabled, user can retry).
  • Cross-tab sync via BroadcastChannel('ha-mcp-settings') so saving in one tab surfaces the banner in others, and restarting in one tab triggers the same reload cycle in others (no tabs left dangling on a dead connection).

Server-side architecture

  • New _SupervisorOptionsError NamedTuple discriminates transport (network/DNS/token-missing → CONNECTION_FAILED 502) from validation (supervisor 4xx → CONFIG_VALIDATION_FAILED with supervisor's real status code) so the UI surfaces the right recovery suggestions.
  • _supervisor_fetch_current_options + _supervisor_merge_and_post_options capture the GET-merge-POST contract once; both _save_feature_flags and _save_backup_config use them.
  • _schedule_supervisor_self_restart fires the supervisor POST from a background asyncio task with a _SUPERVISOR_SELF_RESTART_FLUSH_DELAY_S = 0.3s head start, so the JSON response flushes through ingress before supervisor can kill the addon. Catches (ReadError, RemoteProtocolError) (expected mid-call kill), RuntimeError (token missing — defense-in-depth), and httpx.HTTPError (anything else, logged loud).
  • _PROCESS_INSTANCE_ID (uuid4 at module import) + _PROCESS_STARTED_AT exposed via /api/settings/info so the JS can prove a restart actually happened rather than trusting a 200 from the still-running OLD instance.

JS-side details

  • Module-level restartInProgress boolean guards against DevTools / programmatic re-entry (the button's disabled attribute alone doesn't catch console invocations).
  • saveFeatureFlag defaults to {restart_required: true} on a resp.ok with unparseable body so a truncated response doesn't silently hide the banner.
  • saveBackupConfig no longer reloads the form after a restart_required save (would snap back to stale env-derived values and clobber in-flight edits).
  • Optional chaining for null-checks; early returns flatten the 4xx branch.

Type of change

  • 🐛 Bug fix
  • ✨ New feature
  • 📚 Documentation
  • 🔧 Maintenance/refactor
  • 🧪 Tests only
  • 💥 Breaking change

Testing

  • I have tested these changes with a LLM agent
  • All automated tests pass (uv run pytest)
  • Code follows style guidelines (uv run ruff check)

tests/src/unit/test_settings_ui.py: 90 passed, 1 skipped (chmod 0o000 Windows-only EACCES). New tests added in this PR:

  • TestSupervisorOptionsHelpers — 7 tests covering fetch / merge / POST / restart helpers including the backup_hint-preservation, transport-vs-validation discrimination, and RuntimeError catch.
  • TestSaveBackupConfigAddonMode / TestSaveFeatureFlagsAddonMode — pin save-merge-without-restart, transport/validation error code routing, server=None guard.
  • TestSaveFeatureFlagsStandaloneMode / TestSaveToolsResponseShape — pin the unified {success, applied, mode, restart_required} shape on the file-mode + tools endpoints.
  • TestSettingsInfoEndpoint — pins instance_id + started_at presence and within-process stability.
  • TestFeatureGatedToolsCustomCode — pins the ha_manage_custom_tool stub.
  • TestRestartAddon — restructured: synchronous error paths exercise non-self slugs (the sync code path); a new test pins the self-restart "schedule + return 200" + background-task contract.

Known scope-limited test gaps

The project's JS test infra is node --check syntax-only (no JSDOM, no fetch mock). The new client-side behaviors are structurally untestable in the current setup:

  • restartInProgress concurrency guard
  • 4xx-suppress-reload branch in restartAddon
  • Poll-until-instance-id-flips cycle
  • BroadcastChannel listener (restart-required / restart-initiated)

The server-side endpoints those behaviors depend on are all covered. Closing the JS gap is tracked separately in #1422.

Checklist

  • I have updated documentation if needed

Four related addon-mode bugs in the settings web UI:

1. Server-settings toggles were rendered with the
   "Managed by the add-on Configuration tab — open Settings → Add-ons
   → ha-mcp → Configuration to edit." note and a disabled checkbox.
   They are now editable: each save POSTs through
   /addons/self/options and schedules a self-restart so start.py
   re-derives env vars from config.yaml on next boot. The two
   surfaces (web UI + Configuration tab) now stay in sync
   bidirectionally.

2. Clicking the explicit "Restart Add-on" button after saving tools
   used to show a false "Restart failed" popup even though ha-mcp
   logs proved the restart succeeded. Root cause: Supervisor killed
   the addon mid-response, HA ingress converted the dropped
   upstream into a 5xx Bad Gateway, and the JS interpreted that as
   failure. Fix: self-restart now fires from a background asyncio
   task with a small delay so the JSON response flushes through
   ingress *before* Supervisor restarts the container. Non-self
   slugs (sibling addons used by the inaddon E2E) keep the
   synchronous error-surfacing path so test assertions still work.

3. ha_manage_custom_tool (gated by enable_code_mode) did not appear
   in the tool list when its toggle was off. Other beta tools like
   ha_config_set_yaml *do* appear with a "Beta — set X" hint via
   FEATURE_GATED_TOOLS stubs. Added a stub for ha_manage_custom_tool
   so the asymmetry is gone — the tool now shows up with the same
   Beta-disabled note regardless of the addon-config toggle state.

4. POST /api/settings/backup-config returned a 400 with
   "addon_configuration_invalid_error: Missing option 'backup_hint'
   in root" because the old code POSTed only the auto-backup fields
   to /addons/self/options, dropping backup_hint (the addon
   schema's only required key). Fix: GET current options first,
   merge changes into the full options dict, then POST the
   merged result — same pattern start.py::persist_addon_options
   already uses for the secret_path persistence.

Architecture:

- Two new module-level async helpers (_supervisor_fetch_current_options
  and _supervisor_merge_and_post_options) capture the merge-then-POST
  contract once and reuse it for backup-config + feature-flag saves.
- _schedule_supervisor_self_restart wraps the background-task pattern
  with explicit task-set retention so RUF006 stays happy and the
  fire-and-forget coroutine isn't GC'd before it can POST.
- The cross-tab restart-required banner is hoisted from inside
  panel-tools to a top-level sibling, so a save from the
  Server Settings or Backups tab surfaces the same visible notice
  regardless of which tab the user is on.

Tests: 80 settings_ui unit tests pass (Windows-only kill_signal /
filesystem-symlink skips are pre-existing and unrelated). The
restart synchronous-error paths now exercise non-self slugs; a new
test pins self-restart's "schedule + return 200" contract.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses four bugs in the addon-mode settings UI by standardizing how configuration changes are persisted to Supervisor. By implementing a robust GET-merge-POST pattern and moving self-restart operations to background tasks, the changes ensure that addon settings remain consistent with Supervisor options and eliminate false-positive error reports during restart operations.

Highlights

  • Addon Settings Sync: Implemented bidirectional sync for server-settings toggles by using a GET-merge-POST pattern with Supervisor's /addons/self/options endpoint.
  • Restart Reliability: Fixed false 'Restart failed' errors by scheduling self-restarts in a background task, ensuring the success response flushes through the ingress proxy before the container restarts.
  • Backup Configuration: Resolved 'Missing option backup_hint' errors by merging user changes into the full options dictionary before saving, preventing schema validation failures.
  • UI Improvements: Added 'ha_manage_custom_tool' to the feature-gated tools list and hoisted the restart-required banner to a global position for better visibility across all tabs.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the settings UI by enabling the editing of "addon" origin feature flags directly from the web interface. It addresses a critical bug where partial configuration updates to the Supervisor would fail due to missing required schema keys (such as backup_hint) by implementing a fetch-merge-post pattern. Additionally, it introduces a background task for self-restarts with a slight delay to ensure HTTP responses are fully flushed before the process terminates, preventing 5xx Bad Gateway errors in the UI. The UI layout was also improved by moving the restart notice to a global position and adding the ha_manage_custom_tool to the gated tools list. Comprehensive unit tests have been included to verify the new Supervisor integration and restart logic. I have no feedback to provide as the implementation is robust and well-tested.

Addresses code-reviewer, comment-analyzer, pr-test-analyzer, and
code-simplifier findings from the PR review toolkit pass on
fix/addon-ui-settings-sync.

Critical
- Transport vs validation errors now route to the right ErrorCode
  with the right HTTP status: supervisor 4xx on the merge POST maps
  to CONFIG_VALIDATION_FAILED with supervisor's status code
  preserved (so the UI shows the real 4xx); network / DNS / token /
  malformed-response failures map to CONNECTION_FAILED with 502 so
  the UI surfaces the "is HA reachable" recovery suggestions.
  Collapsing both into a single 502 sent users down the wrong
  recovery path in the previous version. Mechanism: a new
  _SupervisorOptionsError NamedTuple discriminates kind/message/status
  and both _save_feature_flags and _save_backup_config branch on it.

Defense-in-depth
- _supervisor_fetch_current_options and _supervisor_merge_and_post_options
  now catch RuntimeError from make_supervisor_httpx_client (raised
  when SUPERVISOR_TOKEN is unset) and classify it as a transport
  error, so a future third caller missing the env-var gate gets a
  clean 502 instead of an uncaught 500.

Dead-code removal
- Removed the inner `if not os.environ.get("SUPERVISOR_TOKEN")`
  guards in both addon-mode save branches. They are unreachable —
  entry to those branches already requires the token to be set (via
  is_running_in_addon() / get_feature_flag_origin() returning
  "addon"). The helpers' new RuntimeError catch covers the
  defense-in-depth case.

Mixed-origin invariant
- _save_feature_flags now tracks both addon_writes and
  file_or_default_writes and returns INTERNAL_ERROR 500 if a single
  batch ever contains both — currently impossible by construction
  (get_feature_flag_origin returns one mode per environment), but
  the explicit guard makes any future regression fail loudly
  instead of silently routing a file-mode write through Supervisor.

Comments and docstrings
- Dropped the dangling `(#TODO: refer to PR)` placeholder in
  _save_backup_config — rewritten as plain past-tense prose.
- Fixed the _supervisor_fetch_current_options cross-reference: it
  mirrors maybe_persist_secret_path (which spreads existing config
  before posting), not persist_addon_options (which documents that
  callers must do the spread).
- Moved the addon-mode "restart scheduled" comment in
  saveFeatureFlag inside the `if (data.restarting)` branch so it no
  longer reads as unconditional.
- Promoted the 0.3-second restart-flush delay to
  _SUPERVISOR_SELF_RESTART_FLUSH_DELAY_S with a docstring anchoring
  the "tuned conservatively" claim. Tests still override via kwarg.

Tests
- TestSupervisorOptionsHelpers gains
  test_fetch_current_options_accepts_bare_options_dict,
  test_fetch_current_options_runtime_error_returns_transport_error,
  test_merge_and_post_transport_error_returns_transport_kind,
  and the existing supervisor-error tests now assert on the
  NamedTuple's `kind`/`status_code`/`message` shape.
- TestSaveBackupConfigAddonMode and TestSaveFeatureFlagsAddonMode
  gain explicit transport-vs-validation tests that pin the
  error-code routing contract end-to-end.
- TestSaveFeatureFlagsAddonMode also pins the server=None sidecar
  guard.
- Schedule-self-restart tests now wait deterministically via a new
  _drain_background_restart_tasks helper instead of
  asyncio.sleep(0.05) — eliminates CI-runner flakiness.
- test_invalid_slug_in_body_falls_back_to_self switched from
  inline httpx.AsyncClient patching to the class's
  _patch_supervisor_client helper for consistency.

Verified: ruff check, ruff format, mypy src/, and the full
test_settings_ui.py suite (85 passed, 1 Windows-only skip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request May 23, 2026
… Fork-Dev dev124

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kingpanther13 and others added 3 commits May 23, 2026 10:24
Every save endpoint (Tools, Server Settings, Backups) now goes through
the same restart UX: persist the change, surface a "Saved — restart
required" toast and the cross-tab restart banner, and let the user
pick the moment to actually fire the supervisor restart via the
global Restart Add-on button. No save handler auto-restarts the addon
anymore. The button auto-reloads the page 10 seconds after the click.

Why
- Adjusting a Server Setting toggle still produced a "Restart failed"
  alert in addon mode. Root cause: the previous fix moved each save's
  supervisor self-restart into a background task, but the user-visible
  outcome was unchanged — every toggle change immediately fired a
  supervisor restart, and if the user toggled multiple settings in
  quick succession, a later POST would race the in-flight restart
  (supervisor kills the addon → ingress returns 5xx → JS shows
  "Restart failed").
- Saving auto-backup settings on the Backups tab jumped straight to
  "addon is restarting" with no preview, no second-chance confirm,
  and no way to bundle multiple edits before paying the restart cost.
- Clicking the explicit Restart button on the Tools tab left the page
  stuck on "Restart initiated — reload page in ~30s" forever; the
  user had to manually F5.

Server changes
- ``_save_feature_flags`` addon branch: drop
  ``_schedule_supervisor_self_restart`` call. Return
  ``{success, applied, mode, restart_required: True}`` instead of
  ``{..., restarting: True}``.
- ``_save_backup_config`` addon branch: same — no auto-restart.
- ``_save_backup_config`` file branch: rename ``restarting: False``
  → ``restart_required: False`` for naming consistency across all
  save endpoints.
- ``_save_feature_flags`` file/default branch: now also returns
  ``restart_required: True`` (matches the FEATURE_META copy on every
  flag, which says "Requires restart to take effect"). The UI
  surfaces the banner; the restart button stays hidden in non-addon
  mode since there is no supervisor to drive it.

JS changes
- ``saveFeatureFlag``: drops the "add-on is restarting…" branch.
  Every successful save shows ``Saved — restart required`` and
  surfaces the cross-tab banner.
- ``saveBackupConfig``: branches on ``data.restart_required`` instead
  of ``data.restarting``. Re-enables the Save button so the user can
  bundle further edits before clicking Restart.
- ``restartAddon``: auto-reloads the page after
  ``RESTART_RELOAD_DELAY_S = 10`` seconds. 4xx responses (genuine
  config error, e.g. SUPERVISOR_TOKEN missing) suppress the reload
  and surface the error inline; 5xx-from-ingress and network drops
  fall through to the reload because both mean the supervisor killed
  our upstream and the restart is in flight.
- ``ORIGIN_INFO_NOTE.addon`` and ``BACKUP_ORIGIN_LABELS.addon``:
  "save will restart the add-on" → "restart required after save".

Test changes
- ``test_addon_save_merges_and_schedules_restart`` renamed to
  ``test_addon_save_merges_without_restart_returns_restart_required``
  for both the backup-config and feature-flags variants. New
  assertions: ``schedule_mock.assert_not_called()``,
  ``body["restart_required"] is True``,
  ``"restarting" not in body``.
- ``test_standalone_writes_file_and_invalidates_cache``: assert the
  new field name.

Verified: ruff check, ruff format, mypy src/, 85 passing in
test_settings_ui.py (1 pre-existing Windows skip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c, unified shape

Addresses all reviewer + idiot-checker findings on the previous
unified-restart-flow commit. The intent stays the same: save commits
the change, the global Restart Add-on button is the single restart
path, the page reloads automatically when the addon comes back. The
implementation is now actually robust enough to deliver that.

Server
- ``_save_tools`` now returns the same
  ``{success, applied, mode, restart_required}`` shape as
  ``_save_feature_flags`` and ``_save_backup_config``. The previous
  ``disabled``/``pinned`` count fields had no consumers (JS read
  none; no test asserted on them) and pre-empted the unified
  contract — replaced with ``applied`` (the states dict) and
  ``mode: "file"``.
- ``_save_feature_flags`` file/default branch now also returns the
  unified shape including ``applied`` (was a bare ``{"success":
  True}`` before). Standalone / Docker / Claude Desktop users get
  the same restart banner the addon-mode users get.
- ``_save_feature_flags`` docstring no longer claims the addon
  branch schedules a restart; describes the new pending-then-explicit
  flow.
- ``_save_backup_config`` docstring same fix.
- File-mode feature-flag comment no longer makes a grep-falsifiable
  claim about FEATURE_META text; describes the semantic instead.
- "reported by the user" rot-prone phrasing in
  ``_save_feature_flags`` replaced with the enduring tech reason.
- ``_schedule_supervisor_self_restart`` now catches ``RuntimeError``
  from ``make_supervisor_httpx_client`` (raised when
  ``SUPERVISOR_TOKEN`` is unset). Mirrors the parity catch the two
  supervisor options helpers already had. Without it, a race that
  unsets the token between handler entry and the 300ms-later task
  wakeup would surface only as asyncio's "Task exception was never
  retrieved" at GC time.

JS — restart flow
- 10-second fixed timer replaced with poll-until-ready against
  ``./api/settings/info``. Initial 3-second grace lets supervisor
  actually kill the addon before we start probing (so a too-eager
  first probe doesn't return 200 from the OLD instance and reload
  too early); then poll every 2 seconds for up to 60 seconds.
  Reloads as soon as the new instance answers; falls back to
  "did not come back online — reload manually" if the probe
  times out. Obsoletes the magic-number constant.
- Concurrency guard: module-level ``restartInProgress`` boolean
  checked at function entry. Guards against DevTools / programmatic
  invocation that bypass the button's ``disabled`` attribute.
  Cleared only on a 4xx genuine config error so the user can fix
  the underlying cause and retry; otherwise stays true through the
  full restart cycle.
- ``restartAddon`` flattened to early-return on the 4xx config-error
  branch — removes the ``configError`` flag.
- Optional-chaining for ``err?.error?.message`` (drops the verbose
  nullish chain).
- Empty ``catch (_e) {}`` for the network-drop path now logs a
  ``console.warn`` so DevTools shows what happened — useful when
  debugging "I clicked Restart and nothing happened" reports.

JS — multi-tab sync (BroadcastChannel)
- New ``restartChannel`` on ``BroadcastChannel('ha-mcp-settings')``.
  Falls back to ``null`` on browsers without the API; every channel
  use is null-guarded so older browsers see no regression.
- ``saveConfig`` / ``saveFeatureFlag`` / ``saveBackupConfig`` post
  ``{type: 'restart-required'}`` when their response carries
  ``restart_required: true``. Other open tabs surface the same
  banner.
- ``restartAddon`` posts ``{type: 'restart-initiated'}`` BEFORE
  starting its poll cycle. The listener kicks off the same
  poll-then-reload cycle in every other tab so they all come back
  to the fresh addon instead of dangling on a stale connection.

JS — quality of life
- ``saveFeatureFlag`` JSON-parse fallback: on ``resp.ok`` with a
  truncated / non-JSON body, default to ``{restart_required:
  true}`` so the banner still shows. Silently hiding the banner
  would make the user think the change took effect live.
- ``saveBackupConfig`` no longer calls ``loadBackupConfig()`` after
  a ``restart_required: true`` save. In addon mode the GET reads
  env-derived values that are still stale (Supervisor has the new
  options but ``start.py`` doesn't re-derive env vars until the
  next boot) — reloading the form snapped it back to old values
  and wiped any in-flight edits the user wanted to bundle.

Tests
- ``test_schedule_self_restart_catches_runtime_error`` pins the
  RuntimeError parity catch with a caplog assertion targeting the
  specific "SUPERVISOR_TOKEN unset" message.
- ``TestSaveFeatureFlagsStandaloneMode::test_standalone_save_returns_unified_contract_shape``
  pins the new file-mode response shape.
- ``TestSaveToolsResponseShape::test_save_returns_unified_contract_shape``
  pins the new unified shape on ``_save_tools`` and asserts the
  retired ``disabled``/``pinned`` count fields do not leak through.

Verified: ruff check, ruff format, mypy src/, 88 passing in
test_settings_ui.py (1 pre-existing Windows skip). ``node --check``
on the rendered ``<script>`` still passes — confirms the
BroadcastChannel + polling code is syntactically valid JS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t 200

Closes the silent-restart-failure edge case in the new poll-until-
ready cycle. Without this, a supervisor restart that silently
fails (e.g. ``RuntimeError`` from a missing token, supervisor 4xx,
or any future failure mode that logs server-side but doesn't
surface to JS) would leave the OLD addon instance still answering
the JS probe with 200. The page would reload at T+3s to the same
state, the user would see no change, and the only signal would be
in addon logs — exactly the "broken page with no error message"
UX this PR is supposed to eliminate.

Server
- Module-level ``_PROCESS_INSTANCE_ID`` (uuid4 hex, generated at
  import) + ``_PROCESS_STARTED_AT`` (epoch seconds at import).
  Both surfaced via ``GET /api/settings/info``. A fresh Python
  process gets a fresh uuid; an addon restart that actually swaps
  processes flips it.

JS
- Replaced ``_probeAddonReady`` with ``_probeAddonRestarted(prev)``
  that polls ``/api/settings/info`` until ``instance_id`` differs
  from the captured baseline. ``prev=null`` (e.g. when the
  pre-restart fetch fails or an older server build lacks the
  field) falls back to the old "any 200 means up" behavior so we
  degrade gracefully.
- ``restartAddon`` captures ``previousInstanceId`` via a new
  ``_fetchSettingsInfo()`` helper BEFORE firing the supervisor
  POST, then threads it through the reload cycle.
- ``BroadcastChannel`` ``restart-initiated`` message now carries
  ``previousInstanceId`` so every open tab compares against the
  same pre-restart baseline. Without this, other tabs would either
  capture a stale baseline mid-restart or fall through to the old
  status-only probe.

Tests
- ``TestSettingsInfoEndpoint::test_returns_instance_id_and_started_at``
  pins the field presence and types.
- ``TestSettingsInfoEndpoint::test_instance_id_stable_within_process``
  pins the within-process stability invariant — without this, two
  calls in a row could see the value flip, the JS would reload
  immediately, and the restart-detection contract would silently
  break.

Verified: ruff check, ruff format, mypy src/, 90 passing in
test_settings_ui.py (1 pre-existing Windows skip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request May 23, 2026
… Fork-Dev dev126

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two sidecar tests asserted ``resp.json() == {"is_addon": ...,
"is_sidecar": ...}`` against the full settings-info payload. The
preceding commit added ``instance_id`` and ``started_at`` to that
payload (needed by the restart-then-reload JS cycle to prove a
restart actually happened) which broke the exact-equality checks.

Switch to field-by-field assertions on the deployment-mode fields
those tests are actually about — the new per-process identity
fields are covered separately by
``TestSettingsInfoEndpoint`` in ``test_settings_ui.py``.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request May 23, 2026
… Fork-Dev dev127

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported: typing a query in the Tools tab search bar filtered the
list correctly, but toggling a setting on a visible tool snapped the
full list back into view — even though the search bar still showed
the query.

Root cause: ``render()`` (called after every toggle change via
``scheduleSave(); render();``) does ``container.innerHTML = ''`` and
rebuilds the entire ``.tool`` DOM. The search filter applied the
``hidden`` class imperatively to the OLD nodes; the freshly-built
nodes had none. The ``<input>`` element itself is outside the
rebuilt container, so its value persists — producing the
visible-query-but-no-filter mismatch the user reported.

Fix: extract the filter into ``applyToolSearch()`` that reads the
search input's current value directly and applies / clears the
``hidden`` class on every ``.tool`` node it finds. Wire it to the
input's ``input`` event (was inline before) and also call it at the
end of ``render()`` so any rebuild — toggle change, group-master
toggle, future render triggers — re-applies the active filter
against the new DOM.

No regression risk on the no-search path: an empty query short-
circuits ``!q`` to true and every tool stays visible, same outcome
as before. Group auto-expansion when search has matches still
works (q && visible branch unchanged).

Not directly testable in the current JS test infra (the project
bar is ``node --check`` syntax-only — no JSDOM, no DOM event
simulation). Closing that gap is tracked in homeassistant-ai#1422; ``node --check``
on the rebuilt script confirms no syntax regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kingpanther13
kingpanther13 marked this pull request as ready for review May 23, 2026 17:01
@kingpanther13
kingpanther13 requested review from a team and julienld May 23, 2026 17:01
@kingpanther13
kingpanther13 enabled auto-merge (squash) May 23, 2026 17:01
@kingpanther13
kingpanther13 requested a review from Patch76 May 23, 2026 17:02
@kingpanther13
kingpanther13 disabled auto-merge May 23, 2026 17:02
@kingpanther13
kingpanther13 enabled auto-merge (squash) May 23, 2026 17:02
@Patch76
Patch76 disabled auto-merge May 23, 2026 17:24

@Patch76 Patch76 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The merge-then-POST pattern with _SupervisorOptionsError discriminating transport vs validation is the right call — collapsing both into a single 502 in the previous shape sent users down the wrong recovery path. The instance_id baseline propagation through the restart-initiated BroadcastChannel message is the key insight that makes the multi-tab reload actually robust against silent supervisor no-ops.

Two minor observations, not blockers:

  • The PR has no Fixes # / Closes # link. The four bugs are described in the body but issue triage and changelog generation lose the cross-reference.

  • The final applyToolSearch commit (search-filter rebuild bug) is unrelated to the addon-settings-sync theme and could have shipped separately. Inside settings_ui.py already so the boy-scout rule applies; flagging for future scope discipline, not this PR.

Server-side coverage is comprehensive; the BroadcastChannel and restartInProgress JS path is correctly architected even though it's structurally untestable in the current node --check-only infra (tracked in #1422).

@kingpanther13
kingpanther13 merged commit 64bba1c into homeassistant-ai:master May 23, 2026
17 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Your changes are now in the dev channel!

Your PR has been merged to master and is available for testing in the dev channel.

Test your changes before the next stable release (biweekly Wednesday):
📖 Dev Channel Documentation

Quick start

# Run dev version
uvx ha-mcp-dev

# Check version
uvx ha-mcp-dev --version

Docker:

docker pull ghcr.io/homeassistant-ai/ha-mcp:dev
docker run --rm -i \
  -e HOMEASSISTANT_URL=http://your-ha:8123 \
  -e HOMEASSISTANT_TOKEN=your_token \
  ghcr.io/homeassistant-ai/ha-mcp:dev

Found an issue? Please open a new bug report and mention this PR for context.

@kingpanther13

Copy link
Copy Markdown
Member Author

There was no issue filed....It's totally fine to go straight to a PR without filing an issue, especially for bugs. These were all directly observed bugs that I should have caught when I made the previous changes to the web UI but missed them.

kingpanther13 added a commit to kingpanther13/ha-mcp-fork that referenced this pull request May 23, 2026
…omeassistant-ai#966)

Wires the new addon-config toggle into FEATURE_FLAG_FIELDS so it appears
in the Server Settings tab and rides PR homeassistant-ai#1420's _save_feature_flags +
_supervisor_merge_and_post_options + _schedule_supervisor_self_restart
flow when toggled in addon mode.
@Patch76

Patch76 commented May 23, 2026

Copy link
Copy Markdown
Member

Blame Claude ;-)

@kingpanther13

Copy link
Copy Markdown
Member Author

No worries! I know your Claude is obsessed with "scope" lol.

kingpanther13 added a commit that referenced this pull request May 24, 2026
* feat(policy): scaffold policy package for per-tool approval (#966)

* feat(policy): add Predicate/Rule/Policy data models (#966)

* feat(addon): expose enable_per_tool_approval option (#966)

* feat(config): add enable_per_tool_approval setting (#966)

* feat(policy): atomic load/save for tool_policy.json (#966)

* feat(addon): wire enable_per_tool_approval through start.py + docs (#966)

* feat(policy): args-hash + remember-cache for approval queue (#966)

* feat(policy): predicate evaluator (eq/in/regex/exists/...) (#966)

* feat(policy): pending entries with TTL, decisions, and event signalling (#966)

* feat(policy): PolicyMiddleware happy-path branches (#966)

* test(policy): cover block/deny/timeout/recall/remember branches (#966)

* feat(policy): /api/policy/* Starlette handlers (#966)

* fix(policy): wrap ValidationError, scope contains op, regex doc, test bind (#966)

* feat(policy): Policies tab in web UI + sidecar route wiring (#966)

* feat(policy): register PolicyMiddleware on the FastMCP server (#966)

* fix(policy): return 400 on malformed approve/deny bodies (#966)

* feat(toolsearch): unpin yaml-edit and code-mode tools, gated by approval middleware (#966)

* chore: ruff format + lint cleanup for policy package (#966)

- ruff format reflow on PR-touched files (case statements split to two
  lines, function signatures, line continuations).
- UP042: Verdict now inherits from StrEnum instead of (str, Enum).
- E402: hoist `import anyio` to the top of test_approval_queue.py.
- I001: sort imports in test_evaluator.py and test_model.py.

No behavior change.

* fix(policy): mypy narrowing for evaluator comparisons (#966)

`Predicate.value` is `Any | None` and `extract_path` returns `Any`, so
`val == pv`, `val > pv`, etc. inherit `Any` and trip the project's
`warn_return_any` mypy setting on functions declared `-> bool`. Wrap
the comparison branches in `bool(...)` to make the narrowing explicit.

Also guard the `regex` branch with `isinstance(pv, str)` so `re.search`
receives a definite `str` instead of `Any | None`; a non-string regex
value now returns False instead of raising TypeError at evaluation
time, which is the only sensible behavior for a malformed pattern.

No change to any test's expected outcome.

* docs: credit @L1AD and PolicyLayer for #966 inspiration

* docs(addon): fix wrong YAML example in enable_per_tool_approval section (#966)

* fix(policy): CI green + critical bugs from reviewer cycle (#966)

- expires_in_seconds: use time-remaining not total TTL
- middleware: fail-closed on corrupt policy load (was crashing all gated calls)
- handlers: 500-with-corrupt-flag on get_config when policy invalid
- approval URL: wire secret_prefix via lazy getattr so HTTP-standalone
  emits a usable absolute path
- approve/deny: return bool, 409 on already-decided
- Predicate: field validators for op/value compatibility (Gemini)
- persistence: explicit UTF-8 encoding (Gemini)
- middleware: reuse tools/helpers safe_progress
- handlers comment: fix wrong "next call" claim
- test_middleware: unwrap ExceptionGroup for pytest.raises (CI fix)
- test_stdio_settings_sidecar: include new policy_* handler keys (CI fix)

* refactor(policy): drop default_action + tighten Rule.tool_name (#966)

- Policy schema simplified: no default_action field. System is always
  "allow unless a rule matches; rule = require approval". The previous
  default_action='require_approval' option was a bricked-config trap
  since rules can't grant allow-overrides.
- Rule.tool_name now rejects empty string; wildcard '*' documented in
  the docstring.
- Evaluator simplified to match.
- Tests updated/added.

* refactor(policy): encapsulate decision state + clean naming (#966)

- PendingApproval.decide() encapsulates the decision/event coupling;
  property guards read-only access to decision.
- __post_init__ validates expires_at > created_at.
- ApprovalQueue docstring spells out single-process scope and
  restart-loses-tokens semantics.
- Rename args_preview -> args throughout (it was always the full
  unmodified args; "preview" was misleading).
- Remove on_policy_change dead parameter from build_policy_handlers.

* fix(toolsearch): default-pinned tools should be user-unpinnable (#966)

- Computed pinned set now respects tool_config.json — explicit "enabled"
  state removes from defaults.
- Remove ha_restart / ha_reload_core from DEFAULT_PINNED_TOOLS (recovery
  actions, low frequency, low value in default LLM tool surface).
- Add ha_manage_backup to MANDATORY_TOOLS (operational essential).
- Server now declares _settings_secret_prefix on __init__ for pyright.

* refactor(policy): rename feature to "Tool Security Policies" (#966)

User-facing rename: addon config option, env var, Settings attribute,
UI tab label, addon DOCS sections, translations, server method.
Internal Python naming (policy/ package, tool_policy.json, /api/policy/*
routes, class names like PolicyMiddleware/ApprovalQueue) unchanged for
less churn.

* docs: small comment polish for policy review nits (#966)

* feat(ui): per-tool security-gated toggle in Tools tab (#966)

* test(policy): integration + timing-isolation coverage gaps (#966)

- test_server_policy_wiring: assert _apply_tool_security_policies attaches
  middleware + approval_queue when enabled, neither when disabled.
- test_settings_ui_handler_selection: parametrize the live-vs-stub branch
  in build_settings_handlers (sidecar / no server / no queue / live).
- test_middleware wait-loop timing: assert event-wake exit, not polling.
- test_middleware multi-rule precedence: first-match wins for remember_minutes.

* feat(ui): rewrite Tool Security Policies tab — per-tool cards + predicate editor (#966)

* feat(config): expose enable_tool_security_policies as a feature flag (#966)

Wires the new addon-config toggle into FEATURE_FLAG_FIELDS so it appears
in the Server Settings tab and rides PR #1420's _save_feature_flags +
_supervisor_merge_and_post_options + _schedule_supervisor_self_restart
flow when toggled in addon mode.

* fix(policy): address all verified review findings + CI failures (#966)

CI fixes:
- ruff: drop dead noqa: SLF001 suppression
- unit test: test_missing_path_never_matches_except_exists uses
  op-compatible values so Predicate field_validator doesn't reject

Review findings:
- FEATURE_META entry for enable_tool_security_policies (toggle now
  renders in Server Settings tab)
- Approval URL points to /settings?tab=tool-security-policies (was
  POST-only /api/policy/approve which 405'd on browser open)
- policyDecide surfaces network errors + 409 current_decision
- Policy gains version field for optimistic concurrency; PUT 409s
  on version mismatch; client surfaces 'reload before saving'
- _apply_tool_security_policies failure logs spell out security
  impact (TOOL SECURITY GATING IS NOT ACTIVE) and include
  data_dir/env-var context
- Validator rejects value on op='exists'; gt/lt TypeError degrades
  to False
- ToolVisibilityResult -> UserToolStateOverrides, fields are
  frozenset, disjointness asserted
- PendingApproval.event private; expose async wait()
- _SupervisorOptionsError gains transport()/validation() classmethods
  encoding kind->status_code pairing
- Wiring test binds queue identity; handler-selection covers all 3
  live routes
- Comment + doc polish (audit-trail claim, e2e docstring path,
  internal Task references)

* fix(policy): CI green + real e2e test for the approval flow (#966)

- ruff format: tests/src/unit/test_settings_ui.py
- test_save_and_roundtrip: account for save_policy version bump
- test_serialized_shape_is_stable: include 'version' in expected keys
- test_addon_save_returns_500_when_server_is_none: guard
  server._settings_secret_prefix assignment with None check
  (regression from #4's secret-prefix wiring)
- tests/src/e2e/policy/test_approval_flow.py: real e2e exercising
  block -> approve -> re-call cycle with strict args-binding rejection
  on mutated args. Skip-stub replaced with real test driving the live
  middleware via mcp_client + /api/policy/* HTTP.

* fix(ui): broken quote escaping in predicate-form placeholder breaks JS parse (#966)

The Python source `'placeholder=\\'\"lock\"...\\\\'>'` rendered as JS
`'placeholder='\"lock\"..'>'` — the single quote inside the HTML attribute
value closed the outer JS string literal, and subsequent tokens (\"lock\",
'or', '[', ...) broke parsing. With a syntax error in the inline <script>,
the browser stopped executing — Tools tab stuck on 'Loading...', tabs
unclickable.

Switched to a JS-safe double-quoted attribute with &quot; for the
embedded double quotes in the placeholder hint.

* fix(ui): gated toggle reads addon-config flag, not Policy.enabled (#966)

The per-tool 'security gated' toggle was grayed out even when the user
had enable_tool_security_policies turned ON in the addon config + the
Server Settings tab toggle, because the JS was reading Policy.enabled
(the file field) instead of the addon-config feature flag — which is
the single source of truth for whether the middleware is active.

loadPolicyState now reads enable_tool_security_policies from
/api/settings/features (same place renderFeatureFlags consumes from).

* fix(policy): Policy.extra=ignore so old persisted files load cleanly (#966)

Persisted tool_policy.json files from an earlier revision of this PR
carry default_action (since dropped) and rejected with ValidationError
on load — surfacing as 'Could not load policy: 500' when the user
clicked the per-tool gated toggle.

Predicate/Rule keep extra=forbid (typo catching at construction).

* fix(policy): drop Policy.enabled — addon-config flag is the sole switch (#966)

The middleware's server-side gate was checking `policy.enabled` (a
file field with no UI surface), so it returned ALLOW on every call
regardless of rules. The addon-config flag
(`enable_tool_security_policies`) was supposed to be the only switch
— and the middleware is only registered when that flag is true — so
the inner `policy.enabled` check was both redundant and broken.

Remove the field, remove both server-side checks (middleware +
evaluator), update tests, and refresh the JS comment that referred
to it.

* fix(policy): drop approve_url, instruct LLM to send user to settings page (#966)

The relative-path approve_url doesn't resolve cleanly through cloudflared
or other reverse-proxy deployment modes — the LLM can't safely hand it
to the user. The user already knows where the Tool Security Policies
tab is (they set the rule from it), and that page lists all pending
approvals, so a per-request URL is unnecessary noise.

- Drop approve_url from USER_APPROVAL_REQUIRED context; keep `token`
  so a caller could correlate but the user doesn't need to act on it.
- Update message + progress text to instruct the LLM to tell the user
  to open the settings UI Tool Security Policies tab.
- Drop the now-unused approval_url_builder param + the
  _settings_secret_prefix plumbing in server.py / settings_ui.py.

Also fix the failing test_defaults (asserted dropped Policy.enabled field)
and the e2e test PUT body that still carried `"enabled": True`.

* feat(policy): schema-driven condition builder for write/destructive tools (#966)

The previous "Add predicate" UX required users to type both the dotted
arg path (e.g. `args.domain`) and the value as JSON. Two problems:
  1. They need to know what fields each tool takes.
  2. They need to know what values are legal (which HA domains exist,
     which entities, etc.).

Replace the free-text path input with a dropdown sourced from the tool's
JSON schema, and replace the free-text value input with a (multi-)select
sourced from HA when the path has a known value source (domain, service,
entity_id today; trivially extensible). Free-text is still available via
an "(other — type a path)" escape hatch and as the automatic fallback
for ops that don't pair with a registry (regex / contains / gt / lt).

Server:
- New `/api/policy/tool-schema?name=...` returns
  `{paths: [...], value_sources: {path: source_key}}`. Read-only tools
  return empty paths so the UI falls back to free-text (gating those is
  low-value but still permitted manually).
- New `/api/policy/value-source?source=...` resolves a source key to a
  live list of choices. In-process 30s TTL cache avoids hammering HA
  when the user explores paths.
- value_sources.py registry maps (tool_name, arg_path) → source_key for
  the common write/destructive surface (call_service, set_entity,
  set_integration_enabled, get_history, etc.). New mappings are one
  dict entry plus, if a new source key, one fetcher.
- Both endpoints mount in addon + secret-prefix routes. Sidecar serves
  503 stubs (no FastMCP registry / HA client in that process).

UI: rename user-facing "predicate" → "condition" (CS jargon → SQL/JIRA
terminology users actually recognise; internal Pydantic class stays
`Predicate` so the wire format is unchanged). Form fetches the schema
lazily on first open, caches it on the card, refetches value choices
when path/op changes.

Includes test_schema_handlers.py covering: missing-name 400, sidecar
503, unknown-tool 404, read-only empty-paths, write-tool paths +
registry, JSON-schema enum passthrough, value-source 400 paths, both
HA-services payload shapes, domain filtering for entities/services,
and upstream-fetch 502 mapping.

* test: include new policy handler keys in sidecar all-keys assertion (#966)

* fix(ui): clearer condition-builder labels, optional value, bareword input (#966)

User feedback on the new form was:
  1. "args.foo" path placeholder is gibberish; no real label on path/value
  2. value box should not be mandatory for ops where backend allows None
  3. typing `lock` into the value box errored with "Invalid JSON" — every
     normal-looking input has to be quoted
  4. for ha_call_service `data` was the only arg without an obvious meaning

Changes:
- Real `<label>`s on the form rows: "Argument:", "Match when:", "Value:".
- Op dropdown shows friendly text ("is present (any value)", "equals",
  "is one of", "matches regex", etc.); wire values unchanged.
- Hint line under the value row reflects the current op so users know
  whether a value is required and roughly what shape it should take.
- Value is now OPTIONAL for ops where the backend accepts a missing
  field (exists, eq, neq, contains). Submitting an empty value omits
  the `value` key from the predicate entirely.
- Bareword inputs auto-coerce: `lock` → `"lock"`, `lock,alarm` → list,
  `42` → number, `true` → bool. Falls back to a clearer error if even
  the smart-coercion can't make JSON.
- Path dropdown options now carry the schema `description` as a
  `title` tooltip, so `data` reads as "Service data dict" on hover
  instead of being a mystery.
- Schema-declared enums render as a value dropdown automatically (no
  registry entry needed) when the path's JSON-schema has `enum`.

Also fix /tmp/extract_js.py — naive paren-counter broke once form
strings started containing parens; switch to ast.parse so future
edits don't silently break the harness.

* feat(policy): wildcard path "args.*" + clearer empty-value semantics (#966)

User asked for a catch-all "any argument equals X" condition and called
out that the previous "Leave blank to gate on null" hint was nonsensical
— a blank value should mean "any value" to a normal user, not "match
the null literal".

Backend:
- Refactor evaluator: `extract_path` → `iter_path_values`, which yields
  every value the dotted path resolves to. A `*` segment fans out across
  the current node (dict values for dicts, items for lists). A path like
  `args.*` thus yields every top-level arg; `args.config.*` yields every
  leaf of the config sub-dict.
- `match_predicate` rewrites to "ANY matching value satisfies the op",
  which collapses to the previous single-value semantics for non-wildcard
  paths. So `path=args.*, op=eq, value="lock"` gates whenever any arg of
  the tool call equals "lock".

UI:
- New "(any argument)" option at the top of the path dropdown, fills
  `args.*` and carries a tooltip explaining the semantic.
- VALUE_OPTIONAL_OPS shrinks to just `exists` — blank value is no longer
  silently accepted for eq/neq/contains. Instead, the value-required
  error fires, and the hint text under the field tells the user to
  switch op to `is present` if they wanted "any value".
- Hint copy revised across all ops so the "what happens with this op +
  blank value" question has a clear answer at each step.

Tests:
- New TestIterPathValues covering top-level, nested, missing, and the
  three wildcard shapes (dict values, list items, empty).
- New TestWildcardPredicate covering eq/in/exists/regex matching via
  `args.*` plus an end-to-end evaluate() test.
- Existing tests still pass with the refactored matcher; signatures of
  the public functions are unchanged.

* fix(ui): default condition path to '(any argument)'; relabel error (#966)

- Drop the '(pick an argument)' placeholder; default the path dropdown
  to '(any argument)' so the form is immediately submittable.
- 'path is required' error reads 'argument is required' if it ever
  fires (it won't on the happy path now).

* feat(policy): auto-save conditions + surface matched_rule in approval error (#966)

UI:
- Drop the manual "Save changes" button on each rule card. Conditions
  now PUT to disk the moment the user clicks "Save condition", clicks
  the × on a condition row, or edits the remember-minutes field
  (debounced 500ms). The only feedback is a small "Saving…" / "Saved."
  status line next to the card.
- Removed the now-dead .policy-save-rule CSS and the markDirty helper.

Server:
- USER_APPROVAL_REQUIRED error context now carries `matched_rule` with
  the rule's tool_name + when[]. Lets the user (and the LLM) tell at a
  glance which rule fired, instead of guessing whether their condition
  saved correctly.

* feat(policy): case-insensitive string comparison in all ops (#966)

Security gates shouldn't fire differently based on whether the LLM
capitalised its argument — 'Lock' and 'LOCK' and 'lock' are the same
operationally. eq/neq/in/not_in/contains lower-case both sides before
comparing when both are strings; regex uses re.IGNORECASE. Non-string
types pass through unchanged so int(1) != str('1') still holds.

* fix(policy): mypy bool cast + broaden e2e coverage (#966)

mypy: bool(_ci(val) == _ci(pv)) — _ci returns Any (passes non-strings
through unchanged), so eq/neq comparisons need an explicit bool wrap.

Tests: previous e2e only covered the happy block→approve→re-call path.
Add four more cases against the live testcontainer:
- wildcard `args.*` gates when any arg matches the value
- wildcard `args.*` passes through when no arg matches
- case-insensitive matching (rule 'lock' gates caller 'LOCK')
- deny → middleware raises USER_DENIED, tool never runs
- remember_minutes>0: second call within the window skips the queue

* refactor(policy): address review-cycle findings (#966)

Gemini (6 unresolved threads):
- Migrate POLICY_LOAD_FAILED / USER_DENIED / USER_APPROVAL_REQUIRED off
  manual `ToolError(json.dumps(...))` onto the canonical
  `raise_tool_error(create_error_response(...))` pattern. Added the
  three error codes to ErrorCode enum.
- Hoist sync `load_policy()` off the event loop via
  `anyio.to_thread.run_sync` in the middleware's policy provider call.
- Add justification comment on `local_provider._list_tools()` (same
  rationale that's already documented in `settings_ui.py`'s tool
  enumerator: public `list_tools()` filters disabled tools but
  operators may still want to author gating rules for them).

Code-reviewer findings:
- ApprovalQueue TOCTOU: two concurrent `on_call_tool` coroutines with
  identical (tool, args_hash) could both miss `find()` and create
  duplicate pending entries; approving one would leave the other
  waiter blocked. Introduce `find_or_create(...)` serialised behind
  an `anyio.Lock`; middleware now uses it.
- ApprovalQueue had no pending-entries cap → memory exhaustion under
  an LLM retry-loop with mutated args. Add `PENDING_CAP = 1000` with
  FIFO eviction of oldest entries when the cap is hit.

Silent-failure-hunter findings:
- handlers.py: `get_tool_schema` and `get_value_source` now
  `logger.exception` before returning 500/502 so FastMCP version
  bumps or HA outages leave a traceable signal instead of opaque
  client errors.
- value_sources fetchers `logger.warning` on unexpected HA response
  shapes (would otherwise silently return empty dropdowns).
- value_sources cache no longer stores empty results — a transient HA
  glitch returning [] would otherwise pin the dropdown blank for 30s.

PR-test-analyzer findings (the critical one):
- test_persistence.py's `test_save_and_roundtrip` passed `Policy(enabled=True, ...)`
  for a field that no longer exists; `extra="ignore"` silently dropped it
  so the test was a no-op assertion. Replace with real round-tripped
  fields (wait_seconds / approval_ttl_minutes / remember_minutes) and
  add an explicit `test_load_drops_unknown_fields` exercising the
  extra="ignore" back-compat contract with a JSON file carrying
  `default_action` + `enabled`.
- Add wildcard scalar/None tests (`args.x.*` against scalar yields
  nothing; doesn't crash).
- Add ApprovalQueue tests: concurrent `find_or_create` shares one
  pending entry; `create` evicts oldest at PENDING_CAP.
- Add handler tests: sidecar value-source returns 503, tool-schema 500
  on `_list_tools` exception, value-source cache key separates per
  params, `_extract_arg_paths` skips malformed property entries.

Comment-analyzer findings:
- Grammar fix in handlers.py `_is_write_or_destructive` docstring.
- model.py docstring "older version of this PR" → "older builds".
- Strip the `(#966)` / `(issue #966)` parentheticals from module
  docstrings, settings_ui CSS/HTML/comments — git blame and the
  commit message carry the link.

* fix(policy): UI surface fetch failures + middleware reissues swept pending (#966)

- Middleware: after _wait_for_decision returns without a verdict, check
  whether the pending entry was swept (TTL elapsed during the wait). If
  so, create a fresh entry before raising USER_APPROVAL_REQUIRED so the
  LLM isn't told to re-call against a dead token.

- UI: policyLoadConfig now surfaces fetch failures in a visible error
  banner instead of silently rendering blank — picks up the
  policy_file_corrupt:true repair hint from the server's 500 response.

- UI: loadValueChoices records the failure (lastValueSourceError) so
  renderHint can show it under the value row. The dropdown still
  downgrades to free-text, but the user can now tell a transient HA
  outage from "no value source registered for this path".

- UI: renderValueControl uses an autoincrement seq so rapid path/op
  edits don't let an earlier slow fetch's DOM mutation land after a
  newer one's (similar to the autoSave pattern).

* fix(policy): logger.info on silent decide-False; debug log on gt/lt type-mismatch; strengthen event-wake test (#966)

- ApprovalQueue.approve/deny: emit logger.info when the call returns
  False (unknown token or already decided) — was silent. Helps debug
  the case where the middleware's consume_and_maybe_remember races
  with an out-of-band decide.

- Evaluator gt/lt TypeError fallback now logs at debug so a user
  whose 'battery_level < 20' rule never fires can see that the arg
  came in as a string and tighten the rule.

- test_event_wakes_waiter now measures elapsed wait time and asserts
  < 200ms, ruling out a hidden poll-loop impl that would still pass
  the previous decision-only check.

* style: ruff format evaluator.py for CI's 0.15.13 (#966)

Local ruff 0.15.7 didn't wrap the multi-arg logger.debug call;
CI's ruff 0.15.13 does. Upgrading local toolchain to match.

* feat(policy): clear remember-cache on save, clearer disabled-state UX, mirror master toggle (#966)

Three things:

1. Remember-cache invalidation on policy save (B2).
   ApprovalQueue.clear_remember_cache() drops every remembered
   approval; put_config calls it after a successful save. Without
   this, tightening a rule was silently bypassed by any in-flight
   remembered approvals until their window expired.

2. Better 503 / "unavailable" messaging (B8 + the broader issue).
   The stub handler's 503 message used to read "Live approvals
   unavailable in this mode (sidecar)" even when the real cause was
   the feature being turned off in addon config — the user had no
   way to tell from the UI. Updated to call out all three causes
   (feature off, sidecar, ImportError) and point at the addon log.
   The pending-list JS now checks policyState.enabled first and
   shows "Tool Security Policies is turned off" when that's the
   actual reason, falling back to the server's 503 message
   otherwise.

3. Mirror the master toggle onto the Tool Security Policies tab.
   Was only exposed in Server Settings before — users on the
   Policies tab had to navigate away to find the on/off switch.
   New checkbox at the top of the tab posts to the same
   /api/settings/features endpoint, so the two surfaces are live
   mirrors of the same addon-config flag.

* test(policy): fix JS-harness drift guard + lock policy-tab behaviour (#966)

The merged-in JSDOM behaviour test (#1425) failed collection because
its hardcoded _TOP_LEVEL_ELEMENT_IDS list didn't yet know about the
policy-tab handlers this PR adds (policy-master-toggle, policy-save-global-btn).
Add them, plus matching DOM stubs in _build_min_dom so the init pass
doesn't throw on the addEventListener calls.

While the file is open, add three behavioural tests that pin the new
condition-builder UX wiring:

- Master toggle change POSTs to /api/settings/features with the
  enable_tool_security_policies flag (so the on-tab toggle stays a
  true mirror of the Server-Settings checkbox).
- /api/policy/pending 503 renders "Tool Security Policies is turned
  off" when the addon flag is off (avoids the old misleading
  "sidecar / unavailable" copy).
- /api/policy/pending 503 propagates the server's addon-log message
  verbatim when the flag IS on but the queue is unreachable (so users
  know where to look for ImportError details).

The parse-coverage path catches syntax breaks already; these tests
catch behavioural regressions on top of it.

* refactor(policy): address 2nd-round review findings (#966)

Verified all 23 findings from the 2nd pr-review-toolkit pass against
the code; fixed 22 (skipping #13 — the JSDOM seq-cancel race test is
high-effort to author reliably and the production guard is small
enough that bench-level review catches regressions).

## Correctness / silent-failure

- ApprovalQueue PENDING_CAP eviction now sorts by
  `(decision == "pending", created_at)` so resolved entries evict
  first. When a still-pending entry MUST be evicted (cap full, no
  resolved to drop), `.set()` its event so any waiter in
  `_wait_for_decision` wakes immediately instead of blocking the
  full wait_seconds against a row that no longer exists.
- Middleware: log INFO with old + new token on the
  reissue-after-sweep branch so operators can correlate
  "approval row keeps reappearing" with the actual cause.
- Middleware: scope `clear_remember_cache` to "rules actually
  changed" — editing only wait_seconds / approval_ttl_minutes no
  longer blows away in-flight remembered approvals.
- Policy: model_validator requires `wait_seconds < approval_ttl_minutes * 60`
  so the middleware can't repeatedly issue fresh pending entries
  because the wait outlasted the TTL.
- value_sources: cache key uses `urllib.parse.urlencode` so a future
  param value containing `=`/`&` can't collide with another key.
- ApprovalQueue: `approve`/`deny` on unknown token now logs WARNING
  (security-gating endpoint, suggests UI bug or token probing).
  Already-decided stays INFO (legitimate race).

## UI

- settings_ui.policyState gains an `enabledKnown` tri-state bit so
  downstream branches (`policyLoadPending`'s "feature off" copy,
  master-toggle revert) don't false-confidently route to the
  "disabled" message when the features fetch actually failed.
- Master-toggle change handler reverts the checkbox on save failure
  AND syncs from `policyState.enabled` after a successful load —
  no more "UI says on, server says off" drift.
- `policyLoadConfig` appends "(response body unparseable)" when the
  500 body isn't JSON (e.g. HTML error page from a misrouted sidecar),
  so the operator sees more than just "HTTP 500".
- `policyLoadPending` surfaces fetch errors inline ("Lost contact
  with server, retrying") instead of silently freezing the list.
- `fetchToolSchema` records `lastValueSourceError` on failure so the
  hint banner explains why the value dropdown silently downgraded
  to free text.

## Tests

- test_handlers: `test_put_config_clears_remember_cache_when_rules_change`
  + `test_put_config_preserves_remember_cache_when_only_timing_changes`
  lock in the scoped invalidation.
- test_approval_queue: `test_create_evicts_resolved_entries_before_pending`,
  `test_evicting_pending_wakes_its_waiter`,
  `test_create_after_sweep_still_evicts_when_pending_fills_cap`
  cover the new eviction rules. The strengthened
  `test_find_or_create_lock_blocks_concurrent_create_under_real_race`
  inserts a yield point inside the lock body so the lock actually
  matters to the assertion (the previous test would pass even
  without the lock under anyio's cooperative scheduler).
- test_middleware: `test_swept_pending_during_wait_is_reissued_with_fresh_token`
  exercises the previously-untested reissue branch.
- test_schema_handlers: `test_value_source_empty_result_not_cached`
  proves the empty-result no-cache guard actually triggers a refetch.
- test_settings_ui_js_behavior: master-toggle test now JSON-parses
  the POST body and structurally asserts
  `flags.enable_tool_security_policies is True` instead of loose
  substring matching.

## Comments

- approval_queue.py PENDING_CAP docstring now matches implementation
  (was promising "resolved first" before the implementation actually
  did it).
- evaluator.py: gt-branch comment example uses ">" not "<".
- handlers.py: dropped cross-reference to settings_ui that would rot.
- middleware.py: trimmed "fast on warm disk" speculation.
- value_sources.py: trimmed "(WebSocket reconnect, auth lapse)"
  speculation in the empty-cache comment.
- settings_ui.py: four comments still saying "predicates" updated
  to "conditions" to match user-facing terminology.

* fix(ui): blank value on eq/in/etc coerces to op=exists (#966)

User expects 'leave value blank to gate on the argument's mere
presence regardless of value' to work across the equality-ish ops,
not just op=exists. Earlier I had the form reject blank value for
anything other than exists with a 'value is required' error.

Now: for eq / neq / in / not_in / contains / exists, leaving the
value blank silently coerces the predicate to op=exists on save.
The condition row then reads as 'args.* exists' which is the right
description of what's stored.

Ops that genuinely need a value (regex / gt / lt) still raise
'value is required for op=...'. The hint text under each op
updated to call out 'Leave blank to gate on any value' where it
applies.

---------

Co-authored-by: kingpanther13 <kingpanther13@users.noreply.github.com>
doonga pushed a commit to greyrock-labs/home-ops that referenced this pull request May 28, 2026
….0 ) (#141)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/homeassistant-ai/ha-mcp](https://github.com/homeassistant-ai/ha-mcp) | minor | `7.5.0` → `7.6.0` |

---

### Release Notes

<details>
<summary>homeassistant-ai/ha-mcp (ghcr.io/homeassistant-ai/ha-mcp)</summary>

### [`v7.6.0`](https://github.com/homeassistant-ai/ha-mcp/blob/HEAD/CHANGELOG.md#v760-2026-05-27)

[Compare Source](homeassistant-ai/ha-mcp@v7.5.0...v7.6.0)

##### Added

- Make HTTP bind host configurable via MCP\_HOST (closes [#&#8203;1434](homeassistant-ai/ha-mcp#1434))
  ([#&#8203;1436](homeassistant-ai/ha-mcp#1436))
- Tool Security Policies — per-tool approval gating ([#&#8203;966](homeassistant-ai/ha-mcp#966))
  ([#&#8203;1421](homeassistant-ai/ha-mcp#1421))
- Rename ha\_delete\_helpers\_integrations → ha\_remove\_helpers\_integrations + raise on missing target
  ([#&#8203;1424](homeassistant-ai/ha-mcp#1424))
- Auto-backup edited entities before write/destructive tool calls (closes [#&#8203;1288](homeassistant-ai/ha-mcp#1288))
  ([#&#8203;1403](homeassistant-ai/ha-mcp#1403))
- Persistent settings UI for stdio mode
  ([#&#8203;1381](homeassistant-ai/ha-mcp#1381))
- Add fields= projection to ha\_search\_entities, ha\_get\_overview, ha\_get\_state, ha\_get\_history, ha\_config\_list\_areas, ha\_list\_services ([#&#8203;1199](homeassistant-ai/ha-mcp#1199))
  ([#&#8203;1225](homeassistant-ai/ha-mcp#1225))
- Route entity-registration wait through WS events (closes [#&#8203;1152](homeassistant-ai/ha-mcp#1152))
  ([#&#8203;1382](homeassistant-ai/ha-mcp#1382))
- Add config subentry support
  ([#&#8203;1393](homeassistant-ai/ha-mcp#1393))
- Add Assist pipeline management tool
  ([#&#8203;1392](homeassistant-ai/ha-mcp#1392))
- Add knx to ha\_config\_set\_yaml allowlist
  ([#&#8203;1374](homeassistant-ai/ha-mcp#1374))
- Extend automation\_id parity to set/remove automation responses
  ([#&#8203;1343](homeassistant-ai/ha-mcp#1343))
- **haos-e2e**: Add parallel inaddon test tier (ha-mcp runs inside HAOS addon)
  ([#&#8203;1361](homeassistant-ai/ha-mcp#1361))
- Expose integration diagnostics via ha\_get\_integration and ha\_get\_system\_health (closes [#&#8203;1148](homeassistant-ai/ha-mcp#1148))
  ([#&#8203;1328](homeassistant-ai/ha-mcp#1328))
- Return canonical script\_id from ha\_config\_get\_script ([#&#8203;1334](homeassistant-ai/ha-mcp#1334))
  ([#&#8203;1352](homeassistant-ai/ha-mcp#1352))
- Add automation\_id parity key to ha\_config\_get\_automation
  ([#&#8203;1329](homeassistant-ai/ha-mcp#1329))
- Reject empty/whitespace identifiers on registry-metadata writes (closes [#&#8203;1294](homeassistant-ai/ha-mcp#1294))
  ([#&#8203;1312](homeassistant-ai/ha-mcp#1312))
- Add HA brand assets for custom integration
  ([#&#8203;1317](homeassistant-ai/ha-mcp#1317))
- Unify ha\_config\_set\_helper response shape (closes [#&#8203;1293](homeassistant-ai/ha-mcp#1293))
  ([#&#8203;1303](homeassistant-ai/ha-mcp#1303))
- Mirror create-side validation guards onto update path (closes [#&#8203;1292](homeassistant-ai/ha-mcp#1292))
  ([#&#8203;1304](homeassistant-ai/ha-mcp#1304))
- Add array\_patch mode to ha\_manage\_addon for atomic GET-modify-POST
  ([#&#8203;1063](homeassistant-ai/ha-mcp#1063))

##### Changed

- **agents**: Drop ha\_backup\_create + ha\_backup\_restore from accepted exceptions
  ([#&#8203;1445](homeassistant-ai/ha-mcp#1445))
- Update contributors list \[contributors-updated]
  ([`c7665a6`](homeassistant-ai/ha-mcp@c7665a6))
- **overview**: Enumerate dismissed\_repair\_count in fields= description + static drift test
  ([#&#8203;1411](homeassistant-ai/ha-mcp#1411))
- Credit [@&#8203;tomwilkie](https://github.com/tomwilkie) and six other contributors in README
  ([#&#8203;1400](homeassistant-ai/ha-mcp#1400))
- **[#&#8203;1157](homeassistant-ai/ha-mcp#1157: Bump skills-vendor + auto-update via Renovate + native for: field + scrub eval\_template anti-patterns
  ([#&#8203;1383](homeassistant-ai/ha-mcp#1383))
- Extend Boy Scout weasel-phrase list with common variants; clarify semantic match
  ([#&#8203;1373](homeassistant-ai/ha-mcp#1373))
- Merge Boy Scout Rule + Handling Discovered Improvements; tighten deferral gate
  ([#&#8203;1359](homeassistant-ai/ha-mcp#1359))
- Categorize Issue Labels table and document 6 reverse-drift labels
  ([#&#8203;1335](homeassistant-ai/ha-mcp#1335))
- Strip stale L-refs from test\_identifier\_validation\_family docstrings
  ([#&#8203;1324](homeassistant-ai/ha-mcp#1324))
- Align label refs with live label set and fix triaged-removal trigger
  ([#&#8203;1316](homeassistant-ai/ha-mcp#1316))
- Surface tool-discovery / categorized search
  ([#&#8203;1123](homeassistant-ai/ha-mcp#1123))
- Fix two stale ha\_get\_skill\_guide references missed in [#&#8203;1289](homeassistant-ai/ha-mcp#1289)
  ([#&#8203;1305](homeassistant-ai/ha-mcp#1305))
- Clarify setup wizard placeholders need braces removed ([#&#8203;1284](homeassistant-ai/ha-mcp#1284))
  ([#&#8203;1286](homeassistant-ai/ha-mcp#1286))

##### Fixed

- Remove counter from ha\_reload\_core targets ([#&#8203;1453](homeassistant-ai/ha-mcp#1453))
  ([#&#8203;1456](homeassistant-ai/ha-mcp#1456))
- **backup**: Post-timeout match correctness + state-gate (closes [#&#8203;1433](homeassistant-ai/ha-mcp#1433))
  ([#&#8203;1435](homeassistant-ai/ha-mcp#1435))
- Sync addon settings UI with Supervisor options end-to-end
  ([#&#8203;1420](homeassistant-ai/ha-mcp#1420))
- **calendar**: Switch ha\_config\_remove\_calendar\_event to WebSocket (closes [#&#8203;1413](homeassistant-ai/ha-mcp#1413), [#&#8203;1416](homeassistant-ai/ha-mcp#1416))
  ([#&#8203;1418](homeassistant-ai/ha-mcp#1418))
- Error-shape consistency for non-entity not-found (closes [#&#8203;1297](homeassistant-ai/ha-mcp#1297))
  ([#&#8203;1397](homeassistant-ai/ha-mcp#1397))
- Guard against silent automation overwrite on id mismatch ([#&#8203;1404](homeassistant-ai/ha-mcp#1404))
  ([#&#8203;1405](homeassistant-ai/ha-mcp#1405))
- Cache YAML instance to prevent CPU spikes in bulk edits ([#&#8203;1370](homeassistant-ai/ha-mcp#1370))
  ([#&#8203;1371](homeassistant-ai/ha-mcp#1371))
- **client**: Route get\_error\_log via hassio proxy on external-HAOS clients
  ([#&#8203;1360](homeassistant-ai/ha-mcp#1360))
- Classify dashboard 404s ("unknown config specified") as RESOURCE\_NOT\_FOUND
  ([#&#8203;1345](homeassistant-ai/ha-mcp#1345))
- Detect HA addon installs as http transport, not stdio ([#&#8203;1322](homeassistant-ai/ha-mcp#1322))
  ([#&#8203;1327](homeassistant-ai/ha-mcp#1327))
- Actionable 403 suggestion when addon has unmapped container ports ([#&#8203;1319](homeassistant-ai/ha-mcp#1319))
  ([#&#8203;1325](homeassistant-ai/ha-mcp#1325))
- Filter dismissed repairs in overview and system\_health ([#&#8203;1307](homeassistant-ai/ha-mcp#1307))
  ([#&#8203;1309](homeassistant-ai/ha-mcp#1309))
- Exit on HA container death + daily reset before CI check
  ([#&#8203;1295](homeassistant-ai/ha-mcp#1295))
- Align ha\_config\_set\_dashboard with sibling re-fetch-after-save pattern ([#&#8203;1291](homeassistant-ai/ha-mcp#1291))
  ([#&#8203;1301](homeassistant-ai/ha-mcp#1301))
- Allow str.replace in python\_transform; hint at search mode on IndexError
  ([#&#8203;1287](homeassistant-ai/ha-mcp#1287))
- **array\_patch**: Tighten validation and surface silent failures
  ([#&#8203;1285](homeassistant-ai/ha-mcp#1285))
- HA Core proxy fallback for ha\_get\_logs(source=system\_service) on non-addon installs
  ([#&#8203;1283](homeassistant-ai/ha-mcp#1283))

##### Performance Improvements

- Tighten \_poll\_for\_automation\_entity first-poll cadence
  ([#&#8203;1384](homeassistant-ai/ha-mcp#1384))
- Parallelize ha\_get\_system\_health optional sections via asyncio.gather
  ([#&#8203;1336](homeassistant-ai/ha-mcp#1336))

##### Refactoring

- **service**: Compact ha\_call\_service result default ([#&#8203;1446](homeassistant-ai/ha-mcp#1446))
  ([#&#8203;1447](homeassistant-ai/ha-mcp#1447))
- Rename ha\_update\_device → ha\_set\_device
  ([#&#8203;1444](homeassistant-ai/ha-mcp#1444))
- Remove duplicate flat area/floor list tools (consolidation followup to [#&#8203;1016](homeassistant-ai/ha-mcp#1016))
  ([#&#8203;1429](homeassistant-ai/ha-mcp#1429))
- **complexity**: Migrate tools\_utility.py to class-based pattern
  ([#&#8203;1423](homeassistant-ai/ha-mcp#1423))
- **complexity**: Reduce C901 violations in tools/ — batch 4
  ([#&#8203;1408](homeassistant-ai/ha-mcp#1408))
- Route \_poll\_for\_automation\_entity through WS event waiter (closes [#&#8203;1395](homeassistant-ai/ha-mcp#1395))
  ([#&#8203;1406](homeassistant-ai/ha-mcp#1406))
- **yaml**: Use threading.local subclass for cached instance
  ([#&#8203;1396](homeassistant-ai/ha-mcp#1396))
- Align dashboards 404 shape with sibling config tools
  ([#&#8203;1386](homeassistant-ai/ha-mcp#1386))
- Complete singular warning → warnings list migration repo-wide (closes [#&#8203;1332](homeassistant-ai/ha-mcp#1332))
  ([#&#8203;1341](homeassistant-ai/ha-mcp#1341))
- Complete warnings-list migration for lifecycle-write tools
  ([#&#8203;1340](homeassistant-ai/ha-mcp#1340))
- Drop redundant identifier echo key from ha\_config\_get\_automation
  ([#&#8203;1354](homeassistant-ai/ha-mcp#1354))
- Drop logger.error in config-tool except blocks ([#&#8203;1302](homeassistant-ai/ha-mcp#1302))
  ([#&#8203;1353](homeassistant-ai/ha-mcp#1353))
- Extend validate\_identifier\_not\_empty to automations/scripts/dashboards CRUD (closes [#&#8203;1313](homeassistant-ai/ha-mcp#1313))
  ([#&#8203;1321](homeassistant-ai/ha-mcp#1321))
- Migrate tools\_config\_scenes inline empty-id guards to shared helper
  ([#&#8203;1320](homeassistant-ai/ha-mcp#1320))
- Remove ha\_get\_helper\_schema (closes [#&#8203;1186](homeassistant-ai/ha-mcp#1186))
  ([#&#8203;1315](homeassistant-ai/ha-mcp#1315))
- Consolidate skill tools; fix stable submodule packaging
  ([#&#8203;1289](homeassistant-ai/ha-mcp#1289))
- Align tools\_config\_automations.py error-handling with sibling pattern ([#&#8203;1290](homeassistant-ai/ha-mcp#1290))
  ([#&#8203;1298](homeassistant-ai/ha-mcp#1298))

***

<details>
<summary>Internal Changes</summary>

##### Fixed

- **ci**: Install libguestfs in HAOS publish workflow
  ([#&#8203;1358](homeassistant-ai/ha-mcp#1358))

##### Build System

- **deps**: Bump esbuild from 0.24.2 to 0.25.0 in /tests/js
  ([#&#8203;1427](homeassistant-ai/ha-mcp#1427))
- **deps**: Bump devalue from 5.6.4 to 5.8.1 in /site
  ([#&#8203;1282](homeassistant-ai/ha-mcp#1282))
- **deps**: Bump astro from 6.1.6 to 6.1.10 in /site
  ([#&#8203;1274](homeassistant-ai/ha-mcp#1274))

##### Chores

- **addon**: Publish dev addon version 7.5.0.dev360 \[skip ci]
  ([`ad7aed1`](homeassistant-ai/ha-mcp@ad7aed1))
- Sync tool docs after merge \[skip ci]
  ([`9c4984f`](homeassistant-ai/ha-mcp@9c4984f))
- **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.4
  ([#&#8203;1450](homeassistant-ai/ha-mcp#1450))
- **addon**: Publish dev addon version 7.5.0.dev359 \[skip ci]
  ([`b82d4ee`](homeassistant-ai/ha-mcp@b82d4ee))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.16
  ([#&#8203;1449](homeassistant-ai/ha-mcp#1449))
- **addon**: Publish dev addon version 7.5.0.dev358 \[skip ci]
  ([`53fba6d`](homeassistant-ai/ha-mcp@53fba6d))
- Sync tool docs after merge \[skip ci]
  ([`dc7750d`](homeassistant-ai/ha-mcp@dc7750d))
- **addon**: Publish dev addon version 7.5.0.dev357 \[skip ci]
  ([`fd150c9`](homeassistant-ai/ha-mcp@fd150c9))
- **addon**: Publish dev addon version 7.5.0.dev356 \[skip ci]
  ([`5fa1463`](homeassistant-ai/ha-mcp@5fa1463))
- **addon**: Publish dev addon version 7.5.0.dev355 \[skip ci]
  ([`174ac5d`](homeassistant-ai/ha-mcp@174ac5d))
- **addon**: Publish dev addon version 7.5.0.dev354 \[skip ci]
  ([`4f93989`](homeassistant-ai/ha-mcp@4f93989))
- **addon**: Publish dev addon version 7.5.0.dev353 \[skip ci]
  ([`53f282f`](homeassistant-ai/ha-mcp@53f282f))
- **addon**: Publish dev addon version 7.5.0.dev352 \[skip ci]
  ([`4911d46`](homeassistant-ai/ha-mcp@4911d46))
- Sync tool docs after merge \[skip ci]
  ([`8a79837`](homeassistant-ai/ha-mcp@8a79837))
- **addon**: Publish dev addon version 7.5.0.dev351 \[skip ci]
  ([`13afa9d`](homeassistant-ai/ha-mcp@13afa9d))
- **addon**: Publish dev addon version 7.5.0.dev350 \[skip ci]
  ([`e93d680`](homeassistant-ai/ha-mcp@e93d680))
- **addon**: Publish dev addon version 7.5.0.dev349 \[skip ci]
  ([`1631ad1`](homeassistant-ai/ha-mcp@1631ad1))
- **addon**: Publish dev addon version 7.5.0.dev348 \[skip ci]
  ([`18a8aef`](homeassistant-ai/ha-mcp@18a8aef))
- Sync tool docs after merge \[skip ci]
  ([`9e0493d`](homeassistant-ai/ha-mcp@9e0493d))
- **addon**: Publish dev addon version 7.5.0.dev347 \[skip ci]
  ([`e2067e4`](homeassistant-ai/ha-mcp@e2067e4))
- Sync tool docs after merge \[skip ci]
  ([`7bdb3d4`](homeassistant-ai/ha-mcp@7bdb3d4))
- **addon**: Publish dev addon version 7.5.0.dev346 \[skip ci]
  ([`c2d4dd7`](homeassistant-ai/ha-mcp@c2d4dd7))
- Sync tool docs after merge \[skip ci]
  ([`393b354`](homeassistant-ai/ha-mcp@393b354))
- **addon**: Publish dev addon version 7.5.0.dev345 \[skip ci]
  ([`42ede8b`](homeassistant-ai/ha-mcp@42ede8b))
- **addon**: Publish dev addon version 7.5.0.dev344 \[skip ci]
  ([`e6cc7a1`](homeassistant-ai/ha-mcp@e6cc7a1))
- Sync tool docs after merge \[skip ci]
  ([`fb35f30`](homeassistant-ai/ha-mcp@fb35f30))
- **addon**: Publish dev addon version 7.5.0.dev343 \[skip ci]
  ([`401b7b4`](homeassistant-ai/ha-mcp@401b7b4))
- **addon**: Publish dev addon version 7.5.0.dev342 \[skip ci]
  ([`0679371`](homeassistant-ai/ha-mcp@0679371))
- Sync tool docs after merge \[skip ci]
  ([`f6796ec`](homeassistant-ai/ha-mcp@f6796ec))
- **addon**: Publish dev addon version 7.5.0.dev341 \[skip ci]
  ([`3654478`](homeassistant-ai/ha-mcp@3654478))
- **addon**: Publish dev addon version 7.5.0.dev340 \[skip ci]
  ([`64f00b6`](homeassistant-ai/ha-mcp@64f00b6))
- **addon**: Publish dev addon version 7.5.0.dev339 \[skip ci]
  ([`d6e8873`](homeassistant-ai/ha-mcp@d6e8873))
- Sync tool docs after merge \[skip ci]
  ([`7525e93`](homeassistant-ai/ha-mcp@7525e93))
- **addon**: Publish dev addon version 7.5.0.dev338 \[skip ci]
  ([`288ca4a`](homeassistant-ai/ha-mcp@288ca4a))
- **addon**: Publish dev addon version 7.5.0.dev337 \[skip ci]
  ([`f539ae5`](homeassistant-ai/ha-mcp@f539ae5))
- **addon**: Publish dev addon version 7.5.0.dev336 \[skip ci]
  ([`5568a86`](homeassistant-ai/ha-mcp@5568a86))
- **addon**: Publish dev addon version 7.5.0.dev335 \[skip ci]
  ([`e069405`](homeassistant-ai/ha-mcp@e069405))
- **addon**: Publish dev addon version 7.5.0.dev334 \[skip ci]
  ([`f7be6ea`](homeassistant-ai/ha-mcp@f7be6ea))
- **addon**: Publish dev addon version 7.5.0.dev333 \[skip ci]
  ([`cb480ea`](homeassistant-ai/ha-mcp@cb480ea))
- Sync tool docs after merge \[skip ci]
  ([`9a5bc3c`](homeassistant-ai/ha-mcp@9a5bc3c))
- **addon**: Publish dev addon version 7.5.0.dev332 \[skip ci]
  ([`e0e59ee`](homeassistant-ai/ha-mcp@e0e59ee))
- Sync tool docs after merge \[skip ci]
  ([`499ebf0`](homeassistant-ai/ha-mcp@499ebf0))
- **addon**: Publish dev addon version 7.5.0.dev331 \[skip ci]
  ([`3e0ce92`](homeassistant-ai/ha-mcp@3e0ce92))
- **addon**: Publish dev addon version 7.5.0.dev330 \[skip ci]
  ([`e48d056`](homeassistant-ai/ha-mcp@e48d056))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.15
  ([#&#8203;1376](homeassistant-ai/ha-mcp#1376))
- **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.3
  ([#&#8203;1377](homeassistant-ai/ha-mcp#1377))
- **addon**: Publish dev addon version 7.5.0.dev329 \[skip ci]
  ([`c523c50`](homeassistant-ai/ha-mcp@c523c50))
- Sync tool docs after merge \[skip ci]
  ([`5b7a8aa`](homeassistant-ai/ha-mcp@5b7a8aa))
- **addon**: Publish dev addon version 7.5.0.dev328 \[skip ci]
  ([`6c42fba`](homeassistant-ai/ha-mcp@6c42fba))
- **addon**: Publish dev addon version 7.5.0.dev327 \[skip ci]
  ([`aecd025`](homeassistant-ai/ha-mcp@aecd025))
- **addon**: Publish dev addon version 7.5.0.dev326 \[skip ci]
  ([`399c17c`](homeassistant-ai/ha-mcp@399c17c))
- **addon**: Publish dev addon version 7.5.0.dev325 \[skip ci]
  ([`b580b45`](homeassistant-ai/ha-mcp@b580b45))
- Sync tool docs after merge \[skip ci]
  ([`8567c3a`](homeassistant-ai/ha-mcp@8567c3a))
- **addon**: Publish dev addon version 7.5.0.dev324 \[skip ci]
  ([`a65579d`](homeassistant-ai/ha-mcp@a65579d))
- **addon**: Publish dev addon version 7.5.0.dev323 \[skip ci]
  ([`c7667ba`](homeassistant-ai/ha-mcp@c7667ba))
- **addon**: Publish dev addon version 7.5.0.dev322 \[skip ci]
  ([`44d15a8`](homeassistant-ai/ha-mcp@44d15a8))
- **addon**: Publish dev addon version 7.5.0.dev321 \[skip ci]
  ([`8739f6c`](homeassistant-ai/ha-mcp@8739f6c))
- **addon**: Publish dev addon version 7.5.0.dev320 \[skip ci]
  ([`02b6e47`](homeassistant-ai/ha-mcp@02b6e47))
- Sync tool docs after merge \[skip ci]
  ([`ab68c9a`](homeassistant-ai/ha-mcp@ab68c9a))
- **addon**: Publish dev addon version 7.5.0.dev319 \[skip ci]
  ([`4472904`](homeassistant-ai/ha-mcp@4472904))
- **addon**: Publish dev addon version 7.5.0.dev318 \[skip ci]
  ([`e030dbc`](homeassistant-ai/ha-mcp@e030dbc))
- **addon**: Publish dev addon version 7.5.0.dev317 \[skip ci]
  ([`d87855c`](homeassistant-ai/ha-mcp@d87855c))
- Sync tool docs after merge \[skip ci]
  ([`a72a4e8`](homeassistant-ai/ha-mcp@a72a4e8))
- **addon**: Publish dev addon version 7.5.0.dev316 \[skip ci]
  ([`bd9397f`](homeassistant-ai/ha-mcp@bd9397f))
- **addon**: Publish dev addon version 7.5.0.dev315 \[skip ci]
  ([`264bfc2`](homeassistant-ai/ha-mcp@264bfc2))
- **addon**: Publish dev addon version 7.5.0.dev314 \[skip ci]
  ([`df62881`](homeassistant-ai/ha-mcp@df62881))
- **addon**: Publish dev addon version 7.5.0.dev313 \[skip ci]
  ([`f6c47ca`](homeassistant-ai/ha-mcp@f6c47ca))
- **addon**: Publish dev addon version 7.5.0.dev312 \[skip ci]
  ([`2bb7a74`](homeassistant-ai/ha-mcp@2bb7a74))
- Sync tool docs after merge \[skip ci]
  ([`137e279`](homeassistant-ai/ha-mcp@137e279))
- **addon**: Publish dev addon version 7.5.0.dev311 \[skip ci]
  ([`28324ea`](homeassistant-ai/ha-mcp@28324ea))
- Sync tool docs after merge \[skip ci]
  ([`9a753d4`](homeassistant-ai/ha-mcp@9a753d4))
- **addon**: Publish dev addon version 7.5.0.dev310 \[skip ci]
  ([`f893b2e`](homeassistant-ai/ha-mcp@f893b2e))
- **addon**: Publish dev addon version 7.5.0.dev309 \[skip ci]
  ([`8cbdb7b`](homeassistant-ai/ha-mcp@8cbdb7b))
- **addon**: Publish dev addon version 7.5.0.dev308 \[skip ci]
  ([`2d18016`](homeassistant-ai/ha-mcp@2d18016))
- **addon**: Publish dev addon version 7.5.0.dev307 \[skip ci]
  ([`3fc3b28`](homeassistant-ai/ha-mcp@3fc3b28))
- Sync tool docs after merge \[skip ci]
  ([`9e6cff8`](homeassistant-ai/ha-mcp@9e6cff8))
- **addon**: Publish dev addon version 7.5.0.dev306 \[skip ci]
  ([`8bdd0fc`](homeassistant-ai/ha-mcp@8bdd0fc))
- **addon**: Publish dev addon version 7.5.0.dev305 \[skip ci]
  ([`83535b9`](homeassistant-ai/ha-mcp@83535b9))
- **addon**: Publish dev addon version 7.5.0.dev304 \[skip ci]
  ([`1435b3a`](homeassistant-ai/ha-mcp@1435b3a))
- **addon**: Publish dev addon version 7.5.0.dev303 \[skip ci]
  ([`e2da659`](homeassistant-ai/ha-mcp@e2da659))
- Sync tool docs after merge \[skip ci]
  ([`23789fa`](homeassistant-ai/ha-mcp@23789fa))
- **addon**: Publish dev addon version 7.5.0.dev302 \[skip ci]
  ([`6c8e574`](homeassistant-ai/ha-mcp@6c8e574))
- Sync tool docs after merge \[skip ci]
  ([`d2329cb`](homeassistant-ai/ha-mcp@d2329cb))
- **addon**: Publish dev addon version 7.5.0.dev301 \[skip ci]
  ([`bb538f7`](homeassistant-ai/ha-mcp@bb538f7))
- Sync tool docs after merge \[skip ci]
  ([`f70f0e1`](homeassistant-ai/ha-mcp@f70f0e1))
- **addon**: Publish version 7.5.0 \[skip ci]
  ([`9c5eb37`](homeassistant-ai/ha-mcp@9c5eb37))

##### Continuous Integration

- **deps**: Bump actions/upload-artifact in the github-actions group
  ([#&#8203;1437](homeassistant-ai/ha-mcp#1437))
- Share qcow2 cache + GHCR fallback between HAOS lanes
  ([#&#8203;1407](homeassistant-ai/ha-mcp#1407))
- Add ruff format --check on changed Python files
  ([#&#8203;1387](homeassistant-ai/ha-mcp#1387))
- Exempt assigned issues from stale bot
  ([#&#8203;1368](homeassistant-ai/ha-mcp#1368))
- **deps**: Bump the github-actions group with 3 updates
  ([#&#8203;1362](homeassistant-ai/ha-mcp#1362))

##### Refactoring

- Consolidate lovelace/dashboards/list through shared helper
  ([#&#8203;1344](homeassistant-ai/ha-mcp#1344))

##### Testing

- **haos-e2e**: Bake + install webhook-proxy addon and exercise its runtime
  ([#&#8203;1443](homeassistant-ai/ha-mcp#1443))
- **config-subentry**: Mark forecast\_solar e2e as known flaky + relative-import sweep
  ([#&#8203;1430](homeassistant-ai/ha-mcp#1430))
- **haos-e2e**: Trim cache-save race, compress GHCR qcow2, eval boot snapshot
  ([#&#8203;1428](homeassistant-ai/ha-mcp#1428))
- JSDOM behaviour harness + auto-discovery parse coverage for every rendered <script>
  ([#&#8203;1425](homeassistant-ai/ha-mcp#1425))
- **hacs**: Retry TestMcpToolsInstallation on flake
  ([#&#8203;1426](homeassistant-ai/ha-mcp#1426))
- **e2e**: Drop redundant lifecycle roundtrips, keep only Matter Server ([#&#8203;1414](homeassistant-ai/ha-mcp#1414))
  ([#&#8203;1419](homeassistant-ai/ha-mcp#1419))
- **e2e**: Assert backend dispatch matches workflow env on every lane
  ([#&#8203;1409](homeassistant-ai/ha-mcp#1409))
- Escape ideographic space and format file ([#&#8203;1237](homeassistant-ai/ha-mcp#1237))
  ([#&#8203;1410](homeassistant-ai/ha-mcp#1410))
- **e2e**: Measure \_POLL\_CADENCE p50/p99 to validate or retune (closes [#&#8203;1389](homeassistant-ai/ha-mcp#1389))
  ([#&#8203;1398](homeassistant-ai/ha-mcp#1398))
- **e2e**: Wait for addon state=started in haos proxy header test
  ([#&#8203;1402](homeassistant-ai/ha-mcp#1402))
- Pin remaining \_classify\_by\_message branches
  ([#&#8203;1385](homeassistant-ai/ha-mcp#1385))
- **haos-e2e**: Slim addon set + real-addon ha\_manage\_addon coverage (closes [#&#8203;1350](homeassistant-ai/ha-mcp#1350))
  ([#&#8203;1379](homeassistant-ai/ha-mcp#1379))
- **haos-e2e**: Close out [#&#8203;1349](homeassistant-ai/ha-mcp#1349) — lifecycle, integrations, supervisor\_mock migration, no more skips
  ([#&#8203;1375](homeassistant-ai/ha-mcp#1375))
- **e2e**: Consolidate readiness gates onto /api/core/state (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1372](homeassistant-ai/ha-mcp#1372))
- **e2e**: Tighten 5 readiness-gate budgets with 2-63x headroom (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1369](homeassistant-ai/ha-mcp#1369))
- Scaffold HAOS E2E tier image-build pipeline (refs [#&#8203;1281](homeassistant-ai/ha-mcp#1281))
  ([#&#8203;1326](homeassistant-ai/ha-mcp#1326))
- **e2e**: Instrument HA\_MCP\_TOOLS\_WAIT readiness gate (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1346](homeassistant-ai/ha-mcp#1346))
- **e2e**: Centralize wait\_for\_entity\_registration helper (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1308](homeassistant-ai/ha-mcp#1308))
- **e2e**: Unify dict-error message extraction across e2e tests (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1311](homeassistant-ai/ha-mcp#1311))
- **e2e**: Surface readiness-gate elapsed times in CI logs (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1310](homeassistant-ai/ha-mcp#1310))

</details>

</details>

---

### Configuration

📅 **Schedule**: (in timezone America/New_York)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xOTUuNCIsInVwZGF0ZWRJblZlciI6IjQzLjE5NS40IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19-->

Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/141
kingpanther13 added a commit that referenced this pull request May 29, 2026
)

* feat(policy): scaffold policy package for per-tool approval (#966)

* feat(policy): add Predicate/Rule/Policy data models (#966)

* feat(addon): expose enable_per_tool_approval option (#966)

* feat(config): add enable_per_tool_approval setting (#966)

* feat(policy): atomic load/save for tool_policy.json (#966)

* feat(addon): wire enable_per_tool_approval through start.py + docs (#966)

* feat(policy): args-hash + remember-cache for approval queue (#966)

* feat(policy): predicate evaluator (eq/in/regex/exists/...) (#966)

* feat(policy): pending entries with TTL, decisions, and event signalling (#966)

* feat(policy): PolicyMiddleware happy-path branches (#966)

* test(policy): cover block/deny/timeout/recall/remember branches (#966)

* feat(policy): /api/policy/* Starlette handlers (#966)

* fix(policy): wrap ValidationError, scope contains op, regex doc, test bind (#966)

* feat(policy): Policies tab in web UI + sidecar route wiring (#966)

* feat(policy): register PolicyMiddleware on the FastMCP server (#966)

* fix(policy): return 400 on malformed approve/deny bodies (#966)

* feat(toolsearch): unpin yaml-edit and code-mode tools, gated by approval middleware (#966)

* chore: ruff format + lint cleanup for policy package (#966)

- ruff format reflow on PR-touched files (case statements split to two
  lines, function signatures, line continuations).
- UP042: Verdict now inherits from StrEnum instead of (str, Enum).
- E402: hoist `import anyio` to the top of test_approval_queue.py.
- I001: sort imports in test_evaluator.py and test_model.py.

No behavior change.

* fix(policy): mypy narrowing for evaluator comparisons (#966)

`Predicate.value` is `Any | None` and `extract_path` returns `Any`, so
`val == pv`, `val > pv`, etc. inherit `Any` and trip the project's
`warn_return_any` mypy setting on functions declared `-> bool`. Wrap
the comparison branches in `bool(...)` to make the narrowing explicit.

Also guard the `regex` branch with `isinstance(pv, str)` so `re.search`
receives a definite `str` instead of `Any | None`; a non-string regex
value now returns False instead of raising TypeError at evaluation
time, which is the only sensible behavior for a malformed pattern.

No change to any test's expected outcome.

* docs: credit @L1AD and PolicyLayer for #966 inspiration

* docs(addon): fix wrong YAML example in enable_per_tool_approval section (#966)

* fix(policy): CI green + critical bugs from reviewer cycle (#966)

- expires_in_seconds: use time-remaining not total TTL
- middleware: fail-closed on corrupt policy load (was crashing all gated calls)
- handlers: 500-with-corrupt-flag on get_config when policy invalid
- approval URL: wire secret_prefix via lazy getattr so HTTP-standalone
  emits a usable absolute path
- approve/deny: return bool, 409 on already-decided
- Predicate: field validators for op/value compatibility (Gemini)
- persistence: explicit UTF-8 encoding (Gemini)
- middleware: reuse tools/helpers safe_progress
- handlers comment: fix wrong "next call" claim
- test_middleware: unwrap ExceptionGroup for pytest.raises (CI fix)
- test_stdio_settings_sidecar: include new policy_* handler keys (CI fix)

* refactor(policy): drop default_action + tighten Rule.tool_name (#966)

- Policy schema simplified: no default_action field. System is always
  "allow unless a rule matches; rule = require approval". The previous
  default_action='require_approval' option was a bricked-config trap
  since rules can't grant allow-overrides.
- Rule.tool_name now rejects empty string; wildcard '*' documented in
  the docstring.
- Evaluator simplified to match.
- Tests updated/added.

* refactor(policy): encapsulate decision state + clean naming (#966)

- PendingApproval.decide() encapsulates the decision/event coupling;
  property guards read-only access to decision.
- __post_init__ validates expires_at > created_at.
- ApprovalQueue docstring spells out single-process scope and
  restart-loses-tokens semantics.
- Rename args_preview -> args throughout (it was always the full
  unmodified args; "preview" was misleading).
- Remove on_policy_change dead parameter from build_policy_handlers.

* fix(toolsearch): default-pinned tools should be user-unpinnable (#966)

- Computed pinned set now respects tool_config.json — explicit "enabled"
  state removes from defaults.
- Remove ha_restart / ha_reload_core from DEFAULT_PINNED_TOOLS (recovery
  actions, low frequency, low value in default LLM tool surface).
- Add ha_manage_backup to MANDATORY_TOOLS (operational essential).
- Server now declares _settings_secret_prefix on __init__ for pyright.

* refactor(policy): rename feature to "Tool Security Policies" (#966)

User-facing rename: addon config option, env var, Settings attribute,
UI tab label, addon DOCS sections, translations, server method.
Internal Python naming (policy/ package, tool_policy.json, /api/policy/*
routes, class names like PolicyMiddleware/ApprovalQueue) unchanged for
less churn.

* docs: small comment polish for policy review nits (#966)

* feat(ui): per-tool security-gated toggle in Tools tab (#966)

* test(policy): integration + timing-isolation coverage gaps (#966)

- test_server_policy_wiring: assert _apply_tool_security_policies attaches
  middleware + approval_queue when enabled, neither when disabled.
- test_settings_ui_handler_selection: parametrize the live-vs-stub branch
  in build_settings_handlers (sidecar / no server / no queue / live).
- test_middleware wait-loop timing: assert event-wake exit, not polling.
- test_middleware multi-rule precedence: first-match wins for remember_minutes.

* feat(ui): rewrite Tool Security Policies tab — per-tool cards + predicate editor (#966)

* feat(config): expose enable_tool_security_policies as a feature flag (#966)

Wires the new addon-config toggle into FEATURE_FLAG_FIELDS so it appears
in the Server Settings tab and rides PR #1420's _save_feature_flags +
_supervisor_merge_and_post_options + _schedule_supervisor_self_restart
flow when toggled in addon mode.

* fix(policy): address all verified review findings + CI failures (#966)

CI fixes:
- ruff: drop dead noqa: SLF001 suppression
- unit test: test_missing_path_never_matches_except_exists uses
  op-compatible values so Predicate field_validator doesn't reject

Review findings:
- FEATURE_META entry for enable_tool_security_policies (toggle now
  renders in Server Settings tab)
- Approval URL points to /settings?tab=tool-security-policies (was
  POST-only /api/policy/approve which 405'd on browser open)
- policyDecide surfaces network errors + 409 current_decision
- Policy gains version field for optimistic concurrency; PUT 409s
  on version mismatch; client surfaces 'reload before saving'
- _apply_tool_security_policies failure logs spell out security
  impact (TOOL SECURITY GATING IS NOT ACTIVE) and include
  data_dir/env-var context
- Validator rejects value on op='exists'; gt/lt TypeError degrades
  to False
- ToolVisibilityResult -> UserToolStateOverrides, fields are
  frozenset, disjointness asserted
- PendingApproval.event private; expose async wait()
- _SupervisorOptionsError gains transport()/validation() classmethods
  encoding kind->status_code pairing
- Wiring test binds queue identity; handler-selection covers all 3
  live routes
- Comment + doc polish (audit-trail claim, e2e docstring path,
  internal Task references)

* fix(policy): CI green + real e2e test for the approval flow (#966)

- ruff format: tests/src/unit/test_settings_ui.py
- test_save_and_roundtrip: account for save_policy version bump
- test_serialized_shape_is_stable: include 'version' in expected keys
- test_addon_save_returns_500_when_server_is_none: guard
  server._settings_secret_prefix assignment with None check
  (regression from #4's secret-prefix wiring)
- tests/src/e2e/policy/test_approval_flow.py: real e2e exercising
  block -> approve -> re-call cycle with strict args-binding rejection
  on mutated args. Skip-stub replaced with real test driving the live
  middleware via mcp_client + /api/policy/* HTTP.

* fix(ui): broken quote escaping in predicate-form placeholder breaks JS parse (#966)

The Python source `'placeholder=\\'\"lock\"...\\\\'>'` rendered as JS
`'placeholder='\"lock\"..'>'` — the single quote inside the HTML attribute
value closed the outer JS string literal, and subsequent tokens (\"lock\",
'or', '[', ...) broke parsing. With a syntax error in the inline <script>,
the browser stopped executing — Tools tab stuck on 'Loading...', tabs
unclickable.

Switched to a JS-safe double-quoted attribute with &quot; for the
embedded double quotes in the placeholder hint.

* fix(ui): gated toggle reads addon-config flag, not Policy.enabled (#966)

The per-tool 'security gated' toggle was grayed out even when the user
had enable_tool_security_policies turned ON in the addon config + the
Server Settings tab toggle, because the JS was reading Policy.enabled
(the file field) instead of the addon-config feature flag — which is
the single source of truth for whether the middleware is active.

loadPolicyState now reads enable_tool_security_policies from
/api/settings/features (same place renderFeatureFlags consumes from).

* fix(policy): Policy.extra=ignore so old persisted files load cleanly (#966)

Persisted tool_policy.json files from an earlier revision of this PR
carry default_action (since dropped) and rejected with ValidationError
on load — surfacing as 'Could not load policy: 500' when the user
clicked the per-tool gated toggle.

Predicate/Rule keep extra=forbid (typo catching at construction).

* fix(policy): drop Policy.enabled — addon-config flag is the sole switch (#966)

The middleware's server-side gate was checking `policy.enabled` (a
file field with no UI surface), so it returned ALLOW on every call
regardless of rules. The addon-config flag
(`enable_tool_security_policies`) was supposed to be the only switch
— and the middleware is only registered when that flag is true — so
the inner `policy.enabled` check was both redundant and broken.

Remove the field, remove both server-side checks (middleware +
evaluator), update tests, and refresh the JS comment that referred
to it.

* fix(policy): drop approve_url, instruct LLM to send user to settings page (#966)

The relative-path approve_url doesn't resolve cleanly through cloudflared
or other reverse-proxy deployment modes — the LLM can't safely hand it
to the user. The user already knows where the Tool Security Policies
tab is (they set the rule from it), and that page lists all pending
approvals, so a per-request URL is unnecessary noise.

- Drop approve_url from USER_APPROVAL_REQUIRED context; keep `token`
  so a caller could correlate but the user doesn't need to act on it.
- Update message + progress text to instruct the LLM to tell the user
  to open the settings UI Tool Security Policies tab.
- Drop the now-unused approval_url_builder param + the
  _settings_secret_prefix plumbing in server.py / settings_ui.py.

Also fix the failing test_defaults (asserted dropped Policy.enabled field)
and the e2e test PUT body that still carried `"enabled": True`.

* feat(policy): schema-driven condition builder for write/destructive tools (#966)

The previous "Add predicate" UX required users to type both the dotted
arg path (e.g. `args.domain`) and the value as JSON. Two problems:
  1. They need to know what fields each tool takes.
  2. They need to know what values are legal (which HA domains exist,
     which entities, etc.).

Replace the free-text path input with a dropdown sourced from the tool's
JSON schema, and replace the free-text value input with a (multi-)select
sourced from HA when the path has a known value source (domain, service,
entity_id today; trivially extensible). Free-text is still available via
an "(other — type a path)" escape hatch and as the automatic fallback
for ops that don't pair with a registry (regex / contains / gt / lt).

Server:
- New `/api/policy/tool-schema?name=...` returns
  `{paths: [...], value_sources: {path: source_key}}`. Read-only tools
  return empty paths so the UI falls back to free-text (gating those is
  low-value but still permitted manually).
- New `/api/policy/value-source?source=...` resolves a source key to a
  live list of choices. In-process 30s TTL cache avoids hammering HA
  when the user explores paths.
- value_sources.py registry maps (tool_name, arg_path) → source_key for
  the common write/destructive surface (call_service, set_entity,
  set_integration_enabled, get_history, etc.). New mappings are one
  dict entry plus, if a new source key, one fetcher.
- Both endpoints mount in addon + secret-prefix routes. Sidecar serves
  503 stubs (no FastMCP registry / HA client in that process).

UI: rename user-facing "predicate" → "condition" (CS jargon → SQL/JIRA
terminology users actually recognise; internal Pydantic class stays
`Predicate` so the wire format is unchanged). Form fetches the schema
lazily on first open, caches it on the card, refetches value choices
when path/op changes.

Includes test_schema_handlers.py covering: missing-name 400, sidecar
503, unknown-tool 404, read-only empty-paths, write-tool paths +
registry, JSON-schema enum passthrough, value-source 400 paths, both
HA-services payload shapes, domain filtering for entities/services,
and upstream-fetch 502 mapping.

* test: include new policy handler keys in sidecar all-keys assertion (#966)

* fix(ui): clearer condition-builder labels, optional value, bareword input (#966)

User feedback on the new form was:
  1. "args.foo" path placeholder is gibberish; no real label on path/value
  2. value box should not be mandatory for ops where backend allows None
  3. typing `lock` into the value box errored with "Invalid JSON" — every
     normal-looking input has to be quoted
  4. for ha_call_service `data` was the only arg without an obvious meaning

Changes:
- Real `<label>`s on the form rows: "Argument:", "Match when:", "Value:".
- Op dropdown shows friendly text ("is present (any value)", "equals",
  "is one of", "matches regex", etc.); wire values unchanged.
- Hint line under the value row reflects the current op so users know
  whether a value is required and roughly what shape it should take.
- Value is now OPTIONAL for ops where the backend accepts a missing
  field (exists, eq, neq, contains). Submitting an empty value omits
  the `value` key from the predicate entirely.
- Bareword inputs auto-coerce: `lock` → `"lock"`, `lock,alarm` → list,
  `42` → number, `true` → bool. Falls back to a clearer error if even
  the smart-coercion can't make JSON.
- Path dropdown options now carry the schema `description` as a
  `title` tooltip, so `data` reads as "Service data dict" on hover
  instead of being a mystery.
- Schema-declared enums render as a value dropdown automatically (no
  registry entry needed) when the path's JSON-schema has `enum`.

Also fix /tmp/extract_js.py — naive paren-counter broke once form
strings started containing parens; switch to ast.parse so future
edits don't silently break the harness.

* feat(policy): wildcard path "args.*" + clearer empty-value semantics (#966)

User asked for a catch-all "any argument equals X" condition and called
out that the previous "Leave blank to gate on null" hint was nonsensical
— a blank value should mean "any value" to a normal user, not "match
the null literal".

Backend:
- Refactor evaluator: `extract_path` → `iter_path_values`, which yields
  every value the dotted path resolves to. A `*` segment fans out across
  the current node (dict values for dicts, items for lists). A path like
  `args.*` thus yields every top-level arg; `args.config.*` yields every
  leaf of the config sub-dict.
- `match_predicate` rewrites to "ANY matching value satisfies the op",
  which collapses to the previous single-value semantics for non-wildcard
  paths. So `path=args.*, op=eq, value="lock"` gates whenever any arg of
  the tool call equals "lock".

UI:
- New "(any argument)" option at the top of the path dropdown, fills
  `args.*` and carries a tooltip explaining the semantic.
- VALUE_OPTIONAL_OPS shrinks to just `exists` — blank value is no longer
  silently accepted for eq/neq/contains. Instead, the value-required
  error fires, and the hint text under the field tells the user to
  switch op to `is present` if they wanted "any value".
- Hint copy revised across all ops so the "what happens with this op +
  blank value" question has a clear answer at each step.

Tests:
- New TestIterPathValues covering top-level, nested, missing, and the
  three wildcard shapes (dict values, list items, empty).
- New TestWildcardPredicate covering eq/in/exists/regex matching via
  `args.*` plus an end-to-end evaluate() test.
- Existing tests still pass with the refactored matcher; signatures of
  the public functions are unchanged.

* fix(ui): default condition path to '(any argument)'; relabel error (#966)

- Drop the '(pick an argument)' placeholder; default the path dropdown
  to '(any argument)' so the form is immediately submittable.
- 'path is required' error reads 'argument is required' if it ever
  fires (it won't on the happy path now).

* feat(policy): auto-save conditions + surface matched_rule in approval error (#966)

UI:
- Drop the manual "Save changes" button on each rule card. Conditions
  now PUT to disk the moment the user clicks "Save condition", clicks
  the × on a condition row, or edits the remember-minutes field
  (debounced 500ms). The only feedback is a small "Saving…" / "Saved."
  status line next to the card.
- Removed the now-dead .policy-save-rule CSS and the markDirty helper.

Server:
- USER_APPROVAL_REQUIRED error context now carries `matched_rule` with
  the rule's tool_name + when[]. Lets the user (and the LLM) tell at a
  glance which rule fired, instead of guessing whether their condition
  saved correctly.

* feat(policy): case-insensitive string comparison in all ops (#966)

Security gates shouldn't fire differently based on whether the LLM
capitalised its argument — 'Lock' and 'LOCK' and 'lock' are the same
operationally. eq/neq/in/not_in/contains lower-case both sides before
comparing when both are strings; regex uses re.IGNORECASE. Non-string
types pass through unchanged so int(1) != str('1') still holds.

* fix(policy): mypy bool cast + broaden e2e coverage (#966)

mypy: bool(_ci(val) == _ci(pv)) — _ci returns Any (passes non-strings
through unchanged), so eq/neq comparisons need an explicit bool wrap.

Tests: previous e2e only covered the happy block→approve→re-call path.
Add four more cases against the live testcontainer:
- wildcard `args.*` gates when any arg matches the value
- wildcard `args.*` passes through when no arg matches
- case-insensitive matching (rule 'lock' gates caller 'LOCK')
- deny → middleware raises USER_DENIED, tool never runs
- remember_minutes>0: second call within the window skips the queue

* refactor(policy): address review-cycle findings (#966)

Gemini (6 unresolved threads):
- Migrate POLICY_LOAD_FAILED / USER_DENIED / USER_APPROVAL_REQUIRED off
  manual `ToolError(json.dumps(...))` onto the canonical
  `raise_tool_error(create_error_response(...))` pattern. Added the
  three error codes to ErrorCode enum.
- Hoist sync `load_policy()` off the event loop via
  `anyio.to_thread.run_sync` in the middleware's policy provider call.
- Add justification comment on `local_provider._list_tools()` (same
  rationale that's already documented in `settings_ui.py`'s tool
  enumerator: public `list_tools()` filters disabled tools but
  operators may still want to author gating rules for them).

Code-reviewer findings:
- ApprovalQueue TOCTOU: two concurrent `on_call_tool` coroutines with
  identical (tool, args_hash) could both miss `find()` and create
  duplicate pending entries; approving one would leave the other
  waiter blocked. Introduce `find_or_create(...)` serialised behind
  an `anyio.Lock`; middleware now uses it.
- ApprovalQueue had no pending-entries cap → memory exhaustion under
  an LLM retry-loop with mutated args. Add `PENDING_CAP = 1000` with
  FIFO eviction of oldest entries when the cap is hit.

Silent-failure-hunter findings:
- handlers.py: `get_tool_schema` and `get_value_source` now
  `logger.exception` before returning 500/502 so FastMCP version
  bumps or HA outages leave a traceable signal instead of opaque
  client errors.
- value_sources fetchers `logger.warning` on unexpected HA response
  shapes (would otherwise silently return empty dropdowns).
- value_sources cache no longer stores empty results — a transient HA
  glitch returning [] would otherwise pin the dropdown blank for 30s.

PR-test-analyzer findings (the critical one):
- test_persistence.py's `test_save_and_roundtrip` passed `Policy(enabled=True, ...)`
  for a field that no longer exists; `extra="ignore"` silently dropped it
  so the test was a no-op assertion. Replace with real round-tripped
  fields (wait_seconds / approval_ttl_minutes / remember_minutes) and
  add an explicit `test_load_drops_unknown_fields` exercising the
  extra="ignore" back-compat contract with a JSON file carrying
  `default_action` + `enabled`.
- Add wildcard scalar/None tests (`args.x.*` against scalar yields
  nothing; doesn't crash).
- Add ApprovalQueue tests: concurrent `find_or_create` shares one
  pending entry; `create` evicts oldest at PENDING_CAP.
- Add handler tests: sidecar value-source returns 503, tool-schema 500
  on `_list_tools` exception, value-source cache key separates per
  params, `_extract_arg_paths` skips malformed property entries.

Comment-analyzer findings:
- Grammar fix in handlers.py `_is_write_or_destructive` docstring.
- model.py docstring "older version of this PR" → "older builds".
- Strip the `(#966)` / `(issue #966)` parentheticals from module
  docstrings, settings_ui CSS/HTML/comments — git blame and the
  commit message carry the link.

* fix(policy): UI surface fetch failures + middleware reissues swept pending (#966)

- Middleware: after _wait_for_decision returns without a verdict, check
  whether the pending entry was swept (TTL elapsed during the wait). If
  so, create a fresh entry before raising USER_APPROVAL_REQUIRED so the
  LLM isn't told to re-call against a dead token.

- UI: policyLoadConfig now surfaces fetch failures in a visible error
  banner instead of silently rendering blank — picks up the
  policy_file_corrupt:true repair hint from the server's 500 response.

- UI: loadValueChoices records the failure (lastValueSourceError) so
  renderHint can show it under the value row. The dropdown still
  downgrades to free-text, but the user can now tell a transient HA
  outage from "no value source registered for this path".

- UI: renderValueControl uses an autoincrement seq so rapid path/op
  edits don't let an earlier slow fetch's DOM mutation land after a
  newer one's (similar to the autoSave pattern).

* fix(policy): logger.info on silent decide-False; debug log on gt/lt type-mismatch; strengthen event-wake test (#966)

- ApprovalQueue.approve/deny: emit logger.info when the call returns
  False (unknown token or already decided) — was silent. Helps debug
  the case where the middleware's consume_and_maybe_remember races
  with an out-of-band decide.

- Evaluator gt/lt TypeError fallback now logs at debug so a user
  whose 'battery_level < 20' rule never fires can see that the arg
  came in as a string and tighten the rule.

- test_event_wakes_waiter now measures elapsed wait time and asserts
  < 200ms, ruling out a hidden poll-loop impl that would still pass
  the previous decision-only check.

* style: ruff format evaluator.py for CI's 0.15.13 (#966)

Local ruff 0.15.7 didn't wrap the multi-arg logger.debug call;
CI's ruff 0.15.13 does. Upgrading local toolchain to match.

* feat(policy): clear remember-cache on save, clearer disabled-state UX, mirror master toggle (#966)

Three things:

1. Remember-cache invalidation on policy save (B2).
   ApprovalQueue.clear_remember_cache() drops every remembered
   approval; put_config calls it after a successful save. Without
   this, tightening a rule was silently bypassed by any in-flight
   remembered approvals until their window expired.

2. Better 503 / "unavailable" messaging (B8 + the broader issue).
   The stub handler's 503 message used to read "Live approvals
   unavailable in this mode (sidecar)" even when the real cause was
   the feature being turned off in addon config — the user had no
   way to tell from the UI. Updated to call out all three causes
   (feature off, sidecar, ImportError) and point at the addon log.
   The pending-list JS now checks policyState.enabled first and
   shows "Tool Security Policies is turned off" when that's the
   actual reason, falling back to the server's 503 message
   otherwise.

3. Mirror the master toggle onto the Tool Security Policies tab.
   Was only exposed in Server Settings before — users on the
   Policies tab had to navigate away to find the on/off switch.
   New checkbox at the top of the tab posts to the same
   /api/settings/features endpoint, so the two surfaces are live
   mirrors of the same addon-config flag.

* test(policy): fix JS-harness drift guard + lock policy-tab behaviour (#966)

The merged-in JSDOM behaviour test (#1425) failed collection because
its hardcoded _TOP_LEVEL_ELEMENT_IDS list didn't yet know about the
policy-tab handlers this PR adds (policy-master-toggle, policy-save-global-btn).
Add them, plus matching DOM stubs in _build_min_dom so the init pass
doesn't throw on the addEventListener calls.

While the file is open, add three behavioural tests that pin the new
condition-builder UX wiring:

- Master toggle change POSTs to /api/settings/features with the
  enable_tool_security_policies flag (so the on-tab toggle stays a
  true mirror of the Server-Settings checkbox).
- /api/policy/pending 503 renders "Tool Security Policies is turned
  off" when the addon flag is off (avoids the old misleading
  "sidecar / unavailable" copy).
- /api/policy/pending 503 propagates the server's addon-log message
  verbatim when the flag IS on but the queue is unreachable (so users
  know where to look for ImportError details).

The parse-coverage path catches syntax breaks already; these tests
catch behavioural regressions on top of it.

* refactor(policy): address 2nd-round review findings (#966)

Verified all 23 findings from the 2nd pr-review-toolkit pass against
the code; fixed 22 (skipping #13 — the JSDOM seq-cancel race test is
high-effort to author reliably and the production guard is small
enough that bench-level review catches regressions).

## Correctness / silent-failure

- ApprovalQueue PENDING_CAP eviction now sorts by
  `(decision == "pending", created_at)` so resolved entries evict
  first. When a still-pending entry MUST be evicted (cap full, no
  resolved to drop), `.set()` its event so any waiter in
  `_wait_for_decision` wakes immediately instead of blocking the
  full wait_seconds against a row that no longer exists.
- Middleware: log INFO with old + new token on the
  reissue-after-sweep branch so operators can correlate
  "approval row keeps reappearing" with the actual cause.
- Middleware: scope `clear_remember_cache` to "rules actually
  changed" — editing only wait_seconds / approval_ttl_minutes no
  longer blows away in-flight remembered approvals.
- Policy: model_validator requires `wait_seconds < approval_ttl_minutes * 60`
  so the middleware can't repeatedly issue fresh pending entries
  because the wait outlasted the TTL.
- value_sources: cache key uses `urllib.parse.urlencode` so a future
  param value containing `=`/`&` can't collide with another key.
- ApprovalQueue: `approve`/`deny` on unknown token now logs WARNING
  (security-gating endpoint, suggests UI bug or token probing).
  Already-decided stays INFO (legitimate race).

## UI

- settings_ui.policyState gains an `enabledKnown` tri-state bit so
  downstream branches (`policyLoadPending`'s "feature off" copy,
  master-toggle revert) don't false-confidently route to the
  "disabled" message when the features fetch actually failed.
- Master-toggle change handler reverts the checkbox on save failure
  AND syncs from `policyState.enabled` after a successful load —
  no more "UI says on, server says off" drift.
- `policyLoadConfig` appends "(response body unparseable)" when the
  500 body isn't JSON (e.g. HTML error page from a misrouted sidecar),
  so the operator sees more than just "HTTP 500".
- `policyLoadPending` surfaces fetch errors inline ("Lost contact
  with server, retrying") instead of silently freezing the list.
- `fetchToolSchema` records `lastValueSourceError` on failure so the
  hint banner explains why the value dropdown silently downgraded
  to free text.

## Tests

- test_handlers: `test_put_config_clears_remember_cache_when_rules_change`
  + `test_put_config_preserves_remember_cache_when_only_timing_changes`
  lock in the scoped invalidation.
- test_approval_queue: `test_create_evicts_resolved_entries_before_pending`,
  `test_evicting_pending_wakes_its_waiter`,
  `test_create_after_sweep_still_evicts_when_pending_fills_cap`
  cover the new eviction rules. The strengthened
  `test_find_or_create_lock_blocks_concurrent_create_under_real_race`
  inserts a yield point inside the lock body so the lock actually
  matters to the assertion (the previous test would pass even
  without the lock under anyio's cooperative scheduler).
- test_middleware: `test_swept_pending_during_wait_is_reissued_with_fresh_token`
  exercises the previously-untested reissue branch.
- test_schema_handlers: `test_value_source_empty_result_not_cached`
  proves the empty-result no-cache guard actually triggers a refetch.
- test_settings_ui_js_behavior: master-toggle test now JSON-parses
  the POST body and structurally asserts
  `flags.enable_tool_security_policies is True` instead of loose
  substring matching.

## Comments

- approval_queue.py PENDING_CAP docstring now matches implementation
  (was promising "resolved first" before the implementation actually
  did it).
- evaluator.py: gt-branch comment example uses ">" not "<".
- handlers.py: dropped cross-reference to settings_ui that would rot.
- middleware.py: trimmed "fast on warm disk" speculation.
- value_sources.py: trimmed "(WebSocket reconnect, auth lapse)"
  speculation in the empty-cache comment.
- settings_ui.py: four comments still saying "predicates" updated
  to "conditions" to match user-facing terminology.

* fix(ui): blank value on eq/in/etc coerces to op=exists (#966)

User expects 'leave value blank to gate on the argument's mere
presence regardless of value' to work across the equality-ish ops,
not just op=exists. Earlier I had the form reject blank value for
anything other than exists with a 'value is required' error.

Now: for eq / neq / in / not_in / contains / exists, leaving the
value blank silently coerces the predicate to op=exists on save.
The condition row then reads as 'args.* exists' which is the right
description of what's stored.

Ops that genuinely need a value (regex / gt / lt) still raise
'value is required for op=...'. The hint text under each op
updated to call out 'Leave blank to gate on any value' where it
applies.

* docs(addon): drop beta tag from Tool Security Policies (#966)

The feature is stable enough to ship as a default-supported addon
config option, not a beta. Also corrects two pieces of doc drift
that landed here originally:

- 'approval URL' wording → 'tell the user to open the Tool Security
  Policies tab' (the URL field was dropped earlier in this PR)
- 'predicates' → 'conditions' (matches the user-facing terminology
  the UI now uses)

Touches both prod and dev addon directories (config.yaml-driven UI
text + the rendered DOCS.md).

* docs(beta): describe 3-path enabling (dev addon, stable + web UI, env vars) (#1164)

* feat(config): advanced settings registry + beta master toggle field (#1164)

- Add ``ADVANCED_SETTINGS_FIELDS`` registry (21 fields across connection,
  search, operations, diagnostics, tools_surface, beta_codemode sections)
- Add ``_ADVANCED_SETTINGS_BOUNDS`` and ``_ADVANCED_SETTINGS_CHOICES``
  dicts for UI/POST validation
- Add ``BETA_FEATURE_FIELDS`` tuple for master-gate enforcement
- Add ``enable_beta_features`` Settings field (alias ENABLE_BETA_FEATURES,
  default False) as the master beta toggle
- Update ``FEATURE_FLAG_FIELDS``: add ``enable_beta_features`` at front,
  ``enable_code_mode`` at end; reorder for UI grouping
- Extend ``BACKUP_OVERRIDE_FIELDS`` from 3 to 5 entries (add
  ``auto_backup_dir`` and ``auto_backup_calendar_lookahead_days``)
- Extend ``_apply_backup_overrides`` to handle ``str`` type; widen
  ``coerced`` annotation to ``bool | int | str``; add bounds check for
  ``auto_backup_calendar_lookahead_days`` (1..365)
- Add coverage gate test asserting every Settings env alias is registered
  in one of the three panel registries (or in the explicit ALLOWLIST)
- Add ``test_enable_beta_features_default_false``

* refactor(config): code-review fixups — docstring clarity, stricter bool reject, null-byte guard (#1164)

* feat(settings-ui): per-tool env-pin for DISABLED_TOOLS / PINNED_TOOLS (#1164 addendum)

Add env_pinned_tools() and effective_tool_config() helpers so tools listed
in DISABLED_TOOLS / PINNED_TOOLS env vars stay read-only at runtime even
after tool_config.json has been written. The _get_tools GET handler now
includes env_pinned metadata per tool entry; _save_tools rejects incoming
flips of env-pinned tools with HTTP 409. Server.py startup path updated to
use effective_tool_config() so env pins apply at boot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(config): master beta gate + advanced overrides apply (#1164)

- Rewrite _apply_feature_flag_overrides: lift addon-mode short-circuit
  for beta fields (enable_beta_features + BETA_FEATURE_FIELDS), add
  master gate that forces all 5 beta sub-flags to False when master is
  off regardless of env/file state
- Update get_feature_flag_origin: beta fields never return "addon" —
  they follow standalone precedence in either mode
- Add _apply_advanced_overrides: reads feature_flags.json for all editable
  ADVANCED_SETTINGS_FIELDS entries; skips display-only fields; validates
  types, bounds, and choices before setattr
- Wire _apply_advanced_overrides into get_global_settings (runs after
  feature-flag + backup passes)
- Add 18 unit tests covering master gate semantics, addon-mode carve-out,
  advanced-override int/str/float/display-only/out-of-bounds/invalid-
  choice cases, and backward-compat (pre-master override files)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(settings-ui): code-review fixups — drop dup env_pinned, settings param, msg + tests (#1164)

Address code-quality review feedback on 32d67b4d:

- Drop the redundant per-tool-entry `env_pinned` field from the GET
  /api/settings/tools response. The top-level `env_pinned` map is the
  single source of truth; UI does O(1) lookups against it.
- `effective_tool_config()` now accepts an optional `settings`
  parameter (mirrors `load_tool_config()`); restores dependency
  injection at the server.py startup callsite (`self.settings`).
- 409 rejection message uses comma-joined names instead of Python list
  repr for better human readability; structured `context.rejected`
  remains for programmatic access.
- Add `test_get_tools_includes_env_pinned_map` test covering the new
  GET response field.
- Symmetrize `get_data_dir.cache_clear()` calls at both ends of each
  tmp_path test so cross-test cache pollution can't leak in.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(config): code-review fixups — docstring accuracy, top-level import, null-byte test (#1164)

- Correct _apply_advanced_overrides docstring: most advanced fields
  ARE in the addon config.yaml schema (backup_hint, verify_ssl,
  enabled_tool_modules, etc.) and are handled correctly via the env-
  var-wins check because start.py exports them. Only code_mode_* and
  mcp_server_version are file-only in either mode.
- Move `from typing import Any` from function body to module-level
  imports (stdlib typing — no need to defer).
- Add test_advanced_override_str_field_with_null_byte_rejected to
  cover the previously-unexercised null-byte reject branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(settings-ui): render env-pinned tool rows as read-only + update addon translations (#1164)

- Add `toolEnvPinned` module-level map; populate from `data.env_pinned` in `loadTools()`
- Tool rows for env-pinned tools get `.env-pinned` class, all inputs disabled, and
  a `feature-locked-note` banner naming the env var (DISABLED_TOOLS or PINNED_TOOLS)
- Group master toggle excludes env-pinned tools from bulk enable/disable
- Update `pinNotice` copy to describe the env-pinned lock-until-unset semantics
- Update `homeassistant-addon-dev/translations/en.yaml` descriptions for
  `disabled_tools` and `pinned_tools` to reflect that they are operator-level
  locks, not seed-only values
- Add JSDOM behavioural tests for env-pinned disabled and pinned tool rows

* feat(settings-ui): /api/settings/advanced GET+POST handlers (#1164)

* feat(settings-ui): render advanced settings sections in Server Settings tab (#1164)

* feat(settings-ui): beta master toggle + nested sub-row gating + 409 rejection (#1164)

* feat(settings-ui): nest code-mode sub-numerics under enable_code_mode (#1164)

* test(addon): assert start.py auto-enables master beta in dev addon mode + stable schema absence (#1164)

* feat(addon): start.py auto-enables ENABLE_BETA_FEATURES=true when dev addon options carry beta keys (#1164)

* fix(settings-ui): post-CI/post-review fixes — backup-config str+range, addon-mode gate skip, e2e env, JSDOM ids, stale comments (#1164)

* fix(config): beta-sub-flag origin returns addon in dev mode (env var presence as signal) (#1164)

* fix(tests): addon-mode save tests use non-beta flag matching new origin semantics (#1164)

* refactor(settings-ui): loadAdvancedSettings error parity + atomic-write helper reuse + beta_sub_flags via API (#1164)

* refactor(config): narrower setattr errors + hasattr precheck + Gemini coerced-decl nit (#1164)

* refactor(settings-ui): hoist override-file read + display-only warning + missing test coverage (#1164)

* test(settings-ui): section-uniqueness + registry-disjoint + tighter env-pin assertions + accurate docstring (#1164)

* refactor(addon): extract maybe_auto_enable_beta_master helper + real unit tests (#1164)

* test(settings-ui): JSDOM coverage for advanced sections + master live-render; verify file untouched on 400 (#1164)

* refactor(config): NamedTuple registries + import-time validator; finish silent-failure + JSDOM nesting tests (#1164)

* fix(tests): restore standalone-mode assertion + add adv section containers (#1164)

`replace_all` from an earlier sweep had pivoted the standalone-mode save
assertion onto a beta sub-flag, so the master-gate guard now rejected
the request and the test failed. Restore the non-beta `enable_tool_search`
flag here — the assertion is about the unified save-contract shape, not
the beta path.

JSDOM `TestAdvancedSectionRender` tests were failing because MIN_DOM
lacked the five `adv*` section containers; `renderAdvancedSection` would
silently no-op (getElementById returned null) and the assertions fired
against an empty body. Add `advConnection`, `advSearch`, `advOperations`,
`advToolsSurface`, `advDiagnostics` to `_TOP_LEVEL_ELEMENT_IDS` so
`_build_min_dom` emits `<div>` containers for them.

* fix(tests): adopt master beta gate + new advanced handler keys (#1164)

Three failures surfaced after rebasing onto upstream/master:

1. ``test_returns_all_handler_keys`` expected the pre-#1164 handler
   set. Add ``get_advanced_settings`` / ``save_advanced_settings``
   to the expected keys.

2. ``test_tools_filesystem.TestFeatureFlag::test_enabled_with_*``
   broke because the master beta gate now forces every beta sub-flag
   False at runtime when ``ENABLE_BETA_FEATURES`` is unset. Set both
   env vars together in the enabling tests so they exercise the
   sub-flag bool parsing in isolation, and add an explicit test for
   the gated behavior so a future regression in
   ``_apply_feature_flag_overrides`` surfaces here too.

3. ``test_yaml_config_tool.enable_flag`` fixture sets
   ``ENABLE_YAML_CONFIG_EDITING`` but didn't set the master, so the
   cached settings landed with the sub-flag forced False — would have
   broken next-up after the filesystem tests. Set
   ``ENABLE_BETA_FEATURES`` alongside.

* fix(policy): contains operator case-insensitive on list-membership branch

Pre-fix:
  case "contains":
      if isinstance(val, str) and isinstance(pv, str):
          return pv.lower() in val.lower()           # CI
      return isinstance(val, (list, tuple, set)) and pv in val  # case-SENSITIVE

The string-in-string branch was already case-insensitive (matching the
``_ci``-equivalent treatment that ``eq`` / ``in`` / ``not_in`` apply),
but the list-membership branch fell through to Python's default ``in``
operator. A rule listing ``["light.kitchen"]`` would not fire on an
LLM passing ``["Light.Kitchen"]`` — silent gate failure.

Bring it in line with the other string-op branches via per-element
``_ci``, which passes non-string entries through unchanged so
mixed-type collections still get natural equality semantics.

Caught by Gemini Code Assist on #1431; addressing inline rather than
opening a follow-up because the policy module is now in master
(#1421) and any reviewer running the suite would see the
case-sensitivity asymmetry in the existing TestCaseInsensitive class.

* feat(settings-ui): addon-aware locked banner, beta-at-bottom, danger warning, dual save buttons, fork-dev stable copy (#1164)

Five user-feedback fixes against the Server Settings UI:

1. Locked-banner copy adapts in addon mode. The standalone "Set via
   env var X — unset it to edit here." copy is misleading in HA
   addon mode where the operator has no env-var surface (start.py
   writes the env vars from /data/options.json; Supervisor writes
   the rest). Endpoints now return is_addon; the JS helper
   envLockedNoteHtml swaps in addon-aware copy that points users at
   the addon Configuration tab. Master beta gets an extra hint
   explaining the auto-enable rule.

2. Beta block rendered into a dedicated bottom-of-panel betaBody
   container instead of featuresBody. The dangerous block sits last
   so users see safer settings first; a "Beta features (dangerous)"
   header in warning color marks the boundary.

3. enable_beta_features help-text rewritten to lead with an explicit
   danger warning (permanent damage to HA, no warranty, take a
   backup, own risk). Mirrored as a blockquote at the top of the
   dev addon's beta-options section in DOCS.md so the addon UI also
   surfaces the risk.

4. Save button redesigned — primary-CTA styling (bigger, accent
   background, hover state), duplicated at the top of the panel so a
   user scrolling either end can hit save, and paired with a
   prominent two-step note explaining that Save → Restart are both
   required for changes to take effect.

5. New scripts/fork-dev/copy-stable.sh + restore-dev.sh let
   maintainers whose only HA test path is the fork-dev addon flip
   homeassistant-addon-dev/ between dev-flavor and a stable-mirroring
   "stable test" flavor. Round-trip clean — copy → restore restores
   the index exactly.

JSDOM behavioural coverage added for each of (1)-(4); MIN_DOM updated
with the new top-row + beta-section element ids.

* fix(tests): JSDOM beta-block tests assert against production HTML / fixed regex (#1164)

Three CI failures from the previous commit:

- ``test_beta_section_header_present_with_danger_styling`` and
  ``test_two_step_save_note_present`` asserted on static
  ``panel-server`` markup that lives in the rendered HTML template,
  not in any JS-populated container. MIN_DOM doesn't replicate the
  full panel-server children (by design — it's a minimal handler
  stub), so the assertions never found their target strings. Switch
  both tests to assert directly against ``_SETTINGS_HTML``; the
  presence of the markup at the template level is the property we
  actually want to lock down.

- ``test_beta_rows_render_into_betaBody`` used a regex
  ``<div id="betaBody">(.*?)</div>\s*<div`` whose ``</div>\s*<div``
  boundary matched the very first nested ``</div><div`` *inside* the
  master row (after the ``.feature-name`` close tag), so ``bb_content``
  only captured the header of the master row. Anchor the boundary on
  ``</div>\s*</body>`` instead — non-greedy capture between the
  ``betaBody`` open tag and the body close still gives the full
  container content. Added a fallback path that asserts on class
  markers + non-leakage to featuresBody if the regex still misses.

* feat(settings): fix stuck-master bug, master in dev schema, cascade-clear, drop connection panel, sync backup_hint+verify_ssl (#1164)

Six fixes against the Server Settings + addon Configuration surfaces:

1. ``maybe_auto_enable_beta_master`` now requires ``config.get(key) is
   True`` instead of ``key in config``. The bare presence check fired
   the moment HA Supervisor merged the dev addon's schema defaults
   into options.json, locking the master "on" with origin=env on
   every fresh dev install even when all 5 sub-flags were False.
   New unit tests cover the truthy / all-false / one-of-many /
   non-bool-truthy permutations so a future regression on the
   semantic fails loudly.

2. Master ``enable_beta_features`` moved into the dev addon
   Configuration tab (schema + options + translations + DOCS.md).
   Defaults ON in dev — beta tools are the channel's purpose, so a
   fresh install lights them up without the user round-tripping
   to the web UI. Stable's schema is unchanged; the standalone web
   UI master path remains the gate there. start.py writes
   ENABLE_BETA_FEATURES from options.json only when the key is
   present; ``get_feature_flag_origin`` now treats the master like
   the sub-flags (env-var presence in addon mode → origin=addon).
   ``maybe_auto_enable_beta_master`` is kept as a one-cycle legacy
   bridge for installs whose options.json pre-dates the master key.

3. Beta sub-flags also default ON in the dev addon options block.

4. Master-off cascade clear: flipping ``enable_beta_features=False``
   in ``_save_feature_flags`` now also writes False for every truthy
   beta sub-flag in the same save. Without this, sub-flags stayed
   True in the override file and resumed the moment the master was
   flipped back on — UX bug the user reported as "having to turn off
   every toggle individually." JS mirrors the cascade so sub-rows
   visually de-toggle on master-off without a page reload.

5. "Connection (display only)" section removed from the Server
   Settings panel. The read-only HOMEASSISTANT_URL / TOKEN /
   SUPERVISOR_TOKEN fields just wasted space — operators already see
   them in addon logs and configuration. Registry entries kept in
   ADVANCED_SETTINGS_FIELDS so the API still returns them (env-pin
   debugging, future surfaces). ``verify_ssl`` moved from
   ``connection`` to ``operations`` so it still renders in the panel.

6. ``backup_hint`` and ``verify_ssl`` now sync between addon
   Configuration and the web UI like feature flags do. New
   ``ADDON_SYNCED_ADVANCED_FIELDS`` set drives the origin helper
   (returns ``'addon'`` in addon mode for these) and the save
   handler (routes their writes through Supervisor
   ``/addons/self/options`` instead of the override file).

Cross-surface gate visibility note appended to every beta sub-flag
description in ``translations/en.yaml`` (and a top-of-options note
on the master) so addon Configuration users know the web UI master
gates everything. Locked-banner addon-mode copy from the earlier
commit was already in place; tests in this commit re-target
fixtures from the removed connection section to ``search``.

* fix(addon-dev): beta sub-flags default OFF — only the master defaults ON (#1164)

Mis-read of the user's intent in the previous commit. The intended
shape for the dev addon's fresh-install defaults is:

  enable_beta_features:                   true   ← gate unlocked
  enable_yaml_config_editing:             false  ← user opts in
  enable_filesystem_tools:                false  ← user opts in
  enable_custom_component_integration:    false  ← user opts in
  enable_code_mode:                       false  ← user opts in
  enable_lite_docstrings:                 false  ← user opts in

The previous commit defaulted every sub-flag to true alongside the
master, which would have shipped every beta tool live on a fresh
dev install — including filesystem writes and the YAML config
editor. Each sub-flag mutates the user's HA system, so they remain
opt-in even on the dev channel; the master being on just means the
gate is open.

* revert(scope): drop scripts/fork-dev/ — maintainer tooling, wrong repo (#1164)

Pushed these in 69b7a235 as part of task #17. They're personal-fork
test tooling for the fork-dev addon workflow — they have no business
on master. Removing from the PR; they're still in this branch's
git history if anyone needs to fish them back out.

* fix(addon+ui): #1431 review pass — restore MCP_HOST, gate sub-flag env writes, sane save (#1164)

Round-2 review pass addressed 14 verified findings:

**Bugs**:
- **MCP_HOST regression** restored. The PR's earlier merge of
  upstream/master dropped the `bind_host = os.getenv("MCP_HOST",
  "0.0.0.0")` block introduced by #1434/#1436. `mcp.run(host=...)`
  now goes through `bind_host` again.
- **Beta sub-flag env vars** are now written only when the matching
  key is present in `/data/options.json`. start.py was writing
  ENABLE_YAML_CONFIG_EDITING=false (etc.) unconditionally on stable
  addon, marking those fields origin='addon' in the web UI; the
  user's save then POSTed to Supervisor which rejected because the
  keys are not in stable's schema.
- **Env-pinned tool save 409**: `_save_tools` now accepts re-sends
  whose state matches the env-pinned value, rejecting only true
  mismatches. The JS `saveConfig` POSTs the entire `toolStates` map
  including env-pinned rows; without this fix every save with
  `DISABLED_TOOLS` / `PINNED_TOOLS` non-empty would 409.
- **Cascade-clear** now reads the persisted override file directly
  via `_read_feature_flag_override_file()` instead of
  `get_global_settings()` (whose master gate had already forced
  sub-flags to False, hiding stale-true overrides). Also force-False
  sub-flags that are explicitly True in the same payload as
  master=false, so `{master:false, sub:true}` no longer lands an
  inconsistent persisted state.
- **Master beta-gate check** is now applied uniformly (no more
  "skip in addon mode" carve-out). The skip existed because the
  legacy auto-enable wrote ENABLE_BETA_FEATURES from sub-flag
  presence; now start.py writes the master from its own options
  key, so the gate is sound to apply in both modes.
- **Dev-upgrade silent-disable warning**: start.py logs when
  master=false but a sub-flag is true in options.json, so an
  operator who toggled the master off in Configuration after a
  pre-#1164 dev install sees why their previously-enabled beta
  tools went away.
- **Mixed-batch advanced save** is now split client-side. The
  server-side guard that returned 500 stays as a defense, but the
  UI no longer triggers it — `saveAdvancedSettings` partitions
  `_advancedDirty` into addon-routed and file-routed batches.

**Logging / defensive code**:
- Supervisor failure in `_save_advanced_settings` now logs before
  returning, matching the sibling `_save_feature_flags` /
  `_save_backup_config` handlers.
- The three `assert sup_err is not None` sites that would crash
  under `python -O` now explicitly return INTERNAL_ERROR (covers
  the addon-route paths in feature flags and advanced settings).
- `_apply_advanced_overrides` narrows `except Exception` to
  `(ValueError, TypeError)`, matching the parallel
  `_apply_feature_flag_overrides` exception shape.
- `maybe_auto_enable_beta_master` now logs which sub-flag(s)
  triggered the legacy bridge when it fires, with a removal-
  candidate note in the docstring.

**Docs / UX**:
- Docstring drift fixed in `get_feature_flag_origin`,
  `_get_advanced_settings`, `_save_advanced_settings`.
- `envLockedNoteHtml` master copy rewritten — origin='env' on the
  master is now only the legacy-bridge path, not the default
  dev-addon path.
- "Bottom" save button now actually at the bottom of `panel-server`
  (below the beta block and its code-mode sub-numerics). Second
  two-step save note duplicated near the bottom row so users
  editing dangerous beta toggles also see it.

Findings deferred to follow-up (legit but bigger than this pass):
- A.2 concurrent-save read-modify-write race (needs an
  `asyncio.Lock` around the override-file path; functionality is
  safe today because the runtime gate hides the persisted-state
  inconsistency).
- A.4 master-flip via addon Configuration tab → no cascade (the
  runtime gate + new log warning cover the observable surface).
- F.* missing tests for new behaviours — adding in a follow-up
  commit.

* test(settings): cover #1431 review pass fixes (#1164)

- ``test_env_pinned_noop_resend_does_not_409`` — JS saveConfig POSTs
  the entire toolStates map; env-pinned no-op resend must be accepted.
- ``test_env_pinned_value_mismatch_still_409s`` — pin true flips still
  rejected.
- ``test_save_features_cascade_clears_subflag_even_when_payload_says_true``
  — in-payload {master:false, sub:true} → 409 instead of inconsistent
  persisted state.
- ``test_save_features_cascade_reads_override_file_not_post_gate_settings``
  — cascade reads the file directly so it catches stale-true sub-flag
  overrides hidden by the master gate on the resolved Settings.
- ``test_stable_addon_does_not_declare_enable_beta_features`` and
  ``test_dev_addon_declares_enable_beta_features_master_in_schema`` —
  lock the schema asymmetry that makes the dev/stable channel
  distinction work.
- ``test_dev_addon_defaults_every_beta_subflag_to_false`` — sub-flags
  remain opt-in even on dev.

* fix(settings): serialise override-file RMW + cover addon-synced advanced save (#1164)

A.2 (concurrent-save race): both ``_save_feature_flags`` and
``_save_advanced_settings`` touch the same ``feature_flags.json``
override file. Two near-simultaneous requests could interleave their
read/merge/write and clobber each other's persisted state. The
runtime master gate hid the inconsistency for beta sub-flags, but
other field combinations (advanced + feature-flag in the same
window) would have lost state silently.

Wrap the RMW window in an ``asyncio.Lock`` (lazy-initialised under
the live event loop so module import doesn't bind to no loop). Both
handlers acquire the same lock, so saves serialise correctly.

F.35 + F.40 (test coverage):
- ``test_save_advanced_addon_synced_routes_through_supervisor`` —
  ``backup_hint`` / ``verify_ssl`` saves in addon mode call
  ``_supervisor_merge_and_post_options``, return ``mode='addon'``,
  do NOT write the override file.
- ``test_save_advanced_addon_synced_supervisor_4xx_surfaces_validation_failed``
  — Supervisor schema rejection surfaces as
  ``CONFIG_VALIDATION_FAILED`` with the Supervisor status code
  preserved, not a generic 502.
- ``test_origin_for_addon_synced_field_is_addon_in_addon_mode`` —
  pins the origin matrix: ``backup_hint`` / ``verify_ssl`` come back
  ``origin='addon, editable'`` in addon mode; non-synced env-pinned
  fields stay ``origin='env, locked'``.

* fix(settings): A.8 preserve sub-flag visual + cover remaining deferred test gaps (#1164)

A.8 — JS master-off no longer flips sub-flag checkboxes visually.
Previously the cascade-clear set ``_lastFeatureFlags[sub].value =
false`` on master-off so the re-render painted sub-rows unchecked.
That fought the user's mental model — they expressed intent on
individual sub-flags, master-off shouldn't visually wipe that
context. Now sub-rows stay checked + dimmed + disabled after the
master flips off; the server-side cascade still clears the values
on disk, so a refresh shows the cleared state, and a failed save
leaves the visible checked state matching the actual on-disk state.

F.37 — ``test_master_off_click_dims_subrow_live_without_clobbering_value``
dispatches a real change event on the master input and asserts the
sub-row goes dimmed + disabled but keeps its checked attribute.

F.38 — three cross-mode origin permutations for the master:
  - dev-addon env-set → 'addon'
  - standalone env-set → 'env'
  - addon mode + file override (no env) → 'file'

F.42 — ``test_dual_save_buttons_mirror_disabled_and_status_state``
probes both ``advSaveStatus`` / ``advSaveStatusTop`` text and both
buttons' disabled state via a hidden probe div, asserts the mirror
holds at save completion.

* fix(tests): probe JSDOM properties via probe div; assert mid-save mirror (#1164)

Two test bugs in 25b45ab7's new assertions:

1. ``test_master_off_click_dims_subrow_live_without_clobbering_value``
   asserted on the literal string ``"checked"`` in the serialised
   DOM. ``input.checked`` is a DOM property (not an HTML attribute),
   so JSDOM's serialiser doesn't emit it. The .checked state IS true
   at the property level — just invisible to the regex. Probe via a
   hidden ``__sub_state_probe`` div that reads the .checked / .disabled
   properties and writes them to data-* attributes.

2. ``test_dual_save_buttons_mirror_disabled_and_status_state`` probed
   AFTER the full save+reload chain, by which point
   loadAdvancedSettings() had blanked both status text els.
   Restructure the test to probe SYNCHRONOUSLY after ``click()``,
   while saveAdvancedSettings is mid-flight (status="Saving…",
   both buttons disabled). That's the actual mirror invariant we
   wanted to lock — the helpers ``_setAdvSaveDisabled(true)`` and
   ``_setAdvSaveStatus('Saving…')`` run synchronously before the
   first await.

* feat(settings): drop sub-flag cascade-clear; restore advanced_debug_logging translation; clearer Save-button copy (#1164)

Three user-reported fixes from stable-addon testing:

1. Stable add-on's ``translations/en.yaml`` was missing the
   ``advanced_debug_logging`` description — the schema declares the
   toggle in ``config.yaml:49`` but no translation ever shipped, so
   the addon Configuration UI showed an unlabelled checkbox. Add the
   missing entry (same wording as dev's translation).

2. Big "Save advanced settings" button used to say "Nothing to save."
   after the user toggled a feature flag (beta master, Tool Search,
   etc.). Feature-flag toggles auto-save on click via
   ``saveFeatureFlag`` — they never enter ``_advancedDirty``, so the
   advanced-save button sees nothing to do. When the restart banner
   is already showing (recent feature-flag save), surface that
   explicitly: "No advanced changes to save — your feature-flag
   toggles already saved on click. Click Restart above to apply
   them." Falls back to the original "Nothing to save." copy when
   there's no pending restart.

3. Drop the master-off cascade-clear behaviour entirely. The runtime
   master gate in ``_apply_feature_flag_overrides`` already forces
   every beta sub-flag to False whenever the master is off, so the
   tools stay disabled at runtime regardless of file state. Leaving
   the sub-flag values in the override file means toggling the
   master off → on restores the user's prior sub-flag selections
   automatically; the previous cascade-clear forced users to
   re-check each sub-flag after every master cycle, which was the
   wrong UX trade for an opt-in beta surface.

The master-gate check is unchanged — it still rejects payloads that
try to enable a sub-flag while the effective master is off, so the
"sub true while master false in same payload" inconsistency still
can't land. The cascade-clear was a separate (now-removed) defence.

Tests updated:
- ``test_save_features_master_off_preserves_subflag_values`` — was
  ``test_save_features_cascade_clears_subflags_when_master_off``;
  asserts the new "preserve" semantics.
- ``test_save_features_master_on_restores_runtime_subflag_values``
  — new round-trip test for master off → on restoring sub-flags.
- ``test_save_features_payload_master_false_sub_true_rejected_by_gate``
  — renamed; asserts gate rejection still covers the inconsistent
  payload.
- ``test_save_features_cascade_reads_override_file_not_post_gate_settings``
  — deleted (no cascade, no reason to test cascade's read path).
- ``test_save_features_master_off_applied_dict_contains_only_master``
  — new no-cascade pin; ``applied`` carries only the master flip.

* fix(settings): #1431 review pass — address 13 verified findings (#1164)

Round-3 review pass landed 13 verified findings; the cosmetic
"persisted-value visible in UI after F5" item was explicitly skipped
per user direction (UI shows post-gate value when master off; user
accepts this since beta tools are actually disabled at runtime — the
preserve-across-master-cycle UX is at the data layer, not the visual).

Source fixes:

- **Save button copy is now source-blind** — the previous "your
  feature-flag toggles already saved on click" claim only held when
  ``saveFeatureFlag`` raised ``restartNotice``; tool-config pin saves,
  backup-config saves, and cross-tab ``restart-required`` broadcasts
  also raise it. New copy: "a restart is pending. Click Restart above
  to apply your prior changes." (#2)
- **Stale F.37 test docstring** describing the deleted server-side
  cascade rewritten. (#3)
- **Beta-gate INFO log noise** — cascade-clear removal meant the gate
  could fire its "forcing %s=False" line every Settings rebuild,
  spamming addon logs once a user had truthy sub-flags persisted.
  Dedup via ``_BETA_GATE_LOGGED`` set per process, cleared on
  ``_reset_global_settings``. (#9)
- **Lazy-lock docstring** updated to reflect Python 3.13 semantics
  (``asyncio.Lock()`` no longer takes a loop arg; the lazy pattern
  still serves test fixtures and single-loop deployment, with the
  invariant documented). (#10)
- **Addon-mode carve-out comment** clarified to distinguish dev
  (master in schema) from stable (master web-UI-only). (#14)
- **probe-div null branch** in F.37 now writes ``data-error`` so a
  failing test points at "selector missed" vs "value flipped"
  unambiguously. (#17)

Tests added:

- ``test_translations_cover_every_schema_key`` — parity check that
  every ``schema:`` key has a non-empty translation ``name`` and
  ``description``. Parameterised across stable + dev addons. Pins
  the class of silent gap that this PR's ``advanced_debug_logging``
  fix addressed. (#4)
- ``test_save_features_acquires_override_file_lock`` +
  ``test_save_advanced_acquires_override_file_lock`` — counting-lock
  wrapper asserts ``async with _get_override_file_lock()`` runs
  exactly once in each file-mode write path. Pin against a regression
  that silently bypasses concurrent-save serialisation. (#5)
- ``test_dual_save_buttons_mirror_disabled_and_status_on_post_failure``
  — exercises the 500-response branch of the dual-save mirror so a
  regression that broke ``_setAdvSaveStatus``/``_setAdvSaveDisabled``
  for error paths only would still fail. (#6)
- ``test_save_features_master_on_restores_subflag_values_in_addon_mode``
  — addon-mode round-trip mirror of the existing standalone restore
  test; asserts the Supervisor merge-and-post call carries only the
  master flip-on and never zeroes out sub-flag values. (#7)
- ``test_save_button_nothing_to_save_when_no_dirty_and_no_restart`` +
  ``test_save_button_restart_pending_hint_when_dirty_empty_but_restart_showing``
  — both branches of the empty-dirty Save click are exercised; the
  restart-pending branch asserts the copy is source-blind. (#8)

Deferred per user direction:
- #1 (file-vs-Settings visual after F5): user accepts the current
  behavior (UI shows post-gate value; runtime tools actually
  disabled when master off; data-layer preserve still works
  end-to-end).
- #11/#12/#13 (code-simplifier helper extractions): skipped as
  complicated to implement without behavior risk.
- #15 (pre-#1164 users with already-cleared sub-flags): release-note
  concern, not a code change.
- #16 (lock fragility under future thread-pool dispatch):
  speculative future-risk; not actionable today.

* feat(settings): version footer + Patch76 review feedback

Adds the running ha-mcp version to the settings UI footer (issue
#1466). ``info.version`` flows from ``/api/settings/info`` →
``HA_MCP_BUILD_VERSION`` env var on addon builds (set by both
stable and dev Dockerfiles) → package metadata fallback. Empty on
older deployments without the field.

Addresses Patch76's review (no blockers, all bundleable):

- ``OverrideField`` folds the structurally-identical
  ``FeatureFlagField`` and ``BackupOverrideField`` into one
  NamedTuple; aliases preserve readable construction sites.
  ``AdvancedField`` keeps its own type since it c…
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jun 8, 2026
…➔ 7.6.0) (#904)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/homeassistant-ai/ha-mcp](https://github.com/homeassistant-ai/ha-mcp) | minor | `7.5.0` → `7.6.0` |

---

### Release Notes

<details>
<summary>homeassistant-ai/ha-mcp (ghcr.io/homeassistant-ai/ha-mcp)</summary>

### [`v7.6.0`](https://github.com/homeassistant-ai/ha-mcp/blob/HEAD/CHANGELOG.md#v760-2026-05-27)

[Compare Source](homeassistant-ai/ha-mcp@v7.5.0...v7.6.0)

##### Added

- Make HTTP bind host configurable via MCP\_HOST (closes [#&#8203;1434](homeassistant-ai/ha-mcp#1434))
  ([#&#8203;1436](homeassistant-ai/ha-mcp#1436))
- Tool Security Policies — per-tool approval gating ([#&#8203;966](homeassistant-ai/ha-mcp#966))
  ([#&#8203;1421](homeassistant-ai/ha-mcp#1421))
- Rename ha\_delete\_helpers\_integrations → ha\_remove\_helpers\_integrations + raise on missing target
  ([#&#8203;1424](homeassistant-ai/ha-mcp#1424))
- Auto-backup edited entities before write/destructive tool calls (closes [#&#8203;1288](homeassistant-ai/ha-mcp#1288))
  ([#&#8203;1403](homeassistant-ai/ha-mcp#1403))
- Persistent settings UI for stdio mode
  ([#&#8203;1381](homeassistant-ai/ha-mcp#1381))
- Add fields= projection to ha\_search\_entities, ha\_get\_overview, ha\_get\_state, ha\_get\_history, ha\_config\_list\_areas, ha\_list\_services ([#&#8203;1199](homeassistant-ai/ha-mcp#1199))
  ([#&#8203;1225](homeassistant-ai/ha-mcp#1225))
- Route entity-registration wait through WS events (closes [#&#8203;1152](homeassistant-ai/ha-mcp#1152))
  ([#&#8203;1382](homeassistant-ai/ha-mcp#1382))
- Add config subentry support
  ([#&#8203;1393](homeassistant-ai/ha-mcp#1393))
- Add Assist pipeline management tool
  ([#&#8203;1392](homeassistant-ai/ha-mcp#1392))
- Add knx to ha\_config\_set\_yaml allowlist
  ([#&#8203;1374](homeassistant-ai/ha-mcp#1374))
- Extend automation\_id parity to set/remove automation responses
  ([#&#8203;1343](homeassistant-ai/ha-mcp#1343))
- **haos-e2e**: Add parallel inaddon test tier (ha-mcp runs inside HAOS addon)
  ([#&#8203;1361](homeassistant-ai/ha-mcp#1361))
- Expose integration diagnostics via ha\_get\_integration and ha\_get\_system\_health (closes [#&#8203;1148](homeassistant-ai/ha-mcp#1148))
  ([#&#8203;1328](homeassistant-ai/ha-mcp#1328))
- Return canonical script\_id from ha\_config\_get\_script ([#&#8203;1334](homeassistant-ai/ha-mcp#1334))
  ([#&#8203;1352](homeassistant-ai/ha-mcp#1352))
- Add automation\_id parity key to ha\_config\_get\_automation
  ([#&#8203;1329](homeassistant-ai/ha-mcp#1329))
- Reject empty/whitespace identifiers on registry-metadata writes (closes [#&#8203;1294](homeassistant-ai/ha-mcp#1294))
  ([#&#8203;1312](homeassistant-ai/ha-mcp#1312))
- Add HA brand assets for custom integration
  ([#&#8203;1317](homeassistant-ai/ha-mcp#1317))
- Unify ha\_config\_set\_helper response shape (closes [#&#8203;1293](homeassistant-ai/ha-mcp#1293))
  ([#&#8203;1303](homeassistant-ai/ha-mcp#1303))
- Mirror create-side validation guards onto update path (closes [#&#8203;1292](homeassistant-ai/ha-mcp#1292))
  ([#&#8203;1304](homeassistant-ai/ha-mcp#1304))
- Add array\_patch mode to ha\_manage\_addon for atomic GET-modify-POST
  ([#&#8203;1063](homeassistant-ai/ha-mcp#1063))

##### Changed

- **agents**: Drop ha\_backup\_create + ha\_backup\_restore from accepted exceptions
  ([#&#8203;1445](homeassistant-ai/ha-mcp#1445))
- Update contributors list \[contributors-updated]
  ([`c7665a6`](homeassistant-ai/ha-mcp@c7665a6))
- **overview**: Enumerate dismissed\_repair\_count in fields= description + static drift test
  ([#&#8203;1411](homeassistant-ai/ha-mcp#1411))
- Credit [@&#8203;tomwilkie](https://github.com/tomwilkie) and six other contributors in README
  ([#&#8203;1400](homeassistant-ai/ha-mcp#1400))
- **[#&#8203;1157](homeassistant-ai/ha-mcp#1157: Bump skills-vendor + auto-update via Renovate + native for: field + scrub eval\_template anti-patterns
  ([#&#8203;1383](homeassistant-ai/ha-mcp#1383))
- Extend Boy Scout weasel-phrase list with common variants; clarify semantic match
  ([#&#8203;1373](homeassistant-ai/ha-mcp#1373))
- Merge Boy Scout Rule + Handling Discovered Improvements; tighten deferral gate
  ([#&#8203;1359](homeassistant-ai/ha-mcp#1359))
- Categorize Issue Labels table and document 6 reverse-drift labels
  ([#&#8203;1335](homeassistant-ai/ha-mcp#1335))
- Strip stale L-refs from test\_identifier\_validation\_family docstrings
  ([#&#8203;1324](homeassistant-ai/ha-mcp#1324))
- Align label refs with live label set and fix triaged-removal trigger
  ([#&#8203;1316](homeassistant-ai/ha-mcp#1316))
- Surface tool-discovery / categorized search
  ([#&#8203;1123](homeassistant-ai/ha-mcp#1123))
- Fix two stale ha\_get\_skill\_guide references missed in [#&#8203;1289](homeassistant-ai/ha-mcp#1289)
  ([#&#8203;1305](homeassistant-ai/ha-mcp#1305))
- Clarify setup wizard placeholders need braces removed ([#&#8203;1284](homeassistant-ai/ha-mcp#1284))
  ([#&#8203;1286](homeassistant-ai/ha-mcp#1286))

##### Fixed

- Remove counter from ha\_reload\_core targets ([#&#8203;1453](homeassistant-ai/ha-mcp#1453))
  ([#&#8203;1456](homeassistant-ai/ha-mcp#1456))
- **backup**: Post-timeout match correctness + state-gate (closes [#&#8203;1433](homeassistant-ai/ha-mcp#1433))
  ([#&#8203;1435](homeassistant-ai/ha-mcp#1435))
- Sync addon settings UI with Supervisor options end-to-end
  ([#&#8203;1420](homeassistant-ai/ha-mcp#1420))
- **calendar**: Switch ha\_config\_remove\_calendar\_event to WebSocket (closes [#&#8203;1413](homeassistant-ai/ha-mcp#1413), [#&#8203;1416](homeassistant-ai/ha-mcp#1416))
  ([#&#8203;1418](homeassistant-ai/ha-mcp#1418))
- Error-shape consistency for non-entity not-found (closes [#&#8203;1297](homeassistant-ai/ha-mcp#1297))
  ([#&#8203;1397](homeassistant-ai/ha-mcp#1397))
- Guard against silent automation overwrite on id mismatch ([#&#8203;1404](homeassistant-ai/ha-mcp#1404))
  ([#&#8203;1405](homeassistant-ai/ha-mcp#1405))
- Cache YAML instance to prevent CPU spikes in bulk edits ([#&#8203;1370](homeassistant-ai/ha-mcp#1370))
  ([#&#8203;1371](homeassistant-ai/ha-mcp#1371))
- **client**: Route get\_error\_log via hassio proxy on external-HAOS clients
  ([#&#8203;1360](homeassistant-ai/ha-mcp#1360))
- Classify dashboard 404s ("unknown config specified") as RESOURCE\_NOT\_FOUND
  ([#&#8203;1345](homeassistant-ai/ha-mcp#1345))
- Detect HA addon installs as http transport, not stdio ([#&#8203;1322](homeassistant-ai/ha-mcp#1322))
  ([#&#8203;1327](homeassistant-ai/ha-mcp#1327))
- Actionable 403 suggestion when addon has unmapped container ports ([#&#8203;1319](homeassistant-ai/ha-mcp#1319))
  ([#&#8203;1325](homeassistant-ai/ha-mcp#1325))
- Filter dismissed repairs in overview and system\_health ([#&#8203;1307](homeassistant-ai/ha-mcp#1307))
  ([#&#8203;1309](homeassistant-ai/ha-mcp#1309))
- Exit on HA container death + daily reset before CI check
  ([#&#8203;1295](homeassistant-ai/ha-mcp#1295))
- Align ha\_config\_set\_dashboard with sibling re-fetch-after-save pattern ([#&#8203;1291](homeassistant-ai/ha-mcp#1291))
  ([#&#8203;1301](homeassistant-ai/ha-mcp#1301))
- Allow str.replace in python\_transform; hint at search mode on IndexError
  ([#&#8203;1287](homeassistant-ai/ha-mcp#1287))
- **array\_patch**: Tighten validation and surface silent failures
  ([#&#8203;1285](homeassistant-ai/ha-mcp#1285))
- HA Core proxy fallback for ha\_get\_logs(source=system\_service) on non-addon installs
  ([#&#8203;1283](homeassistant-ai/ha-mcp#1283))

##### Performance Improvements

- Tighten \_poll\_for\_automation\_entity first-poll cadence
  ([#&#8203;1384](homeassistant-ai/ha-mcp#1384))
- Parallelize ha\_get\_system\_health optional sections via asyncio.gather
  ([#&#8203;1336](homeassistant-ai/ha-mcp#1336))

##### Refactoring

- **service**: Compact ha\_call\_service result default ([#&#8203;1446](homeassistant-ai/ha-mcp#1446))
  ([#&#8203;1447](homeassistant-ai/ha-mcp#1447))
- Rename ha\_update\_device → ha\_set\_device
  ([#&#8203;1444](homeassistant-ai/ha-mcp#1444))
- Remove duplicate flat area/floor list tools (consolidation followup to [#&#8203;1016](homeassistant-ai/ha-mcp#1016))
  ([#&#8203;1429](homeassistant-ai/ha-mcp#1429))
- **complexity**: Migrate tools\_utility.py to class-based pattern
  ([#&#8203;1423](homeassistant-ai/ha-mcp#1423))
- **complexity**: Reduce C901 violations in tools/ — batch 4
  ([#&#8203;1408](homeassistant-ai/ha-mcp#1408))
- Route \_poll\_for\_automation\_entity through WS event waiter (closes [#&#8203;1395](homeassistant-ai/ha-mcp#1395))
  ([#&#8203;1406](homeassistant-ai/ha-mcp#1406))
- **yaml**: Use threading.local subclass for cached instance
  ([#&#8203;1396](homeassistant-ai/ha-mcp#1396))
- Align dashboards 404 shape with sibling config tools
  ([#&#8203;1386](homeassistant-ai/ha-mcp#1386))
- Complete singular warning → warnings list migration repo-wide (closes [#&#8203;1332](homeassistant-ai/ha-mcp#1332))
  ([#&#8203;1341](homeassistant-ai/ha-mcp#1341))
- Complete warnings-list migration for lifecycle-write tools
  ([#&#8203;1340](homeassistant-ai/ha-mcp#1340))
- Drop redundant identifier echo key from ha\_config\_get\_automation
  ([#&#8203;1354](homeassistant-ai/ha-mcp#1354))
- Drop logger.error in config-tool except blocks ([#&#8203;1302](homeassistant-ai/ha-mcp#1302))
  ([#&#8203;1353](homeassistant-ai/ha-mcp#1353))
- Extend validate\_identifier\_not\_empty to automations/scripts/dashboards CRUD (closes [#&#8203;1313](homeassistant-ai/ha-mcp#1313))
  ([#&#8203;1321](homeassistant-ai/ha-mcp#1321))
- Migrate tools\_config\_scenes inline empty-id guards to shared helper
  ([#&#8203;1320](homeassistant-ai/ha-mcp#1320))
- Remove ha\_get\_helper\_schema (closes [#&#8203;1186](homeassistant-ai/ha-mcp#1186))
  ([#&#8203;1315](homeassistant-ai/ha-mcp#1315))
- Consolidate skill tools; fix stable submodule packaging
  ([#&#8203;1289](homeassistant-ai/ha-mcp#1289))
- Align tools\_config\_automations.py error-handling with sibling pattern ([#&#8203;1290](homeassistant-ai/ha-mcp#1290))
  ([#&#8203;1298](homeassistant-ai/ha-mcp#1298))

***

<details>
<summary>Internal Changes</summary>

##### Fixed

- **ci**: Install libguestfs in HAOS publish workflow
  ([#&#8203;1358](homeassistant-ai/ha-mcp#1358))

##### Build System

- **deps**: Bump esbuild from 0.24.2 to 0.25.0 in /tests/js
  ([#&#8203;1427](homeassistant-ai/ha-mcp#1427))
- **deps**: Bump devalue from 5.6.4 to 5.8.1 in /site
  ([#&#8203;1282](homeassistant-ai/ha-mcp#1282))
- **deps**: Bump astro from 6.1.6 to 6.1.10 in /site
  ([#&#8203;1274](homeassistant-ai/ha-mcp#1274))

##### Chores

- **addon**: Publish dev addon version 7.5.0.dev360 \[skip ci]
  ([`ad7aed1`](homeassistant-ai/ha-mcp@ad7aed1))
- Sync tool docs after merge \[skip ci]
  ([`9c4984f`](homeassistant-ai/ha-mcp@9c4984f))
- **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.4
  ([#&#8203;1450](homeassistant-ai/ha-mcp#1450))
- **addon**: Publish dev addon version 7.5.0.dev359 \[skip ci]
  ([`b82d4ee`](homeassistant-ai/ha-mcp@b82d4ee))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.16
  ([#&#8203;1449](homeassistant-ai/ha-mcp#1449))
- **addon**: Publish dev addon version 7.5.0.dev358 \[skip ci]
  ([`53fba6d`](homeassistant-ai/ha-mcp@53fba6d))
- Sync tool docs after merge \[skip ci]
  ([`dc7750d`](homeassistant-ai/ha-mcp@dc7750d))
- **addon**: Publish dev addon version 7.5.0.dev357 \[skip ci]
  ([`fd150c9`](homeassistant-ai/ha-mcp@fd150c9))
- **addon**: Publish dev addon version 7.5.0.dev356 \[skip ci]
  ([`5fa1463`](homeassistant-ai/ha-mcp@5fa1463))
- **addon**: Publish dev addon version 7.5.0.dev355 \[skip ci]
  ([`174ac5d`](homeassistant-ai/ha-mcp@174ac5d))
- **addon**: Publish dev addon version 7.5.0.dev354 \[skip ci]
  ([`4f93989`](homeassistant-ai/ha-mcp@4f93989))
- **addon**: Publish dev addon version 7.5.0.dev353 \[skip ci]
  ([`53f282f`](homeassistant-ai/ha-mcp@53f282f))
- **addon**: Publish dev addon version 7.5.0.dev352 \[skip ci]
  ([`4911d46`](homeassistant-ai/ha-mcp@4911d46))
- Sync tool docs after merge \[skip ci]
  ([`8a79837`](homeassistant-ai/ha-mcp@8a79837))
- **addon**: Publish dev addon version 7.5.0.dev351 \[skip ci]
  ([`13afa9d`](homeassistant-ai/ha-mcp@13afa9d))
- **addon**: Publish dev addon version 7.5.0.dev350 \[skip ci]
  ([`e93d680`](homeassistant-ai/ha-mcp@e93d680))
- **addon**: Publish dev addon version 7.5.0.dev349 \[skip ci]
  ([`1631ad1`](homeassistant-ai/ha-mcp@1631ad1))
- **addon**: Publish dev addon version 7.5.0.dev348 \[skip ci]
  ([`18a8aef`](homeassistant-ai/ha-mcp@18a8aef))
- Sync tool docs after merge \[skip ci]
  ([`9e0493d`](homeassistant-ai/ha-mcp@9e0493d))
- **addon**: Publish dev addon version 7.5.0.dev347 \[skip ci]
  ([`e2067e4`](homeassistant-ai/ha-mcp@e2067e4))
- Sync tool docs after merge \[skip ci]
  ([`7bdb3d4`](homeassistant-ai/ha-mcp@7bdb3d4))
- **addon**: Publish dev addon version 7.5.0.dev346 \[skip ci]
  ([`c2d4dd7`](homeassistant-ai/ha-mcp@c2d4dd7))
- Sync tool docs after merge \[skip ci]
  ([`393b354`](homeassistant-ai/ha-mcp@393b354))
- **addon**: Publish dev addon version 7.5.0.dev345 \[skip ci]
  ([`42ede8b`](homeassistant-ai/ha-mcp@42ede8b))
- **addon**: Publish dev addon version 7.5.0.dev344 \[skip ci]
  ([`e6cc7a1`](homeassistant-ai/ha-mcp@e6cc7a1))
- Sync tool docs after merge \[skip ci]
  ([`fb35f30`](homeassistant-ai/ha-mcp@fb35f30))
- **addon**: Publish dev addon version 7.5.0.dev343 \[skip ci]
  ([`401b7b4`](homeassistant-ai/ha-mcp@401b7b4))
- **addon**: Publish dev addon version 7.5.0.dev342 \[skip ci]
  ([`0679371`](homeassistant-ai/ha-mcp@0679371))
- Sync tool docs after merge \[skip ci]
  ([`f6796ec`](homeassistant-ai/ha-mcp@f6796ec))
- **addon**: Publish dev addon version 7.5.0.dev341 \[skip ci]
  ([`3654478`](homeassistant-ai/ha-mcp@3654478))
- **addon**: Publish dev addon version 7.5.0.dev340 \[skip ci]
  ([`64f00b6`](homeassistant-ai/ha-mcp@64f00b6))
- **addon**: Publish dev addon version 7.5.0.dev339 \[skip ci]
  ([`d6e8873`](homeassistant-ai/ha-mcp@d6e8873))
- Sync tool docs after merge \[skip ci]
  ([`7525e93`](homeassistant-ai/ha-mcp@7525e93))
- **addon**: Publish dev addon version 7.5.0.dev338 \[skip ci]
  ([`288ca4a`](homeassistant-ai/ha-mcp@288ca4a))
- **addon**: Publish dev addon version 7.5.0.dev337 \[skip ci]
  ([`f539ae5`](homeassistant-ai/ha-mcp@f539ae5))
- **addon**: Publish dev addon version 7.5.0.dev336 \[skip ci]
  ([`5568a86`](homeassistant-ai/ha-mcp@5568a86))
- **addon**: Publish dev addon version 7.5.0.dev335 \[skip ci]
  ([`e069405`](homeassistant-ai/ha-mcp@e069405))
- **addon**: Publish dev addon version 7.5.0.dev334 \[skip ci]
  ([`f7be6ea`](homeassistant-ai/ha-mcp@f7be6ea))
- **addon**: Publish dev addon version 7.5.0.dev333 \[skip ci]
  ([`cb480ea`](homeassistant-ai/ha-mcp@cb480ea))
- Sync tool docs after merge \[skip ci]
  ([`9a5bc3c`](homeassistant-ai/ha-mcp@9a5bc3c))
- **addon**: Publish dev addon version 7.5.0.dev332 \[skip ci]
  ([`e0e59ee`](homeassistant-ai/ha-mcp@e0e59ee))
- Sync tool docs after merge \[skip ci]
  ([`499ebf0`](homeassistant-ai/ha-mcp@499ebf0))
- **addon**: Publish dev addon version 7.5.0.dev331 \[skip ci]
  ([`3e0ce92`](homeassistant-ai/ha-mcp@3e0ce92))
- **addon**: Publish dev addon version 7.5.0.dev330 \[skip ci]
  ([`e48d056`](homeassistant-ai/ha-mcp@e48d056))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.15
  ([#&#8203;1376](homeassistant-ai/ha-mcp#1376))
- **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.3
  ([#&#8203;1377](homeassistant-ai/ha-mcp#1377))
- **addon**: Publish dev addon version 7.5.0.dev329 \[skip ci]
  ([`c523c50`](homeassistant-ai/ha-mcp@c523c50))
- Sync tool docs after merge \[skip ci]
  ([`5b7a8aa`](homeassistant-ai/ha-mcp@5b7a8aa))
- **addon**: Publish dev addon version 7.5.0.dev328 \[skip ci]
  ([`6c42fba`](homeassistant-ai/ha-mcp@6c42fba))
- **addon**: Publish dev addon version 7.5.0.dev327 \[skip ci]
  ([`aecd025`](homeassistant-ai/ha-mcp@aecd025))
- **addon**: Publish dev addon version 7.5.0.dev326 \[skip ci]
  ([`399c17c`](homeassistant-ai/ha-mcp@399c17c))
- **addon**: Publish dev addon version 7.5.0.dev325 \[skip ci]
  ([`b580b45`](homeassistant-ai/ha-mcp@b580b45))
- Sync tool docs after merge \[skip ci]
  ([`8567c3a`](homeassistant-ai/ha-mcp@8567c3a))
- **addon**: Publish dev addon version 7.5.0.dev324 \[skip ci]
  ([`a65579d`](homeassistant-ai/ha-mcp@a65579d))
- **addon**: Publish dev addon version 7.5.0.dev323 \[skip ci]
  ([`c7667ba`](homeassistant-ai/ha-mcp@c7667ba))
- **addon**: Publish dev addon version 7.5.0.dev322 \[skip ci]
  ([`44d15a8`](homeassistant-ai/ha-mcp@44d15a8))
- **addon**: Publish dev addon version 7.5.0.dev321 \[skip ci]
  ([`8739f6c`](homeassistant-ai/ha-mcp@8739f6c))
- **addon**: Publish dev addon version 7.5.0.dev320 \[skip ci]
  ([`02b6e47`](homeassistant-ai/ha-mcp@02b6e47))
- Sync tool docs after merge \[skip ci]
  ([`ab68c9a`](homeassistant-ai/ha-mcp@ab68c9a))
- **addon**: Publish dev addon version 7.5.0.dev319 \[skip ci]
  ([`4472904`](homeassistant-ai/ha-mcp@4472904))
- **addon**: Publish dev addon version 7.5.0.dev318 \[skip ci]
  ([`e030dbc`](homeassistant-ai/ha-mcp@e030dbc))
- **addon**: Publish dev addon version 7.5.0.dev317 \[skip ci]
  ([`d87855c`](homeassistant-ai/ha-mcp@d87855c))
- Sync tool docs after merge \[skip ci]
  ([`a72a4e8`](homeassistant-ai/ha-mcp@a72a4e8))
- **addon**: Publish dev addon version 7.5.0.dev316 \[skip ci]
  ([`bd9397f`](homeassistant-ai/ha-mcp@bd9397f))
- **addon**: Publish dev addon version 7.5.0.dev315 \[skip ci]
  ([`264bfc2`](homeassistant-ai/ha-mcp@264bfc2))
- **addon**: Publish dev addon version 7.5.0.dev314 \[skip ci]
  ([`df62881`](homeassistant-ai/ha-mcp@df62881))
- **addon**: Publish dev addon version 7.5.0.dev313 \[skip ci]
  ([`f6c47ca`](homeassistant-ai/ha-mcp@f6c47ca))
- **addon**: Publish dev addon version 7.5.0.dev312 \[skip ci]
  ([`2bb7a74`](homeassistant-ai/ha-mcp@2bb7a74))
- Sync tool docs after merge \[skip ci]
  ([`137e279`](homeassistant-ai/ha-mcp@137e279))
- **addon**: Publish dev addon version 7.5.0.dev311 \[skip ci]
  ([`28324ea`](homeassistant-ai/ha-mcp@28324ea))
- Sync tool docs after merge \[skip ci]
  ([`9a753d4`](homeassistant-ai/ha-mcp@9a753d4))
- **addon**: Publish dev addon version 7.5.0.dev310 \[skip ci]
  ([`f893b2e`](homeassistant-ai/ha-mcp@f893b2e))
- **addon**: Publish dev addon version 7.5.0.dev309 \[skip ci]
  ([`8cbdb7b`](homeassistant-ai/ha-mcp@8cbdb7b))
- **addon**: Publish dev addon version 7.5.0.dev308 \[skip ci]
  ([`2d18016`](homeassistant-ai/ha-mcp@2d18016))
- **addon**: Publish dev addon version 7.5.0.dev307 \[skip ci]
  ([`3fc3b28`](homeassistant-ai/ha-mcp@3fc3b28))
- Sync tool docs after merge \[skip ci]
  ([`9e6cff8`](homeassistant-ai/ha-mcp@9e6cff8))
- **addon**: Publish dev addon version 7.5.0.dev306 \[skip ci]
  ([`8bdd0fc`](homeassistant-ai/ha-mcp@8bdd0fc))
- **addon**: Publish dev addon version 7.5.0.dev305 \[skip ci]
  ([`83535b9`](homeassistant-ai/ha-mcp@83535b9))
- **addon**: Publish dev addon version 7.5.0.dev304 \[skip ci]
  ([`1435b3a`](homeassistant-ai/ha-mcp@1435b3a))
- **addon**: Publish dev addon version 7.5.0.dev303 \[skip ci]
  ([`e2da659`](homeassistant-ai/ha-mcp@e2da659))
- Sync tool docs after merge \[skip ci]
  ([`23789fa`](homeassistant-ai/ha-mcp@23789fa))
- **addon**: Publish dev addon version 7.5.0.dev302 \[skip ci]
  ([`6c8e574`](homeassistant-ai/ha-mcp@6c8e574))
- Sync tool docs after merge \[skip ci]
  ([`d2329cb`](homeassistant-ai/ha-mcp@d2329cb))
- **addon**: Publish dev addon version 7.5.0.dev301 \[skip ci]
  ([`bb538f7`](homeassistant-ai/ha-mcp@bb538f7))
- Sync tool docs after merge \[skip ci]
  ([`f70f0e1`](homeassistant-ai/ha-mcp@f70f0e1))
- **addon**: Publish version 7.5.0 \[skip ci]
  ([`9c5eb37`](homeassistant-ai/ha-mcp@9c5eb37))

##### Continuous Integration

- **deps**: Bump actions/upload-artifact in the github-actions group
  ([#&#8203;1437](homeassistant-ai/ha-mcp#1437))
- Share qcow2 cache + GHCR fallback between HAOS lanes
  ([#&#8203;1407](homeassistant-ai/ha-mcp#1407))
- Add ruff format --check on changed Python files
  ([#&#8203;1387](homeassistant-ai/ha-mcp#1387))
- Exempt assigned issues from stale bot
  ([#&#8203;1368](homeassistant-ai/ha-mcp#1368))
- **deps**: Bump the github-actions group with 3 updates
  ([#&#8203;1362](homeassistant-ai/ha-mcp#1362))

##### Refactoring

- Consolidate lovelace/dashboards/list through shared helper
  ([#&#8203;1344](homeassistant-ai/ha-mcp#1344))

##### Testing

- **haos-e2e**: Bake + install webhook-proxy addon and exercise its runtime
  ([#&#8203;1443](homeassistant-ai/ha-mcp#1443))
- **config-subentry**: Mark forecast\_solar e2e as known flaky + relative-import sweep
  ([#&#8203;1430](homeassistant-ai/ha-mcp#1430))
- **haos-e2e**: Trim cache-save race, compress GHCR qcow2, eval boot snapshot
  ([#&#8203;1428](homeassistant-ai/ha-mcp#1428))
- JSDOM behaviour harness + auto-discovery parse coverage for every rendered <script>
  ([#&#8203;1425](homeassistant-ai/ha-mcp#1425))
- **hacs**: Retry TestMcpToolsInstallation on flake
  ([#&#8203;1426](homeassistant-ai/ha-mcp#1426))
- **e2e**: Drop redundant lifecycle roundtrips, keep only Matter Server ([#&#8203;1414](homeassistant-ai/ha-mcp#1414))
  ([#&#8203;1419](homeassistant-ai/ha-mcp#1419))
- **e2e**: Assert backend dispatch matches workflow env on every lane
  ([#&#8203;1409](homeassistant-ai/ha-mcp#1409))
- Escape ideographic space and format file ([#&#8203;1237](homeassistant-ai/ha-mcp#1237))
  ([#&#8203;1410](homeassistant-ai/ha-mcp#1410))
- **e2e**: Measure \_POLL\_CADENCE p50/p99 to validate or retune (closes [#&#8203;1389](homeassistant-ai/ha-mcp#1389))
  ([#&#8203;1398](homeassistant-ai/ha-mcp#1398))
- **e2e**: Wait for addon state=started in haos proxy header test
  ([#&#8203;1402](homeassistant-ai/ha-mcp#1402))
- Pin remaining \_classify\_by\_message branches
  ([#&#8203;1385](homeassistant-ai/ha-mcp#1385))
- **haos-e2e**: Slim addon set + real-addon ha\_manage\_addon coverage (closes [#&#8203;1350](homeassistant-ai/ha-mcp#1350))
  ([#&#8203;1379](homeassistant-ai/ha-mcp#1379))
- **haos-e2e**: Close out [#&#8203;1349](homeassistant-ai/ha-mcp#1349) — lifecycle, integrations, supervisor\_mock migration, no more skips
  ([#&#8203;1375](homeassistant-ai/ha-mcp#1375))
- **e2e**: Consolidate readiness gates onto /api/core/state (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1372](homeassistant-ai/ha-mcp#1372))
- **e2e**: Tighten 5 readiness-gate budgets with 2-63x headroom (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1369](homeassistant-ai/ha-mcp#1369))
- Scaffold HAOS E2E tier image-build pipeline (refs [#&#8203;1281](homeassistant-ai/ha-mcp#1281))
  ([#&#8203;1326](homeassistant-ai/ha-mcp#1326))
- **e2e**: Instrument HA\_MCP\_TOOLS\_WAIT readiness gate (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1346](homeassistant-ai/ha-mcp#1346))
- **e2e**: Centralize wait\_for\_entity\_registration helper (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1308](homeassistant-ai/ha-mcp#1308))
- **e2e**: Unify dict-error message extraction across e2e tests (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1311](homeassistant-ai/ha-mcp#1311))
- **e2e**: Surface readiness-gate elapsed times in CI logs (refs [#&#8203;366](homeassistant-ai/ha-mcp#366))
  ([#&#8203;1310](homeassistant-ai/ha-mcp#1310))

</details>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/904
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jun 10, 2026
…➔ 7.7.0) (#994)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/homeassistant-ai/ha-mcp](https://github.com/homeassistant-ai/ha-mcp) | minor | `7.5.0` → `7.7.0` |

---

### Release Notes

<details>
<summary>homeassistant-ai/ha-mcp (ghcr.io/homeassistant-ai/ha-mcp)</summary>

### [`v7.7.0`](https://github.com/homeassistant-ai/ha-mcp/blob/HEAD/CHANGELOG.md#v770-2026-06-10)

[Compare Source](https://github.com/homeassistant-ai/ha-mcp/compare/v7.6.0...v7.7.0)

##### Added

- User-configurable custom filesystem directories for the file tools (closes [#&#8203;1567](https://github.com/homeassistant-ai/ha-mcp/issues/1567))
  ([#&#8203;1568](https://github.com/homeassistant-ai/ha-mcp/pull/1568))
- Expose all user-tunable env vars in the settings UI (add-on parity)
  ([#&#8203;1554](https://github.com/homeassistant-ai/ha-mcp/pull/1554))
- Warn when default MCP\_SECRET\_PATH is bound non-loopback
  ([#&#8203;1472](https://github.com/homeassistant-ai/ha-mcp/pull/1472))
- **search**: Consolidate ha\_search\_entities + ha\_deep\_search into ha\_search
  ([#&#8203;1529](https://github.com/homeassistant-ai/ha-mcp/pull/1529))
- **addon**: Install the MCP Server add-on from the ha\_mcp\_tools integration
  ([#&#8203;1528](https://github.com/homeassistant-ai/ha-mcp/pull/1528))
- Add opt-in dashboard screenshot mode
  ([#&#8203;1510](https://github.com/homeassistant-ai/ha-mcp/pull/1510))
- Add Linux support for install & docs
  ([#&#8203;1096](https://github.com/homeassistant-ai/ha-mcp/pull/1096))
- Surface the web settings page for non-add-on installs ([#&#8203;1458](https://github.com/homeassistant-ai/ha-mcp/issues/1458))
  ([#&#8203;1511](https://github.com/homeassistant-ai/ha-mcp/pull/1511))
- Convert Pydantic arg-validation errors to actionable ToolErrors
  ([#&#8203;1491](https://github.com/homeassistant-ai/ha-mcp/pull/1491))
- Per-key toggles for automation/script/scene in packages/\*.yaml
  ([#&#8203;1476](https://github.com/homeassistant-ai/ha-mcp/pull/1476))
- Direct skills retrieval for write tools + improved  best practice checker warnings with embedded skills responses ([#&#8203;1182](https://github.com/homeassistant-ai/ha-mcp/issues/1182))
  ([#&#8203;1448](https://github.com/homeassistant-ai/ha-mcp/pull/1448))
- Detect last\_changed/last\_updated duration math and suggest for: field ([#&#8203;1157](https://github.com/homeassistant-ai/ha-mcp/issues/1157))
  ([#&#8203;1264](https://github.com/homeassistant-ai/ha-mcp/pull/1264))
- Advanced settings panel + nested beta master toggle ([#&#8203;1164](https://github.com/homeassistant-ai/ha-mcp/issues/1164))
  ([#&#8203;1431](https://github.com/homeassistant-ai/ha-mcp/pull/1431))
- Allow automation/script/scene yaml\_path in packages/\*.yaml only
  ([#&#8203;1452](https://github.com/homeassistant-ai/ha-mcp/pull/1452))
- Restrict ha\_mcp\_tools services to ha-mcp callers (caller token + ha\_call\_service refusal)
  ([#&#8203;1459](https://github.com/homeassistant-ai/ha-mcp/pull/1459))

##### Changed

- **security**: Soften vulnerability-response SLA to best-effort
  ([`a24e7da`](https://github.com/homeassistant-ai/ha-mcp/commit/a24e7da4787aad570ac2998a5732de06229fa160))
- Update contributors list \[contributors-updated]
  ([`7cc187f`](https://github.com/homeassistant-ai/ha-mcp/commit/7cc187f4de2f6eb58cafaa4fb21f8016cb180a61))
- Telemetry wording — follow HA analytics setting, not opt-in-only
  ([#&#8203;1481](https://github.com/homeassistant-ai/ha-mcp/pull/1481))
- **security**: Note that security-advisory disposition is API-blind (UI-only)
  ([#&#8203;1561](https://github.com/homeassistant-ai/ha-mcp/pull/1561))
- **tools**: Sharpen ha\_eval\_template usage routing for compute-from-state queries
  ([#&#8203;1550](https://github.com/homeassistant-ai/ha-mcp/pull/1550))
- Differentiate ha-mcp from Home Assistant's built-in MCP Server
  ([#&#8203;1542](https://github.com/homeassistant-ai/ha-mcp/pull/1542))
- Update advanced mode notes
  ([#&#8203;1533](https://github.com/homeassistant-ai/ha-mcp/pull/1533))
- **[#&#8203;1157](https://github.com/homeassistant-ai/ha-mcp/issues/1157)**: Scripts native-for: guidance + fix numeric\_state-condition for: overclaim
  ([#&#8203;1480](https://github.com/homeassistant-ai/ha-mcp/pull/1480))
- Clarify add-on vs uvx; add Codex + HTTP-native client setup
  ([#&#8203;1478](https://github.com/homeassistant-ai/ha-mcp/pull/1478))
- **security**: Explicit threat model for trusted clients, LAN, sandbox, and OAuth tokens
  ([#&#8203;1463](https://github.com/homeassistant-ai/ha-mcp/pull/1463))
- Clarify telemetry is a planned future feature, not implemented
  ([#&#8203;1469](https://github.com/homeassistant-ai/ha-mcp/pull/1469))

##### Fixed

- **tools**: Bring HA API field names to 2026.6 — stale docstrings, automation plural canonicalization, fan speed (closes [#&#8203;1540](https://github.com/homeassistant-ai/ha-mcp/issues/1540))
  ([#&#8203;1566](https://github.com/homeassistant-ai/ha-mcp/pull/1566))
- **energy**: Accept "water" energy source in ha\_manage\_energy\_prefs
  ([#&#8203;1553](https://github.com/homeassistant-ai/ha-mcp/pull/1553))
- **tools**: Use action: not service: in automation docstring examples
  ([#&#8203;1539](https://github.com/homeassistant-ai/ha-mcp/pull/1539))
- **security**: Narrow GHSA-mc92-ww4q-6fg4 to the masker and log redaction
  ([#&#8203;1512](https://github.com/homeassistant-ai/ha-mcp/pull/1512))
- **addon**: Restrict settings UI root routes to HA ingress
  ([#&#8203;1508](https://github.com/homeassistant-ai/ha-mcp/pull/1508))
- Add name attributes to generated settings-UI form controls (a11y)
  ([#&#8203;1497](https://github.com/homeassistant-ai/ha-mcp/pull/1497))
- Add-on-aware code-mode locked note + suppress settings-UI favicon 404
  ([#&#8203;1494](https://github.com/homeassistant-ai/ha-mcp/pull/1494))
- Surface the real reason a WebSocket connection failed
  ([#&#8203;1495](https://github.com/homeassistant-ai/ha-mcp/pull/1495))
- Remove str from ha\_bulk\_control.operations schema + fix wrong-reason test
  ([#&#8203;1492](https://github.com/homeassistant-ai/ha-mcp/pull/1492))
- Remove str from bool/int param schemas across all tools
  ([#&#8203;1490](https://github.com/homeassistant-ai/ha-mcp/pull/1490))
- **addon**: Expose non-beta tool options on the stable add-on
  ([#&#8203;1488](https://github.com/homeassistant-ai/ha-mcp/pull/1488))
- **addon**: Enable ingress so the stable add-on shows the Open Web UI / Settings UI
  ([#&#8203;1486](https://github.com/homeassistant-ai/ha-mcp/pull/1486))
- Refine last\_changed/last\_updated duration-math detector ([#&#8203;1157](https://github.com/homeassistant-ai/ha-mcp/issues/1157))
  ([#&#8203;1483](https://github.com/homeassistant-ai/ha-mcp/pull/1483))
- Remove str from config param schema on service and entity tools
  ([#&#8203;1487](https://github.com/homeassistant-ai/ha-mcp/pull/1487))
- Surface flow-helper config to agents reading UI-created templates
  ([#&#8203;1474](https://github.com/homeassistant-ai/ha-mcp/pull/1474))
- Remove str from config param schema on set tools
  ([#&#8203;1485](https://github.com/homeassistant-ai/ha-mcp/pull/1485))
- Route malformed ha\_mcp\_tools version to a distinct reinstall error
  ([#&#8203;1484](https://github.com/homeassistant-ai/ha-mcp/pull/1484))
- Persist DCR client registrations and HMAC secret across restarts ([#&#8203;1261](https://github.com/homeassistant-ai/ha-mcp/issues/1261))
  ([#&#8203;1265](https://github.com/homeassistant-ai/ha-mcp/pull/1265))
- Stop dev builds from publishing the :latest Docker tag
  ([#&#8203;1477](https://github.com/homeassistant-ai/ha-mcp/pull/1477))
- Subscribe to HACS dispatch signal instead of 10x1s blind poll
  ([#&#8203;1455](https://github.com/homeassistant-ai/ha-mcp/pull/1455))
- Reject python\_transform while loops
  ([#&#8203;1462](https://github.com/homeassistant-ai/ha-mcp/pull/1462))

##### Performance Improvements

- Fast-fail HACS not-found lookups + batch-verify deep-search E2E fixtures ([#&#8203;1515](https://github.com/homeassistant-ai/ha-mcp/issues/1515))
  ([#&#8203;1552](https://github.com/homeassistant-ai/ha-mcp/pull/1552))

##### Refactoring

- Add display title to ha\_get\_skill\_guide tool
  ([#&#8203;1543](https://github.com/homeassistant-ai/ha-mcp/pull/1543))
- Fold ha\_check\_config into ha\_get\_system\_health include="config\_check"
  ([#&#8203;1516](https://github.com/homeassistant-ai/ha-mcp/pull/1516))
- **c901**: Smart\_search.py below C901 threshold
  ([#&#8203;1507](https://github.com/homeassistant-ai/ha-mcp/pull/1507))
- **hacs**: Consolidate HACS tools into ha\_get\_hacs + ha\_manage\_hacs ([#&#8203;1045](https://github.com/homeassistant-ai/ha-mcp/issues/1045))
  ([#&#8203;1502](https://github.com/homeassistant-ai/ha-mcp/pull/1502))
- **c901**: Tools\_config\_helpers.py below C901 threshold
  ([#&#8203;1498](https://github.com/homeassistant-ai/ha-mcp/pull/1498))
- **complexity**: Reduce C901 in tools\_addons.py via class-based pattern
  ([#&#8203;1432](https://github.com/homeassistant-ai/ha-mcp/pull/1432))

***

<details>
<summary>Internal Changes</summary>

##### Changed

- Trim AGENTS.md below 40k + improve subdirectory CLAUDE.md files
  ([#&#8203;1499](https://github.com/homeassistant-ai/ha-mcp/pull/1499))

##### Chores

- **addon**: Publish dev addon version 7.6.0.dev415 \[skip ci]
  ([`2c0148b`](https://github.com/homeassistant-ai/ha-mcp/commit/2c0148ba512a02b5299edbb4086c38410777a295))
- **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.6.1
  ([#&#8203;1571](https://github.com/homeassistant-ai/ha-mcp/pull/1571))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.19
  ([#&#8203;1570](https://github.com/homeassistant-ai/ha-mcp/pull/1570))
- **addon**: Publish dev addon version 7.6.0.dev414 \[skip ci]
  ([`0bf9e39`](https://github.com/homeassistant-ai/ha-mcp/commit/0bf9e39947e8bf87d18bae23877d6b0d955dd35f))
- Sync tool docs after merge \[skip ci]
  ([`8f5e037`](https://github.com/homeassistant-ai/ha-mcp/commit/8f5e037b89296fd483888d158d8f72f4b1b7dfa8))
- **addon**: Publish dev addon version 7.6.0.dev413 \[skip ci]
  ([`6729327`](https://github.com/homeassistant-ai/ha-mcp/commit/6729327de8c0747f5cc7bb639859f95bbe1f0cfc))
- Sync tool docs after merge \[skip ci]
  ([`4341e95`](https://github.com/homeassistant-ai/ha-mcp/commit/4341e9574b3270c67bc016676f854f585fec816f))
- **addon**: Publish dev addon version 7.6.0.dev412 \[skip ci]
  ([`148f506`](https://github.com/homeassistant-ai/ha-mcp/commit/148f506a98c8f89292616a3a05dd0c9156516439))
- **addon**: Publish dev addon version 7.6.0.dev411 \[skip ci]
  ([`287fe55`](https://github.com/homeassistant-ai/ha-mcp/commit/287fe5503917e551f2dcb363cd28250237ed880c))
- Sync tool docs after merge \[skip ci]
  ([`b642b8e`](https://github.com/homeassistant-ai/ha-mcp/commit/b642b8eba67d8015d8fe5a1c1f88c9dd72ffc45f))
- **addon**: Publish dev addon version 7.6.0.dev410 \[skip ci]
  ([`29e34dd`](https://github.com/homeassistant-ai/ha-mcp/commit/29e34dd6eb517399de3c0a6a886e45ecf4749923))
- **addon**: Publish dev addon version 7.6.0.dev409 \[skip ci]
  ([`dec16a1`](https://github.com/homeassistant-ai/ha-mcp/commit/dec16a12d2ac3341908b130e9ea79f950d02466b))
- Sync tool docs after merge \[skip ci]
  ([`dd72535`](https://github.com/homeassistant-ai/ha-mcp/commit/dd725357f5d48a00959aaa7eceb4bad67a7ba202))
- **addon**: Publish dev addon version 7.6.0.dev408 \[skip ci]
  ([`c3c89f1`](https://github.com/homeassistant-ai/ha-mcp/commit/c3c89f1a758b6086b59ff21f9044597da103fa25))
- **addon**: Publish dev addon version 7.6.0.dev407 \[skip ci]
  ([`94195ee`](https://github.com/homeassistant-ai/ha-mcp/commit/94195ee4c8dbd0d8eec13d7c3d7ee392e5b7240f))
- **addon**: Publish dev addon version 7.6.0.dev406 \[skip ci]
  ([`f439424`](https://github.com/homeassistant-ai/ha-mcp/commit/f4394240dd1170f9d978e3c4a5011bab12f17f54))
- Sync tool docs after merge \[skip ci]
  ([`7464277`](https://github.com/homeassistant-ai/ha-mcp/commit/746427745d3debccea57c844278d1ba834d7ebfb))
- **addon**: Publish dev addon version 7.6.0.dev405 \[skip ci]
  ([`146eb07`](https://github.com/homeassistant-ai/ha-mcp/commit/146eb073a6f260ed0b0653cdb71346e9d29deae7))
- **addon**: Publish dev addon version 7.6.0.dev404 \[skip ci]
  ([`6a707bb`](https://github.com/homeassistant-ai/ha-mcp/commit/6a707bba576b987f8c3395adca3d59f7dd8a1050))
- Sync tool docs after merge \[skip ci]
  ([`85f3935`](https://github.com/homeassistant-ai/ha-mcp/commit/85f3935f80d9677f791b3098b201ec9c27e643be))
- **addon**: Publish dev addon version 7.6.0.dev403 \[skip ci]
  ([`be1fa1d`](https://github.com/homeassistant-ai/ha-mcp/commit/be1fa1dd2693170cbe3c037e7a88cf02f1847dcf))
- Sync tool docs after merge \[skip ci]
  ([`d9adbd2`](https://github.com/homeassistant-ai/ha-mcp/commit/d9adbd2c27750c9bc8faa55470d24564c48b3dbf))
- **addon**: Publish dev addon version 7.6.0.dev402 \[skip ci]
  ([`41ad7ca`](https://github.com/homeassistant-ai/ha-mcp/commit/41ad7caf1e0d982dd9e7bda3420ba3ca577cf765))
- **addon**: Publish dev addon version 7.6.0.dev401 \[skip ci]
  ([`8205034`](https://github.com/homeassistant-ai/ha-mcp/commit/820503485386f13dc66a0da3d304739e1f03dafd))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.18
  ([#&#8203;1523](https://github.com/homeassistant-ai/ha-mcp/pull/1523))
- **addon**: Publish dev addon version 7.6.0.dev400 \[skip ci]
  ([`532cdc4`](https://github.com/homeassistant-ai/ha-mcp/commit/532cdc440a8db053c524579f3e6b48b099c13c66))
- Sync tool docs after merge \[skip ci]
  ([`e2c08ff`](https://github.com/homeassistant-ai/ha-mcp/commit/e2c08ff797ffabf7b39bfc4b22b498b1c02ce1fa))
- **addon**: Publish dev addon version 7.6.0.dev399 \[skip ci]
  ([`9501348`](https://github.com/homeassistant-ai/ha-mcp/commit/95013488ea99761535ea9532285839bcd0ec8e3b))
- Sync tool docs after merge \[skip ci]
  ([`e70a050`](https://github.com/homeassistant-ai/ha-mcp/commit/e70a050efcb6edeb65d51dabd5307a192a22e66c))
- **addon**: Publish dev addon version 7.6.0.dev398 \[skip ci]
  ([`0b61424`](https://github.com/homeassistant-ai/ha-mcp/commit/0b61424ed823453dec27bb173d049f901eddc2d3))
- Sync tool docs after merge \[skip ci]
  ([`6df7987`](https://github.com/homeassistant-ai/ha-mcp/commit/6df79877669686a9fee3ec04979dff3960bc5e3f))
- **addon**: Publish dev addon version 7.6.0.dev397 \[skip ci]
  ([`c34915f`](https://github.com/homeassistant-ai/ha-mcp/commit/c34915f40cd82f83f0eb4af5c3a5655843608770))
- Sync tool docs after merge \[skip ci]
  ([`6f88e7e`](https://github.com/homeassistant-ai/ha-mcp/commit/6f88e7e0c8dcb7fee27792e0f87f6acd8aa892c4))
- **addon**: Publish dev addon version 7.6.0.dev396 \[skip ci]
  ([`1490e19`](https://github.com/homeassistant-ai/ha-mcp/commit/1490e190382454b17fc14cd362b17612a6af253f))
- Sync tool docs after merge \[skip ci]
  ([`ac2c7d0`](https://github.com/homeassistant-ai/ha-mcp/commit/ac2c7d038a90fb44466afa8f047378d7e8daed94))
- **addon**: Publish dev addon version 7.6.0.dev395 \[skip ci]
  ([`11e5ee8`](https://github.com/homeassistant-ai/ha-mcp/commit/11e5ee8f07f63f63b8cdf017397263c7602e80f6))
- **addon**: Publish dev addon version 7.6.0.dev394 \[skip ci]
  ([`5fc3e0f`](https://github.com/homeassistant-ai/ha-mcp/commit/5fc3e0f572bfd1bb367e65b3f4d78b96cb78fa91))
- Flag untrusted third-party content in HACS and add-on tool responses
  ([#&#8203;1509](https://github.com/homeassistant-ai/ha-mcp/pull/1509))
- **addon**: Publish dev addon version 7.6.0.dev393 \[skip ci]
  ([`9a37c28`](https://github.com/homeassistant-ai/ha-mcp/commit/9a37c28cfa0d1f3f652fe441ce2fe1d72a7360ab))
- **addon**: Publish dev addon version 7.6.0.dev392 \[skip ci]
  ([`4f737ac`](https://github.com/homeassistant-ai/ha-mcp/commit/4f737ac7c116e9afa046ab45b1230e2cdb5ae959))
- **addon**: Publish dev addon version 7.6.0.dev391 \[skip ci]
  ([`fd5d3f2`](https://github.com/homeassistant-ai/ha-mcp/commit/fd5d3f2d4ce87baa4cfcc85a173bb0cc9d132a35))
- **addon**: Publish dev addon version 7.6.0.dev390 \[skip ci]
  ([`28dedf4`](https://github.com/homeassistant-ai/ha-mcp/commit/28dedf43f3cd094b1c40fb50e535166cc8c33388))
- **addon**: Publish dev addon version 7.6.0.dev389 \[skip ci]
  ([`9443446`](https://github.com/homeassistant-ai/ha-mcp/commit/9443446fdd607a75d8db5fdad009130b188267d6))
- Sync tool docs after merge \[skip ci]
  ([`94fcbeb`](https://github.com/homeassistant-ai/ha-mcp/commit/94fcbeb14bd9cfa3bef274182f95e4562a379971))
- **addon**: Publish dev addon version 7.6.0.dev388 \[skip ci]
  ([`ccc1816`](https://github.com/homeassistant-ai/ha-mcp/commit/ccc18162eec520950c58cc8eabb2aac04ac57ab3))
- Drop dead entity\_cache attr + ruff-format fuzzy\_search.py
  ([#&#8203;1503](https://github.com/homeassistant-ai/ha-mcp/pull/1503))
- **addon**: Publish dev addon version 7.6.0.dev387 \[skip ci]
  ([`057c108`](https://github.com/homeassistant-ai/ha-mcp/commit/057c108b2958959bfee34225899b47b5c989a119))
- **addon**: Publish dev addon version 7.6.0.dev386 \[skip ci]
  ([`66f4ac6`](https://github.com/homeassistant-ai/ha-mcp/commit/66f4ac6a37d85412b895962afef1b03707400e56))
- **addon**: Publish dev addon version 7.6.0.dev385 \[skip ci]
  ([`f331f11`](https://github.com/homeassistant-ai/ha-mcp/commit/f331f1150f525a18bf5bd2e27007b46a07a3492c))
- **addon**: Publish dev addon version 7.6.0.dev384 \[skip ci]
  ([`17c319e`](https://github.com/homeassistant-ai/ha-mcp/commit/17c319e68ba4b4d4e6acd84deb57c588aafcb48b))
- Sync tool docs after merge \[skip ci]
  ([`f36cbb7`](https://github.com/homeassistant-ai/ha-mcp/commit/f36cbb7e620839b16808b37b61dbc7602557b409))
- **addon**: Publish dev addon version 7.6.0.dev383 \[skip ci]
  ([`782ba3b`](https://github.com/homeassistant-ai/ha-mcp/commit/782ba3b2bbcfe50564c5524d6a2c018a4c9c9ba6))
- **addon**: Publish dev addon version 7.6.0.dev382 \[skip ci]
  ([`b647ab6`](https://github.com/homeassistant-ai/ha-mcp/commit/b647ab6eea76a030c7e3c46f72d732ff339398fe))
- **addon**: Publish dev addon version 7.6.0.dev381 \[skip ci]
  ([`e999c24`](https://github.com/homeassistant-ai/ha-mcp/commit/e999c242553a60561ca06dbefe3177c627bb9e2d))
- Sync tool docs after merge \[skip ci]
  ([`50d76b0`](https://github.com/homeassistant-ai/ha-mcp/commit/50d76b04a0691c565fafaabc76fd4bd3cf321953))
- **addon**: Publish dev addon version 7.6.0.dev380 \[skip ci]
  ([`ebdc69b`](https://github.com/homeassistant-ai/ha-mcp/commit/ebdc69bde929fd3342234149956af0710b9f260a))
- Sync tool docs after merge \[skip ci]
  ([`1f03967`](https://github.com/homeassistant-ai/ha-mcp/commit/1f03967160156cbffdfc9fc7deb5ed9a2b25f2ba))
- **addon**: Publish dev addon version 7.6.0.dev379 \[skip ci]
  ([`7cabb2e`](https://github.com/homeassistant-ai/ha-mcp/commit/7cabb2ec342561dbc74251f7697e3c4df0a8fcb3))
- **addon**: Publish dev addon version 7.6.0.dev378 \[skip ci]
  ([`7b631c7`](https://github.com/homeassistant-ai/ha-mcp/commit/7b631c75e0e4163ee13906b90ddad7a1b744e402))
- **addon**: Publish dev addon version 7.6.0.dev377 \[skip ci]
  ([`ea7b614`](https://github.com/homeassistant-ai/ha-mcp/commit/ea7b614615606a8fc05a0a18cb9cafa2f0a42c0d))
- **addon**: Publish dev addon version 7.6.0.dev376 \[skip ci]
  ([`fd33ecd`](https://github.com/homeassistant-ai/ha-mcp/commit/fd33ecd4d260f5fc2ab4f229c90b1a4514c5ccde))
- Sync tool docs after merge \[skip ci]
  ([`ba8fae3`](https://github.com/homeassistant-ai/ha-mcp/commit/ba8fae3cc4f451dc65022756a86cce5275f6e35f))
- **addon**: Publish dev addon version 7.6.0.dev375 \[skip ci]
  ([`afcd0d8`](https://github.com/homeassistant-ai/ha-mcp/commit/afcd0d8e061e1cba65fd119727fc64ed77f1818d))
- Sync tool docs after merge \[skip ci]
  ([`f9b55ad`](https://github.com/homeassistant-ai/ha-mcp/commit/f9b55ad59c6f03d6a98c3d3f6dd52ba9ff7c8a7a))
- **addon**: Publish dev addon version 7.6.0.dev374 \[skip ci]
  ([`eecdf8b`](https://github.com/homeassistant-ai/ha-mcp/commit/eecdf8b058ba932ba94cfb292ebf2331a54d2def))
- Sync tool docs after merge \[skip ci]
  ([`ef16a8a`](https://github.com/homeassistant-ai/ha-mcp/commit/ef16a8a197c76e51a49ae6ac69eecb15d202a194))
- **addon**: Publish dev addon version 7.6.0.dev373 \[skip ci]
  ([`9a93703`](https://github.com/homeassistant-ai/ha-mcp/commit/9a9370362fe5573ac71cd6e90353877faccce8a6))
- **addon**: Publish dev addon version 7.6.0.dev372 \[skip ci]
  ([`7352ce8`](https://github.com/homeassistant-ai/ha-mcp/commit/7352ce88ef59feef74c29ab47f6991cc2ac132a8))
- **addon**: Publish dev addon version 7.6.0.dev371 \[skip ci]
  ([`d7601f5`](https://github.com/homeassistant-ai/ha-mcp/commit/d7601f5796308e1c42a01dbce9861460027a93c4))
- Sync tool docs after merge \[skip ci]
  ([`d0a4482`](https://github.com/homeassistant-ai/ha-mcp/commit/d0a448276306a08f8a52c4645f1b9c5aeaafd68e))
- **addon**: Publish dev addon version 7.6.0.dev370 \[skip ci]
  ([`f83c32c`](https://github.com/homeassistant-ai/ha-mcp/commit/f83c32c747ddaeccffb040bc28d3e08d7507ded4))
- **addon**: Publish dev addon version 7.6.0.dev369 \[skip ci]
  ([`ea16661`](https://github.com/homeassistant-ai/ha-mcp/commit/ea16661f1d144feada93aee119b625f968f990cb))
- **addon**: Publish dev addon version 7.6.0.dev368 \[skip ci]
  ([`af49ea5`](https://github.com/homeassistant-ai/ha-mcp/commit/af49ea5aff5c9f54914943cc2f8a67ebeea2fdee))
- **addon**: Publish dev addon version 7.6.0.dev367 \[skip ci]
  ([`7dd5f4c`](https://github.com/homeassistant-ai/ha-mcp/commit/7dd5f4cd0237ef4d80a454efe4dfc0801f18fdbd))
- **addon**: Publish dev addon version 7.6.0.dev366 \[skip ci]
  ([`6273b7e`](https://github.com/homeassistant-ai/ha-mcp/commit/6273b7ec8695706948eefe8e617cac39363940ce))
- Sync tool docs after merge \[skip ci]
  ([`4e3fd5f`](https://github.com/homeassistant-ai/ha-mcp/commit/4e3fd5f331ce5fefa8e0e5560da411de17f2a8db))
- **addon**: Publish dev addon version 7.6.0.dev365 \[skip ci]
  ([`f3bf17b`](https://github.com/homeassistant-ai/ha-mcp/commit/f3bf17be132ebc67005c33a3c8fde85025d56809))
- Sync tool docs after merge \[skip ci]
  ([`5307b60`](https://github.com/homeassistant-ai/ha-mcp/commit/5307b608879154fccf67bc1894f61e97f26719c6))
- **addon**: Publish dev addon version 7.6.0.dev364 \[skip ci]
  ([`76ec1ae`](https://github.com/homeassistant-ai/ha-mcp/commit/76ec1aefab8e904abf620bf1c1130e5f5d0c4bd9))
- Sync tool docs after merge \[skip ci]
  ([`be864b5`](https://github.com/homeassistant-ai/ha-mcp/commit/be864b5d685f167334487d4fe83324afbb4999ca))
- **addon**: Publish dev addon version 7.6.0.dev363 \[skip ci]
  ([`da10c75`](https://github.com/homeassistant-ai/ha-mcp/commit/da10c75a248dcc19442e4db6f34f325683cb0a71))
- **addon**: Publish dev addon version 7.6.0.dev362 \[skip ci]
  ([`69f4527`](https://github.com/homeassistant-ai/ha-mcp/commit/69f45279707d18c724bd702ced46111b26cd000c))
- **addon**: Publish version 7.6.0 \[skip ci]
  ([`086d75d`](https://github.com/homeassistant-ai/ha-mcp/commit/086d75d72dd7d1c735413d96ef2fd38ae57b90ca))

##### Continuous Integration

- Vendor Puppet add-on as a pinned submodule + retry transient add-on builds
  ([#&#8203;1565](https://github.com/homeassistant-ai/ha-mcp/pull/1565))
- Make sync-tool-docs push resilient to concurrent master advances
  ([#&#8203;1564](https://github.com/homeassistant-ai/ha-mcp/pull/1564))
- Fix stale token-cap comment and harden triage budget tests ([#&#8203;1514](https://github.com/homeassistant-ai/ha-mcp/issues/1514))
  ([#&#8203;1560](https://github.com/homeassistant-ai/ha-mcp/pull/1560))
- **deps**: Bump the github-actions group with 2 updates
  ([#&#8203;1556](https://github.com/homeassistant-ai/ha-mcp/pull/1556))
- Budget triage prompt dynamically under the GitHub Models cap ([#&#8203;1514](https://github.com/homeassistant-ai/ha-mcp/issues/1514))
  ([#&#8203;1522](https://github.com/homeassistant-ai/ha-mcp/pull/1522))
- Reduce Docker Hub pulls in performance-tests workflow
  ([#&#8203;1549](https://github.com/homeassistant-ai/ha-mcp/pull/1549))
- Add JavaScript to the CodeQL code-quality gate
  ([#&#8203;1548](https://github.com/homeassistant-ai/ha-mcp/pull/1548))
- Add CodeQL code-quality CI gate and clear all code-quality findings
  ([#&#8203;1526](https://github.com/homeassistant-ai/ha-mcp/pull/1526))
- Drop tool list from evaluate prompt + tighten caps to fit 8K token limit
  ([`6f47c92`](https://github.com/homeassistant-ai/ha-mcp/commit/6f47c92507ef0d854153632107c0c702812f0c3c))
- Remove broken maintainer check (GITHUB\_TOKEN lacks read:org)
  ([`010ec58`](https://github.com/homeassistant-ai/ha-mcp/commit/010ec58d1f3b6d46b20aa83c3caee373dd826694))
- Switch evaluate step to gpt-4o-mini (16K token free tier vs 8K on gpt-4.1)
  ([#&#8203;1496](https://github.com/homeassistant-ai/ha-mcp/pull/1496))
- Issue bot v2 — GitHub Models triage, needs-info auto-close, duplicate detection
  ([#&#8203;1442](https://github.com/homeassistant-ai/ha-mcp/pull/1442))

##### Refactoring

- Extract settings-UI JavaScript and CSS to separate files
  ([#&#8203;1505](https://github.com/homeassistant-ai/ha-mcp/pull/1505))
- Collapse settings-UI route registration into one table
  ([#&#8203;1504](https://github.com/homeassistant-ai/ha-mcp/pull/1504))

##### Testing

- **oauth**: Add HTTP smoke tests for OAuth metadata-discovery endpoints
  ([#&#8203;1562](https://github.com/homeassistant-ai/ha-mcp/pull/1562))
- **uat**: Relabel c01 as a mode-discrimination probe (taxonomy consistency)
  ([#&#8203;1563](https://github.com/homeassistant-ai/ha-mcp/pull/1563))
- **uat**: Rework c01 routing probe to ha\_search registry-listing mode after [#&#8203;1529](https://github.com/homeassistant-ai/ha-mcp/issues/1529)
  ([#&#8203;1559](https://github.com/homeassistant-ai/ha-mcp/pull/1559))
- **uat**: Add response\_contains\_any check and always log agent responses
  ([#&#8203;1537](https://github.com/homeassistant-ai/ha-mcp/pull/1537))
- **uat**: Treat request timeout as per-story failure, not suite abort
  ([#&#8203;1536](https://github.com/homeassistant-ai/ha-mcp/pull/1536))
- **uat**: Harden BAT story runner against agent crashes
  ([#&#8203;1535](https://github.com/homeassistant-ai/ha-mcp/pull/1535))
- **uat**: Log model and quantization in BAT story results
  ([#&#8203;1525](https://github.com/homeassistant-ai/ha-mcp/pull/1525))
- Capture reasoning tokens and detect inert --no-think in BAT openai agent
  ([#&#8203;1524](https://github.com/homeassistant-ai/ha-mcp/pull/1524))

</details>

### [`v7.6.0`](https://github.com/homeassistant-ai/ha-mcp/blob/HEAD/CHANGELOG.md#v760-2026-05-27)

[Compare Source](https://github.com/homeassistant-ai/ha-mcp/compare/v7.5.0...v7.6.0)

##### Added

- Make HTTP bind host configurable via MCP\_HOST (closes [#&#8203;1434](https://github.com/homeassistant-ai/ha-mcp/issues/1434))
  ([#&#8203;1436](https://github.com/homeassistant-ai/ha-mcp/pull/1436))
- Tool Security Policies — per-tool approval gating ([#&#8203;966](https://github.com/homeassistant-ai/ha-mcp/issues/966))
  ([#&#8203;1421](https://github.com/homeassistant-ai/ha-mcp/pull/1421))
- Rename ha\_delete\_helpers\_integrations → ha\_remove\_helpers\_integrations + raise on missing target
  ([#&#8203;1424](https://github.com/homeassistant-ai/ha-mcp/pull/1424))
- Auto-backup edited entities before write/destructive tool calls (closes [#&#8203;1288](https://github.com/homeassistant-ai/ha-mcp/issues/1288))
  ([#&#8203;1403](https://github.com/homeassistant-ai/ha-mcp/pull/1403))
- Persistent settings UI for stdio mode
  ([#&#8203;1381](https://github.com/homeassistant-ai/ha-mcp/pull/1381))
- Add fields= projection to ha\_search\_entities, ha\_get\_overview, ha\_get\_state, ha\_get\_history, ha\_config\_list\_areas, ha\_list\_services ([#&#8203;1199](https://github.com/homeassistant-ai/ha-mcp/issues/1199))
  ([#&#8203;1225](https://github.com/homeassistant-ai/ha-mcp/pull/1225))
- Route entity-registration wait through WS events (closes [#&#8203;1152](https://github.com/homeassistant-ai/ha-mcp/issues/1152))
  ([#&#8203;1382](https://github.com/homeassistant-ai/ha-mcp/pull/1382))
- Add config subentry support
  ([#&#8203;1393](https://github.com/homeassistant-ai/ha-mcp/pull/1393))
- Add Assist pipeline management tool
  ([#&#8203;1392](https://github.com/homeassistant-ai/ha-mcp/pull/1392))
- Add knx to ha\_config\_set\_yaml allowlist
  ([#&#8203;1374](https://github.com/homeassistant-ai/ha-mcp/pull/1374))
- Extend automation\_id parity to set/remove automation responses
  ([#&#8203;1343](https://github.com/homeassistant-ai/ha-mcp/pull/1343))
- **haos-e2e**: Add parallel inaddon test tier (ha-mcp runs inside HAOS addon)
  ([#&#8203;1361](https://github.com/homeassistant-ai/ha-mcp/pull/1361))
- Expose integration diagnostics via ha\_get\_integration and ha\_get\_system\_health (closes [#&#8203;1148](https://github.com/homeassistant-ai/ha-mcp/issues/1148))
  ([#&#8203;1328](https://github.com/homeassistant-ai/ha-mcp/pull/1328))
- Return canonical script\_id from ha\_config\_get\_script ([#&#8203;1334](https://github.com/homeassistant-ai/ha-mcp/issues/1334))
  ([#&#8203;1352](https://github.com/homeassistant-ai/ha-mcp/pull/1352))
- Add automation\_id parity key to ha\_config\_get\_automation
  ([#&#8203;1329](https://github.com/homeassistant-ai/ha-mcp/pull/1329))
- Reject empty/whitespace identifiers on registry-metadata writes (closes [#&#8203;1294](https://github.com/homeassistant-ai/ha-mcp/issues/1294))
  ([#&#8203;1312](https://github.com/homeassistant-ai/ha-mcp/pull/1312))
- Add HA brand assets for custom integration
  ([#&#8203;1317](https://github.com/homeassistant-ai/ha-mcp/pull/1317))
- Unify ha\_config\_set\_helper response shape (closes [#&#8203;1293](https://github.com/homeassistant-ai/ha-mcp/issues/1293))
  ([#&#8203;1303](https://github.com/homeassistant-ai/ha-mcp/pull/1303))
- Mirror create-side validation guards onto update path (closes [#&#8203;1292](https://github.com/homeassistant-ai/ha-mcp/issues/1292))
  ([#&#8203;1304](https://github.com/homeassistant-ai/ha-mcp/pull/1304))
- Add array\_patch mode to ha\_manage\_addon for atomic GET-modify-POST
  ([#&#8203;1063](https://github.com/homeassistant-ai/ha-mcp/pull/1063))

##### Changed

- **agents**: Drop ha\_backup\_create + ha\_backup\_restore from accepted exceptions
  ([#&#8203;1445](https://github.com/homeassistant-ai/ha-mcp/pull/1445))
- Update contributors list \[contributors-updated]
  ([`c7665a6`](https://github.com/homeassistant-ai/ha-mcp/commit/c7665a6f5a08737d8fcbebbbe6afdc88ec5c4901))
- **overview**: Enumerate dismissed\_repair\_count in fields= description + static drift test
  ([#&#8203;1411](https://github.com/homeassistant-ai/ha-mcp/pull/1411))
- Credit [@&#8203;tomwilkie](https://github.com/tomwilkie) and six other contributors in README
  ([#&#8203;1400](https://github.com/homeassistant-ai/ha-mcp/pull/1400))
- **[#&#8203;1157](https://github.com/homeassistant-ai/ha-mcp/issues/1157)**: Bump skills-vendor + auto-update via Renovate + native for: field + scrub eval\_template anti-patterns
  ([#&#8203;1383](https://github.com/homeassistant-ai/ha-mcp/pull/1383))
- Extend Boy Scout weasel-phrase list with common variants; clarify semantic match
  ([#&#8203;1373](https://github.com/homeassistant-ai/ha-mcp/pull/1373))
- Merge Boy Scout Rule + Handling Discovered Improvements; tighten deferral gate
  ([#&#8203;1359](https://github.com/homeassistant-ai/ha-mcp/pull/1359))
- Categorize Issue Labels table and document 6 reverse-drift labels
  ([#&#8203;1335](https://github.com/homeassistant-ai/ha-mcp/pull/1335))
- Strip stale L-refs from test\_identifier\_validation\_family docstrings
  ([#&#8203;1324](https://github.com/homeassistant-ai/ha-mcp/pull/1324))
- Align label refs with live label set and fix triaged-removal trigger
  ([#&#8203;1316](https://github.com/homeassistant-ai/ha-mcp/pull/1316))
- Surface tool-discovery / categorized search
  ([#&#8203;1123](https://github.com/homeassistant-ai/ha-mcp/pull/1123))
- Fix two stale ha\_get\_skill\_guide references missed in [#&#8203;1289](https://github.com/homeassistant-ai/ha-mcp/issues/1289)
  ([#&#8203;1305](https://github.com/homeassistant-ai/ha-mcp/pull/1305))
- Clarify setup wizard placeholders need braces removed ([#&#8203;1284](https://github.com/homeassistant-ai/ha-mcp/issues/1284))
  ([#&#8203;1286](https://github.com/homeassistant-ai/ha-mcp/pull/1286))

##### Fixed

- Remove counter from ha\_reload\_core targets ([#&#8203;1453](https://github.com/homeassistant-ai/ha-mcp/issues/1453))
  ([#&#8203;1456](https://github.com/homeassistant-ai/ha-mcp/pull/1456))
- **backup**: Post-timeout match correctness + state-gate (closes [#&#8203;1433](https://github.com/homeassistant-ai/ha-mcp/issues/1433))
  ([#&#8203;1435](https://github.com/homeassistant-ai/ha-mcp/pull/1435))
- Sync addon settings UI with Supervisor options end-to-end
  ([#&#8203;1420](https://github.com/homeassistant-ai/ha-mcp/pull/1420))
- **calendar**: Switch ha\_config\_remove\_calendar\_event to WebSocket (closes [#&#8203;1413](https://github.com/homeassistant-ai/ha-mcp/issues/1413), [#&#8203;1416](https://github.com/homeassistant-ai/ha-mcp/issues/1416))
  ([#&#8203;1418](https://github.com/homeassistant-ai/ha-mcp/pull/1418))
- Error-shape consistency for non-entity not-found (closes [#&#8203;1297](https://github.com/homeassistant-ai/ha-mcp/issues/1297))
  ([#&#8203;1397](https://github.com/homeassistant-ai/ha-mcp/pull/1397))
- Guard against silent automation overwrite on id mismatch ([#&#8203;1404](https://github.com/homeassistant-ai/ha-mcp/issues/1404))
  ([#&#8203;1405](https://github.com/homeassistant-ai/ha-mcp/pull/1405))
- Cache YAML instance to prevent CPU spikes in bulk edits ([#&#8203;1370](https://github.com/homeassistant-ai/ha-mcp/issues/1370))
  ([#&#8203;1371](https://github.com/homeassistant-ai/ha-mcp/pull/1371))
- **client**: Route get\_error\_log via hassio proxy on external-HAOS clients
  ([#&#8203;1360](https://github.com/homeassistant-ai/ha-mcp/pull/1360))
- Classify dashboard 404s ("unknown config specified") as RESOURCE\_NOT\_FOUND
  ([#&#8203;1345](https://github.com/homeassistant-ai/ha-mcp/pull/1345))
- Detect HA addon installs as http transport, not stdio ([#&#8203;1322](https://github.com/homeassistant-ai/ha-mcp/issues/1322))
  ([#&#8203;1327](https://github.com/homeassistant-ai/ha-mcp/pull/1327))
- Actionable 403 suggestion when addon has unmapped container ports ([#&#8203;1319](https://github.com/homeassistant-ai/ha-mcp/issues/1319))
  ([#&#8203;1325](https://github.com/homeassistant-ai/ha-mcp/pull/1325))
- Filter dismissed repairs in overview and system\_health ([#&#8203;1307](https://github.com/homeassistant-ai/ha-mcp/issues/1307))
  ([#&#8203;1309](https://github.com/homeassistant-ai/ha-mcp/pull/1309))
- Exit on HA container death + daily reset before CI check
  ([#&#8203;1295](https://github.com/homeassistant-ai/ha-mcp/pull/1295))
- Align ha\_config\_set\_dashboard with sibling re-fetch-after-save pattern ([#&#8203;1291](https://github.com/homeassistant-ai/ha-mcp/issues/1291))
  ([#&#8203;1301](https://github.com/homeassistant-ai/ha-mcp/pull/1301))
- Allow str.replace in python\_transform; hint at search mode on IndexError
  ([#&#8203;1287](https://github.com/homeassistant-ai/ha-mcp/pull/1287))
- **array\_patch**: Tighten validation and surface silent failures
  ([#&#8203;1285](https://github.com/homeassistant-ai/ha-mcp/pull/1285))
- HA Core proxy fallback for ha\_get\_logs(source=system\_service) on non-addon installs
  ([#&#8203;1283](https://github.com/homeassistant-ai/ha-mcp/pull/1283))

##### Performance Improvements

- Tighten \_poll\_for\_automation\_entity first-poll cadence
  ([#&#8203;1384](https://github.com/homeassistant-ai/ha-mcp/pull/1384))
- Parallelize ha\_get\_system\_health optional sections via asyncio.gather
  ([#&#8203;1336](https://github.com/homeassistant-ai/ha-mcp/pull/1336))

##### Refactoring

- **service**: Compact ha\_call\_service result default ([#&#8203;1446](https://github.com/homeassistant-ai/ha-mcp/issues/1446))
  ([#&#8203;1447](https://github.com/homeassistant-ai/ha-mcp/pull/1447))
- Rename ha\_update\_device → ha\_set\_device
  ([#&#8203;1444](https://github.com/homeassistant-ai/ha-mcp/pull/1444))
- Remove duplicate flat area/floor list tools (consolidation followup to [#&#8203;1016](https://github.com/homeassistant-ai/ha-mcp/issues/1016))
  ([#&#8203;1429](https://github.com/homeassistant-ai/ha-mcp/pull/1429))
- **complexity**: Migrate tools\_utility.py to class-based pattern
  ([#&#8203;1423](https://github.com/homeassistant-ai/ha-mcp/pull/1423))
- **complexity**: Reduce C901 violations in tools/ — batch 4
  ([#&#8203;1408](https://github.com/homeassistant-ai/ha-mcp/pull/1408))
- Route \_poll\_for\_automation\_entity through WS event waiter (closes [#&#8203;1395](https://github.com/homeassistant-ai/ha-mcp/issues/1395))
  ([#&#8203;1406](https://github.com/homeassistant-ai/ha-mcp/pull/1406))
- **yaml**: Use threading.local subclass for cached instance
  ([#&#8203;1396](https://github.com/homeassistant-ai/ha-mcp/pull/1396))
- Align dashboards 404 shape with sibling config tools
  ([#&#8203;1386](https://github.com/homeassistant-ai/ha-mcp/pull/1386))
- Complete singular warning → warnings list migration repo-wide (closes [#&#8203;1332](https://github.com/homeassistant-ai/ha-mcp/issues/1332))
  ([#&#8203;1341](https://github.com/homeassistant-ai/ha-mcp/pull/1341))
- Complete warnings-list migration for lifecycle-write tools
  ([#&#8203;1340](https://github.com/homeassistant-ai/ha-mcp/pull/1340))
- Drop redundant identifier echo key from ha\_config\_get\_automation
  ([#&#8203;1354](https://github.com/homeassistant-ai/ha-mcp/pull/1354))
- Drop logger.error in config-tool except blocks ([#&#8203;1302](https://github.com/homeassistant-ai/ha-mcp/issues/1302))
  ([#&#8203;1353](https://github.com/homeassistant-ai/ha-mcp/pull/1353))
- Extend validate\_identifier\_not\_empty to automations/scripts/dashboards CRUD (closes [#&#8203;1313](https://github.com/homeassistant-ai/ha-mcp/issues/1313))
  ([#&#8203;1321](https://github.com/homeassistant-ai/ha-mcp/pull/1321))
- Migrate tools\_config\_scenes inline empty-id guards to shared helper
  ([#&#8203;1320](https://github.com/homeassistant-ai/ha-mcp/pull/1320))
- Remove ha\_get\_helper\_schema (closes [#&#8203;1186](https://github.com/homeassistant-ai/ha-mcp/issues/1186))
  ([#&#8203;1315](https://github.com/homeassistant-ai/ha-mcp/pull/1315))
- Consolidate skill tools; fix stable submodule packaging
  ([#&#8203;1289](https://github.com/homeassistant-ai/ha-mcp/pull/1289))
- Align tools\_config\_automations.py error-handling with sibling pattern ([#&#8203;1290](https://github.com/homeassistant-ai/ha-mcp/issues/1290))
  ([#&#8203;1298](https://github.com/homeassistant-ai/ha-mcp/pull/1298))

***

<details>
<summary>Internal Changes</summary>

##### Fixed

- **ci**: Install libguestfs in HAOS publish workflow
  ([#&#8203;1358](https://github.com/homeassistant-ai/ha-mcp/pull/1358))

##### Build System

- **deps**: Bump esbuild from 0.24.2 to 0.25.0 in /tests/js
  ([#&#8203;1427](https://github.com/homeassistant-ai/ha-mcp/pull/1427))
- **deps**: Bump devalue from 5.6.4 to 5.8.1 in /site
  ([#&#8203;1282](https://github.com/homeassistant-ai/ha-mcp/pull/1282))
- **deps**: Bump astro from 6.1.6 to 6.1.10 in /site
  ([#&#8203;1274](https://github.com/homeassistant-ai/ha-mcp/pull/1274))

##### Chores

- **addon**: Publish dev addon version 7.5.0.dev360 \[skip ci]
  ([`ad7aed1`](https://github.com/homeassistant-ai/ha-mcp/commit/ad7aed13d6843967e2a259ae46fec2bbc6abc896))
- Sync tool docs after merge \[skip ci]
  ([`9c4984f`](https://github.com/homeassistant-ai/ha-mcp/commit/9c4984fc44d30c15a96372fbc1b401fbd679dc8f))
- **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.4
  ([#&#8203;1450](https://github.com/homeassistant-ai/ha-mcp/pull/1450))
- **addon**: Publish dev addon version 7.5.0.dev359 \[skip ci]
  ([`b82d4ee`](https://github.com/homeassistant-ai/ha-mcp/commit/b82d4eea85792b620368d4859e99c143f9fbfadf))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.16
  ([#&#8203;1449](https://github.com/homeassistant-ai/ha-mcp/pull/1449))
- **addon**: Publish dev addon version 7.5.0.dev358 \[skip ci]
  ([`53fba6d`](https://github.com/homeassistant-ai/ha-mcp/commit/53fba6da76ce8c78a0de035883233a36762601bc))
- Sync tool docs after merge \[skip ci]
  ([`dc7750d`](https://github.com/homeassistant-ai/ha-mcp/commit/dc7750de16b60f70e40e6e5c8908d9e84e544683))
- **addon**: Publish dev addon version 7.5.0.dev357 \[skip ci]
  ([`fd150c9`](https://github.com/homeassistant-ai/ha-mcp/commit/fd150c9b9c4bdb9026e67eae340226c36b270d94))
- **addon**: Publish dev addon version 7.5.0.dev356 \[skip ci]
  ([`5fa1463`](https://github.com/homeassistant-ai/ha-mcp/commit/5fa14630229ba0d6b97d9328768e247bf9c20af4))
- **addon**: Publish dev addon version 7.5.0.dev355 \[skip ci]
  ([`174ac5d`](https://github.com/homeassistant-ai/ha-mcp/commit/174ac5dca7667702cd664420c50e0101366e6c3a))
- **addon**: Publish dev addon version 7.5.0.dev354 \[skip ci]
  ([`4f93989`](https://github.com/homeassistant-ai/ha-mcp/commit/4f939892dea2d1951541b1a557c51e957f6751c0))
- **addon**: Publish dev addon version 7.5.0.dev353 \[skip ci]
  ([`53f282f`](https://github.com/homeassistant-ai/ha-mcp/commit/53f282f19ee736637106f5baececa3939393778e))
- **addon**: Publish dev addon version 7.5.0.dev352 \[skip ci]
  ([`4911d46`](https://github.com/homeassistant-ai/ha-mcp/commit/4911d4682c4a865a93a8e821bc86bd26cef0ed5a))
- Sync tool docs after merge \[skip ci]
  ([`8a79837`](https://github.com/homeassistant-ai/ha-mcp/commit/8a798373a7b2f1adb94b93e65d626ab582c7bd5d))
- **addon**: Publish dev addon version 7.5.0.dev351 \[skip ci]
  ([`13afa9d`](https://github.com/homeassistant-ai/ha-mcp/commit/13afa9df026dd27332277140c34f7d41d0229efd))
- **addon**: Publish dev addon version 7.5.0.dev350 \[skip ci]
  ([`e93d680`](https://github.com/homeassistant-ai/ha-mcp/commit/e93d6806025c6d7fd2ef94c509a607c4ac85c8e0))
- **addon**: Publish dev addon version 7.5.0.dev349 \[skip ci]
  ([`1631ad1`](https://github.com/homeassistant-ai/ha-mcp/commit/1631ad10bdada4487d7b54049e92df6ddb29439a))
- **addon**: Publish dev addon version 7.5.0.dev348 \[skip ci]
  ([`18a8aef`](https://github.com/homeassistant-ai/ha-mcp/commit/18a8aef50675c1a881b279666e0af1b093679850))
- Sync tool docs after merge \[skip ci]
  ([`9e0493d`](https://github.com/homeassistant-ai/ha-mcp/commit/9e0493d248e23a8042841e90847662926395a448))
- **addon**: Publish dev addon version 7.5.0.dev347 \[skip ci]
  ([`e2067e4`](https://github.com/homeassistant-ai/ha-mcp/commit/e2067e446b127097e61d07516ae0e742ae2de6c8))
- Sync tool docs after merge \[skip ci]
  ([`7bdb3d4`](https://github.com/homeassistant-ai/ha-mcp/commit/7bdb3d402b60900a1012b8269e52b7cbeb400f84))
- **addon**: Publish dev addon version 7.5.0.dev346 \[skip ci]
  ([`c2d4dd7`](https://github.com/homeassistant-ai/ha-mcp/commit/c2d4dd7091653a618326f665fd34b5458d26c8f6))
- Sync tool docs after merge \[skip ci]
  ([`393b354`](https://github.com/homeassistant-ai/ha-mcp/commit/393b354e8a8e1280029eee9521971c693526970a))
- **addon**: Publish dev addon version 7.5.0.dev345 \[skip ci]
  ([`42ede8b`](https://github.com/homeassistant-ai/ha-mcp/commit/42ede8b0e6066faf3ed6f6920629a8d94d77f69a))
- **addon**: Publish dev addon version 7.5.0.dev344 \[skip ci]
  ([`e6cc7a1`](https://github.com/homeassistant-ai/ha-mcp/commit/e6cc7a1b8ed252f1aaa2b8c964e56c97a27b94c2))
- Sync tool docs after merge \[skip ci]
  ([`fb35f30`](https://github.com/homeassistant-ai/ha-mcp/commit/fb35f305cbf7de6807c29bd3ada9da2f028ff730))
- **addon**: Publish dev addon version 7.5.0.dev343 \[skip ci]
  ([`401b7b4`](https://github.com/homeassistant-ai/ha-mcp/commit/401b7b4d707dbb3c56a3afecad9e9d22ecbc7d1e))
- **addon**: Publish dev addon version 7.5.0.dev342 \[skip ci]
  ([`0679371`](https://github.com/homeassistant-ai/ha-mcp/commit/06793710b04a985ed0d66861266b9b79494e4111))
- Sync tool docs after merge \[skip ci]
  ([`f6796ec`](https://github.com/homeassistant-ai/ha-mcp/commit/f6796ec19b4596455ff54b58746bd3e075d5d280))
- **addon**: Publish dev addon version 7.5.0.dev341 \[skip ci]
  ([`3654478`](https://github.com/homeassistant-ai/ha-mcp/commit/3654478f80428d15671e49162acecea19d67f672))
- **addon**: Publish dev addon version 7.5.0.dev340 \[skip ci]
  ([`64f00b6`](https://github.com/homeassistant-ai/ha-mcp/commit/64f00b658846cad0e0714f37d221fb65327015cf))
- **addon**: Publish dev addon version 7.5.0.dev339 \[skip ci]
  ([`d6e8873`](https://github.com/homeassistant-ai/ha-mcp/commit/d6e88731556464deb768524dc24c90fd0a622a96))
- Sync tool docs after merge \[skip ci]
  ([`7525e93`](https://github.com/homeassistant-ai/ha-mcp/commit/7525e930a872871fed78f9b1461aea8e78eb5ad7))
- **addon**: Publish dev addon version 7.5.0.dev338 \[skip ci]
  ([`288ca4a`](https://github.com/homeassistant-ai/ha-mcp/commit/288ca4abd00b2b366cbaf671c1ace3398cc8f2fd))
- **addon**: Publish dev addon version 7.5.0.dev337 \[skip ci]
  ([`f539ae5`](https://github.com/homeassistant-ai/ha-mcp/commit/f539ae5aa881853d831e6952ff3af8bc85eb8b2a))
- **addon**: Publish dev addon version 7.5.0.dev336 \[skip ci]
  ([`5568a86`](https://github.com/homeassistant-ai/ha-mcp/commit/5568a861002cfb909e29368bd4e93ef0bd2e4c03))
- **addon**: Publish dev addon version 7.5.0.dev335 \[skip ci]
  ([`e069405`](https://github.com/homeassistant-ai/ha-mcp/commit/e0694054bd8627f6464379c34c1e3eec5b080a44))
- **addon**: Publish dev addon version 7.5.0.dev334 \[skip ci]
  ([`f7be6ea`](https://github.com/homeassistant-ai/ha-mcp/commit/f7be6ead486a32840a935a1e5ec78a34f8003c0d))
- **addon**: Publish dev addon version 7.5.0.dev333 \[skip ci]
  ([`cb480ea`](https://github.com/homeassistant-ai/ha-mcp/commit/cb480eae4d855345c19a6465351e0bdef2bdf5c3))
- Sync tool docs after merge \[skip ci]
  ([`9a5bc3c`](https://github.com/homeassistant-ai/ha-mcp/commit/9a5bc3c61372159bb936d7b929166924343dc76f))
- **addon**: Publish dev addon version 7.5.0.dev332 \[skip ci]
  ([`e0e59ee`](https://github.com/homeassistant-ai/ha-mcp/commit/e0e59ee08072bb4aac51e90928fab2d68f6156ce))
- Sync tool docs after merge \[skip ci]
  ([`499ebf0`](https://github.com/homeassistant-ai/ha-mcp/commit/499ebf0c9d4c4ec3784dd72782fe301cd9c19d60))
- **addon**: Publish dev addon version 7.5.0.dev331 \[skip ci]
  ([`3e0ce92`](https://github.com/homeassistant-ai/ha-mcp/commit/3e0ce92e11777a8fd9c86153e4c248007b8edae9))
- **addon**: Publish dev addon version 7.5.0.dev330 \[skip ci]
  ([`e48d056`](https://github.com/homeassistant-ai/ha-mcp/commit/e48d056ab356e11f8b2952b6c675307d1e76a895))
- **deps**: Update ghcr.io/astral-sh/uv docker tag to v0.11.15
  ([#&#8203;1376](https://github.com/homeassistant-ai/ha-mcp/pull/1376))
- **deps**: Update ghcr.io/home-assistant/home-assistant docker tag to v2026.5.3
  ([#&#8203;1377](https://github.com/homeassistant-ai/ha-mcp/pull/1377))
- **addon**: Publish dev addon version 7.5.0.dev329 \[skip ci]
  ([`c523c50`](https://github.com/homeassistant-ai/ha-mcp/commit/c523c502c5c6f282905620d3db489e9c69ec2472))
- Sync tool docs after merge \[skip ci]
  ([`5b7a8aa`](https://github.com/homeassistant-ai/ha-mcp/commit/5b7a8aa0794359a90d7b89a9cc61d73d8cf038d2))
- **addon**: Publish dev addon version 7.5.0.dev328 \[skip ci]
  ([`6c42fba`](https://github.com/homeassistant-ai/ha-mcp/commit/6c42fba92d4786d7498f738c53dfe50c899d0890))
- **addon**: Publish dev addon version 7.5.0.dev327 \[skip ci]
  ([`aecd025`](https://github.com/homeassistant-ai/ha-mcp/commit/aecd025eeea3d1edcd9466db3720cb17de71b90a))
- **addon**: Publish dev addon version 7.5.0.dev326 \[skip ci]
  ([`399c17c`](https://github.com/homeassistant-ai/ha-mcp/commit/399c17c36602a7767491df6e317186cb6291f3e4))
- **addon**: Publish dev addon version 7.5.0.dev325 \[skip ci]
  ([`b580b45`](https://github.com/homeassistant-ai/ha-mcp/commit/b580b452e9007deb33acc49098238988aa768603))
- Sync tool docs after merge \[skip ci]
  ([`8567c3a`](https://github.com/homeassistant-ai/ha-mcp/commit/8567c3a6e66e7bebbbad8e96b94058f8456deb14))
- **addon**: Publish dev addon version 7.5.0.dev324 \[skip ci]
  ([`a65579d`](https://github.com/homeassistant-ai/ha-mcp/commit/a65579d0b6e4a8bb7f8c1ed97fd432cd770c47a4))
- **addon**: Publish dev addon version 7.5.0.dev323 \[skip ci]
  ([`c7667ba`](https://github.com/homeassistant-ai/ha-mcp/commit/c7667ba66538f1ba8a06b7616fb15896bf021744))
- **addon**: Publish dev addon version 7.5.0.dev322 \[skip ci]
  ([`44d15a8`](https://github.com/homeassistant-ai/ha-mcp/commit/44d15a89cbacc2c38ab39868b380414507a6708b))
- **addon**: Publish dev addon version 7.5.0.dev321 \[skip ci]
  ([`8739f6c`](https://github.com/homeassistant-ai/ha-mcp/commit/8739f6c09121c88a596470df293ac8e7e0f1050a))
- **addon**: Publish dev addon version 7.5.0.dev320 \[skip ci]
  ([`02b6e47`](https://github.com/homeassistant-ai/ha-mcp/commit/02b6e47f8eb6c778f709d7adca115638c2fe98a2))
- Sync tool docs after merge \[skip ci]
  ([`ab68c9a`](https://github.com/homeassistant-ai/ha-mcp/commit/ab68c9a9aa94ac6861af7b59997ec679f1849503))
- **addon**: Publish dev addon version 7.5.0.dev319 \[skip ci]
  ([`4472904`](https://github.com/homeassistant-ai/ha-mcp/commit/44729040c11af0bd97189080c1ec59995fc563b4))
- **addon**: Publish dev addon version 7.5.0.dev318 \[skip ci]
  ([`e030dbc`](https://github.com/homeassistant-ai/ha-mcp/commit/e030dbcc99e002751834274b1d53da161ab27759))
- **addon**: Publish dev addon version 7.5.0.dev317 \[skip ci]
  ([`d87855c`](https://github.com/homeassistant-ai/ha-mcp/commit/d87855cf07cef9d9d7babf0bbec74d228149327d))
- Sync tool docs after merge \[skip ci]
  ([`a72a4e8`](https://github.com/homeassistant-ai/ha-mcp/commit/a72a4e8c0de99a8516f3820545642c37a31c17e5))
- **addon**: Publish dev addon version 7.5.0.dev316 \[skip ci]
  ([`bd9397f`](https://github.com/homeassistant-ai/ha-mcp/commit/bd9397f3a088a24a14af66a97afcf1c97904ef61))
- **addon**: Publish dev addon version 7.5.0.dev315 \[skip ci]
  ([`264bfc2`](https://github.com/homeassistant-ai/ha-mcp/commit/264bfc2fcb5ef6fe3ee6f9e073b69e0469eeafdd))
- **addon**: Publish dev addon version 7.5.0.dev314 \[skip ci]
  ([`df62881`](https://github.com/homeassistant-ai/ha-mcp/commit/df6288194cdde8b625132f19b0dd3edb2142a2b3))
- **addon**: Publish dev addon version 7.5.0.dev313 \[skip ci]
  ([`f6c47ca`](https://github.com/homeassistant-ai/ha-mcp/commit/f6c47caa21b0d82799b2be9330f97aa5494aa3c8))
- **addon**: Publish dev addon version 7.5.0.dev312 \[skip ci]
  ([`2bb7a74`](https://github.com/homeassistant-ai/ha-mcp/commit/2bb7a74f06e0652a729a735b08fc7cdc20a034f6))
- Sync tool docs after merge \[skip ci]
  ([`137e279`](https://github.com/homeassistant-ai/ha-mcp/commit/137e27943b1b704fe1d3e3a5b525b4d82bce33eb))
- **addon**: Publish dev addon version 7.5.0.dev311 \[skip ci]
  ([`28324ea`](https://github.com/homeassistant-ai/ha-mcp/commit/28324ea4a92fbf61f5ee40c561286c466af2309a))
- Sync tool docs after merge \[skip ci]
  ([`9a753d4`](https://github.com/homeassistant-ai/ha-mcp/commit/9a753d49abb4ccd84e874451dbcc563260f9e19d))
- **addon**: Publish dev addon version 7.5.0.dev310 \[skip ci]
  ([`f893b2e`](https://github.com/homeassistant-ai/ha-mcp/commit/f893b2ebf91967d0e2548f9205a15bfe0e7d86c2))
- **addon**: Publish dev addon version 7.5.0.dev309 \[skip ci]
  ([`8cbdb7b`](https://github.com/homeassistant-ai/ha-mcp/commit/8cbdb7bf318707fed1a0b3e8cb9922e6c170521e))
- **addon**: Publish dev addon version 7.5.0.dev308 \[skip ci]
  ([`2d18016`](https://github.com/homeassistant-ai/ha-mcp/commit/2d18016de8eb26a18cfb471e7b5f755f1309bd36))
- **addon**: Publish dev addon version 7.5.0.dev307 \[skip ci]
  ([`3fc3b28`](https://github.com/homeassistant-ai/ha-mcp/commit/3fc3b28881f237df02da8b65467fba9a7d693009))
- Sync tool docs after merge \[skip ci]
  ([`9e6cff8`](https://github.com/homeassistant-ai/ha-mcp/commit/9e6cff8f641bcd8122e94fc8be9c30aa18456e80))
- **addon**: Publish dev addon version 7.5.0.dev306 \[skip ci]
  ([`8bdd0fc`](https://github.com/homeassistant-ai/ha-mcp/commit/8bdd0fca9c107f45e4c3719e7616984b68876a9e))
- **addon**: Publish dev addon version 7.5.0.dev305 \[skip ci]
  ([`83535b9`](https://github.com/homeassistant-ai/ha-mcp/commit/83535b99e190fb56b7ce227334331ebf2affb3e1))
- **addon**: Publish dev addon version 7.5.0.dev304 \[skip ci]
  ([`1435b3a`](https://github.com/homeassistant-ai/ha-mcp/commit/1435b3a24d73503e189b83f8ea5aaeea20d9ebf3))
- **addon**: Publish dev addon version 7.5.0.dev303 \[skip ci]
  ([`e2da659`](https://github.com/homeassistant-ai/ha-mcp/commit/e2da6591d6ad2def0b48a9be750cb8a41f40524d))
- Sync tool docs after merge \[skip ci]
  ([`23789fa`](https://github.com/homeassistant-ai/ha-mcp/commit/23789fa978591b7d3966894082a2683ca6a6ae3b))
- **addon**: Publish dev addon version 7.5.0.dev302 \[skip ci]
  ([`6c8e574`](https://github.com/homeassistant-ai/ha-mcp/commit/6c8e574e7e3d8d78b3a2e48f6a70531d7a95eed8))
- Sync tool docs after merge \[skip ci]
  ([`d2329cb`](https://github.com/homeassistant-ai/ha-mcp/commit/d2329cbff39891d209317528fd7b42759c3414a8))
- **addon**: Publish dev addon version 7.5.0.dev301 \[skip ci]
  ([`bb538f7`](https://github.com/homeassistant-ai/ha-mcp/commit/bb538f70d2fc57072f09cfc6202e9d3dc1a2d257))
- Sync tool docs after merge \[skip ci]
  ([`f70f0e1`](https://github.com/homeassistant-ai/ha-mcp/commit/f70f0e14222b29a8923e706f113d1fafb4a5c23e))
- **addon**: Publish version 7.5.0 \[skip ci]
  ([`9c5eb37`](https://github.com/homeassistant-ai/ha-mcp/commit/9c5eb37779236ef19366a2a59a667d3916458e5a))

##### Continuous Integration

- **deps**: Bump actions/upload-artifact in the github-actions group
  ([#&#8203;1437](https://github.com/homeassistant-ai/ha-mcp/pull/1437))
- Share qcow2 cache + GHCR fallback between HAOS lanes
  ([#&#8203;1407](https://github.com/homeassistant-ai/ha-mcp/pull/1407))
- Add ruff format --check on changed Python files
  ([#&#8203;1387](https://github.com/homeassistant-ai/ha-mcp/pull/1387))
- Exempt assigned issues from stale bot
  ([#&#8203;1368](https://github.com/homeassistant-ai/ha-mcp/pull/1368))
- **deps**: Bump the github-actions group with 3 updates
  ([#&#8203;1362](https://github.com/homeassistant-ai/ha-mcp/pull/1362))

##### Refactoring

- Consolidate lovelace/dashboards/list through shared helper
  ([#&#8203;1344](https://github.com/homeassistant-ai/ha-mcp/pull/1344))

##### Testing

- **haos-e2e**: Bake + install webhook-proxy addon and exercise its runtime
  ([#&#8203;1443](https://github.com/homeassistant-ai/ha-mcp/pull/1443))
- **config-subentry**: Mark forecast\_solar e2e as known flaky + relative-import sweep
  ([#&#8203;1430](https://github.com/homeassistant-ai/ha-mcp/pull/1430))
- **haos-e2e**: Trim cache-save race, compress GHCR qcow2, eval boot snapshot
  ([#&#8203;1428](https://github.com/homeassistant-ai/ha-mcp/pull/1428))
- JSDOM behaviour harness + auto-discovery parse coverage for every rendered <script>
  ([#&#8203;1425](https://github.com/homeassistant-ai/ha-mcp/pull/1425))
- **hacs**: Retry TestMcpToolsInstallation on flake
  ([#&#8203;1426](https://github.com/homeassistant-ai/ha-mcp/pull/1426))
- **e2e**: Drop redundant lifecycle roundtrips, keep only Matter Server ([#&#8203;1414](https://github.com/homeassistant-ai/ha-mcp/issues/1414))
  ([#&#8203;1419](https://github.com/homeassistant-ai/ha-mcp/pull/1419))
- **e2e**: Assert backend dispatch matches workflow env on every lane
  ([#&#8203;1409](https://github.com/homeassistant-ai/ha-mcp/pull/1409))
- Escape ideographic space and format file ([#&#8203;1237](https://github.com/homeassistant-ai/ha-mcp/issues/1237))
  ([#&#8203;1410](https://github.com/homeassistant-ai/ha-mcp/pull/1410))
- **e2e**: Measure \_POLL\_CADENCE p50/p99 to validate or retune (closes [#&#8203;1389](https://github.com/homeassistant-ai/ha-mcp/issues/1389))
  ([#&#8203;1398](https://github.com/homeassistant-ai/ha-mcp/pull/1398))
- **e2e**: Wait for addon state=started in haos proxy header test
  ([#&#8203;1402](https://github.com/homeassistant-ai/ha-mcp/pull/1402))
- Pin remaining \_classify\_by\_message branches
  ([#&#8203;1385](https://github.com/homeassistant-ai/ha-mcp/pull/1385))
- **haos-e2e**: Slim addon set + real-addon ha\_manage\_addon coverage (closes [#&#8203;1350](https://github.com/homeassistant-ai/ha-mcp/issues/1350))
  ([#&#8203;1379](https://github.com/homeassistant-ai/ha-mcp/pull/1379))
- **haos-e2e**: Close out [#&#8203;1349](https://github.com/homeassistant-ai/ha-mcp/issues/1349) — lifecycle, integrations, supervisor\_mock migration, no more skips
  ([#&#8203;1375](https://github.com/homeassistant-ai/ha-mcp/pull/1375))
- **e2e**: Consolidate readiness gates onto /api/core/state (refs [#&#8203;366](https://github.com/homeassistant-ai/ha-mcp/issues/366))
  ([#&#8203;1372](https://github.com/homeassistant-ai/ha-mcp/pull/1372))
- **e2e**: Tighten 5 readiness-gate budgets with 2-63x headroom (refs [#&#8203;366](https://github.com/homeassistant-ai/ha-mcp/issues/366))
  ([#&#8203;1369](https://github.com/homeassistant-ai/ha-mcp/pull/1369))
- Scaffold HAOS E2E tier image-build pipeline (refs [#&#8203;1281](https://github.com/homeassistant-ai/ha-mcp/issues/1281))
  ([#&#8203;1326](https://github.com/homeassistant-ai/ha-mcp/pull/1326))
- **e2e**: Instrument HA\_MCP\_TOOLS\_WAIT readiness gate (refs [#&#8203;366](https://github.com/homeassistant-ai/ha-mcp/issues/366))
  ([#&#8203;1346](https://github.com/homeassistant-ai/ha-mcp/pull/1346))
- **e2e**: Centralize wait\_for\_entity\_registration helper (refs [#&#8203;366](https://github.com/homeassistant-ai/ha-mcp/issues/366))
  ([#&#8203;1308](https://github.com/homeassistant-ai/ha-mcp/pull/1308))
- **e2e**: Unify dict-error message extraction across e2e tests (refs [#&#8203;366](https://github.com/homeassistant-ai/ha-mcp/issues/366))
  ([#&#8203;1311](https://github.com/homeassistant-ai/ha-mcp/pull/1311))
- **e2e**: Surface readiness-gate elapsed times in CI logs (refs [#&#8203;366](https://github.com/homeassistant-ai/ha-mcp/issues/366))
  ([#&#8203;1310](https://github.com/homeassistant-ai/ha-mcp/pull/1310))

</details>

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL21pbm9yIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/994
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants