NixOS could not detect a graphical session at all — and the module was never checked by CI #158

Merged
enricobuehler merged 5 commits from worktree-nixos-wrapped-comm-detection into main 2026-08-10 18:37:36 +00:00
Owner

A field report ("NixOS with KDE is broken — no usable compositor") turned out to be a one-line string comparison, and pulling on it surfaced a packaging sweep's worth of adjacent problems.

The bug

The live-session probe matched /proc/<pid>/comm exactly against kwin_wayland / gamescope / gnome-shell / Hyprland. comm is the kernel's name for the executed file, truncated to 15 bytes — not argv[0]. nixpkgs wraps essentially every graphical binary: wrapProgram moves the real ELF aside to .<name>-wrapped and installs a wrapper that exec -a "$0"s it. So the kernel reports .kwin_wayland-w while ps and pgrep -a show a perfectly ordinary kwin_wayland, because they read argv.

Reproduced against a live kernel, installing binaries exactly the way nixpkgs does:

on-disk file comm argv[0]
.kwin_wayland-wrapped .kwin_wayland-w kwin_wayland
.kwin_wayland_wrapper-wrapped .kwin_wayland_w kwin_wayland_wrapper
.gamescope-wrapped .gamescope-wrap gamescope
unwrapped kwin_wayland kwin_wayland — (control)

