Commit Graph
322 Commits
Author SHA1 Message Date
Stefan Agner 0a23c537ac Merge branch 'main' into network-v2-state-config-split 2026-09-16 14:24:54 +02:00
Mike Degatano 0e7c4175c5 Block start/restart/rebuild/update APIs while Supervisor is not fully running (#7194)
* Block start/restart/rebuild/update APIs while Supervisor is not fully running

Supervisor boots apps and Home Assistant Core in a specific order, and
replays a similarly ordered sequence while restoring a backup (during
which it is in the freeze state). Calling start/restart/rebuild/update
via the API while one of those sequences is still in progress risks
running it out of order, or blocking a scheduled start from that
sequence outright via job concurrency.

Add a require_running_system decorator that rejects these API calls
unless Supervisor is fully started (CoreState.RUNNING), and apply it to:
- App start/restart/rebuild (fixes #7189) and update
- Home Assistant Core start/restart/rebuild/update
- Plugin (audio/dns/multicast) restart/update, and cli/observer update

Also add a translatable APISystemNotReadyError so the API returns a
clean, user-facing message and error_key instead of leaking the
technical JobConditionException message.

* Address review: neutral error wording + require_running_system test coverage

- Reword APISystemNotReadyError so it no longer implies the block is only
  due to startup, since the same guard also applies while frozen for a
  backup restore.
- Add tests/api/test_require_running_system.py covering the rejection path
  (400 + system_not_ready_error, business logic not invoked) for app
  start/restart/rebuild, app update via the store API, Core
  start/restart/rebuild/update, and all plugin update/restart routes,
  parametrized across SETUP/STARTUP/FREEZE.

* Simplify APISystemNotReadyError message wording

* Switch status code to 503
2026-09-15 14:45:26 +02:00
Stefan AgnerandClaude Fable 5.1 94b4a0b77c Forward Core API proxy paths byte-for-byte and match deny rules on decoded path (#7225)
The Core API proxy compared its deny pattern against the once-decoded route
capture and then formatted that string into the upstream URL, where yarl
decoded percent-encoded unreserved characters a second time. A path such as
hassio%255Fauth/password_reset passed both the middleware blacklist and the
proxy's deny check but reached Core as /api/hassio_auth/password_reset,
which the proxy executes as the Supervisor user.

Forward the raw path exactly as received instead of the decoded capture, and
build the upstream URL with encoded=True so the bytes checked are the bytes
sent. This also stops lossy re-encoding of legitimate paths, e.g. an encoded
slash no longer turns into a path separator. Match both the middleware
blacklist and the proxy deny pattern against the recursively unquoted path
so any encoding depth resolves to the same decision. The recursive unquote
helper moves to module level so both call sites share it.

Reported in GHSA-m2gm-724m-7rf9.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 13:11:08 +02:00
1a7853de9a Start the Supervisor auto update on every version reload (#7201)
* Start the Supervisor auto update on every version reload

A pending Supervisor update blocks Core, OS and app updates through the SUPERVISOR_UPDATED job condition. Only the startup and the daily scheduled reload started the auto update. Every other reload, such as the one Core requests when the user opens Settings, made the new version known without installing it. The user then saw "supervisor needs to be updated first" until the daily task ran.

The updater now starts the auto update task after every reload while the system is running. The scheduled task calls the updater reload directly. The Supervisor update job rejects a concurrent update request, so a user requested update during the auto update fails cleanly instead of creating an update failed issue.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAWqYzDiFEvYjZwfBWoxkC

* Report success for an update request during a running Supervisor update

The auto update can already run when Core or a user requests the update. The requested outcome is underway, so the API reports success and the caller sees the result through the Supervisor restart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAWqYzDiFEvYjZwfBWoxkC

* Trim comments around the Supervisor auto update

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAWqYzDiFEvYjZwfBWoxkC

* Drop comment on the updater auto update trigger

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SAWqYzDiFEvYjZwfBWoxkC

* Raise specific errors when restoring a backup requires a Supervisor update

Move the supervisor version check in do_restore_full/do_restore_partial into
a shared _check_supervisor_version helper. If the backup requires a newer
Supervisor version:
- Raise the new translatable BackupSupervisorVersionError (400) with the
  existing message when auto update is disabled.
- Otherwise kick off the Supervisor auto update in the background via
  sys_create_task(auto_update_supervisor()) and raise the new translatable
  BackupSupervisorUpdateInProgressError (503), telling the caller to retry
  once the update completes.

* Recheck for Supervisor update before failing a version-mismatch restore

When a restore requires a newer Supervisor version and auto update is
enabled, refresh version info first in case a newer Supervisor was just
released. If an update is already known to be needed, make sure it gets
kicked off directly instead of waiting on a reload. Only fall back to
the translatable version-mismatch error if no update is available after
the recheck.

Also move auto_update_supervisor from Tasks to the Supervisor class so
it can be reused outside of the task scheduler.

* Track auto update and version fetch tasks to close restore-check races

Supervisor.auto_update_supervisor() now stores and returns its update
task (created with eager_start so a synchronous failure is reflected
immediately) instead of firing-and-forgetting it. This lets callers
check the task's actual done state rather than guessing based on
need_update, which could be wrong if the update job rejected the call
for an unrelated reason.

HomeAssistantCore's install retry loop now also catches
SupervisorJobError when triggering the Supervisor update it depends on,
so it keeps retrying while an update is already in progress instead of
giving up.

Add Updater.start_fetch_data(), which starts (or reuses) a version
fetch and returns its task. fetch_data() sets its throttle timestamp
before running, even on failure, so a caller relying on fetch_data()
directly can't tell "someone else just refreshed" from "someone else's
fetch just failed" - it would silently skip either way. Callers that
need to know the true outcome should await the shared task instead.
reload() and BackupManager._check_supervisor_version() are updated to
use it: the latter awaits the fetch task directly (skipping it entirely
if there's no connectivity to fetch with) before checking
auto_update_supervisor()'s task, closing a race where a stale/failed
throttle window could cause a 400 (version mismatch) response instead
of a 503 (update in progress).

* Use Job decorator's detach option for update and version fetch tasks

#7211 added a `detach` option to the Job decorator, making the manual
task-tracking previously added here redundant. Replace it with the
decorator-native option:

- `Supervisor.update()` is now `detach=True` with
  `concurrency=JobConcurrency.REJECT`. Calling it returns the task
  performing the update - which may already be in progress from any
  previous caller, manual or automatic - instead of raising
  `SupervisorJobError` or blocking the caller until it (and the
  Supervisor restart it triggers) completes. Errors are raised on the
  returned task, not to the immediate caller.
- `Supervisor.auto_update_supervisor()` is now a thin, non-detached
  method that simply returns `update()`'s own task directly instead of
  tracking a separate task itself. This ensures a concurrent manual
  update and an auto update share the exact same underlying task
  regardless of which caller started it. It never awaits the task to
  completion, since update() restarts Supervisor and awaiting that here
  could drop the caller's connection before a response is sent.
- `Updater.fetch_data()` is now `detach=True` with
  `concurrency=JobConcurrency.REJECT`, replacing the bespoke
  `start_fetch_data()`/`_fetch_task` tracking. `reload()` and
  `BackupManager._check_supervisor_version()` await the returned task
  directly to get the real outcome of a fetch instead of relying on the
  throttle window.
- The Supervisor update API endpoint and the Home Assistant Core
  install retry loop now use `if task := await ...: await task` instead
  of catching `SupervisorJobError`, since a concurrent call no longer
  raises that error - it returns the shared in-progress task instead.

Updated tests across supervisor.py, updater.py, the API and Home
Assistant Core install retry loop to match the new return values and
removed now-obsolete `SupervisorJobError`-based concurrency tests.

* Fix too-many-nested-blocks pylint warning in HomeAssistantCore.install()

Flatten the nested need_update/auto_update if/else into an if/elif so
the Supervisor update retry try/except isn't nested one level deeper,
which pylint 4.0.8 (unlike the previously installed version in this
container) now flags as too-many-nested-blocks (6/5). Cache
need_update in a local variable since it's now referenced twice in the
same iteration and must be consistent between both checks.

* Update supervisor/api/supervisor.py

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Mike Degatano <michael.degatano@gmail.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-09-14 15:56:21 +02:00
Mike Degatano 2e32d0d285 Fix Wi-Fi auth visibility, PSK validation, and a connection lookup race
Report unsupported Wi-Fi auth methods (WPA3/sae, wpa-eap, owe, ...) instead
of hiding the interface's state/config entirely. _map_nm_wifi() used to
return None for these, blanking out both state.wifi and config.wifi even
while genuinely connected. Adds AuthMethod.UNSUPPORTED, reported on GET
but rejected on write; get_connection_from_interface() now leaves the
existing 802-11-wireless-security section untouched for it instead of
clearing it as if it were open auth; update_config() allows omitting
wifi entirely for such an interface, since its config can't be safely
echoed back or overwritten.

Require psk when creating or switching to a new wpa-psk profile, since
GET never returns the actual secret (only psk_set). A psk-less wpa-psk
request used to pass schema validation, get persisted, and only fail
asynchronously at activation with NO_SECRETS. Omitting psk remains
valid when an existing wpa-psk profile is kept unchanged. Raised as a
new translatable HostNetworkWifiPskRequiredError.

Broaden the exception handling in find_connection_settings() to also
catch DBusInterfaceError/DBusObjectError, not just DBusError. A profile
can vanish between list_connections() and connect() (e.g. another
client deleting it), surfacing as UnknownObject/UnknownMethod, neither
of which derives from DBusError.

Adds regression tests for all three, each confirmed to fail without
its corresponding fix.

Addresses review feedback on #7192.
2026-09-12 02:28:48 +00:00
Mike Degatano 56ad67606c Return job id for backgrounded activation on v2 network config PUT
NetworkManager can take up to CONNECTION_ACTIVATION_TIMEOUT (60s) to give
up activating a connection that never gets e.g. a carrier. Previously
apply_changes_v2() always awaited that inline, blocking the request and
then surfacing a misleading 400 on settings that had already been
persisted successfully.

Activation now always runs as a background job for v2. apply_changes()
gains a background_activation flag (off by default, preserving v1's
blocking behavior) and a new cleanup=False _activate_connection_job to
track it; apply_changes_v2() always backgrounds and returns the job id.
The v2 PUT response wraps this alongside the interface struct:
{"job_id": ..., "interface": {...}} (matching the GET shape) instead
of splicing job_id into the interface dict.

Adds a regression test simulating a stuck-activating (no carrier)
connection and confirms the PUT returns promptly with a job id that
later captures the timeout error.
2026-09-12 02:28:45 +00:00
Mike Degatano d833d5d722 Don't emit stale wireless-security clear sentinel when creating new connections
get_connection_from_interface()'s empty 802-11-wireless-security: {}
sentinel is only understood by NetworkSetting.update()'s own merge
logic (_merge_settings_attribute pops the section on an empty dict
before the merged payload ever reaches D-Bus). _apply_create() and
create_vlan() hand the same output unchanged to
add_and_activate_connection()/add_connection(), which have no merge
step of their own: NetworkManager would instantiate an actual
NMSettingWirelessSecurity with an unset key-mgmt and reject the
connection on verify.

Add a for_update flag to get_connection_from_interface() so the clear
sentinel (and the matching 802-11-wireless.security reference clear)
is only emitted for the two call sites that feed
NetworkSetting.update() (_apply_update, _apply_deactivate), not the
two that create brand new connections (create_vlan, _apply_create).

Addresses review feedback on #7192.
2026-09-12 02:28:43 +00:00
Mike Degatano f03aab26db Fix 4 more Wi-Fi/round-trip issues from second review round
- api/network.py: reject addresses/gateway in a v2 ipv4/ipv6 config block
  when method isn't static, since only static values are ever serialized
  back to NetworkManager (generate.py). Previously a GET->PUT round-trip
  of an auto profile with stored addresses/gateway would silently drop
  them instead of reporting an error. Supersedes (and removes as dead
  code) the old "gateway requires an address" check, which is now fully
  subsumed by this broader rule.
- api/network.py: require a non-empty wifi ssid in v2 config PUT. An
  empty ssid was previously accepted by validation but silently omitted
  from the generated NetworkManager connection (falsy check in
  generate.py), turning a client error into a confusing D-Bus/activation
  failure instead of an immediate 400.
- dbus/network/setting/generate.py: explicitly clear the
  `802-11-wireless.security` reference property (not just the
  `802-11-wireless-security` section itself) when switching a wifi
  profile to open auth. `NetworkSetting.update()` merges rather than
  replaces the `802-11-wireless` section, so a stale reference to a
  removed section could otherwise survive a merge.
- api/network.py: reject a v2 config PUT with `enabled: false` against
  an interface with no existing stored profile at all (`config: null`).
  This combination can never actually be persisted (disabling never
  creates/activates a connection), so it was silently reporting success
  while leaving `config: null` unchanged. Creating a brand new
  intentionally-disabled profile isn't a supported use case for now;
  this is an explicit exception to round-tripping, to be revisited
  alongside `config_source`/profile-ownership tracking (#7110).

Adds regression tests for all four, and updates existing v2 API tests
whose fixtures incidentally relied on the now-rejected auto+addresses
combination.
2026-09-12 02:28:41 +00:00
Mike Degatano 4d3dd69240 Fix Wi-Fi round-tripping and full-document replace bugs from review
Addresses issues flagged by automated review on the Network API v2 PR:

- `_apply_deactivate()` only updated `autoconnect`, discarding the rest of
  a full-document PUT (IPv4/IPv6/Wi-Fi/mDNS/LLMNR) when disabling an
  interface; it now generates and writes the complete profile with
  `autoconnect=False`, so a disable that also edits other fields no longer
  loses those edits, and re-enabling doesn't restore stale values.
- The v2 wifi config PUT schema rejected the `psk_set` marker that GET
  returns, breaking round-tripping of an unchanged config; the schema now
  accepts (and ignores) it.
- `state.wifi.ssid` was sourced from the stored connection profile instead
  of the actually-connected access point, violating the state/config
  split for a disconnected-but-configured interface. Added
  `WifiConfig.active_ssid`, populated from the device's active access
  point, and pointed the state struct at it while config keeps the
  profile SSID.
- `get_connection_from_interface()` always wrote `mode: infrastructure`
  regardless of the configured `WifiMode`, silently misconfiguring
  mesh/adhoc/ap interfaces; it now writes the configured mode.
- Switching an existing WPA/WEP profile to open auth left the old
  `802-11-wireless-security` section (and PSK) in place because
  `NetworkSetting.update()`'s merge only touches sections present in the
  update payload. The generator now explicitly signals a clear with an
  empty dict, and `_merge_settings_attribute()` treats an empty dict as
  "remove this section" instead of a no-op merge.

Added test coverage for each of these round-trip/replace scenarios.
2026-09-12 02:28:40 +00:00
Mike Degatano 96b92b95fa Split disable/delete network apply helpers, add translatable host network errors
Replace the destructive_disable-argument-driven _apply_disable() with two
dedicated helpers, _apply_delete() and _apply_deactivate(), so log messages
and error handling can be specific to each path instead of sharing generic
wording.

Give every error apply_changes() can raise an error_key plus a status code
matching who is actually responsible: bad client-supplied connection
settings stay 400 (activation failed/timed out), a request to update an
interface that does not exist or is disabled is 404, and DBus-call
failures that aren't caused by anything the caller sent become 500
(APIUnknownSupervisorError) instead of the previous blanket 400.

Failures that wrap a DBus error no longer interpolate the raw exception
text into the client-facing message, since that unbounded text cannot be
translated. Instead the full error is logged at ERROR level exactly as
before, and the raised exception carries only the interface name.
2026-09-12 02:28:39 +00:00
Mike Degatano 14d6a54930 Network API v2: state/config split, add wifi validation, non-destructive disable, expand test coverage
- Refactor interface_config_struct/interface_struct_v2 to accept ResolvedInterface directly
- Remove temporary VLAN/type restriction from find_connection_settings, relying on v2 API's outer layer for VLAN filtering
- Remove destructive_disable parameter from apply_changes_v2 (v2 never destructively disables)
- Split v2 network API tests into tests/api/test_network_v2.py
- Add coverage for accesspoints v2, 404 handling, wifi happy-path, mdns/llmnr updates, and wifi/interface-type validation
2026-09-12 02:28:38 +00:00
Mike Degatano 54a430f55e Hide new ntp host feature from v1 /host/info features field (#7207)
The python-supervisor-client library's HostInfo model types
`features` as the strict `list[HostFeature]` instead of
`list[HostFeature | str]`:
https://github.com/home-assistant-libs/python-supervisor-client/blob/3459d25b7b223dfd2a49e5aa3f794ea822faab5c/aiohasupervisor/models/host.py#L49

#7181 added HostFeature.NTP, which now breaks the hassio integration
on nightly since the client library raises on parsing it:

    Failed setup, will retry: Field "features" of type list[HostFeature] in HostInfo has invalid value ['reboot', 'shutdown', 'services', 'network', 'hostname', 'timedate', 'os_agent', 'haos', 'resolved', 'journal', 'disk', 'mount', 'ntp']

Work around this on the v1 /host/info endpoint by hiding "ntp" from
the features field and exposing the full list via a new all_features
field instead. The v2 API is unaffected since it has not shipped yet
and its features field is already typed as list[HostFeature | str].
2026-09-10 11:20:35 +02:00
Jan Čermák 8684f7e4cd Add /time API for setting NTP servers on HAOS (#7181)
* Add `/ntp` API for setting NTP servers on HAOS

Add API for customizing NTP servers, using fixed DBus methods available
since OS 18.3. The API is limited to HAOS only with OS Agent that sets
the values in a daemon drop-in config. Changing the values triggers a
systemd-timesyncd unit restart.

The API currently exposes only the configuration overrides, not the
currently applied value (like `timedatectl show-timesync
--property=ServerName` does). If we want to add the effective NTP
server, another field should be introduced in the API for this.

Closes #6278

* Rename `/ntp` to `/time`

* Nest options under `config` key

Move the config options under a separate key to distinguish between
runtime data that we plan to add.

* Tighten NTP server regexp

* Require systemd D-Bus for the NTP host feature

* Always restart timesyncd when NTP servers are set
2026-09-02 17:22:20 +02:00
Petar Petrov fbab5e10a5 Report mount storage usage from the probe, not cached state (#7188)
* Report mount storage usage from the probe, not cached state

The per-mount usage endpoint refused any mount whose cached state was not
active. Since mounts became autofs-triggered, that state is only as fresh
as the last reconcile probe, fifteen minutes apart, so a share that came
back stayed unreportable for up to that long even though a probe would
have found it healthy and mounted it.

Drop the gate and let the probe decide. Its statvfs walks with
LOOKUP_AUTOMOUNT, so it activates a dormant trigger and then reports what
it found, which also means a usage request is no longer strictly read-only
with respect to system state - it can cause the mount it measures.

A path that still does not cross a filesystem boundary after that statvfs
is one whose trigger is gone and which reverted to a plain directory; the
existing boundary check catches it and its comment now says so. With the
gate removed that is the only remaining not-mounted condition, so the
mount_usage_not_active_error key, which named the cached state the
endpoint no longer consults, gives way to mount_usage_not_mounted_error,
which names what the probe observed.

* Tighten comments for probe-authoritative mount usage.

Cached state can be stale; the live probe is the authority, including dormant automount activation.
2026-09-01 16:29:49 -04:00
Stefan AgnerandClaude Fable 5 324d17e68b Use systemd automount units for network mounts (#7167)
* dbus: support aux units in start_transient_unit

Extend `Systemd.start_transient_unit` to accept the `aux` parameter
(`a(sa(sv))`), which has been hardcoded to `[]` since the wrapper was
introduced. Aux entries take the form `(unit_name, properties)` and
let systemd create multiple transient units atomically — most usefully
a `.mount` and its `.automount` companion in one D-Bus call.

Add the unit-property constants we'll need to drive that:

- `DBUS_ATTR_LAZY_UNMOUNT` ("LazyUnmount"): set on the `.mount` so
  systemd umounts with MNT_DETACH. Pairs with `softerr`/`soft` to make
  stop/restart sequences reliable even when the server is gone.
- `DBUS_ATTR_TIMEOUT_IDLE_USEC` ("TimeoutIdleUSec"): set on the
  `.automount` to control how long the mount stays around after the
  last access before autofs expires it.
- `DBUS_ATTR_WHERE` ("Where"): mount-point property; required on the
  `.automount` unit (and useful explicitly on `.mount` units too).

No call sites change in this commit — pure plumbing.

* mounts: pair each network .mount with a .automount companion

Network mounts now get a transient `.automount` unit created
atomically alongside their `.mount`, via the aux parameter on
`StartTransientUnit`. The systemd-managed autofs trigger handles
activation lazily: the path exists in the VFS even before the
underlying network mount runs, and the first access that crosses the
trigger fires `mount.cifs`/`mount.nfs` on demand.

Why this matters:

- PID 1's path-walks (chase, daemon-reload, generator scans) stop
  crossing dead NFS/CIFS lookups — autofs returns from kernel memory
  rather than entering the network filesystem. The class of failure
  fixed at the systemd-timeout layer in #6834 stops being reachable
  in the first place.
- Failed accesses fail fast, bounded by the `.mount`'s `TimeoutSec=`
  rather than hanging forever.
- Background reconnect (NFSv4 state-manager kthread, CIFS
  delayed_work) recovers transparently when the server returns; no
  remount needed.

The `.automount` carries `TimeoutIdleUSec=5min` so kernels can expire
the mount after inactivity and re-trigger on the next access. The
companion `.mount` carries `LazyUnmount=true` (MNT_DETACH on stop),
so umount returns immediately even when the server is unreachable;
existing fds drain in their soft/softerr timeout regime instead of
pinning the umount syscall.

`Mount.unmount()` now stops the `.automount` first so the autofs
trigger can't re-fire the underlying `.mount` during cleanup. The
automount stop is best-effort — if the unit is gone or refuses to
stop, we log and proceed to the `.mount` stop, which remains
authoritative.

Bind mounts opt out via a `creates_automount` class flag — they have
no server to wait on and the lazy semantics would only add
indirection. Network mounts (CIFS/NFS) opt in.

This is wiring only; the periodic mount reload, the reload/restart
escalation, and the inner bind-mount layer for media/share usage are
still in place. The next commit removes them now that automount
makes them unnecessary.

* mounts: drop bind layer, periodic reload, and reload/restart machinery

With the kernel's autofs trigger handling lazy activation and
transparent reconnect, almost all of the supervisor-side mount
choreography becomes unnecessary. Strip it out.

What goes away:

* The inner bind-mount layer. Media/share mounts used to live at
  `path_extern_mounts/{name}` and have a second `.mount` unit bind
  them into `path_extern_media/{name}` (resp. `share`). The
  `.automount` companion can sit directly at the container-facing
  path, and the parent-dir RSLAVE bind into add-on containers
  surfaces the autofs trigger the same way. `NetworkMount.where`
  now switches on usage:
    - MEDIA → `path_extern_media/{name}`
    - SHARE → `path_extern_share/{name}`
    - BACKUP → `path_extern_mounts/{name}` (unchanged)
* `BindMount`, `BoundMount`, `MountManager._bound_mounts`, the
  `_bind_mount`/`_bind_media`/`_bind_share` helpers, and the entire
  emergency-fallback dance (`path_emergency`). The empty-read-only
  dir trick was a workaround for the harder PID 1 wedge problem,
  which autofs solves at the root. Failed shares now surface as
  ETIMEDOUT/EHOSTDOWN on access plus a resolution issue.
* `MountManager.reload()` and the 15-minute `RUN_RELOAD_MOUNTS`
  periodic task. autofs re-activates on access; we don't need to
  poll, and not polling means we don't randomly trip stale-FH /
  softreval / dead-server edge cases on a timer.
* `Mount.reload()` and `Mount._restart()`. The reload→restart
  escalation existed to make `is_mounted` honest in the face of
  systemd's local-only state; now `is_mounted` IS honest (probe-
  based), and any recovery the kernel knows how to do happens
  inside autofs without our involvement.
* The `RELOADING` safety net introduced by #6834. That whole class
  of PID 1 wedge stops being reachable when path lookups don't
  cross dead network mounts.

What changes shape:

* `NetworkMount.is_mounted()` no longer gates on systemd's
  `ActiveState` before probing. The `.mount` unit is dormant
  whenever autofs hasn't recently triggered it, so systemd state
  is meaningless as a health signal. The probe (now via
  `statvfs("/path/.")` so the trailing dot forces `LOOKUP_DIRECTORY`
  and triggers autofs) is the source of truth, and after it returns
  we set `self._state` to ACTIVE/INACTIVE so the API reports
  reachability rather than autofs idle state.
* `MountManager.reload_mount()` becomes probe-only — no systemd
  reload/restart calls. The user-facing semantics: "tell me if this
  mount is reachable right now, and refresh the resolution issue
  accordingly." If the mount is dead, autofs will re-trigger it on
  the next consumer access; the supervisor doesn't need to force it.
* `Mount.load()` keeps `_update_state_await` for the no-job-dispatched
  case where a previous supervisor left a unit in `activating`, then
  probes once via `is_mounted` so state reflects reachability rather
  than the lazy-mount idle state.
* `BackupManager` no longer walks `bound_mounts`. Backups skip
  network-mount subdirectories during folder archive (so we don't
  recurse into the share), and unmount/re-mount them around folder
  restores (so writes target the local mount-point dir, not the
  remote share).

Behavior tradeoffs accepted:

* Dead media/share access now ~30s ETIMEDOUT instead of empty
  read-only dir. The empty-dir was a workaround for the PID 1
  wedge that autofs eliminates at the root; if specific add-ons
  rely on the old behavior we can revisit with a 2-stage automount
  whose fallback target is an emergency dir.
* Resolution-issue lag: failed mounts no longer surface within
  15 min on their own. They appear when the user probes via the
  API, when `BackupManager.reload()` walks the location, or when
  load-time activation fails.

* tests: adapt bind-layer-era tests after rebase

Main gained tests for the bind layer after this branch was written:
the #7013 rebind regression test and the #7072 bind-step rollback test
cover machinery that no longer exists, and the healthy-reload test
asserted the unconditional rebind. Drop the first two and reduce the
third to asserting that a healthy probe performs no systemd operations
at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: rearchitect automount setup from systemd/kernel review

A code-level review of systemd's automount implementation (verified
against v254.13, the version HAOS ships) and the kernel autofs/VFS
plumbing surfaced three correctness-critical flaws in the automount
design, plus several hardening gaps. See automount-rearchitecture.md
for the full analysis.

- Make the .automount the primary transient unit with the .mount as
  aux, mirroring `systemd-mount --automount=yes`. Aux units get no
  start job: with the .mount as primary the trigger was never armed
  and the design silently degraded to eager mounting.

- Set StartLimitIntervalUSec=0 on the .mount. With the default start
  rate limit (5 starts/10s, counting successful starts) a fast-failing
  mount plus any polling consumer trips the limit within seconds, and
  systemd then detaches the autofs trigger entirely
  (AUTOMOUNT_FAILURE_MOUNT_START_LIMIT_HIT) — the path silently
  becomes a plain writable local directory.

- Drop the 5-minute TimeoutIdleUSec (default 0 = never expire).
  Kernel idle expiry decides busyness via may_umount_tree(), which
  only counts the init-namespace mount instance — open files held by
  container processes are invisible, so expiry would unmount shares
  under actively writing add-ons.

- Probe with a plain statvfs: the statfs syscall walks with
  LOOKUP_AUTOMOUNT and triggers by itself; the trailing-dot trick is
  unnecessary. Classify ELOOP as a mount-propagation
  misconfiguration in the probe error handling.

- Re-arm the trigger from reload_mount(): if the .automount unit is
  failed or gone (e.g. autofs unmounted out-of-band), reset failure
  state and re-create the pair before probing.

- Tear down legacy eager-mount units during load(). The old design's
  bind unit for media/share occupies the exact unit name the network
  .mount uses now; on a warm upgrade adoption would mistake the old
  bind mount for the network mount and leak the legacy data mount.

- Honor the systemd job result when stopping the .mount during
  unmount and reset failure state on both units afterwards so dead
  transient units get garbage-collected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: adapt local data repair to the autofs design

Port of the mount-target-not-empty repair (#7089) on top of the
automount rearchitecture:

- relocate_local_data() handles a single target directory — the mount
  sits directly at its container-facing path, the bind layer and with
  it the multi-directory case are gone
- a repair_trigger() failure caused by blocking local data raises the
  mount failed issue with the move_local_data suggestion instead of
  the plain variant
- local data at load is detected via the mount unit itself; the
  bind-layer detection test is rewritten accordingly

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docker: use rslave propagation for execute_command share mount

The temporary container used for execute_command (e.g. core config
check) mounted /share without a propagation mode, unlike every other
share/media mount. With eager mounts this only meant missing mounts
made after container start; with lazy automount activation the shares
are routinely mounted after start, and accessing a not-yet-activated
automount from a private mount namespace fails with ELOOP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: harden automount lifecycle from review findings

Address the findings of an adversarial review of the automount
rearchitecture:

- load() no longer adopts a dead automount trigger: a failed or
  stopped .automount leaves the path a plain writable directory — the
  silent local-write degradation this design must prevent. Tear down
  and re-arm instead. An adopted pair whose probe fails now raises so
  the manager surfaces the mount failed issue, same as a fresh mount.
- unmount() resolves the .mount unit only after stopping the
  .automount (the lazy detach can garbage-collect the transient unit,
  invalidating an earlier proxy), raises instead of warning when the
  automount stop errors, and checks the stop job result so a failed
  stop cannot leave an armed trigger behind a "successful" removal.
- repair_trigger() fully unmounts before re-arming: with the share
  still attached, arming fails — or worse, mount() misreads the
  mounted share's contents as blocking local data.
- Legacy unit teardown honors the stop job result for the same reason.
- reload_mount() escalates once to re-creating the unit pair when an
  established mount is unreachable: a permanently dead session (e.g.
  replaced server) keeps the path mounted, so the trigger can never
  re-fire and kernel reconnection never succeeds. Teardown is safe now
  (lazy unmount, no PID 1 path walks), unlike the removed reload →
  restart escalation of the eager design.
- Reinstate the periodic mounts task as a probe-based reconcile: re-arm
  dead triggers, refresh the reachability state reported by the API and
  used by backup locations, and sync the mount failed issue in both
  directions. No reload or restart of established mounts.
- Folder restore no longer fails after a successful restore when
  re-mounting nested mounts cannot verify an unreachable server — the
  trigger is armed, the share recovers on next access.
- Drop the now-unused Mount.update(), deduplicate unit name escaping,
  hoist the backup exclusion set out of the per-file filter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* tests: expect rslave propagation in core check container

The config check assertion missed the update for the rslave
propagation on the execute_command share mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* tests: cover reconcile trigger repair failure paths

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: arm automount before stopping the legacy data mount

Address review: the legacy teardown stopped the media/share bind unit
and the eager data mount in sequence, both strictly. When the bind
stop succeeded but the data mount stop failed — its unmount can time
out against an unreachable server, legacy units have no LazyUnmount —
load() raised with the container-facing path left a plain writable
directory and no trigger armed: the pollution mode this design is
meant to prevent. Reordering the stops would not help either, as
systemd stops the bind first anyway as a dependent of the data mount
(the implicit Requires= from RequiresMountsFor= on the bind's What=).

Instead, strictly stop only the path-conflicting unit (a failure
leaves the path covered, which is safe and retryable), arm the
automount right after, and stop the conflict-free legacy data mount
best-effort last — a failed stop logs a warning and leaves an orphaned
mount for the next Supervisor restart or a host reboot, with nothing
writable exposed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: discard dead session instead of re-creating units on reload

Address review: re-creating the unit pair on reload of an unreachable
established mount exposed the target as a plain writable directory
between the lazy unmount and arming the replacement — a continuously
writing add-on could block the re-arm or slip writes under the new
mount in the check-to-arm window.

Stop only the .mount unit instead, keeping the .automount armed:
systemd re-installs the autofs trigger over the path — the same
mechanism idle expiry uses, with the automount's Triggers= reference
keeping the transient .mount definition alive — so the path is never
locally writable. The re-probe then mounts fresh through the trigger,
which establishes a new session and thereby covers the permanently
dead session case (e.g. a replaced server) the escalation exists for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: surface arming failures, keep restore teardown re-armed

Address review findings on the arming/re-arming error paths:

- mount() checks the StartTransientUnit job result: a failed start job
  means the trigger never armed and the path is a plain writable
  directory — a hard MountError before the probe, so it cannot be
  mistaken for the armed-but-unreachable MountActivationError.
- Folder restore includes the nested-mount teardown in the try block:
  an unmount failing halfway (trigger disarmed, share still attached)
  previously exited before any re-arm, leaving the path unprotected.
  The finally re-arms via repair_trigger(), which no-ops on a still
  armed trigger and handles partially torn-down pairs.
- The post-restore re-arm suppresses only MountActivationError (armed,
  recovers on next access). Any other failure — arming failed, local
  data blocking the target — left the path unprotected and surfaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: fold the two legacy unit teardowns into one helper

The eager-mount-era cleanup queried the .mount unit a second time
although load() had just fetched it, and the strict teardown of the
unit occupying the automount's path and the best-effort teardown of
the mount at the mounts data directory were near identical. Reuse the
unit from load() and give the shared helper a strict flag, which drops
a D-Bus round trip per mount on every load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: re-arm the trigger when unmount cannot stop the mount

Stopping the automount detaches the whole stack at the path, so an
unmount that then fails to stop the .mount leaves a plain writable
directory behind. Arm a fresh pair before raising, best effort — the
local data repair covers what lands there if that fails too.

The automount stop itself needs no such handling: systemd's
automount_stop() enters dead synchronously and the detach it performs
(MNT_DETACH, no server contact) only logs its errors, so a failure
there means systemd could not be reached at all.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: raise translatable errors for mount operations

Errors from setting up, unmounting and reloading a mount reach users
through the API, so describe what failed in a translatable message and
leave the systemd job result or D-Bus error to the log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: remove the emergency folder of the eager-mount design

The read-only fallback directory has no place in the automount design.
Remove what is left of it on existing installations, next to the legacy
addons directory cleanup, keeping it if it holds anything but the empty
mount points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: trim comments to the behavior they describe

Drop the narration of what changed and why from comments and
docstrings, keeping the notes that record why a tempting alternative
does not work. Two comments still described the reload to restart
escalation this branch removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: correct stale claims in manager and reload test

The manager docstring credited the kernel with idle expiry, which is
deliberately disabled, and claimed no polling although the periodic
reconcile probes every 15 minutes. The reload test claimed systemd is
never contacted while the escalation stops the .mount unit; only reload
and restart of the unit are avoided.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: arm the trigger alone if re-creating the pair is rejected

Address review: the re-arm after a failed unmount submits the .mount as
an aux unit, but transient creation requires a pristine unit and the
definition is still loaded precisely when stopping it is what failed.
Fall back to creating the .automount on its own, which covers the path
and fires the surviving mount definition.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* mounts: never arm the trigger over local data after a failed unmount

Address review: the fallback caught the target validation errors too,
so data written while the path was uncovered would end up beneath a
fresh trigger. Reconciliation would then find a healthy mount and the
repair skips mount points, leaving the data hidden for good. Leave the
path untouched instead, so the next reconcile offers to move it away.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 20:30:23 +02:00
Mike Degatano 2d153ac497 Add one-shot mode and docker-given totals to stats API (#7168)
* Add one-shot mode and docker-given totals to stats API

- Add one-shot query option to container stats to skip the calculated
  CPU percentage window, returning docker-given totals directly.
- Refactor container_stats() with translatable, structured Docker
  errors and timeout handling instead of generic Docker exceptions.
- Translate low-level Docker errors into component-flavored errors
  (NotRunningError/StatsTimeoutError/UnknownError) for Home Assistant,
  Supervisor, and each plugin (Audio, Cli, CoreDNS, Multicast,
  Observer) as well as apps, mirroring the existing App.stats()
  pattern for consistent, informative API responses.
- Add a shared api_return_stats() helper to remove duplicated stats
  handling logic across the 8 stats API endpoints.
- DockerContainerNotFoundError now inherits from DockerNotFound.

* Contain aiodocker _query access and split v1/v2 stats response model

- Contain the reach into aiodocker's protected _query internals (needed
  for one-shot stats until aio-libs/aiodocker#1054 lands) to a single
  DockerAPI._query_one_shot_stats() helper.
- Split the stats API response model between v1 (legacy, windowed CPU
  percentage still available via opt-in one_shot query string) and v2
  (always one-shot, no query string, cpu_percent dropped since it would
  never carry a meaningful value). Both share the same
  api_return_stats(stats, legacy=...) helper so the response model
  stays in one place.
- DockerAPI.container_stats() keeps its existing one_shot keyword only;
  the legacy/v1 vs v2 distinction is handled entirely in the API layer.

* Detect stopped/restarting containers in one-shot stats response

Docker returns a stub response containing only the container's id/name
(no cpu_stats/memory_stats/networks data) for a stopped or restarting
container instead of an error, even in one-shot mode - the not-running
check in daemon.ContainerStats happens before the one-shot branch, so
requesting one-shot stats doesn't change this. Detect the missing
online_cpus field (only ever present while running) and raise
DockerContainerNotRunningError instead of returning a payload that
DockerStats would otherwise turn into a successful all-zero response.

* Add tests for stats error paths introduced in one-shot/totals PR

- Cover DockerAPI.container_stats/_query_one_shot_stats edge cases: empty
  one-shot response, empty windowed response, and the raw _query_one_shot_stats
  helper.
- Add stats() error-translation tests (not-running, timeout, unknown) for the
  audio, cli, multicast and observer plugins, none of which had any coverage.
- Add stats endpoint tests (v1 + v2) for the cli, audio, dns, multicast and
  observer REST APIs, plus a v1 success-path test for the apps stats endpoint.

These close the codecov-flagged gaps for newly introduced code in this PR.

* Unify one-shot and windowed container_stats() code paths

Both modes now share the same error handling and not-running detection:
- The windowed path no longer inspects the container first to check its
  running state. Docker returns the same id/name-only stub response (no
  online_cpus in cpu_stats) for a stopped or restarting container in
  windowed mode as it does in one-shot mode, so the existing online_cpus
  presence check now covers both, closing a race window where a container
  stopping between the inspect and the stats call would previously reach
  the DockerStats parser with an incomplete payload.
- The windowed path builds its container handle via containers.container(),
  which constructs a DockerContainer from the name with no I/O, instead of
  containers.get(), which performs an unnecessary inspect call just to
  fetch the same information the stats call already returns.

* Fix protected access pylint issue
2026-08-31 17:11:38 +02:00
Jan Čermák 68ed3b5dec Add endpoint to reset Docker storage (#7166)
* Add endpoint to reset Docker storage

Corrupted Docker image layers cannot be always fixed by removing and
re-pulling a single image, because layers are shared between images. The
only (easy) way to recover is wiping all of Docker's storage which is
currently cumbersome and requires OS shell access.

Home Assistant OS added a service that performs the Docker storage wipe
on boot when a flag file exists in home-assistant/operating-system#4982,
and home-assistant/os-agent#283 adds OS Agent DBus interface for
creating that file.

This adds a new API method POST /docker/reset-storage which calls OS
Agent's ScheduleDockerStorageReset and creates a reboot_required issue,
following the same pattern used for the Docker storage driver migration.

This is expected to ship in HAOS 18.3, so the endpoint checks for that
and returns 404 on older OS versions. Technically, it might be nicer to
gate on OS Agent version, but from user perspective it's better to
report the required OS version.

Refs #6555

* Add const for minimum OS version supporting the reset
2026-08-27 12:53:16 +02:00
Stefan Agner 210b49fb03 Add API to manage SSH authorized keys on Home Assistant OS (#7039)
* Add API to manage SSH authorized keys on Home Assistant OS

The OS Agent has long exposed AddSSHAuthKey and ClearSSHAuthKeys on its
io.hass.os System D-Bus object, but the Supervisor never wrapped them, so
there was no way to manage root's SSH authorized keys through the
Supervisor API. Add POST /os/ssh/authorized_keys, which replaces the
configured keys with the submitted list (an empty list just clears them).

Since OS Agent writes each key verbatim to /root/.ssh/authorized_keys as
root, the endpoint validates strictly before anything is written: plain
public keys only (no options, no certificates), a key type allowlist
matching what dropbear on Home Assistant OS can verify, no control
characters (one submitted key can never write more than one line), the
base64 blob must embed the declared key type, and entries are capped at
dropbear's 3000 byte per-line limit. The endpoint is admin-only for
add-on tokens.

Replacement clears the existing keys and then adds each key. OS Agent
releases up to 1.10.x return an error when clearing an already absent
file (inverted error check, since fixed), which is the state of every
first-time user, so this specific error is treated as the empty state it
reports.

dropbear on Home Assistant OS is gated by ConditionFileNotEmpty on the
authorized_keys file, which systemd only evaluates when the unit starts,
so the service is started after a non-empty key set is written. A running
dropbear re-reads the file on every authentication attempt and needs no
restart.

* Delegate SSH key validation to OS Agent

Review discussion questioned the full key validation (type allowlist,
base64 blob checks, canonicalization): OS Agent 1.10.0 validates
submitted keys itself and treats clearing an already absent
authorized_keys file as success, so the Supervisor can rely on it
instead of duplicating the logic.

Require OS Agent 1.10.0 or newer and reject requests on older releases
with 404, like the Raspberry Pi firmware endpoints do; this also makes
the missing-file compatibility shim for the clear call unnecessary. A
key rejected by OS Agent surfaces as an error response including its
validation message. The Supervisor keeps only a basic per-key sanity
check that runs before anything is written: no control characters (one
submitted key can never write more than one authorized_keys line) and
at most 3000 bytes (dropbear ignores longer lines, which would leave a
key that passes but never works).

* Support OS Agent releases before 1.10.0

Requiring OS Agent 1.10.0 would keep the feature unavailable until the
next OS update reaches users, while Supervisor updates roll out
independently. Drop the version requirement and accept that validation
is lossy on older releases: the Supervisor sanity check still prevents
writing more than one line per key or lines dropbear ignores, but
proper key validation only happens on OS Agent 1.10.0 or newer.

This brings back the need to tolerate the error OS Agent releases
before 1.10.0 return when clearing an already absent authorized_keys
file (inverted error check): match the complete os.Remove error message
for the authorized_keys path and treat it as the empty state clearing
aims for, on affected versions only.

* Split SSH authorized keys API into add and clear endpoints

Review feedback preferred endpoints mapping 1:1 onto the OS Agent D-Bus
methods over a single replace-the-set API, whose POST semantics were
also questioned (an idempotent full replacement would be PUT).

POST /os/ssh/authorized_keys now takes a single key ({"key": "..."})
and appends it via AddSSHAuthKey; DELETE /os/ssh/authorized_keys
removes all keys via ClearSSHAuthKeys. Each call maps to exactly one
OS Agent operation, so no request can partially succeed. Clients that
want to replace the configured set clear and re-add; a GET (which
needs an OS Agent extension first) and an idempotent PUT can be added
later.

The per-key sanity check, the dropbear service start after adding a
key, and the tolerance for the missing-file clear error of OS Agent
releases before 1.10.0 carry over unchanged.

* Add endpoint to list SSH authorized keys

With only add and clear operations the authorized_keys file is
write-only for API consumers: a user cannot audit which keys grant
access to the box, or whether any exist at all — including keys that
were imported from USB or written by add-ons.

OS Agent 1.11.0 added a ListSSHAuthKeys D-Bus method. Expose it as
GET /os/ssh/authorized_keys, returning the configured entries verbatim.
The endpoint requires OS Agent 1.11.0 and returns 404 on older
releases, like the Raspberry Pi firmware endpoints do; add and clear
keep working on all OS Agent releases.

* Restrict SSH authorized keys endpoints to Home Assistant Core

Review decision: manipulating root's SSH access is not a capability
add-ons should have, even with the admin role, so move the endpoints
from admin-only to the core_only middleware pattern. Only requests
authenticated with the Home Assistant Core token pass; add-on tokens of
any role, the CLI plugin, and the observer are rejected. This also
means the host shell (ha CLI) cannot use the endpoints for now — the
restriction can be opened up later.

The exclusion from the manager role allowlist is kept: if the path is
ever removed from core_only again, it falls back to admin-only rather
than becoming manager-accessible.

* Stop dropbear after clearing SSH authorized keys

Clearing all authorized keys is a revocation, but without stopping
dropbear the listener keeps running until reboot and established
sessions survive, as only a service stop terminates them. Stop the
service after a successful clear, mirroring the USB config import
(haos-config), which also stops dropbear when the imported
authorized_keys file is removed. Stopping an inactive unit is a no-op.

* Serialize SSH authorized keys jobs on a common lock

The add and clear jobs each perform a file operation followed by a
service operation, and nothing prevented them from running
concurrently. An interleaving like clear-file, add-key, start-dropbear,
stop-dropbear lets both requests succeed while the final service state
does not match the final key state (key configured, dropbear stopped).

Make OSManager a JobGroup and run both jobs with GROUP_QUEUE
concurrency, so each file-and-service operation completes before the
next starts. The regression test fails without the shared lock.
2026-08-27 10:12:44 +02:00
Stefan AgnerandClaude Fable 5 69c5a53864 Surface fixup failures to the caller applying a suggestion (#7145)
* Surface fixup failures to the caller applying a suggestion

Applying a suggestion whose fixup failed reported success: the fixup
base swallowed ResolutionFixupError, the API returned OK, and the
repair flow in Home Assistant completed and removed the repair from
the UI while the issue persisted in Supervisor.

Let the error propagate from the fixup instead. The autofix loop
already handles and logs per-fixup errors, bus-event triggered fixups
now get the same treatment in the event callback, and a user-applied
suggestion surfaces the failure as an API error so clients can show
that the fix did not apply. ResolutionFixupError becomes an APIError
with a translatable error key so it is reported as a client-visible
error instead of an unexpected one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop ResolutionFixupError, let fixup failures bubble

Per review: the generic wrapper existed for a time when suggestions
were only applied by autofix and the one job was separating fixup
failures from real bugs for Sentry. The suggestion API is in regular
use now and the wrapper actively hurts it — well-defined errors from
the underlying operations were caught and replaced with a generic
message.

Remove the exception type entirely and let the original errors reach
the caller. The autofix loop and the bus-event fixup path treat any
HassioError as an environmental/config failure: log and continue
without Sentry capture (the raise site reports to Sentry where
warranted); everything else is still captured. Direct raises in the
data disk fixups become HassOSDataDiskError, the could-not-start check
in the app start fixup becomes AppsError. ResolutionFixupJobError now
derives from ResolutionError and JobException.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Share unattended fixup error handling between autofix and bus events

Address review: the bus event listener only caught HassioError, so a
Supervisor bug in a bus-triggered fixup ended as an unretrieved task
exception without Sentry report, unlike the same bug during autofix.
Both unattended paths now use a common apply_fixup_safely() helper:
log HassioError (environmental, reported at the raise site if
warranted), capture everything else.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-25 23:14:44 +02:00
Thibault MollemanandStefan Agner 6947d8b2b2 Strip whitespace from repository URLs in store validation (#7161)
* Strip whitespace from repository URLs in store validation

* Reject repository URLs containing internal whitespace

Tighten RE_REPOSITORY so the URL group rejects whitespace that
.strip() cannot fix, failing validation with a clear error instead of
an unclear git clone failure later. Also parametrize the existing add
repository API test instead of duplicating it and collapse the strip
test cases into one mixed-whitespace case.

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-08-24 15:48:48 +02:00
Mike Degatano 090ef31ec0 Refactor v2 API contracts to remove deprecated supervisor/apps/backups fields (#7111)
* tests(api): cover v1/v2 deprecated contract split

* api: drop deprecated fields from v2 supervisor/apps/backups contracts

* Proper fix for review finding

* Fixes from feedback
2026-08-21 11:55:28 +02:00
Petar Petrov f148020f3a Harden mount storage usage edge cases (#7154)
Follow-up to the per-mount storage usage endpoint, addressing three review
findings on the merged change.

Depths below 2 all yield the same totals-only response for a mount, so they
are normalized to one value before the probe registry is keyed. Concurrent
callers varying only the depth now share a single probe instead of each
parking an executor thread on the same mount.

A directory walk that races a deletion can count data the filesystem figure
no longer includes, producing children that sum past their parent. Such a
breakdown now gets dropped in favor of the totals, which are the consistent
part; the next request re-walks a settled tree.

The v1 legacy addon-id remap is now applied only to the system disk target.
A mount's children are real directory names, so one that happens to be
called apps_data must not be renamed - and a mount response dict is shared
with every concurrent waiter on the same probe, so remapping it in place
could leak renamed ids into v2 responses.
2026-08-20 15:13:35 -04:00
Stefan AgnerandClaude Fable 5 ccdd8c1ef4 Add repair to move local data blocking a mount target (#7089)
* Add repair to move local data blocking a mount target

When an add-on writes into a media/share directory while its network
mount is not in place (#7037), the local data blocks re-creating the
mount: mounting over a non-empty directory is refused. Until now this
failed silently at Supervisor startup — the bind mounts were created as
fire-and-forget tasks — and the only way out was to remove the data
manually over SSH/Samba and re-create the mount via the API.

Surface the condition as a new mount_target_not_empty issue and offer a
move_local_data suggestion. The fixup moves the blocking data to a
<name>_local_recovery folder in a user-accessible location — media or
share for bind mount targets, local backup storage for backup mounts
(their data mount directory is not reachable for users) — then reloads
the mount. Nothing is deleted; users can inspect and clean up the
recovered data via the media browser or the share and backup folders.

Bind mount failures during load are now awaited and routed into
resolution issues instead of being swallowed as fire-and-forget tasks;
bind failures other than blocking local data create the existing
mount_failed issue. A successful mount reload dismisses a stale local
data issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Attach move local data suggestion to the mount failed issue

Review feedback on the repair: rather than introducing a separate
mount_target_not_empty issue type, keep a single mount failed issue
per mount and offer moving the blocking data as an additional
suggestion alongside reload and remove. Reload stays available for
users who prefer to clear the data themselves, and at most one repair
exists per mount. Adding is idempotent, so an already-raised mount
failed issue just gains the extra suggestion.

When re-creating the bind mount after a successful reload fails on
blocking local data, the mount failed issue is re-added together with
the move suggestion, since the reload already dismissed it.

This also resolves the reviewer note about not-a-directory conflicts
being reported under a not-empty issue type: the issue type no longer
encodes the filesystem detail, while the error messages keep the
distinction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Keep an empty directory in place after relocating local data

If remounting fails after the local data was moved aside (e.g. the
server is unreachable at that moment), the renamed directory left
nothing behind: media/share consumers saw the folder disappear
entirely. Recreate an empty directory right after the rename so the
path stays present regardless of whether the remount succeeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Drop move local data suggestion once the data was moved

When the remount after relocating local data fails (e.g. the server is
unreachable at that moment), the mount failed issue stays — but the
move suggestion stayed with it, offering to move data that is no
longer in the way. Dismiss the suggestion after the relocation step so
only reload and remove remain for the leftover failure. Detection
re-adds the move suggestion if local data blocks the target again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Filter Core-facing suggestions by minimum Core version

The fix flow translations for a new suggestion ship with a Core
release. Older Core frontends render an unknown suggestion as an
empty, unlabeled menu entry in the repair fix flow. Filter such
suggestions from Core-facing output — the issue events sent over the
websocket and the resolution API responses when the caller is Home
Assistant — until the connected Core is new enough. Other API
consumers like the CLI always see the full suggestion list.

The move_local_data suggestion requires Core 2026.9.0b0, the release
its fix flow translations are targeted at.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Make fixup failure tests independent of error propagation behavior

Suppress a potential ResolutionFixupError from the failing fixup calls
so the tests pass both while fixup errors are swallowed and once they
propagate to the caller (#7150).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address review: one recovery folder, check OSError for known issues

Move all local data blocking a mount into a single recovery folder so
the user finds it as one fix: when more than one directory holds data,
later ones become subfolders named after their parent directory
instead of numbered sibling folders.

Also run OSError from the relocation through check_oserror to pick up
known filesystem issues like corruption (bad message).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Only filter Core-facing suggestions on v1 surfaces

Per review: no Core version predating the repair suggestion filtering
in its own fix flow (home-assistant/core#179540) supports the v2 API,
so the Supervisor-side compatibility filter is only needed where old
Core versions actually look. Filter the v1 resolution endpoints and
the legacy websocket issue payloads; the v2 endpoints and v2 event
payloads always carry the full suggestion list. The suggestions for
issue endpoint gets a v1 handler for this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 23:40:47 +02:00
Petar Petrov 143da593b8 Report storage usage per mount (#7146)
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.
2026-08-19 11:09:06 +03:00
Mike Degatano 9ad1c198c2 Block supervisor/hassio WebSocket command types in the HA proxy (#7123)
* Block supervisor/hassio WebSocket command types in the HA proxy

An app with only homeassistant_api: true could send supervisor/api (or
other supervisor/* / hassio/* command type) frames through the Supervisor's
Core WebSocket proxy at /core/websocket. Core executes those via the hassio
integration's websocket_supervisor_api handler, which calls back into the
Supervisor using Core's own token. That token bypasses all role checks in
security.py, giving the app unrestricted Supervisor API access regardless of
its declared hassio_role or hassio_api flag.

Fix: filter command types on the app-to-Core direction of _proxy_message.
Any TEXT frame whose type field starts with 'supervisor/' or 'hassio/' is
rejected with a Core-shaped result/unauthorized response instead of being
forwarded. The connection stays open. Fail-open on unparseable frames (Core's
own validation already rejects those). The Core-to-app direction is
unfiltered.

* Remove the double json parse
2026-08-19 08:57:10 +02:00
Jan Čermák 384ed39b39 Use port 80 as default for Core/landingpage, omit scheme's default port in API URL (#7153)
Set default port of HA to 80, as this default is authoritative for
what's shown on the HA CLI banner when landing page is running - there's
no homeassistant.json at this time, so this value applies. We shouldn't
need to care about installs running an old landing page (without port 80
support at all), as Supervisor update results in the old default being
persisted homeassistant.json which is written on Supervisor restart, so
the banner still shows port 8123 if the old Supervisor (and hence old
landing page) were baked in an OS image.

Also append the port to the API URL only when it's not the scheme's
default to avoid :80/:443 suffixes when they're not necessary.

Closes #7151
2026-08-18 18:30:39 +02:00
2578b1bf03 Hassio integration auth bypass from app with API access (#7122)
* 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>
2026-08-12 08:15:50 +02:00
c01efa05a5 Rename resolution addon issue types and check slugs to app-based naming (#7098)
* 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>
2026-08-04 10:57:42 +02:00
Stefan AgnerandClaude Fable 5 c5ea8e174e Gracefully handle lost systemd-journal-gatewayd connections (#7106)
* 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>
2026-08-04 10:12:23 +02:00
Mike Degatano 3dea234189 Add v1/v2 terminology shim for host disk usage response (#7100)
Implement API versioned response terminology for GET /host/disks/default/usage:
- V2 returns app terminology as-is (apps_data/apps_config)
- V1 applies compatibility shim and returns legacy addon terminology
  (addons_data/addons_config)

Also adds tests to verify:
- v1 keeps legacy IDs
- v2 keeps app IDs
- disk usage endpoint behavior remains stable across max_depth scenarios

Fixes #7030
2026-07-31 14:56:57 +02:00
Stefan AgnerandClaude Fable 5 f8ba166ed2 Enter STOPPING state before Supervisor restart API response (#7085)
* 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>
2026-07-28 13:14:55 +02:00
Stefan AgnerandClaude Fable 5 6bbb82aec0 Return a proper API error when adding a duplicate store repository (#7087)
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>
2026-07-28 12:00:14 +02:00
Stefan AgnerandClaude Fable 5 f4ec256b94 Report real OS version to Core versions consuming version_pending (#7086)
* 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>
2026-07-27 17:34:53 +02:00
Mike Degatano 4152855713 Rename backup and restore job stages from addon(s) to app(s) (#7059)
* 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.
2026-07-24 12:59:58 +02:00
Mike Degatano 96798b5041 Migrate addon API error metadata to app naming (#7058) 2026-07-23 16:02:49 +02:00
Mike DegatanoandStefan Agner c119e0cff1 Rename Docker app container and builder names from addon_ to app_ (#7024)
* Rename Docker app container and builder names from addon_ to app_

* Update container state event tests to app_ names

* Migrate legacy app container names on attach

* Fix error handling

* Fixes from feedback

* Fix pytest

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-07-23 15:43:33 +02:00
Mike Degatano a101e2eb5b Add websocket v2 jobs legacy-name toggle (#7051) 2026-07-17 14:07:36 +02:00
Brandon b977824507 Fix Core API proxy stripping the multipart boundary from Content-Type (#7050)
The proxy forwarded aiohttp's parsed request.content_type property, which
drops header parameters — including the multipart boundary. Core then
cannot parse any multipart body proxied from an add-on and raises
"boundary missed for Content-Type".

Forward the raw Content-Type header instead, matching what the stream()
proxy path already does.

Fixes #7049
2026-07-17 14:06:41 +02:00
Stefan AgnerandClaude Fable 5 dfe727410c Fix length validation of str and password options with bounds (#7044)
* Fix length validation of str and password options with bounds

App option schemas like str(1,32) validated the string value with
vol.Range, which compares the value itself against the numeric bounds.
Comparing a str with a float raises TypeError, which voluptuous reports
as "invalid value or type (must have a partial ordering)". As a result,
saving options always failed for any add-on using a bounded str or
password schema; unbounded str/password was unaffected since vol.Range
without bounds performs no comparison.

Use vol.Length to check the string length against the bounds, which is
the documented meaning of str(min,max). Also replace the str(value)
literal (a self-equality check) with the str type so non-string values
still fail validation, now with a clearer error message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Update API test for changed non-string option error message

Validating a str option against the str type instead of the str(value)
literal changed the voluptuous error for non-string values from "not a
valid value" to "expected str". Update the expected message in the API
options error test accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 13:31:28 +02:00
Mike Degatano 4a54a229b9 Migrate addon job names to app and scope legacy API compatibility (#7014)
* Migrate addon job names to app and trim legacy API aliases

* Limit job name compatibility to app_manager_update only
2026-07-14 13:45:40 +02:00
Stefan AgnerandClaude Fable 5 83d15aadcc Track installed OS update pending reboot activation (#7006)
* Track installed OS update pending reboot activation

Since #6982 the OS update no longer reboots the host automatically, so the
system keeps running the old version until the user reboots. In that window
the update kept being offered: need_update compares the running OS version,
which only changes on reboot. Requesting the same update again re-downloaded
and reinstalled the full image. Worse, a Supervisor restart in that window
dropped the in-memory REBOOT_REQUIRED issue and canceled the pending update
altogether, because mark_healthy marks the booted slot active on startup,
reverting the primary boot slot set by the rauc install.

Track the installed version awaiting a reboot as version_pending and expose
it in /os/info. A successful install sets it, updating again to that version
is rejected with a hint to reboot, and need_update no longer reports true for
an update that is already installed. On load, the pending state is recovered
from rauc by comparing the primary boot slot (via a new GetPrimary D-Bus
wrapper) with the booted slot, re-creating the REBOOT_REQUIRED issue as well.
mark_healthy now skips marking the booted slot active while an update is
pending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Split pending update detection condition

Fix pylint R0916 (too-many-boolean-expressions) by splitting the guard in
_detect_pending_update into a slot data validity check and the actual
pending update check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Report pending OS update as current version in /os/info

The Core update entity derives update availability by comparing version
with version_latest from /os/info, so it keeps offering an update that is
already installed until the system is rebooted. Report an installed update
pending activation as the current version so existing Core releases
reflect update availability correctly. Once Core consumes version_pending,
this can be limited to Core versions predating that support.

The hassos field of the root /info endpoint keeps reporting the running
version, Core only uses it as a HAOS presence check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 18:45:18 +02:00
Mike Degatano 35542d5bcc Replace addon with app in docker container names for apps (#6997)
* Support dual app/addon log identifiers during migration

* Use single journald epoch query for dual app identifiers

* Add API-keyed container epoch error handling

* Template CONTAINER_LOG_EPOCH via extra fields
2026-07-03 15:44:22 +02:00
Mike Degatano a483d4a503 Make /os/update stop auto-rebooting after OS update (#6982)
* Make OS update require explicit reboot

* Avoid protected OS field access in API update tests
2026-06-30 14:49:24 +02:00
Stefan AgnerandClaude Opus 4.8 1857753e22 Redact app options in info for unprivileged apps (#6953)
The `/addons/{slug}/info` endpoint returned the target app's user options,
which can contain secrets such as passwords and API keys. The security
middleware grants every role (including the default role) access to any
`/.+/info` path, so an installed app with `hassio_api: true` and the default
role could read another app's options simply by requesting its info.

Redact the options field in info_data() unless the caller is entitled to see
it: Home Assistant Core (and other non-app internals), the app reading its
own info, or an app with the manager or admin role. Other apps reading a
different app's info now receive an empty options dict while all non-secret
metadata stays available for discovery. This mirrors the existing self-only
restriction on the dedicated /options/config endpoint.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 14:54:57 +02:00
Stefan AgnerandClaude Opus 4.8 81e235376e Fix typos repo-wide and add codespell pre-commit hook (#6949)
* Fix typos in comments, docstrings and log messages

Correct 39 spelling mistakes across comments, docstrings and log/error
message strings throughout the package (e.g. "conection" -> "connection",
"Incomming" -> "Incoming", "Rasie" -> "Raise"). All changes are confined
to human-readable text; no identifiers, attributes or D-Bus contracts are
touched, so there is no behavior change. Found with codespell.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix typos in tests and CI workflow

Correct spelling mistakes in test comments, docstrings and data, plus one
in the builder workflow, so the whole tree is clean for the codespell hook
added next. The assertion in test_network_manager.py is updated to match
the corrected "Unknown error while processing" log message in the source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add codespell pre-commit hook

Wire up codespell so spelling mistakes in comments, docstrings and strings
are caught automatically. The vendored frontend panel is excluded, and
"hass" and "astroid" are added to the ignore list as known false positives
(the Home Assistant abbreviation and the pylint dependency package).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Address review feedback

Improve grammar in several of the touched comments and docstrings: use the
plural "ignore conditions" for the list-returning property, add the missing
auxiliary verb and fix agreement in the timezone-filter comment, fix
"backups ... use" agreement, and reword "underlay" to "underlying" in the
arch module docstring.

Also drop the "*.json" skip from the codespell hook. It was carried over
from another project but is unnecessary here (all tracked JSON is clean),
and skipping it would needlessly leave translation and data JSON unchecked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Reword onboarding comment

"overflight" was a literal calque of the German "überflogen"; use the
idiomatic "skimmed through" instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 14:09:16 +02:00
Stefan AgnerandClaude Opus 4.8 dae48c62e4 Treat mount errors as API errors instead of unexpected failures (#6946)
Mount failures generally reflect user configuration or host conditions
(unreachable server, wrong credentials, ...) rather than a Supervisor
bug. As a plain HassioError, MountError reached the generic error branch
of api_process, which logged a full traceback as an "Unexpected error
during API call" and captured the exception to Sentry. The mount reload
path already encoded the opposite intent by explicitly skipping Sentry
for MountError.

Make MountError an APIError so mount failures are surfaced as client-side
errors with their explicit message, without a traceback or Sentry noise,
matching the existing JobException handling. MountNotFound additionally
inherits from APINotFound so it returns a 404 instead of a 400.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 09:32:22 +02:00
Jan Čermák ecf890a41c Fix blacklist for v2 endpoints in security middleware, add v2 security tests (#6933)
The blacklist in the security middleware didn't take the v2 prefix into
account, allowing to call routes that are supposed to be blacklisted for
apps with hassio and homeassistant APIs enabled in app config.

Add test checking that these routes are always blacklisted, and
parametrize other tests using v2 endpoints in the test_security module.
2026-06-15 17:55:58 +02:00
Jan Čermák 446a1aacd9 Return 404 for raspberrypi endpoints on boards without firmware (#6926)
* Return 404 for raspberrypi endpoints on boards without firmware

Return "not found" when board doesn't have the firmware update
available. Any other error may indicate it's worth retrying later (as
discussed in [1]), so 404 is more appropriate here.

[1] https://github.com/home-assistant/core/pull/172929#discussion_r3363706261

* Consistently return APINotFound with debug log
2026-06-09 17:15:19 +02:00
1e5d7812cd Bump aiohttp from 3.14.0 to 3.14.1 (#6922)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Stefan Agner <stefan@agner.ch>
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 11:44:30 -05:00
Stefan Agner d910efa49e Surface Home Assistant update failures as translatable API errors (#6915)
Updating Core to a non-existent version (e.g. a mistyped beta tag) reported
success on the CLI: the image pull failed with a 404, but update() swallowed
the error via "with suppress(HomeAssistantError)" and then ran its post-update
health check against the still-running old Core, which passed.

Split the update routine into image install and start phases. A failed image
install leaves the running Core untouched, so there is nothing to health-check
or roll back; let it bubble out instead of masking it. Only failures after the
image is in place (e.g. the new container starting unhealthy) fall through to
the health check and rollback logic, where the error is now captured at debug
level rather than silently suppressed.

Model the update errors as client-facing APIError subclasses carrying an
error_key so the frontend can translate them and they no longer reach Sentry as
unexpected errors:

- HomeAssistantUpdateError: generic update failure
- HomeAssistantUpdateImageError: image download failed (includes the version)
- HomeAssistantUpdateAlreadyInstalledError: requested version already installed
2026-06-08 15:39:23 +02:00