cat / goq
clone

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

Merge pull request #98 from FelineStateMachine/feat/portable-installer-gates

FelineStateMachine committed 48635dd
fix(install): ship portable host activation
commit
48635dde1f13bd5f5e82d87a6c8472359f4e2228
parent
f8679be7a4c84a5f3ee0774fb08a1307f1b06973
parent
209456b72ebf505e371aacc21ed4135104f5465c
author
FelineStateMachine <dami.etoile@gmail.com>
14 files changed, +500 -19
MREADME.md25+-
Mdocs/fresh-bazzite-host.md8+-
Adocs/sigil-host-activation.md338++++++++++++++++++++++++++++++
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.sh68++++++
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 +338 -0
@@ -0,0 +1,338 @@
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+ if test ! -f "$rule" || test -L "$rule"; then29+ echo "unsafe or missing package rule: $rule" >&230+ exit 131+ fi32+done33+34+getent group sigil-uinput >/dev/null || sudo groupadd --system sigil-uinput35+sudo usermod --append --groups sigil-uinput "$USER"36+sudo install -o root -g root -m 0644 \37+ "$remote_input_rule" /etc/udev/rules.d/70-sigil-remote-input.rules38+sudo install -o root -g root -m 0644 \39+ "$early_uinput_rule" /etc/udev/rules.d/72-sigil-uinput.rules40+sudo install -o root -g root -m 0644 \41+ "$final_uinput_rule" /etc/udev/rules.d/99-sigil-uinput.rules42+sudo modprobe uinput43+sudo udevadm control --reload-rules44+sudo udevadm trigger --action=add --subsystem-match=misc --sysname-match=uinput45+```46+47+Reboot, or fully sign out and back in, before continuing. A new login is48+required for the user service to inherit the `sigil-uinput` group.49+50+After that login, fail closed unless the device has the expected Linux uinput51+identity, ownership, mode, and no extended ACL:52+53+```bash54+set -euo pipefail55+56+uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"57+test -n "$uinput_gid"58+id -G | tr ' ' '\n' | grep -Fx "$uinput_gid"59+test ! -L /dev/uinput60+test -c /dev/uinput && test -r /dev/uinput && test -w /dev/uinput61+test "$(stat -Lc '%u:%g:%a:%t:%T' /dev/uinput)" = "0:$uinput_gid:660:a:df"62+if getfacl -cp /dev/uinput | grep -Eq '^(user|group):[^:]|^mask:'; then63+ echo '/dev/uinput has an extended access ACL' >&264+ exit 165+fi66+```67+68+Do not add this account to the broad `input` group and do not run Sigil as69+root. Access to uinput is equivalent to local controller, keyboard, and pointer70+control.71+72+## 2. Create the host identity once73+74+```bash75+set -euo pipefail76+umask 07777+78+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"79+identity="$HOME/.local/share/sigil-spark/identity/host.key"80+install -d -m 0700 "$(dirname "$identity")" \81+ "$HOME/.config/sigil-spark" "$HOME/.local/state/sigil-spark"82+if test ! -e "$identity" && test ! -L "$identity"; then83+ "$sigil" identity init --output "$identity"84+fi85+if test ! -f "$identity" || test -L "$identity"; then86+ echo "unsafe host identity: $identity" >&287+ exit 188+fi89+chmod 0600 "$identity"90+"$sigil" identity show --identity "$identity"91+```92+93+Never copy this seed to a client or rotate it during an upgrade. Portal pairs94+to the host identity represented by this file.95+96+## 3. Select the exact AMD render node and GstVA H.264 factory97+98+Sigil binds the encoder factory to its inspected read-only `device-path`; a99+factory name alone is not enough on a multi-GPU machine. The following chooses100+automatically only when there is one usable AMD node/factory pair. Set101+`RENDER_NODE` and `VA_ENCODER` explicitly before running it when the printed102+inventory is intentionally ambiguous.103+104+```bash105+set -euo pipefail106+107+gst_inspect_path="$(command -v gst-inspect-1.0)"108+timeout_path="$(command -v timeout)"109+inspect_max_bytes=1048576110+inspection_dir="$(mktemp -d "${TMPDIR:-/tmp}/sigil-activation-inspect.XXXXXX")"111+chmod 0700 "$inspection_dir"112+trap 'rm -rf -- "$inspection_dir"' EXIT INT TERM HUP113+114+bounded_gst_inspect() {115+ local output_path116+ local output_size117+ output_path="$(mktemp "$inspection_dir/gst-inspect.XXXXXX")"118+ if ! "$timeout_path" --signal=TERM --kill-after=2s 5s \119+ "$gst_inspect_path" "$@" 2>/dev/null \120+ | head -c "$((inspect_max_bytes + 1))" >"$output_path"121+ then122+ rm -f -- "$output_path"123+ return 1124+ fi125+ output_size="$(wc -c <"$output_path" | tr -d '[:space:]')"126+ if test "$output_size" -gt "$inspect_max_bytes"; then127+ rm -f -- "$output_path"128+ return 1129+ fi130+ cat "$output_path"131+ rm -f -- "$output_path"132+}133+134+mapfile -t amd_nodes < <(135+ for sysfs_node in /sys/class/drm/renderD*; do136+ test -e "$sysfs_node/device/driver" || continue137+ test "$(basename "$(readlink -f "$sysfs_node/device/driver")")" = amdgpu \138+ || continue139+ node="/dev/dri/$(basename "$sysfs_node")"140+ test -c "$node" && test -r "$node" && test -w "$node" && printf '%s\n' "$node"141+ done142+)143+printf 'AMD render node: %s\n' "${amd_nodes[@]}"144+if test -n "${RENDER_NODE:-}"; then145+ render_node="$RENDER_NODE"146+else147+ test "${#amd_nodes[@]}" -eq 1148+ render_node="${amd_nodes[0]}"149+fi150+printf '%s\n' "${amd_nodes[@]}" | grep -Fx "$render_node"151+152+registry="$(bounded_gst_inspect)"153+mapfile -t va_factories < <(154+ awk '$2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {155+ sub(/:$/, "", $2); print $2156+ }' <<<"$registry" | sort -u157+)158+matching_factories=()159+for factory in "${va_factories[@]}"; do160+ inspection="$(bounded_gst_inspect "$factory")" || continue161+ grep -Fq "Default: \"$render_node\"" <<<"$inspection" || continue162+ grep -Eq '\([0-9]+\): cbr([[:space:]]|$)' <<<"$inspection" || continue163+ matching_factories+=("$factory")164+done165+printf 'matching CBR GstVA factory: %s\n' "${matching_factories[@]}"166+if test -n "${VA_ENCODER:-}"; then167+ va_encoder="$VA_ENCODER"168+else169+ test "${#matching_factories[@]}" -eq 1170+ va_encoder="${matching_factories[0]}"171+fi172+printf '%s\n' "${matching_factories[@]}" | grep -Fx "$va_encoder"173+174+printf 'export SIGIL_RENDER_NODE=%q\n' "$render_node"175+printf 'export SIGIL_VA_ENCODER=%q\n' "$va_encoder"176+177+rm -rf -- "$inspection_dir"178+trap - EXIT INT TERM HUP179+```180+181+Keep the two printed export commands for the next step. If automatic selection182+fails, inspect every printed node and factory and rerun with an exact pair, for183+example `RENDER_NODE=/dev/dri/renderD129 VA_ENCODER=varenderD129h264enc`.184+Never substitute a software encoder just to make preflight pass.185+186+## 4. Restart the package-owned audio sink187+188+The package already linked its PipeWire drop-in into the gaming user's config.189+Restart only the PulseAudio-compatible PipeWire service during provisioning,190+then select the persistent headless sink:191+192+```bash193+set -euo pipefail194+195+test -f "$HOME/.config/pipewire/pipewire-pulse.conf.d/50-sigil-spark-audio.conf"196+systemctl --user restart pipewire-pulse.service197+audio_sink_id="$(wpctl status -n | awk '198+ $0 ~ /sigil_spark/ {199+ for (field = 1; field <= NF; field++) {200+ if ($field ~ /^[0-9]+\.$/) {201+ gsub("\\.", "", $field); print $field; exit202+ }203+ }204+ }205+')"206+test "$audio_sink_id" -gt 0207+wpctl set-default "$audio_sink_id"208+pactl get-default-sink | grep -Fx sigil_spark209+```210+211+This restart can interrupt existing local playback. Perform it before starting212+a game.213+214+## 5. Write and validate the hardware configuration215+216+First run the two `export` commands printed by step 3 in this shell. Width and217+height are intentionally omitted so Sigil follows the bounded native Gamescope218+mode instead of assuming one panel resolution.219+220+```bash221+set -euo pipefail222+: "${SIGIL_RENDER_NODE:?run the render-node exports from step 3}"223+: "${SIGIL_VA_ENCODER:?run the encoder exports from step 3}"224+225+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"226+uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"227+config="$HOME/.config/sigil-spark/host.toml"228+ffmpeg_path="$(command -v ffmpeg)"229+pw_dump_path="$(command -v pw-dump)"230+gst_launch_path="$(command -v gst-launch-1.0)"231+gst_inspect_path="$(command -v gst-inspect-1.0)"232+test -n "$uinput_gid"233+234+umask 077235+set +e236+set -o noclobber237+exec 3>"$config"238+create_status=$?239+set +o noclobber240+set -e241+if test "$create_status" -ne 0; then242+ echo "refusing to replace existing host configuration: $config" >&2243+ exit 1244+fi245+cat >&3 <<EOF246+identity_path = "$HOME/.local/share/sigil-spark/identity/host.key"247+state_path = "$HOME/.local/state/sigil-spark/runtime"248+source = "gamescope-pipewire"249+framerate = 60250+codec = "h264"251+input_mode = "uinput"252+ffmpeg_path = "$ffmpeg_path"253+254+[uinput]255+device_path = "/dev/uinput"256+expected_owner_uid = 0257+expected_group_gid = $uinput_gid258+expected_mode = 0o660259+260+[gamescope_pipewire]261+node_name = "gamescope"262+media_class = "Video/Source"263+xwayland_display = ":0"264+pw_dump_path = "$pw_dump_path"265+gst_launch_path = "$gst_launch_path"266+gst_inspect_path = "$gst_inspect_path"267+encoder_backend = "external-gst-launch"268+vaapi_encoder = "$SIGIL_VA_ENCODER"269+vaapi_render_node = "$SIGIL_RENDER_NODE"270+rate_control = "cbr"271+bitrate_kbps = 12000272+273+[audio]274+node_name = "sigil_spark"275+media_class = "Audio/Sink"276+bitrate_bps = 96000277+EOF278+exec 3>&-279+test "$(stat -Lc '%a:%U' "$config")" = "600:$(id -un)"280+281+"$sigil" config check --config "$config"282+"$sigil" capture probe \283+ --source gamescope-pipewire --config "$config" --frames 300284+```285+286+Both commands must exit zero. The capture probe must report 300 encoded frames,287+zero post-encode drops, and the actual resolved dimensions. Do not enable the288+service after a failed preflight or by weakening the device/factory checks.289+290+## 6. Start, inspect, and only then enable Sigil291+292+```bash293+set -euo pipefail294+295+systemctl --user daemon-reload296+systemctl --user start sigil-host.service297+systemctl --user is-active --quiet sigil-host.service298+invocation="$(systemctl --user show -p InvocationID --value sigil-host.service)"299+test -n "$invocation"300+ready=false301+for _attempt in $(seq 1 60); do302+ if journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" \303+ _SYSTEMD_USER_UNIT=sigil-host.service --no-pager -o cat \304+ | grep -Fxq status=ready305+ then306+ ready=true307+ break308+ fi309+ sleep 1310+done311+test "$ready" = true312+systemctl --user status sigil-host.service --no-pager313+journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" --no-pager -o cat314+315+sigil="$HOME/.local/libexec/sigil-spark/current/sigil"316+"$sigil" appliance status \317+ --config "$HOME/.config/sigil-spark/host.toml" --json318+319+systemctl --user enable sigil-host.service320+```321+322+The exact service invocation must report `status=ready` before enablement. The323+unit runs in the gaming user's ordinary user manager and retries safely if its324+configured Gamescope or PipeWire source is not ready yet; no root daemon or325+interactive desktop capture portal is involved.326+327+Create a one-time, short-lived invitation only after Portal shows its peer ID:328+329+```bash330+"$HOME/.local/libexec/sigil-spark/current/sigil" invitation create \331+ --config "$HOME/.config/sigil-spark/host.toml" \332+ --peer '<PORTAL_IROH_PEER_ID>' \333+ --expires-in-seconds 900 --pointer-keyboard --gamepad \334+ --output "$HOME/sigil.goq-invite"335+```336+337+Move that owner-only file to Portal and confirm the displayed host and grants.338+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 +68 -0
@@ -0,0 +1,68 @@
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 'set -o noclobber' "$guide" \48+ || fail 'the activation guide does not create host.toml exclusively'49+grep -Fq -- '--kill-after=2s 5s' "$guide" \50+ || fail 'the activation guide does not time-bound GStreamer inspection'51+grep -Fq 'inspect_max_bytes=1048576' "$guide" \52+ || fail 'the activation guide does not size-bound GStreamer inspection'53+grep -Fq 'current/assets/72-sigil-uinput.rules' "$guide" \54+ || fail 'the activation guide does not use its packaged early uinput rule'55+grep -Fq 'current/assets/99-sigil-uinput.rules' "$guide" \56+ || fail 'the activation guide does not use its packaged final uinput rule'57+grep -Fq 'native Gamescope' "$guide" \58+ || fail 'the activation guide does not preserve dynamic native resolution'59+60+for output_script in "$installer" "$bootstrap"; do61+ grep -Fq 'current/docs/sigil-host-activation.md' "$output_script" \62+ || fail "$(basename "$output_script") does not print the persistent guide path"63+ if grep -Fq 'fresh-bazzite-host.md' "$output_script"; then64+ fail "$(basename "$output_script") still delegates activation to the lab runbook"65+ fi66+done67+68+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