From 090a7b622b575c6bcd76754f31ca2a8400e736c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ryan4yin=20=7C=20=E4=BA=8C=E8=8A=B1?= Date: Sun, 1 Nov 2026 10:05:15 +0800 Subject: [PATCH] refactor(nixpak): split policy checks into per-app suites (#493) * refactor(nixpak): split policy checks into per-app suites Make hardening/tests/policy.nix a small loader that auto-discovers one suite per app under hardening/tests/policy. Shared checks live in policy-lib.nix and common expectations are generated by mkAppPolicy; app files only declare differences and special cases. The loader also discovers wrappers from hardening/nixpaks and fails when wrapper/suite inventory or check names diverge. Assisted-by: DeepSeek Harness (deepseek-flash) * fix(nixpak): reject duplicate per-app policy checks mkAppPolicy merges generated and custom checks, so an extraChecks key could silently override a standard assertion. Fail explicitly when a per-app check name is defined twice. Assisted-by: DeepSeek Harness (deepseek-flash) --- hardening/README.md | 2 +- hardening/tests/policy-lib.nix | 170 ++++++++++++ hardening/tests/policy.nix | 251 +++--------------- hardening/tests/policy/brave-origin.nix | 10 + hardening/tests/policy/chromium.nix | 10 + hardening/tests/policy/firefox.nix | 31 +++ hardening/tests/policy/google-chrome.nix | 10 + hardening/tests/policy/microsoft-edge.nix | 10 + hardening/tests/policy/qq.nix | 8 + hardening/tests/policy/telegram-desktop.nix | 8 + hardening/tests/policy/wechat.nix | 9 + hardening/tests/policy/zoom.nix | 28 ++ .../x86_64-linux/tests/nixpak-policy/expr.nix | 9 +- 13 files changed, 335 insertions(+), 221 deletions(-) create mode 100644 hardening/tests/policy-lib.nix create mode 100644 hardening/tests/policy/brave-origin.nix create mode 100644 hardening/tests/policy/chromium.nix create mode 100644 hardening/tests/policy/firefox.nix create mode 100644 hardening/tests/policy/google-chrome.nix create mode 100644 hardening/tests/policy/microsoft-edge.nix create mode 100644 hardening/tests/policy/qq.nix create mode 100644 hardening/tests/policy/telegram-desktop.nix create mode 100644 hardening/tests/policy/wechat.nix create mode 100644 hardening/tests/policy/zoom.nix diff --git a/hardening/README.md b/hardening/README.md index 9e9ee2ba..91a81564 100644 --- a/hardening/README.md +++ b/hardening/README.md @@ -66,7 +66,7 @@ All browsers get read-only NSS certificates, printing, native notifications and - WeChat uses the native nixpkgs-patched package with nixpak as the only bwrap layer. Build/policy pass; interactive Wayland validation is still pending. - Evidence and repeat commands: [runtime validation](tests/runtime-validation.md), - [policy test](tests/policy.nix). + [policy test](tests/policy.nix), which auto-loads every suite under [tests/policy](tests/policy/). ## Code map diff --git a/hardening/tests/policy-lib.nix b/hardening/tests/policy-lib.nix new file mode 100644 index 00000000..96fb8885 --- /dev/null +++ b/hardening/tests/policy-lib.nix @@ -0,0 +1,170 @@ +{ pkgs, mkNixPak }: +let + inherit (pkgs) lib; + + load = name: (pkgs.callPackage (../nixpaks + "/${name}.nix") { inherit mkNixPak; }).config; + + # Wrapper files are the source of truth for the app inventory. The test + # loader compares this with the per-app suites under ./policy. + appNames = map (lib.removeSuffix ".nix") ( + builtins.attrNames ( + lib.filterAttrs ( + name: type: type == "regular" && name != "default.nix" && lib.hasSuffix ".nix" name + ) (builtins.readDir ../nixpaks) + ) + ); + apps = map load appNames; + + mountStrings = + app: builtins.filter builtins.isString (lib.flatten (with app.bubblewrap.bind; rw ++ ro ++ dev)); + + forbiddenDbus = + argument: + lib.any (name: lib.hasInfix name argument) [ + "org.freedesktop.portal.Usb" + "org.freedesktop.portal.RemoteDesktop" + "org.gnome.Shell.Screencast" + ] + || argument == "--talk=org.freedesktop.portal.Desktop"; + + withoutSecretPortal = + calls: + calls + // { + "org.freedesktop.portal.Desktop" = lib.filter ( + arg: !lib.hasInfix "org.freedesktop.portal.Secret" arg + ) calls."org.freedesktop.portal.Desktop"; + }; + + forbiddenMount = + path: + path == "/dev" + || lib.hasPrefix "/dev/video" path + || lib.hasPrefix "/dev/bus/usb" path + || path == "/run/dbus" + || path == "/run/dbus/system_bus_socket"; + + hasPrivateTmp = + app: + let + tmpPath = lib.findFirst ( + entry: builtins.isList entry && builtins.length entry == 2 && builtins.elem "/tmp" entry + ) null app.bubblewrap.bind.rw; + in + builtins.isList tmpPath && builtins.length tmpPath == 2; + + hasDbusFragment = app: fragment: lib.any (argument: lib.hasInfix fragment argument) app.dbus.args; + + networkCheck = + app: mode: + if mode == "host" then + app.bubblewrap.network && !app.pasta.enable + else if mode == "pasta" then + app.bubblewrap.network + && app.pasta.enable + && app.pasta.mode == "isolate" + && lib.elem "--no-map-gw" app.pasta.args + else if mode == "none" then + !app.bubblewrap.network && !app.pasta.enable + else + throw "unknown network mode for ${app.flatpak.appId}: ${mode}"; + + mediaCheck = + app: expected: + if expected then + app.bubblewrap.sockets.pulse && app.portalGroups.camera && app.portalGroups.screenCast + else + !app.bubblewrap.sockets.pulse + && !app.bubblewrap.sockets.pipewire + && !app.portalGroups.camera + && !app.portalGroups.screenCast + && !(hasDbusFragment app "org.freedesktop.portal.Camera") + && !(hasDbusFragment app "org.freedesktop.portal.ScreenCast"); + + secretPortalCheck = + app: expected: + if expected then + (app.browserPermissions.secretPortal or false) + && lib.any ( + argument: + argument + == "--call=org.freedesktop.portal.Desktop=org.freedesktop.portal.Secret.RetrieveSecret@/org/freedesktop/portal/desktop" + ) app.dbus.args + else + !(app.browserPermissions.secretPortal or false) + && !(hasDbusFragment app "org.freedesktop.portal.Secret"); + + trayCheck = + app: expected: + if expected then + app.desktopIntegration.tray + && (app.dbus.policies."org.freedesktop.DBus" or null) == "talk" + && (app.dbus.policies."org.kde.StatusNotifierWatcher" or null) == "talk" + else + !app.desktopIntegration.tray; + + mkAppPolicy = + name: spec: + let + app = load name; + allowedVideoDevices = spec.allowedVideoDevices or [ ]; + forbiddenMountFor = path: forbiddenMount path && !lib.elem path allowedVideoDevices; + checks = { + "${name}/clearEnv" = app.bubblewrap.clearEnv; + "${name}/dbus" = app.dbus.enable; + "${name}/noRawPipewire" = !app.bubblewrap.sockets.pipewire; + "${name}/noForbiddenDbus" = !lib.any forbiddenDbus app.dbus.args; + "${name}/noBroadKeyring" = + !lib.any (argument: argument == "--talk=org.freedesktop.secrets") app.dbus.args; + "${name}/noForbiddenMounts" = !lib.any forbiddenMountFor (mountStrings app); + "${name}/privateTmp" = hasPrivateTmp app; + "${name}/network" = networkCheck app spec.network; + "${name}/x11" = app.bubblewrap.sockets.x11 == (spec.x11 or false); + "${name}/media" = mediaCheck app (spec.media or false); + "${name}/mpris" = app.desktopIntegration.mpris == (spec.mpris or false); + "${name}/notifications" = app.desktopIntegration.notifications == (spec.notifications or true); + "${name}/tray" = trayCheck app (spec.tray or false); + "${name}/browserpass" = + (app.browserPermissions.browserpass or false) == (spec.browserpass or false); + "${name}/personalFiles" = + (app.browserPermissions.personalFiles or false) == (spec.personalFiles or false); + "${name}/secretPortal" = secretPortalCheck app (spec.secretPortal or false); + }; + extraChecks = lib.mapAttrs' (checkName: value: { + name = "${name}/${checkName}"; + inherit value; + }) (spec.extraChecks or { }); + duplicates = lib.attrNames (lib.intersectAttrs checks extraChecks); + in + if duplicates != [ ] then + throw "duplicate nixpak policy checks for ${name}: ${lib.concatStringsSep ", " duplicates}" + else + checks // extraChecks; + + globalChecks = + let + paths = map (app: app.bubblewrap.bind.rw) apps; + tmpPaths = map ( + bind: + lib.findFirst ( + entry: builtins.isList entry && builtins.length entry == 2 && builtins.elem "/tmp" entry + ) null bind + ) paths; + in + { + appTmpIsolation = + lib.all (path: builtins.isList path && builtins.length path == 2) tmpPaths + && lib.length (lib.unique (map builtins.head tmpPaths)) == builtins.length apps; + }; +in +{ + inherit + lib + appNames + load + mountStrings + withoutSecretPortal + mkAppPolicy + globalChecks + ; +} diff --git a/hardening/tests/policy.nix b/hardening/tests/policy.nix index 4dd9bc6b..68ee14cb 100644 --- a/hardening/tests/policy.nix +++ b/hardening/tests/policy.nix @@ -1,221 +1,36 @@ -# Evaluate with the same pkgs/mkNixPak as the wrappers. Returns true or identifies -# the violated policy boundary; does not build or launch an application. -{ pkgs, mkNixPak }: +# Evaluate with the same pkgs/mkNixPak as the wrappers. Every file under +# ./policy is imported as one app policy suite. Wrapper files are auto-discovered +# from ../nixpaks, so a wrapper and its suite must be added together. +{ + pkgs, + mkNixPak, + mylib, +}: let inherit (pkgs) lib; - load = name: (pkgs.callPackage (../nixpaks + "/${name}.nix") { inherit mkNixPak; }).config; - firefox = load "firefox"; - chromium = load "chromium"; - chrome = load "google-chrome"; - restrictedBrowsers = map load [ - "brave-origin" - "microsoft-edge" - ]; - chromiumFamily = map load [ - "chromium" - "google-chrome" - "brave-origin" - "microsoft-edge" - ]; - browsers = map load [ - "firefox" - "chromium" - "google-chrome" - "brave-origin" - "microsoft-edge" - ]; - nonBrowserApps = map load [ - "qq" - "telegram-desktop" - "zoom" - "wechat" - ]; - browserAndChatApps = - browsers - ++ map load [ - "qq" - "telegram-desktop" - ]; - apps = browsers ++ nonBrowserApps; - # Inspect effective proxy arguments, not just capability switches: an extra - # direct service grant could otherwise bypass a disabled portal group. - forbiddenDbus = - argument: - lib.any (name: lib.hasInfix name argument) [ - "org.freedesktop.portal.Usb" - "org.freedesktop.portal.RemoteDesktop" - "org.gnome.Shell.Screencast" - ] - || argument == "--talk=org.freedesktop.portal.Desktop"; - withoutSecretPortal = - calls: - calls - // { - "org.freedesktop.portal.Desktop" = lib.filter ( - arg: !lib.hasInfix "org.freedesktop.portal.Secret" arg - ) calls."org.freedesktop.portal.Desktop"; - }; - mountStrings = - app: builtins.filter builtins.isString (lib.flatten (with app.bubblewrap.bind; rw ++ ro ++ dev)); - forbiddenMount = - path: - path == "/dev" - || lib.hasPrefix "/dev/video" path - || lib.hasPrefix "/dev/bus/usb" path - || path == "/run/dbus" - || path == "/run/dbus/system_bus_socket"; - checks = { - personalBrowserCapabilitiesMatch = - firefox.browserPermissions.media == chromium.browserPermissions.media - && firefox.browserPermissions.personalFiles == chromium.browserPermissions.personalFiles - && firefox.browserPermissions.browserpass == chromium.browserPermissions.browserpass - && - (removeAttrs firefox.portalGroups [ "secret" ]) == (removeAttrs chromium.portalGroups [ "secret" ]) - && firefox.desktopIntegration == chromium.desktopIntegration; - personalBrowserPortalCallsMatch = - withoutSecretPortal firefox.dbus.rules.call == withoutSecretPortal chromium.dbus.rules.call; - personalBrowserHostLoopback = lib.all (app: app.bubblewrap.network && !app.pasta.enable) [ - firefox - chromium - ]; - workBrowserIsolatedLoopback = - chrome.bubblewrap.network - && chrome.pasta.enable - && chrome.pasta.mode == "isolate" - && lib.elem "--no-map-gw" chrome.pasta.args; - zoomDrmSysfs = lib.elem "/sys/class/drm" (load "zoom").bubblewrap.bind.ro; - zoomCameraDevices = - let - zoom = load "zoom"; - in - lib.all (dev: lib.elem dev zoom.bubblewrap.bind.dev) [ - "/dev/video0" - "/dev/video1" - ]; - fhsTrayIntegration = - lib.all - ( - app: - app.desktopIntegration.tray - && app.dbus.policies."org.freedesktop.DBus" == "talk" - && app.dbus.policies."org.kde.StatusNotifierWatcher" == "talk" - && app.dbus.policies."org.kde.StatusNotifierItem-2-1" == "own" - && app.dbus.policies."org.kde.StatusNotifierItem-2-2" == "own" - ) - ( - map load [ - "wechat" - "zoom" - ] - ); - appTmpIsolation = - let - tmpPath = - app: - lib.findFirst ( - entry: builtins.isList entry && builtins.length entry == 2 && builtins.elem "/tmp" entry - ) null app.bubblewrap.bind.rw; - paths = map tmpPath apps; - in - lib.all (path: builtins.isList path && builtins.length path == 2) paths - && lib.length (lib.unique (map builtins.head paths)) == builtins.length apps; - workBrowserMedia = - chrome.portalGroups.camera - && chrome.portalGroups.screenCast - && chrome.bubblewrap.sockets.pulse - && chrome.gpu.enable - && lib.elem "--call=org.freedesktop.portal.Desktop=org.freedesktop.portal.Camera.OpenPipeWireRemote@/org/freedesktop/portal/desktop" chrome.dbus.args - && lib.elem "--call=org.freedesktop.portal.Desktop=org.freedesktop.portal.ScreenCast.OpenPipeWireRemote@/org/freedesktop/portal/desktop" chrome.dbus.args; - secretPortalScope = - lib.all ( - app: - app.browserPermissions.secretPortal - && lib.any ( - arg: - arg - == "--call=org.freedesktop.portal.Desktop=org.freedesktop.portal.Secret.RetrieveSecret@/org/freedesktop/portal/desktop" - ) app.dbus.args - && !lib.any (arg: arg == "--talk=org.freedesktop.secrets") app.dbus.args - ) chromiumFamily - && !firefox.browserPermissions.secretPortal - && lib.all ( - app: - !(app.browserPermissions.secretPortal or false) - && !lib.any (arg: lib.hasInfix "org.freedesktop.portal.Secret" arg) app.dbus.args - ) nonBrowserApps; - mediaScope = - lib.all - ( - app: - app.browserPermissions.media - && app.bubblewrap.sockets.pulse - && app.portalGroups.camera - && app.portalGroups.screenCast - ) - [ - firefox - chromium - chrome - ]; - restrictedBrowserMedia = lib.all ( - app: - !app.browserPermissions.media - && !app.bubblewrap.sockets.pulse - && !app.bubblewrap.sockets.pipewire - && !app.portalGroups.camera - && !app.portalGroups.screenCast - && !app.desktopIntegration.mpris - && !lib.any ( - arg: - lib.hasInfix "org.freedesktop.portal.Camera" arg - || lib.hasInfix "org.freedesktop.portal.ScreenCast" arg - || lib.hasInfix "org.mpris.MediaPlayer2" arg - ) app.dbus.args - && !lib.any (path: lib.hasPrefix "/dev/snd" path) (mountStrings app) - ) restrictedBrowsers; - personalFileScope = - firefox.browserPermissions.personalFiles - && chromium.browserPermissions.personalFiles - && !chrome.browserPermissions.personalFiles; - browserpassScope = - lib.all (app: app.browserPermissions.browserpass) [ - firefox - chromium - chrome - ] - && lib.all (app: !app.browserPermissions.browserpass) ( - map load [ - "brave-origin" - "microsoft-edge" - ] - ); - deniedServices = lib.all (app: !lib.any forbiddenDbus app.dbus.args) apps; - noBroadKeyring = lib.all ( - app: !lib.any (arg: arg == "--talk=org.freedesktop.secrets") app.dbus.args - ) apps; - deniedDevicesAndSystemBus = - let - zoomVideoDevices = [ - "/dev/video0" - "/dev/video1" - ]; - forbiddenMountFor = - app: - if app.flatpak.appId == "us.zoom.Zoom" then - path: forbiddenMount path && !lib.elem path zoomVideoDevices - else - forbiddenMount; - in - lib.all (app: !lib.any (forbiddenMountFor app) (mountStrings app)) apps; - filteredEnvironmentAndMedia = lib.all ( - app: - app.bubblewrap.clearEnv - && app.dbus.enable - && !app.bubblewrap.sockets.pipewire - && !app.bubblewrap.sockets.x11 - ) browserAndChatApps; - }; + ctx = import ./policy-lib.nix { inherit pkgs mkNixPak; }; + suitePaths = mylib.scanPaths ./policy; + suiteNames = map (path: lib.removeSuffix ".nix" (baseNameOf (toString path))) suitePaths; + suites = map (path: import path ctx) suitePaths; + merge = + acc: suite: + let + duplicates = lib.attrNames (lib.intersectAttrs acc suite); + in + if duplicates != [ ] then + throw "duplicate nixpak policy checks: ${lib.concatStringsSep ", " duplicates}" + else + acc // suite; + checks = ctx.globalChecks // lib.foldl' merge { } suites; + missing = lib.subtractLists suiteNames ctx.appNames; + orphans = lib.subtractLists ctx.appNames suiteNames; + failures = builtins.filter (name: !(checks.${name} or false)) (builtins.attrNames checks); + problems = + lib.optional (missing != [ ]) "missing policy suites: ${lib.concatStringsSep ", " missing}" + ++ lib.optional (orphans != [ ]) "orphan policy suites: ${lib.concatStringsSep ", " orphans}" + ++ lib.optional (failures != [ ]) "failed checks:\n - ${lib.concatStringsSep "\n - " failures}"; in -lib.all (name: checks.${name} || throw "nixpak policy check failed: ${name}") ( - builtins.attrNames checks -) +if problems == [ ] then + true +else + throw "nixpak policy check failed:\n${lib.concatStringsSep "\n" problems}" diff --git a/hardening/tests/policy/brave-origin.nix b/hardening/tests/policy/brave-origin.nix new file mode 100644 index 00000000..9116c697 --- /dev/null +++ b/hardening/tests/policy/brave-origin.nix @@ -0,0 +1,10 @@ +{ mkAppPolicy, ... }: +mkAppPolicy "brave-origin" { + network = "pasta"; + media = false; + mpris = false; + notifications = true; + personalFiles = false; + browserpass = false; + secretPortal = true; +} diff --git a/hardening/tests/policy/chromium.nix b/hardening/tests/policy/chromium.nix new file mode 100644 index 00000000..a4c7da69 --- /dev/null +++ b/hardening/tests/policy/chromium.nix @@ -0,0 +1,10 @@ +{ mkAppPolicy, ... }: +mkAppPolicy "chromium" { + network = "host"; + media = true; + mpris = true; + notifications = true; + personalFiles = true; + browserpass = true; + secretPortal = true; +} diff --git a/hardening/tests/policy/firefox.nix b/hardening/tests/policy/firefox.nix new file mode 100644 index 00000000..bb583b94 --- /dev/null +++ b/hardening/tests/policy/firefox.nix @@ -0,0 +1,31 @@ +{ + lib, + load, + mkAppPolicy, + withoutSecretPortal, + ... +}: +let + firefox = load "firefox"; + chromium = load "chromium"; +in +mkAppPolicy "firefox" { + network = "host"; + media = true; + mpris = true; + notifications = true; + personalFiles = true; + browserpass = true; + secretPortal = false; + extraChecks = { + capabilitiesMatchChromium = + firefox.browserPermissions.media == chromium.browserPermissions.media + && firefox.browserPermissions.personalFiles == chromium.browserPermissions.personalFiles + && firefox.browserPermissions.browserpass == chromium.browserPermissions.browserpass + && + (removeAttrs firefox.portalGroups [ "secret" ]) == (removeAttrs chromium.portalGroups [ "secret" ]) + && firefox.desktopIntegration == chromium.desktopIntegration; + portalCallsMatchChromium = + withoutSecretPortal firefox.dbus.rules.call == withoutSecretPortal chromium.dbus.rules.call; + }; +} diff --git a/hardening/tests/policy/google-chrome.nix b/hardening/tests/policy/google-chrome.nix new file mode 100644 index 00000000..7bb2d342 --- /dev/null +++ b/hardening/tests/policy/google-chrome.nix @@ -0,0 +1,10 @@ +{ mkAppPolicy, ... }: +mkAppPolicy "google-chrome" { + network = "pasta"; + media = true; + mpris = true; + notifications = true; + personalFiles = false; + browserpass = true; + secretPortal = true; +} diff --git a/hardening/tests/policy/microsoft-edge.nix b/hardening/tests/policy/microsoft-edge.nix new file mode 100644 index 00000000..cb9045d0 --- /dev/null +++ b/hardening/tests/policy/microsoft-edge.nix @@ -0,0 +1,10 @@ +{ mkAppPolicy, ... }: +mkAppPolicy "microsoft-edge" { + network = "pasta"; + media = false; + mpris = false; + notifications = true; + personalFiles = false; + browserpass = false; + secretPortal = true; +} diff --git a/hardening/tests/policy/qq.nix b/hardening/tests/policy/qq.nix new file mode 100644 index 00000000..3910f36b --- /dev/null +++ b/hardening/tests/policy/qq.nix @@ -0,0 +1,8 @@ +{ mkAppPolicy, ... }: +mkAppPolicy "qq" { + network = "pasta"; + media = true; + mpris = true; + notifications = true; + tray = true; +} diff --git a/hardening/tests/policy/telegram-desktop.nix b/hardening/tests/policy/telegram-desktop.nix new file mode 100644 index 00000000..4d01de2c --- /dev/null +++ b/hardening/tests/policy/telegram-desktop.nix @@ -0,0 +1,8 @@ +{ mkAppPolicy, ... }: +mkAppPolicy "telegram-desktop" { + network = "pasta"; + media = true; + mpris = true; + notifications = true; + tray = true; +} diff --git a/hardening/tests/policy/wechat.nix b/hardening/tests/policy/wechat.nix new file mode 100644 index 00000000..8b1de185 --- /dev/null +++ b/hardening/tests/policy/wechat.nix @@ -0,0 +1,9 @@ +{ mkAppPolicy, ... }: +mkAppPolicy "wechat" { + network = "pasta"; + media = true; + mpris = false; + notifications = false; + x11 = true; + tray = true; +} diff --git a/hardening/tests/policy/zoom.nix b/hardening/tests/policy/zoom.nix new file mode 100644 index 00000000..12ad8375 --- /dev/null +++ b/hardening/tests/policy/zoom.nix @@ -0,0 +1,28 @@ +{ + lib, + load, + mkAppPolicy, + ... +}: +let + zoom = load "zoom"; +in +mkAppPolicy "zoom" { + network = "pasta"; + media = true; + mpris = false; + notifications = false; + x11 = true; + tray = true; + allowedVideoDevices = [ + "/dev/video0" + "/dev/video1" + ]; + extraChecks = { + drmSysfs = lib.elem "/sys/class/drm" zoom.bubblewrap.bind.ro; + videoDevices = lib.all (dev: lib.elem dev zoom.bubblewrap.bind.dev) [ + "/dev/video0" + "/dev/video1" + ]; + }; +} diff --git a/outputs/x86_64-linux/tests/nixpak-policy/expr.nix b/outputs/x86_64-linux/tests/nixpak-policy/expr.nix index 6755acc9..7e54e306 100644 --- a/outputs/x86_64-linux/tests/nixpak-policy/expr.nix +++ b/outputs/x86_64-linux/tests/nixpak-policy/expr.nix @@ -1,9 +1,14 @@ -{ inputs, outputs, ... }: +{ + inputs, + mylib, + outputs, + ... +}: let pkgs = outputs.nixosConfigurations.ai-niri.pkgs; in import ../../../../hardening/tests/policy.nix { - inherit pkgs; + inherit pkgs mylib; mkNixPak = inputs.nixpak.lib.nixpak { inherit pkgs; inherit (pkgs) lib;