From e90eb76b369b87a936cf1f313113aff9056683dd Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Tue, 25 Aug 2026 21:01:38 +0000 Subject: [PATCH 1/5] Port GameServer appliance to Trixie Install the official game catalog wrapper and LinuxGSM v26.2.0 from pinned, verified artifacts. Preserve custom first-boot repositories as an explicit trust boundary, retain non-root server processes, and move temporary installation privilege into a validated sudoers drop-in. Add a supervised dual-channel updater and acceptance coverage for the management surface, 120-entry server catalog, exact source state, and systemd lifecycle through a lightweight fixture. Static syntax, source transformation, provenance, and evidence-schema gates pass; exact runtime remains pending the known shared runner blocker. --- README.rst | 10 +- changelog | 14 ++ conf.d/downloads | 27 +++ conf.d/main | 92 ++++++++--- docs/usage.rst | 40 ++--- docs/v19.0-testing.md | 68 ++++++++ .../plugins.d/Game_Server/update_list.py | 59 ++----- overlay/usr/lib/inithooks/bin/gameserver.py | 76 +++++---- .../lib/inithooks/firstboot.d/40gameserver | 8 +- overlay/usr/local/bin/gameserver-init | 8 +- overlay/usr/local/bin/gameserver-start | 3 +- overlay/usr/local/bin/gameserver-stop | 3 +- .../usr/local/sbin/turnkey-gameserver-update | 156 ++++++++++++++++++ plan/main | 14 ++ tests/v19.sh | 118 +++++++++++++ 15 files changed, 551 insertions(+), 145 deletions(-) create mode 100755 conf.d/downloads create mode 100644 docs/v19.0-testing.md create mode 100755 overlay/usr/local/sbin/turnkey-gameserver-update create mode 100755 tests/v19.sh diff --git a/README.rst b/README.rst index ee86364..35cb4f7 100644 --- a/README.rst +++ b/README.rst @@ -15,9 +15,11 @@ and on top of that: - Managing game servers using `Linux Gameservers`_: - - Downloads newest version during first boot to ensure best possible game - support. - - Wrapper for `LinuxGSM`_ with support for up to 100 games. + - Installs a verified game catalog and LinuxGSM stable release. + - Wrapper for `LinuxGSM`_ with support for more than 100 game servers. + - ``turnkey-gameserver-update`` checks or applies verified catalog and + LinuxGSM updates. Individual game servers retain their LinuxGSM update + command. - Fully automatic or interactive game server selection: @@ -31,7 +33,7 @@ and on top of that: Credentials *(passwords set at first boot)* ------------------------------------------- -- Webmin, SSH, Shellinabox: username **root** +- Webmin, SSH: username **root** - Game server: username **gameuser** .. _TurnKey GNU/Linux: https://www.turnkeylinux.org/ diff --git a/changelog b/changelog index 0bf46ba..1a183a3 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,17 @@ +turnkey-gameserver-19.0 (1) turnkey; urgency=low + + * Port the appliance to Debian 13 (Trixie). + + * Pin the official game catalog wrapper and LinuxGSM v26.2.0 bootstrap with + verified source records. + + * Add a supervised updater for the wrapper master and LinuxGSM stable tags. + + * Preserve interactive and unattended game selection, Configuration Console + management, non-root game processes, and systemd service lifecycle. + + -- TurnKey Linux release engineering Tue, 25 Aug 2026 22:30:00 +0000 + turnkey-gameserver-18.1 (1) turnkey; urgency=low * v18.1 rebuild - includes latest Debian & TurnKey packages. diff --git a/conf.d/downloads b/conf.d/downloads new file mode 100755 index 0000000..2d1ad5c --- /dev/null +++ b/conf.d/downloads @@ -0,0 +1,27 @@ +#!/bin/bash -e + +set -o pipefail +set -x + +SRC=/usr/local/src +WRAPPER_COMMIT=d2017be9f56db0da2a1c45651e656f47bc7ce42a +WRAPPER_ARCHIVE=linux-gameservers-$WRAPPER_COMMIT.zip +WRAPPER_SHA256=302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983 +LINUXGSM_VERSION=v26.2.0 +LINUXGSM_BOOTSTRAP=linuxgsm-$LINUXGSM_VERSION.sh +LINUXGSM_SHA256=0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd + +proxy_args=() +if [ -n "${FAB_HTTP_PROXY:-}" ]; then + proxy_args=(--proxy "$FAB_HTTP_PROXY") +fi + +curl -LfsS "${proxy_args[@]}" \ + "https://github.com/jesinmat/linux-gameservers/archive/$WRAPPER_COMMIT.zip" \ + -o "$SRC/$WRAPPER_ARCHIVE" +curl -LfsS "${proxy_args[@]}" \ + "https://raw.githubusercontent.com/GameServerManagers/LinuxGSM/$LINUXGSM_VERSION/linuxgsm.sh" \ + -o "$SRC/$LINUXGSM_BOOTSTRAP" + +printf '%s %s\n' "$WRAPPER_SHA256" "$SRC/$WRAPPER_ARCHIVE" | sha256sum -c - +printf '%s %s\n' "$LINUXGSM_SHA256" "$SRC/$LINUXGSM_BOOTSTRAP" | sha256sum -c - diff --git a/conf.d/main b/conf.d/main index 5c5b78f..38e7b92 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,37 +1,77 @@ -#!/bin/sh -ex +#!/bin/bash -e -GAMEUSER="gameuser" -GAME_REPO_DIR="/root/gameservers" -GAME_REPO_URL="https://github.com/jesinmat/linux-gameservers.git" +set -o pipefail +set -x -# Add i386 arch (common for lots of games) and install lib32gcc1 -dpkg --add-architecture i386 -dir=/etc/apt/sources.list.d -for list in $dir/sources.list $dir/security.sources.list; do - sed -i "\|turnkeylinux.org| s|\[|\[arch=amd64, |" $list +GAMEUSER=gameuser +GAME_REPO_DIR=/root/gameservers +SRC=/usr/local/src +WRAPPER_COMMIT=d2017be9f56db0da2a1c45651e656f47bc7ce42a +WRAPPER_ARCHIVE=linux-gameservers-$WRAPPER_COMMIT.zip +WRAPPER_SHA256=302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983 +LINUXGSM_VERSION=v26.2.0 +LINUXGSM_TAG_COMMIT=bded3376bc44d20b89fcc6c32f23a66347fffec6 +LINUXGSM_BOOTSTRAP=linuxgsm-$LINUXGSM_VERSION.sh +LINUXGSM_SHA256=0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd + +# Older game servers still need i386 libraries. Restrict TurnKey package +# sources to amd64 before enabling Debian multiarch. +for source_file in /etc/apt/sources.list.d/*; do + [ -f "$source_file" ] || continue + grep -q 'turnkeylinux.org' "$source_file" || continue + if grep -q '^Types:' "$source_file"; then + if grep -q '^Architectures:' "$source_file"; then + sed -i 's/^Architectures:.*/Architectures: amd64/' "$source_file" + else + sed -i '/^Types:/a Architectures: amd64' "$source_file" + fi + else + sed -i -E '/turnkeylinux\.org/ { /\[[^]]*arch=/! s|^deb[[:space:]]+|deb [arch=amd64] |; }' "$source_file" + fi done +dpkg --add-architecture i386 apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y lib32gcc-s1 -# Create GAMEUSER as (limited) sudo user (initially commented out - running -# gameserver-init will enable it; then disable it afterwards). useradd -m -s /bin/bash "$GAMEUSER" -match="# Cmnd alias specification" -line1="Cmnd_Alias APT_GET = /usr/bin/apt-get" -line2="Cmnd_Alias TRUE = /usr/bin/true" -line3="#$GAMEUSER ALL=(ALL) NOPASSWD: APT_GET, TRUE" -for line in "$line1" "$line2"; do - sed -i "\|$match| a $line" /etc/sudoers -done -echo >> /etc/sudoers -echo "$line3" >> /etc/sudoers +cat > /etc/sudoers.d/gameserver-install < /etc/ssh/sshd_config.d/turnkey-gameserver.conf + +unzip -q "$SRC/$WRAPPER_ARCHIVE" -d /root +mv "/root/linux-gameservers-$WRAPPER_COMMIT" "$GAME_REPO_DIR" +rm -f "$SRC/$WRAPPER_ARCHIVE" + +install -d -m 0755 /usr/local/share/turnkey-gameserver +install -m 0755 "$SRC/$LINUXGSM_BOOTSTRAP" \ + /usr/local/share/turnkey-gameserver/linuxgsm.sh +rm -f "$SRC/$LINUXGSM_BOOTSTRAP" -# disable SSH login for GAMEUSER -echo "DenyUsers $GAMEUSER" >> /etc/ssh/sshd_config +# Keep the wrapper on Debian package names and the verified LinuxGSM release. +sed -i 's/bsdmainutils/bsdextrautils/' "$GAME_REPO_DIR/auto_install.sh" +sed -i '\|run_as_user "wget -O .*https://linuxgsm.sh"|c\ install -o "$GAMEUSER" -g "$GAMEUSER" -m 0755 /usr/local/share/turnkey-gameserver/linuxgsm.sh "$LGSM_PATH"' \ + "$GAME_REPO_DIR/auto_install.sh" +grep -q 'install -o "$GAMEUSER".*turnkey-gameserver/linuxgsm.sh' \ + "$GAME_REPO_DIR/auto_install.sh" -git clone --depth=1 "$GAME_REPO_URL" "$GAME_REPO_DIR" +cat > /usr/local/share/turnkey-gameserver/source <`_ list is missing, you need to update the gameserver list. There are two methods to update the gameserver list, through the Configuration Console or updating the repo from the CLI. +The Configuration Console and CLI use the same verified update channel for the +game catalog and LinuxGSM bootstrap. @@ -81,26 +82,16 @@ Method 1: Method 2: ^^^^^^^^^^^ -#. SSH into the appliance and stop the service:: - - ~# systemctl stop gameserver - -#. Change to gameservers directory:: - - ~# cd /root/gameservers/ +#. SSH into the appliance and check the official channels:: -#. Pull down the latest from the git repo:: + ~# turnkey-gameserver-update --check - ~/gameservers# git pull origin master +#. Apply the verified catalog and LinuxGSM release:: -#. Start the gameserver service:: - - ~/gameservers# systemctl start gameserver - -Done! + ~# turnkey-gameserver-update --apply Update game server version ----------------------------- +-------------------------- If there is an update to the game after deploying your server, you may need to update the server to allow clients with the newer version to connect. Like with updating the game server list, this can either be done through the GUI or manually. Here are instructions for the two methods: Method 1: @@ -127,21 +118,14 @@ Method 2: ~# systemctl stop gameserver -#. Change to gameservers directory:: - - ~# cd /root/gameservers/ - -#. Update your game server (replace ```` with the Code from step 1):: - - ~# ./auto_install.sh -g -u gameuser -p /home/gameuser/gameserver - - e.g. for a Satisfactory server (code ``sf``) +#. Check and update your game server (replace ```` with its code):: - ``~# ./auto_install.sh -g sf -u gameuser -p /home/gameuser/gameserver`` + ~# sudo -H -u gameuser /home/gameuser/gameserver/server check-update + ~# sudo -H -u gameuser /home/gameuser/gameserver/server update #. Start the game server back up:: - ~# ./sfserver start + ~# systemctl start gameserver The server should now be updated to the latest version of the game. diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..b690258 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,68 @@ +# GameServer v19 migration evidence + +## Source and state boundaries + +Debian supplies Nginx, the 32-bit compatibility library, and all management +dependencies. The game catalog wrapper and LinuxGSM are application sources +that Debian does not package for this appliance contract. + +The build uses these exact official upstream sources: + +- `jesinmat/linux-gameservers` master commit + `d2017be9f56db0da2a1c45651e656f47bc7ce42a` +- Wrapper archive SHA256 + `302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983` +- LinuxGSM `v26.2.0` tag commit + `bded3376bc44d20b89fcc6c32f23a66347fffec6` +- LinuxGSM bootstrap SHA256 + `0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd` + +LinuxGSM v26.2.0 includes Debian 13 dependency metadata and explicit handling +for game servers whose vendor binaries do not support newer distributions. +LinuxGSM accepts private vulnerability reports through its GitHub security +advisory form. The wrapper has no separate published security process, so its +official repository issue and maintainer channels are the available route. + +The default source record binds both upstreams. Selecting a custom wrapper +repository at first boot changes that record to the custom channel, and the +official updater then stops with an explicit custom-source boundary instead of +overwriting it. + +## Acceptance crosswalk + +| Appliance contract | `tests/v19.sh` evidence | +| --- | --- | +| Management UI | Loads the Nginx GameServer landing page, its Webmin link, the selector command, and the Configuration Console update action. | +| Supported server list | Runs the installed wrapper list, requires at least 100 entries, and checks a known lightweight definition. | +| Catalog and LinuxGSM update | Resolves the official wrapper master and latest stable LinuxGSM tag, verifies exact candidate artifacts, and exercises the apply dry run. | +| Service lifecycle | Starts and stops the systemd unit against a disposable non-root fixture and observes both actions. | +| Non-root server boundary | Runs the fixture through the same `runuser` start and stop scripts used by installed servers. | +| Automatic and interactive selection | Checks the installed `gameserver-init` entry point and catalog consumed by both paths. | +| TurnKey platform surfaces | Core supplies SSH, Webmin, TLS, backups, and first-boot integration. The appliance test covers GameServer-specific behavior. | + +The runtime test emits exactly the seven Wave 2 result keys: package source, +installed version, runtime checks, updater command, updater result, updater +channel, and integrity evidence. + +## Candidate status + +The candidate passed source and static gates. An exact Wave 2 run was not +started because the shared runner has a known private-source cleanup blocker. +The committed candidate is ready for its exact container run once that shared +runner is available. + +Product-fix accounting is 0 of 3. Source and test corrections do not consume a +product loop. + +## Deferred issues + +- MEDIUM: The appliance catalog has 120 definitions but its latest wrapper + commit predates the current LinuxGSM release. The updater checks both + channels independently, but a newly added LinuxGSM server still needs a + wrapper catalog entry before it appears in the appliance selector. +- MEDIUM: The acceptance fixture proves the non-root systemd lifecycle without + downloading a vendor game binary. Game downloads differ substantially in + size, licensing, authentication, and network behavior, so one vendor binary + would not prove the broader catalog. +- LOW: The test checks the real landing page and Configuration Console action + boundary without automating the terminal dialog renderer. diff --git a/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py b/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py index 649798f..0f1ebf6 100755 --- a/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py +++ b/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py @@ -1,51 +1,20 @@ -'''Update LinuxGameservers management software''' -import os -import subprocess - -GAME_REPO_DIR="/root/gameservers" -GAME_REPO_URL="https://github.com/jesinmat/linux-gameservers.git" - -def is_update_available(): - ret = subprocess.run( - ['git', 'ls-remote', 'origin', 'refs/heads/master'], - capture_output=True, text=True) - if ret.returncode != 0: - return ('error', ret.stderr) - - # get first line of output, split by spaces (should be a commit id of - # remote head) - remote_head = ret.stdout.splitlines()[0].strip().split()[0] +"""Update the verified game-server catalog and LinuxGSM bootstrap.""" - ret = subprocess.run(['git', 'rev-parse', 'HEAD'], - capture_output=True, text=True) - if ret.returncode != 0: - return ('error', ret.stderr) +import subprocess - local_head = ret.stdout.splitlines()[0].strip() - return ('ok', local_head != remote_head) def run(): - curdir = os.getcwd() - os.chdir(GAME_REPO_DIR) - - ok, data = is_update_available() - if ok == 'error': - console.msgbox('Error', - 'An error occured checking for updates:\n', - data) - os.chdir(curdir) + console.infobox('Checking the official game-server update channels...') + result = subprocess.run( + ['/usr/local/sbin/turnkey-gameserver-update', '--apply'], + capture_output=True, + text=True, + ) + if result.returncode != 0: + console.msgbox( + 'Update Error', + 'The game-server catalog update failed:\n' + result.stderr, + ) return - if data: - console.infobox('Updates found, performing updates...') - ret = subprocess.run(['git', 'pull'], capture_output=True, text=True) - - if ret.returncode != 0: - console.msgbox('Error', - 'An error occured during update:\n', - ret.stderr) - else: - console.msgbox('Update', 'Update success') - else: - console.msgbox('Updates', 'No updates found') - os.chdir(curdir) + console.msgbox('Update', result.stdout) diff --git a/overlay/usr/lib/inithooks/bin/gameserver.py b/overlay/usr/lib/inithooks/bin/gameserver.py index b54d0ee..0d037d4 100755 --- a/overlay/usr/lib/inithooks/bin/gameserver.py +++ b/overlay/usr/lib/inithooks/bin/gameserver.py @@ -9,9 +9,16 @@ import os import sys import getopt +import shutil import subprocess +import tempfile from libinithooks.dialog_wrapper import Dialog +DEFAULT_GAMESERVER_REPO = 'https://github.com/jesinmat/linux-gameservers.git' +DEFAULT_GAMESERVER_BRANCH = 'master' +GAME_REPO_DIR = '/root/gameservers' +SOURCE_RECORD = '/usr/local/share/turnkey-gameserver/source' + def usage(s=None): if s: print("Error:", s, file=sys.stderr) @@ -26,9 +33,6 @@ def main(): except getopt.GetoptError as e: usage(e) - default_gameserver_repo = 'https://github.com/jesinmat/linux-gameservers.git' - default_gameserver_branch = 'master' - gameserver_repo = "" gameserver_branch = "" for opt, val in opts: @@ -50,48 +54,52 @@ def main(): ok, gameserver_repo = dialog.inputbox( 'TKL Gameserver', 'Choose gameserver repo url', - default_gameserver_repo) + DEFAULT_GAMESERVER_REPO) if not ok: - gameserver_repo = default_gameserver_repo + gameserver_repo = DEFAULT_GAMESERVER_REPO if not gameserver_branch: ok, gameserver_branch = dialog.inputbox( 'TKL Gameserver', 'Choose gameserver branch', - default_gameserver_branch) + DEFAULT_GAMESERVER_BRANCH) if not ok: - gameserver_branch = default_gameserver_branch + gameserver_branch = DEFAULT_GAMESERVER_BRANCH else: - gameserver_repo = default_gameserver_repo - gameserver_branch = default_gameserver_branch + gameserver_repo = DEFAULT_GAMESERVER_REPO + gameserver_branch = DEFAULT_GAMESERVER_BRANCH - needs_pull = False - old_dir = os.getcwd() - if gameserver_repo != default_gameserver_repo: - os.chdir('/root/gameservers') - subprocess.run([ - 'git', 'remote', 'set-url', 'origin', - gameserver_repo - ]) - needs_pull = True - if gameserver_branch != default_gameserver_branch: - os.chdir('/root/gameservers') - subprocess.run([ - 'git', 'fetch' - ]) - subprocess.run([ - 'git', 'checkout', '--track', - f'origin/{gameserver_branch}', - ]) - needs_pull = True + if (gameserver_repo, gameserver_branch) == ( + DEFAULT_GAMESERVER_REPO, DEFAULT_GAMESERVER_BRANCH): + return - if needs_pull: - os.chdir('/root/gameservers') + temp_dir = tempfile.mkdtemp(prefix='.gameservers-', dir='/root') + candidate_dir = os.path.join(temp_dir, 'repo') + try: subprocess.run([ - 'git', 'pull' - ]) - - os.chdir(old_dir) + 'git', 'clone', '--depth=1', '--branch', gameserver_branch, + gameserver_repo, candidate_dir, + ], check=True) + if not os.path.isfile(os.path.join(candidate_dir, 'auto_install.sh')): + raise RuntimeError('custom repository has no auto_install.sh') + if not os.path.isdir(os.path.join(candidate_dir, 'games')): + raise RuntimeError('custom repository has no games directory') + commit = subprocess.run([ + 'git', '-C', candidate_dir, 'rev-parse', 'HEAD', + ], check=True, capture_output=True, text=True).stdout.strip() + + shutil.rmtree(GAME_REPO_DIR) + os.replace(candidate_dir, GAME_REPO_DIR) + with open(SOURCE_RECORD, 'w', encoding='utf-8') as source_record: + source_record.write( + 'wrapper_channel=custom git repository\n' + f'wrapper_repository={gameserver_repo}\n' + f'wrapper_ref={gameserver_branch}\n' + f'wrapper_commit={commit}\n' + 'linuxgsm_channel=custom wrapper policy\n' + ) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) if __name__ == '__main__': main() diff --git a/overlay/usr/lib/inithooks/firstboot.d/40gameserver b/overlay/usr/lib/inithooks/firstboot.d/40gameserver index 7476fef..6c0e111 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/40gameserver +++ b/overlay/usr/lib/inithooks/firstboot.d/40gameserver @@ -3,10 +3,10 @@ . /etc/default/inithooks -[ -e $INITHOOKS_CONF ] && . $INITHOOKS_CONF +[ -e "$INITHOOKS_CONF" ] && . "$INITHOOKS_CONF" GAMEUSER=${GAMEUSER:-gameuser} -$INITHOOKS_PATH/bin/setpass.py $GAMEUSER --pass="$APP_PASS" +$INITHOOKS_PATH/bin/setpass.py "$GAMEUSER" --pass="$APP_PASS" unset skip_init # if sec updates set (or turnkey-init-fence is active), then we must be running @@ -16,6 +16,8 @@ if [[ -n "$SEC_UPDATES" ]] || systemctl -q is-active turnkey-init-fence ; then APP_GAMESERVER_BRANCH="${APP_GAMESERVER_BRANCH:-master}" [[ -n "$GAME" ]] || skip_init=true fi -$INITHOOKS_PATH/bin/gameserver.py --gameserver-repo="$APP_GAMESERVER_REPO" --gameserver-branch="$APP_GAMESERVER_BRANCH" +$INITHOOKS_PATH/bin/gameserver.py \ + --gameserver-repo="$APP_GAMESERVER_REPO" \ + --gameserver-branch="$APP_GAMESERVER_BRANCH" [[ -n "$skip_init" ]] || /usr/local/bin/gameserver-init diff --git a/overlay/usr/local/bin/gameserver-init b/overlay/usr/local/bin/gameserver-init index 86647b2..0480537 100755 --- a/overlay/usr/local/bin/gameserver-init +++ b/overlay/usr/local/bin/gameserver-init @@ -3,11 +3,12 @@ GAMEUSER="gameuser" LOGFILE="/var/log/gameserver/install.log" GAME_REPO_DIR="/root/gameservers" +SUDOERS_FILE="/etc/sudoers.d/gameserver-install" clean_exit() { exit_code=$1 shift - sed -i "\|^$GAMEUSER| s|^|#|" /etc/sudoers + sed -i "\|^$GAMEUSER| s|^|#|" "$SUDOERS_FILE" if [[ $exit_code -ne 0 ]]; then rm -f /etc/gameserver/gameserver echo @@ -48,7 +49,7 @@ install_game() { if [ ! -f "/etc/gameserver/installation.done" ]; then conf=/etc/gameserver/gameserver [[ -f "$conf" ]] || clean_exit 1 "Conf file ($conf) not found." - . $conf + . "$conf" exit_code=0 cd "$GAME_REPO_DIR" chmod +x "auto_install.sh" @@ -70,7 +71,8 @@ install_game() { install_exit_code=0 trap 'clean_exit $?' INT TERM ERR -sed -i "\|$GAMEUSER| s|^\s*#||" /etc/sudoers +sed -i "\|$GAMEUSER| s|^[[:space:]]*#||" "$SUDOERS_FILE" +visudo -cf "$SUDOERS_FILE" select_game install_game 2>&1 | tee -a "$LOGFILE" || install_exit_code=$? clean_exit $install_exit_code diff --git a/overlay/usr/local/bin/gameserver-start b/overlay/usr/local/bin/gameserver-start index 4fbd1a8..c35e6da 100755 --- a/overlay/usr/local/bin/gameserver-start +++ b/overlay/usr/local/bin/gameserver-start @@ -5,5 +5,6 @@ fatal() { echo "FATAL: $@"; exit 1; } conf=/etc/gameserver/gameserver [[ -f "$conf" ]] || fatal "Conf file ($conf) not found." -. $conf +. "$conf" +[[ "$GAME" =~ ^[a-z0-9_-]+$ ]] || fatal "Invalid game code in $conf." runuser -l "gameuser" -c "~/gameserver/${GAME}server start" diff --git a/overlay/usr/local/bin/gameserver-stop b/overlay/usr/local/bin/gameserver-stop index 0194248..cc8b4fb 100755 --- a/overlay/usr/local/bin/gameserver-stop +++ b/overlay/usr/local/bin/gameserver-stop @@ -5,5 +5,6 @@ fatal() { echo "FATAL: $@"; exit 1; } conf=/etc/gameserver/gameserver [[ -f "$conf" ]] || fatal "Conf file ($conf) not found." -. $conf +. "$conf" +[[ "$GAME" =~ ^[a-z0-9_-]+$ ]] || fatal "Invalid game code in $conf." runuser -l "gameuser" -c "~/gameserver/${GAME}server stop" diff --git a/overlay/usr/local/sbin/turnkey-gameserver-update b/overlay/usr/local/sbin/turnkey-gameserver-update new file mode 100755 index 0000000..effa9cb --- /dev/null +++ b/overlay/usr/local/sbin/turnkey-gameserver-update @@ -0,0 +1,156 @@ +#!/bin/bash -e + +set -o pipefail + +WRAPPER_REPO=https://github.com/jesinmat/linux-gameservers.git +WRAPPER_DIR=/root/gameservers +LINUXGSM_REPO=https://github.com/GameServerManagers/LinuxGSM.git +SHARE_DIR=/usr/local/share/turnkey-gameserver +SOURCE_RECORD=$SHARE_DIR/source +TEMP_DIR= + +usage() { + echo "usage: turnkey-gameserver-update --check | --apply [--dry-run]" >&2 + exit 2 +} + +fail() { + echo "gameserver update: $*" >&2 + exit 1 +} + +record_value() { + key=$1 + value=$(sed -n "s/^${key}=//p" "$SOURCE_RECORD") + [ -n "$value" ] || fail "missing source record key: $key" + printf '%s\n' "$value" +} + +cleanup() { + if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" + fi +} +trap cleanup EXIT HUP INT TERM + +prepare_wrapper() { + wrapper_root=$1 + [ -f "$wrapper_root/auto_install.sh" ] || fail "wrapper archive has no auto_install.sh" + [ -d "$wrapper_root/games" ] || fail "wrapper archive has no games directory" + sed -i 's/bsdmainutils/bsdextrautils/' "$wrapper_root/auto_install.sh" + sed -i '\|run_as_user "wget -O .*https://linuxgsm.sh"|c\ install -o "$GAMEUSER" -g "$GAMEUSER" -m 0755 /usr/local/share/turnkey-gameserver/linuxgsm.sh "$LGSM_PATH"' \ + "$wrapper_root/auto_install.sh" + grep -q 'install -o "$GAMEUSER".*turnkey-gameserver/linuxgsm.sh' \ + "$wrapper_root/auto_install.sh" || fail "wrapper bootstrap patch did not apply" +} + +fetch_candidate() { + wrapper_candidate_commit=$(git ls-remote "$WRAPPER_REPO" refs/heads/master | awk 'NR == 1 {print $1}') + printf '%s\n' "$wrapper_candidate_commit" | grep -Eq '^[0-9a-f]{40}$' || + fail "unable to resolve the official wrapper master commit" + + linuxgsm_refs=$(git ls-remote --refs --tags "$LINUXGSM_REPO" 'refs/tags/v*') + linuxgsm_candidate_version=$(printf '%s\n' "$linuxgsm_refs" | + sed -n 's|^[^[:space:]]*[[:space:]]*refs/tags/||p' | + grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) + [ -n "$linuxgsm_candidate_version" ] || fail "no official LinuxGSM stable tag found" + linuxgsm_candidate_commit=$(printf '%s\n' "$linuxgsm_refs" | + awk -v ref="refs/tags/$linuxgsm_candidate_version" '$2 == ref {print $1}') + printf '%s\n' "$linuxgsm_candidate_commit" | grep -Eq '^[0-9a-f]{40}$' || + fail "unable to resolve the official LinuxGSM tag commit" + + TEMP_DIR=$(mktemp -d /var/tmp/turnkey-gameserver-update.XXXXXX) + wrapper_archive=$TEMP_DIR/wrapper.zip + linuxgsm_bootstrap=$TEMP_DIR/linuxgsm.sh + curl -LfsS \ + "https://github.com/jesinmat/linux-gameservers/archive/$wrapper_candidate_commit.zip" \ + -o "$wrapper_archive" + curl -LfsS \ + "https://raw.githubusercontent.com/GameServerManagers/LinuxGSM/$linuxgsm_candidate_version/linuxgsm.sh" \ + -o "$linuxgsm_bootstrap" + wrapper_candidate_sha256=$(sha256sum "$wrapper_archive" | awk '{print $1}') + linuxgsm_candidate_sha256=$(sha256sum "$linuxgsm_bootstrap" | awk '{print $1}') + + wrapper_candidate_root=$TEMP_DIR/linux-gameservers-$wrapper_candidate_commit + unzip -q "$wrapper_archive" -d "$TEMP_DIR" + prepare_wrapper "$wrapper_candidate_root" + grep -qx "version=\"$linuxgsm_candidate_version\"" "$linuxgsm_bootstrap" || + fail "LinuxGSM bootstrap version does not match its tag" +} + +mode= +dry_run=false +while [ "$#" -gt 0 ]; do + case "$1" in + --check) [ -z "$mode" ] || usage; mode=check ;; + --apply) [ -z "$mode" ] || usage; mode=apply ;; + --dry-run) dry_run=true ;; + *) usage ;; + esac + shift +done +[ -n "$mode" ] || usage +[ "$mode" = apply ] || [ "$dry_run" = false ] || usage +[ -f "$SOURCE_RECORD" ] || fail "missing source record" + +wrapper_channel=$(record_value wrapper_channel) +[ "$wrapper_channel" = "official jesinmat/linux-gameservers master" ] || + fail "custom wrapper configured; update it through its selected repository" +wrapper_installed_commit=$(record_value wrapper_commit) +wrapper_installed_sha256=$(record_value wrapper_archive_sha256) +linuxgsm_installed_version=$(record_value linuxgsm_version) +linuxgsm_installed_commit=$(record_value linuxgsm_tag_commit) +linuxgsm_installed_sha256=$(record_value linuxgsm_bootstrap_sha256) + +fetch_candidate + +if [ "$wrapper_candidate_commit" = "$wrapper_installed_commit" ]; then + [ "$wrapper_candidate_sha256" = "$wrapper_installed_sha256" ] || + fail "official wrapper archive changed for the installed commit" +fi +if [ "$linuxgsm_candidate_version" = "$linuxgsm_installed_version" ]; then + [ "$linuxgsm_candidate_commit" = "$linuxgsm_installed_commit" ] || + fail "official LinuxGSM tag changed for the installed version" + [ "$linuxgsm_candidate_sha256" = "$linuxgsm_installed_sha256" ] || + fail "official LinuxGSM bootstrap changed for the installed version" +fi + +status=up-to-date +if [ "$wrapper_candidate_commit" != "$wrapper_installed_commit" ] || + dpkg --compare-versions "${linuxgsm_installed_version#v}" lt "${linuxgsm_candidate_version#v}"; then + status=update-available +fi + +printf 'wrapper_installed_commit=%s\n' "$wrapper_installed_commit" +printf 'wrapper_candidate_commit=%s\n' "$wrapper_candidate_commit" +printf 'wrapper_candidate_archive_sha256=%s\n' "$wrapper_candidate_sha256" +printf 'linuxgsm_installed=%s\n' "$linuxgsm_installed_version" +printf 'linuxgsm_candidate=%s\n' "$linuxgsm_candidate_version" +printf 'linuxgsm_candidate_commit=%s\n' "$linuxgsm_candidate_commit" +printf 'linuxgsm_candidate_bootstrap_sha256=%s\n' "$linuxgsm_candidate_sha256" +printf 'status=%s\n' "$status" + +[ "$mode" = apply ] || exit 0 +if [ "$dry_run" = true ]; then + printf 'apply=dry-run verified exact candidates\n' + exit 0 +fi +[ "$(id -u)" -eq 0 ] || fail "apply requires root" +if [ "$status" = up-to-date ]; then + printf 'apply=no-op\n' + exit 0 +fi + +rsync -a --delete "$wrapper_candidate_root/" "$WRAPPER_DIR/" +install -m 0755 "$linuxgsm_bootstrap" "$SHARE_DIR/linuxgsm.sh" +cat > "$TEMP_DIR/source" <&2 + exit 1 +} + +record_value() { + key=$1 + value=$(sed -n "s/^${key}=//p" "$SOURCE_RECORD") + [ -n "$value" ] || fail "missing source record key: $key" + printf '%s\n' "$value" +} + +[ -f "$SOURCE_RECORD" ] || fail "missing source record" +[ "$(record_value wrapper_commit)" = d2017be9f56db0da2a1c45651e656f47bc7ce42a ] || + fail "unexpected wrapper commit" +[ "$(record_value wrapper_archive_sha256)" = 302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983 ] || + fail "unexpected wrapper archive digest" +[ "$(record_value linuxgsm_version)" = v26.2.0 ] || fail "unexpected LinuxGSM version" +[ "$(record_value linuxgsm_tag_commit)" = bded3376bc44d20b89fcc6c32f23a66347fffec6 ] || + fail "unexpected LinuxGSM tag commit" +[ "$(record_value linuxgsm_bootstrap_sha256)" = 0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd ] || + fail "unexpected LinuxGSM bootstrap digest" +printf '%s %s\n' \ + 0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd \ + "$LINUXGSM_BOOTSTRAP" | sha256sum -c - >/dev/null +grep -qx 'version="v26.2.0"' "$LINUXGSM_BOOTSTRAP" || fail "LinuxGSM runtime version differs" + +server_list=$(cd "$WRAPPER_DIR" && ./auto_install.sh --list) +server_count=$(printf '%s\n' "$server_list" | grep -Ec '^\| [a-z0-9_-]+[[:space:]]+\|') +[ "$server_count" -ge 100 ] || fail "supported server catalog is unexpectedly small" +printf '%s\n' "$server_list" | grep -Eq '^\| mumble[[:space:]]+\| Mumble' || + fail "lightweight Mumble server definition is missing" + +nginx -t >/dev/null 2>&1 +landing_page=$(curl -fsS http://127.0.0.1/) +printf '%s' "$landing_page" | grep -q '

