cat / goq
clone

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

fix/hero-release-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. The5commands use only `$HOME` and `$USER`; no hostname or account name is assumed.67The package installer deliberately stops before these steps. Identity creation,8system input permissions, PipeWire restart, hardware configuration, and service9enablement each remain visible operator decisions. Re-running a package install10or rollback preserves the identity and `host.toml` created here.1112## 1. Grant the gaming user access to only uinput1314The package stages its virtual-input udev rule but does not write `/etc` or15change group membership. Install that exact package asset together with the two16rules that bound `/dev/uinput` to a dedicated group:1718```bash19set -euo pipefail2021current="$HOME/.local/libexec/sigil-spark/current"22remote_input_rule="$current/assets/70-sigil-remote-input.rules"23early_uinput_rule="$current/assets/72-sigil-uinput.rules"24final_uinput_rule="$current/assets/99-sigil-uinput.rules"25test -x "$current/sigil"26for rule in "$remote_input_rule" "$early_uinput_rule" "$final_uinput_rule"; do27 if test ! -f "$rule" || test -L "$rule"; then28 echo "unsafe or missing package rule: $rule" >&229 exit 130 fi31done3233getent group sigil-uinput >/dev/null || sudo groupadd --system sigil-uinput34sudo usermod --append --groups sigil-uinput "$USER"35sudo install -o root -g root -m 0644 \36 "$remote_input_rule" /etc/udev/rules.d/70-sigil-remote-input.rules37sudo install -o root -g root -m 0644 \38 "$early_uinput_rule" /etc/udev/rules.d/72-sigil-uinput.rules39sudo install -o root -g root -m 0644 \40 "$final_uinput_rule" /etc/udev/rules.d/99-sigil-uinput.rules41sudo modprobe uinput42sudo udevadm control --reload-rules43sudo udevadm trigger --action=add --subsystem-match=misc --sysname-match=uinput44```4546Reboot, or fully sign out and back in, before continuing. A new login is47required for the user service to inherit the `sigil-uinput` group.4849After that login, fail closed unless the device has the expected Linux uinput50identity, ownership, mode, and no extended ACL:5152```bash53set -euo pipefail5455uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"56test -n "$uinput_gid"57id -G | tr ' ' '\n' | grep -Fx "$uinput_gid"58test ! -L /dev/uinput59test -c /dev/uinput && test -r /dev/uinput && test -w /dev/uinput60test "$(stat -Lc '%u:%g:%a:%t:%T' /dev/uinput)" = "0:$uinput_gid:660:a:df"61if getfacl -cp /dev/uinput | grep -Eq '^(user|group):[^:]|^mask:'; then62 echo '/dev/uinput has an extended access ACL' >&263 exit 164fi65```6667Do not add this account to the broad `input` group and do not run Sigil as68root. Access to uinput is equivalent to local controller, keyboard, and pointer69control.7071The shipped Bazzite/SteamOS-like setup above is the default **group-only**72scheme. Sigil expects the configured owner, group, and mode and rejects every73extended access ACL. Its preflight error names that scheme explicitly.7475On a distribution that deliberately provisions `/dev/uinput` with a76seat-managed POSIX ACL instead, opt in to exactly one named-user entry:7778```toml79[uinput]80device_path = "/dev/uinput"81expected_owner_uid = 082expected_group_gid = 98683expected_mode = 0o66084expected_acl_user_uid = 100085```8687Use the machine-local numeric UID of the non-root account that runs Sigil.88Sigil requires it to equal the daemon's effective UID. The access ACL must have89exactly the canonical `user::rw-`, `user:<uid>:rw-`, `group::rw-`, `mask::rw-`,90and `other::---` entries matching the configured mode; extra users, named91groups, different permissions, or a missing ACL fail closed. Owner, group,92mode, character-device identity, and major/minor checks still apply.9394This opt-in does not make logind seat state optional. If a `uaccess` ACL is95removed during a headless boot, SSH session, or seat transition, Sigil refuses96to start. Prefer the shipped dedicated-group scheme for an appliance; use the97one-user ACL form only when the operator owns and verifies that distribution's98seat lifecycle.99100## 2. Create the host identity once101102```bash103set -euo pipefail104umask 077105106sigil="$HOME/.local/libexec/sigil-spark/current/sigil"107identity="$HOME/.local/share/sigil-spark/identity/host.key"108install -d -m 0700 "$(dirname "$identity")" \109 "$HOME/.config/sigil-spark" "$HOME/.local/state/sigil-spark"110if test ! -e "$identity" && test ! -L "$identity"; then111 "$sigil" identity init --output "$identity"112fi113if test ! -f "$identity" || test -L "$identity"; then114 echo "unsafe host identity: $identity" >&2115 exit 1116fi117chmod 0600 "$identity"118"$sigil" identity show --identity "$identity"119```120121Never copy this seed to a client or rotate it during an upgrade. Portal pairs122to the host identity represented by this file.123124## 3. Select the exact VA-API render node and GstVA H.264 factory125126Sigil binds the encoder factory to its programmatically queried read-only127`device-path`; a128factory name alone is not enough on a multi-GPU machine. The following chooses129automatically only when there is one usable capability-backed node/factory130pair, independent of its kernel driver. Set131`RENDER_NODE` and `VA_ENCODER` explicitly before running it when the printed132inventory is intentionally ambiguous.133134```bash135set -euo pipefail136137gst_inspect_path="$(command -v gst-inspect-1.0)"138timeout_path="$(command -v timeout)"139inspect_max_bytes=1048576140inspection_dir="$(mktemp -d "${TMPDIR:-/tmp}/sigil-activation-inspect.XXXXXX")"141chmod 0700 "$inspection_dir"142trap 'rm -rf -- "$inspection_dir"' EXIT INT TERM HUP143144bounded_gst_inspect() {145 local output_path146 local output_size147 output_path="$(mktemp "$inspection_dir/gst-inspect.XXXXXX")"148 if ! "$timeout_path" --signal=TERM --kill-after=2s 5s \149 "$gst_inspect_path" "$@" 2>/dev/null \150 | head -c "$((inspect_max_bytes + 1))" >"$output_path"151 then152 rm -f -- "$output_path"153 return 1154 fi155 output_size="$(wc -c <"$output_path" | tr -d '[:space:]')"156 if test "$output_size" -gt "$inspect_max_bytes"; then157 rm -f -- "$output_path"158 return 1159 fi160 cat "$output_path"161 rm -f -- "$output_path"162}163164bounded_encoder_preflight() {165 local node="$1"166 local factory="$2"167 local output_path168 local output_size169 output_path="$(mktemp "$inspection_dir/encoder-preflight.XXXXXX")"170 if ! "$timeout_path" --signal=TERM --kill-after=2s 5s \171 "$sigil" capture encoder-preflight \172 --vaapi-encoder "$factory" --vaapi-render-node "$node" \173 --rate-control cbr --rate-control cqp 2>&1 \174 | head -c "$((inspect_max_bytes + 1))" >"$output_path"175 then176 rm -f -- "$output_path"177 return 1178 fi179 output_size="$(wc -c <"$output_path" | tr -d '[:space:]')"180 rm -f -- "$output_path"181 test "$output_size" -le "$inspect_max_bytes"182}183184mapfile -t render_nodes < <(185 for node in /dev/dri/renderD*; do186 test -c "$node" && test -r "$node" && test -w "$node" && printf '%s\n' "$node"187 done188)189printf 'accessible render node: %s\n' "${render_nodes[@]}"190191registry="$(bounded_gst_inspect)"192mapfile -t va_factories < <(193 awk '$2 ~ /^va(renderD[0-9]+)?h264(lp)?enc:$/ {194 sub(/:$/, "", $2); print $2195 }' <<<"$registry" | sort -u196)197eligible_pairs=()198for node in "${render_nodes[@]}"; do199 for factory in "${va_factories[@]}"; do200 bounded_encoder_preflight "$node" "$factory" || continue201 eligible_pairs+=("$node $factory")202 done203done204printf 'eligible CBR GstVA pair: %s\n' "${eligible_pairs[@]}"205if test -n "${RENDER_NODE:-}" || test -n "${VA_ENCODER:-}"; then206 test -n "${RENDER_NODE:-}" && test -n "${VA_ENCODER:-}"207 selected_pair="$RENDER_NODE $VA_ENCODER"208 printf '%s\n' "${eligible_pairs[@]}" | grep -Fx "$selected_pair"209else210 test "${#eligible_pairs[@]}" -eq 1211 selected_pair="${eligible_pairs[0]}"212fi213read -r render_node va_encoder <<<"$selected_pair"214215printf 'export SIGIL_RENDER_NODE=%q\n' "$render_node"216printf 'export SIGIL_VA_ENCODER=%q\n' "$va_encoder"217218rm -rf -- "$inspection_dir"219trap - EXIT INT TERM HUP220```221222Keep the two printed export commands for the next step. If automatic selection223fails, inspect every printed node and factory and rerun with an exact pair, for224example `RENDER_NODE=/dev/dri/renderD129 VA_ENCODER=varenderD129h264enc`.225Never substitute a software encoder just to make preflight pass.226227## 4. Restart the package-owned audio sink228229The package already linked its PipeWire drop-in into the gaming user's config.230Restart only the PulseAudio-compatible PipeWire service during provisioning,231then select the persistent headless sink:232233```bash234set -euo pipefail235236test -f "$HOME/.config/pipewire/pipewire-pulse.conf.d/50-sigil-spark-audio.conf"237systemctl --user restart pipewire-pulse.service238audio_sink_id="$(wpctl status -n | awk '239 $0 ~ /sigil_spark/ {240 for (field = 1; field <= NF; field++) {241 if ($field ~ /^[0-9]+\.$/) {242 gsub("\\.", "", $field); print $field; exit243 }244 }245 }246')"247test "$audio_sink_id" -gt 0248wpctl set-default "$audio_sink_id"249pactl get-default-sink | grep -Fx sigil_spark250```251252This restart can interrupt existing local playback. Perform it before starting253a game.254255## 5. Write and validate the hardware configuration256257First run the two `export` commands printed by step 3 in this shell. Width and258height are intentionally omitted so Sigil follows the bounded native Gamescope259mode instead of assuming one panel resolution.260261```bash262set -euo pipefail263: "${SIGIL_RENDER_NODE:?run the render-node exports from step 3}"264: "${SIGIL_VA_ENCODER:?run the encoder exports from step 3}"265266sigil="$HOME/.local/libexec/sigil-spark/current/sigil"267uinput_gid="$(getent group sigil-uinput | cut -d: -f3)"268config="$HOME/.config/sigil-spark/host.toml"269ffmpeg_path="$(command -v ffmpeg)"270pw_dump_path="$(command -v pw-dump)"271gst_launch_path="$(command -v gst-launch-1.0)"272gst_inspect_path="$(command -v gst-inspect-1.0)"273test -n "$uinput_gid"274275umask 077276set +e277set -o noclobber278exec 3>"$config"279create_status=$?280set +o noclobber281set -e282if test "$create_status" -ne 0; then283 echo "refusing to replace existing host configuration: $config" >&2284 exit 1285fi286cat >&3 <<EOF287identity_path = "$HOME/.local/share/sigil-spark/identity/host.key"288state_path = "$HOME/.local/state/sigil-spark/runtime"289source = "gamescope-pipewire"290framerate = 60291codec = "h264"292input_mode = "uinput"293ffmpeg_path = "$ffmpeg_path"294295[uinput]296device_path = "/dev/uinput"297expected_owner_uid = 0298expected_group_gid = $uinput_gid299expected_mode = 0o660300301[gamescope_pipewire]302node_name = "gamescope"303media_class = "Video/Source"304xwayland_display = ":0"305pw_dump_path = "$pw_dump_path"306gst_launch_path = "$gst_launch_path"307gst_inspect_path = "$gst_inspect_path"308encoder_backend = "external-gst-launch"309vaapi_encoder = "$SIGIL_VA_ENCODER"310vaapi_render_node = "$SIGIL_RENDER_NODE"311rate_control = "cbr"312bitrate_kbps = 12000313314[audio]315node_name = "sigil_spark"316media_class = "Audio/Sink"317bitrate_bps = 96000318EOF319exec 3>&-320test "$(stat -Lc '%a:%U' "$config")" = "600:$(id -un)"321322"$sigil" config check --config "$config"323"$sigil" capture probe \324 --source gamescope-pipewire --config "$config" --frames 300325```326327Both commands must exit zero. The capture probe must report 300 encoded frames,328zero post-encode drops, and the actual resolved dimensions. Do not enable the329service after a failed preflight or by weakening the device/factory checks.330331## 6. Start, inspect, and only then enable Sigil332333```bash334set -euo pipefail335336systemctl --user daemon-reload337systemctl --user start sigil-host.service338systemctl --user is-active --quiet sigil-host.service339invocation="$(systemctl --user show -p InvocationID --value sigil-host.service)"340test -n "$invocation"341ready=false342for _attempt in $(seq 1 60); do343 if journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" \344 _SYSTEMD_USER_UNIT=sigil-host.service --no-pager -o cat \345 | grep -Fxq status=ready346 then347 ready=true348 break349 fi350 sleep 1351done352test "$ready" = true353systemctl --user status sigil-host.service --no-pager354journalctl --user _SYSTEMD_INVOCATION_ID="$invocation" --no-pager -o cat355356sigil="$HOME/.local/libexec/sigil-spark/current/sigil"357"$sigil" appliance status \358 --config "$HOME/.config/sigil-spark/host.toml" --json359360systemctl --user enable sigil-host.service361```362363The exact service invocation must report `status=ready` before enablement. The364unit runs in the gaming user's ordinary user manager and retries safely if its365configured Gamescope or PipeWire source is not ready yet; no root daemon or366interactive desktop capture portal is involved.367368Create a one-time, short-lived invitation only after Portal shows its peer ID:369370```bash371"$HOME/.local/libexec/sigil-spark/current/sigil" invitation create \372 --config "$HOME/.config/sigil-spark/host.toml" \373 --peer '<PORTAL_IROH_PEER_ID>' \374 --expires-in-seconds 900 --pointer-keyboard --gamepad \375 --output "$HOME/sigil.goq-invite"376```377378Move that owner-only file to Portal and confirm the displayed host and grants.379After the one-time enrollment, ordinary startup remains **PIN -> tap -> play**.