cat / goq
clone

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

fix/drop-hero-notarization-note
goq / docs / sigil-host-activation.md
sigil-host-activation.md 12.7K rendered
1# Activate an installed Sigil host23This is the portable post-install path for a dedicated Bazzite or SteamOS-like4AMD gaming host. Run it as the gaming user that owns the Gamescope session, not5as root. The commands use only `$HOME` and `$USER`; no hostname or account name6is assumed.78The package installer deliberately stops before these steps. Identity creation,9system input permissions, PipeWire restart, hardware configuration, and service10enablement each remain visible operator decisions. Re-running a package install11or rollback preserves the identity and `host.toml` created here.1213## 1. Grant the gaming user access to only uinput1415The package stages its virtual-input udev rule but does not write `/etc` or16change group membership. Install that exact package asset together with the two17rules that bound `/dev/uinput` to a dedicated group:1819```bash20set -euo pipefail2122current="$HOME/.local/libexec/sigil-spark/current"23remote_input_rule="$current/assets/70-sigil-remote-input.rules"24early_uinput_rule="$current/assets/72-sigil-uinput.rules"25final_uinput_rule="$current/assets/99-sigil-uinput.rules"26test -x "$current/sigil"27for 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 fi32done3334getent group sigil-uinput >/dev/null || sudo groupadd --system sigil-uinput35sudo usermod --append --groups sigil-uinput "$USER"36sudo install -o root -g root -m 0644 \37 "$remote_input_rule" /etc/udev/rules.d/70-sigil-remote-input.rules38sudo install -o root -g root -m 0644 \39 "$early_uinput_rule" /etc/udev/rules.d/72-sigil-uinput.rules40sudo install -o root -g root -m 0644 \41 "$final_uinput_rule" /etc/udev/rules.d/99-sigil-uinput.rules42sudo modprobe uinput43sudo udevadm control --reload-rules44sudo udevadm trigger --action=add --subsystem-match=misc --sysname-match=uinput45```4647Reboot, or fully sign out and back in, before continuing. A new login is48required for the user service to inherit the `sigil-uinput` group.4950After that login, fail closed unless the device has the expected Linux uinput51identity, ownership, mode, and no extended ACL:5253```bash54set -euo pipefail5556uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"57test -n "$uinput_gid"58id -G | tr ' ' '\n' | grep -Fx "$uinput_gid"59test ! -L /dev/uinput60test -c /dev/uinput && test -r /dev/uinput && test -w /dev/uinput61test "$(stat -Lc '%u:%g:%a:%t:%T' /dev/uinput)" = "0:$uinput_gid:660:a:df"62if getfacl -cp /dev/uinput | grep -Eq '^(user|group):[^:]|^mask:'; then63 echo '/dev/uinput has an extended access ACL' >&264 exit 165fi66```6768Do not add this account to the broad `input` group and do not run Sigil as69root. Access to uinput is equivalent to local controller, keyboard, and pointer70control.7172The shipped Bazzite/SteamOS-like setup above is the default **group-only**73scheme. Sigil expects the configured owner, group, and mode and rejects every74extended access ACL. Its preflight error names that scheme explicitly.7576On a distribution that deliberately provisions `/dev/uinput` with a77seat-managed POSIX ACL instead, opt in to exactly one named-user entry:7879```toml80[uinput]81device_path = "/dev/uinput"82expected_owner_uid = 083expected_group_gid = 98684expected_mode = 0o66085expected_acl_user_uid = 100086```8788Use the machine-local numeric UID of the non-root account that runs Sigil.89Sigil requires it to equal the daemon's effective UID. The access ACL must have90exactly the canonical `user::rw-`, `user:<uid>:rw-`, `group::rw-`, `mask::rw-`,91and `other::---` entries matching the configured mode; extra users, named92groups, different permissions, or a missing ACL fail closed. Owner, group,93mode, character-device identity, and major/minor checks still apply.9495This opt-in does not make logind seat state optional. If a `uaccess` ACL is96removed during a headless boot, SSH session, or seat transition, Sigil refuses97to start. Prefer the shipped dedicated-group scheme for an appliance; use the98one-user ACL form only when the operator owns and verifies that distribution's99seat lifecycle.100101## 2. Create the host identity once102103```bash104set -euo pipefail105umask 077106107sigil="$HOME/.local/libexec/sigil-spark/current/sigil"108identity="$HOME/.local/share/sigil-spark/identity/host.key"109install -d -m 0700 "$(dirname "$identity")" \110 "$HOME/.config/sigil-spark" "$HOME/.local/state/sigil-spark"111if test ! -e "$identity" && test ! -L "$identity"; then112 "$sigil" identity init --output "$identity"113fi114if test ! -f "$identity" || test -L "$identity"; then115 echo "unsafe host identity: $identity" >&2116 exit 1117fi118chmod 0600 "$identity"119"$sigil" identity show --identity "$identity"120```121122Never copy this seed to a client or rotate it during an upgrade. Portal pairs123to the host identity represented by this file.124125## 3. Select the exact VA-API render node and GstVA H.264 factory126127Sigil binds the encoder factory to its programmatically queried read-only128`device-path`; a129factory name alone is not enough on a multi-GPU machine. The following chooses130automatically only when there is one usable capability-backed node/factory131pair, independent of its kernel driver. Set132`RENDER_NODE` and `VA_ENCODER` explicitly before running it when the printed133inventory is intentionally ambiguous.134135```bash136set -euo pipefail137138gst_inspect_path="$(command -v gst-inspect-1.0)"139timeout_path="$(command -v timeout)"140inspect_max_bytes=1048576141inspection_dir="$(mktemp -d "${TMPDIR:-/tmp}/sigil-activation-inspect.XXXXXX")"142chmod 0700 "$inspection_dir"143trap 'rm -rf -- "$inspection_dir"' EXIT INT TERM HUP144145bounded_gst_inspect() {146 local output_path147 local output_size148 output_path="$(mktemp "$inspection_dir/gst-inspect.XXXXXX")"149 if ! "$timeout_path" --signal=TERM --kill-after=2s 5s \150 "$gst_inspect_path" "$@" 2>/dev/null \151 | head -c "$((inspect_max_bytes + 1))" >"$output_path"152 then153 rm -f -- "$output_path"154 return 1155 fi156 output_size="$(wc -c <"$output_path" | tr -d '[:space:]')"157 if test "$output_size" -gt "$inspect_max_bytes"; then158 rm -f -- "$output_path"159 return 1160 fi161 cat "$output_path"162 rm -f -- "$output_path"163}164165bounded_encoder_preflight() {166 local node="$1"167 local factory="$2"168 local output_path169 local output_size170 output_path="$(mktemp "$inspection_dir/encoder-preflight.XXXXXX")"171 if ! "$timeout_path" --signal=TERM --kill-after=2s 5s \172 "$sigil" capture encoder-preflight \173 --vaapi-encoder "$factory" --vaapi-render-node "$node" \174 --rate-control cbr --rate-control cqp 2>&1 \175 | head -c "$((inspect_max_bytes + 1))" >"$output_path"176 then177 rm -f -- "$output_path"178 return 1179 fi180 output_size="$(wc -c <"$output_path" | tr -d '[:space:]')"181 rm -f -- "$output_path"182 test "$output_size" -le "$inspect_max_bytes"183}184185mapfile -t render_nodes < <(186 for node in /dev/dri/renderD*; do187 test -c "$node" && test -r "$node" && test -w "$node" && printf '%s\n' "$node"188 done189)190printf 'accessible render node: %s\n' "${render_nodes[@]}"191192registry="$(bounded_gst_inspect)"193mapfile -t va_factories < <(194 awk '$2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {195 sub(/:$/, "", $2); print $2196 }' <<<"$registry" | sort -u197)198eligible_pairs=()199for node in "${render_nodes[@]}"; do200 for factory in "${va_factories[@]}"; do201 bounded_encoder_preflight "$node" "$factory" || continue202 eligible_pairs+=("$node $factory")203 done204done205printf 'eligible CBR GstVA pair: %s\n' "${eligible_pairs[@]}"206if test -n "${RENDER_NODE:-}" || test -n "${VA_ENCODER:-}"; then207 test -n "${RENDER_NODE:-}" && test -n "${VA_ENCODER:-}"208 selected_pair="$RENDER_NODE $VA_ENCODER"209 printf '%s\n' "${eligible_pairs[@]}" | grep -Fx "$selected_pair"210else211 test "${#eligible_pairs[@]}" -eq 1212 selected_pair="${eligible_pairs[0]}"213fi214read -r render_node va_encoder <<<"$selected_pair"215216printf 'export SIGIL_RENDER_NODE=%q\n' "$render_node"217printf 'export SIGIL_VA_ENCODER=%q\n' "$va_encoder"218219rm -rf -- "$inspection_dir"220trap - EXIT INT TERM HUP221```222223Keep the two printed export commands for the next step. If automatic selection224fails, inspect every printed node and factory and rerun with an exact pair, for225example `RENDER_NODE=/dev/dri/renderD129 VA_ENCODER=varenderD129h264enc`.226Never substitute a software encoder just to make preflight pass.227228## 4. Restart the package-owned audio sink229230The package already linked its PipeWire drop-in into the gaming user's config.231Restart only the PulseAudio-compatible PipeWire service during provisioning,232then select the persistent headless sink:233234```bash235set -euo pipefail236237test -f "$HOME/.config/pipewire/pipewire-pulse.conf.d/50-sigil-spark-audio.conf"238systemctl --user restart pipewire-pulse.service239audio_sink_id="$(wpctl status -n | awk '240 $0 ~ /sigil_spark/ {241 for (field = 1; field <= NF; field++) {242 if ($field ~ /^[0-9]+\.$/) {243 gsub("\\.", "", $field); print $field; exit244 }245 }246 }247')"248test "$audio_sink_id" -gt 0249wpctl set-default "$audio_sink_id"250pactl get-default-sink | grep -Fx sigil_spark251```252253This restart can interrupt existing local playback. Perform it before starting254a game.255256## 5. Write and validate the hardware configuration257258First run the two `export` commands printed by step 3 in this shell. Width and259height are intentionally omitted so Sigil follows the bounded native Gamescope260mode instead of assuming one panel resolution.261262```bash263set -euo pipefail264: "${SIGIL_RENDER_NODE:?run the render-node exports from step 3}"265: "${SIGIL_VA_ENCODER:?run the encoder exports from step 3}"266267sigil="$HOME/.local/libexec/sigil-spark/current/sigil"268uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"269config="$HOME/.config/sigil-spark/host.toml"270ffmpeg_path="$(command -v ffmpeg)"271pw_dump_path="$(command -v pw-dump)"272gst_launch_path="$(command -v gst-launch-1.0)"273gst_inspect_path="$(command -v gst-inspect-1.0)"274test -n "$uinput_gid"275276umask 077277set +e278set -o noclobber279exec 3>"$config"280create_status=$?281set +o noclobber282set -e283if test "$create_status" -ne 0; then284 echo "refusing to replace existing host configuration: $config" >&2285 exit 1286fi287cat >&3 <<EOF288identity_path = "$HOME/.local/share/sigil-spark/identity/host.key"289state_path = "$HOME/.local/state/sigil-spark/runtime"290source = "gamescope-pipewire"291framerate = 60292codec = "h264"293input_mode = "uinput"294ffmpeg_path = "$ffmpeg_path"295296[uinput]297device_path = "/dev/uinput"298expected_owner_uid = 0299expected_group_gid = $uinput_gid300expected_mode = 0o660301302[gamescope_pipewire]303node_name = "gamescope"304media_class = "Video/Source"305xwayland_display = ":0"306pw_dump_path = "$pw_dump_path"307gst_launch_path = "$gst_launch_path"308gst_inspect_path = "$gst_inspect_path"309encoder_backend = "external-gst-launch"310vaapi_encoder = "$SIGIL_VA_ENCODER"311vaapi_render_node = "$SIGIL_RENDER_NODE"312rate_control = "cbr"313bitrate_kbps = 12000314315[audio]316node_name = "sigil_spark"317media_class = "Audio/Sink"318bitrate_bps = 96000319EOF320exec 3>&-321test "$(stat -Lc '%a:%U' "$config")" = "600:$(id -un)"322323"$sigil" config check --config "$config"324"$sigil" capture probe \325 --source gamescope-pipewire --config "$config" --frames 300326```327328Both commands must exit zero. The capture probe must report 300 encoded frames,329zero post-encode drops, and the actual resolved dimensions. Do not enable the330service after a failed preflight or by weakening the device/factory checks.331332## 6. Start, inspect, and only then enable Sigil333334```bash335set -euo pipefail336337systemctl --user daemon-reload338systemctl --user start sigil-host.service339systemctl --user is-active --quiet sigil-host.service340invocation="$(systemctl --user show -p InvocationID --value sigil-host.service)"341test -n "$invocation"342ready=false343for _attempt in $(seq 1 60); do344 if journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" \345 _SYSTEMD_USER_UNIT=sigil-host.service --no-pager -o cat \346 | grep -Fxq status=ready347 then348 ready=true349 break350 fi351 sleep 1352done353test "$ready" = true354systemctl --user status sigil-host.service --no-pager355journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" --no-pager -o cat356357sigil="$HOME/.local/libexec/sigil-spark/current/sigil"358"$sigil" appliance status \359 --config "$HOME/.config/sigil-spark/host.toml" --json360361systemctl --user enable sigil-host.service362```363364The exact service invocation must report `status=ready` before enablement. The365unit runs in the gaming user's ordinary user manager and retries safely if its366configured Gamescope or PipeWire source is not ready yet; no root daemon or367interactive desktop capture portal is involved.368369Create a one-time, short-lived invitation only after Portal shows its peer ID:370371```bash372"$HOME/.local/libexec/sigil-spark/current/sigil" invitation create \373 --config "$HOME/.config/sigil-spark/host.toml" \374 --peer '<PORTAL_IROH_PEER_ID>' \375 --expires-in-seconds 900 --pointer-keyboard --gamepad \376 --output "$HOME/sigil.goq-invite"377```378379Move that owner-only file to Portal and confirm the displayed host and grants.380After the one-time enrollment, ordinary startup remains **PIN -> tap -> play**.