Files
Nomarchy/modules/nixos/hardware.nix
Bernardo Magri 7aae204014
Some checks failed
Check / eval (push) Failing after 2m45s
feat(fingerprint): password OR fingerprint in parallel at one prompt
Bernardo promoted the PROPOSED item live: with fingerprint PAM on,
sudo/login should accept whichever factor comes first instead of
pam_fprintd's wait-for-the-reader-then-password. Stock PAM cannot
express parallel factors (linux-pam#301), so this packages
pam-fprint-grosshack v0.3.0 (pkgs/, pinned from GitLab — the
field-standard fprintd fork), source-reviewed before packaging: every
failure path (no reader, no prints, fprintd absent/hung, timeout,
password typed) returns PAM_AUTHINFO_UNAVAIL and falls through; a
typed password is only ferried via PAM_AUTHTOK to the stock
`auth sufficient pam_unix.so … try_first_pass` rule — the module never
validates passwords itself, so it cannot lock out password login.

New option nomarchy.hardware.fingerprint.parallel, default TRUE (the
better UX is what opting into fingerprint PAM buys; false = stock
sequential). Wiring swaps the modulePath of stock fprintd's rule slot
(mkForce) so the sufficient-before-pam_unix ordering is inherited, not
recomputed. README + downstream template rows added.

Verified: V2 — checks.hardware-toggles extended to three nodes, green:
parallel node asserts the grosshack auth line precedes pam_unix in
/etc/pam.d/sudo and that with NO reader a correct password still
passes sudo while a wrong one fails (the lockout-safety invariant);
seqpam node gets stock pam_fprintd and no grosshack; nopam gets
neither. flake check + option-docs + template-sot green.
V3 pending (HARDWARE-QUEUE, AMD dev box): the real type-or-touch race,
fprintd-stopped fallback, hyprlock/greeter after a fingerprint win.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 19:03:56 +01:00

316 lines
14 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Hardware enablement beyond nixos-hardware.
#
# The nixos-hardware "common-*" profiles the installer selects cover the
# BASICS (microcode, the Intel/AMD VA-API media stack, weekly fstrim), and
# power.nix adds thermald + power-profiles-daemon. This module fills the GAP
# above those: broadly-beneficial bits that default ON when the installer
# detects the vendor (opt-OUT), and heavier/experimental bits behind opt-IN
# toggles. The installer's hardware-db.sh probes what's present and writes the
# matching nomarchy.hardware.* into the generated system.nix.
#
# Audited against the commons so we don't double-set: we add GuC/HuC, the
# amd-pstate governor, the AMD VA-API env, GPU-compute runtimes, fprintd, and
# the NPU driver — none of which the commons turn on.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.hardware;
# Fingerprint PAM can follow theme-state.json (menu toggle → next
# sys-rebuild), same bridge as autoTimezone (BACKLOG #55). Missing or
# invalid JSON fails closed (theme-state-read.nix) instead of a raw stack.
hwState =
if config.nomarchy.system.stateFile != null
then import ../theme-state-read.nix { inherit lib; } config.nomarchy.system.stateFile
else { };
pamFromState = (hwState.settings or { }).fingerprint.pam or false;
in
{
options.nomarchy.hardware = {
intel = {
enable = lib.mkEnableOption ''
Intel CPU/GPU enablement (the installer turns this on when it detects
an Intel CPU or GPU). Complements nixos-hardware's common-gpu-intel'';
guc = lib.mkOption {
type = lib.types.bool;
default = cfg.intel.enable;
defaultText = lib.literalExpression "config.nomarchy.hardware.intel.enable";
description = ''
Load the GPU's GuC/HuC firmware via the i915 param
(i915.enable_guc=3) better power management and HuC-accelerated
media. On by default with intel.enable. NOTE: this is the *i915*
driver's param; the newer `xe` driver (Lunar Lake / Battlemage /
Panther Lake and other recent Xe GPUs) enables GuC by default and
ignores it, so the installer turns this off on xe-driver hardware.
'';
};
computeRuntime = lib.mkEnableOption ''
Intel GPU compute: the OpenCL / Level Zero (intel-compute-runtime) and
oneVPL (vpl-gpu-rt) runtimes for GPU compute and transcode. Opt-in (a
few hundred MB) the Intel counterpart to AMD ROCm'';
};
amd = {
enable = lib.mkEnableOption ''
AMD CPU/GPU enablement (installer-set on an AMD CPU or GPU).
Complements nixos-hardware's common-cpu-amd / common-gpu-amd'';
pstate = lib.mkOption {
type = lib.types.bool;
default = cfg.amd.enable;
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
description = ''
Use the amd-pstate EPP driver (amd_pstate=active) the modern Zen
power/perf governor that power-profiles-daemon drives per profile.
On by default with amd.enable (broadly beneficial on Zen 2+).
'';
};
vaapi = lib.mkOption {
type = lib.types.bool;
default = cfg.amd.enable;
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
description = ''
Point VA-API at mesa's radeonsi (LIBVA_DRIVER_NAME=radeonsi) for
hardware video decode/encode. On by default with amd.enable.
'';
};
rocm = {
enable = lib.mkEnableOption ''
AMD ROCm: the HIP / OpenCL GPU-compute stack (multi-GB closure).
Opt-in unlocks GPU PyTorch / Ollama on Radeon'';
gfxOverride = lib.mkOption {
type = lib.types.str;
default = "";
example = "11.0.0";
description = ''
HSA_OVERRIDE_GFX_VERSION for GPUs ROCm doesn't officially list
(e.g. an RDNA3 780M iGPU, gfx1103, needs "11.0.0"). Empty = no
override.
'';
};
};
};
fingerprint = {
enable = lib.mkEnableOption ''
a fingerprint reader via fprintd (the installer turns this on when it
detects a known reader). Enroll with `fprintd-enroll`'';
pam = lib.mkOption {
type = lib.types.bool;
default = pamFromState;
defaultText = lib.literalExpression
"(settings.fingerprint.pam from theme-state.json) or false";
description = ''
Use the fingerprint for login and sudo (PAM). Opt-in password-only
stays the default for the cautious; enroll a finger first. Defaults
from theme-state.json `settings.fingerprint.pam` (System Fingerprint
menu) when set; otherwise false.
'';
};
parallel = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
With fingerprint PAM on, accept the password OR a fingerprint at the
same prompt (type or touch, whichever comes first) instead of stock
pam_fprintd's sequential wait-for-the-reader-then-password. Uses the
pam-fprint-grosshack module (an fprintd fork source-reviewed; every
failure path falls through to the normal password rule, so password
login can never be locked out by it). Set false for the stock
sequential behavior.
'';
};
};
npu.enable = lib.mkEnableOption ''
the on-die NPU (AI accelerator) kernel driver amdxdna on AMD (Ryzen
AI), intel_vpu on Intel (Core Ultra and newer). Opt-in and experimental:
this loads the in-kernel driver only; the userspace runtime (AMD XRT /
oneAPI Level Zero NPU) is yours to add. Needs a recent kernel (see
latestKernel)'';
latestKernel = lib.mkEnableOption ''
the latest mainline kernel (pkgs.linuxPackages_latest) instead of the
distro default for very new hardware whose drivers (a fresh NPU, the
`xe` GPU driver, new-platform enablement) only landed recently. Off by
default; the default kernel already carries amd-pstate and amdxdna (6.14+)'';
camera = {
hideIrSensor = lib.mkEnableOption ''
hiding a dual-sensor webcam's IR (face-unlock) node from PipeWire so
apps only ever see the colour camera. Such modules (common on recent
ThinkPads) expose the IR sensor as a SECOND, identically-named
"Integrated Camera"; selecting it gives a dark, 8-bit-greyscale image
the classic "my webcam is dark" symptom. The installer turns this on
when it detects a paired RGB+IR webcam. Only the PipeWire node is
disabled the kernel /dev/video* device stays open, so face-unlock
(Howdy) still works. Acts on the V4L2 path only; the libcamera monitor
is left untouched, so an external camera you plug in is never affected.
See irMatch'';
irMatch = lib.mkOption {
type = lib.types.str;
default = "~.*(Integrated I|IR Camera|Infrared).*";
description = ''
WirePlumber regex (matched against a V4L2 node's api.v4l2.cap.card)
selecting the IR sensor to hide. The default catches the common
dual-sensor naming (" Integrated I", "IR Camera", "Infrared"); set it
to your camera's IR card name if it differs find it with
`v4l2-ctl --list-devices` or `wpctl inspect`. Only consulted when
camera.hideIrSensor is on.
'';
};
};
i2c = {
enable = lib.mkEnableOption ''
I2C devices support. Enables access to /dev/i2c-* (useful for RGB
controllers, sensors, and DDC/CI monitor control)'';
ddcci = lib.mkEnableOption ''
the ddcci-driver kernel module to expose external monitors as standard
backlight devices via DDC/CI. This allows brightness keys and swayosd
to natively control external displays'';
};
};
config = lib.mkMerge [
# ── Intel ──────────────────────────────────────────────────────────
(lib.mkIf cfg.intel.guc {
boot.kernelParams = [ "i915.enable_guc=3" ];
})
(lib.mkIf cfg.intel.computeRuntime {
hardware.graphics.extraPackages = with pkgs; [ intel-compute-runtime vpl-gpu-rt ];
})
# ── AMD ────────────────────────────────────────────────────────────
(lib.mkIf cfg.amd.pstate {
boot.kernelParams = [ "amd_pstate=active" ];
})
(lib.mkIf cfg.amd.vaapi {
# radeonsi itself comes from mesa (via common-gpu-amd); this just steers
# libva at it. mkDefault so a hand-set value or another module wins.
environment.sessionVariables.LIBVA_DRIVER_NAME = lib.mkDefault "radeonsi";
hardware.graphics.extraPackages = [ pkgs.libva ];
})
(lib.mkIf cfg.amd.rocm.enable {
hardware.graphics.extraPackages = [ pkgs.rocmPackages.clr pkgs.rocmPackages.clr.icd ];
environment.sessionVariables = lib.optionalAttrs (cfg.amd.rocm.gfxOverride != "") {
HSA_OVERRIDE_GFX_VERSION = cfg.amd.rocm.gfxOverride;
};
})
# ── Fingerprint ────────────────────────────────────────────────────
# NixOS defaults security.pam.services.*.fprintAuth to
# services.fprintd.enable — so turning on fprintd alone would enable
# finger auth for login/sudo/greetd/passwd/… even when the user (or
# the migration template) left fingerprint.pam commented off. Force
# every interactive service we care about to follow our opt-in flag.
(lib.mkIf cfg.fingerprint.enable {
services.fprintd.enable = true;
security.pam.services = lib.genAttrs [
"login" "sudo" "su" "greetd" "hyprlock" "sshd"
"passwd" "chsh" "chfn" "chpasswd"
"polkit-1" "swaylock"
"groupadd" "groupdel" "groupmod" "groupmems"
] (_: {
fprintAuth = cfg.fingerprint.pam;
} // lib.optionalAttrs (cfg.fingerprint.pam && cfg.fingerprint.parallel) {
# Parallel mode: same rule slot as stock fprintd (so ordering —
# sufficient, before pam_unix — is inherited), different module.
# grosshack prompts for the password itself while polling the
# reader; whichever lands first wins. A typed password makes the
# rule FAIL with the token stored, and the stock
# `auth sufficient pam_unix.so … try_first_pass` right after it
# does the actual validation — password stays sufficient on its
# own, so a broken reader/fprintd can never lock login out.
rules.auth.fprintd.modulePath = lib.mkForce
"${pkgs.pam-fprint-grosshack}/lib/security/pam_fprintd_grosshack.so";
});
})
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
(lib.mkIf cfg.latestKernel {
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
})
# ── NPU (in-kernel driver only; userspace runtime is BYO) ──────────
# The driver has to actually be in the running kernel — warn (don't fail)
# when it predates the shipped one, pointing at latestKernel.
(lib.mkIf (cfg.npu.enable && cfg.amd.enable) {
boot.kernelModules = [ "amdxdna" ];
warnings = lib.optional
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14")
''
nomarchy.hardware.npu: the amdxdna driver needs kernel >= 6.14, but
this config ships ${config.boot.kernelPackages.kernel.version}. Set
nomarchy.hardware.latestKernel = true.'';
})
(lib.mkIf (cfg.npu.enable && cfg.intel.enable) {
boot.kernelModules = [ "intel_vpu" ];
warnings = lib.optional
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.11")
''
nomarchy.hardware.npu: the intel_vpu driver (especially for newer
NPUs) wants a recent kernel, but this config ships
${config.boot.kernelPackages.kernel.version}. Consider
nomarchy.hardware.latestKernel = true.'';
})
# ── Webcam: hide a dual-sensor module's IR node ────────────────────
# A built-in RGB+IR webcam exposes its IR (face-unlock) sensor as a second,
# identically-named camera; an app that picks it gets a dark greyscale
# image. Disable that node on the V4L2 PipeWire path so only the colour
# camera is offered. Matched by card name (irMatch). libcamera is left
# alone on purpose — an external camera may rely on it, and surgical
# internal-only libcamera scoping isn't possible (the distinguishing
# device props bind after the monitor rule runs). The kernel /dev/video*
# stays open, so Howdy face-unlock still reads the IR sensor directly.
(lib.mkIf (cfg.camera.hideIrSensor && config.services.pipewire.wireplumber.enable) {
services.pipewire.wireplumber.extraConfig."90-nomarchy-hide-ir-camera" = {
"monitor.v4l2.rules" = [
{
matches = [ { "api.v4l2.cap.card" = cfg.camera.irMatch; } ];
actions."update-props"."node.disabled" = true;
}
];
};
})
# ── I2C / DDC/CI ───────────────────────────────────────────────────
(lib.mkIf cfg.i2c.enable {
hardware.i2c.enable = true;
})
(lib.mkIf cfg.i2c.ddcci {
# The driver needs I2C underneath it
hardware.i2c.enable = true;
boot.extraModulePackages = [ config.boot.kernelPackages.ddcci-driver ];
boot.kernelModules = [ "ddcci_backlight" ];
})
# ── Sanity ─────────────────────────────────────────────────────────
{
assertions = [
{
assertion = cfg.amd.rocm.enable -> cfg.amd.enable;
message = "nomarchy.hardware.amd.rocm.enable needs nomarchy.hardware.amd.enable.";
}
{
assertion = cfg.intel.computeRuntime -> cfg.intel.enable;
message = "nomarchy.hardware.intel.computeRuntime needs nomarchy.hardware.intel.enable.";
}
{
assertion = cfg.npu.enable -> (cfg.amd.enable || cfg.intel.enable);
message = "nomarchy.hardware.npu.enable needs a detected Intel or AMD platform.";
}
];
}
];
}