2026-07-04 Home Manager athena Server Profile
What I set out to do
Scope and then prototype bringing declarative Home Manager to athena (the Intel Mac home server, x86_64-darwin), which the flake had intentionally excluded because arrow-cpp (pulled by the LiteLLM stack) is broken on that platform. Goal for the session: the de-risking “step 1” — admit the arch for a curated server profile and get it to evaluate, so a build on athena can surface the real Intel breakage.
What I actually did
Confirmed the current config is keyed on system, not machine role — a single auto-discovering home.nix builds every module everywhere, with only a thin local.* identity layer. Designed a capability-flag model (dotfiles.<feature>.enable) with a machine→profile matrix (atlas=desktop, athena=server, oracle=headless, enterprise=work-via-private-flake) and generated an overview image (~/Pictures/generated/hm-capability-matrix.png).
Then implemented step 1:
- Extracted
nix/home-manager/base.nix(shared host-agnostic identity/XDG/Nix-daemon settings), imported by bothhome.nix(desktop) and a new curatedhome-server.nix(server). - Admitted
x86_64-darwinto the flakesystems, for the server profile only. - Refactored
homeConfigurationsinto amkHomehelper +homeConfigurations."x86_64-darwin" -> home-server.nix, keepingchecksdesktop-only. - Gated ghostty off on
x86_64-darwininshell.nix(no prebuilt; server is headless) and madeshell.nix’suvLibarg optional (defaulted to null inbase.nix, overridden bypython.nix) so it imports without the Python stack.
What was striking
- Provable no-op refactor: diffing atlas’s
activationPackage.drvPathbefore/after the base.nix extraction — byte-identical (gjlka2k6…). Cross-eval on atlas also proved athena evaluates (9zc85ann…), catching broken-marked packages without needing the Intel box at all. - The
uvLib ? nullfootgun: a formal-level default does NOT work in the HM module system — it eagerly passes every declared function argument, shadowing the default. The fallback has to live in_module.args(base.nixsets_module.args.uvLib = lib.mkDefault null). - Cross-eval only catches broken-marked packages; genuine build-broken-but-not-marked Intel deps (jankyborders,
packages.nixentries) will only appear when athena actually builds — the whack-a-mole loop starts there.
Top 3 tomorrow
- Commit + push, then
hm switchon athena to surface build-time Intel breakage. - Gate /
overrideRemovewhatever fails to build, iterate until green. - Once the buildable set is known, convert survivors to the durable
dotfiles.<feature>.enableflags + a realprofiles/server.nix.
2026-07-05 — Bring-up complete
athena is now running Home Manager (generation 1). The whole path in one sitting: installed upstream Nix (nixos.org/nix/install --daemon — Determinate ships no Intel binary anymore), adopted ~/.config as the repo, added extra-trusted-users = achhina, split packages.nix into lean core + desktop extras (114→87 packages on athena, no Zotero), and activated via nix run home-manager -- switch -b backup.
The big result: the entire profile builds on Intel with zero package errors — the whack-a-mole I braced for all session never happened. Intel’s only tax is source-build time, not breakage.
What was striking (day 2):
- Determinate’s installer 404s on x86_64-darwin — Intel is being dropped ecosystem-wide (installer, nixpkgs 26.05 last release).
- Two fresh-bootstrap-only bugs that never show on a desktop: non-interactive SSH has no
nix/flakes (fix: full path +NIX_CONFIG), andmkZshCompletionGenused$XDG_DATA_HOMEunderset -uwhich is unbound on a bare shell (fix: bakeconfig.xdg.dataHome). Both invisible on atlas because a live HM session always provides that env. - The SSH session dropped mid-build once (athena slept — its keep-awake is part of the not-yet-active config), so the rebuild ran detached under
nohup caffeinate -s. Store cache made the resume near-instant.
Related
2026-07-05 — Merged to main + second capability flag
Cleared the three bring-up follow-ups. Merged hm-athena-server-profile (3 commits) into main as a clean fast-forward, then added the second capability flag on a fresh branch and merged that too.
local.devTools— a dev-toolchain master switch with independentlanguageServers/formatters/runtimessub-toggles, copying thereleaseCooldownmaster+sub-toggle idiom already inmachine.nix.packages.nixgates its three dev groups on it;home-server.nixsets the master false → athena 87→60 packages (drops the 11 LSPs + 8 formatters + 8 runtimes/compilers). If editing config over SSH later wants LSPs, justlocal.devTools.languageServers.enable = true.- Verified no desktop regression the empirical way: stashed my changes, eval’d atlas’s
activationPackage.drvPath, popped, eval’d again — byte-identical (229c3i27…). Theoptionals true xs == xsno-reorder property means desktop hosts don’t rebuild. (Amusing self-own: thegit stash popun-staged my files mid-flow, so the first commit aborted with “no changes added” until I re-git added.)
What was striking: the genuinely valuable design call this round was a restraint — deciding not to migrate the curated import list to auto-discovery + per-module flags. That path sounds cleaner but would need a correct enable guard on every Intel-broken heavy module, and one miss re-breaks the build. Codified the two-axis split (curated imports = arch boundary; capability flags = content trimming) directly in the home-server.nix header so the next person doesn’t “tidy” the import list into a landmine.
Code is on main but athena’s active generation still predates it — the trim only lands on the next hm switch over SSH.
2026-07-05 (later) — deployed the trim, then made uv a real server capability
Deployed the devTools trim to athena (generation 2): 87→60 packages live, expired generation 1, GC’d 5.8 GiB of reclaimed dev toolchain. Then a good question surfaced mid-cleanup: a uv-missing warning during activation prompted me to gate the check-upstream-issues completion on hasUv — but the real question was “athena should have uv, what feature flag is it under?” Answer: none. uv wasn’t flagged at all; it rode inside python.nix, gated purely by whether the module was imported (the server’s curated list omitted it).
So I promoted uv to a proper capability — the third after desktopApps and devTools:
local.python— masterenable(uv CLI + managed CPython +uvLib+ the local uv2nix tools) andtools.enable(the heavyuv tool installset: jupyter/ipython/comfy/torch). Same master+sub-toggle idiom asdevTools.- athena imports
python.nixwithtools.enable = false, so it gets uv 0.11.25, CPython 3.10–3.15, and the nix-built claude-ops + gh-review-preview, butuv tool listis empty. Deployed as generation 3; expired generation 2, GC’d another 437.6 MiB.
What was striking: the whole session was really about the difference between the two profile axes I codified yesterday. uv sat on the wrong axis — controlled by import (arch boundary) when it should have been a content flag. Moving it to a capability flag is exactly the migration the note has been circling. And once more the feared Intel breakage didn’t happen: the uv2nix venvs (claude-ops, gh-review-preview) built clean on x86_64-darwin. Three capability flags are now real (desktopApps, devTools, python), all verified against atlas’s unchanged drvPath (229c3i27…).
Also a small self-own worth remembering: git stash pop returns previously-staged files as unstaged, which silently aborted a commit with “no changes added” until I re-git added. The stash-and-diff trick for proving drvPath equivalence has that footgun.
2026-07-05 (later still) — named local.profile bundle
Closed the loop on the capability model with the piece the note had been circling since April: named profile bundles. Added local.profile (enum desktop|server); home-server.nix now says just local.profile = "server" instead of three separate flag lines, and machine.nix maps the profile to mkDefault false on desktopApps/devTools/python.tools. The mkDefault priority (1000) is the key: it overrides the option’s own default (1500) but loses to an explicit host setting (100), so “server” is a floor, not a cage — a server host can still turn one capability back on.
Pure refactor, and I could prove it: both drvPaths unchanged (atlas 229c3i27…, athena gqa9ngn8…). The athena redeploy was a genuine no-op — home-manager switch re-ran activation but created no new generation (still id 3), which is exactly the signal that the profile bundle resolves to byte-identical config. Adding oracle later is now a one-liner.
What was striking: the arc of the last two days was really about putting each capability on the right axis. desktop apps, dev tooling, and uv all started life gated by module import (the arch-safety boundary) and moved, one at a time, to content-level capability flags — and now those flags roll up into a single declarative profile. The whole thing held the same invariant end to end: atlas never rebuilt once. Seven commits, three real capability flags, one profile bundle, one live headless host.