docs: condense comments across the repo

Comments had drifted into multi-paragraph narrative (git commit
lineage, debugging stories, restated code) in several hot spots
(scripts/deploy, hermes-agent.nix, flake.nix, gitea.nix, headscale.nix).
Trim every comment to its load-bearing "why" — gotchas, safety
warnings, and non-obvious rationale survive verbatim in substance,
just tightened to 1-2 sentences; historical narrative and anything
already covered in CLAUDE.md is cut. No code/logic changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJqEmY1y3AYX3JoX4Y6b21
This commit is contained in:
2026-09-18 21:36:30 +02:00
co-authored by Claude Sonnet 5
parent 3899290c5b
commit 6f24ab69ad
47 changed files with 1051 additions and 1965 deletions
+34 -75
View File
@@ -44,17 +44,9 @@ in
# https://nix.dev/permalink/stub-ld ----
programs.nix-ld.enable = true;
# The default set above is deliberately minimal and carries no X11,
# freetype, wayland or xkbcommon, so a prebuilt *graphical* binary dies
# before it draws anything. JetBrains IDEs installed through Toolbox are the
# case that surfaced this: their bundled JBR aborts with `libX11.so.6:
# cannot open shared object file` unless the Toolbox GUI — itself an FHS
# wrapper — is what launches them, which makes them unusable from a terminal
# or from a per-repo devShell. These are the libraries `ldd` reports missing
# across a JBR's own .so files, plus the three it resolves by dlopen rather
# than DT_NEEDED: fontconfig for font discovery, libGL, and libsecret for
# the credential store. Definitions merge, so this adds to the module's base
# list rather than replacing it (zlib is already there).
# JetBrains IDEs installed via Toolbox bundle a JBR that aborts with
# `libX11.so.6: cannot open shared object file` under the default (X11-less)
# nix-ld set. Additive — merges with the module's own base list (zlib etc).
programs.nix-ld.libraries = with pkgs; [
freetype
fontconfig
@@ -72,28 +64,19 @@ in
libxinerama
libxcb
# CLion Nova's C++ backend (the clion-radler plugin) is a .NET 10
# application bundling its own runtime, and .NET refuses to start
# without ICU: libSystem.Globalization.Native.so dlopens libicuuc.so
# and libicui18n.so, and failing that the IDE reports "Couldn't find a
# valid ICU package installed on the system" and comes up degraded.
# CLion Nova's C++ backend is a .NET 10 app that needs ICU or reports
# "Couldn't find a valid ICU package installed on the system".
icu
];
# ---- envfs: serves /bin and /usr/bin from the calling process's PATH ----
# NixOS ships only /bin/sh, but plenty of third-party tooling writes scripts
# with a hardcoded interpreter. JetBrains Toolbox is the standing example:
# it generates ~/.local/share/JetBrains/Toolbox/scripts/{clion,rider,...}
# with `#!/bin/bash`, so every one of those shims fails with `bad
# interpreter` in any shell. envfs resolves such shebangs against PATH,
# which fixes them all at once instead of per-IDE wrappers.
# NixOS only ships /bin/sh; envfs serves /bin and /usr/bin from PATH so
# third-party scripts hardcoding `#!/bin/bash` (e.g. JetBrains Toolbox's
# generated launchers) still resolve.
services.envfs.enable = true;
# ---- home-manager (user-level config for darman) ----
# Base settings (useGlobalPkgs/useUserPackages/backupFileExtension) and the
# shared zsh baseline now live in common.nix + home/common.nix, applied to
# every host. This just layers terra's desktop/dev-specific profile on top
# — home-manager.users.darman.imports merges additively across modules.
# Base settings + shared zsh baseline live in common.nix + home/common.nix
# (every host); this layers terra's desktop profile on top (imports merge).
home-manager.extraSpecialArgs = { inherit unstable inputs; };
home-manager.users.darman.imports = [ ./home.nix ];
@@ -102,71 +85,47 @@ in
boot.loader.efi.canTouchEfiVariables = true;
hardware.cpu.amd.updateMicrocode = true;
# mercury (aarch64) is built/flashed from here. Without this, `nix build`
# for it dies with "platform mismatch" — no qemu binfmt handler registered
# and aarch64-linux missing from nix.settings.extra-platforms. This module
# sets up both (see CLAUDE.md's aarch64 gotcha).
# Lets `nix build` target mercury (aarch64) from here — see CLAUDE.md's
# aarch64 gotcha.
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
# ---- GPU (Radeon RX 6800 XT / Navi 21) ----
hardware.enableRedistributableFirmware = true;
boot.initrd.kernelModules = [ "amdgpu" ];
# /dev/dri/renderD128 is root:render 0660, so rootless podman containers can
# only reach the GPU if the *host* user is in render. Needed by the Vulkan
# whisper.cpp/llama.cpp containers in ~/Data/Dev/repos/content-trigger-scanner.
# /dev/dri/renderD128 is root:render 0660 — host user needs render group for
# rootless podman GPU containers (Vulkan whisper.cpp/llama.cpp).
users.users.darman.extraGroups = [ "render" "video" ];
# ---- ollama (local LLM server, ROCm on the 6800 XT) ----
# Navi 21 is gfx1030 officially supported by ROCm, so no
# rocmOverrideGfx/HSA_OVERRIDE_GFX_VERSION needed (that's for gpus ROCm
# doesn't recognize, e.g. RDNA1/gfx101x). The upstream module runs the
# service under DynamicUser with SupplementaryGroups=["render"] and
# DeviceAllow for char-kfd/char-drm/char-fb already, so unlike jellyfin's
# static user it needs no extraGroups wiring here.
# Navi 21 (gfx1030) is officially ROCm-supported, so no
# HSA_OVERRIDE_GFX_VERSION needed. Upstream module already runs under
# DynamicUser with render/kfd/drm access wired, unlike jellyfin's static user.
services.ollama = {
enable = true;
package = pkgs.ollama-rocm;
# keep in sync with services/desktop/librechat.nix's endpoints.custom
# default model — LibreChat's config schema needs a non-empty default
# even though fetch=true replaces it with whatever's actually pulled.
# gemma4:12b: general chat/coding daily driver, fits fully in 16G VRAM
# also doubles as the memory-extraction agent (see librechat.nix): a
# 3b model (llama3.2:3b, dropped) couldn't reliably tell the user's
# stated facts apart from its own boilerplate, e.g. saving "I am an AI
# assistant with tool calling capabilities" as the user's personal_info
# after "Hi I'm Erik Simon". Reusing gemma4:12b for both roles also means
# no second model needs to swap into VRAM while it's already the active
# chat model.
# qwen3.6:35b-a3b: MoE (3B active/36B total), ~24GB Q4_K_M — doesn't fit
# in VRAM alone, so ollama offloads the inactive experts to CPU RAM.
# Sparse activation makes that far less painful than it'd be for a dense
# model this size, but still expect it to run slower than the two above.
# VladimirGav/qwen3.8-27B-14GB-IQ4: dense 27B at IQ4, ~14GB of weights —
# nominally fits the 6800 XT's 16G, but that leaves only ~2G for the KV
# cache and the compositor, so expect partial CPU offload as context grows
# (OLLAMA_CONTEXT_LENGTH below applies to every model on this server).
# keep default model in sync with services/desktop/librechat.nix's
# endpoints.custom default (its schema needs a non-empty value even
# though fetch=true overrides it).
# gemma4:12b: daily-driver chat/coding model, fits fully in 16G VRAM; also
# doubles as LibreChat's memory-extraction agent (librechat.nix) since a
# smaller model confused the user's stated facts with its own boilerplate.
# qwen3.6:35b-a3b: MoE (3B active/36B total, ~24GB Q4_K_M) — doesn't fit
# in VRAM alone, so ollama offloads inactive experts to CPU RAM; sparsity
# makes that less painful than for a dense model this size, but still slower.
# VladimirGav/qwen3.8-27B-14GB-IQ4: dense 27B at IQ4 (~14GB) — nominally
# fits the 16G card but leaves little headroom, so expect partial CPU
# offload as context grows.
loadModels = [
"gemma4:12b"
"qwen3.6:35b-a3b"
"VladimirGav/qwen3.8-27B-14GB-IQ4"
];
# Ollama truncates context far below the model's real window unless
# told otherwise (the OpenAI-compat /v1 route it's reached through has
# no way to set this per-request). 131072 chosen as the practical
# ceiling after load-testing with real prompts, not just idle
# `ollama ps` checks:
# 32768 (31.6k-token prompt) and 65536 (40.8k-token prompt) both stayed
# 100% GPU with VRAM barely moving (~10.1G / ~10.67G of 16G) — KV cache
# cost barely grows with context, likely sliding-window/local attention
# on most of gemma4:12b's layers. At 131072 that stopped being true: a
# ~108k-token prompt pushed VRAM to ~11.4G/16G (still 100% GPU, no CPU
# spillover, negligible GTT) but with visibly shrinking headroom, and
# prefill throughput measurably dropped (~490 -> ~460 tok/s) over just
# the last 13k tokens — filling the full window would take minutes of
# pure prompt processing. Stopped here rather than push further: next
# doubling would risk CPU spillover under any concurrent GPU load
# (desktop compositor, jellyfin transcode) for diminishing benefit.
# Ollama truncates context far below a model's real window unless told
# otherwise. 131072 is the practical ceiling from load-testing: VRAM stays
# 100% GPU with no CPU spillover up to here, but headroom and prefill
# throughput both degrade near the top — going higher risks CPU spillover
# under concurrent GPU load (compositor, jellyfin transcode) for little gain.
environmentVariables.OLLAMA_CONTEXT_LENGTH = "131072";
};
+9 -23
View File
@@ -5,27 +5,14 @@
# `fileSystems.*` entries, so hardware-configuration.nix must NOT define
# fileSystems for "/" or "/boot".
#
# ⚠️ disko's `mkfs` create step SKIPS formatting when `blkid` still detects a
# filesystem signature on the freshly-cut partition:
#
# if ! (blkid "$device" -o export | grep -q '^TYPE='); then
# mkfs.btrfs "$device" -f # ← -f only runs WHEN this line runs
# fi
#
# The disk previously held a CachyOS btrfs root. The whole-disk `wipefs`
# disko runs before partitioning clears the signature at the OLD layout's
# offsets, but `sgdisk --clear --align-end` then re-cuts the partitions, so
# a stale btrfs superblock survives at the NEW root partition's own 64 KiB
# offset. `blkid` sees TYPE=btrfs, `mkfs` is skipped entirely, and the
# later `mount` fails on the leftover bytes ("wrong fs type / bad
# superblock"). Switching ext4→btrfs did NOT fix this: `mkfs.btrfs -f` is
# never reached, because the guard is on whether `mkfs` runs at all, not on
# its flags. The ESP hits the same trap (its `mkfs.vfat` gets skipped too).
#
# Fix: `preCreateHook = wipefs --all --force "$device"` on each partition's
# content. The hook runs AFTER sgdisk re-cuts the partition but BEFORE the
# `blkid` guard, so it erases the stale signature at the FINAL offset;
# `blkid` then comes back empty and `mkfs` actually runs.
# ⚠️ disko's `mkfs` step skips formatting if `blkid` still detects a
# filesystem signature on the partition. Repartitioning doesn't erase
# signatures at the new offsets, so this disk's old CachyOS btrfs
# superblock survived, causing mkfs (and the ESP's mkfs.vfat) to be
# skipped and the later mount to fail on the stale superblock.
# Fix: `preCreateHook = wipefs --all --force "$device"` on each
# partition — it runs after sgdisk re-cuts the partition but before the
# `blkid` guard, so the guard sees no signature and `mkfs` actually runs.
#
# ⚠️ This disk is WIPED on install. This is the Kingston SA400 SSD that
# currently holds CachyOS (btrfs root+subvols on sdb2, ESP on sdb1).
@@ -58,8 +45,7 @@
type = "btrfs";
extraArgs = [ "-f" ];
mountpoint = "/";
# erase the stale CachyOS btrfs superblock before disko's blkid
# format-guard, otherwise mkfs.btrfs is skipped (see header comment)
# same wipefs fix as the ESP above (see header comment)
preCreateHook = ''wipefs --all --force "$device"'';
};
};
+12 -20
View File
@@ -2,23 +2,17 @@
let
tome = pkgs.callPackage ../../pkgs/tome.nix { src = inputs.tome; };
# SUDO_ASKPASS helper: renders sudo's password prompt in the quickshell
# shell (HyprChrome/Widgets/Askpass) instead of on the terminal.
# SUDO_ASKPASS helper: shows sudo's password prompt in quickshell
# (HyprChrome/Widgets/Askpass) instead of the terminal. sudo doesn't speak
# polkit (setuid + PAM reading the tty), so this reuses the polkit dialog's
# look via the askpass mechanism instead — `run0` is the actual polkit-native
# alternative.
#
# sudo does NOT speak polkit — it is setuid + PAM reading the tty, and no
# sudoers option bridges the two — so this is the askpass mechanism, a
# separate path that happens to reuse the polkit dialog's look. `run0` is the
# polkit-native alternative if you want the agent itself.
# Must be a package, not a dotfiles file: SUDO_ASKPASS needs an executable,
# and xdg.configFile copies keep store-copy permissions.
#
# A package rather than a file in dotfiles/quickshell because SUDO_ASKPASS
# must point at something EXECUTABLE, and xdg.configFile copies keep their
# store mode — which is why open_launcher.sh has to be invoked as
# `bash <path>` rather than run directly.
#
# The secret comes back over a 0600 fifo, never in argv or the environment,
# so it is not visible in /proc to anything. Cancelling closes the fifo
# without writing: `cat` reads nothing, this exits non-zero, and sudo aborts
# instead of burning a retry on an empty password.
# The secret returns over a 0600 fifo (never argv/env, so not visible in
# /proc); cancelling closes the fifo unwritten so sudo aborts cleanly.
qs-askpass = pkgs.writeShellApplication {
name = "qs-askpass";
runtimeInputs = [ pkgs.quickshell pkgs.coreutils ];
@@ -74,11 +68,9 @@ in
nix-direnv.enable = true;
};
# Rootless podman: containers run as darman, not root. services/containers.nix
# gives us the `docker` CLI shim (dockerCompat), but compose v2 is a separate
# binary and talks to a socket rather than the CLI — the NixOS podman module
# enables the *user* socket (systemd.user.sockets.podman), so point compose at
# it instead of the root /var/run/docker.sock.
# Rootless podman runs containers as darman; compose v2 talks to a socket
# rather than the docker CLI shim, so point it at the user podman socket
# instead of the root one.
home.sessionVariables.DOCKER_HOST = "unix:///run/user/1000/podman/podman.sock";
# Only sets WHICH helper sudo uses; it still only calls it when asked with
+15 -36
View File
@@ -1,32 +1,16 @@
{ lib, pkgs, config, inputs, ... }:
# Hyprland config migrated from github.com/darman96/hyprland-dotfiles (the
# hyprlang `hypr/*.conf` files) into the home-manager lua-style `settings`
# (configType defaults to "lua" on stateVersion 26.05). Each top-level
# `settings` attr becomes an `hl.<name>(...)` call in ~/.config/hypr/hyprland.lua;
# `_args` lists become multi-arg calls, `_var` locals become `local x = ...`, and
# `lib.generators.mkLuaInline` values render as raw Lua expressions.
# Hyprland config migrated from github.com/darman96/hyprland-dotfiles into
# home-manager's lua-style `settings` (each attr becomes an `hl.<name>(...)`
# call in hyprland.lua). Imported by home.nix; system-level enable lives in
# ../../services/desktop/desktop-hyprland.nix.
#
# Imported by home.nix. System-level Hyprland enable (session entry, portals)
# lives in ../../services/desktop/desktop-hyprland.nix; this manages the user's
# own hyprland.lua.
#
# Deliberately NOT migrated:
# - hyprbars.conf: config for the third-party `hyprbevelbars` plugin, which
# isn't packaged in nixpkgs. Load it via
# `wayland.windowManager.hyprland.plugins` and re-add its config once
# available. (hyprredsquare.conf's plugin was renamed hypr-chrome and
# rewritten since - it's wired in below via the `hypr-chrome` flake
# input instead, with its own `plugin.hyprchrome` config.)
# - hyprqt6engine.conf + `QT_QPA_PLATFORMTHEME=hyprqt6engine`: terra themes Qt
# through qtct/Dracula in home.nix, so that env var is left off to avoid a conflict.
# - hyprlock.conf: a separate program (use `programs.hyprlock` if wanted).
# - the duplicate pamixer/amixer + `.wob` volume binds: kept only the clean
# pipewire `wpctl`/`playerctl` set (no wob overlay is configured here).
# - `XDG_MENU_PREFIX=arch-` and `VCPKG_ROOT`: Arch-/user-specific.
# Many binds reference apps/scripts not packaged on terra yet (vivaldi-stable,
# dolphin, vicinae, grimblast, waypaper, discord, gitkraken, qbz,
# ~/.config/scripts/start-communications.sh); add them separately.
# Not migrated: hyprbars (unpackaged plugin; its successor hypr-chrome is
# wired in below instead), hyprqt6engine (conflicts with home.nix's qtct/
# Dracula Qt theming), hyprlock (use programs.hyprlock), the old wob volume
# overlay (kept only wpctl/playerctl), and Arch-specific env vars. Several
# binds reference apps not yet packaged here (vivaldi-stable, dolphin,
# vicinae, grimblast, waypaper, discord, gitkraken, qbz).
let
lua = lib.generators.mkLuaInline;
@@ -37,11 +21,9 @@ let
cursorName = config.home.pointerCursor.name;
cursorSize = toString config.home.pointerCursor.size;
# Wallpaper images aren't checked into this repo (binary blobs) — pulled
# from the existing Wallhaven library on /mnt/hdd_01 instead. Picked once
# here rather than at runtime, since hyprpaper has no built-in "random"
# mode; re-pick and rebuild (or swap in real per-monitor selection) when
# this stops being a placeholder.
# Wallpapers aren't checked into this repo (binaries) — pulled from the
# Wallhaven library on /mnt/hdd_01. Picked once here since hyprpaper has
# no built-in "random" mode.
# wallpaper = "/mnt/hdd_01/data/Pictures/Wallhaven/wallhaven-ym81rl.png";
wallpaper = "/mnt/hdd_01/data/Pictures/Wallhaven/wallhaven-mlwz78.png";
@@ -317,11 +299,8 @@ in
"hyprland.start"
(lua ''
function()
-- No polkit agent is started here: quickshell registers one
-- itself (HyprChrome/Widgets/Polkit), and a session admits only
-- one. The hyprpolkitagent line this replaces had been dead for
-- a while anyway the unit was never installed, so the start
-- failed silently and the session ran with no agent at all.
-- No polkit agent started here: quickshell registers its own
-- (HyprChrome/Widgets/Polkit), and a session admits only one.
hl.exec_cmd("cosmic-settings-daemon")
hl.exec_cmd("quickshell")
hl.exec_cmd("alacritty", { workspace = "special:terminal silent" })
+8 -13
View File
@@ -23,14 +23,11 @@ in
};
};
# The cursor theme. XCURSOR_THEME alone is not enough for Steam: the client
# UI (steamwebhelper) runs inside a pressure-vessel container that rebuilds
# /etc, so the /etc/profiles/per-user/darman/share/icons entry of
# XCURSOR_PATH does not exist in there and libXcursor finds no theme by
# that name — it falls back to the built-in core X11 cursor. $HOME and
# /nix are bind-mounted into the container, so the ~/.icons symlink that
# `dotIcons` (on by default) drops does resolve. Same class of problem as
# the ~/.themes/~/.icons flatpak workaround above.
# XCURSOR_THEME alone isn't enough for Steam: steamwebhelper runs inside a
# pressure-vessel container with its own /etc, so XCURSOR_PATH doesn't
# resolve there and it falls back to the core X11 cursor. $HOME and /nix are
# bind-mounted in though, so the ~/.icons symlink `dotIcons` drops still
# resolves — same fix as the flatpak workaround below.
home.pointerCursor = {
name = "Bibata-Modern-Classic";
package = pkgs.bibata-cursors;
@@ -39,11 +36,9 @@ in
hyprcursor.enable = true;
};
# Flatpak apps are sandboxed and can't see XDG_DATA_DIRS/nix-store theme
# paths, so the portal-reported GTK theme / icon theme names resolve to
# nothing inside the sandbox and they fall back to Adwaita. Flatpak
# auto-exposes ~/.themes and ~/.icons read-only to every sandboxed app
# specifically for this case.
# Flatpak apps can't see XDG_DATA_DIRS/nix-store theme paths, so the
# portal-reported theme names resolve to nothing and fall back to Adwaita;
# Flatpak auto-exposes ~/.themes and ~/.icons read-only as the workaround.
home.file.".themes/Dracula".source =
"${pkgs.dracula-theme}/share/themes/Dracula";
home.file.".icons/${iconTheme}".source = iconThemeFolder;