feat(nixpak): unify desktop app sandboxes (#491)

* refactor(nixpak): unify desktop app sandboxes

Move common GUI, portal, and network policy into reusable modules and migrate browsers plus Zoom/WeChat/QQ/Telegram to the same boundary.

Assisted-by: DeepSeek Harness (deepseek-flash)
This commit is contained in:
ryan4yin | 二花 authored and GitHub committed 2026-10-31 22:27:58 +08:00
1 parent 707da35996
commit 818965a11f
35 files changed
+1858 -565

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
{
callPackage,
mkNixPak,
brave-origin,
...
}:
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
package = brave-origin;
appId = "com.brave.Origin";
profile = "BraveSoftware/Brave-Origin";
command = "brave-origin";
desktopName = "Brave Origin";
desktopFile = "brave-origin";
browserpass = false;
secretPortal = true;
}
+19
View File
@@ -0,0 +1,19 @@
{
callPackage,
mkNixPak,
chromium,
...
}:
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
package = chromium;
appId = "org.chromium.Chromium";
profile = "chromium";
command = "chromium";
desktopName = "Chromium";
desktopFile = "chromium-browser";
browserpass = true;
media = true;
hostLoopback = true;
personalFiles = true;
secretPortal = true;
}
+6
View File
@@ -21,6 +21,12 @@ in
qq = wrapper pkgs-master ./qq.nix;
telegram-desktop = wrapper super ./telegram-desktop.nix;
firefox = wrapper super ./firefox.nix;
google-chrome = wrapper super ./google-chrome.nix;
chromium = wrapper super ./chromium.nix;
brave-origin = wrapper super ./brave-origin.nix;
microsoft-edge = wrapper super ./microsoft-edge.nix;
zoom = wrapper super ./zoom.nix;
wechat = wrapper super ./wechat.nix;
};
})
];
+8 -34
View File
@@ -21,9 +21,6 @@ let
sloth,
...
}:
let
passwordStore = sloth.concat' sloth.homeDir "/.local/share/password-store";
in
{
app = {
package = firefox;
@@ -32,14 +29,19 @@ let
flatpak.appId = appId;
imports = [
./modules/gui-base.nix
./modules/browser-permissions.nix
./modules/network.nix
./modules/custom-policy.nix
];
browserPermissions = {
media = true;
personalFiles = true;
browserpass = true;
};
# Firefox keeps the host network: local development servers and the
# mihomo controller live on 127.0.0.1, and pasta's isolated loopback
# would hide them. The other sandboxed apps do not need host loopback.
# would hide them. Chromium uses the same personal-browser policy.
pasta.enable = lib.mkForce false;
bubblewrap = {
@@ -55,16 +57,6 @@ let
# path over MPRIS. Without this bind it lands under the sandboxed
# XDG_DATA_HOME and Noctalia (outside) cannot read it.
(sloth.mkdir (sloth.concat' sloth.homeDir "/.local/share/firefox-mpris"))
# Files deleted from Firefox go to the real trash instead of a
# private one under the sandboxed XDG_DATA_HOME.
(sloth.mkdir (sloth.concat' sloth.homeDir "/.local/share/Trash"))
sloth.xdgDocumentsDir
sloth.xdgDownloadDir
sloth.xdgMusicDir
sloth.xdgVideosDir
sloth.xdgPicturesDir
];
bind.ro = [
"/sys/bus/pci"
@@ -72,25 +64,7 @@ let
"${config.app.package}/lib/firefox"
"/app/etc/firefox"
]
# ================ for browserpass extension ===============================
"/etc/gnupg"
(sloth.concat' sloth.homeDir "/.gnupg") # gpg's config
passwordStore # my secrets
(sloth.concat' sloth.runtimeDir "/gnupg") # for access gpg-agent socket
# Match the host's default-app associations and XDG user dirs.
(sloth.concat' sloth.homeDir "/.config/mimeapps.list")
(sloth.concat' sloth.homeDir "/.config/user-dirs.dirs")
# Shared NSS client-certificate store (imported outside Firefox).
(sloth.concat' sloth.homeDir "/.pki/nssdb")
# dconf settings. Not verified as needed; candidate for removal.
(sloth.concat' sloth.xdgConfigHome "/dconf")
];
# browserpass looks for the store here, not at pass's default path.
env.PASSWORD_STORE_DIR = passwordStore;
};
};
};
+17
View File
@@ -0,0 +1,17 @@
{
callPackage,
mkNixPak,
google-chrome,
...
}:
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
package = google-chrome;
appId = "com.google.Chrome";
profile = "google-chrome";
command = "google-chrome";
desktopName = "Google Chrome";
desktopFile = "google-chrome";
browserpass = true;
media = true;
secretPortal = true;
}
+17
View File
@@ -0,0 +1,17 @@
{
callPackage,
mkNixPak,
microsoft-edge,
...
}:
(callPackage ./modules/chromium-browser.nix { inherit mkNixPak; }) {
package = microsoft-edge;
appId = "com.microsoft.Edge";
profile = "microsoft-edge";
command = "microsoft-edge";
desktopName = "Microsoft Edge";
desktopFile = "microsoft-edge";
browserpass = false;
secretPortal = true;
privateWindowFlag = "--inprivate";
}
@@ -0,0 +1,64 @@
# Shared browser capabilities. Network topology and application profiles are
# selected by the app; personal files, browserpass, Secret portal and media are explicit opt-ins.
{
config,
lib,
sloth,
...
}:
let
cfg = config.browserPermissions;
passwordStore = sloth.concat' sloth.homeDir "/.local/share/password-store";
in
{
imports = [ ./desktop-base.nix ];
options.browserPermissions = {
media = lib.mkEnableOption "audio playback/recording, camera and desktop screen sharing";
personalFiles = lib.mkEnableOption "personal Documents, media directories and Trash";
browserpass = lib.mkEnableOption "browserpass access to the password store and GPG agent";
secretPortal = lib.mkEnableOption "the app-scoped Secret portal for v12 cookie encryption";
};
config = {
portalGroups = {
fileChooser = true;
openUri = true;
notifications = true;
screenCast = cfg.media;
camera = cfg.media;
print = true;
inhibit = cfg.media;
inputMethod = true;
secret = cfg.secretPortal;
};
desktopIntegration = {
notifications = true;
mpris = cfg.media;
accessibility = true;
};
bubblewrap = {
sockets.pulse = cfg.media;
bind.rw = [
sloth.xdgDownloadDir
]
++ lib.optionals cfg.personalFiles [
sloth.xdgDocumentsDir
sloth.xdgMusicDir
sloth.xdgVideosDir
sloth.xdgPicturesDir
(sloth.mkdir (sloth.concat' sloth.homeDir "/.local/share/Trash"))
];
bind.ro = [
(sloth.concat' sloth.homeDir "/.config/mimeapps.list")
(sloth.concat' sloth.homeDir "/.config/user-dirs.dirs")
(sloth.concat' sloth.homeDir "/.pki/nssdb")
]
++ lib.optionals cfg.browserpass [
"/etc/gnupg"
(sloth.concat' sloth.homeDir "/.gnupg")
passwordStore
(sloth.concat' sloth.runtimeDir "/gnupg")
];
env = lib.optionalAttrs cfg.browserpass { PASSWORD_STORE_DIR = passwordStore; };
};
};
}
@@ -0,0 +1,121 @@
# Chromium-family wrapper: nixpak adds filesystem, D-Bus and network boundaries;
# the upstream Chromium renderer sandbox remains enabled.
{
lib,
mkNixPak,
buildEnv,
makeDesktopItem,
runCommandLocal,
makeWrapper,
...
}:
{
package,
appId,
profile,
command,
desktopName,
desktopFile ? command,
browserpass ? false,
media ? false,
secretPortal ? false,
hostLoopback ? false,
personalFiles ? false,
privateWindowFlag ? "--incognito",
}:
let
# Chromium 156 disables PipeWire camera capture by default; media apps need
# the portal FD path instead of raw video devices.
# Keep the Wayland feature from nixpkgs: the last --enable-features wins.
enabledFeatures = lib.concatStringsSep "," (
[ "WaylandWindowDecorations" ]
++ lib.optional media "WebRtcPipeWireCamera"
++ lib.optional secretPortal "SecretPortalKeyProviderUseForEncryption"
);
usernsPackage =
runCommandLocal "${package.name}-userns"
{
nativeBuildInputs = [ makeWrapper ];
}
''
mkdir -p $out/bin
makeWrapper ${package}/bin/${command} $out/bin/${command} \
--add-flags "--enable-features=${enabledFeatures}"
'';
wrapped = mkNixPak {
config =
{ sloth, ... }:
{
imports = [
./browser-permissions.nix
./network.nix
];
app = {
package = usernsPackage;
binPath = "bin/${command}";
};
flatpak = { inherit appId; };
# Firefox/Chromium need host loopback for local services; Chrome and the rest use pasta.
pasta.enable = lib.mkForce (!hostLoopback);
pasta.mode = "isolate";
browserPermissions = {
inherit
browserpass
personalFiles
media
secretPortal
;
};
bubblewrap.bind.rw = [
(sloth.mkdir (sloth.concat' sloth.homeDir "/.config/${profile}"))
];
};
};
exe = "${wrapped.config.script}/bin/${command}";
# Copy desktop assets only; the full package would expose unwrapped binaries.
assets = runCommandLocal "${command}-desktop-assets" { } ''
mkdir -p $out/share
for directory in icons pixmaps; do
if [ -d ${package}/share/$directory ]; then
ln -s ${package}/share/$directory $out/share/$directory
fi
done
'';
in
buildEnv {
inherit (wrapped.config.script) name meta passthru;
paths = [
wrapped.config.script
assets
(makeDesktopItem {
name = appId;
inherit desktopName;
genericName = "Web Browser";
exec = "${exe} %U";
icon = desktopFile;
terminal = false;
startupNotify = true;
categories = [
"Network"
"WebBrowser"
];
mimeTypes = [
"text/html"
"application/xhtml+xml"
"x-scheme-handler/http"
"x-scheme-handler/https"
];
actions = {
new-window = {
name = "New Window";
exec = "${exe} --new-window %U";
};
new-private-window = {
name = "New Private Window";
exec = "${exe} ${privateWindowFlag} %U";
};
};
extraConfig.X-Flatpak = appId;
})
];
}
-228
View File
@@ -1,228 +0,0 @@
# Custom, repo-local policy for the sandboxed desktop apps (Firefox, QQ,
# Telegram). Unlike ./gui-base.nix, which forks nixpak/pkgs's gui-base and is
# meant to track upstream, this file is hand-written for this repository: it
# grants the D-Bus surface those apps need (portals, MPRIS, tray, input
# methods, a11y), redirects the XDG dirs Flatpak-style, and adds the remaining
# document/device mounts. Keep only app-specific mounts in the app files.
#
# Adapted from:
# - https://github.com/mnixry/nixos-config/blob/74913c2b90d06e31170bbbaa0074f915721da224/desktop/packages/nixpaks-common.nix
# - https://github.com/Kraftland/portable/blob/09c4a4227538a3f42de208a6ecbdc938ac9c00dd/portable.sh
# - https://flatpak.github.io/xdg-desktop-portal/docs/api-reference.html
{
lib,
sloth,
config,
...
}:
let
inherit (config.flatpak) appId;
in
{
config = {
# list all dbus services:
# ls -al /run/current-system/sw/share/dbus-1/services/
# ls -al /etc/profiles/per-user/ryan/share/dbus-1/services/
dbus = {
# `--see`: The bus name can be enumerated by the application.
# `--talk`: The application can send messages to, and receive replies and signals from, the bus name.
# `--own`: The application can own the bus name
policies = {
"${appId}" = "own";
"${appId}.*" = "own";
"org.freedesktop.DBus" = "talk";
"ca.desrt.dconf" = "talk";
"org.freedesktop.appearance" = "talk";
"org.freedesktop.appearance.*" = "talk";
}
// (builtins.listToAttrs (
map (id: lib.nameValuePair "org.kde.StatusNotifierItem-${toString id}-1" "own") (
lib.lists.range 2 29
)
))
// {
# --- MPRIS Media Control ---
# Allows the app to register as a media player. These are derived from the appID.
"org.mpris.MediaPlayer2.${appId}" = "own";
"org.mpris.MediaPlayer2.${appId}.*" = "own";
"org.mpris.MediaPlayer2.${lib.lists.last (lib.strings.splitString "." appId)}" = "own";
"org.mpris.MediaPlayer2.${lib.lists.last (lib.strings.splitString "." appId)}.*" = "own";
# --- General Desktop Integration ---
"com.canonical.AppMenu.Registrar" = "talk"; # For Ubuntu AppMenu
"org.freedesktop.FileManager1" = "talk";
"org.freedesktop.Notifications" = "talk";
"org.kde.StatusNotifierWatcher" = "talk";
"org.gnome.Shell.Screencast" = "talk";
# --- Accessibility (a11y) 无障碍服务 ---
"org.a11y.Bus" = "see";
# --- Portal Access ---
# "org.freedesktop.portal.*" = "talk";
"org.freedesktop.portal.Documents" = "talk";
"org.freedesktop.portal.FileTransfer" = "talk";
"org.freedesktop.portal.FileTransfer.*" = "talk";
"org.freedesktop.portal.Notification" = "talk";
"org.freedesktop.portal.OpenURI" = "talk";
"org.freedesktop.portal.OpenURI.OpenFile" = "talk";
"org.freedesktop.portal.OpenURI.OpenURI" = "talk";
"org.freedesktop.portal.Print" = "talk";
"org.freedesktop.portal.Request" = "see";
# --- Input Method Portals ---
"org.freedesktop.portal.Fcitx" = "talk";
"org.freedesktop.portal.Fcitx.*" = "talk";
"org.freedesktop.portal.IBus" = "talk";
"org.freedesktop.portal.IBus.*" = "talk";
};
# '--call' rules permit specific method calls on D-Bus interfaces.
rules.call = {
# --- Accessibility (a11y) 无障碍服务 ---
"org.a11y.Bus" = [
"org.a11y.Bus.GetAddress@/org/a11y/bus"
"org.freedesktop.DBus.Properties.Get@/org/a11y/bus"
];
# --- General Portal Rules ---
"org.freedesktop.FileManager1" = [ "*" ];
"org.freedesktop.Notifications.*" = [ "*" ];
"org.freedesktop.portal.Documents" = [ "*" ];
"org.freedesktop.portal.FileTransfer" = [ "*" ];
"org.freedesktop.portal.FileTransfer.*" = [ "*" ];
"org.freedesktop.portal.Fcitx" = [ "*" ];
"org.freedesktop.portal.Fcitx.*" = [ "*" ];
"org.freedesktop.portal.IBus" = [ "*" ];
"org.freedesktop.portal.IBus.*" = [ "*" ];
"org.freedesktop.portal.Notification" = [ "*" ];
"org.freedesktop.portal.OpenURI" = [ "*" ];
"org.freedesktop.portal.OpenURI.OpenFile" = [ "*" ];
"org.freedesktop.portal.OpenURI.OpenURI" = [ "*" ];
"org.freedesktop.portal.Print" = [ "*" ];
"org.freedesktop.portal.Request" = [ "*" ];
# --- Main Desktop Portal Interface ---
# A comprehensive list of permissions for interacting with the desktop environment.
"org.freedesktop.portal.Desktop" = [
# Properties & Settings
"org.freedesktop.DBus.Properties.GetAll"
"org.freedesktop.DBus.Properties.Get@/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Session.Close"
"org.freedesktop.portal.Settings.ReadAll"
"org.freedesktop.portal.Settings.Read"
"org.freedesktop.portal.Account.GetUserInformation"
# Network & Proxy
"org.freedesktop.portal.NetworkMonitor"
"org.freedesktop.portal.NetworkMonitor.*"
"org.freedesktop.portal.ProxyResolver.Lookup"
"org.freedesktop.portal.ProxyResolver.Lookup.*"
# Screenshot / Screen Capture & Sharing
"org.freedesktop.portal.ScreenCast"
"org.freedesktop.portal.ScreenCast.*"
"org.freedesktop.portal.Screenshot"
"org.freedesktop.portal.Screenshot.Screenshot"
# Device Access(Camera / USB)
"org.freedesktop.portal.Camera"
"org.freedesktop.portal.Camera.*"
"org.freedesktop.portal.Usb"
"org.freedesktop.portal.Usb.*"
# Remote Desktop
"org.freedesktop.portal.RemoteDesktop"
"org.freedesktop.portal.RemoteDesktop.*"
# File Operations
"org.freedesktop.portal.Documents"
"org.freedesktop.portal.Documents.*"
"org.freedesktop.portal.FileChooser"
"org.freedesktop.portal.FileChooser.*"
"org.freedesktop.portal.FileTransfer"
"org.freedesktop.portal.FileTransfer.*"
# Notifications & Printing
"org.freedesktop.portal.Notification"
"org.freedesktop.portal.Notification.*"
"org.freedesktop.portal.Print"
"org.freedesktop.portal.Print.*"
# Open/Launch Handlers
"org.freedesktop.portal.OpenURI"
"org.freedesktop.portal.OpenURI.*"
"org.freedesktop.portal.Email.ComposeEmail"
# Input Methods
"org.freedesktop.portal.Fcitx"
"org.freedesktop.portal.Fcitx.*"
"org.freedesktop.portal.IBus"
"org.freedesktop.portal.IBus.*"
# Secrets (Keyring)
"org.freedesktop.portal.Secret"
"org.freedesktop.portal.Secret.RetrieveSecret"
# Get/Update GlobalShortcuts
# "org.freedesktop.portal.GlobalShortcuts"
# "org.freedesktop.portal.GlobalShortcuts.*"
# -- get the user's location
# "org.freedesktop.portal.Location"
# "org.freedesktop.portal.Location.*"
# -- inhibit the user session from ending, suspending, idling or getting switched away.
"org.freedesktop.portal.Inhibit"
"org.freedesktop.portal.Inhibit.*"
# Generic Request Fallback
"org.freedesktop.portal.Request"
];
};
# 'broadcast' rules permit receiving signals from D-Bus names.
rules.broadcast = {
"org.freedesktop.portal.*" = [ "@/org/freedesktop/portal/*" ];
};
args = [
"--filter"
"--sloppy-names"
];
};
etc.sslCertificates.enable = true;
bubblewrap = {
network = lib.mkDefault true;
# Desktop-wide mounts (Wayland/at-spi/gvfsd, the app cache, fontconfig and
# Mesa caches, GTK config, /dev/shm) are provided by ./gui-base.nix, so
# each path is bound exactly once. This is what keeps a read-only bind
# from shadowing a writable one. Only app-specific mounts belong in this
# file.
bind.rw = with sloth; [
[
(mkdir appDataDir)
xdgDataHome
]
[
(mkdir appConfigDir)
xdgConfigHome
]
# FileChooser may return a Document Portal path even for a directly mapped XDG directory;
# keep the FUSE mount writable so SaveFile can create the selected file.
(sloth.concat' sloth.runtimeDir "/doc")
(sloth.concat' sloth.runtimeDir "/dconf")
(sloth.concat' sloth.xdgCacheHome "/mesa_shader_cache_db")
(sloth.concat' sloth.xdgCacheHome "/radv_builtin_shaders")
];
bind.ro = [
(sloth.concat' sloth.xdgConfigHome "/kdeglobals")
(sloth.concat' sloth.xdgConfigHome "/dconf")
];
bind.dev = map (id: "/dev/video${toString id}") (lib.lists.range 0 9);
};
};
}
+161
View File
@@ -0,0 +1,161 @@
# Shared desktop runtime; portal and native services are opt-in. No host GVfs,
# shared caches, raw camera or PipeWire socket.
{
config,
lib,
pkgs,
sloth,
...
}:
{
imports = [
./portal-groups.nix
./desktop-integration.nix
];
gpu = {
enable = lib.mkDefault true;
provider = "nixos";
bundlePackage = pkgs.mesa.drivers;
};
fonts = {
enable = true;
# Preserve nixpak's default coverage and add Chinese/Japanese/Korean glyphs.
fonts = with pkgs; [
cantarell-fonts
dejavu_fonts
liberation_ttf
gyre-fonts
source-sans
source-code-pro
noto-fonts-color-emoji
noto-fonts-cjk-sans
];
};
locale.enable = true;
dbus = {
policies = {
"${config.flatpak.appId}" = "own";
"${config.flatpak.appId}.*" = "own";
};
rules.call."org.freedesktop.portal.Desktop" = [
"org.freedesktop.DBus.Properties.Get@/org/freedesktop/portal/desktop"
"org.freedesktop.DBus.Properties.GetAll@/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Settings.Read@/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Settings.ReadAll@/org/freedesktop/portal/desktop"
"org.freedesktop.portal.Request.Close@/org/freedesktop/portal/desktop/request/*"
"org.freedesktop.portal.Session.Close@/org/freedesktop/portal/desktop/session/*"
];
rules.broadcast."org.freedesktop.portal.Desktop" = [
"org.freedesktop.portal.Settings.SettingChanged@/org/freedesktop/portal/desktop"
"@/org/freedesktop/portal/desktop/*"
];
};
bubblewrap = {
network = lib.mkDefault false;
newSession = true;
dieWithParent = true;
clearEnv = true;
sockets = {
wayland = true;
x11 = lib.mkDefault false;
pulse = lib.mkDefault true;
pipewire = false;
};
bind.rw = with sloth; [
[
(mkdir appCacheDir)
xdgCacheHome
]
[
(mkdir appConfigDir)
xdgConfigHome
]
[
(mkdir appDataDir)
xdgDataHome
]
[
(mkdir (concat' appDir "/state"))
xdgStateHome
]
(concat' runtimeDir "/doc")
# Chrome/Firefox/Telegram/etc. use /tmp for single-instance IPC. Share
# this directory across launches of the same app only; app IDs differ.
[
(mkdir (concat' runtimeDir "/nixpak-${config.flatpak.appId}-tmp"))
"/tmp"
]
];
bind.ro = [
"/etc/localtime"
"/etc/zoneinfo"
(sloth.concat' sloth.xdgConfigHome "/gtk-2.0")
(sloth.concat' sloth.xdgConfigHome "/gtk-3.0")
(sloth.concat' sloth.xdgConfigHome "/gtk-4.0")
(sloth.concat' sloth.xdgConfigHome "/kdeglobals")
"/sys/devices/platform"
]
++ lib.optionals config.gpu.enable [
"/etc/egl"
"/etc/static/egl"
];
# nixpak's NixOS GPU provider maps DRM nodes, but NVIDIA also needs its
# dedicated interfaces. Missing nodes are skipped by --dev-bind-try.
bind.dev = lib.optionals config.gpu.enable [
"/dev/nvidia0"
"/dev/nvidiactl"
"/dev/nvidia-modeset"
"/dev/nvidia-uvm"
];
tmpfs = [
"/dev/shm"
];
env = {
HOME = sloth.homeDir;
USER = sloth.env "USER";
XDG_RUNTIME_DIR = sloth.runtimeDir;
XDG_CACHE_HOME = sloth.xdgCacheHome;
XDG_CONFIG_HOME = sloth.xdgConfigHome;
XDG_DATA_HOME = sloth.xdgDataHome;
XDG_STATE_HOME = sloth.xdgStateHome;
XDG_SESSION_TYPE = "wayland";
# Selects the Wayland path for portal clients.
XDG_CURRENT_DESKTOP = sloth.envOr "XDG_CURRENT_DESKTOP" "niri";
WAYLAND_DISPLAY = sloth.envOr "WAYLAND_DISPLAY" "wayland-0";
NIXOS_OZONE_WL = "1";
GTK_USE_PORTAL = "1";
GTK_A11Y = sloth.envOr "GTK_A11Y" "0";
XDG_DATA_DIRS = lib.makeSearchPath "share" [
pkgs.papirus-icon-theme
pkgs.bibata-cursors
pkgs.shared-mime-info
];
XCURSOR_PATH = "${pkgs.bibata-cursors}/share/icons:${pkgs.bibata-cursors}/share/pixmaps";
# Fontconfig configuration mounted by nixpak fonts.enable.
FONTCONFIG_FILE = "/etc/fonts/fonts.conf";
NIXOS_XDG_OPEN_USE_PORTAL = "1";
}
// lib.genAttrs [
"LANG"
"LANGUAGE"
"LC_ALL"
"LC_ADDRESS"
"LC_COLLATE"
"LC_CTYPE"
"LC_IDENTIFICATION"
"LC_MEASUREMENT"
"LC_MESSAGES"
"LC_MONETARY"
"LC_NAME"
"LC_NUMERIC"
"LC_PAPER"
"LC_TELEPHONE"
"LC_TIME"
"XCURSOR_THEME"
"XCURSOR_SIZE"
"NO_AT_BRIDGE"
"QT_QPA_PLATFORMTHEME"
"QT_WAYLAND_DISABLE_WINDOWDECORATION"
] (name: sloth.envOr name "");
};
}
@@ -0,0 +1,67 @@
# Non-portal desktop services are separate opt-ins from portal capabilities.
{
config,
lib,
sloth,
...
}:
let
cfg = config.desktopIntegration;
appId = config.flatpak.appId;
shortName = lib.last (lib.splitString "." appId);
in
{
options.desktopIntegration = {
notifications = lib.mkEnableOption "native desktop notifications";
tray = lib.mkEnableOption "StatusNotifier tray integration";
mpris = lib.mkEnableOption "MPRIS media controls for this application";
accessibility = lib.mkEnableOption "the accessibility bus and AT-SPI socket";
};
config = lib.mkMerge [
(lib.mkIf cfg.notifications {
dbus.policies."org.freedesktop.Notifications" = "talk";
})
(lib.mkIf cfg.tray {
dbus.policies = {
# Qt probes NameHasOwner/GetNameOwner before registering its SNI.
"org.freedesktop.DBus" = "talk";
"org.kde.StatusNotifierWatcher" = "talk";
}
# SNI name is <pid>-<Qt tray instance ID>; WeChat 4.1.13 uses <pid>-2.
// builtins.listToAttrs (
lib.concatMap (
id:
map (
instance: lib.nameValuePair "org.kde.StatusNotifierItem-${toString id}-${toString instance}" "own"
) (lib.range 1 5)
) (lib.range 2 5)
);
})
(lib.mkIf cfg.mpris {
# Chromium's MPRIS name is lowercase even though its app ID ends in
# Chromium; retain the full ID and toolkit spelling as well.
dbus.policies = builtins.listToAttrs (
lib.concatMap
(name: [
(lib.nameValuePair "org.mpris.MediaPlayer2.${name}" "own")
(lib.nameValuePair "org.mpris.MediaPlayer2.${name}.*" "own")
])
(
lib.unique [
appId
shortName
(lib.toLower shortName)
]
)
);
})
(lib.mkIf cfg.accessibility {
dbus.policies."org.a11y.Bus" = "see";
dbus.rules.call."org.a11y.Bus" = [
"org.a11y.Bus.GetAddress@/org/a11y/bus"
"org.freedesktop.DBus.Properties.Get@/org/a11y/bus"
];
bubblewrap.bind.ro = [ (sloth.concat' sloth.runtimeDir "/at-spi/bus") ];
})
];
}
-173
View File
@@ -1,173 +0,0 @@
# Fork of nixpak/pkgs's gui-base.nix: the shared GUI runtime plumbing (GPU,
# fonts, desktop caches, /dev/shm, and the audio/display sockets) that lets a
# GUI app run under bubblewrap. Paths bound here must not be bound again in
# ./custom-policy.nix or the app configs: nixpak emits read-only binds last, so
# a duplicate read-only bind would silently shadow a writable one.
#
# https://github.com/nixpak/pkgs/blob/master/pkgs/modules/gui-base.nix
{
config,
lib,
pkgs,
sloth,
...
}:
let
envSuffix = envKey: suffix: sloth.concat' (sloth.env envKey) suffix;
# nixpak cannot pass a variable only when it is set, so an unset one arrives
# empty; only list variables whose readers treat empty as unset.
passHostEnv = names: lib.genAttrs names (name: sloth.envOr name "");
# cursor & icon's theme should be the same as the host's one.
cursorTheme = pkgs.bibata-cursors;
iconTheme = pkgs.papirus-icon-theme;
in
{
config = {
dbus.policies = {
"${config.flatpak.appId}" = "own";
# we add other policies in ./custom-policy.nix
};
# https://github.com/nixpak/nixpak/blob/master/modules/gpu.nix
# The NixOS provider exposes host drivers at /run/opengl-driver (read-only)
# and all /dev/dri nodes (device access), including Intel, AMD and Asahi DRM devices.
# Nixpak also maps /sys/dev/char and /sys/devices/pci0000:00 read-only for discovery.
# These mappings grant access; they do not select the GPU used by an application.
gpu = {
enable = lib.mkDefault true;
provider = "nixos";
bundlePackage = pkgs.mesa.drivers;
};
# https://github.com/nixpak/nixpak/blob/master/modules/gui/fonts.nix
# it works not well, bind system's /etc/fonts directly instead
fonts.enable = false;
# https://github.com/nixpak/nixpak/blob/master/modules/locale.nix
locale.enable = true;
bubblewrap = {
# Nixpak uses --ro-bind-try / --dev-bind-try: absent host paths are skipped,
# so shared mappings work on Intel/NVIDIA hosts and Apple Silicon alike.
network = lib.mkDefault false;
# setsid keeps the app off the launching terminal (TIOCSTI), and the
# sandbox goes down with the launcher instead of outliving it.
newSession = true;
dieWithParent = true;
# nixpak binds sockets read-only, which is sufficient: connect() only needs
# the socket inode's write bit, and the kernel's read-only-filesystem check
# does not apply to sockets. Every app here is Wayland-only, so X11 stays
# disabled (the nixpak default, spelled out for intent) and PipeWire is
# shared for screensharing.
sockets = {
wayland = true;
pipewire = true;
pulse = true;
x11 = false;
};
bind.rw = [
[
(envSuffix "HOME" "/.var/app/${config.flatpak.appId}/cache")
sloth.xdgCacheHome
]
(sloth.concat' sloth.xdgCacheHome "/fontconfig")
(sloth.concat' sloth.xdgCacheHome "/mesa_shader_cache")
# gvfsd is a directory that holds FUSE mounts, not a socket, so it must
# stay writable.
(envSuffix "XDG_RUNTIME_DIR" "/gvfsd")
];
bind.ro = [
# Sockets only need to be connectable, and connect() works on a read-only
# bind (see the note on `sockets` above).
(envSuffix "XDG_RUNTIME_DIR" "/at-spi/bus")
(sloth.concat' sloth.xdgConfigHome "/gtk-2.0")
(sloth.concat' sloth.xdgConfigHome "/gtk-3.0")
(sloth.concat' sloth.xdgConfigHome "/gtk-4.0")
(sloth.concat' sloth.xdgConfigHome "/fontconfig")
"/etc/fonts" # for fontconfig
"/etc/localtime" # this is a symlink to /etc/zoneinfo/xxx
"/etc/zoneinfo"
# Host EGL configuration (read-only); needed by some driver setups.
"/etc/egl"
"/etc/static/egl"
# Asahi GPU discovery follows /sys/dev/char links into platform devices.
# Expose their targets read-only; this includes non-GPU platform metadata too.
"/sys/devices/platform"
];
bind.dev = [
"/dev/shm" # Shared Memory
# Additional NVIDIA interfaces, retained for primary-GPU and offload use.
# Unlike /dev/dri above, numbered NVIDIA nodes are not mapped as a directory.
"/dev/nvidia0" # GPU index 0; additional GPUs need explicit mappings.
"/dev/nvidiactl" # Driver control interface.
"/dev/nvidia-modeset" # Display mode setting.
"/dev/nvidia-uvm" # Unified virtual memory.
];
tmpfs = [
"/tmp"
];
# Start from an empty environment so host secrets (tokens, credentials)
# stay outside; everything the apps need is listed here.
clearEnv = true;
env = {
HOME = sloth.homeDir;
USER = sloth.env "USER";
XDG_RUNTIME_DIR = sloth.runtimeDir;
XDG_CACHE_HOME = sloth.xdgCacheHome;
XDG_CONFIG_HOME = sloth.xdgConfigHome;
XDG_DATA_HOME = sloth.xdgDataHome;
XDG_STATE_HOME = sloth.xdgStateHome;
# Same fallback as nixpak's Wayland socket bind, so they always agree.
WAYLAND_DISPLAY = sloth.envOr "WAYLAND_DISPLAY" "wayland-0";
XDG_SESSION_TYPE = "wayland";
# GTK reads an empty GTK_A11Y as an unknown backend; "0" is its unset value.
GTK_A11Y = sloth.envOr "GTK_A11Y" "0";
XDG_DATA_DIRS = lib.mkForce (
lib.makeSearchPath "share" [
iconTheme
cursorTheme
pkgs.shared-mime-info
]
);
XCURSOR_PATH = lib.mkForce (
lib.concatStringsSep ":" [
"${cursorTheme}/share/icons"
"${cursorTheme}/share/pixmaps"
]
);
}
// passHostEnv [
"LANG"
"LANGUAGE"
"LC_ALL"
"LC_ADDRESS"
"LC_COLLATE"
"LC_CTYPE"
"LC_IDENTIFICATION"
"LC_MEASUREMENT"
"LC_MESSAGES"
"LC_MONETARY"
"LC_NAME"
"LC_NUMERIC"
"LC_PAPER"
"LC_TELEPHONE"
"LC_TIME"
"NO_AT_BRIDGE"
"NIXOS_XDG_OPEN_USE_PORTAL"
"NIXOS_OZONE_WL" # QQ's wrapper adds its Wayland flags only when set
"XCURSOR_THEME"
"XCURSOR_SIZE"
"QT_QPA_PLATFORMTHEME"
"QT_WAYLAND_DISABLE_WINDOWDECORATION"
];
};
};
}
@@ -0,0 +1,62 @@
# xdg-dbus-proxy rules target interfaces on the Desktop bus name, not fictitious
# per-interface bus names. Each capability is opt-in.
{ config, lib, ... }:
let
groups = {
fileChooser = [
"FileChooser.OpenFile"
"FileChooser.SaveFile"
"FileChooser.SaveFiles"
];
openUri = [
"OpenURI.OpenURI"
"OpenURI.OpenFile"
"OpenURI.OpenDirectory"
];
notifications = [
"Notification.AddNotification"
"Notification.RemoveNotification"
];
screenCast = [
"ScreenCast.CreateSession"
"ScreenCast.SelectSources"
"ScreenCast.Start"
"ScreenCast.OpenPipeWireRemote"
];
camera = [
"Camera.AccessCamera"
"Camera.OpenPipeWireRemote"
];
print = [
"Print.PreparePrint"
"Print.Print"
];
secret = [ "Secret.RetrieveSecret" ];
inhibit = [
"Inhibit.Inhibit"
"Inhibit.CreateMonitor"
"Inhibit.QueryEndResponse"
];
};
in
{
options.portalGroups =
lib.mapAttrs (name: _: lib.mkEnableOption "the ${name} desktop portal methods") groups
// {
inputMethod = lib.mkEnableOption "Fcitx and IBus input method portals";
};
config = {
dbus.rules.call."org.freedesktop.portal.Desktop" = lib.concatLists (
lib.mapAttrsToList (
name: methods:
lib.optionals config.portalGroups.${name} (
map (method: "org.freedesktop.portal.${method}@/org/freedesktop/portal/desktop") methods
)
) groups
);
dbus.policies = lib.mkIf config.portalGroups.inputMethod {
"org.freedesktop.portal.Fcitx" = "talk";
"org.freedesktop.portal.IBus" = "talk";
};
};
}
@@ -0,0 +1,54 @@
# The existing FHS layer owns WeChat's HOME remapping and xwechat_files link.
{
config,
lib,
pkgs,
sloth,
...
}:
{
imports = [
./desktop-base.nix
./network.nix
];
pasta.mode = "isolate";
desktopIntegration.tray = true;
portalGroups = {
fileChooser = true;
openUri = true;
notifications = true;
screenCast = true;
camera = true;
inputMethod = true;
};
bubblewrap = {
sockets.x11 = true;
# libdbus aborts without /etc/machine-id; use a synthetic app-scoped value.
bind.ro = [
[
(toString (
pkgs.writeText "wechat-machine-id" "${
builtins.substring 0 32 (builtins.hashString "sha256" config.flatpak.appId)
}\n"
))
"/etc/machine-id"
]
];
# The inner FHS /home reset replaces base XDG mounts with this backing tree.
bind.rw = lib.mkForce [
(sloth.mkdir (sloth.concat' sloth.xdgDocumentsDir "/WeChat_Data"))
(sloth.concat' sloth.runtimeDir "/doc")
[
(sloth.mkdir (sloth.concat' sloth.runtimeDir "/nixpak-${config.flatpak.appId}-tmp"))
"/tmp"
]
];
env = {
DISPLAY = sloth.envOr "DISPLAY" "";
XAUTHORITY = sloth.envOr "XAUTHORITY" (sloth.concat' sloth.homeDir "/.Xauthority");
XDG_DOCUMENTS_DIR = sloth.xdgDocumentsDir;
# The existing pre-FHS script uses mkdir/ln before /usr/bin exists.
PATH = lib.makeBinPath [ pkgs.coreutils ];
};
};
}
+42
View File
@@ -0,0 +1,42 @@
# Zoom keeps the upstream FHS runtime inside the outer nixpak boundary.
{ lib, sloth, ... }:
{
imports = [
./desktop-base.nix
./network.nix
];
pasta.mode = "isolate";
desktopIntegration.tray = true;
portalGroups = {
fileChooser = true;
openUri = true;
notifications = true;
screenCast = true;
camera = true;
inhibit = true;
inputMethod = true;
};
bubblewrap = {
# Screen sharing enumerates DRM connectors through sysfs. Missing this
# directory makes Zoom abort with std::filesystem_error.
bind.ro = [ "/sys/class/drm" ];
# ZoomLauncher selects xcb; the X11 connection retains X11's display trust.
sockets.x11 = true;
env = {
DISPLAY = sloth.envOr "DISPLAY" "";
XAUTHORITY = sloth.envOr "XAUTHORITY" (sloth.concat' sloth.homeDir "/.Xauthority");
};
bind.dev = [
# Zoom 7.2.1 still opens V4L2 device nodes directly for webcam capture.
"/dev/video0"
"/dev/video1"
];
bind.rw = [
(sloth.mkdir (sloth.concat' sloth.homeDir "/.zoom"))
(sloth.concat' sloth.xdgConfigHome "/zoomus.conf")
(sloth.concat' sloth.xdgConfigHome "/zoom.conf")
(sloth.mkdir (sloth.concat' sloth.xdgDocumentsDir "/Zoom"))
sloth.xdgDownloadDir
];
};
}
+17 -2
View File
@@ -26,11 +26,26 @@ let
flatpak.appId = appId;
imports = [
./modules/gui-base.nix
./modules/desktop-base.nix
./modules/network.nix
./modules/custom-policy.nix
];
portalGroups = {
fileChooser = true;
openUri = true;
notifications = true;
screenCast = true;
camera = true;
inhibit = true;
inputMethod = true;
};
desktopIntegration = {
notifications = true;
tray = true;
mpris = true;
accessibility = true;
};
bubblewrap = {
# To trace all the home files QQ accesses, you can use the following nushell command:
# just trace-access qq
+17 -2
View File
@@ -13,9 +13,8 @@ let
{ sloth, ... }:
{
imports = [
./modules/gui-base.nix
./modules/desktop-base.nix
./modules/network.nix
./modules/custom-policy.nix
];
app.package = telegram-desktop;
flatpak = {
@@ -30,6 +29,22 @@ let
};
};
portalGroups = {
fileChooser = true;
openUri = true;
notifications = true;
screenCast = true;
camera = true;
inhibit = true;
inputMethod = true;
};
desktopIntegration = {
notifications = true;
tray = true;
mpris = true;
accessibility = true;
};
bubblewrap = {
# nixpak exposes /.flatpak-info, so Telegram uses FLATPAK_ID as its Wayland app ID.
env.FLATPAK_ID = appId;
+72
View File
@@ -0,0 +1,72 @@
{
lib,
bwraps,
mkNixPak,
buildEnv,
makeDesktopItem,
runCommandLocal,
...
}:
let
appId = "com.tencent.WeChat";
# As with Zoom, preserve the outer metadata file across the inner FHS layer.
# Keep the existing AppImage runtime and WeChat_Data layout unchanged.
package = bwraps.wechat.overrideAttrs (old: {
# Refuse unexpected layouts before the existing ln -snf can modify them.
extraPreBwrapCmds = ''
wechat_link="''${XDG_DOCUMENTS_DIR}/WeChat_Data/home/xwechat_files"
wechat_files="''${XDG_DOCUMENTS_DIR}/WeChat_Data/xwechat_files"
if [[ -e "$wechat_link" || -L "$wechat_link" ]]; then
if [[ ! -L "$wechat_link" || "$(readlink "$wechat_link")" != "$wechat_files" ]]; then
echo 'WeChat: unexpected xwechat_files layout; refusing to change existing data.' >&2
exit 1
fi
fi
''
+ (old.extraPreBwrapCmds or "");
extraBwrapArgs = (old.extraBwrapArgs or [ ]) ++ [
"--ro-bind /.flatpak-info /.flatpak-info"
# The inner /home reset must not hide an Xauthority file stored there.
''--ro-bind-try "''${XAUTHORITY}" "''${XAUTHORITY}"''
];
});
wrapped = mkNixPak {
config = {
imports = [ ./modules/wechat-policy.nix ];
app = {
inherit package;
binPath = "bin/wechat";
};
flatpak = { inherit appId; };
};
};
exe = lib.getExe wrapped.config.script;
assets = runCommandLocal "wechat-desktop-assets" { } ''
mkdir -p $out/share
ln -s ${package}/share/pixmaps $out/share/pixmaps
'';
in
buildEnv {
inherit (wrapped.config.script) name passthru;
meta = wrapped.config.script.meta // {
mainProgram = "wechat";
};
paths = [
wrapped.config.script
assets
(makeDesktopItem {
name = "wechat";
desktopName = "WeChat";
exec = "${exe} %U";
icon = "wechat";
terminal = false;
startupWMClass = "wechat";
categories = [
"Network"
"InstantMessaging"
];
mimeTypes = [ "x-scheme-handler/weixin" ];
extraConfig.X-Flatpak = appId;
})
];
}
+70
View File
@@ -0,0 +1,70 @@
{
lib,
zoom-us,
buildFHSEnv,
mkNixPak,
buildEnv,
makeDesktopItem,
runCommandLocal,
...
}:
let
appId = "us.zoom.Zoom";
# Preserve the upstream FHS wrapper and /opt mount; its root mounts omit the
# metadata file needed for portal identification inside the nested sandbox.
package = zoom-us.override {
xdgDesktopPortalSupport = true;
buildFHSEnv =
args:
buildFHSEnv (
args
// {
extraBwrapArgs = (args.extraBwrapArgs or [ ]) ++ [
"--ro-bind /.flatpak-info /.flatpak-info"
];
}
);
};
wrapped = mkNixPak {
config = {
imports = [ ./modules/zoom-policy.nix ];
app = {
inherit package;
binPath = "bin/zoom";
};
flatpak = { inherit appId; };
};
};
exe = lib.getExe wrapped.config.script;
assets = runCommandLocal "zoom-desktop-assets" { } ''
mkdir -p $out/share
for directory in icons pixmaps; do
if [ -d ${package}/share/$directory ]; then
ln -s ${package}/share/$directory $out/share/$directory
fi
done
'';
in
buildEnv {
inherit (wrapped.config.script) name meta passthru;
paths = [
wrapped.config.script
assets
(makeDesktopItem {
name = "Zoom";
desktopName = "Zoom Workplace";
comment = "Zoom Video Conference";
exec = "${exe} %U";
icon = "Zoom";
terminal = false;
startupWMClass = "zoom";
categories = [ "Network" ];
mimeTypes = [
"x-scheme-handler/zoommtg"
"x-scheme-handler/zoomus"
"application/x-zoom"
];
extraConfig.X-Flatpak = appId;
})
];
}