ci / web (pull_request) Successful in 58s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / docs-site (pull_request) Successful in 1m14s
apple / swift (pull_request) Successful in 1m19s
apple / screenshots (pull_request) Skipped
android / android (pull_request) Successful in 3m1s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m0s
ci / rust-arm64 (pull_request) Successful in 3m45s
ci / rust (pull_request) Successful in 9m22s
PR #58 hardened the art proxy in the same three files this branch rewrote, and the two changes pull in opposite directions: #58 narrowed what the host will read from disk, while WP1.2 widened what counts as a local art path so an extracted scanner's covers can be served at all. Resolved so the widening goes through the gate rather than beside it. Kept from #58, unchanged: art_path_is_confined (UNC refusal, canonicalize-or- refuse, config-dir exclusion, roots check), the image-extension whitelist, sniff_image_type, validate_art_paths as write-time validation, the AuthLane privileged-field check on every entry in a reconcile payload, and the launch redaction in GET /library. Three reconciliations: * `local_art_bytes` converts a `file://` value to a path BEFORE calling art_path_is_servable, so the confinement check and the read see the same path. Ordering is the point: percent-decoding happens before canonicalization, so a `%2e%2e` escape cannot hide from the traversal check. Pinned by a test. * `art_roots()` gains $HOME on POSIX. This is the one that would have bitten silently: the list was empty on non-Windows, which was correct while is_local_art_path was Windows-shaped (Playnite is Windows-only, so nothing on a POSIX host was ever classified as local art and the confinement had nothing to confine). Once WP1.2 classifies POSIX paths as local, an empty root list is not "secure by default" — it serves NO plugin art on Linux, which is every cover the lutris and steam plugins emit. $HOME is the exact analogue of the Windows users base #58 already ships, and covers Steam's librarycache and grid overrides, Lutris's coverart/banners (both copies), Heroic's caches and all the Flatpak variants. It is not the load-bearing control: a value still needs an image extension, must canonicalize to a real regular file inside a root and outside the config dir, and must CONTAIN image bytes. * The two tests that both wanted to mutate PUNKTFUNK_LIBRARY_ART_ROOTS became one. Cargo runs tests as parallel threads of a single process, so two tests setting the same env var race. The `file://` and confinement assertions moved into #58's existing confined test; what remains of the WP1.2 test is the pure classification/rewrite half, which touches neither env nor filesystem. Also: `steam_ui` was missing from the list of host-resolved launch kinds in privileged_field's doc comment and in the 403 a plugin sees. Prose only — the check is a denylist (prep, launch.kind = "command"), so steam_ui was never actually refused — but a plugin author reading that error would have concluded otherwise. Gates on .21: punktfunk-host 433 passed / 0 failed (including #58's H-2 tests and the new file:// ones), full workspace tests clean, workspace clippy -D warnings clean, cargo fmt --all --check clean, OpenAPI drift test green.