ci / bun-nix (pull_request) Successful in 20s
android / android (pull_request) Canceled after 1m22s
ci / rust-arm64 (pull_request) Successful in 1m17s
ci / rust (pull_request) Canceled after 1m22s
ci / web (pull_request) Canceled after 1m18s
ci / docs-site (pull_request) Canceled after 1m18s
A field report: the Steam plugin installed, the grid stayed empty, and the only clue was one warn per sync — `art.hero: local art must be an image file … inside an allowed art root`. Two defects, both here. The art roots defaulted to the users base (`C:\Users`, from `%PUBLIC%`'s parent). That covers the launchers that install per-user, but not Steam, which installs to `C:\Program Files (x86)\Steam` and keeps both the things the plugin publishes there — `appcache\librarycache\<appid>\<hash>\` and each account's `userdata\<id>\config\grid\`. So every cover was out of root. It is a v0.28.0 regression: the built-in scanner the plugin replaced served covers through the legacy `steam:` art-proxy branch, which never passed through the H-2 confinement, so deleting the scanner routed that art through a gate it had never been measured against. `art_roots()` now also carries every Steam install it can find, from the three Program Files vars and from HKLM `Valve\Steam\InstallPath` so a Steam on another drive counts too. POSIX needs no equivalent — native and Flatpak Steam are both already under `$HOME`. The confinement is not weakened. It exists to stop the host (SYSTEM) reading what the plugin lane (LocalService) cannot reach itself; the Steam directory is readable by LocalService already, so nothing there is reachable *because* the host is privileged, and the extension, regular-file, magic-byte and config-dir gates still apply on top. Tested: `config.vdf` is not servable from an art root, nor is a non-image wearing `.png`. Second, and the reason this cost a whole library rather than a thumbnail: the provider reconcile validated art per entry and 400'd the WHOLE payload on the first bad value. A path mismatch therefore deleted every game from that store, and the plugin — which only ever sees `HostRequestError` — could not say which. A reconcile now strips unservable local art and syncs the rest, logging one aggregated warn with the count, an example path and the env var. The invariant the 400 held is unchanged: no unservable path is persisted. The operator's own single-entry writes keep the hard 400, because there the path was typed by hand and silence would be the wrong answer. Verified on Linux (.25: 493/493, clippy clean) and Windows (.133: 12/12 art tests, clippy clean). The new Windows test is hermetic — it repoints `%ProgramFiles(x86)%` at a synthetic Steam tree rather than asserting over whatever Steam the box happens to have, since the vacuous version of that test is what would have let this ship. Confirmed non-vacuous by disabling the fix: it fails on "the DEFAULT art roots must include it".