Nothing downstream could recover from that one comparison: wayland logged as -, a correct WAYLAND_DISPLAY changed nothing, Auto returns the detected backend so a live KWin already in available() was never chosen, and a PUNKTFUNK_COMPOSITOR pin turned the miss into a hard error via pinned_at_a_dead_session. It was never KDE-only — gamescope, gnome-shell and Hyprland are all wrapped, so Gaming and every desktop kind were equally blind. sway and river survived by accident (nixpkgs' wrapper execs a binary still called sway).

Fix: resolve the name through /proc/<pid>/exe, which is untruncated, and strip the nixpkgs decoration. Stripping requires both the leading . and a trailing -wrapped, so KWin's own real kwin_wayland_wrapper binary keeps its name rather than collapsing into kwin_wayland and handing the probe the parent's PID. The comm fast path is unchanged for every ordinary distro — one read, no readlink — and no name that matched before can stop matching.

The CI gap

nix flake check does not check nixosModules. It forces the value and asserts it is a lambda taking an open attribute set, then stops — nix's source still carries // FIXME: if we have a 'nixpkgs' input, use it to check the module. Measured with a throwaway flake whose module sets a nonexistent option, references a nonexistent pkgs attribute and calls a nonexistent lib function:

checking NixOS module 'nixosModules.default'... all checks passed!
exit code = 0

nix.yml's header claimed that leg covered the module. It never had, for the module's whole life — on a flake whose history is Nix regressions reaching main invisibly.

checks.<system>.nixos-module (packaging/nix/module-check.nix) closes it: it evaluates the module against real nixpkgs across four scenarios and asserts on the rendered systemd units. The assertions are pure Nix, so instantiating the check runs them — meaning the eval-only --no-build leg CI already runs is sufficient, and no Rust is built. Stub fake-derivation packages keep it independent of punktfunk-host/-client and the from-source gamescope.

Mutation-tested: 8 mutants, each re-introducing one real defect, all 8 rejected, baseline green. The suite earned it twice during development — its first run failed a correct module because systemd renders After= as one space-separated line (those assertions now read the evaluated lists), and the wiring test caught that a flake input nixpkgs is an attrset, not a path.

Divergences from the shipped units

Three decisions that were made, documented and deliberate everywhere else, and simply never ported to the module:

  • punktfunk-web was missing StartLimitIntervalSec=0. Its mgmt-token EnvironmentFile is mandatory on purpose, so the unit genuinely fails until the host's first serve. systemd's default 5-starts-per-10 s against RestartSec=2 gives up permanently after ~10 s — exactly the appliance cold-boot window, so a console enabled before the host's first run stayed dead until someone restarted it by hand.
  • Restart=on-failurealways. A console that exits 0 has still stopped serving.
  • punktfunk-scripting had no sandbox at all. The shipped unit confines it with NoNewPrivileges, ProtectSystem=strict, ReadWritePaths=%h /tmp, restricted address families and PrivateTmp=no. The one unit that runs arbitrary operator TypeScript by design was strictly less confined on NixOS than on deb/rpm.

Other sweep findings, closed

  • host.desktopSession (new, default false) — binds the host to graphical-session.target, the declarative form of the punktfunk-host-desktop-session.conf drop-in. Without it a Plasma/GNOME restart leaves the host holding a Wayland socket and portal D-Bus connection that died with the old compositor: still listening, still answering, failing at capture on every session after. Off by default because an appliance may never reach that target and would be left permanently stopped.
  • scripting.autoStart now defaults ON (behaviour change), matching the deb postinst and RPM %post which both systemctl --global enable it, and the sysext's baked-in symlink. It was opt-in here on reasoning that stopped being true when the game-library scanners became plugins — a NixOS host came up with an empty library and no obvious cause.
  • A warning when the host is enabled and xdg.portal.enable is not — a warning rather than mkDefault true, because portals with no extraPortals backend is its own broken state and only the operator knows which backend they need.
  • build-gamescope dispatch input: gamescopeHdr defaults true, so the patched gamescope is on the critical path of every host build, yet nothing compiled it — and it tracks nixpkgs' gamescope, so a flake.lock bump is what breaks it.

Docs

Nix is documented in four places and all had drifted. The one worth calling out isn't Nix-specific: install.md told everyone the plugin runner "ships disabled". That is true only of Arch and source installs — deb and RPM --global enable it, and the Bazzite sysext bakes a default.target.wants symlink (build-sysext.sh:113). bazzite.md repeated the same wrong claim about its own image. Both corrected per channel.

Also: running-as-a-service.md documented the desktop-session drop-in for packaged installs only and now names desktopSession beside it; packaging/nix/README.md gained the missing options, the punktfunk group, the security.wrappers entry with the KWin-identification reasoning, and the flake-check caveat; packaging/README.md and a CHANGELOG.md v0.27.0 section.

Verification

  • scripts/xcheck.sh linux clippy and windows check clean at -D warnings with --all-targets (so the new tests compile), cargo fmt --check clean — re-run after merging current main, since #156 rewrote 197 lines of the same gamescope.rs.
  • match_name and undecorate extracted verbatim (diff-proven) and executed on Linux: 14/14 logic rows, 10/10 /proc fixture rows, 0 failures.
  • Module rendered against the pinned nixpkgs: assertions clean, cap_sys_nice=ep on the worker wrapper, firewall 47984/47989/47990/47992/47993/48010.
  • All .nix files formatted with the flake's declared formatter, from the pinned nixpkgs rather than a channel's.

Not run locally: the full cargo test -p pf-vdisplay (an emulated amd64 link risked an ENOSPC on a volume down to ~7 GiB) and a full nix flake check --no-build (it fetches crane, rust-overlay and bun2nix). I verified the exact pkgs.callPackage path the flake uses instead, producing an identical derivation hash. CI covers both.

Tests are fixture-driven rather than spawn-driven on purpose: modern coreutils is a multi-call binary that dispatches on the executable's own name, so a copy called .kwin_wayland-wrapped exits instantly and /proc/<pid>/exe is gone before it can be read — which reads exactly like the resolver being broken, and cost one debugging round here.

A field report ("NixOS with KDE is broken — no usable compositor") turned out to be a one-line string comparison, and pulling on it surfaced a packaging sweep's worth of adjacent problems. ## The bug The live-session probe matched `/proc/<pid>/comm` exactly against `kwin_wayland` / `gamescope` / `gnome-shell` / `Hyprland`. `comm` is the kernel's name for the **executed file**, truncated to 15 bytes — not `argv[0]`. nixpkgs wraps essentially every graphical binary: `wrapProgram` moves the real ELF aside to `.<name>-wrapped` and installs a wrapper that `exec -a "$0"`s it. So the kernel reports `.kwin_wayland-w` while `ps` and `pgrep -a` show a perfectly ordinary `kwin_wayland`, because they read argv. Reproduced against a live kernel, installing binaries exactly the way nixpkgs does: | on-disk file | `comm` | `argv[0]` | | --- | --- | --- | | `.kwin_wayland-wrapped` | `.kwin_wayland-w` | `kwin_wayland` | | `.kwin_wayland_wrapper-wrapped` | `.kwin_wayland_w` | `kwin_wayland_wrapper` | | `.gamescope-wrapped` | `.gamescope-wrap` | `gamescope` | | unwrapped `kwin_wayland` | `kwin_wayland` | — (control) | Nothing downstream could recover from that one comparison: `wayland` logged as `-`, a correct `WAYLAND_DISPLAY` changed nothing, `Auto` returns the *detected* backend so a live KWin already in `available()` was never chosen, and a `PUNKTFUNK_COMPOSITOR` pin turned the miss into a hard error via `pinned_at_a_dead_session`. It was never KDE-only — gamescope, gnome-shell and Hyprland are all wrapped, so `Gaming` and every desktop kind were equally blind. sway and river survived by accident (nixpkgs' wrapper execs a binary still called `sway`). **Fix:** resolve the name through `/proc/<pid>/exe`, which is untruncated, and strip the nixpkgs decoration. Stripping requires **both** the leading `.` and a trailing `-wrapped`, so KWin's own real `kwin_wayland_wrapper` binary keeps its name rather than collapsing into `kwin_wayland` and handing the probe the parent's PID. The `comm` fast path is unchanged for every ordinary distro — one read, no readlink — and **no name that matched before can stop matching**. ## The CI gap `nix flake check` does **not** check `nixosModules`. It forces the value and asserts it is a lambda taking an open attribute set, then stops — nix's source still carries `// FIXME: if we have a 'nixpkgs' input, use it to check the module.` Measured with a throwaway flake whose module sets a nonexistent *option*, references a nonexistent `pkgs` attribute **and** calls a nonexistent `lib` function: ``` checking NixOS module 'nixosModules.default'... all checks passed! exit code = 0 ``` `nix.yml`'s header claimed that leg covered the module. It never had, for the module's whole life — on a flake whose history is Nix regressions reaching main invisibly. `checks.<system>.nixos-module` (`packaging/nix/module-check.nix`) closes it: it evaluates the module against real nixpkgs across four scenarios and asserts on the rendered systemd units. The assertions are **pure Nix**, so instantiating the check runs them — meaning the eval-only `--no-build` leg CI already runs is sufficient, and no Rust is built. Stub fake-derivation packages keep it independent of `punktfunk-host`/`-client` and the from-source gamescope. **Mutation-tested: 8 mutants, each re-introducing one real defect, all 8 rejected, baseline green.** The suite earned it twice during development — its first run failed a *correct* module because systemd renders `After=` as one space-separated line (those assertions now read the evaluated lists), and the wiring test caught that a flake input `nixpkgs` is an attrset, not a path. ## Divergences from the shipped units Three decisions that were made, documented and deliberate everywhere else, and simply never ported to the module: - **`punktfunk-web` was missing `StartLimitIntervalSec=0`.** Its mgmt-token `EnvironmentFile` is mandatory on purpose, so the unit genuinely fails until the host's first `serve`. systemd's default 5-starts-per-10 s against `RestartSec=2` gives up **permanently after ~10 s** — exactly the appliance cold-boot window, so a console enabled before the host's first run stayed dead until someone restarted it by hand. - **`Restart=on-failure` → `always`.** A console that exits 0 has still stopped serving. - **`punktfunk-scripting` had no sandbox at all.** The shipped unit confines it with `NoNewPrivileges`, `ProtectSystem=strict`, `ReadWritePaths=%h /tmp`, restricted address families and `PrivateTmp=no`. The one unit that runs arbitrary operator TypeScript by design was strictly *less* confined on NixOS than on deb/rpm. ## Other sweep findings, closed - **`host.desktopSession`** (new, default `false`) — binds the host to `graphical-session.target`, the declarative form of the `punktfunk-host-desktop-session.conf` drop-in. Without it a Plasma/GNOME restart leaves the host holding a Wayland socket and portal D-Bus connection that died with the old compositor: still listening, still answering, failing at capture on every session after. Off by default because an appliance may never reach that target and would be left permanently stopped. - ⚠ **`scripting.autoStart` now defaults ON** (behaviour change), matching the deb `postinst` and RPM `%post` which both `systemctl --global enable` it, and the sysext's baked-in symlink. It was opt-in here on reasoning that stopped being true when the game-library scanners became plugins — a NixOS host came up with an empty library and no obvious cause. - A **warning** when the host is enabled and `xdg.portal.enable` is not — a warning rather than `mkDefault true`, because portals with no `extraPortals` backend is its own broken state and only the operator knows which backend they need. - **`build-gamescope`** dispatch input: `gamescopeHdr` defaults true, so the patched gamescope is on the critical path of every host build, yet nothing compiled it — and it tracks nixpkgs' gamescope, so a `flake.lock` bump is what breaks it. ## Docs Nix is documented in four places and all had drifted. The one worth calling out isn't Nix-specific: `install.md` told everyone the plugin runner "ships **disabled**". That is true only of Arch and source installs — deb and RPM `--global enable` it, and the Bazzite sysext bakes a `default.target.wants` symlink (`build-sysext.sh:113`). `bazzite.md` repeated the same wrong claim about its own image. Both corrected per channel. Also: `running-as-a-service.md` documented the desktop-session drop-in for packaged installs only and now names `desktopSession` beside it; `packaging/nix/README.md` gained the missing options, the `punktfunk` group, the `security.wrappers` entry with the KWin-identification reasoning, and the flake-check caveat; `packaging/README.md` and a `CHANGELOG.md` v0.27.0 section. ## Verification - `scripts/xcheck.sh linux clippy` and `windows check` clean at `-D warnings` with `--all-targets` (so the new tests compile), `cargo fmt --check` clean — **re-run after merging current main**, since #156 rewrote 197 lines of the same `gamescope.rs`. - `match_name` and `undecorate` extracted **verbatim** (diff-proven) and executed on Linux: 14/14 logic rows, 10/10 `/proc` fixture rows, 0 failures. - Module rendered against the pinned nixpkgs: assertions clean, `cap_sys_nice=ep` on the worker wrapper, firewall `47984/47989/47990/47992/47993/48010`. - All `.nix` files formatted with the flake's declared formatter, from the **pinned** nixpkgs rather than a channel's. **Not run locally:** the full `cargo test -p pf-vdisplay` (an emulated amd64 link risked an ENOSPC on a volume down to ~7 GiB) and a full `nix flake check --no-build` (it fetches crane, rust-overlay and bun2nix). I verified the exact `pkgs.callPackage` path the flake uses instead, producing an identical derivation hash. CI covers both. Tests are fixture-driven rather than spawn-driven on purpose: modern coreutils is a multi-call binary that dispatches on the executable's own name, so a copy called `.kwin_wayland-wrapped` exits instantly and `/proc/<pid>/exe` is gone before it can be read — which reads exactly like the resolver being broken, and cost one debugging round here.
enricobuehler added 5 commits 2026-08-10 18:27:43 +00:00
The session probe decided "is a desktop live?" by reading /proc/<pid>/comm for
every process of our uid and exact-matching it against "kwin_wayland" /
"gamescope" / "gnome-shell" / "Hyprland". comm is the kernel's name for the
executed FILE, truncated to 15 bytes — not argv[0].

nixpkgs wraps essentially every graphical binary: wrapProgram moves the real
ELF aside to `.<name>-wrapped` and installs a wrapper under the original name,
which then `exec -a "$0"`s the hidden file. So on NixOS the kernel reports
`.kwin_wayland-w` (15 bytes of `.kwin_wayland-wrapped`) while ps/pgrep -a show
a perfectly ordinary `kwin_wayland`, because they read argv. Measured against a
live kernel: `.kwin_wayland-w`, `.kwin_wayland_w` (KWin's own
kwin_wayland_wrapper), `.gamescope-wrap`, all 15 bytes.

Nothing downstream could recover from that one string comparison:

  - detect_active_session returned ActiveKind::None on a *running* KDE desktop;
  - wayland_display is only resolved for a detected kind, so the connect log
    reported wayland="-" even though WAYLAND_DISPLAY was correct;
  - pick_compositor's Auto arm returns the DETECTED backend, so a live, fully
    working KWin sitting in available() was never chosen — every connect died
    "no usable compositor";
  - and PUNKTFUNK_COMPOSITOR could not rescue it: pinned_at_a_dead_session
    consults the same probe, turning the miss into a hard error instead.

No environment variable reached the comparison — the XDG_CURRENT_DESKTOP
fallback in detect() is only on the pinned path. Capture itself was never at
fault: a decoy process merely NAMED kwin_wayland satisfied the probe and the
stream came up against the real KWin.

Resolve the name through /proc/<pid>/exe (the full, untruncated file name) and
strip the nixpkgs decoration. Both the leading `.` and the trailing `-wrapped`
are required before anything is stripped, so KWin's own real
`kwin_wayland_wrapper` binary keeps its name rather than collapsing into
`kwin_wayland` and handing the probe the parent's PID. The comm fast path is
kept for every ordinary distro — one read, no readlink, and no name that
matched before can stop matching.

Also applied to foreign_gamescope_running, which had the same defect: nixpkgs
wraps gamescope too, so the attach-vs-spawn ladder saw no foreign session.

Tests are fixture-driven rather than spawn-driven on purpose: a stand-in has to
be a real ELF that tolerates being renamed, and /bin/sleep is not one — modern
coreutils is a multi-call binary that dispatches on the executable's own name,
so a copy called `.kwin_wayland-wrapped` exits instantly and /proc/<pid>/exe is
gone before it can be read. That failure looks exactly like this resolver being
broken; it cost one debugging round here and the same trap is already recorded
in punktfunk-host's /proc matcher.
A sweep of the Nix packaging against the units the deb/rpm actually install
found three decisions that were made, documented and deliberate everywhere
else, and simply not carried into packaging/nix/nixos-module.nix.

punktfunk-web — StartLimitIntervalSec=0.
  The unit's EnvironmentFile for the mgmt token is mandatory ON PURPOSE, so the
  console genuinely fails until the host's first `serve` writes it. systemd's
  default rate limit (5 starts / 10 s) against RestartSec=2 then gives up
  permanently after ~10 s — which on an appliance is exactly the window before
  the host is ready, so a console enabled before the host's first run stayed
  dead until someone restarted it by hand. scripts/punktfunk-web.service has
  carried the override since that defect was found; the Nix module omitted it
  while its own comment went on promising "Restart retries until the host has
  created it".

punktfunk-web — Restart=always, not on-failure.
  A console that exits 0 has still stopped serving, and on-failure leaves it
  down. Matches the shipped unit and web-run.cmd on Windows, both of which
  relaunch bun on ANY exit. An explicit `systemctl --user stop` is unaffected.

punktfunk-scripting — the sandbox was missing entirely.
  The shipped unit confines the runner with NoNewPrivileges, ProtectSystem=
  strict, ReadWritePaths=%h /tmp and an AF_UNIX/AF_INET/AF_INET6 address-family
  restriction, plus PrivateTmp=no (a field report: a private /tmp hides
  /tmp/vhclient and /tmp/.X11-unix, so a plugin launches its vendor binary and
  then cannot reach the daemon behind it). The NixOS unit had none of it — so
  the one unit here that executes arbitrary operator TypeScript by design ran
  strictly LESS confined on NixOS than on every other channel.

Verified by evaluating the module against the pinned nixpkgs and rendering the
units: assertions clean, cap_sys_nice=ep on the encode-worker wrapper, firewall
47984/47989/47990/47992/47993/48010, and each unit carrying exactly the
directives above. That evaluation is NOT something CI does — measured: `nix
flake check` passes a nixosModule containing a nonexistent option, a nonexistent
pkgs attribute and a nonexistent lib function, printing "checking NixOS module
... all checks passed!" while never evaluating it against nixpkgs. nix.yml's
header claims that leg covers the module. It does not; tracked separately.
THE CI GAP. `nix flake check` does not check `nixosModules`. It forces the value
and asserts it is a lambda taking an open attribute set — nothing more; nix's own
source carries `// FIXME: if we have a 'nixpkgs' input, use it to check the
module.` Measured: a flake whose module sets a nonexistent OPTION, references a
nonexistent `pkgs` attribute AND calls a nonexistent `lib` function passes clean,
printing `checking NixOS module 'nixosModules.default'... all checks passed!`.
nix.yml's header claimed that leg covered the module; it never did, for the
module's whole life — on a flake whose history is Nix regressions reaching main
invisibly.

Closed with `checks.<system>.nixos-module` (packaging/nix/module-check.nix): it
evaluates the module against real nixpkgs in four scenarios (desktop, appliance,
native-only, client-only) and asserts on the rendered systemd units. The
assertions are PURE NIX so instantiating the check runs them — which means the
eval-only `--no-build` leg CI already runs is sufficient, and no Rust is built.
Stub fake-derivation packages keep it independent of punktfunk-host/-client and
the from-source gamescope; crane and bun2nix are provably not needed (they are
`throw`s in the wiring test and it still instantiates).

17 checks, including regression guards for every divergence the sweep found and
for the KWin identification trap (host ExecStart must stay on the plain store
path, never a capability wrapper, while the encode worker points AT the wrapper).
Mutation-tested: 8 mutants, each re-introducing one real defect, all 8 rejected,
baseline green. The suite already earned it once — its first run failed a correct
module because systemd renders `After=` as one space-separated line, so those
assertions now read the evaluated lists instead of the text.

Also closed from the sweep:

  * services.punktfunk.host.desktopSession (new, default false) — binds the host
    to graphical-session.target, the declarative form of the
    punktfunk-host-desktop-session.conf drop-in. Without it a Plasma/GNOME
    restart leaves the host holding a Wayland socket and portal D-Bus connection
    that died with the old compositor: it still listens, still answers, and every
    session it then serves fails at capture. Off by default because an appliance
    may never reach that target and would be left permanently stopped.

  * scripting.autoStart now defaults ON, matching the deb postinst and RPM %post,
    which both `systemctl --global enable` the runner. It was opt-in here on the
    reasoning that the runner is inert until you add automation — which stopped
    being true when the game-library scanners became plugins. A NixOS host came up
    with an empty library and no obvious reason why. The module and README carried
    the superseded rationale verbatim; both updated.

  * A warning when the host is enabled and xdg.portal is not. A warning rather
    than `xdg.portal.enable = mkDefault true`, because enabling the portal service
    with no `extraPortals` backend is its own broken state and only the operator
    knows which backend their compositor needs.

  * punktfunk-gamescope gets a `build-gamescope` dispatch input. It is on the
    critical path of every host build (`gamescopeHdr` defaults true) yet nothing
    compiled it; it tracks nixpkgs' gamescope, so a flake.lock bump — not a change
    of ours — is what breaks it, and the first to find out would be an operator
    whose system rebuild fails.

All .nix files reformatted with the flake's own declared formatter
(nixfmt-rfc-style from the PINNED nixpkgs, not a channel's).
There are three places Nix is documented — the public docs-site, packaging/nix/
README.md, and packaging/README.md — plus the changelog. All had drifted.

STALE CLAIM, and not only for Nix. install.md said the plugin runner's "user unit
ships **disabled** — enable it once you have" something to run. That is true only
of Arch and source installs: the deb postinst and RPM %post both
`systemctl --global enable punktfunk-scripting.service`, and the Bazzite sysext
bakes in a default.target.wants symlink (build-sysext.sh:113). bazzite.md carried
the same claim about its own image. Both corrected, per channel, with the reason
the default flipped — the library scanners are plugins, so a host without the
runner can come up with an empty library — and the `mask`-not-`disable` opt-out
the sysext's own comment documents.

docs-site:
  * install.md NixOS — `desktopSession` in the example and explained, the runner
    no longer needs enabling, and the host/console line says what autoStart does.
  * running-as-a-service.md — "Restart the host with your desktop" documented the
    drop-in for packaged installs only; NixOS gets its one-liner beside it.
  * bazzite.md — the runner is started for you, not "isn't started".

packaging/nix/README.md:
  * option tables gain `desktopSession`, `gamescopeHdr`, `gamescopePackage`, and
    the `punktfunk` group next to `input` (both are required — the udev rule
    chgrp's the vhci nodes and fails outright if the group was never created).
  * "what the module configures" gains the security.wrappers entry, and a note on
    why the capability sits on the encode worker and never on the host: a wrapper
    raises it into the ambient set, which lands it in the permitted set and fails
    KWin's /proc/<pid>/exe readlink identically to a file capability.
  * the appliance snippet no longer tells you to put pkgs.gamescope on PATH —
    gamescopeHdr does that with the patched build, and desktopSession is called
    out as the thing to leave off there.
  * a caveat recording that `nix flake check` does not check the module, and the
    two rules for editing module-check.nix (assertions stay pure Nix; assert
    list-valued unit fields on the lists, not the rendered text).

packaging/README.md: the flake ships five packages, not "host + client".

CHANGELOG.md v0.27.0: a NixOS section covering the comm/session-detection fix, the
module changes including the scripting default flip as an explicit behaviour
change, and the flake-check gap — plus the documentation bullets above.
Merge remote-tracking branch 'origin/main' into worktree-nixos-wrapped-comm-detection
ci / bun-nix (pull_request) Successful in 40s
ci / web (pull_request) Successful in 1m19s
ci / docs-site (pull_request) Successful in 1m30s
ci / rust-arm64 (pull_request) Successful in 1m46s
android / android (pull_request) Successful in 5m22s
ci / rust (pull_request) Successful in 6m47s
apple / swift (pull_request) Failing after 13m41s
apple / screenshots (pull_request) Skipped
nix / flake (pull_request) Successful in 13m48s
61dfc3dadc
enricobuehler merged commit 30c47eb691 into main 2026-08-10 18:37:36 +00:00
enricobuehler deleted branch worktree-nixos-wrapped-comm-detection 2026-08-10 18:37:42 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#158