429 files
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user