Compare commits

...

2 Commits

Author SHA1 Message Date
Alexandre
bfe91cbeac refactor(webtop,webtop_kde,claude_desktop): share the Selkies startup scripts (#2920)
* refactor(webtop,webtop_kde,claude_desktop): share the Selkies startup scripts

All three add-ons are built on the LinuxServer Selkies base image and had
independently drifted copies of the same startup scripts. claude_desktop's
copies carry a set of fixes the two webtops never received, so make
claude_desktop the single source and symlink the shared scripts from
webtop_kde/rootfs (which webtop/rootfs already symlinks in full).

Shared by symlink: 20-folders.sh, 21-gpu_permissions.sh, 80-configuration.sh,
90-ingress.sh and the six etc/nginx/includes files.

Kept add-on specific: everything Claude-only stays in claude_desktop
(81/82/83/84 tool installs, 85-openbox_autostart.sh, defaults/, usr/local/bin,
svc-headroom), and everything webtop-only stays in webtop_kde (90-ssl.sh,
helpers/microsoft-edge-stable, and the new 81-microsoft_edge.sh).

To make the shared scripts add-on agnostic:
- 20-folders.sh derives its default data location from the home directory the
  Dockerfile baked into the abc user instead of hardcoding /data/data. That
  yields /data/data on claude_desktop and /config/data_kde on both webtops,
  matching each add-on's previous behaviour exactly.
- The permission_mode: bypass root guard is skipped on add-ons that do not
  declare that option.
- 80-configuration.sh falls back to pip when the image does not ship uv.
- The Microsoft Edge install moves out of 80-configuration.sh into a
  webtop-only 81-microsoft_edge.sh, which also absorbs the ownership fixup
  that used to run in 20-folders.sh before Edge was installed and so never
  matched anything.

CI: the builder's symlink-resolution step made a single pass over a
pre-computed file list, so resolving webtop/rootfs (a directory symlink)
could copy the symlinks inside it verbatim, leaving links that escape the
webtop build context. Verified on this tree: the old loop leaves 10 dangling
symlinks under webtop/. Extract it to .github/scripts/resolve_symlinks.sh,
repeat until a pass finds nothing, and run it in the PR check too, which had
no resolution step at all.

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

* fix: guard two startup aborts found in review

Both are crash paths in code added by this PR, not hardening:

- 20-folders.sh: getent exits 2 when the user does not exist, and under
  bashio's `set -o pipefail` plus the script's `set -e` that aborts at the
  assignment, so the "could not read abc's home" fallback below it was
  unreachable. Same trap already documented in 21-gpu_permissions.sh.
  Verified: without the guard the shell exits 2; with it the fallback runs.

- 81-microsoft_edge.sh: the ownership fixup lost the `-f` guard the original
  had in 20-folders.sh. Without nullglob an unmatched /usr/bin/microsoft-edge*
  reaches chown as a literal and `set -e` kills container startup. Now a
  nullglob array with a warning when empty.

Also make resolve_symlinks.sh fail on a broken symlink instead of deleting it.
Dropping it silently yields an image that builds clean and misbehaves at
runtime; a red build is easier to diagnose. Verified both paths: the repo as-is
resolves to 0 symlinks and exit 0, and an injected broken link exits 1.

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

* fix: address review comments on data-location default and Edge downloads

Two findings from CodeRabbit, both correctness rather than hardening:

- 20-folders.sh rewrites abc's home in /etc/passwd further down, so re-reading
  it on the next boot returned the *previously selected* location as the image
  default. On a restart that reuses the container's writable layer, clearing
  data_location would strand the user on their old custom path instead of
  restoring the built-in one. Cache the value in /etc/.addon_image_home, which
  shares the writable layer's lifetime with the edit it compensates for: a
  rebuilt or recreated container starts from a pristine /etc/passwd and
  regenerates it. Simulated all three cases (first boot, reused container with
  a rewritten passwd, recreated container) under `set -e` + `set -o pipefail`.

- 81-microsoft_edge.sh: both curl calls were unbounded, so a stalled
  packages.microsoft.com would hang cont-init.d and with it the whole add-on.
  Add --fail/--connect-timeout/--max-time and skip the install with a logged
  error when version discovery or the download fails. The desktop is useful
  without Edge; an add-on wedged before Selkies starts is not.

Not addressed, deliberately: escaping $LOCATION/$DEFAULT_LOCATION for sed, and
validating symlink targets in resolve_symlinks.sh. Both are hardening against
inputs that are not reachable in normal use, both predate this PR, and the
maintainer has asked to prioritise usability over that class of change.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 14:23:52 +02:00
Alexandre
5eb7cd2744 fix(seerr): re-encode ingress query strings for the OpenAPI validator (#2917)
* fix(seerr): re-encode ingress query strings for the OpenAPI validator

Searches through ingress fail with a 400 from the Seerr API, which the UI
reports as "500 Internal Server Error". The same searches succeed on the
directly published port 5055.

Supervisor proxies ingress traffic with `params=request.query`
(supervisor/api/ingress.py) - an already-decoded MultiDict - so aiohttp/yarl
re-encodes the query string on the way to the add-on. yarl's safe set is far
wider than the one express-openapi-validator accepts: it emits a space as "+"
and forwards ":", "/", "@", "!", "$", "'", "(", ")", "*" and "," bare, while
the validator tests the raw, still-encoded value against

    RESERVED_CHARS = /[\:\/\?#\[\]@!\$&\'()\*\+,;=]/

and rejects the request. That breaks most real titles - "Monsters, Inc.",
"Ocean's Eleven", "Mission: Impossible", "Mamma Mia!".

An njs handler now re-encodes exactly those characters before proxying. This
is lossless: yarl only emits them bare when they were literal characters of
the value, since anything ambiguous arrives already encoded ("+" as %2B, "&"
as %26, "=" as %3D). "&" and "=" are left alone as the query string's own
separators, and the path is forwarded byte-for-byte.

Verified against a real express-openapi-validator over all 19 characters in
RESERVED_CHARS, and diffed byte-for-byte against the previous config across
representative traffic: only query-string encoding changes.

Fixes #2906
Fixes #2646

* fix(seerr): ship the njs load_module snippet and encode "?" and ";"

Addresses two review findings.

1. The njs module never loaded. .templates/ha_automatic_packages.sh moves the
   rootfs /etc/nginx aside to /etc/nginx2 before installing nginx, then does
   `rm -r /etc/nginx` and restores the saved tree. That deletes the
   load_module snippet nginx-mod-http-js installs, so nginx aborted with
   `unknown directive "js_import"` and the service's finish hook would have
   shut the add-on down - all ingress dead, not just search. The image build
   still passed CI because it never starts nginx. The snippet now ships in the
   rootfs so it survives the swap, under the package's own filename so the two
   can never both be present and double-load the module.

2. "?" arrives bare from yarl and was not encoded. It slipped through testing
   because the validator strips one occurrence with `qs.replace('?', '')`
   before checking, so a single "?" passes by accident and only a second one
   ("Who? What?") returned 400.

NEEDS_ENCODING is now derived from the validator's RESERVED_CHARS minus the
"&" and "=" separators, rather than from the characters yarl happens to emit
bare today, so it stays correct if either side changes its safe set. The added
characters are a no-op for current traffic: yarl already percent-encodes
"# [ ] ;", so Seerr receives them encoded regardless.

Verified by replaying the build-time /etc/nginx2 swap and starting nginx, which
fails without the snippet and serves correctly with it; by sending every
RESERVED_CHAR bare; and by diffing forwarded bytes against master, unchanged at
2/20 with all query-parser edge cases identical.

* style(seerr): satisfy Codacy - double quotes in njs, changelog blank lines

Clears the 11 new Info-level Codacy findings: 9x ESLint 'quotes' in
njs/ingress.js and 2x markdownlint MD022/MD032 on the changelog entry.
No behaviour change; re-verified through the build-time /etc/nginx2 swap.
2026-07-28 09:13:31 +02:00
29 changed files with 302 additions and 350 deletions

45
.github/scripts/resolve_symlinks.sh vendored Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Replace every symlink in the checked-out repository with a real copy of its target.
#
# Add-ons share files by symlinking across add-on directories (e.g. webtop/rootfs ->
# ../webtop_kde/rootfs, and files inside it -> ../../../../claude_desktop/rootfs/...). A
# Docker build context is a single add-on directory, so any symlink that escapes it has to be
# materialised before the build.
#
# The loop repeats because resolving one symlink can create others: copying a directory
# symlink with `cp -a` preserves the symlinks *inside* it, and those copies are not part of
# the file list the current pass is iterating over. Repeating until a pass finds nothing makes
# the result independent of the order `find` happens to return.
set -euo pipefail
for _ in 1 2 3 4 5; do
mapfile -t links < <(find . -type l)
if [ "${#links[@]}" -eq 0 ]; then
exit 0
fi
for link in "${links[@]}"; do
target=$(readlink -f "$link" || true)
if [ -z "$target" ] || [ ! -e "$target" ]; then
# Fail rather than drop it. A broken link here means an add-on is missing a file
# it expects to ship; silently removing it produces an image that builds fine and
# misbehaves at runtime, which is far harder to diagnose than a red build.
echo "::error::Broken symlink: $link -> $(readlink "$link")"
exit 1
fi
rm "$link"
if [ -d "$target" ]; then
mkdir -p "$link"
cp -a "$target/." "$link/"
else
cp "$target" "$link"
fi
done
done
if [ -n "$(find . -type l)" ]; then
echo "::error::Symlinks still present after 5 resolution passes; possible symlink cycle"
find . -type l
exit 1
fi

View File

@@ -108,6 +108,9 @@ jobs:
- name: ↩️ Checkout
uses: actions/checkout@v7.0.1
- name: Resolve symlinks in repository copy
run: bash .github/scripts/resolve_symlinks.sh
- name: Copy templates into addon build context
env:
ADDON: ${{ matrix.addon }}

View File

@@ -142,24 +142,7 @@ jobs:
persist-credentials: false
- name: Resolve symlinks in repository copy
run: |
set -euo pipefail
find . -type l | while read -r link; do
target=$(readlink -f "$link" || true)
if [ -z "$target" ]; then
echo "Skipping broken symlink: $link"
continue
fi
rm "$link"
if [ -d "$target" ]; then
mkdir -p "$link"
cp -a "$target/." "$link/"
else
cp "$target" "$link"
fi
done
run: bash .github/scripts/resolve_symlinks.sh
- name: Copy templates into addon build context
env:

View File

@@ -1,3 +1,7 @@
## 1.36.3 (28-07-2026)
- Make the Selkies startup scripts add-on agnostic so `webtop` and `webtop_kde` can share them by symlink instead of carrying their own drifted copies. `20-folders.sh` now derives its default data location from the home directory the Dockerfile baked into the `abc` user (`getent passwd abc`) rather than hardcoding `/data/data`, and the `permission_mode: bypass` root guard is skipped on add-ons that do not declare that option. `80-configuration.sh` falls back to `pip` when the image does not ship `uv`. No behaviour change for Claude Desktop: `getent passwd abc` returns `/data/data`, which is exactly the value that was hardcoded before.
## 1.36.2 (28-07-2026)
- Minor bugs fixed

View File

@@ -122,5 +122,5 @@ slug: claude_desktop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "1.36.2"
version: "1.36.3"
video: true

View File

@@ -3,6 +3,33 @@
# shellcheck disable=SC2046
set -e
# Shared by every Selkies-based add-on in this repo (claude_desktop, webtop, webtop_kde) via a
# symlink; keep it add-on agnostic. The only per-add-on input is the home directory baked into
# the image by the Dockerfile's `usermod --home <dir> abc`, read back below.
# Default data location for this image: whatever the Dockerfile set as abc's home.
#
# Cached in a marker file rather than read from /etc/passwd on every boot, because this script
# rewrites that entry further down to the *selected* location. On a restart that reuses the
# container's writable layer, re-reading /etc/passwd would hand back the previous selection as
# the "image default", so clearing data_location would strand the user on their old custom path
# instead of restoring the built-in one. The marker shares its lifetime with the /etc/passwd
# edit it compensates for: both live in the writable layer, so a rebuilt or recreated container
# starts from a pristine /etc/passwd and regenerates the marker correctly.
#
# The `|| true` is load-bearing: getent exits 2 when the user does not exist, and under bashio's
# `set -o pipefail` plus this script's `set -e` that aborts the script at the assignment, before
# the fallback below can run. Same trap documented in 21-gpu_permissions.sh.
DEFAULT_LOCATION_MARKER="/etc/.addon_image_home"
if [ ! -s "$DEFAULT_LOCATION_MARKER" ]; then
getent passwd abc 2> /dev/null | cut -d: -f6 > "$DEFAULT_LOCATION_MARKER" || true
fi
DEFAULT_LOCATION="$(cat "$DEFAULT_LOCATION_MARKER" 2> /dev/null || true)"
if [[ -z "$DEFAULT_LOCATION" || "$DEFAULT_LOCATION" == "/" ]]; then
DEFAULT_LOCATION="/config/data"
bashio::log.warning "Could not read the abc home directory from /etc/passwd; defaulting to $DEFAULT_LOCATION"
fi
# Align the shared desktop user (abc) with the configured PUID/PGID before any storage is
# chowned and before any service or s6-setuidgid call resolves abc. The base image's
# init-adduser applies the same remap, but it runs after cont-init, so doing it here first is
@@ -11,8 +38,8 @@ PUID="$(if bashio::config.has_value 'PUID'; then bashio::config 'PUID'; else ech
PGID="$(if bashio::config.has_value 'PGID'; then bashio::config 'PGID'; else echo '1000'; fi)"
# Claude Code refuses bypass-permissions mode under an effective root UID, so bypass mode
# always needs a non-root desktop user.
if [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
# always needs a non-root desktop user. Add-ons without a permission_mode option skip this.
if bashio::config.has_value 'permission_mode' && [ "$(bashio::config 'permission_mode')" = "bypass" ] && [ "$PUID" -eq 0 ]; then
bashio::log.warning "permission_mode: bypass cannot run Claude Code as root; using UID 1000 instead of the configured PUID 0"
PUID=1000
fi
@@ -37,7 +64,7 @@ fi
LOCATION="$(bashio::config 'data_location')"
if [[ "$LOCATION" = "null" || -z "$LOCATION" ]]; then
LOCATION="/data/data"
LOCATION="$DEFAULT_LOCATION"
else
LOCATIONOK=""
for location in "/share" "/config" "/data" "/mnt"; do
@@ -47,7 +74,7 @@ else
done
if [ -z "$LOCATIONOK" ]; then
LOCATION="/data/data"
LOCATION="$DEFAULT_LOCATION"
bashio::log.fatal "Your data_location value can only be set in /share, /config, /data or /mnt. It will be reset to the default location : $LOCATION"
fi
fi
@@ -73,11 +100,15 @@ for file in /etc/s6-overlay/s6-rc.d/*/run; do
fi
done
for folders in /defaults /etc/cont-init.d /etc/services.d /etc/s6-overlay/s6-rc.d; do
if [ -d "$folders" ]; then
find "$folders" -type f -exec sed -i "s|/data/data|$LOCATION|g" {} + &> /dev/null || true
fi
done
# Rewrite the home path baked into the image to the user-chosen one. No-op when data_location
# is left at its default.
if [ "$LOCATION" != "$DEFAULT_LOCATION" ]; then
for folders in /defaults /etc/cont-init.d /etc/services.d /etc/s6-overlay/s6-rc.d; do
if [ -d "$folders" ]; then
find "$folders" -type f -exec sed -i "s|$DEFAULT_LOCATION|$LOCATION|g" {} + &> /dev/null || true
fi
done
fi
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd

View File

@@ -2,7 +2,11 @@
# shellcheck shell=bash
set -e
# The image is Debian-based (apt) and always ships uv, so those are the only installers used.
# Shared by every Selkies-based add-on in this repo (claude_desktop, webtop, webtop_kde) via a
# symlink; keep it add-on agnostic. Every option read here is guarded, so an add-on that does
# not declare a given option simply skips that block.
#
# All three images are Debian/Ubuntu-based, so apt is the only system package manager used.
if bashio::config.has_value 'additional_apps'; then
bashio::log.info "Installing additional apps :"
apt-get update -o Acquire::http::Timeout=10 -o Acquire::https::Timeout=10 &> /dev/null || bashio::log.warning "Unable to update apt package lists"
@@ -13,9 +17,14 @@ if bashio::config.has_value 'additional_apps'; then
fi
if bashio::config.has_value 'additional_pip'; then
if command -v uv &> /dev/null; then
pip_install=(uv pip install --system --break-system-packages)
else
pip_install=(pip install --break-system-packages)
fi
for p in $(bashio::config 'additional_pip' | tr ',' ' '); do
bashio::log.green "... pip: $p"
uv pip install --system --break-system-packages "$p" || bashio::log.fatal "Error: pip package $p failed"
"${pip_install[@]}" "$p" || bashio::log.fatal "Error: pip package $p failed"
done
fi

View File

@@ -1,3 +1,7 @@
## 3.3.0.1 (2026-07-28)
- Fixed searches failing through ingress with `500 Internal Server Error` (#2906, #2646). Home Assistant's ingress proxy re-encodes the query string and passes a space as `+`, along with a bare `:` `/` `?` `@` `!` `$` `'` `(` `)` `*` `,` - all of which Seerr's OpenAPI validator rejects as reserved characters. Nginx now re-encodes them before proxying, so titles such as `Monsters, Inc.`, `Ocean's Eleven`, `Mission: Impossible` and `Who? What?` search correctly. Only ingress was affected; the directly published port 5055 always worked.
## 3.3.0 (2026-06-05)
- Update to latest version from seerr-team/seerr (changelog : https://github.com/seerr-team/seerr/releases)

View File

@@ -56,7 +56,7 @@ COPY ha_automodules.sh /ha_automodules.sh
RUN chmod 744 /ha_automodules.sh && /ha_automodules.sh "$MODULES" && rm /ha_automodules.sh
# Manual apps
ENV PACKAGES="nginx"
ENV PACKAGES="nginx nginx-mod-http-js"
# Automatic apps & bashio
COPY ha_autoapps.sh /ha_autoapps.sh

View File

@@ -96,4 +96,4 @@ schema:
slug: seerr
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/seerr
version: "3.3.0"
version: "3.3.0.1"

View File

@@ -0,0 +1,14 @@
# Load the njs module used by njs/ingress.js.
#
# The nginx-mod-http-js package ships this same file, but it cannot be relied
# on: .templates/ha_automatic_packages.sh moves the rootfs /etc/nginx aside to
# /etc/nginx2 before installing nginx, then does `rm -r /etc/nginx` and restores
# the saved tree afterwards. That deletes anything the package placed under
# /etc/nginx, including its own load_module snippet, and nginx would then fail
# to start with "unknown directive js_import".
#
# Shipping it in the rootfs makes it survive that swap. The filename matches the
# package's on purpose: if the package's copy is ever kept instead, it overwrites
# this one rather than adding a second load_module for the same module, which
# nginx rejects as a duplicate.
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;

View File

@@ -49,6 +49,11 @@ http {
'' close;
}
# Repair the query string mangled by Supervisor's ingress proxy before it
# reaches Seerr's OpenAPI validator. See njs/ingress.js for the full story.
js_import ingress from /etc/nginx/njs/ingress.js;
js_set $ingress_uri ingress.uri;
include /etc/nginx/includes/resolver.conf;
include /etc/nginx/includes/upstream.conf;

View File

@@ -0,0 +1,83 @@
/*
* Repair the query string of a Home Assistant ingress request.
*
* Supervisor proxies ingress traffic with `params=request.query`
* (supervisor/api/ingress.py), so aiohttp/yarl re-encodes an already-decoded
* query string on the way to this add-on. yarl's "safe" set is much wider than
* the one Seerr's express-openapi-validator will accept: yarl emits a space as
* "+" and passes ":", "/", "?", "@", "!", "$", "'", "(", ")", "*" and ","
* through bare, while the validator checks the raw, still-encoded value against
*
* RESERVED_CHARS = /[\:\/\?#\[\]@!\$&\'()\*\+,;=]/
*
* and answers 400 "Parameter '<name>' must be url encoded". Every search for a
* title containing a space or punctuation therefore fails - "Monsters, Inc.",
* "Ocean's Eleven", "Mission: Impossible" - which Seerr's UI reports as a
* 500. The same requests succeed on the directly published port 5055, which
* does not pass through Supervisor.
*
* "?" deserves a note: the validator strips one with `qs.replace('?', '')`
* before testing, so a single bare "?" slips through by accident and only a
* second one ("Who? What?") produces the 400. It is encoded here regardless.
*
* Re-encoding those characters here is lossless, because yarl only ever emits
* them bare when they were literal characters of the value: anything the user
* actually typed that is ambiguous comes through already percent-encoded
* (a typed "+" arrives as "%2B", "&" as "%26", "=" as "%3D").
*
* "&" and "=" are deliberately NOT re-encoded: they are the query string's own
* separators, so a bare one is always structural.
*/
/*
* Every character of the validator's RESERVED_CHARS except "&" and "=", which
* are the query string's own separators and are handled above. Deriving the
* set from what the validator rejects - rather than from what yarl currently
* emits bare - keeps this correct if either side changes its safe set.
*/
var NEEDS_ENCODING = /[:\/?#\[\]@!$'()*,;]/g;
function encodePart(part) {
return part
/* yarl encodes a space as "+"; a literal "+" arrives as "%2B". */
.replace(/\+/g, "%20")
.replace(NEEDS_ENCODING, function (c) {
return "%" + c.charCodeAt(0).toString(16).toUpperCase();
});
}
/*
* Returns the request URI with the path untouched byte-for-byte and only the
* query string repaired. Used as the proxy_pass target.
*/
function uri(r) {
var raw = r.variables.request_uri;
var split = raw.indexOf("?");
if (split < 0) {
return raw;
}
var path = raw.substring(0, split);
var args = raw.substring(split + 1);
/* A bare trailing "?" is forwarded as-is, so the URI stays byte-for-byte. */
if (args === "") {
return raw;
}
var repaired = args
.split("&")
.map(function (pair) {
var eq = pair.indexOf("=");
if (eq < 0) {
return encodePart(pair);
}
return encodePart(pair.substring(0, eq)) + "=" + encodePart(pair.substring(eq + 1));
})
.join("&");
return path + "?" + repaired;
}
export default { uri };

View File

@@ -10,9 +10,11 @@ server {
location ^~ / {
set $app '%%ingress_entry%%';
# Forward the raw request URI exactly as received by this nginx.
# This is the safest way to preserve query-string encoding.
proxy_pass http://127.0.0.1:5055$request_uri;
# Forward the request URI with the path byte-for-byte as received, and
# the query string re-encoded so the characters Supervisor's ingress
# proxy passes through bare (a space as "+", plus ":/@!$'()*,") do not
# trip Seerr's OpenAPI validator. See njs/ingress.js for the details.
proxy_pass http://127.0.0.1:5055$ingress_uri;
proxy_http_version 1.1;
proxy_set_header Referer $http_referer;

View File

@@ -1,3 +1,7 @@
## 4.16-r0-ls95-6 (28-07-2026)
- Share the Selkies startup scripts with the `claude_desktop` add-on by symlink (`20-folders.sh`, `21-gpu_permissions.sh`, `80-configuration.sh`, `90-ingress.sh` and the nginx includes), so the fixes made there now apply here too. This brings in: GPU render-node permissions granted before the graphical services start (fixes `libEGL warning: failed to open /dev/dri/card0: Permission denied` and the resulting "waiting for stream" hang); the s6 envdir and `XDG_RUNTIME_DIR` created up front; the cache redirected to tmpfs; `/tmp/.X11-unix` pre-created so Xorg can bind its socket as a non-root user; the `init-video` and `init-selkies-config` oneshots made non-fatal so a partially permitted device setup no longer crash-loops the add-on; and an ingress config that keeps the correct (non-SSL) nginx server block. The Microsoft Edge install moves to its own webtop-only `81-microsoft_edge.sh`, which also picks up the ownership fixup that previously ran in `20-folders.sh` before Edge was installed and so never matched anything.
- Added support for configuring extra environment variables via the `env_vars` add-on option alongside config.yaml. See https://github.com/alexbelgium/hassio-addons/wiki/Add-Environment-variables-to-your-Addon-2 for details.
## 4.16-r0-ls95-5 (2026-02-23)

View File

@@ -138,5 +138,5 @@ slug: webtop-kde
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: 4.16-r0-ls95-5
version: 4.16-r0-ls95-6
video: true

View File

@@ -1,3 +1,7 @@
## 4.16-r0-ls93.1 (28-07-2026)
- Share the Selkies startup scripts with the `claude_desktop` add-on by symlink (`20-folders.sh`, `21-gpu_permissions.sh`, `80-configuration.sh`, `90-ingress.sh` and the nginx includes), so the fixes made there now apply here too. This brings in: GPU render-node permissions granted before the graphical services start (fixes `libEGL warning: failed to open /dev/dri/card0: Permission denied` and the resulting "waiting for stream" hang); the s6 envdir and `XDG_RUNTIME_DIR` created up front; the cache redirected to tmpfs; `/tmp/.X11-unix` pre-created so Xorg can bind its socket as a non-root user; the `init-video` and `init-selkies-config` oneshots made non-fatal so a partially permitted device setup no longer crash-loops the add-on; and an ingress config that keeps the correct (non-SSL) nginx server block. The Microsoft Edge install moves to its own webtop-only `81-microsoft_edge.sh`, which also picks up the ownership fixup that previously ran in `20-folders.sh` before Edge was installed and so never matched anything.
## 4.16-r0-ls93 (2026-07-21)
- Update to latest version from linuxserver/docker-webtop (changelog : https://github.com/linuxserver/docker-webtop/releases)

View File

@@ -143,5 +143,5 @@ slug: webtop
tmpfs: true
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "4.16-r0-ls93"
version: "4.16-r0-ls93.1"
video: true

View File

@@ -1,83 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2046
set -e
# Define user
PUID=$(bashio::config "PUID")
PGID=$(bashio::config "PGID")
# Set user for microsoft edge if available
if [ -f /usr/bin/microsoft-edge-real ]; then
chown "$PUID:$PGID" /usr/bin/microsoft-edge*
chmod +x /usr/bin/microsoft-edge*
fi
# Check data location
LOCATION=$(bashio::config 'data_location')
if [[ "$LOCATION" = "null" || -z "$LOCATION" ]]; then
# Default location
LOCATION="/config/data_kde"
else
# Check if config is located in an acceptable location
LOCATIONOK=""
for location in "/share" "/config" "/data" "/mnt"; do
if [[ "$LOCATION" == "$location"* ]]; then
LOCATIONOK=true
fi
done
if [ -z "$LOCATIONOK" ]; then
LOCATION="/config/data_kde"
bashio::log.fatal "Your data_location value can only be set in /share, /config or /data (internal to addon). It will be reset to the default location : $LOCATION"
fi
fi
# Set data location
bashio::log.info "Setting data location to $LOCATION"
# Correct home locations
for file in /etc/s6-overlay/s6-rc.d/*/run; do
if [ "$(sed -n '1{/bash/p};q' "$file")" ]; then
sed -i "1a export HOME=$LOCATION" "$file"
sed -i "1a export FM_HOME=$LOCATION" "$file"
fi
done
# Correct home location
for folders in /defaults /etc/cont-init.d /etc/services.d /etc/s6-overlay/s6-rc.d; do
if [ -d "$folders" ]; then
sed -i "s|/config/data_kde|$LOCATION|g" $(find "$folders" -type f) &> /dev/null || true
fi
done
# Change user home
sed -i "s|^\(abc:[^:]*:[^:]*:[^:]*:[^:]*:\)[^:]*|\1$LOCATION|" /etc/passwd
#usermod --home "$LOCATION" abc || true
# Add environment variables
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$LOCATION" > /var/run/s6/container_environment/HOME; fi
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$LOCATION" > /var/run/s6/container_environment/FM_HOME; fi
{
printf "%s\n" "export HOME=\"$LOCATION\""
printf "%s\n" "export FM_HOME=\"$LOCATION\""
} >> ~/.bashrc
# Create folder
echo "Creating $LOCATION"
mkdir -p "$LOCATION"
# Create cache
mkdir -p /.cache
chmod 755 /.cache
if [ -d "/config/.cache" ]; then
cp -rf /config/.cache /.cache
rm -r /config/.cache
fi
ln -sf /config/.cache /.cache
# Set ownership
bashio::log.info "Setting ownership to $PUID:$PGID"
chown -R "$PUID":"$PGID" "$LOCATION"
chmod -R 700 "$LOCATION"

View File

@@ -0,0 +1 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/20-folders.sh

View File

@@ -0,0 +1 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/21-gpu_permissions.sh

View File

@@ -1,72 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2015
set -e
# Install specific apps
if bashio::config.has_value 'additional_apps'; then
bashio::log.info "Installing additional apps :"
# hadolint ignore=SC2005
NEWAPPS=$(bashio::config 'additional_apps')
for packagestoinstall in ${NEWAPPS//,/ }; do
bashio::log.green "... $packagestoinstall"
if command -v "apk" &> /dev/null; then
apk add --no-cache "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
elif command -v "apt" &> /dev/null; then
apt-get install -yqq --no-install-recommends "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
elif command -v "pacman" &> /dev/null; then
pacman --noconfirm -S "$packagestoinstall" &> /dev/null || (bashio::log.fatal "Error : $packagestoinstall not found")
fi
done
fi
# Set TZ
if bashio::config.has_value 'TZ'; then
TIMEZONE=$(bashio::config 'TZ')
bashio::log.info "Setting timezone to $TIMEZONE"
ln -snf /usr/share/zoneinfo/"$TIMEZONE" /etc/localtime
echo "$TIMEZONE" > /etc/timezone
fi || (bashio::log.fatal "Error : $TIMEZONE not found. Here is a list of valid timezones : https://manpages.ubuntu.com/manpages/focal/man3/DateTime::TimeZone::Catalog.3pm.html")
# Set keyboard
if bashio::config.has_value 'KEYBOARD'; then
KEYBOARD=$(bashio::config 'KEYBOARD')
bashio::log.info "Setting keyboard to $KEYBOARD"
if [ -d /var/run/s6/container_environment ]; then printf "%s" "$KEYBOARD" > /var/run/s6/container_environment/KEYBOARD; fi
printf "%s\n" "KEYBOARD=\"$KEYBOARD\"" >> ~/.bashrc
fi || true
# Set password
if bashio::config.has_value 'PASSWORD'; then
bashio::log.info "Setting password to the value defined in options"
PASSWORD=$(bashio::config 'PASSWORD')
passwd -d abc
echo -e "$PASSWORD\n$PASSWORD" | passwd abc
elif ! bashio::config.has_value 'PASSWORD' && [[ -n "$(bashio::addon.port "3000")" ]] && [[ -n $(bashio::addon.port "3001") ]]; then
bashio::log.warning "SEVERE RISK IDENTIFIED"
bashio::log.warning "You are opening an external port but your password is not defined"
bashio::log.warning "You risk being hacked ! Please disable the external ports, or use a password"
fi
# Set password
if bashio::config.true 'install_ms_edge'; then
bashio::log.info "Adding microsoft edge"
# Install edge
apt-get update
echo "**** install edge ****"
apt-get install --no-install-recommends -y ca-certificates
if [ -z ${EDGE_VERSION+x} ]; then
EDGE_VERSION=$(curl -sL https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/ \
| awk -F'(<a href="microsoft-edge-stable_|_amd64.deb\")' '/href=/ {print $2}' | sort --version-sort | tail -1)
fi
curl -o /tmp/edge.deb -L "https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable/microsoft-edge-stable_${EDGE_VERSION}_amd64.deb"
dpkg -I /tmp/edge.deb
apt-get install --no-install-recommends -y /tmp/edge.deb
echo "**** edge docker tweaks ****"
if [ -f /usr/bin/microsoft-edge-stable ]; then
mv /usr/bin/microsoft-edge-stable /usr/bin/microsoft-edge-real
else
mv /usr/bin/microsoft-edge /usr/bin/microsoft-edge-real
fi
mv /helpers/microsoft-edge-stable /usr/bin/
fi

View File

@@ -0,0 +1 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/80-configuration.sh

View File

@@ -0,0 +1,64 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# Webtop-only. Lives here rather than in the shared 80-configuration.sh so the Selkies startup
# scripts stay identical across claude_desktop, webtop and webtop_kde. It also carries the
# ownership fixup that used to sit in 20-folders.sh, which ran before this install and so
# never had anything to match.
if ! bashio::config.true 'install_ms_edge'; then
exit 0
fi
bashio::log.info "Adding microsoft edge"
apt-get update
apt-get install --no-install-recommends -y ca-certificates
# Both requests are bounded and non-fatal. cont-init.d blocks the whole add-on, so an
# unreachable or stalled packages.microsoft.com must not hang or kill startup: the desktop is
# useful without Edge, an add-on stuck before Selkies starts is not.
EDGE_REPO="https://packages.microsoft.com/repos/edge/pool/main/m/microsoft-edge-stable"
if [ -z ${EDGE_VERSION+x} ]; then
EDGE_VERSION=$(curl -sL --fail --connect-timeout 15 --max-time 120 "$EDGE_REPO/" \
| awk -F'(<a href="microsoft-edge-stable_|_amd64.deb\")' '/href=/ {print $2}' | sort --version-sort | tail -1 || true)
fi
if [ -z "$EDGE_VERSION" ]; then
bashio::log.error "Could not determine the latest Microsoft Edge version; skipping the Edge install"
exit 0
fi
if ! curl -o /tmp/edge.deb -L --fail --connect-timeout 15 --max-time 600 \
"$EDGE_REPO/microsoft-edge-stable_${EDGE_VERSION}_amd64.deb"; then
bashio::log.error "Downloading Microsoft Edge ${EDGE_VERSION} failed; skipping the Edge install"
exit 0
fi
dpkg -I /tmp/edge.deb
apt-get install --no-install-recommends -y /tmp/edge.deb
bashio::log.info "Applying edge docker tweaks"
if [ -f /usr/bin/microsoft-edge-stable ]; then
mv /usr/bin/microsoft-edge-stable /usr/bin/microsoft-edge-real
elif [ -f /usr/bin/microsoft-edge ]; then
mv /usr/bin/microsoft-edge /usr/bin/microsoft-edge-real
fi
if [ -f /helpers/microsoft-edge-stable ]; then
mv /helpers/microsoft-edge-stable /usr/bin/
fi
# The wrapper and the real binary must be usable by the desktop user, whose identity
# 20-folders.sh has already settled by the time this runs. Guarded against an empty glob:
# without nullglob the literal pattern would reach chown, and `set -e` would then abort
# container startup rather than just skipping a fixup that has nothing to do.
shopt -s nullglob
edge_binaries=(/usr/bin/microsoft-edge*)
shopt -u nullglob
if [ "${#edge_binaries[@]}" -gt 0 ]; then
chown "$(id -u abc):$(id -g abc)" "${edge_binaries[@]}"
chmod +x "${edge_binaries[@]}"
else
bashio::log.warning "Edge install reported success but no /usr/bin/microsoft-edge* binary is present"
fi

View File

@@ -1,28 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# nginx Path
NGINX_CONFIG=/etc/nginx/sites-available/ingress.conf
SUBFOLDER="$(bashio::addon.ingress_entry)"
# Copy template
cp /defaults/default.conf "${NGINX_CONFIG}"
# Remove ssl part
awk -v n=4 '/server/{n--}; n > 0' "${NGINX_CONFIG}" > tmpfile
mv tmpfile "${NGINX_CONFIG}"
# Remove ipv6
sed -i '/listen \[::\]/d' "${NGINX_CONFIG}"
# Add ingress parameters
sed -i "s|3000|$(bashio::addon.ingress_port)|g" "${NGINX_CONFIG}"
sed -i "s|CWS|8082|g" "${NGINX_CONFIG}"
sed -i "s|SUBFOLDER|/|g" "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a proxy_set_header Accept-Encoding "";' "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a sub_filter_once off;' "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a sub_filter_types *;' "${NGINX_CONFIG}"
sed -i '/proxy_buffering/a sub_filter "vnc/index.html?autoconnect" "vnc/index.html?path=%%path%%/websockify?autoconnect";' "${NGINX_CONFIG}"
sed -i "s|%%path%%|${SUBFOLDER:1}|g" "${NGINX_CONFIG}"
# Enable ingress
cp "${NGINX_CONFIG}" /etc/nginx/sites-enabled

View File

@@ -0,0 +1 @@
../../../../claude_desktop/rootfs/etc/cont-init.d/90-ingress.sh

View File

@@ -1,96 +0,0 @@
types {
text/html html htm shtml;
text/css css;
text/xml xml;
image/gif gif;
image/jpeg jpeg jpg;
application/javascript js;
application/atom+xml atom;
application/rss+xml rss;
text/mathml mml;
text/plain txt;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/x-component htc;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-icon ico;
image/x-jng jng;
image/x-ms-bmp bmp;
font/woff woff;
font/woff2 woff2;
application/java-archive jar war ear;
application/json json;
application/mac-binhex40 hqx;
application/msword doc;
application/pdf pdf;
application/postscript ps eps ai;
application/rtf rtf;
application/vnd.apple.mpegurl m3u8;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.ms-excel xls;
application/vnd.ms-fontobject eot;
application/vnd.ms-powerpoint ppt;
application/vnd.oasis.opendocument.graphics odg;
application/vnd.oasis.opendocument.presentation odp;
application/vnd.oasis.opendocument.spreadsheet ods;
application/vnd.oasis.opendocument.text odt;
application/vnd.openxmlformats-officedocument.presentationml.presentation
pptx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xlsx;
application/vnd.openxmlformats-officedocument.wordprocessingml.document
docx;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-perl pl pm;
application/x-pilot prc pdb;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert der pem crt;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xspf+xml xspf;
application/zip zip;
application/octet-stream bin exe dll;
application/octet-stream deb;
application/octet-stream dmg;
application/octet-stream iso img;
application/octet-stream msi msp msm;
audio/midi mid midi kar;
audio/mpeg mp3;
audio/ogg ogg;
audio/x-m4a m4a;
audio/x-realaudio ra;
video/3gpp 3gpp 3gp;
video/mp2t ts;
video/mp4 mp4;
video/mpeg mpeg mpg;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-m4v m4v;
video/x-mng mng;
video/x-ms-asf asx asf;
video/x-ms-wmv wmv;
video/x-msvideo avi;
}

View File

@@ -0,0 +1 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/mime.types

View File

@@ -1,15 +0,0 @@
proxy_http_version 1.1;
proxy_ignore_client_abort off;
proxy_read_timeout 86400s;
proxy_redirect off;
proxy_send_timeout 86400s;
proxy_max_temp_file_size 0;
proxy_set_header Accept-Encoding "";
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $http_host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-NginX-Proxy true;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -0,0 +1 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/proxy_params.conf

View File

@@ -1 +0,0 @@
resolver 127.0.0.11 ipv6=off;

View File

@@ -0,0 +1 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/resolver.conf

View File

@@ -1,6 +0,0 @@
root /dev/null;
server_name $hostname;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Robots-Tag none;

View File

@@ -0,0 +1 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/server_params.conf

View File

@@ -1,9 +0,0 @@
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA;
ssl_ecdh_curve secp384r1;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;

View File

@@ -0,0 +1 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/ssl_params.conf

View File

@@ -1,3 +0,0 @@
upstream backend {
server 127.0.0.1:8080;
}

View File

@@ -0,0 +1 @@
../../../../../claude_desktop/rootfs/etc/nginx/includes/upstream.conf