429 files

This commit is contained in:
Home Assistant Version Control
2026-08-30 02:43:31 +00:00
parent 472c19bceb
commit 963cce81a7
566 changed files with 50311 additions and 47616 deletions
@@ -42,16 +42,19 @@ from .const import (
OPT_SECRET_PATH_OVERRIDE,
OPT_WEBHOOK_AUTH,
OPT_WEBHOOK_ID_OVERRIDE,
WEBHOOK_AUTH_HA,
WEBHOOK_AUTH_LEGACY,
WEBHOOK_AUTH_NONE,
)
# NOTE: embedded_setup / coordinator (and their embedded_server / mcp_webhook
# chain) are imported lazily inside the entry lifecycle functions below, not at
# module top level. They pull in aiohttp and several homeassistant.* submodules
# (auth, requirements, util.package, components.http/webhook) that the
# entry-point wiring here never touches directly, so a top-level import would
# make importing this package require that whole stack — breaking hermetic unit
# tests that stub only the modules they use.
# chain), plus websocket_api, are imported lazily inside the entry lifecycle
# functions below, not at module top level. They pull in aiohttp, yaml and
# several homeassistant.* submodules (auth, requirements, util.package,
# components.http/webhook, components.websocket_api, half a dozen helpers) that
# the entry-point wiring here never touches directly, so a top-level import
# would make importing this package require that whole stack — breaking
# hermetic unit tests that stub only the modules they use.
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigEntry
@@ -65,18 +68,33 @@ async def async_setup_server_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
startup. It runs as a config-entry background task — automatically cancelled
on unload. The secret webhook id and secret path are generated first, before
the update listener is registered, so those ``entry.data`` writes never
trigger a mid-setup reload.
trigger a mid-setup reload. The ``ha_mcp_tools/*`` WebSocket command surface
is registered up front, before any of that.
"""
# Imported lazily (see the import note) so the aiohttp / auth / requirements
# chain is pulled in only when an entry is actually set up.
from .coordinator import ServerVersionCoordinator
from .embedded_setup import async_bring_up_server, async_maybe_auto_update
from .ui_panel import async_register_ui_panel
from .websocket_api import async_register_commands
# The ha_mcp_tools/* WS commands are entry-agnostic read/search machinery
# over HA-core state (entity components, storage collections, lovelace,
# registries) — nothing in them reads the tools entry's hass.data, and HA
# core authenticates the connection while @require_admin gates each command.
# So the server entry registers them too (#2289): a server-entry-only
# install used to have no ha_mcp_tools/* commands at all, silently dropping
# every ha_search to the legacy path. Only the filesystem/YAML HA *services*
# stay tools-entry-only. Registered first, before anything that can fail or
# await, so a broken bring-up later never costs the install this surface;
# registration is idempotent, so a dual-entry install re-registering is
# harmless.
async_register_commands(hass)
_ensure_secrets(hass, entry)
# Bind the legacy OAuth root views synchronously here — before the slow
# background bring-up below — so they are live at boot (see the helper).
_prebind_legacy_oauth_views(hass, entry)
# Bind every applicable OAuth route synchronously here — before the slow
# background bring-up below — so it is live at boot (see the helper).
_prebind_oauth_views(hass, entry)
# Admin-only "Open Web UI" sidebar panel + proxy. Registered while the entry
# exists (its proxy returns 503 until the server is actually running), so the
@@ -190,30 +208,44 @@ async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> Non
await hass.config_entries.async_reload(entry.entry_id)
def _prebind_legacy_oauth_views(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Register the legacy OAuth root ``/authorize`` + ``/token`` views during
entry setup, before the background bring-up's (slow) package install.
def _prebind_oauth_views(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Register OAuth routes before the background bring-up's slow install.
An aiohttp route is only ever live if it is registered before Home Assistant
freezes its HTTP app at the end of startup. Binding these views from the
background bring-up task races HA reaching RUNNING: on a slow-install boot
the routes would register AFTER the freeze — never live until a restart —
and ``bind_legacy_views`` would see ``hass.is_running`` True and file a
restart repair that the restart cannot clear. Binding here, while the entry
is still setting up (``hass.is_running`` is False at boot), mirrors the
webhook-proxy add-on, which binds in its own ``async_setup_entry``. The
bring-up's ``async_register_webhook`` then reuses this already-bound provider.
the advertised metadata, scoped authorize/token, and DCR routes would never
become live until a restart. Bind the shared metadata/scoped pair for every
remote mode, DCR for none/ha_auth, and legacy's root aliases when credentials
are available. The later webhook registration reuses these bound views and
supplies the live per-request providers through ``hass.data``.
Only relevant when legacy is the configured mode and the webhook endpoint is
enabled (legacy OAuth guards that endpoint; with no webhook there is nothing
to protect). A route-ownership conflict with the webhook-proxy add-on is
swallowed here — the bring-up re-encounters it and files the user-facing
start-failed repair.
A legacy root-route ownership conflict with the webhook-proxy add-on is
swallowed here; the scoped pair remains live and background bring-up selects
its scoped-only provider.
"""
if str(entry.options.get(OPT_WEBHOOK_AUTH, "")) != WEBHOOK_AUTH_LEGACY:
return
if not bool(entry.options.get(OPT_ENABLE_WEBHOOK, True)):
return
auth_mode = str(entry.options.get(OPT_WEBHOOK_AUTH, ""))
if auth_mode not in (
WEBHOOK_AUTH_NONE,
WEBHOOK_AUTH_HA,
WEBHOOK_AUTH_LEGACY,
):
return
from .mcp_webhook import _register_metadata_views
from .oauth_autoapprove import bind_autoapprove_views
_register_metadata_views(hass)
bind_autoapprove_views(hass)
if auth_mode != WEBHOOK_AUTH_LEGACY:
from .oauth_dcr import bind_dcr_view
bind_dcr_view(hass)
return
client_id = entry.data.get(DATA_OAUTH_CLIENT_ID)
client_secret = entry.data.get(DATA_OAUTH_CLIENT_SECRET)
signing_key = entry.data.get(DATA_OAUTH_SIGNING_KEY)
@@ -221,16 +253,9 @@ def _prebind_legacy_oauth_views(hass: HomeAssistant, entry: ConfigEntry) -> None
# _ensure_secrets mints these whenever legacy mode is configured; a gap
# means a partial config — let the bring-up path surface it.
return
from .mcp_webhook import _register_metadata_views
from .oauth_legacy import LegacyOAuthRouteConflict, bind_legacy_views
with suppress(LegacyOAuthRouteConflict):
# Register the RFC 8414/9728 discovery views alongside the root
# /authorize + /token views, both at setup time, so the discovery
# doc's resource_metadata URL resolves at boot for RFC-compliant
# clients — not just the root views. Both are idempotent, so the
# bring-up's async_register_webhook reuses them.
_register_metadata_views(hass)
bind_legacy_views(hass, client_id, client_secret, signing_key)