Nobara's gamescope-session-plus HARDCODES an absolute gamescope path and never reads GAMESCOPE_BIN — verified on the box: `grep GAMESCOPE_BIN` over the script returns nothing, and line 244 opens `GAMESCOPECMD="/usr/bin/gamescope \`. So neither the wrapper nor a PATH shim can reach it, and the managed session comes up as stock gamescope with none of our flags. `verify_managed_spawn_flags` catches that and refuses HDR and the in-node cursor rather than streaming a session planned around flags that never arrived — correct, but it leaves every Nobara-family box with no composited cursor and no HDR, and the operator's only recourse is overwriting a distro-owned binary. Bind it instead, inside the session unit's own mount namespace: the session gets our gamescope, and nothing outside the unit changes — the distro still owns the file on disk. ⚠ The bind source is the WRAPPER, not the patched binary. Binding the binary would have been a vacuous fix: the flags this whole mechanism exists to deliver are injected BY the wrapper, so a bound binary arrives with no flags and `verify_managed_spawn_flags` refuses exactly as before. Binding the wrapper reproduces what GAMESCOPE_BIN would have done had the script consulted it. Applied only where the escape hatch is structurally absent, decided by READING the script rather than by distro name: if `GAMESCOPE_BIN` appears anywhere in it, we keep the mechanism that already works and take no mount namespace we don't need. Parsing is pure and unit-tested — the `GAMESCOPECMD+=` appends and the `[ -z "$GAMESCOPECMD" ]` test must not be mistaken for the opening assignment, or the bind would land over `$socket`. Three further refusals: the target must be a file; the resolved source must be absolute and NOT the target (a fork-bomb guard — the wrapper execs the real binary, so shadowing that same path would exec itself forever); and the preflight must pass. Availability is PROVEN, not inferred, with a throwaway `systemd-run --user --wait --collect --property=BindReadOnlyPaths=<the real value> -- /bin/true`. That matters: on .25 the bind fails with `status=226/NAMESPACE` while `kernel.unprivileged_userns_clone=1` and `max_user_namespaces=29006` are both permissive — AppArmor's `apparmor_restrict_unprivileged_userns` blocks it. A sysctl check would have said "available" and armed a bind that cannot work. The payload is /bin/true, so the preflight can never accidentally start a compositor. Runtime backstop for anything the preflight cannot see: `ExecMainStatus == 226` disarms the bind and relaunches plain, latching one-way per process. The transient host-owned unit takes the setting via `systemd-run --property=`, which is atomic with the start and leaves nothing behind if the host dies. The box's own unit needs a drop-in, so it gets `zz-punktfunk-bind.conf` on the TEMPLATE, sorted last so nothing can take the setting back out; removal targets that one filename only — never the directory, never a glob — because the operator's `10-headless.conf` next to it is what lets game mode start on that box at all.
punktfunk-docs
The Punktfunk documentation site: Fumadocs on TanStack Start (Vite + Nitro/bun preset).
Content lives in content/docs/ as .md/.mdx. This site is the source of truth
for the user-facing guides; design rationale lives in the internal punktfunk-planning repo.
API reference
/api renders the host's management REST API as an interactive
Scalar reference (linked from the top nav, the docs
sidebar, and the landing page). It reads public/openapi.json — a
snapshot of the repo's generated spec. Refresh it after a management-API change:
# from the repo root — regenerate the spec, then copy the snapshot in:
cargo run -p punktfunk-host -- openapi > api/openapi.json
cp api/openapi.json docs-site/public/openapi.json
Nothing in CI diffs the two, so the snapshot goes stale silently — that manual cp is the only
thing keeping them in sync. Before publishing docs, check that they match:
diff <(jq -S . api/openapi.json) <(jq -S . docs-site/public/openapi.json)
That should print nothing. Right now it doesn't: the committed snapshot predates the
/api/v1/update/check, /api/v1/update/apply and /api/v1/update/status endpoints, so the
published /api reference is missing the host self-update surface — re-copy it.
Develop
bun install
bun run dev # http://localhost:3001 (docs at /docs)
CI gates every change on bun run build followed by bun run lint (the TypeScript typecheck), in
that order — the build emits the .source typegen the typecheck imports. Run both before you push.
Build & serve
bun run build
bun run start # serves .output/ via Bun
Layout
source.config.ts Fumadocs MDX collection (content/docs)
content/docs/ the docs content (.md/.mdx) + meta.json nav
src/
routes/
__root.tsx RootProvider + html shell
index.tsx landing page
docs/$.tsx catch-all docs renderer (Fumadocs DocsLayout)
api/index.tsx Scalar API reference (reads public/openapi.json)
api/search.ts Orama search endpoint
lib/source.ts Fumadocs loader over the generated collection
lib/layout.shared.tsx shared nav chrome
components/mdx.tsx MDX component map
styles/app.css Tailwind 4 + Fumadocs preset