cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix(install): ship portable host activation

Dami committed 8d2730e
commit
8d2730e29da4b324a47a942a3ead9ea3b9f40d46
parent
f8679be7a4c84a5f3ee0774fb08a1307f1b06973
author
Dami <dami.etoile@gmail.com>
14 files changed, +450 -19
MREADME.md25+-
Mdocs/fresh-bazzite-host.md8+-
Adocs/sigil-host-activation.md294++++++++++++++++++++++++++++++
Ascripts/72-sigil-uinput.rules1+
Ascripts/99-sigil-uinput.rules1+
Mscripts/install-bazzite-package.sh9+-
Mscripts/package-bazzite-release.sh16++-
Mscripts/tests/bazzite-package-install-e2e.sh29+++-
Mscripts/tests/package-bazzite-product-contract.sh3+
Ascripts/tests/sigil-host-activation-guide.sh62++++++
Mscripts/tests/sigil-release-verifier.sh10+-
Mscripts/tests/test_sigil_bootstrap.py5+
Mscripts/verify-sigil-release.sh4+
Mwebsite/install-sigil2+-
M README.md +14 -11
@@ -183,9 +183,12 @@ Portal, then use **client -> reset enrollment** and confirm the displayed host.
183183 The reset is intentionally explicit and controller reachable; importing a new184184 invitation never silently replaces a working enrollment.185185 186-Provision a dedicated AMD host with the187-[fresh Bazzite host runbook](docs/fresh-bazzite-host.md). The runbook also188-defines the temporary `slate` stand-in used for protocol and daemon extraction.186+Activate an installed dedicated AMD host with the portable, package-owned187+[Sigil host activation guide](docs/sigil-host-activation.md). It uses the188+current gaming user, dynamically discovered hardware, and installed package189+assets without assuming a maintainer hostname or checkout path. The190+[fresh Bazzite host runbook](docs/fresh-bazzite-host.md) remains the engineering191+and hardware-acceptance log, including the temporary `slate` stand-in.189192 The backward-compatible [appliance status v1 contract](docs/sigil-appliance-status-v1.md)190193 and explicit [v2 contract](docs/sigil-appliance-status-v2.md) define the local,191194 redacted interface intended for the Decky management surface.
@@ -217,7 +220,7 @@ scripts/stage-bazzite-release.sh \
217220 The stager atomically updates the user-owned `current` symlink only after both218221 binaries, their dynamic libraries, and bounded startup commands validate. Host219222 identity, hardware-specific configuration, and service activation remain220-separate gates in the Bazzite runbook.223+separate gates in the package-owned activation guide.221224 222225 The thin binary stager is only for unmanaged development layouts. It refuses223226 activation when package-managed service, audio, rollback, or udev links follow
@@ -255,13 +258,13 @@ The GitHub workflow and package builder never receive the Minisign secret.
255258 256259 The package contains the primary `sigil` host executable, its byte-identical257260 `sigil-host` compatibility copy, the `sigil-probe` diagnostic, installer and258-rollback tools, service/audio/udev assets, license, checksums, and build259-provenance. It cannot contain the source tree, `.env` files, identities, host260-configuration, or test evidence. Its release ID is the SHA-256 of the complete261-installed-file checksum manifest. Identical inputs produce a byte-identical262-archive. The manifest marks product binaries as built from clean `HEAD`;263-caller-supplied development binaries are explicitly marked as having264-unverified provenance.261+rollback tools, service/audio/udev assets, the portable activation guide,262+license, checksums, and build provenance. It cannot contain the source tree,263+`.env` files, identities, host configuration, or test evidence. Its release ID264+is the SHA-256 of the complete installed-file checksum manifest. Identical265+inputs produce a byte-identical archive. The manifest marks product binaries266+as built from clean `HEAD`; caller-supplied development binaries are explicitly267+marked as having unverified provenance.265268 266269 During the legacy rollback window, `sigil-host.service` deliberately starts267270 the byte-identical `sigil-host` compatibility filename. Interactive and
M docs/fresh-bazzite-host.md +7 -1
A docs/sigil-host-activation.md +294 -0
@@ -0,0 +1,294 @@
1+# Activate an installed Sigil host2+3+This is the portable post-install path for a dedicated Bazzite or SteamOS-like4+AMD gaming host. Run it as the gaming user that owns the Gamescope session, not5+as root. The commands use only `$HOME` and `$USER`; no hostname or account name6+is assumed.7+8+The package installer deliberately stops before these steps. Identity creation,9+system input permissions, PipeWire restart, hardware configuration, and service10+enablement each remain visible operator decisions. Re-running a package install11+or rollback preserves the identity and `host.toml` created here.12+13+## 1. Grant the gaming user access to only uinput14+15+The package stages its virtual-input udev rule but does not write `/etc` or16+change group membership. Install that exact package asset together with the two17+rules that bound `/dev/uinput` to a dedicated group:18+19+```bash20+set -euo pipefail21+22+current="$HOME/.local/libexec/sigil-spark/current"23+remote_input_rule="$current/assets/70-sigil-remote-input.rules"24+early_uinput_rule="$current/assets/72-sigil-uinput.rules"25+final_uinput_rule="$current/assets/99-sigil-uinput.rules"26+test -x "$current/sigil"27+for rule in "$remote_input_rule" "$early_uinput_rule" "$final_uinput_rule"; do28+ test -f "$rule" && test ! -L "$rule"29+done30+31+getent group sigil-uinput >/dev/null || sudo groupadd --system sigil-uinput32+sudo usermod --append --groups sigil-uinput "$USER"33+sudo install -o root -g root -m 0644 \34+ "$remote_input_rule" /etc/udev/rules.d/70-sigil-remote-input.rules35+sudo install -o root -g root -m 0644 \36+ "$early_uinput_rule" /etc/udev/rules.d/72-sigil-uinput.rules37+sudo install -o root -g root -m 0644 \38+ "$final_uinput_rule" /etc/udev/rules.d/99-sigil-uinput.rules39+sudo modprobe uinput40+sudo udevadm control --reload-rules41+sudo udevadm trigger --action=add --subsystem-match=misc --sysname-match=uinput42+```43+44+Reboot, or fully sign out and back in, before continuing. A new login is45+required for the user service to inherit the `sigil-uinput` group.46+47+After that login, fail closed unless the device has the expected Linux uinput48+identity, ownership, mode, and no extended ACL:49+50+```bash51+set -euo pipefail52+53+uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"54+test -n "$uinput_gid"55+id -G | tr ' ' '\n' | grep -Fx "$uinput_gid"56+test ! -L /dev/uinput57+test -c /dev/uinput && test -r /dev/uinput && test -w /dev/uinput58+test "$(stat -Lc '%u:%g:%a:%t:%T' /dev/uinput)" = "0:$uinput_gid:660:a:df"59+if getfacl -cp /dev/uinput | grep -Eq '^(user|group):[^:]|^mask:'; then60+ echo '/dev/uinput has an extended access ACL' >&261+ exit 162+fi63+```64+65+Do not add this account to the broad `input` group and do not run Sigil as66+root. Access to uinput is equivalent to local controller, keyboard, and pointer67+control.68+69+## 2. Create the host identity once70+71+```bash72+set -euo pipefail73+umask 07774+75+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"76+identity="$HOME/.local/share/sigil-spark/identity/host.key"77+install -d -m 0700 "$(dirname "$identity")" \78+ "$HOME/.config/sigil-spark" "$HOME/.local/state/sigil-spark"79+if ! test -e "$identity"; then80+ "$sigil" identity init --output "$identity"81+fi82+test -f "$identity" && test ! -L "$identity"83+chmod 0600 "$identity"84+"$sigil" identity show --identity "$identity"85+```86+87+Never copy this seed to a client or rotate it during an upgrade. Portal pairs88+to the host identity represented by this file.89+90+## 3. Select the exact AMD render node and GstVA H.264 factory91+92+Sigil binds the encoder factory to its inspected read-only `device-path`; a93+factory name alone is not enough on a multi-GPU machine. The following chooses94+automatically only when there is one usable AMD node/factory pair. Set95+`RENDER_NODE` and `VA_ENCODER` explicitly before running it when the printed96+inventory is intentionally ambiguous.97+98+```bash99+set -euo pipefail100+101+mapfile -t amd_nodes < <(102+ for sysfs_node in /sys/class/drm/renderD*; do103+ test -e "$sysfs_node/device/driver" || continue104+ test "$(basename "$(readlink -f "$sysfs_node/device/driver")")" = amdgpu \105+ || continue106+ node="/dev/dri/$(basename "$sysfs_node")"107+ test -c "$node" && test -r "$node" && test -w "$node" && printf '%s\n' "$node"108+ done109+)110+printf 'AMD render node: %s\n' "${amd_nodes[@]}"111+if test -n "${RENDER_NODE:-}"; then112+ render_node="$RENDER_NODE"113+else114+ test "${#amd_nodes[@]}" -eq 1115+ render_node="${amd_nodes[0]}"116+fi117+printf '%s\n' "${amd_nodes[@]}" | grep -Fx "$render_node"118+119+mapfile -t va_factories < <(120+ gst-inspect-1.0 | awk '$2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {121+ sub(/:$/, "", $2); print $2122+ }' | sort -u123+)124+matching_factories=()125+for factory in "${va_factories[@]}"; do126+ inspection="$(gst-inspect-1.0 "$factory")" || continue127+ grep -Fq "Default: \"$render_node\"" <<<"$inspection" || continue128+ grep -Eq '\([0-9]+\): cbr([[:space:]]|$)' <<<"$inspection" || continue129+ matching_factories+=("$factory")130+done131+printf 'matching CBR GstVA factory: %s\n' "${matching_factories[@]}"132+if test -n "${VA_ENCODER:-}"; then133+ va_encoder="$VA_ENCODER"134+else135+ test "${#matching_factories[@]}" -eq 1136+ va_encoder="${matching_factories[0]}"137+fi138+printf '%s\n' "${matching_factories[@]}" | grep -Fx "$va_encoder"139+140+printf 'export SIGIL_RENDER_NODE=%q\n' "$render_node"141+printf 'export SIGIL_VA_ENCODER=%q\n' "$va_encoder"142+```143+144+Keep the two printed export commands for the next step. If automatic selection145+fails, inspect every printed node and factory and rerun with an exact pair, for146+example `RENDER_NODE=/dev/dri/renderD129 VA_ENCODER=varenderD129h264enc`.147+Never substitute a software encoder just to make preflight pass.148+149+## 4. Restart the package-owned audio sink150+151+The package already linked its PipeWire drop-in into the gaming user's config.152+Restart only the PulseAudio-compatible PipeWire service during provisioning,153+then select the persistent headless sink:154+155+```bash156+set -euo pipefail157+158+test -f "$HOME/.config/pipewire/pipewire-pulse.conf.d/50-sigil-spark-audio.conf"159+systemctl --user restart pipewire-pulse.service160+audio_sink_id="$(wpctl status -n | awk '161+ $0 ~ /sigil_spark/ {162+ for (field = 1; field <= NF; field++) {163+ if ($field ~ /^[0-9]+\.$/) {164+ gsub("\\.", "", $field); print $field; exit165+ }166+ }167+ }168+')"169+test "$audio_sink_id" -gt 0170+wpctl set-default "$audio_sink_id"171+pactl get-default-sink | grep -Fx sigil_spark172+```173+174+This restart can interrupt existing local playback. Perform it before starting175+a game.176+177+## 5. Write and validate the hardware configuration178+179+First run the two `export` commands printed by step 3 in this shell. Width and180+height are intentionally omitted so Sigil follows the bounded native Gamescope181+mode instead of assuming one panel resolution.182+183+```bash184+set -euo pipefail185+: "${SIGIL_RENDER_NODE:?run the render-node exports from step 3}"186+: "${SIGIL_VA_ENCODER:?run the encoder exports from step 3}"187+188+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"189+uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"190+config="$HOME/.config/sigil-spark/host.toml"191+ffmpeg_path="$(command -v ffmpeg)"192+pw_dump_path="$(command -v pw-dump)"193+gst_launch_path="$(command -v gst-launch-1.0)"194+gst_inspect_path="$(command -v gst-inspect-1.0)"195+test -n "$uinput_gid"196+197+umask 077198+if test -e "$config" || test -L "$config"; then199+ echo "refusing to replace existing host configuration: $config" >&2200+ exit 1201+fi202+cat >"$config" <<EOF203+identity_path = "$HOME/.local/share/sigil-spark/identity/host.key"204+state_path = "$HOME/.local/state/sigil-spark/runtime"205+source = "gamescope-pipewire"206+framerate = 60207+codec = "h264"208+input_mode = "uinput"209+ffmpeg_path = "$ffmpeg_path"210+211+[uinput]212+device_path = "/dev/uinput"213+expected_owner_uid = 0214+expected_group_gid = $uinput_gid215+expected_mode = 0o660216+217+[gamescope_pipewire]218+node_name = "gamescope"219+media_class = "Video/Source"220+xwayland_display = ":0"221+pw_dump_path = "$pw_dump_path"222+gst_launch_path = "$gst_launch_path"223+gst_inspect_path = "$gst_inspect_path"224+encoder_backend = "external-gst-launch"225+vaapi_encoder = "$SIGIL_VA_ENCODER"226+vaapi_render_node = "$SIGIL_RENDER_NODE"227+rate_control = "cbr"228+bitrate_kbps = 12000229+230+[audio]231+node_name = "sigil_spark"232+media_class = "Audio/Sink"233+bitrate_bps = 96000234+EOF235+chmod 0600 "$config"236+237+"$sigil" config check --config "$config"238+"$sigil" capture probe \239+ --source gamescope-pipewire --config "$config" --frames 300240+```241+242+Both commands must exit zero. The capture probe must report 300 encoded frames,243+zero post-encode drops, and the actual resolved dimensions. Do not enable the244+service after a failed preflight or by weakening the device/factory checks.245+246+## 6. Start, inspect, and only then enable Sigil247+248+```bash249+set -euo pipefail250+251+systemctl --user daemon-reload252+systemctl --user start sigil-host.service253+systemctl --user is-active --quiet sigil-host.service254+invocation="$(systemctl --user show -p InvocationID --value sigil-host.service)"255+test -n "$invocation"256+ready=false257+for _attempt in $(seq 1 60); do258+ if journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" \259+ _SYSTEMD_USER_UNIT=sigil-host.service --no-pager -o cat \260+ | grep -Fxq status=ready261+ then262+ ready=true263+ break264+ fi265+ sleep 1266+done267+test "$ready" = true268+systemctl --user status sigil-host.service --no-pager269+journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" --no-pager -o cat270+271+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"272+"$sigil" appliance status \273+ --config "$HOME/.config/sigil-spark/host.toml" --json274+275+systemctl --user enable sigil-host.service276+```277+278+The exact service invocation must report `status=ready` before enablement. The279+unit runs in the gaming user's ordinary user manager and retries safely if its280+configured Gamescope or PipeWire source is not ready yet; no root daemon or281+interactive desktop capture portal is involved.282+283+Create a one-time, short-lived invitation only after Portal shows its peer ID:284+285+```bash286+"$HOME/.local/libexec/sigil-spark/current/sigil" invitation create \287+ --config "$HOME/.config/sigil-spark/host.toml" \288+ --peer '<PORTAL_IROH_PEER_ID>' \289+ --expires-in-seconds 900 --pointer-keyboard --gamepad \290+ --output "$HOME/sigil.goq-invite"291+```292+293+Move that owner-only file to Portal and confirm the displayed host and grants.294+After the one-time enrollment, ordinary startup remains **PIN -> tap -> play**.
A scripts/72-sigil-uinput.rules +1 -0
A scripts/99-sigil-uinput.rules +1 -0
M scripts/install-bazzite-package.sh +8 -1
M scripts/package-bazzite-release.sh +15 -1
@@ -133,6 +133,9 @@ fi
133133 [[ -f "$script_dir/sigil-host.service" ]] || die "systemd user unit is missing"134134 [[ -f "$script_dir/50-sigil-spark-audio.conf" ]] || die "PipeWire audio drop-in is missing"135135 [[ -f "$script_dir/70-sigil-remote-input.rules" ]] || die "udev rule is missing"136+[[ -f "$script_dir/72-sigil-uinput.rules" ]] || die "early uinput rule is missing"137+[[ -f "$script_dir/99-sigil-uinput.rules" ]] || die "final uinput rule is missing"138+[[ -f "$repo_dir/docs/sigil-host-activation.md" ]] || die "host activation guide is missing"136139 command -v git >/dev/null 2>&1 || die "git is required"137140 command -v python3 >/dev/null 2>&1 || die "python3 is required for deterministic archives"138141 command -v tar >/dev/null 2>&1 || die "tar is required"
@@ -214,7 +217,9 @@ fi
214217 215218 payload="$temp_root/payload"216219 release_tree="$payload/release"217-install -d -m 0700 "$payload" "$release_tree" "$release_tree/assets" "$release_tree/tools"220+install -d -m 0700 \221+ "$payload" "$release_tree" "$release_tree/assets" "$release_tree/docs" \222+ "$release_tree/tools"218223 install -m 0755 "$host_binary" "$release_tree/sigil"219224 # Retain one byte-identical compatibility executable for existing automation.220225 install -m 0755 "$host_binary" "$release_tree/sigil-host"
@@ -226,6 +231,12 @@ install -m 0600 "$script_dir/50-sigil-spark-audio.conf" \
226231 "$release_tree/assets/50-sigil-spark-audio.conf"227232 install -m 0644 "$script_dir/70-sigil-remote-input.rules" \228233 "$release_tree/assets/70-sigil-remote-input.rules"234+install -m 0644 "$script_dir/72-sigil-uinput.rules" \235+ "$release_tree/assets/72-sigil-uinput.rules"236+install -m 0644 "$script_dir/99-sigil-uinput.rules" \237+ "$release_tree/assets/99-sigil-uinput.rules"238+install -m 0644 "$repo_dir/docs/sigil-host-activation.md" \239+ "$release_tree/docs/sigil-host-activation.md"229240 install -m 0644 "$repo_dir/LICENSE" "$release_tree/LICENSE"230241 install -m 0755 "$script_dir/install-bazzite-package.sh" "$payload/install-bazzite-package.sh"231242
@@ -298,7 +309,10 @@ for relative in \
298309 sigil-probe \299310 assets/50-sigil-spark-audio.conf \300311 assets/70-sigil-remote-input.rules \312+ assets/72-sigil-uinput.rules \313+ assets/99-sigil-uinput.rules \301314 assets/sigil-host.service \315+ docs/sigil-host-activation.md \302316 tools/rollback-bazzite-release.sh \303317 LICENSE \304318 release-manifest.json
M scripts/tests/bazzite-package-install-e2e.sh +26 -3
@@ -39,7 +39,8 @@ make_payload() {
3939 true_binary="$(readlink -f /usr/bin/true 2>/dev/null || readlink -f /bin/true)"4040 [[ -x "$true_binary" ]]4141 42- install -d -m 0700 "$root" "$release/assets" "$release/tools"42+ install -d -m 0700 \43+ "$root" "$release/assets" "$release/docs" "$release/tools"4344 install -m 0755 "$true_binary" "$release/sigil"4445 install -m 0755 "$true_binary" "$release/sigil-host"4546 install -m 0755 "$true_binary" "$release/sigil-probe"
@@ -47,10 +48,19 @@ make_payload() {
4748 printf '[Service]\nExecStart=/fixture serve\n' >"$release/assets/sigil-host.service"4849 printf 'audio fixture\n' >"$release/assets/50-sigil-spark-audio.conf"4950 printf 'udev fixture\n' >"$release/assets/70-sigil-remote-input.rules"51+ printf 'early uinput fixture\n' >"$release/assets/72-sigil-uinput.rules"52+ printf 'final uinput fixture\n' >"$release/assets/99-sigil-uinput.rules"53+ printf '# Activation fixture %s\n' "$tag" \54+ >"$release/docs/sigil-host-activation.md"5055 printf 'MIT\n' >"$release/LICENSE"5156 chmod 0600 "$release/assets/50-sigil-spark-audio.conf"52- chmod 0644 "$release/assets/70-sigil-remote-input.rules" \53- "$release/assets/sigil-host.service" "$release/LICENSE"57+ chmod 0644 \58+ "$release/assets/70-sigil-remote-input.rules" \59+ "$release/assets/72-sigil-uinput.rules" \60+ "$release/assets/99-sigil-uinput.rules" \61+ "$release/assets/sigil-host.service" \62+ "$release/docs/sigil-host-activation.md" \63+ "$release/LICENSE"5464 5565 python3 - "$release/release-manifest.json" "$tag" "$commit" "$asset_name" <<'PY'5666 import json
@@ -88,7 +98,10 @@ PY
8898 sigil sigil-host sigil-probe \8999 assets/50-sigil-spark-audio.conf \90100 assets/70-sigil-remote-input.rules \101+ assets/72-sigil-uinput.rules \102+ assets/99-sigil-uinput.rules \91103 assets/sigil-host.service \104+ docs/sigil-host-activation.md \92105 tools/rollback-bazzite-release.sh \93106 LICENSE release-manifest.json94107 do
@@ -147,6 +160,14 @@ run_installer "$payload_a" >"$temp_root/install-a.log"
147160 install_root="$test_home/.local/libexec/sigil-spark"148161 [[ "$(basename -- "$(readlink -f "$install_root/current")")" == "$release_a" ]]149162 [[ ! -e "$install_root/previous" && ! -L "$install_root/previous" ]]163+grep -Fqx '# Activation fixture v0.1.0' \164+ "$install_root/current/docs/sigil-host-activation.md"165+grep -Fq "activation_guide=$install_root/current/docs/sigil-host-activation.md" \166+ "$temp_root/install-a.log"167+if grep -Fq 'fresh-bazzite-host.md' "$temp_root/install-a.log"; then168+ printf 'FAIL: package installer delegated activation to the lab runbook\n' >&2169+ exit 1170+fi150171 151172 run_installer "$payload_a" >"$temp_root/repeat-a.log"152173 [[ "$(basename -- "$(readlink -f "$install_root/current")")" == "$release_a" ]]
@@ -164,6 +185,8 @@ config_sha="$(sha256sum "$config" | awk '{print $1}')"
164185 run_installer "$payload_b" >"$temp_root/install-b.log"165186 [[ "$(basename -- "$(readlink -f "$install_root/current")")" == "$release_b" ]]166187 [[ "$(basename -- "$(readlink -f "$install_root/previous")")" == "$release_a" ]]188+grep -Fqx '# Activation fixture v0.1.1' \189+ "$install_root/current/docs/sigil-host-activation.md"167190 [[ "$(sha256sum "$identity" | awk '{print $1}')" == "$identity_sha" ]]168191 [[ "$(sha256sum "$config" | awk '{print $1}')" == "$config_sha" ]]169192
M scripts/tests/package-bazzite-product-contract.sh +3 -0
A scripts/tests/sigil-host-activation-guide.sh +62 -0
@@ -0,0 +1,62 @@
1+#!/usr/bin/env bash2+3+set -euo pipefail4+5+script_dir="$(CDPATH='' cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"6+repo_dir="$(CDPATH='' cd -- "$script_dir/../.." && pwd -P)"7+guide="$repo_dir/docs/sigil-host-activation.md"8+installer="$repo_dir/scripts/install-bazzite-package.sh"9+bootstrap="$repo_dir/website/install-sigil"10+temp_parent="${TMPDIR:-/tmp}"11+temp_parent="${temp_parent%/}"12+temp_root="$(mktemp -d "$temp_parent/sigil-activation-guide.XXXXXX")"13+14+cleanup() {15+ local status=$?16+ trap - EXIT INT TERM HUP17+ case "$temp_root" in18+ "$temp_parent"/sigil-activation-guide.??????) rm -rf -- "$temp_root" ;;19+ esac20+ exit "$status"21+}22+trap cleanup EXIT INT TERM HUP23+24+fail() {25+ printf 'FAIL: %s\n' "$1" >&226+ exit 127+}28+29+[[ -f "$guide" && ! -L "$guide" ]] || fail 'the activation guide is missing or unsafe'30+31+awk '32+ /^```bash$/ { inside = 1; next }33+ /^```$/ && inside { inside = 0; print ""; next }34+ inside { print }35+' "$guide" >"$temp_root/guide-code.sh"36+bash -n "$temp_root/guide-code.sh"37+38+if grep -Eq 'tank@|/Users/dami|Developer/sigil-spark|scripts/' "$guide"; then39+ fail 'the product activation guide contains a maintainer or source-checkout path'40+fi41+if grep -Eq '/dev/dri/renderD128|vaapi_encoder = "vah264enc"' "$guide"; then42+ fail 'the activation guide pins the original test host GPU contract'43+fi44+45+grep -Fq 'refusing to replace existing host configuration' "$guide" \46+ || fail 'the activation guide can overwrite an existing host configuration'47+grep -Fq 'current/assets/72-sigil-uinput.rules' "$guide" \48+ || fail 'the activation guide does not use its packaged early uinput rule'49+grep -Fq 'current/assets/99-sigil-uinput.rules' "$guide" \50+ || fail 'the activation guide does not use its packaged final uinput rule'51+grep -Fq 'native Gamescope' "$guide" \52+ || fail 'the activation guide does not preserve dynamic native resolution'53+54+for output_script in "$installer" "$bootstrap"; do55+ grep -Fq 'current/docs/sigil-host-activation.md' "$output_script" \56+ || fail "$(basename "$output_script") does not print the persistent guide path"57+ if grep -Fq 'fresh-bazzite-host.md' "$output_script"; then58+ fail "$(basename "$output_script") still delegates activation to the lab runbook"59+ fi60+done61+62+printf 'sigil_host_activation_guide_tests=ok\n'
M scripts/tests/sigil-release-verifier.sh +9 -1
M scripts/tests/test_sigil_bootstrap.py +5 -0
M scripts/verify-sigil-release.sh +4 -0
M website/install-sigil +1 -1