pyrowave-sys's build.rs runs cmake to build the vendored PyroWave C library,
but cmake wasn't in the distrobox apt list — so `cargo build -p punktfunk-host`
dies on every SteamOS host with "is cmake not installed?". Add it.
Also move sudo acquisition from step 4 (after the ~15-min build) into preflight
and keep the timestamp warm across the build. Before, a non-interactive run
(`ssh host 'bash install.sh'`) silently skipped the UDP-buffer/udev/vhci-hcd/
input-group tuning AND enable-linger — a "successful" host with no gamepad
passthrough, lossy streaming, and no headless persistence (dies on logout).
Now sudo is prompted once up front (authorize and walk away), and a no-TTY /
no-password run fails loudly before the build instead of silently at the end.
Validated end-to-end on SteamOS 3.8 in a VM with Radeon 780M passthrough:
full host + web build, all planes listening, VAAPI render node, headless
auto-start via linger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The system-tuning block (UDP buffers, gamepad udev rule, vhci-hcd autoload,
input group) was gated on `sudo -n true` — passwordless sudo — so on a stock
SteamOS box (the deck account needs a password) the whole block was silently
skipped: /dev/uhid stayed root-only so the gamepad degraded to an Xbox 360 pad,
and the UDP buffers stayed at the 416 KB default. Video was unaffected because
Game Mode/gamescope needs none of it, which masked the skip on-glass.
- install.sh + update.sh: acquire sudo interactively (`sudo -v` prompt on a TTY)
instead of requiring passwordless; skip only when there's no TTY or auth fails,
and then print the exact manual block plus a `passwd` hint — a stock SteamOS
'deck' account has no password, so sudo can't work until one is set.
- update.sh: also retrofit the UDP-buffer sysctl for older/skipped installs, and
loudly nag to reboot when the input group was just added (a --user restart does
not pick up the new group; only a fresh login does).
- steamos-host.md: note this step prompts for the sudo password.
Root is unavoidable: the udev rule, input group, and vhci-hcd module are all
kernel operations with no user-space alternative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fresh SteamOS host install failed on-glass two ways: KWin denied Desktop-mode
capture (zkde_screencast_unstable_v1) so every session died in the pipeline build,
and the native Steam Deck pad degraded to an Xbox 360 controller. Both stem from
setup that only a fresh login applies — the host user-service keeps the
pre-install group set and KWin reads its .desktop grant at session start — plus a
missing vhci-hcd autoload the pad's USB/IP transport needs (the deb/rpm/arch
packages ship it; the Deck installer did not).
- install.sh: install punktfunk-modules.conf (+ modprobe vhci-hcd now) for the
native Deck controller transport, seed the KDE RemoteDesktop grant
(kde-authorized) for Desktop-mode input, and print a loud "reboot before
streaming" notice when a relogin is actually required (input group added or the
.desktop grant first installed).
- update.sh: retrofit the udev rule, vhci-hcd autoload, input group, and grant so
existing installs pick them up on the next update without a reinstall.
- steamos-host.md: document the first-install reboot and the native controller
passthrough requirements (input group + vhci-hcd + client-side Steam Input Off).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The SteamOS scripts built the host featureless, so PlatformAuto could never
pick the Vulkan Video backend (default-on since 84329205) and every session
silently ran libav VAAPI — unlike the deb/arch/rpm/sysext builds, which all
pass the feature. Three gaps, found live on a Deck host:
- install.sh/update.sh: cargo build with --features punktfunk-host/vulkan-encode
(matches the packaged builds; pure-Rust ash, no new system dep).
- host.env: RADV_PERFTEST=video_encode — Van Gogh RADV still gates
VK_KHR_video_encode_* behind it, so without it the backend can't open and
falls back to VAAPI. Harmless where encode is exposed by default.
- io.unom.Punktfunk.Host.desktop (Exec rewritten to this install's binary):
KWin only grants zkde_screencast_unstable_v1/org_kde_kwin_fake_input to an
exe a .desktop authorizes — without it Desktop-Mode capture fails outright
and a mid-stream Game↔Desktop switch strands the stream on the old backend.
update.sh retrofits the last two idempotently so existing installs get them
without a reinstall.
Validated on a SteamOS 3.8.14 Deck (Van Gogh): Vulkan HEVC opened on both the
KWin and gamescope paths at 5120x1440 — in-place ABR reconfigure (no IDR, no
encoder rebuild) vs the VAAPI path's full teardown per ABR step, and Desktop
capture came up without a re-login.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Six parallel audits swept the root docs, docs-site, every per-directory
README, and the packaging docs; every claim below was verified against
the source before editing.
- README: Layout gains the six missing crates (pf-client-core,
pf-presenter, pf-console-ui, pf-ffvk, pf-driver-proto, punktfunk-tray),
clients/session, api/ and ci/; Linux/Windows client rows reflect the
shell + Vulkan-session split and the Vulkan Video -> VAAPI/D3D11VA ->
software decode chains; the "every client over a C ABI" claim is
corrected (Rust clients link the core directly); tiered stats overlay
+ console shell noted; Apple row mentions AV1.
- CONTRIBUTING: drop the dead CLAUDE.md link (deliberately untracked);
point at the README's build/invariants sections. SECURITY: 0.9.0.
- host-cli/pairing: --allow-pairing/--require-pairing are no-op legacy
names — pairing is required by default, --allow-tofu is the real flag;
document --data-port and --idle-timeout-ms.
- configuration: document PUNKTFUNK_RECOVER_SESSION_CMD (session-crash
recovery hook), PUNKTFUNK_MDNS, PUNKTFUNK_DATA_PORT.
- virtual-displays/gnome: GNOME per-client scaling shipped (host-
persisted) — flip the ❌ to ✅ and describe how it works.
- stats: new "Detail levels" section (Off/Compact/Normal/Detailed +
per-platform cycle gestures); retire the GTK hand-off note.
- clients/install-client/status/roadmap: decode chains, Windows client
validation narrowed to HDR-only pending, adaptive bitrate, console
shell, Apple AV1, Windows host vendor list.
- Sub-READMEs: clients/linux rewritten for the re-architecture; session
Windows decode rung + d3d11va knob; Windows tiered overlay; Android
minSdk 28; decky file table; host zerocopy/ path; scripts port
47992 and steamos-host.md; pf-dualsense source path.
- packaging: canary version bases are tag-derived (<next-minor> via
pf-version.sh/.ps1), codecs-extra not ffmpeg-full, document the
pinned offline-Skia tarball + SKIA_BINARIES_URL and vulkan-headers.
- Convert 15 dangling design/*.md links to the punktfunk-planning
prose convention (those docs live in the private planning repo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remediates the two web-console residuals from the 2026-07-05 posture audit:
- Brute-force throttle (loginThrottle.ts): per-IP exponential backoff
after 5 free attempts, plus a global floor for spread-out floods, keyed
on the socket peer IP (not spoofable X-Forwarded-For) with a size-capped
map. The constant-time compare already stopped the timing leak; this
bounds guess *volume* against a by-design LAN-exposed console.
- Session seal key now derives from the high-entropy mgmt token instead of
the low-entropy login password, so a captured cookie is no longer an
offline password oracle. Falls back to the password only when no token
is configured (dev/local). Rotating the token now invalidates sessions.
- Replace the process-wide NODE_TLS_REJECT_UNAUTHORIZED=0 with per-request
Bun TLS scoped to the loopback proxy hop; a non-loopback mgmt URL now
verifies normally. Dropped the env var from the systemd unit, Steam Deck
installer, Windows run scripts, env examples, and web README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Port 3000 collides with half the dev-server ecosystem; 47992 sits next
to the mgmt API (47990) in the punktfunk port family. Updates the run
scripts, systemd/scheduled-task units, Dockerfile, Windows firewall
rule + installer, packaging, and every doc that referenced :3000.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a client-selectable **preferred codec** and wires the core + ABI + probe + Linux client to
negotiate and decode it. (Windows/Apple/Android follow in 2b.)
**Core:**
- `Hello.preferred_codec` (a single CODEC_* bit, 0 = auto) — a soft hint appended after
`video_codecs`. `resolve_codec(client, host, preferred)` now honors the preference when the host
can also emit it, else falls back to precedence (HEVC > AV1 > H.264). Roundtrip + preference tests.
- `NativeClient::connect` takes `video_codecs` + `preferred_codec`; `NativeClient.codec` exposes the
resolved `Welcome.codec`.
- ABI: `punktfunk_connect_ex7` (adds the two codec params; `ex6` delegates to it advertising
HEVC-only) + `punktfunk_connection_codec` getter + `PUNKTFUNK_CODEC_{H264,HEVC,AV1}` constants
(drift-guarded against the wire values). Header regenerated.
**Host:** passes `hello.preferred_codec` into `resolve_codec`.
**probe:** `--codec h264|hevc|av1|auto` sets the preference (still advertises it can decode all
three); the dump extension already follows the resolved codec.
**Linux client:** advertises the codecs FFmpeg can actually decode (`decodable_codecs()`), threads
the user's `codec` setting as the preference, and builds the decoder — both the software and VAAPI
paths, plus the mid-session VAAPI→software demotion — from the negotiated `Welcome.codec` instead of
hardcoding HEVC. New "Video codec" dropdown in Preferences (Automatic/HEVC/H.264/AV1).
Live-validated on the dev box: probe `--codec hevc` against a software (H.264-only) host resolves to
H.264 (graceful soft-preference fallback), no failure. clippy + core (57) + host (133) tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Web console
- Pairing/Library/Stats refactored into self-contained subsections that each own
their own queries + mutations; a shared slot-based layout (view.tsx) is filled by
the live page (containers) and Storybook (pure cards + fixtures) so the layout can't
drift.
- All paired devices in one list on Pairing with a protocol column (punktfunk/1 +
Moonlight), routing each unpair to the right endpoint; the redundant Clients page is
removed.
- Library: overview grid split from the add/edit form into separate files.
- Login screen links out to the docs.
Docs
- "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows)
plus a new "Forgot your Password?" troubleshooting page, linked from the login screen.
- Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the
Bun entry, launchers, systemd units, and packaging.
Tooling
- Biome now respects .gitignore (stops linting generated code), config migrated to
2.5.1; all lint issues fixed cleanly.
Also includes this branch's in-progress host, Apple client, packaging, and CI changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the GameStream-opt-in posture (0de648f) on the SteamOS path: the installer keeps
Moonlight compat on by default (`serve --gamestream`, the Deck commonly streams to Moonlight),
but `--no-gamestream` now installs a secure native-only host with no GameStream on-path surface
(plain-HTTP pairing / legacy GCM nonce reuse — security-review #5/#9; native clients only).
Documented in the installer --help; the SteamOS host doc references it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follows the security audit (#5/#9): the GameStream-compat plane carries inherent on-path weaknesses
that can't be fixed on the wire without breaking stock Moonlight — its pairing runs over plain HTTP
(#9, MITM-able during the pairing window) and its legacy control encryption can reuse GCM nonces (#5,
a passive eavesdropper can recover/forge input). The native punktfunk/1 plane (SPAKE2 PIN pairing +
per-direction AEAD nonces) has neither. So flip the default to secure-by-default:
- `serve` → native punktfunk/1 plane + management API ONLY (no GameStream surface).
- `serve --gamestream` → ALSO the GameStream/Moonlight-compat planes (nvhttp pairing, RTSP, ENet
control, _nvstream mDNS). Opt-in, logged with a trusted-LAN caveat. `--moonlight` is an alias.
- The native plane is now ALWAYS on in `serve` (`--native` is a kept-for-compat no-op); the unified
GameStream+native host is `serve --gamestream`.
`gamestream::serve` gates the GameStream spawns (nvhttp/rtsp/control/mdns) on the flag; the native
plane + mgmt + native-pairing handle always run.
To avoid silently regressing validated Moonlight deployments, the explicit deployment configs PRESERVE
Moonlight via `--gamestream` (each documents dropping it for a secure native-only host): the Linux
systemd unit, the Steam Deck installer, and the Windows service default (DEFAULT_HOST_CMD). The bare
`serve` default (new/manual use) is secure.
Docs swept to match (host-cli, moonlight, quickstart, install, packaging READMEs, CLAUDE.md, README,
…): Moonlight setup now instructs `--gamestream`; native/console refs use bare `serve`. OpenAPI
regenerated (a stale "run `serve --native`" string). fmt + clippy clean; 94 host tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The management console is a Nitro `node-server` build (per web/vite.config.ts) — it must be
run with `node`, not `bun`. Run under bun it 500s on every page render with
"Cannot find package 'srvx'": bun mis-resolves Nitro's externalized server deps from the
nested SSR chunk at request time. (This was pre-existing — the old manual pfweb.sh ran it
with bun too.)
- Provision `nodejs` in the pf2 distrobox; run the web service with `node .output/server/index.mjs`.
- Use `enable` + `restart` (not `enable --now`) so re-running the installer actually applies
unit-file changes instead of no-opping against the running service.
Verified on the Deck: web `/login` now returns 200 (was 500), "Listening on http://0.0.0.0:3000",
no srvx error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SteamOS is immutable read-only Arch, and the Deck is AMD (VAAPI) — so none of the
checked-in packaging (arch/sysext is NVENC-first + client-oriented, deb/rpm are
soname-mismatched) actually installs a working host on a Steam Deck. The proven path
(distrobox-built native binary + systemd-run units) was 100% manual. Make it one command.
- scripts/steamdeck/install.sh — idempotent installer: ensure the pf2 Debian-trixie
distrobox + toolchain → build host (+web console) → write config (generated web login
password) → raise UDP buffers to 32 MB + udev + input group (sudo, skipped gracefully
if unavailable) → install + start punktfunk-host / punktfunk-web systemd USER services
with linger. Flags: --open (accept unpaired clients), --no-web, --src=DIR. Builds
on-device so a rebuild always matches the running SteamOS (no prebuilt-binary fragility
across OS updates); VAAPI on the Deck's AMD GPU.
- scripts/steamdeck/update.sh — rebuild from current source + restart (config/pairings persist).
- scripts/steamdeck/README.md — deep reference (why on-device, what's installed, gotchas).
- docs-site: new "Steam Deck (Host)" guide + sidebar entry; install.md splits Arch from the
Steam Deck host path; packaging/arch/README points Deck-host users here and corrects the
stale "NVENC-only" note (VAAPI host encode landed).
Live-validated on the Deck: installer runs clean, both services come up, host listens
(QUIC :9777 + mgmt :47990), web serves (302→login); on a client connect it takes over the
Game-Mode gamescope session at the client's mode, captures via PipeWire, and VAAPI-encodes
(hevc_vaapi) — full pipeline confirmed in the host journal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>