TurnKey GameServer

' || + fail "game-server management landing page did not render" +printf '%s' "$landing_page" | grep -q 'Webmin' || fail "management link is missing" +[ -x /usr/local/bin/gameserver-init ] || fail "game-server selector is missing" +[ -f /usr/lib/confconsole/plugins.d/Game_Server/update_list.py ] || + fail "catalog update action is missing from Configuration Console" + +update_check=$(turnkey-gameserver-update --check) +printf '%s\n' "$update_check" | grep -qx \ + 'wrapper_candidate_commit=d2017be9f56db0da2a1c45651e656f47bc7ce42a' || + fail "wrapper update candidate is not the verified commit" +printf '%s\n' "$update_check" | grep -qx 'linuxgsm_candidate=v26.2.0' || + fail "LinuxGSM update candidate is not the verified release" +printf '%s\n' "$update_check" | grep -qx \ + 'linuxgsm_candidate_commit=bded3376bc44d20b89fcc6c32f23a66347fffec6' || + fail "LinuxGSM candidate commit mismatch" +printf '%s\n' "$update_check" | grep -qx 'status=up-to-date' || + fail "updater did not report a stable installed state" +update_apply=$(turnkey-gameserver-update --apply --dry-run) +printf '%s\n' "$update_apply" | grep -qx 'apply=dry-run verified exact candidates' || + fail "updater apply plan was not verified" + +[ ! -e /etc/gameserver/installation.done ] || + fail "fixture requires a fresh appliance without an installed game" +fixture_root=/run/gameserver-v19-fixture +fixture_script=/home/gameuser/gameserver/fixtureserver +cleanup() { + systemctl stop gameserver >/dev/null 2>&1 || true + rm -f /etc/gameserver/gameserver "$fixture_script" + rm -rf "$fixture_root" +} +trap cleanup EXIT HUP INT TERM + +install -d -o gameuser -g gameuser -m 0755 "$fixture_root" +install -d -o gameuser -g gameuser -m 0755 /home/gameuser/gameserver +cat > "$fixture_script" <<'EOF' +#!/bin/bash -e + +case "$1" in + start) touch /run/gameserver-v19-fixture/running ;; + stop) rm -f /run/gameserver-v19-fixture/running ;; + *) exit 2 ;; +esac +EOF +chown gameuser:gameuser "$fixture_script" +chmod 0755 "$fixture_script" +install -d -m 0755 /etc/gameserver +cat > /etc/gameserver/gameserver <<'EOF' +GAME="fixture" +GAME_LONG_NAME="Wave 2 lightweight lifecycle fixture" +EOF + +systemctl daemon-reload +systemctl start gameserver +systemctl -q is-active gameserver || fail "game-server service did not become active" +[ -f "$fixture_root/running" ] || fail "game-server start action did not reach the fixture" +systemctl stop gameserver +[ ! -e "$fixture_root/running" ] || fail "game-server stop action did not reach the fixture" +cleanup +trap - EXIT HUP INT TERM + +if [ -n "${TKL_TEST_RESULT:-}" ]; then + cat > "$TKL_TEST_RESULT" < Date: Wed, 26 Aug 2026 05:45:58 +0000 Subject: [PATCH 2/5] Exercise the real game installation path Propagate auto-install failures through the logging pipeline so interrupted or failed catalog installs cannot be reported as complete. Run acceptance through the lightweight supported Mumble catalog target and verify it using LinuxGSM itself, while retaining the inexpensive disposable service-wrapper lifecycle check. This establishes the actual selection and installer path without downloading a large game. --- docs/usage.rst | 3 +-- docs/v19.0-testing.md | 10 +++++----- overlay/usr/local/bin/gameserver-init | 2 ++ tests/v19.sh | 15 +++++++++++++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 1fea679..360bd9f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -11,8 +11,7 @@ using a graphical interface. During the installation, you will be asked to provide basic server settings for the game server. -If for some reason the install is interupted or fails, but reports success; -please remove /etc/gameserver/installation.done and retry. I.e.:: +If an installation is interrupted or fails, retry it with:: rm /etc/gameserver/installation.done gameserver-init diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index b690258..5a32bca 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -37,7 +37,7 @@ overwriting it. | Catalog and LinuxGSM update | Resolves the official wrapper master and latest stable LinuxGSM tag, verifies exact candidate artifacts, and exercises the apply dry run. | | Service lifecycle | Starts and stops the systemd unit against a disposable non-root fixture and observes both actions. | | Non-root server boundary | Runs the fixture through the same `runuser` start and stop scripts used by installed servers. | -| Automatic and interactive selection | Checks the installed `gameserver-init` entry point and catalog consumed by both paths. | +| Automatic and interactive selection | Runs `gameserver-init` with the catalog's lightweight Mumble target, requires LinuxGSM installation completion, and verifies the server through LinuxGSM's monitor command. | | TurnKey platform surfaces | Core supplies SSH, Webmin, TLS, backups, and first-boot integration. The appliance test covers GameServer-specific behavior. | The runtime test emits exactly the seven Wave 2 result keys: package source, @@ -60,9 +60,9 @@ product loop. commit predates the current LinuxGSM release. The updater checks both channels independently, but a newly added LinuxGSM server still needs a wrapper catalog entry before it appears in the appliance selector. -- MEDIUM: The acceptance fixture proves the non-root systemd lifecycle without - downloading a vendor game binary. Game downloads differ substantially in - size, licensing, authentication, and network behavior, so one vendor binary - would not prove the broader catalog. +- MEDIUM: Acceptance installs the lightweight Mumble target through the real + catalog and LinuxGSM path. Other game downloads differ substantially in size, + licensing, authentication, and network behavior, so this does not prove every + vendor-specific installer. - LOW: The test checks the real landing page and Configuration Console action boundary without automating the terminal dialog renderer. diff --git a/overlay/usr/local/bin/gameserver-init b/overlay/usr/local/bin/gameserver-init index 0480537..fff83aa 100755 --- a/overlay/usr/local/bin/gameserver-init +++ b/overlay/usr/local/bin/gameserver-init @@ -1,5 +1,7 @@ #!/bin/bash -e +set -o pipefail + GAMEUSER="gameuser" LOGFILE="/var/log/gameserver/install.log" GAME_REPO_DIR="/root/gameservers" diff --git a/tests/v19.sh b/tests/v19.sh index 9a7e9b4..9088059 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -102,11 +102,22 @@ systemctl stop gameserver cleanup trap - EXIT HUP INT TERM +GAME=mumble /usr/local/bin/gameserver-init +[ -e /etc/gameserver/installation.done ] || + fail "Mumble catalog installation did not complete" +grep -qx 'GAME="mumble"' /etc/gameserver/gameserver || + fail "Mumble catalog selection was not retained" +[ -x /home/gameuser/gameserver/mumbleserver ] || + fail "LinuxGSM Mumble entry point was not installed" +systemctl -q is-active gameserver || fail "installed Mumble server is not active" +runuser -l gameuser -c '~/gameserver/mumbleserver monitor' >/dev/null || + fail "LinuxGSM did not report the installed Mumble server running" + if [ -n "${TKL_TEST_RESULT:-}" ]; then cat > "$TKL_TEST_RESULT" < Date: Wed, 26 Aug 2026 11:56:52 +0000 Subject: [PATCH 3/5] Exercise the HTTPS landing page reliably Verify the common Nginx HTTP-to-HTTPS redirect, then inspect the GameServer landing page over HTTPS. Feed the fetched page directly to grep so pipefail cannot turn grep's successful early exit into a false assertion failure. This changes only acceptance evaluation; the appliance integration and required management content remain unchanged. Verified with git diff --check and bash -n tests/v19.sh. --- tests/v19.sh | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/v19.sh b/tests/v19.sh index 9088059..a5e1bae 100755 --- a/tests/v19.sh +++ b/tests/v19.sh @@ -40,10 +40,13 @@ printf '%s\n' "$server_list" | grep -Eq '^\| mumble[[:space:]]+\| Mumble' || fail "lightweight Mumble server definition is missing" nginx -t >/dev/null 2>&1 -landing_page=$(curl -fsS http://127.0.0.1/) -printf '%s' "$landing_page" | grep -q '

TurnKey GameServer

' || +http_redirect=$(curl -sS -o /dev/null -w '%{http_code} %{redirect_url}' http://127.0.0.1/) +[ "$http_redirect" = '307 https://127.0.0.1/' ] || + fail "HTTP did not redirect to the HTTPS management page" +landing_page=$(curl -kfsS https://127.0.0.1/) +grep -q '

TurnKey GameServer

' <<<"$landing_page" || fail "game-server management landing page did not render" -printf '%s' "$landing_page" | grep -q 'Webmin' || fail "management link is missing" +grep -q 'Webmin' <<<"$landing_page" || fail "management link is missing" [ -x /usr/local/bin/gameserver-init ] || fail "game-server selector is missing" [ -f /usr/lib/confconsole/plugins.d/Game_Server/update_list.py ] || fail "catalog update action is missing from Configuration Console" From a57d42a3f90562d241ec9f85aa00d2856eae3e0c Mon Sep 17 00:00:00 2001 From: Liraz Siri Date: Wed, 26 Aug 2026 12:55:59 +0000 Subject: [PATCH 4/5] Exclude the removed Mumble target LinuxGSM v26.2.0 no longer publishes the Mumble server identifier, while the wrapper still offered it and sent mumbleserver to LinuxGSM. This made a normal catalog selection fail after dependency installation. Remove that known-broken choice from both the initial catalog and updater candidates. Exercise the supported lightweight TeamSpeak 3 target through the real installer instead, while recording broader upstream catalog drift as deferred. Verified shell syntax, clean diffs, the wrapper ts3 mapping, and the pinned LinuxGSM ts3server entry. --- conf.d/main | 3 ++ docs/v19.0-testing.md | 14 ++++----- .../usr/local/sbin/turnkey-gameserver-update | 1 + tests/v19.sh | 29 ++++++++++--------- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/conf.d/main b/conf.d/main index 38e7b92..09ea4e1 100755 --- a/conf.d/main +++ b/conf.d/main @@ -61,6 +61,9 @@ sed -i '\|run_as_user "wget -O .*https://linuxgsm.sh"|c\ install -o "$GAMEUSE grep -q 'install -o "$GAMEUSER".*turnkey-gameserver/linuxgsm.sh' \ "$GAME_REPO_DIR/auto_install.sh" +# Mumble was removed from LinuxGSM and can no longer be installed. +rm -rf "$GAME_REPO_DIR/games/mumble" + cat > /usr/local/share/turnkey-gameserver/source </dev/null 2>&1 http_redirect=$(curl -sS -o /dev/null -w '%{http_code} %{redirect_url}' http://127.0.0.1/) @@ -105,22 +108,22 @@ systemctl stop gameserver cleanup trap - EXIT HUP INT TERM -GAME=mumble /usr/local/bin/gameserver-init +GAME=ts3 /usr/local/bin/gameserver-init [ -e /etc/gameserver/installation.done ] || - fail "Mumble catalog installation did not complete" -grep -qx 'GAME="mumble"' /etc/gameserver/gameserver || - fail "Mumble catalog selection was not retained" -[ -x /home/gameuser/gameserver/mumbleserver ] || - fail "LinuxGSM Mumble entry point was not installed" -systemctl -q is-active gameserver || fail "installed Mumble server is not active" -runuser -l gameuser -c '~/gameserver/mumbleserver monitor' >/dev/null || - fail "LinuxGSM did not report the installed Mumble server running" + fail "TeamSpeak catalog installation did not complete" +grep -qx 'GAME="ts3"' /etc/gameserver/gameserver || + fail "TeamSpeak catalog selection was not retained" +[ -x /home/gameuser/gameserver/ts3server ] || + fail "LinuxGSM TeamSpeak entry point was not installed" +systemctl -q is-active gameserver || fail "installed TeamSpeak server is not active" +runuser -l gameuser -c '~/gameserver/ts3server monitor' >/dev/null || + fail "LinuxGSM did not report the installed TeamSpeak server running" if [ -n "${TKL_TEST_RESULT:-}" ]; then cat > "$TKL_TEST_RESULT" < Date: Wed, 26 Aug 2026 13:08:05 +0000 Subject: [PATCH 5/5] Record exact GameServer acceptance Document the exact passing container run, retained report digest, primary behavior covered, and the two product correction loops. This gives independent review a reproducible evidence trail without changing the tested appliance or acceptance code. Verified the report SHA256 against the retained run and checked the documentation diff. --- docs/v19.0-testing.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md index 31ee5c0..b91a4f6 100644 --- a/docs/v19.0-testing.md +++ b/docs/v19.0-testing.md @@ -46,12 +46,25 @@ channel, and integrity evidence. ## Candidate status -The candidate passed source and static gates. An exact Wave 2 run was not -started because the shared runner has a known private-source cleanup blocker. -The committed candidate is ready for its exact container run once that shared -runner is available. +Candidate `a57d42a3f90562d241ec9f85aa00d2856eae3e0c` passed the exact container +acceptance command: -Product-fix accounting is 0 of 3. Source and test corrections do not consume a +```sh +/sandboxed-git/turnkey/tools/test-v19-appliance gameserver \ + --source /home/agent/.local/worktrees/turnkey-apps/gameserver/wish-gameserver-v19-trixie +``` + +Run `20260826t130005z-2603-3322` built and imported the Trixie rootfs, reached +multi-user state with inithooks complete, and passed every application check in +the crosswalk. The retained report is +`/home/agent/.local/state/turnkey-v19-harness/runs/gameserver/20260826t130005z-2603-3322/report.txt` +with SHA256 +`70bc728761c5d0515e5ff95467ba06feda0bfc09e14d672b8a5b3c871c8579a9`. + +Product-fix accounting is 2 of 3. The first loop made installer failures +propagate through the logging pipeline. The second removed the Mumble choice +after exact acceptance proved that LinuxGSM v26.2.0 no longer supports its +server identifier. Test-only HTTP acceptance corrections did not consume a product loop. ## Deferred issues