Skip to content
Open
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
718968a
fix(addon): use direct Supervisor API for app tools
kingpanther13 Aug 24, 2026
f86f58e
test(internal): add beta HAOS compatibility lanes
kingpanther13 Aug 25, 2026
93ff1cf
fix(addon): harden Supervisor beta coverage
kingpanther13 Aug 25, 2026
8f9bbcb
fix(addon): validate Supervisor path identifiers
kingpanther13 Aug 25, 2026
c116cc2
test(e2e): align Supervisor coverage contracts
kingpanther13 Aug 25, 2026
c184b74
test(haos): preserve beta lane diagnostics
kingpanther13 Aug 25, 2026
4677d8a
test(haos): enforce beta lane contracts
kingpanther13 Aug 25, 2026
f321818
fix(haos): preserve Supervisor failure detail
kingpanther13 Aug 25, 2026
af9aee0
fix(haos): harden beta compatibility coverage
kingpanther13 Aug 25, 2026
76d1192
fix(addon): harden Supervisor compatibility paths
kingpanther13 Aug 25, 2026
ebe7ee7
fix(addon): clarify Supervisor failure recovery
kingpanther13 Aug 25, 2026
2fa59cf
fix(addon): preserve unknown write outcomes
kingpanther13 Aug 25, 2026
aada5a2
docs(addon): clarify app management contracts
kingpanther13 Aug 25, 2026
902deba
docs(e2e): align app coverage claims
kingpanther13 Aug 25, 2026
ae18882
fix(addon): preserve in-app self-update
kingpanther13 Aug 25, 2026
0bceb17
fix(addon): report unsupported self-update
kingpanther13 Aug 25, 2026
b8ae145
fix(addon): preserve write outcome safety
kingpanther13 Aug 25, 2026
da19506
fix(addon): classify Core bridge ambiguity
kingpanther13 Aug 25, 2026
2cac226
fix(addon): preserve ambiguous Supervisor writes
kingpanther13 Aug 25, 2026
95cd79a
fix(addon): retain uncertain server outcomes
kingpanther13 Aug 25, 2026
e4fcad9
test(haos): pin Supervisor error frames
kingpanther13 Aug 25, 2026
5efcd34
fix(addon): tighten Supervisor contracts
kingpanther13 Aug 25, 2026
fdaba94
fix(addon): reject ambiguous lifecycle inputs
kingpanther13 Aug 25, 2026
76b231e
test(haos): validate Supervisor result frames
kingpanther13 Aug 25, 2026
f4b40e7
fix(addons): avoid Supervisor error type collision
kingpanther13 Aug 25, 2026
7161e9a
test(haos): retry Supervisor setup readiness
kingpanther13 Aug 25, 2026
8ff5c5a
fix: address PR review findings
kingpanther13 Aug 25, 2026
4c86717
style: apply Ruff formatting
kingpanther13 Aug 25, 2026
c965216
test(haos): retry initial Supervisor readiness
kingpanther13 Aug 25, 2026
80c9e9c
test(haos): simplify Supervisor readiness retry
kingpanther13 Aug 25, 2026
05c86df
fix(addons): preserve direct read timeout code
kingpanther13 Aug 25, 2026
d3e565b
test(e2e): restore HAOS running-state helper
kingpanther13 Aug 25, 2026
a99a04b
test(e2e): account for beta HAOS marker skip
kingpanther13 Aug 25, 2026
881069b
fix(addons): make error paths explicit for CodeQL
kingpanther13 Aug 25, 2026
873c36c
fix(haos): enforce polling deadlines
kingpanther13 Aug 25, 2026
9834dac
test(haos): use live app endpoint contracts
kingpanther13 Aug 25, 2026
18cbcb7
style(haos): format readiness helper
kingpanther13 Aug 25, 2026
303d33d
fix(haos): bound reconnect to readiness deadline
kingpanther13 Aug 25, 2026
05568ca
test(haos): expect deadline-aware reconnect
kingpanther13 Aug 25, 2026
87f29d7
test(haos): require bounded reconnect deadline
kingpanther13 Aug 25, 2026
da51639
test(haos): scope ingress probes by runtime
kingpanther13 Aug 25, 2026
952b04f
test(haos): cover in-app response transforms
kingpanther13 Aug 25, 2026
72a258a
fix(haos): bound supervisor websocket receives
kingpanther13 Aug 25, 2026
baabc93
fix(haos): reject expired supervisor commands
kingpanther13 Aug 25, 2026
25deea4
fix(haos): bound supervisor websocket sends
kingpanther13 Aug 25, 2026
d39fbc9
fix(haos): distinguish queued websocket sends
kingpanther13 Aug 25, 2026
d0f5ce3
fix(haos): bound beta image reconnects
kingpanther13 Aug 25, 2026
05f4686
fix(haos): enforce image build deadlines
kingpanther13 Aug 25, 2026
e1e43e7
style(tests): sort supervisor helper imports
kingpanther13 Aug 25, 2026
e147a94
fix(haos): reject post-deadline websocket results
kingpanther13 Aug 25, 2026
e1f3662
fix(ci): simplify supervisor deadline checks
kingpanther13 Aug 25, 2026
cb6a6d5
test(haos): wait for Node-RED ingress readiness
kingpanther13 Aug 25, 2026
598dc48
test(haos): bound Node-RED readiness retries
kingpanther13 Aug 25, 2026
e79ea30
fix(addons): harden Supervisor REST failures
kingpanther13 Aug 25, 2026
9a8b68c
fix(addons): complete Supervisor error hardening
kingpanther13 Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions .github/workflows/haos-e2e-embedded-beta-tests.yml

