Parameterize the disk usage endpoint as /host/disks/{disk}/usage so a
supervisor mount can be asked for its own storage figures. "default" still
means the system disk and keeps its existing response byte for byte; any
other value names a mount, which must exist and be active. The reserved
target wins over a mount of the same name, which the mount name pattern
permits.
Usage comes from the mount's own filesystem, with used derived as total
minus free so that reserved space counts as used, as it does for the system
disk.
A mount reports totals only by default. The directory walker recurses
regardless of max_depth and only gates whether children appear in its
output, so walking a whole network mount to report a single number would
cost many round trips for nothing; at depth 0 it is skipped outright rather
than called. Depth otherwise means what it means for the system disk, where
level 1 is the labeled known paths that a mount does not have, so a mount's
own subdirectories start at level 2. When a breakdown is produced, whatever
the walk cannot attribute is reported as an "other" child, which keeps every
node's children summing to its used_bytes.
Probes are deliberately not cut short. A caller showing a loader is better
served by a real answer than a fast failure, so the timeout is only a
backstop against a probe that never returns, and a slow one is confined to
its executor thread rather than blocking the rest of the API. Concurrent
callers asking for the same mount at the same depth share one probe instead
of each parking a thread on identical work.
The hand-wrapped warning call in #7122 does not match ruff's formatting,
so the ruff-format CI check (which runs on all files) has been failing on
main and on every branch cut from it since 2026-08-12.
* Block Core hassio_auth endpoints from the add-on proxy
The API security blacklist is meant to stop add-ons from reaching Core's
"hassio" endpoints through the /core/api and /homeassistant/api proxy, but
the pattern only matched "hassio/" (with a trailing slash). Core's auth
endpoints are served at /api/hassio_auth and /api/hassio_auth/password_reset,
so they slipped past the blacklist and were passed through to the proxy.
The proxy authenticates upstream to Core as the Supervisor user, and Core's
HassIOPasswordReset only checks that the caller is the Supervisor user (no
owner check). As a result an add-on with homeassistant_api access could reach
the password-reset endpoint through the proxy and reset any user's password,
including the owner.
Widen the boundary after "hassio" to match both the loopback ("hassio/...")
and the auth endpoints ("hassio_auth...") so all hassio-prefixed Core
endpoints are blocked, and extend the blacklist test to cover them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJ56EftdwXvqw2p3vjYS1z
* Refuse to proxy Core hassio endpoints (defense in depth)
Add a redundant guard in the Home Assistant API proxy so an add-on can never
reach Core's Supervisor-only "hassio" endpoints (hassio_auth,
hassio_auth/password_reset, the hassio loopback) through the proxy. These run
as the Supervisor user on Core, so forwarding them would let an add-on reset
arbitrary user passwords.
The security middleware blacklist already blocks these paths; this guard sits
at the proxy itself so the proxy cannot become a confused deputy if that
blacklist ever regresses. The two checks are independent by design.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XJ56EftdwXvqw2p3vjYS1z
* Check access before denylist
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Add websocket addon-to-app compatibility shim
Update websocket metadata and app update command terminology from addon to
app, while preserving V1 compatibility through a single send-time shim when
SUPERVISOR_WEBSOCKET_V2_API is disabled.
Also add a tests-only Ruff override for PLR0917 so pytest fixture-heavy test
functions remain lint-clean.
Fixes#7031
* Fixes from feedback
* Rename resolution addon issue types and check slugs to app-based naming
Renames the following for consistency with the new apps terminology:
- Issue types: deprecated_addon -> deprecated_app,
deprecated_arch_addon -> deprecated_arch_app,
detached_addon_missing -> detached_app_missing,
detached_addon_removed -> detached_app_removed
- Check slugs: addon_pwned -> app_pwned,
deprecated_addon -> deprecated_app,
deprecated_arch_addon -> deprecated_arch_app,
detached_addon_missing -> detached_app_missing,
detached_addon_removed -> detached_app_removed
Full backward compatibility is maintained:
- REST API V1 (root) returns legacy names and accepts legacy slugs
- REST API V2 (/v2) returns and accepts new names only
- WebSocket events use legacy names unless SUPERVISOR_WEBSOCKET_V2_API
feature flag is enabled
- resolution.json files with legacy check slugs are automatically
migrated to new names on load
Fixes#7029
* Apply suggestions from code review
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Address review nits for resolution compatibility maps
* Update supervisor/resolution/const.py
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
* Respect user-overridden Core image for install and update
The override_image setting was only honored by load(): the landingpage
install, the initial Core install and Core updates always pulled the
image from the update information and wrote it back to the Home
Assistant config afterwards, discarding the user override on the next
install or update.
Add a HomeAssistant.install_image property that returns the
user-overridden image if set and the image from the update information
otherwise. Use it in the landingpage install, Core install and Core
update paths, and persist that same image afterwards so the override
is kept. Version resolution is unchanged. With an override set, a
fresh install now pulls <image>:landingpage from the overridden image
as well.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Mark system unsupported when a custom Core image is used
As raised in review, running a Home Assistant Core fork should be
visible: add an evaluation that marks the system unsupported when the
configured Core image differs from the default image for the machine.
The evaluation compares the configured image against the default image
instead of checking the override_image flag, so it also catches images
recorded through container adoption or manual configuration edits
where the flag is not set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Capture install image once per Core install and update
As raised in review, the install image was read separately for the
image pull and for persisting it to the Home Assistant config. If the
image option changes while an install or update job is running, the
pulled and the persisted image could diverge. Capture the value once
per install attempt respectively once per update job (covering the
rollback path) and use it for both the pull and set_image().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Gracefully end log stream when journal gateway connection is lost
When systemd-journal-gatewayd is stopped while a client follows logs
(e.g. on host reboot with the log viewer open), aiohttp raises
ClientPayloadError and advanced_logs_handler converted it to an
APIError. For the /supervisor/logs endpoints this got logged as an
unexpected error with a full traceback and captured to Sentry on every
occurrence (#7103, SUPERVISOR-1FHT).
Once the streaming response has started, an error response can no
longer be delivered anyway, so treat a lost connection to
systemd-journal-gatewayd like a client-side disconnect and end the
stream gracefully. The APIError is still raised when the connection is
lost before any data was sent to the client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Treat unreachable systemd-journal-gatewayd as a known API error
Manual testing of the previous commit (killing systemd-journal-gatewayd
while Supervisor is running) showed that every log API request hitting
the dead gateway logs an "Unexpected error during API call" traceback
and captures HostServiceError to Sentry (SUPERVISOR-K8C), in addition
to the ERROR already logged at the raise site in journald_logs().
Make HostServiceError inherit from APIError as well, following the
HostContainerLogEpochError precedent, so the api_process decorators
return a plain 400 response without the redundant traceback and Sentry
capture. Also treat it like HostNotSupportedError in the supervisor
logs fallback wrapper: fall back to Docker container logs with a
warning instead of an exception log plus Sentry capture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use dedicated exception for journal gateway connection errors
HostServiceError is also raised by ServiceManager for systemd units
called through the API (e.g. /os/config/sync), where inheriting from
APIError would hide genuine service breakage from Sentry. Introduce
HostJournalGatewaydConnectionError subclassing HostServiceError and
APIError, and raise it for the gatewayd connection failure only, as
suggested in the PR review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Ensure test_restart_returns_once_requests_rejected waits for the
background stop task to reach loop.stop while it is still patched.
This prevents the real loop.stop from firing after the patch context
exits, which could surface as a cleanup error:
RuntimeError: Event loop stopped before Future completed.
The test started the _adjust_system_datetime() task and used a fixed
asyncio.sleep(0.1) before emitting the systemd-timesyncd PropertiesChanged
signal that wait_for_active_state() waits on. On a busy CI runner the task
could still be short of installing its D-Bus signal subscription when the
signal fired, so the signal was broadcast to no subscriber and lost.
wait_for_signal() then blocked until the 10s pytest-timeout killed it.
Replace the sleep with a deterministic handshake: the SystemdUnit mock now
exposes an active_state_read Event, set whenever ActiveState is queried.
Inside wait_for_active_state() that read happens only after the
PropertiesChanged subscription is installed, so awaiting the Event before
emitting guarantees the signal cannot be missed.
Verified under full CPU load: the old sleep-based sync point timed out 10/10,
the Event-based version passed 30/30.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Store repository URLs may contain userinfo credentials (e.g.
https://x-access-token:<token>@github.com/...) so Supervisor can clone
private app repositories. These URLs currently end up verbatim in
Sentry events: in captured exception messages, in log messages sent as
breadcrumbs, and in the store repositories context that is attached to
every event, leaking the embedded secrets. Sentry events for the
duplicate-repository-add error (SUPERVISOR-1JYE) show GitHub personal
access tokens of affected installations.
Add sanitize_url_credentials() alongside the existing sanitize_url()
and use it in the before_send filter to remove credentials from
exception values, log entries, breadcrumbs and the store repositories
context. This also covers messages produced outside Supervisor, like
git's stderr in GitPython exceptions.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Enter STOPPING state before Supervisor restart API response
The Supervisor accepts new API requests in the window after responding
to /supervisor/restart: restart() schedules Core.stop() as a task and
returns immediately, so the state only changes to stopping after the
restart response has been sent. A request accepted in that window keeps
running while the Supervisor shuts down, and when stop() tears down the
API server after the 10 second stage 1 timeout the connection is
dropped without a response - the client sees EOF instead of an error.
This is what made the CI restore step flaky (see #7084 for the CI-side
fix): the restore request landed on the old, dying instance.
Make restart() transition to STOPPING before returning, via a new
Core.begin_stop() which contains the transition part previously at the
start of Core.stop(). The system validation middleware already rejects
requests outside of STARTUP/RUNNING/FREEZE, so any request arriving
after the restart response now gets a clear "System is not ready" error
instead of possibly being accepted and killed. Since the state can now
already be STOPPING when stop() runs, its re-entry guard is changed
from a state check to an explicit flag.
Supervisor.update() schedules the same stop task but is left unchanged:
entering STOPPING before update() returns would suppress the final job
progress event to Home Assistant (WebSocket messages are dropped in
CLOSING_STATES), regressing update progress reporting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Allow stop() retry when begin_stop() fails
Address review feedback: stop() set _stop_initiated before awaiting
begin_stop(), so an exception or cancellation during the state
transition (e.g. a failing config write in _update_last_boot) would
leave the flag set and turn every later stop() call into a no-op,
with no way to retry the teardown.
Reset the flag and re-raise when begin_stop() fails. Nothing has been
torn down at that point, so a retry is safe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use a stopping_complete event instead of begin_stop()
Address review feedback questioning the extra infrastructure: instead
of splitting the state transition out of stop() (begin_stop()) and
guarding re-entry with an explicit flag, have stop() take an optional
stopping_complete event which is set once the STOPPING state is
entered, following the same pattern as backup/restore's
validation_complete. Core.stop() keeps its original structure and
re-entry semantics, and restart() waits for the event before
returning.
Should the stop task fail ahead of the state transition (only
possible through a failing config write in _update_last_boot()), the
event is never set and the restart request runs into the client
timeout - an accepted trade-off to keep restart() simple.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Make app port-conflict repairs always fixable
* Handle default app ports in conflict repair suggestions
* Test execute-start-only conflict for apps without ports
Adding a repository that is already in the store raised a plain
StoreError. Since that is not an APIError, the api_process decorator
logs it as an unexpected error and reports it to Sentry, where it is
one of the most frequent issues (SUPERVISOR-1JYE, ~8000 affected
installations in 90 days). The events are ordinary client actions:
add-on setup flows and documentation links re-submitting a repository
the user already has, plus third-party automation re-adding its
repositories on every add-on start.
Introduce StoreRepositoryAlreadyAddedError, which is both a StoreError
and an APIConflict with an error key and message template, so the
request fails with a structured 409 Conflict response and no Sentry
report.
Fixes SUPERVISOR-1JYE
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Pull Core HTTP server config over the Unix socket
Supervisor learns the port and SSL state of Core's HTTP server only when
the hassio integration pushes them via the Supervisor options API during
its setup. Supervisor's own startup API and frontend checks can run before
that push lands, so they probe Core with stale or default connection
parameters.
Pull the connection parameters instead: fetch Core's live HTTP server
configuration from /api/core/http_config (home-assistant/core#176976,
socket-only) whenever a connection to Core is established, and update the
stored port and SSL settings from it. On older Core versions without the
endpoint the pushed options remain in effect.
Core also reports the addresses its HTTP server binds to, kept in the new
runtime-only http_server_host attribute. The frontend verification after an
update is skipped when Supervisor cannot reach that bind: only a bind
including 0.0.0.0 or the container IP is reachable, since Supervisor
connects over IPv4 and Core's v6-only sockets (IPV6_V6ONLY) do not accept
IPv4 connections.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reset known HTTP server binds when the config is unavailable
Review feedback: _update_http_config() left http_server_host untouched when
the HTTP config could not be fetched, so after a downgrade to a Core without
the endpoint the frontend reachability decision could still use the previous
Core's bind addresses. Reset it to unknown instead; unknown binds mean the
verification probes simply run, as they did before the pull existed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Report real OS version to Core versions consuming version_pending
The /os/info endpoint reports an installed update pending activation as
the current version so that Core update entities unaware of
version_pending don't offer the update again. Since
home-assistant/core#177155 Core uses version_pending to determine the
OS update state, so newer Core versions should get the real current
version again.
Limit the compat shim to Core versions predating that support. The Core
PR merged 2026-07-24 04:21 UTC, after that day's 02:00 UTC nightly
build, so the first nightly containing it is 2026-07-25's
(2026.8.0.dev202607250xxx).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use exact first nightly version for the version_pending gate
The 2026-07-25 nightly (2026.8.0.dev202607250310) is the first build
containing home-assistant/core#177155: the 2026-07-24 nightly was built
from a commit predating the merge (verified via commit ancestry of the
builder workflow runs). Replace the midnight floor with the actual
published nightly version, matching the CORE_UNIX_SOCKET_MIN_VERSION
precedent of using the exact nightly stamp.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Mark backup/restore app stage names as breaking
Rename backup and restore job stages from addon/addons to app/apps, including delta and await restart stages, and update manager and tests accordingly.
* Add legacy backup stage mapping and fix enum typo
Map backup/restore job stage names to legacy websocket/REST v1 values while preserving v2 values, add API tests for both modes, and rename COPY_ADDITONAL_LOCATIONS to COPY_ADDITIONAL_LOCATIONS.
* Check for local data before adding or updating a mount
Adding a mount whose media/share directory already holds local data
(e.g. Frigate recordings written before network storage was set up, or
after systemd tore down the bind mount, see #7013) only failed at the
bind-mount step: the data mount was already mounted and the mount
registered in memory but never persisted, leaving a half-created mount
until the next Supervisor restart. Updating an existing mount in that
state even unmounted the working data mount first, just to fail on the
non-empty bind target afterwards.
Check the mount's target directories for local data upfront and fail
the add/update before anything is touched. Paths that are already mount
points are skipped since they get unmounted before reuse.
If mounting still fails halfway (e.g. the bind mount unit fails to
start), roll back by unmounting the new units instead of keeping the
half-created mount in the list.
Also pass the missing mount name argument to the debug log statement in
create_mount.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Make mount target errors translatable
Raise the mount target error conditions as dedicated exceptions with an
error_key, message template and extra fields following the established
pattern, so the frontend can localize them and show the mount name and
path separately:
- MountTargetNotEmptyError (mount_target_not_empty_error), raised both
by the upfront local data check and the mount-time non-empty check
- MountTargetNotDirectoryError (mount_target_not_directory_error) for
a target path that exists but is not a directory
Both remain subclasses of MountInvalidError so existing exception
handling keeps working. Frontends without the translation strings fall
back to the English message as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Raise not-a-directory error from upfront local data check
The upfront check collapsed a target path that exists but is not a
directory into MountTargetNotEmptyError. Distinguish the two conditions
like the mount-time check does and raise MountTargetNotDirectoryError
for the non-directory case.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Offer reset for corrupt store repositories on load
When a saved git store repository loads but fails validation (for
example because the local clone lost its repository configuration file),
the only suggestion offered was to remove the repository. Removal is
refused when an installed add-on still uses that repository, leaving the
user with no way to recover a corrupt local copy.
Offer a reset alongside removal so the repository can re-clone and
self-heal. The reset fixup runs automatically, matching how the pull
path already handles corruption. This branch is only reached for
repositories that validated before, so a now-invalid copy is most likely
local corruption that re-cloning fixes.
* Re-validate repository after reset
A reset only recovers a corrupt local copy of a repository. If the
freshly cloned repository still doesn't validate, the problem is
upstream (for example the repository configuration was removed). In that
case the reset fixup would previously consider the reset successful and
dismiss the issue, hiding a persistent problem and re-cloning on every
run.
Re-validate the repository after a reset and raise when it is still
invalid, so the issue stays surfaced to the user instead of being
silently dismissed.
* Report invalid repository after reset as a known error
When a manual repository reset re-clones successfully but the repository
still fails validation, the failure mode is known: it isn't a valid
add-on repository. Raising StoreRepositoryUnknownError reported this as
an unknown error (HTTP 500), which is misleading for users triggering a
reset through the API.
Raise StoreInvalidAppRepo instead, which carries a clear message and
maps to a 400. It is still a StoreError, so the reset fixup keeps the
issue surfaced as before.
* Stop auto-retrying reset when repository stays invalid
When a reset re-clones a repository successfully but it still fails
validation, the problem is upstream and retrying won't help. The reset
fixup runs automatically on every hourly resolution healthcheck as long
as a reset suggestion exists, which would re-clone such a repository
every hour with no chance of recovery.
Drop the reset suggestion in that case so the auto-retry stops, while
leaving the issue and its remove suggestion in place so the user stays
informed and can still act.
* Update supervisor/store/repository.py
* Adapt reset-suggestion dismissal to Suggestion-typed process_fixup
Main changed process_fixup to receive the applied Suggestion object
instead of a reference string (#6916). The branch predates that change
and still used the removed reference name, which the rebase merged
cleanly but left as an undefined variable.
Since the caller passes the exact suggestion being applied, dismiss it
directly instead of scanning all_suggestions for a matching reference.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Stefan Agner <stefan@agner.ch>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Lower app config advisory log level for non-local store apps
App config deprecation and misconfiguration advisories were logged at
warning level for every app in every store repository on each store
reload. Because store data is validated for all available apps, not just
installed ones, regular users saw "Please report this to the maintainer"
warnings for apps they had never installed and could not fix, which is
confusing and backwards for that audience.
Route these advisories through an injected logging callable so the store
data layer can pick the level that matches who can act on them. Apps from
the local repository (authored by the user) and any app on the dev channel
(a developer testing store apps) still log at warning level; everything
else logs at debug, keeping the messages available for troubleshooting
without spamming regular users' logs. Installed apps re-validated from
apps.json on every boot now also log at debug, since they were already
migrated and their advisories logged when first read from the store.
Reword the messages to state the deprecation or misconfiguration as a fact
instead of asking the reader to report it to the maintainer, since the
remaining warning-level audiences are the ones who can address it directly.
The hard rejection for a dynamic ingress port conflict keeps its existing
wording, as it aborts loading regardless of channel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Also warn for installed apps from custom repositories
The initial change kept app config advisories at warning level only for
the local repository and the dev channel, silencing them for every store
app on stable/beta. Review feedback pointed out that this also hides the
warning for installed apps whose maintainer has moved on - often thin
wrapper apps in custom repositories where the warning is the user's only
heads-up before the app breaks.
Warn for installed apps from custom (non-built-in) repositories as well.
Apps from the curated built-in stores (Core, Community, ESPHome, Music
Assistant) stay quiet even when installed, since deprecations there are
fixed via PRs and are not actionable for the user. Uninstalled apps
remain quiet everywhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reload fixup for a failed media/share mount (#6938) brought the data
mount back but left the media/share directory empty until the user
re-sent the mount config via the API.
systemd adds an implicit RequiresMountsFor= dependency from the bind
mount unit to the data mount unit through its What= path (Requires= +
After=). Stopping or restarting a failed data mount therefore unmounts
the bind mount as well. The MountManager's BoundMount bookkeeping does
not track this: after a successful reload it only re-created the bind
mount when it had been set up as an emergency fallback during load.
Always re-create the bind mount after reloading a mount instead. This
also covers the case where the data mount recovered on its own while
the bind mount was already torn down by an earlier failed restart.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>