From 80b94fb21f35ebb6f65226544f9429308c1d2096 Mon Sep 17 00:00:00 2001 From: librelad Date: Tue, 18 Aug 2026 20:54:49 +0100 Subject: [PATCH] fix(stoat): write every bind-mounted file before anything that can fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install read the generated LiveKit credentials with a plain grep, but secrets.env is chmod 600 and owned by the docker install user while the hooks run as the manager — so the read returned nothing, the hook errored out, and Caddyfile and livekit.yml were never written. Compose then refused to start, because a bind mount whose source does not exist is not a soft failure. Read secrets through runFileOp, and reorder so the Caddyfile and the three URL-bearing files are written first: any step that can fail now comes after every mount source already exists. The missing LiveKit keys are downgraded from fatal to a warning for the same reason — losing voice is worth reporting, but it is no reason to take the other fifteen services down with it. Co-Authored-By: Claude Opus 5 --- .../stoat/scripts/stoat_install_hooks.sh | 46 +++++++++++-------- scripts/app/auth_adapter.sh | 29 +++++++++++- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/containers/stoat/scripts/stoat_install_hooks.sh b/containers/stoat/scripts/stoat_install_hooks.sh index 156f11d..c52f27a 100644 --- a/containers/stoat/scripts/stoat_install_hooks.sh +++ b/containers/stoat/scripts/stoat_install_hooks.sh @@ -192,31 +192,44 @@ stoat_install_post_compose() "$app_dir/data/caddy-data" "$app_dir/data/caddy-config") checkSuccess "Creating $app_name data folders" - _stoatWriteSecrets "$app_dir/secrets.env" - - # Read the LiveKit credentials back out — either the ones just generated or - # the ones preserved from a previous install — because livekit.yml has to - # carry the same pair the API is configured with. - local livekit_key livekit_secret - livekit_key=$(grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__KEY='\K[^']*" "$app_dir/secrets.env" 2>/dev/null) - livekit_secret=$(grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__SECRET='\K[^']*" "$app_dir/secrets.env" 2>/dev/null) - if [[ -z "$livekit_key" || -z "$livekit_secret" ]]; then - isError "Could not read the LiveKit credentials from secrets.env — voice will not work." - return 1 - fi + # Ordering rule for everything below: every file bind-mounted into a + # container must be written before the first step that could fail. A missing + # mount source is not a soft failure — docker either creates a directory in + # its place or refuses to start the container, and both outcomes outlive the + # install and break every later run. + result=$(copyResource "$app_name" "Caddyfile" "" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) + checkSuccess "Copying Caddyfile to $app_dir" local video_enabled="" [[ "$CFG_STOAT_ENABLE_VIDEO" != "false" ]] && video_enabled="true" # The port is not allocated yet, so this is a guess whenever there is no - # domain. The files still have to exist now: they are bind-mounted, and - # docker would silently create directories in their place otherwise. - # stoat_install_post_start rewrites them once the real port is known. + # domain; stoat_install_post_start rewrites these once it is known. local base base=$(_stoatBaseUrl "$app_name") _stoatWriteUrlFiles "$app_dir" "$base" "$video_enabled" checkSuccess "Writing .env.web, stoat.json and Revolt.toml for $base" + _stoatWriteSecrets "$app_dir/secrets.env" + + # Read the LiveKit credentials back out — either the ones just generated or + # the ones preserved from a previous install — because livekit.yml has to + # carry the same pair the API is configured with. + # + # Read via runFileOp: secrets.env is chmod 600 and owned by the docker + # install user, while these hooks run as the manager, so a plain grep gets + # EACCES and silently yields nothing. + local livekit_key livekit_secret + livekit_key=$(runFileOp grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__KEY='\K[^']*" "$app_dir/secrets.env" 2>/dev/null) + livekit_secret=$(runFileOp grep -oP "REVOLT__API__LIVEKIT__NODES__WORLDWIDE__SECRET='\K[^']*" "$app_dir/secrets.env" 2>/dev/null) + if [[ -z "$livekit_key" || -z "$livekit_secret" ]]; then + # Deliberately not fatal. livekit.yml still gets written below so the + # bind mount is a file; voice is broken until the keys are fixed, but + # the other fifteen services come up and text chat works. + isError "Could not read the LiveKit credentials from secrets.env — voice will not work." + isNotice "Fix the keys in $app_dir/secrets.env and livekit.yml, then restart $app_name." + fi + # use_external_ip lets LiveKit discover the address to advertise for WebRTC. # The port range matches the literal UDP mapping in the compose file; change # one and you must change the other. @@ -243,9 +256,6 @@ webhook: EOF checkSuccess "Writing livekit.yml" - result=$(copyResource "$app_name" "Caddyfile" "" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) - checkSuccess "Copying Caddyfile to $app_dir" - runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir" checkSuccess "Setting ownership on the $app_name install directory" } diff --git a/scripts/app/auth_adapter.sh b/scripts/app/auth_adapter.sh index 5cea55f..d1214b0 100644 --- a/scripts/app/auth_adapter.sh +++ b/scripts/app/auth_adapter.sh @@ -36,12 +36,39 @@ authAdapterCall() { } # Persist a value to CFG__ in the per-app config file. +# +# Adapters call with the bare name (ADMIN_PASSWORD), but a key whose value is +# generated carries a slot number (CFG__ADMIN_PASSWORD_1). Resolve to the +# slot when the bare key isn't there, so an adapter never has to know how a +# credential is numbered — and so adding a slot to a config can't quietly +# disconnect the adapter that writes it. +# +# updateConfigOption only rewrites a key that already exists; handed a name that +# is absent it just prints a notice. That is a silent failure from the caller's +# side: the app's password really did change, but the config and the WebUI carry +# on showing the old one. Hence the explicit return 1 and warning below. authPersistCfg() { local app="$1" key="$2" value="$3" local cfg="${containers_dir}${app}/${app}.config" [[ ! -f "$cfg" ]] && cfg="${install_containers_dir}/${app}/${app}.config" [[ ! -f "$cfg" ]] && return 1 - updateConfigOption "CFG_${app^^}_${key}" "$value" "$cfg" + + local app_upper="${app^^}" + app_upper="${app_upper//-/_}" + local name="CFG_${app_upper}_${key}" + + if ! grep -q "^${name}=" "$cfg" 2>/dev/null; then + local slotted + slotted=$(grep -oE "^CFG_${app_upper}_${key}_[0-9]+=" "$cfg" 2>/dev/null | head -1) + if [[ -n "$slotted" ]]; then + name="${slotted%=}" + else + isNotice "$app has no ${name} (or numbered slot) in $(basename "$cfg") — the new value was applied to the app but not recorded in its config." + return 1 + fi + fi + + updateConfigOption "$name" "$value" "$cfg" } # Read a tool-modal arg (pipe-encoded) and unescape pipes.