Large diffs are not rendered by default.

381 changes: 381 additions & 0 deletions .github/workflows/haos-e2e-inaddon-beta-tests.yml

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,9 @@ markers = [
"error_handling: error handling and edge case tests",
"cleanup: tests that create entities needing cleanup",
"performance: performance measurement and regression tests",
"external_only: skips on HAOS inaddon tier (server runs in a separate addon container, so in-process env monkeypatching doesn't reach it)",
"external_only: in-process-server tiers only; skips inaddon, stdio, container-embedded, and HAOS-embedded servers",
"inaddon_only: only runs on HAOS inaddon tier",
"beta_haos_only: beta HAOS image lanes with Supervisor/Core version expectations configured",
]
asyncio_mode = "auto"

Expand Down
82 changes: 42 additions & 40 deletions src/ha_mcp/client/rest_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,8 +704,9 @@ async def _is_supervised_install(self) -> bool:
async def get_addon_logs(self, slug: str, lines: int | None = None) -> str:
"""Fetch an add-on's container logs.

Branch on ``is_running_in_addon()`` (which keys off ``SUPERVISOR_TOKEN``
in env): inside the add-on container goes directly to the Supervisor
Branch on ``is_running_in_addon()``, which requires a truthy
``SUPERVISOR_TOKEN`` and excludes ``HA_MCP_EMBEDDED``: inside the app
(add-on) container goes directly to the Supervisor
REST API at ``http://supervisor/addons/{slug}/logs`` with the
Supervisor token. The HA Core proxy at
``/api/hassio/addons/{slug}/logs`` rejects this token+path combination
Expand All @@ -730,8 +731,9 @@ async def get_addon_logs(self, slug: str, lines: int | None = None) -> str:
Raises:
HomeAssistantAuthError: 401 response, or ``SUPERVISOR_TOKEN`` empty
at call time on the addon branch.
HomeAssistantAPIError: 403 (role too low — addon needs hassio_role
``manager``), 404 (unknown slug), or other non-2xx. The
HomeAssistantAPIError: 403 (unrecognized token, missing
``hassio_api``, or insufficient ``hassio_role``), 404
(unknown slug), or other non-2xx. The
``status_code`` attribute lets callers map to specific
suggestions.
HomeAssistantConnectionError: Network, timeout, or transport error.
Expand Down Expand Up @@ -775,25 +777,28 @@ async def _supervisor_logs_get(self, path: str, lines: int | None = None) -> str

``path`` is everything between ``http://supervisor/`` and ``/logs``:

- ``"addons/<slug>"`` for add-on container logs
- ``"addons/<slug>"`` for app (add-on) container logs
- ``"<service>"`` (where service ∈ {supervisor, host, core, dns, audio,
cli, multicast, observer}) for system-service logs

``lines`` maps to the endpoint's ``?lines=`` journald-window query
param; omitted → Supervisor's 100-line default window.

Bypasses ``HomeAssistantClient.httpx_client`` because the Supervisor
endpoint uses a different base URL (``http://supervisor``) and a
different token (``SUPERVISOR_TOKEN``) than HA Core REST. Both
endpoints require the addon's ``hassio_role`` to be ``manager`` (not
``default``); a ``default`` role gets a 403 here — see #1116.
Bypasses ``HomeAssistantClient.httpx_client`` because that client targets
Home Assistant Core through ``http://supervisor/core/api``, while logs
belong to Supervisor at ``http://supervisor``. Both clients use the same
``SUPERVISOR_TOKEN``; the Core proxy requires ``homeassistant_api``, while
system-service and arbitrary app-log paths require ``hassio_api`` and
``hassio_role: manager``. The recognized-app-token exception is
``/addons/self/logs``.

Raises:
HomeAssistantAuthError: ``SUPERVISOR_TOKEN`` absent at call time,
or 401 from Supervisor.
HomeAssistantAPIError: 403 (role too low — distinct branch with
role hint), 404, other 4xx/5xx. Tries to parse Supervisor's
``{"result":"error","message":"..."}`` JSON envelope before
HomeAssistantAPIError: 403 (unrecognized token, missing
``hassio_api``, or insufficient role), 404, other 4xx/5xx.
Parses Supervisor's ``{"result":"error","message":"..."}``
JSON envelope before
falling back to text body / reason phrase / placeholder.
HomeAssistantConnectionError: Timeout or transport error, with
distinct messages so callers can tell them apart.
Expand Down Expand Up @@ -839,18 +844,19 @@ async def _supervisor_logs_get(self, path: str, lines: int | None = None) -> str
if response.status_code == 401:
raise HomeAssistantAuthError(f"Invalid Supervisor token for /{path}/logs")
if response.status_code == 403:
# Distinct from 401: token is valid but addon's hassio_role isn't
# high enough. Most-likely cause for this exact endpoint at the
# time #1116 surfaced (defaultmanager bump in addon config.yaml
# is the same-PR companion fix).
# Supervisor uses 403 for an unrecognized app token, missing
# hassio_api permission, or a hassio_role that cannot access this
# endpoint. The default-to-manager role bump fixed #1116, but it is
# not the only possible cause.
logger.warning(
"Supervisor returned 403 for /%s/logs — addon hassio_role may "
"be too low (need 'manager')",
"Supervisor returned 403 for /%s/logs — check token, hassio_api, "
"and hassio_role (need 'manager')",
path,
)
raise HomeAssistantAPIError(
f"Supervisor forbids /{path}/logs (403) — addon's hassio_role "
"may be 'default'; need 'manager' or higher",
f"Supervisor forbids /{path}/logs (403) — token may be unrecognized, "
"app may lack hassio_api, or hassio_role may not allow this endpoint "
"(manager required)",
status_code=403,
response_data={"path": path},
)
Expand All @@ -873,14 +879,15 @@ async def _supervisor_logs_get(self, path: str, lines: int | None = None) -> str
async def _get_addon_logs_via_supervisor(
self, slug: str, lines: int | None = None
) -> str:
"""Fetch add-on container logs directly from Supervisor's REST API.
"""Fetch app (add-on) container logs directly from Supervisor REST.

Distinct from ``tools_bug_report._fetch_addon_logs``: that helper is
hardcoded to ``/addons/self/logs`` and silently swallows failures
(it's an aux-data fetch for bug reports, fine to skip on error). This
helper takes arbitrary slugs and surfaces failures as exceptions
because callers (``ha_get_logs(source="supervisor", slug=...)``) need
them. Both endpoints require ``hassio_role: manager``.
Distinct from ``tools_bug_report._fetch_addon_logs``: that auxiliary
helper uses the app self-service path ``/addons/self/logs`` and may skip
failures. This helper takes arbitrary slugs and surfaces failures because
callers (``ha_get_logs(source="supervisor", slug=...)``) need them.
Arbitrary app-log slugs require a recognized app token with ``hassio_api``
and ``hassio_role: manager``; ``/addons/self/logs`` bypasses the role and
API-permission checks but still requires a recognized app token.

Delegates to ``_supervisor_logs_get`` so error handling stays in
lockstep with ``_get_system_service_logs``.
Expand All @@ -895,26 +902,21 @@ async def _get_system_service_logs(
``service`` must be one of the eight Supervisor-managed services:
``supervisor``, ``host``, ``core``, ``dns``, ``audio``, ``cli``,
``multicast``, ``observer``. Caller is responsible for validating
``service`` against the allowed set; this helper does no validation
and will raise ``HomeAssistantAPIError`` on any unknown path (404).
``service``; this helper performs no validation, and unsupported paths
are rejected by the selected direct-Supervisor or Core-proxy route.

Branch on ``is_running_in_addon()`` — mirror of ``get_addon_logs``:
inside the addon container goes directly to Supervisor at
inside the app (add-on) container goes directly to Supervisor at
``http://supervisor/{service}/logs`` with the Supervisor token
(``hassio_role: manager`` required). On non-addon installs (Docker
(``hassio_api`` and ``hassio_role: manager`` required). On non-app
installs (Docker
without Supervisor, pyinstaller, pip pointing at a normal HA URL),
falls back to the HA Core proxy at ``/api/hassio/{service}/logs``.

All seven slugs are whitelisted in HA Core's hassio proxy
All eight service slugs are whitelisted in HA Core's hassio proxy
(``homeassistant/components/hassio/http.py`` — ``PATHS_ADMIN``), so
an admin LLA is sufficient to reach any of them from outside the
addon.

Closes #1260: pre-fix this method had only the addon-direct branch,
so non-addon installs (the Docker image, uvx ha-mcp, etc.) hit the
``SUPERVISOR_TOKEN``-absent fail-fast in ``_supervisor_logs_get`` for
every service, while the sibling ``source="supervisor"`` (addon
logs) call kept working through its own Core-proxy fallback.
app.
"""
if is_running_in_addon():
return await self._supervisor_logs_get(service, lines=lines)
Expand Down
35 changes: 13 additions & 22 deletions src/ha_mcp/client/supervisor_client.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
"""Shared factory for direct-Supervisor httpx clients.

Three call sites in the codebase talk directly to the Home Assistant
Supervisor REST API at ``http://supervisor`` rather than through
``HomeAssistantClient.httpx_client`` (which is bound to HA Core, not the
Supervisor — different base URL, different token, different role gate):
Several code paths call the Home Assistant Supervisor REST API directly with
a short-lived client configured for the Supervisor base URL and
``SUPERVISOR_TOKEN``. Callers cover log collection, app (add-on) management
and restart, backup operations, and screenshot-engine discovery.

- :meth:`ha_mcp.client.rest_client.HomeAssistantClient._supervisor_logs_get`
— fetches addon and system-service logs
- :func:`ha_mcp.tools.tools_bug_report._fetch_addon_logs` — bundles ha-mcp's
own addon logs into a bug-report payload
- :func:`ha_mcp.settings_ui._handlers_server._restart_addon` — POSTs
``/addons/self/restart`` from the settings UI

All three share the same boilerplate (base URL, ``Authorization: Bearer
${SUPERVISOR_TOKEN}`` header), so this module supplies a single factory and
keeps the three sites consistent.
These calls cannot use ``HomeAssistantClient.httpx_client``, which targets
Home Assistant Core rather than Supervisor. In app mode it carries the same
token value, but requests use a different base URL and authorization surface.
This module centralizes the direct transport setup.
"""

from __future__ import annotations
Expand Down Expand Up @@ -56,14 +50,11 @@ def make_supervisor_httpx_client(

Raises:
RuntimeError: ``SUPERVISOR_TOKEN`` is unset or empty in the
environment. Each call site has its own absent-token policy
(a rich :class:`HomeAssistantAuthError`, a silent ``""``
return, or a 400 ``JSONResponse``) that does not share a
common shape, so the factory cannot translate. Detecting the
absence at construction time prevents a malformed
``Authorization: Bearer `` header from being read as a token
rejection by Supervisor, which would mask the missing-env-var
root cause.
environment. Callers must either gate construction on token
presence or translate this exception for their response surface.
Detecting it here prevents a malformed
``Authorization: Bearer `` header from masking the missing
environment variable as a token rejection from Supervisor.

Note:
``SUPERVISOR_TOKEN`` is read from env at construction time and
Expand Down
Loading
Loading