{ config, pkgs, lib, ... }: { config = lib.mkIf (config.networking.hostName == "FredOS-Mediaserver") { # Create symlink from home to storage systemd.tmpfiles.rules = [ "L+ /home/fred/storage - - - - /mnt/storage" ]; # Default max-jobs = 56 × cores = all let a from-source build thrash 31G # into a 10h livelock, taking routing down with it. Cap parallelism and # fence builds in a cgroup so the kernel OOM-kills a compiler, not the LAN. nix.settings.max-jobs = 8; nix.settings.cores = 8; nix.daemonCPUSchedPolicy = "batch"; systemd.services.nix-daemon.serviceConfig = { MemoryHigh = "8G"; MemoryMax = "10G"; OOMPolicy = "continue"; # a killed build shouldn't stop the daemon }; # System-wide backstop for whatever else runs away. oomd was running but # monitoring nothing; now it kills the worst cgroup once memory stalls # hit 80% for 30s (start of a thrash, not 10h in). systemd.oomd.enableRootSlice = true; systemd.oomd.enableSystemSlice = true; # Keep 1G truly free (default ~88M) so kernel networking and the watchdog # can still allocate while userspace is squeezed. boot.kernel.sysctl."vm.min_free_kbytes" = 1048576; # FredOS-Eee dials in and holds localhost:2222 -> its sshd open (see # hosts/FredOS-Eee.nix). Its key may open exactly that forward, nothing else. users.users.fred.openssh.authorizedKeys.keys = [ ''restrict,port-forwarding,permitlisten="localhost:2222",command="${pkgs.coreutils}/bin/false" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDytimrM126GOrVlroh048Gt/w6Yv2x+Dp4rtUEueoYr eee-tunnel'' ]; # Reap dead sessions in ~3min so a dropped tunnel's port frees up for the # reconnect (default is TCP keepalive: 2h). services.openssh.settings.ClientAliveInterval = 60; services.openssh.settings.ClientAliveCountMax = 3; programs.ssh.extraConfig = '' Host eee HostName localhost Port 2222 HostKeyAlias FredOS-Eee StrictHostKeyChecking accept-new Host gaming HostName 10.0.0.20 HostKeyAlias FredOS-Gaming StrictHostKeyChecking accept-new ''; # Basic system packages environment.systemPackages = with pkgs; [ mergerfs wget util-linux # This host is the router (services/router.nix) but shipped without any # way to see a packet, so "is that nftables rule actually matching?" was # unanswerable. Counters tell you a rule fired; these tell you what it # fired on. tcpdump conntrack-tools javaPackages.compiler.temurin-bin.jre-25 unzip screen # The Eee can't evaluate the flake in 1GB: build here, push over its # tunnel, apply on next boot (a live switch froze it out of memory). # The out-link keeps the closure from the weekly GC. (pkgs.writeShellScriptBin "eee-update" '' set -euo pipefail sys=$(nix build --refresh --print-out-paths --out-link ~/eee-system \ git+https://forg.gregersen.it/rope/nixos#nixosConfigurations.FredOS-Eee.config.system.build.toplevel) nix copy --no-check-sigs --to ssh-ng://root@eee "$sys" ssh root@eee "nix-env -p /nix/var/nix/profiles/system --set $sys && $sys/bin/switch-to-configuration boot" echo "Eee updated; applies on its next reboot." '') yt-dlp # Audible library -> /mnt/storage/torrents/audiobooks for Jellyfin. # audible-cli pulls the .aaxc + voucher, AAXtoMP3 decrypts to m4b. audible-cli aaxtomp3 ghostty.terminfo usbutils lm_sensors (pkgs.writeShellScriptBin "transcode-hevc" '' export PATH="${pkgs.jellyfin-ffmpeg}/bin:${pkgs.coreutils}/bin:${pkgs.findutils}/bin:${pkgs.gnugrep}/bin:${pkgs.gawk}/bin:${pkgs.bc}/bin:${pkgs.curl}/bin:$PATH" exec ${pkgs.bash}/bin/bash ${../scripts/transcode-hevc.sh} "$@" '') # Stats stream for the quickshell server monitor on the desktops: # `ssh mediaserver qs-stats` = top's 2s batch stream, interleaved with # one @STAT line per tick (hottest coretemp across both sockets, WAN # byte rates from eno1). Everything rides one SSH connection. (pkgs.writeShellScriptBin "qs-stats" '' # Single writer: the loop re-emits top's lines itself and injects a # @STAT line at each frame header. top writing the pipe directly in # parallel raced the injected lines (pipe writes aren't line-atomic) # and spliced @STAT mid-frame, where the client parser never saw it. C=${pkgs.coreutils}/bin export LC_ALL=C prev_rx="" ${pkgs.procps}/bin/top -b -d 2 -w 512 | while IFS= read -r line; do printf '%s\n' "$line" case $line in "top - "*) t=0 for h in /sys/class/hwmon/*; do [ "$($C/cat "$h/name" 2>/dev/null)" = coretemp ] || continue for f in "$h"/temp*_input; do v=$($C/cat "$f" 2>/dev/null || echo 0) [ "$v" -gt "$t" ] && t=$v done done read -r rx tx < <(${pkgs.gawk}/bin/awk '$1 == "eno1:" {print $2, $10}' /proc/net/dev) if [ -n "$prev_rx" ]; then printf '@STAT temp=%s rxbps=%s txbps=%s\n' "$((t / 1000))" "$(( (rx - prev_rx) / 2 ))" "$(( (tx - prev_tx) / 2 ))" fi prev_rx=$rx prev_tx=$tx ;; esac done '') # Instant-answer backend for the quickshell launcher on the desktops: # `printf '%s' "question" | ssh mediaserver qs-ask`. # # Runs through Codex CLI using fred's ChatGPT subscription. Sign in on # the server as fred with `codex login` once; no API key is needed. # The launcher shows Wikipedia first and replaces it when this answers. # # Reads the question from stdin — passing it as an ssh argv element would # send it through the remote shell for a second round of word splitting. # # Luna at low reasoning effort is enough for a short fact. The CLI runs # read-only, without web search, shell access, user config, exec rules, # or session persistence. # # STDOUT is the launcher's channel: the answer, or nothing at all if the # call fails or the model doesn't know — silence means "keep Wikipedia". # Reasons go to STDERR and the journal (`journalctl -t qs-ask`). (pkgs.writeShellScriptBin "qs-ask" '' # Every line carries this script's pid, because the launcher fires a # fresh call per debounce and concurrent runs otherwise interleave with # no way to pair a question with its outcome. The pid goes in the # message text rather than through logger's --id: --id sets SYSLOG_PID, # which journalctl does not display — it shows the pid of the logger # process itself, a different number on every line. log() { printf 'qs-ask: %s\n' "$*" >&2 ${pkgs.util-linux}/bin/logger -t qs-ask -- "[$$] $*" } q=$(${pkgs.coreutils}/bin/cat) [ -z "$q" ] && { log "no question on stdin"; exit 0; } log "ask: $q" # Keep repository instructions and project files out of this invocation. work="$HOME/.cache/qs-ask" ${pkgs.coreutils}/bin/mkdir -p "$work" || { log "cannot create $work"; exit 0; } cd "$work" || { log "cannot enter $work"; exit 0; } ans=$(printf 'Answer the question in one or two short sentences, under 240 characters. Lead with the specific fact asked for, including units. The query is often bare keywords rather than a question ("shadow hunter knife classic wow"); treat that as a request for the key facts about that thing. No preamble, caveats, markdown, or follow-up offers. Do not use tools. Reply with exactly UNKNOWN only when you genuinely do not know, or the answer depends on live data you do not have.\n\nQuestion: %s\n' "$q" | \ ${pkgs.coreutils}/bin/timeout 30s ${pkgs.codex}/bin/codex exec \ --model gpt-5.6-luna \ --config model_reasoning_effort=low \ --config web_search=disabled \ --disable shell_tool \ --sandbox read-only \ --skip-git-repo-check \ --ephemeral \ --ignore-user-config \ --ignore-rules \ - \ 2>/dev/null) \ || { log "codex exited non-zero (check 'codex login status' as fred)"; exit 0; } ans=$(printf '%s' "$ans" | ${pkgs.coreutils}/bin/tr -d '\r' | ${pkgs.gnused}/bin/sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//') case "$ans" in "") log "empty answer"; exit 0 ;; UNKNOWN*) log "model replied UNKNOWN"; exit 0 ;; esac log "ok: $ans" printf '%s' "$ans" '') ]; # Basic networking networking.useDHCP = lib.mkForce false; # Allow fred to act as a remote Nix builder (trusted users can import # unsigned store paths sent by the build client). nix.settings.trusted-users = [ "root" "fred" ]; # services/deployment.nix owns the 05:15 update and conditional reboot, # using the same health-checked transaction as manual and router UI applies. # WAN exposure is controlled by nftables in services/router.nix + # ports.toml (networking.firewall is disabled on this host). services.openssh = { enable = true; settings = { PermitRootLogin = "no"; PasswordAuthentication = false; }; }; }; }