chore: retire consumed specs and tasks, trim CLAUDE.md gotchas
The .claude specs and tasks have been distilled into the notes vault, so remove the 37 consumed files. Trim the CLAUDE.md gotchas to their project-specific residue: drop the entries now captured as general notes, and fix the two references that pointed at the deleted specs.
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
## Problem Statement
|
||||
|
||||
Neogaia now boots into the keyboard-driven Hyprland desktop, but the session ships no web browser.
|
||||
The operator lives in this desktop daily and needs a browser, yet a stock browser install would arrive un-themed, telemetry-on, cluttered with sponsored surfaces, and requiring a round of manual clicking to reach a usable state.
|
||||
That manual state would also be invisible to the flake and would not survive a reimage or transfer to the future desktop Host.
|
||||
|
||||
The operator wants the browser configured the same way as the rest of the system: declared once, hardened and themed by default, and reproduced automatically on any Host that runs the desktop.
|
||||
|
||||
## Solution
|
||||
|
||||
Add Firefox as a new single-purpose desktop Module, configured entirely through home-manager's `programs.firefox`, and fold it into the desktop aggregator so the browser is part of the daily-drivable session rather than a separate opt-in.
|
||||
|
||||
Ship stock mainline Firefox, hardened and de-monetized through locked enterprise policies, with a small fixed set of extensions force-installed by policy from Mozilla's add-on site.
|
||||
Default search to DuckDuckGo over a lean, pruned engine list.
|
||||
Theme the browser Nord from the same single Stylix source that themes the rest of the graphical layer, with no hand-maintained browser CSS.
|
||||
Register Firefox as the system default handler for web links.
|
||||
|
||||
Leave the most personal, frequently-changing state — bookmarks and container tabs — to Firefox's own runtime management rather than declaring it, keeping the Module lean and avoiding the destructive overwrite those declarative options impose.
|
||||
|
||||
Because the Module joins the aggregator, it comes up on any Host with the desktop enabled: neogaia now, and the future desktop Host for free, with no per-Host browser flag.
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As the operator, I want a web browser present the moment the desktop comes up, so that a daily-drivable session includes browsing without a separate install step.
|
||||
2. As the operator, I want the browser expressed as one more enable in the desktop aggregator, so that any Host running the desktop inherits it and the future desktop Host adopts it without rework.
|
||||
3. As the operator, I want the browser configured declaratively alongside every other Module, so that it is reproduced identically on reimage and never depends on manual post-install clicking.
|
||||
4. As the operator, I want to stay on current mainline Firefox rather than an older release train, so that I get up-to-date browser features and security without maintaining an unusual package variant.
|
||||
5. As the operator, I want a fixed set of extensions installed and actually enabled automatically, so that ad-blocking, password management, and sponsor-skipping work on first launch with no add-on hunting.
|
||||
6. As the operator, I want ad and content blocking, so that pages are lighter and less hostile.
|
||||
7. As the operator, I want my password manager available in the browser, so that credentials autofill without me reaching for another app.
|
||||
8. As the operator, I want sponsor segments skipped in videos, so that watching is uninterrupted.
|
||||
9. As the operator, I want telemetry, studies, the read-it-later widget, and the sponsored surfaces on the new-tab and address bar turned off and kept off, so that the browser is quiet, private, and un-monetized without me policing settings.
|
||||
10. As the operator, I want the browser to stop offering to save logins and to stop nagging about being the default, so that it does not fight the password manager or interrupt me.
|
||||
11. As the operator, I want Firefox accounts and sync disabled, so that no account surface appears for a feature I do not use.
|
||||
12. As the operator, I want DuckDuckGo as the default search with only a lean set of engines present, so that search is private and uncluttered.
|
||||
13. As the operator, I want the browser themed Nord from the same source as the rest of the desktop, so that it coheres with the bar, launcher, and lock screen without me hand-theming it.
|
||||
14. As the operator, I want the browser registered as the system default for web links, so that links opened from the bar, notifications, the launcher, or the terminal land in it.
|
||||
15. As the operator, I want bookmarks and container tabs left to the browser itself, so that the things I add in the moment are never wiped by a rebuild.
|
||||
16. As the operator, I want the whole Host to still build green under the existing check, so that I gain confidence before switching a live machine.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
**Module and placement**
|
||||
|
||||
- A new single-purpose Module is added under the desktop group, namespaced to mirror its location per the Namespace convention, exposing one `enable` option guarded by the Enable convention.
|
||||
- The Module is configured entirely through the primary user's home-manager `programs.firefox`, matching every other user-facing Module in the repo; no NixOS-level Firefox program integration and no manual package override are used.
|
||||
- The desktop aggregator turns the Module on at default priority alongside the terminal and the other session pieces, so a single desktop flag brings the browser up while a Host retains the ability to override it.
|
||||
The browser is treated as an essential application of the session rather than optional plumbing, following the precedent that the aggregator already enables the terminal.
|
||||
|
||||
**Package variant and extension mechanism**
|
||||
|
||||
- The package is stock mainline Firefox, not ESR, unbranded, or Developer Edition.
|
||||
- Extensions are installed through Mozilla's enterprise policy `force_installed`, keyed by add-on id with an install URL, so the browser fetches the signed add-on from Mozilla's add-on site and enables it automatically.
|
||||
- Nix-built or hash-pinned add-on packages are not used, because stock Firefox refuses unsigned locally-built add-ons; the trade-off — losing build-time reproducibility of the extension binaries in exchange for current mainline Firefox with add-ons that are actually enabled and no new flake input — is accepted deliberately.
|
||||
This decision is the pivotal, hard-to-reverse one and is called out for an ADR in Further Notes.
|
||||
|
||||
**Extensions**
|
||||
|
||||
- Three extensions are force-installed: an ad and content blocker, the operator's password manager, and a video sponsor-skipper.
|
||||
- All three are self-contained web extensions, so no native messaging host is wired.
|
||||
|
||||
**Hardening**
|
||||
|
||||
- Hardening is split across two mechanisms by intent: things with a corresponding enterprise policy are set as locked policies so they cannot be toggled back in the UI, and the remainder are set as ordinary profile preferences.
|
||||
- Locked policies turn off telemetry and studies and data reporting, turn off the read-it-later widget, stop the browser offering to save logins, stop the default-browser check, strip the sponsored shortcuts, sponsored stories, and snippets from the new-tab page, and disable Firefox accounts and sync.
|
||||
- Profile preferences turn off sponsored address-bar suggestions and tidy the new-tab surface.
|
||||
- Fingerprinting resistance is deliberately left off, because it breaks enough everyday browsing to be a deliberate per-need choice rather than a baseline.
|
||||
|
||||
**Search**
|
||||
|
||||
- A single profile is declared, named as the default profile.
|
||||
- The default engine is DuckDuckGo, and the engine list is pruned to a lean set with the general-purpose commercial engines removed; the removed engines remain reachable through DuckDuckGo's bang syntax.
|
||||
- Declaring search requires the module's authoritative-overwrite acknowledgement, so engines added later through the UI are not preserved across a rebuild; this is accepted as the point of declaring search.
|
||||
|
||||
**Theming**
|
||||
|
||||
- The browser is themed by enabling the Stylix Firefox target against the declared profile, driven from the same single Nord scheme that themes the rest of the graphical layer.
|
||||
- No hand-written browser chrome CSS is shipped; a small chrome-CSS layer remains a later addition on top of Stylix if deeper chrome restyling is ever wanted.
|
||||
- The Firefox Stylix target is enabled from within the browser Module, mirroring how the theming Module already sets per-Module Stylix targets, so the target only takes effect when the browser is enabled.
|
||||
|
||||
**Default browser**
|
||||
|
||||
- Firefox is registered as the default handler for the web-link schemes and HTML through the primary user's home-manager mime-association config, placed in the browser Module.
|
||||
|
||||
**Existing conventions already satisfied**
|
||||
|
||||
- The Waybar workspace indicator already carries a window-rewrite icon mapping for Firefox, so the graphical-application icon convention needs no change.
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
- A good test asserts externally-observable evaluation and build success of the whole Host, not the internals of the Module.
|
||||
This mirrors the desktop and laptop-MVI stance, where the config-merge model makes the whole-Host build the meaningful unit and the highest available seam.
|
||||
- Primary seam, required and existing: the neogaia Host evaluates and its system toplevel builds under the flake check.
|
||||
Building the toplevel drives the Auto-loader discovering the new Module, the aggregator fan-out, home-manager wrapping the Firefox package with the policies, extensions, search, and preferences baked in, the Stylix Firefox target wiring, and the default-handler association, surfacing nearly all config-authoring errors short of launching the browser.
|
||||
- Cheap targeted checks: evaluate specific configuration paths to confirm the aggregator fans the browser enable out, the rendered policies carry the three force-installed extensions, the default search engine resolves to DuckDuckGo, and the Stylix Firefox target is on, reusing the repo's existing lightweight eval-probe pattern.
|
||||
- No Module-level unit tests are added; there is no seam below the whole-Host build worth testing here, and the prior art is the desktop and laptop-MVI build-the-toplevel checks.
|
||||
- The genuine end-to-end confirmation is manual and irreducible: switch the configuration on neogaia, launch the browser, and confirm the three extensions are present and enabled, the Nord theme is applied, DuckDuckGo is the default search, and a link opened from another application lands in the browser.
|
||||
A browser cannot self-test headless, but unlike a reimage this is reversible, so verification is done by living in it with a safety net of a generation rollback or the desktop flag.
|
||||
- The build validates that the policy document is well-formed and baked into the package, but not that Firefox accepts every policy key semantically, since the policy document is only text until the browser loads it; that last mile is part of the manual confirmation.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- ESR, unbranded, and Developer Edition Firefox variants, and the nix-built or hash-pinned add-on path they would enable; the reproducibility trade-off was weighed and declined.
|
||||
- Native messaging hosts of any kind, including a password-manager native connector and desktop browser-integration bridges; none of the chosen extensions need one.
|
||||
- Declarative bookmarks and declarative container tabs, both deliberately left to the browser's own runtime state.
|
||||
- The Multi-Account Containers extension and the contextual-identity preference it relies on.
|
||||
- Multiple Firefox profiles; a single default profile carries everything.
|
||||
- Custom or Nix-oriented search engines beyond the lean pruned set.
|
||||
- Hand-written browser chrome CSS and any deep chrome-layout restyling such as compact tabs or a hidden title bar.
|
||||
- Fingerprinting resistance and any harder privacy posture that routinely breaks everyday browsing.
|
||||
- Firefox accounts and sync.
|
||||
- Any second browser, and any per-Host browser divergence; the future desktop Host inherits this same Module unchanged.
|
||||
- Any Skeleton, Auto-loader, or secret-wiring change; the Module uses the existing plumbing unchanged.
|
||||
|
||||
## Further Notes
|
||||
|
||||
- The stock-Firefox-plus-policy-extensions decision is hard to reverse — the variant dictates the entire extension mechanism — surprising without context, since a Nix-purist default would expect hash-pinned add-on packages, and the product of a real trade-off between reproducibility and current mainline Firefox with working extensions.
|
||||
It should be recorded as an ADR.
|
||||
- Home-manager's `programs.firefox` was confirmed to expose a top-level policies option that merges into the wrapper's enterprise policies, which is what lets the whole Module, hardening included, live under home-manager rather than needing the NixOS-level program integration.
|
||||
- The declarative search, bookmarks, and containers options all share one authoritative-overwrite model: the browser owns those files at runtime, so declaring them means home-manager overwrites them wholesale and runtime-added entries do not survive a rebuild.
|
||||
This is why bookmarks and containers are left out and why declaring search is an explicit acknowledgement.
|
||||
- The extension set fetches signed add-ons from Mozilla's add-on site at runtime rather than from the Nix store, so first launch after a fresh build needs network to populate the extensions; this is inherent to the policy-based path chosen over the pinned-package path.
|
||||
@@ -1,173 +0,0 @@
|
||||
## Problem Statement
|
||||
|
||||
Neogaia currently boots to a console only.
|
||||
The laptop minimum viable install deliberately deferred everything graphical, so the operator works entirely from the terminal (fish, tmux, nvim, Claude Code) with no desktop to live in.
|
||||
|
||||
The operator wants a graphical desktop, and it must be primarily keyboard-driven.
|
||||
Their mental model is i3: numbered workspaces, `Super`+number to switch, manual tiling.
|
||||
They type on a 60% keyboard where arrow keys and the navigation cluster live behind a layer, so a keyboard-first workflow that never reaches for those keys matters.
|
||||
|
||||
The same desktop has to transfer to the future desktop Host (zeus), which must game well and stay low-friction in general.
|
||||
The operator previously ran KDE Plasma 6 bent into an i3 imitation and valued above all that it "just worked", but remembers past i3/Sway minimalism costing hours of fixing random breakage.
|
||||
That concern is now largely mitigated: NixOS makes the whole stack declarative, pinned, and reversible, and the agent absorbs the discovery and debugging that used to eat evenings.
|
||||
|
||||
## Solution
|
||||
|
||||
Add a keyboard-driven Wayland desktop built on Hyprland, expressed as a new grouped set of Modules.
|
||||
Enable it on neogaia now, and design it Host-agnostic so zeus adopts the identical desktop later by flipping a single flag.
|
||||
|
||||
Deliver a complete, daily-drivable session in one pass rather than a bare stub, because unlike the reimage this is fully reversible ("edit and rebuild"), so there is no safety reason to under-scope.
|
||||
The session covers the compositor, a text login, Nord theming, a status bar, a launcher, notifications, lock and idle, a wallpaper, clipboard history, screenshots, screen recording, and the desktop portals, together with the operator's ported keybinds and input tuning.
|
||||
|
||||
Theme the whole graphical layer Nord from a single source (Stylix), scoped so it owns only the new graphical surface and leaves the existing terminal Modules' hand-themes untouched.
|
||||
Leave gaming out entirely; it belongs to a future zeus-oriented Module, and neogaia is not a gaming machine.
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As the operator, I want neogaia to boot into a keyboard-driven Hyprland session, so that I can live in a graphical desktop without leaving my keyboard-first workflow.
|
||||
2. As the operator, I want the desktop expressed as Host-agnostic Modules enabled by a single flag, so that zeus can adopt the identical desktop later without rework.
|
||||
3. As the operator, I want a text-based login, so that I log in mouse-free without a heavy graphical display manager.
|
||||
4. As the operator, I want the whole graphical layer themed Nord from one source, so that GTK, Qt, the bar, the lock screen, and the launcher cohere without me hand-theming each, while my existing nvim, tmux, and fish themes stay exactly as they are.
|
||||
5. As the operator, I want a fast modern terminal launched on `Super+Return`, so that my tmux and nvim stack has a clean frame and my old muscle memory for opening a terminal carries over.
|
||||
6. As the operator, I want a status bar showing workspaces with per-application icons, clock, battery, network, audio, media controls, and a do-not-disturb toggle, so that I can read system state at a glance.
|
||||
7. As the operator, I want a search-everything launcher covering applications, open windows, math evaluation, and emoji, reused as the frontend for clipboard history and a power menu, so that one keybound tool handles launching and utility menus.
|
||||
8. As the operator, I want notification toasts with do-not-disturb and history recall, so that I see notifications and can retrieve ones I missed.
|
||||
9. As the operator, I want a secure lock screen and idle management, so that going idle, suspending, or closing the lid always lands me at a locked screen, and the lock survives even if the locker process crashes.
|
||||
10. As the operator, I want a single static Nord wallpaper, so that the desktop looks coherent with no extra moving parts and no battery cost.
|
||||
11. As the operator, I want clipboard history picked through the launcher, so that I can paste from recent copies entirely by keyboard.
|
||||
12. As the operator, I want keyboard-driven screenshots for a region, the active window, or the full screen that open in an annotation editor by default and land in both the clipboard and a file, so that capturing and marking up is a single reflex.
|
||||
13. As the operator, I want a keybound screen recorder that selects a region and then toggles recording, so that capturing demos becomes a habit.
|
||||
14. As the operator, I want screen sharing to work inside applications, so that video calls and browser screen-share function.
|
||||
15. As the operator, I want my keybinds ported from my KDE/i3 scheme but expressed entirely in `hjkl` and letters with no arrow or navigation-cluster keys, so that every binding is reachable on my 60% keyboard.
|
||||
16. As the operator, I want numbered-workspace bindings, so that my i3 muscle memory of `Super`+number to switch and `Super`+`Shift`+number to move carries over unchanged.
|
||||
17. As the operator, I want Caps mapped to Escape, a US-only layout, snappy key-repeat, and touchpad tap-to-click, natural scroll, and disable-while-typing with flat mouse acceleration, so that input feels like home on the laptop.
|
||||
18. As the operator, I want subtle animations with rounding and small gaps but no blur on the laptop, so that the desktop feels modern without draining the battery.
|
||||
19. As the operator, I want the desktop built from granular single-purpose Modules grouped together with an explicit aggregator, so that a Host enables the whole desktop with one flag yet can still override any single piece.
|
||||
20. As the operator, I want gaming deliberately excluded from this pass, so that neogaia's desktop stays focused and the gaming stack lands with zeus.
|
||||
21. As the operator, I want the whole Host to still build green under the existing check, so that I gain confidence before switching a live machine.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
**Direction and compositor**
|
||||
|
||||
- Hyprland is the compositor, on neogaia now and zeus later, chosen as a single keyboard-driven tiler that serves both Hosts.
|
||||
It matches the operator's i3 workflow (numbered workspaces, manual tiling) while getting closest to "just works" among true tilers through its cohesive first-party companion tools and large ecosystem.
|
||||
- The choice was made over Sway (its minimalism is the very thing that cost the operator evenings), KDE Plasma (mouse-first at heart), and niri (its scrollable-tiling paradigm abandons numbered workspaces and so breaks the operator's core muscle memory).
|
||||
- The gaming/driver dimension does not discriminate here: neogaia is Intel and zeus is AMD, both of which drive Wayland flawlessly, so the decision rested on workflow and low-friction rather than on surviving a hostile driver.
|
||||
This corrects a stale assumption in the laptop spec (see Further Notes).
|
||||
- Hyprland is sourced from nixpkgs rather than the upstream Hyprland flake.
|
||||
The NixOS-level program integration owns the session, portals, and polkit, and home-manager owns the user configuration, sharing one Hyprland package so there is never a version split.
|
||||
The session is launched through the universal Wayland session manager from the greeter for clean systemd session and environment integration.
|
||||
No compositor plugins are adopted this pass, which removes the main reason to take the flake; moving to the flake later is a contained change that mirrors the existing chaotic-nyx pattern (an input that must not follow nixpkgs, with its own binary cache).
|
||||
|
||||
**Session and theming**
|
||||
|
||||
- Login uses greetd with the tuigreet text greeter.
|
||||
This is mouse-free and lightweight, avoiding a heavy graphical display manager and its Qt/GTK weight.
|
||||
- Theming uses Stylix, scoped to the graphical layer.
|
||||
A single Nord base16 scheme drives colors, system fonts, cursor, and the static wallpaper across the new graphical surface (GTK, Qt, bar, lock, launcher, notifications, compositor colors).
|
||||
Stylix targets for the existing terminal tools (nvim, tmux, fish) are left off so their established hand-themes stand unchanged.
|
||||
The terminal is themed by Stylix where its target is mature, with hand-written Nord as a fallback otherwise.
|
||||
This decision is highly reversible: Stylix toggles per target, so the graphical layer can migrate toward or away from manual theming later at low cost.
|
||||
|
||||
**Components**
|
||||
|
||||
- Terminal: Ghostty.
|
||||
- Status bar: Waybar, showing workspaces with per-application icons plus clock, battery, network, audio, MPRIS media controls, and a do-not-disturb toggle.
|
||||
No overview/exposé plugin is used; the workspace indicators are sufficient.
|
||||
- Launcher: rofi (the Wayland fork), combining application-run, binary-run, and window-switch modes into one prompt, plus math-evaluation and emoji modes.
|
||||
It is reused as the dmenu-style frontend for clipboard history and a power menu, so one tool serves several jobs.
|
||||
- Notifications: mako, with do-not-disturb and history recall.
|
||||
Media controls and the do-not-disturb toggle live in the bar rather than in a separate notification center.
|
||||
- Lock and idle: hyprlock and hypridle.
|
||||
hyprlock uses the compositor session-lock protocol, so the lock surface is owned by the compositor and survives a locker crash.
|
||||
hypridle is wired for lock-on-idle, screen-off, lock-before-suspend, and lid-close, with tunable timeouts.
|
||||
- Wallpaper: a single static image set by Stylix.
|
||||
- Clipboard: cliphist with wl-clipboard, storing text and image history, picked through rofi.
|
||||
- Screenshots: grim and slurp wrapped by grimblast, routed through the satty annotation editor so that annotation is the default on region, active-window, and full-screen captures, each exporting to both the clipboard and a file.
|
||||
- Screen recording: wf-recorder, region-select-first and video-only (no audio), toggled by a keybind with a bar recording indicator and notifications.
|
||||
- Portals: the Hyprland desktop portal (screencast, screenshot, global shortcuts) plus the GTK portal (file dialogs and appearance).
|
||||
Screen sharing in applications depends on these regardless of whether the recorder is present.
|
||||
|
||||
**Keybinds**
|
||||
|
||||
The scheme ports the operator's KDE/i3 bindings but is expressed entirely in `hjkl` and letters, with no arrow or navigation-cluster keys, so it is fully reachable on a 60% keyboard.
|
||||
Bindings that were quick-tile-to-half on the floating KDE desktop are reclaimed for real tiling actions, because that gesture is meaningless in an automatic tiler.
|
||||
|
||||
| Action | Bind |
|
||||
|---|---|
|
||||
| Switch to workspace N | `Super`+`1`..`9` |
|
||||
| Move window to workspace N | `Super`+`Shift`+`1`..`9` |
|
||||
| Move focus | `Super`+`h`/`j`/`k`/`l` |
|
||||
| Move window in layout | `Super`+`Shift`+`h`/`j`/`k`/`l` |
|
||||
| Resize | `Super`+`Alt`+`h`/`j`/`k`/`l` |
|
||||
| Terminal | `Super`+`Return` |
|
||||
| Launcher | `Super`+`R` |
|
||||
| Lock | `Super`+`X` |
|
||||
| Toggle floating | `Super`+`Space` |
|
||||
| Fullscreen | `Super`+`F` |
|
||||
| Toggle split | `Super`+`T` |
|
||||
| Close window | `Super`+`Shift`+`Q` |
|
||||
| Force-kill | `Super`+`Ctrl`+`Q` |
|
||||
| Screenshot region / window / full (to satty) | `Super`+`L` / `Super`+`Shift`+`L` / `Super`+`Ctrl`+`L` |
|
||||
| Clipboard history | `Super`+`Shift`+`V` |
|
||||
| Record toggle (region first) | `Super`+`Shift`+`R` |
|
||||
| Volume / brightness / media | `XF86` hardware keys |
|
||||
|
||||
**Input**
|
||||
|
||||
- US-only keyboard layout, with no layout switcher.
|
||||
- Caps mapped to Escape, with Shift+Caps still producing a real CapsLock.
|
||||
- Key-repeat tuned snappy (a short delay before repeat begins, a fast repeat rate).
|
||||
- Touchpad with tap-to-click, natural scroll, and disable-while-typing.
|
||||
- Mouse with flat acceleration.
|
||||
|
||||
**Feel**
|
||||
|
||||
- Tasteful, subtle animations with modest rounding and small gaps.
|
||||
- Blur is off on the laptop, where it is the single biggest battery cost; it remains a knob a Host such as zeus can enable.
|
||||
|
||||
**Module structure**
|
||||
|
||||
- The desktop is a set of granular, single-purpose Modules grouped under a desktop directory, each independently toggleable so pieces stay reusable and the files stay focused.
|
||||
The tightly coupled Hyprland-native pieces (compositor, lock, idle) are grouped in a subdirectory within that group.
|
||||
- An explicit aggregator Module turns the desktop on.
|
||||
Guarded by its own enable, it sets each piece's enable at default priority so that a Host enables the whole desktop with one flag while retaining the ability to override any single piece.
|
||||
The aggregator hand-lists the pieces it enables rather than scanning the directory, so a newly added file stays inert until deliberately added to that list; this keeps the whole desktop legible from one file.
|
||||
- The desktop's enable options are namespaced under a single desktop group, so a Host's checklist gains one desktop entry and the aggregator itself reads as the sub-checklist of what that entry means.
|
||||
- This uses the existing recursive Auto-loader as-is: every file in the group is imported unconditionally (inert until enabled), so no Skeleton change is needed.
|
||||
The aggregator/namespaced-group pattern is a deliberate evolution of the flat Module plus Host-as-checklist convention (see Further Notes).
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
- A good test asserts externally-observable evaluation and build success of the whole Host, not the internals of any individual Module.
|
||||
This mirrors the laptop MVI's stance, where the config-merge model makes the whole-Host build the meaningful unit and the highest available seam.
|
||||
- Primary seam (required, existing): the neogaia Host evaluates and its system toplevel builds under the flake check.
|
||||
Building the toplevel drives the Auto-loader discovering the new Modules, the aggregator fan-out, home-manager integration, the full Stylix wiring, the Hyprland program integration, and package availability, surfacing nearly all config-authoring errors short of rendering a frame.
|
||||
- Cheap targeted checks: evaluate specific configuration paths to confirm the desktop aggregator fans out, the Stylix scheme resolves to Nord, and Hyprland is enabled, reusing the repo's existing lightweight eval-probe pattern.
|
||||
- No Module-level unit tests are added; there is no seam below the whole-Host build worth testing here, and the prior art is the laptop MVI's build-the-toplevel check.
|
||||
- The genuine end-to-end confirmation is manual and irreducible: switch the configuration on neogaia, log in through the greeter, and exercise the live session (keybinds, lock, screenshot, clipboard, launcher).
|
||||
A graphical session cannot self-test headless, but unlike the reimage this is reversible, so verification is done by living in it with a safety net (roll back a generation, drop to a console, or disable the desktop flag).
|
||||
- VM-based graphical CI (boot assertions under a NixOS test) is deferred, consistent with the laptop MVI's stated stance.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- The gaming stack (Steam, gamescope, Proton, replay-buffer recording, OBS), which belongs to a future zeus-oriented Module.
|
||||
- The zeus Host itself, and multi-monitor/output configuration, since there is no second display to design against yet; monitor focus and move bindings are deferred with it.
|
||||
- Any non-Hyprland desktop; KDE, Sway, and niri were considered and rejected.
|
||||
- The upstream Hyprland flake and compositor plugins, including workspace-overview/exposé, which are explicitly not adopted this pass.
|
||||
- Migrating the existing nvim, tmux, and fish themes into Stylix; they stay hand-themed.
|
||||
- Dynamic, animated, or video wallpaper, and wallpaper cycling.
|
||||
- Screen-recording audio and a full-screen recording variant.
|
||||
- A slide-out notification-center panel and a batteries-included desktop panel; both were considered and rejected in favor of the minimal, Stylix-coherent stack.
|
||||
- Any change to the Skeleton or the Auto-loader; the structure uses them unchanged.
|
||||
- Any secret wiring.
|
||||
|
||||
## Further Notes
|
||||
|
||||
- The compositor decision (Hyprland over Sway, KDE, and niri) is hard to reverse and the result of a real trade-off, so it should be recorded as an ADR.
|
||||
- The aggregator plus namespaced-group Module pattern is a deliberate departure from the flat, Host-as-full-checklist convention, and is worth recording as a short ADR or a conventions note so its intent is not relearned.
|
||||
- The laptop MVI's out-of-scope line attributing Nvidia to zeus is factually wrong: zeus runs an AMD GPU, and Raichu (a server with no desktop) is the only Nvidia machine.
|
||||
The laptop MVI is a historical document and is left unchanged, so the accurate fact is recorded here and in ADR 0003 instead.
|
||||
- The ported keybind scheme is derived from the operator's prior KDE Plasma 6 configuration (nine numbered desktops, `Super`+number bindings, a terminal on `Super`+`Return`, Caps mapped to Escape, and custom per-desktop tile layouts), which lives in this repo's git history under the old reference config.
|
||||
- Neogaia is Intel and zeus is AMD, both of which drive Wayland without driver caveats; this is what lets one keyboard-first compositor serve both Hosts rather than forcing a per-Host divergence.
|
||||
@@ -1,92 +0,0 @@
|
||||
## Problem Statement
|
||||
|
||||
I'm returning to NixOS after ~2 years away, and I want to start by moving my laptop (`neogaia`, a Dell XPS 13 9380 currently running CachyOS) onto it. My old config still exists but is stale and written in a style I no longer want to copy verbatim. Eventually this same config has to grow to cover my desktop and three servers, so whatever I build for the laptop has to be a clean, scalable foundation — not a throwaway.
|
||||
|
||||
Reimaging the laptop is destructive and I only get one machine, so I need a tightly-scoped, well-understood **minimum viable install (MVI)**: the smallest config that boots the laptop into a usable state I can then iterate on live, without risking a half-defined system that strands me at a dead console.
|
||||
|
||||
## Solution
|
||||
|
||||
Rebuild the `Skeleton` and a single `neogaia` `Host` to the point where the laptop:
|
||||
|
||||
- boots from an encrypted disk (LUKS + btrfs + zram),
|
||||
- comes up on wifi,
|
||||
- lets me log into a console as my user and run `nixos-rebuild switch`,
|
||||
- and already carries my core terminal tooling (fish, tmux, nvim, Claude Code).
|
||||
|
||||
Everything graphical and everything multi-host is deliberately left for later iterative passes, which are safe because a mistake then is "edit and rebuild," not "reimage." The MVI is the one step that must be right *before* reimaging; the rest is reversible.
|
||||
|
||||
The install itself is done from the NixOS live ISO by cloning the repo from my Gitea and running a single `disko-install` against the `neogaia` `Host`, then setting a bootstrap password by hand.
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As the operator, I want the `Skeleton` rewritten around my old scalable ideas (the `Auto-loader`, the `Enable convention`, per-`Host` layout), so that the config stays legible and shareable across all five future machines without me copying stale code.
|
||||
2. As the operator, I want the flake hand-rolled and cleaned up (no framework layer), so that the whole plumbing stays readable in one place for a config that only targets a handful of `x86_64-linux` machines.
|
||||
3. As the operator, I want every `Module` auto-discovered and imported but inert until a `Host` sets its `enable` flag, so that each `Host` reads as a checklist of features.
|
||||
4. As the operator, I want a `nixos-unstable` base with an `unstable overlay` and a `stable overlay`, so that I can run rolling by default but reach up to bleeding-edge or down to rock-solid on a per-package basis.
|
||||
5. As the operator, I want home-manager integrated as a NixOS module with global packages, so that one `nixos-rebuild switch` builds both the system and my user environment atomically.
|
||||
6. As the operator, I want my user modelled as an explicit option defaulting to `alexion` (no impure environment lookup), so that the config is reproducible and honest about who the user is.
|
||||
7. As the operator, I want the laptop's disk declared with `disko` as encrypted btrfs plus zram swap, so that the install is reproducible and the laptop is encrypted at rest.
|
||||
8. As the operator, I want the system to prompt for the LUKS passphrase at boot via systemd-boot and the initrd, so that the encrypted disk unlocks on a normal boot.
|
||||
9. As the operator, I want the CachyOS kernel from chaotic-nyx with the chaotic binary cache wired in from the first build, so that I get the performance/feel I'm used to without compiling the kernel from source.
|
||||
10. As the operator, I want Intel microcode and the redistributable firmware for the QCA6174 wifi included, so that the laptop's hardware works out of the box.
|
||||
11. As the operator, I want NetworkManager enabled, so that I can join wifi easily from the console.
|
||||
12. As the operator, I want an SSH daemon running, so that I can drive the rest of the setup remotely if the console is inconvenient.
|
||||
13. As the operator, I want my user in `wheel` with a manually-set bootstrap password, so that I can log in and use sudo on first boot without committing any secret to a public repo.
|
||||
14. As the operator, I want fish as my default login shell, configured natively via home-manager with my `cachyos-config.fish` translated (greeting, bat-manpager, `done` and bang-bang plugins, helper functions, eza/nav aliases) and all Arch/pacman-specific parts dropped or replaced with NixOS equivalents, so that my shell feels like home but is correct for NixOS.
|
||||
15. As the operator, I want tmux configured natively via home-manager using my exact existing `tmux.conf` text, so that my terminal multiplexer is identical to today with no plugin manager needed.
|
||||
16. As the operator, I want my nvim config brought in verbatim (lazy.nvim managing its own plugins) via a writable out-of-store symlink, with `git`/`gcc`/`ripgrep`/`fd` provided by Nix, so that my editor is identical to today and lazy.nvim can still update and write its lockfile.
|
||||
17. As the operator, I want Claude Code installed declaratively and authenticatable without a browser on the laptop, so that I can use it over the console/SSH via the paste-code flow or an API key.
|
||||
18. As the operator, I want timezone `America/New_York`, locale `en_GB.UTF-8`, and console keymap `us` set, so that the base system matches my locale preferences.
|
||||
19. As the operator, I want to install by cloning the repo from my Gitea onto the live ISO and running `disko-install` against `neogaia`, so that I avoid self-signed-TLS/auth problems with flake fetching during install.
|
||||
20. As the operator, I want the `Skeleton` designed so that per-`Host` disk layouts, per-`Host` kernels, and preserved ZFS pools are all expressible, so that the same foundation extends to the desktop and the three servers later without restructuring.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
**Skeleton**
|
||||
- Hand-rolled flake, rewritten and trimmed; no flake-parts.
|
||||
- `Auto-loader` rewritten: recursively discovers and imports every `Module` under the modules tree without the old null-placeholder traversal hack; a single discovery helper feeds the `Host` imports. The old `nixosModules` flake output is dropped.
|
||||
- Helper lib trimmed to the `Auto-loader`, the host-builder, and the script-from-file helper. `with lib.my` replaced by explicit `inherit`s throughout. `enable` flags use the stdlib enable-option helper rather than bespoke sugar.
|
||||
- `nixos-unstable` as the base channel. An `unstable overlay` exposes `nixpkgs-unstable` packages; a `stable overlay` exposes the latest stable release (`nixos-26.05`). chaotic-nyx added as an input with its overlay and binary cache from the start.
|
||||
- home-manager sourced from `nix-community`, tracking master with nixpkgs followed, integrated as a NixOS module with global packages and user packages.
|
||||
- User modelled as an explicit option defaulting to `alexion`, in `wheel`, driving the system user and the home-manager user in lockstep.
|
||||
|
||||
**neogaia Host**
|
||||
- Disk declared via `disko`: LUKS-encrypted btrfs with subvolumes plus zram swap. systemd-boot on an EFI system partition; initrd LUKS unlock.
|
||||
- CachyOS kernel selected via a small per-`Host` kernel mechanism; chaotic substituter and trusted key in the Nix settings.
|
||||
- Intel microcode; redistributable firmware enabled for the QCA6174 wifi. NetworkManager for networking. A zram toggle `Module` enabled here.
|
||||
- SSH daemon enabled. Baseline CLI (git, editor, flakes) present. Claude Code installed declaratively.
|
||||
- fish `Module`: native home-manager configuration; translated aliases/functions/plugins/init; set as the default login shell. tmux `Module`: native home-manager, exact existing config text inlined. nvim `Module`: verbatim config placed as a writable out-of-store symlink with runtime dependencies provided by Nix.
|
||||
- Locale, timezone, and keymap set to the detected values.
|
||||
|
||||
**Install flow**
|
||||
- Repo pushed to Gitea first. From the NixOS live ISO: join wifi, clone the repo locally, run `disko-install` against the `neogaia` `Host` with the chaotic substituter passed to the install-time daemon, set bootstrap passwords via `nixos-enter`, reboot.
|
||||
|
||||
**Secrets (design only in MVI)**
|
||||
- Per ADR 0001, secrets use `sops-nix` with age keys derived from each `Host`'s SSH host key. The MVI does not wire any secret, because a `Host`'s age key does not exist until its first install generates the SSH host key. The bootstrap password is set by hand and never committed; moving passwords to a `hashedPasswordFile` backed by a sops secret is the first post-boot task, out of scope here.
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
- A good test here asserts externally-observable evaluation/build success of the whole `Host`, not the internals of any individual `Module`.
|
||||
- **Primary seam (required):** the `neogaia` `Host` evaluates and its system toplevel builds. Building the toplevel drives the entire `Skeleton` — the `Auto-loader` discovering every `Module`, all three overlays resolving, home-manager integration, and every enabled module's config merging without conflict — plus the `disko` layout, which builds from the same tree. Nearly all config-authoring errors surface at this seam short of booting real hardware.
|
||||
- No unit-level tests of individual modules; the config-merge model makes the whole-`Host` build the meaningful unit, and it is the highest available seam.
|
||||
- Prior art: none in this repo yet (it starts empty); this build-the-toplevel check is the pattern to reuse for every future `Host`.
|
||||
- The genuine end-to-end confirmation is the real reimage, which is manual and irreversible by nature and is not automated.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Any graphical environment: Wayland-vs-i3 choice, greeter/display manager, theming (Nord via Stylix or otherwise), fonts, terminal emulator, browser, general desktop apps, gaming (Steam/Lutris/proton-cachyos), emulation.
|
||||
- Full `sops-nix` wiring and moving passwords off the bootstrap value (immediate post-boot follow-up, but not MVI).
|
||||
- Migrating nvim to a native home-manager configuration with Nix-managed plugins.
|
||||
- chaotic-nyx packages beyond the kernel (`mesa-git`, `proton-cachyos`, `scx` schedulers).
|
||||
- Flatpak strategy (`nix-flatpak` vs dropping the old imperative helper).
|
||||
- The desktop `Host` (`zeus`), including Nvidia.
|
||||
- The three servers: deployment model, service migration (plex/arr/kavita/nfs/torrent-through-protonvpn), ZFS wiring and pool import, backups/monitoring, and per-server kernel/channel pinning.
|
||||
- VM-based CI (`nixosTest` boot assertions) — explicitly a future addition, not part of this deliverable.
|
||||
|
||||
## Further Notes
|
||||
|
||||
- **Bootstrap ordering:** the flake must exist on Gitea before the install can consume it, and the manual password step keeps the public repo free of any secret while still yielding a login on first boot.
|
||||
- **Gitea is a bootstrap dependency:** every NixOS install pulls the config from self-hosted Gitea, so the Gitea host must stay reachable during any install — relevant when sequencing the servers so the migration never locks the operator out of their own configs.
|
||||
- **chaotic cache at install time:** the install-time Nix daemon on the live ISO must have the chaotic substituter configured, or it compiles the CachyOS kernel from source on the USB stick.
|
||||
- **Extends to future Hosts by construction:** disk layout, kernel, and channel are all per-`Host` concerns in the `Skeleton`, and existing ZFS pools are preserved by import rather than declared through `disko`. This is what lets the desktop and the three servers join later without reworking the foundation.
|
||||
- **Theme target is Nord** (the current CachyOS setup is Nord across terminal, tmux, and nvim), superseding the old repo's Dracula — relevant when the theming branch is grilled.
|
||||
@@ -1,76 +0,0 @@
|
||||
## Problem Statement
|
||||
|
||||
I run Claude Code as my coding agent on `neogaia`, wired deeply into this flake: skills, a sudo-guard hook, shared agent instructions, and the whole `.claude/` workflow.
|
||||
Pi is a young, fast-moving, self-modifying terminal coding agent that I want to evaluate as an alternative harness.
|
||||
I need to install it on the laptop in a way that lets me try it honestly — same model, same account — without unpicking any of the Claude Code setup and without committing myself to Pi before it has earned a permanent place.
|
||||
|
||||
The evaluation only means something if the one variable under test is the harness itself, and if backing Pi out later is trivial.
|
||||
|
||||
## Solution
|
||||
|
||||
Add a new `pi` Module that installs Pi for the primary user on `neogaia`, enabled by the Enable convention like every other feature.
|
||||
It flips the home-manager `programs.pi-coding-agent` module on and freezes exactly one file — `settings.json` — pinning the provider and model so Pi runs the same brain as Claude Code (Anthropic, Opus) and disabling analytics so Pi never attempts a runtime write to that frozen file.
|
||||
|
||||
Everything else is left to Pi's own writable state directory (`~/.pi/agent/`): no agent context, no skills, no extensions, no keybindings, no custom model providers.
|
||||
Pi authenticates by reusing my existing Claude subscription, and that credential is deliberately left unmanaged by the flake so no secret touches the repo and re-auth survives rebuilds — exactly as the `claude-code` Module already treats its login.
|
||||
|
||||
The result is a minimal, non-disruptive, side-by-side experiment: Claude Code stays the daily driver, Pi sits alongside it, and removing Pi is a one-line `enable` flip.
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As the operator, I want Pi installed as its own auto-discovered Module that stays inert until a Host enables it, so that Pi reads as one more `enable = true` line on `neogaia` and never rides silently onto future Hosts.
|
||||
2. As the operator, I want Pi enabled only on `neogaia`, so that the experiment is contained to the machine I actually drive.
|
||||
3. As the operator, I want Pi to run Anthropic's Opus by default, matching Claude Code's model, so that any difference I observe between the two is attributable to the harness and not the model.
|
||||
4. As the operator, I want Pi's provider and default model pinned reproducibly in the flake, so that the same Pi configuration would rebuild identically on any Host.
|
||||
5. As the operator, I want Pi's analytics disabled in that same pinned configuration, so that Pi never attempts the one runtime write it would otherwise make to the frozen settings file.
|
||||
6. As the operator, I want Pi to authenticate by reusing my existing Claude subscription rather than a separate API key, so that the comparison hits the same account at zero marginal cost.
|
||||
7. As the operator, I want Pi's credential left unmanaged by the flake, so that no secret is committed to a public repo and my authentication survives rebuilds.
|
||||
8. As the operator, I want Pi installed with no agent context, no skills, and no extensions, so that I see Pi's native behaviour rather than a port of the Claude Code setup.
|
||||
9. As the operator, I want Pi to keep full ownership of its writable state directory, so that its self-modifying behaviour — generated extensions, skills, prompt templates, installed packages, sessions — works unimpeded.
|
||||
10. As the operator, I want Pi sourced from the base package set and bumped with the normal flake update, so that it stays reasonably fresh without a second package set evaluated for one tool.
|
||||
11. As the operator, I want the Module laid out as a directory rather than a single file, so that promoting Pi later — adding rendered skills or extensions — is an additive change rather than a restructure.
|
||||
12. As the operator, I want backing Pi out to be a single `enable` flip, so that an experiment that does not pan out leaves no residue.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
**pi Module**
|
||||
- A new Module under its own directory, following the shape of the existing `claude-code` Module, declaring a single `enable` option under the `modules` tree and guarding its body with the Enable convention.
|
||||
- The directory layout (rather than a single file) is chosen so that later rendering of a skills or extensions source is an additive edit, not a move.
|
||||
- On enable, the Module turns on the home-manager `programs.pi-coding-agent` module for the primary user. That upstream module ships the `pi-coding-agent` package (base package set) and manages the state directory's declared files.
|
||||
- The Module freezes exactly one file through that upstream module's `settings` option: the default provider set to Anthropic, the default model set to Opus (the exact model-id string confirmed against Pi's own model catalogue at build time), and analytics disabled.
|
||||
- No other upstream option is set: `context`, `models`, `keybindings`, `extraPackages`, and `configDir` are all left at their defaults, so home-manager renders nothing but `settings.json` into `~/.pi/agent/` and Pi owns every other path there.
|
||||
- Rationale for the single frozen file: among the files the upstream module can render, `settings.json` is the only one Pi writes at runtime, and only its analytics keys — disabling analytics removes even that, so freezing it is safe and never fights Pi's self-modification, which targets other paths entirely.
|
||||
- Credentials are out of the flake by design. Pi reuses the Claude subscription via its own login, and the resulting token lives under Pi's state directory, which home-manager does not overwrite — mirroring the `claude-code` Module's treatment of its login.
|
||||
- Security posture is inherited, not added: the `claude-code` Module already widens the sudo credential cache system-wide for the primary user, so Pi's `bash` tool can spend a warm credential. Pi does not pass through Claude Code's cold-cache sudo guard, and no equivalent guard is added for Pi in this deliverable. This is an accepted, bounded posture for a supervised single-user experiment.
|
||||
|
||||
**neogaia Host**
|
||||
- The `neogaia` Host enables the new Module with a single `enable = true`, alongside its existing feature list.
|
||||
- No other Host is touched; the desktop and servers do not yet exist in the flake and would each opt in on their own terms.
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
- A good test here asserts externally-observable evaluation and build success of the whole `neogaia` Host, not the internals of the Module. Installing a Module is config authoring, and the meaningful unit is the Host it composes into.
|
||||
- **Primary seam (required, reused):** the `neogaia` Host evaluates and its system toplevel builds via `nix flake check` (the `checks.x86_64-linux.neogaia` target). Building the toplevel drives the Auto-loader discovering the new Module, the `programs.pi-coding-agent` home-manager integration resolving, the frozen `settings.json` rendering, and every enabled Module's config merging without conflict.
|
||||
- No new seam is introduced. This is the single high seam that `laptop-mvi.md` established and that every Module in this repo is verified through; an install-a-Module feature does not justify a second one.
|
||||
- No unit-level test of the Module in isolation. The config-merge model makes the whole-Host build the highest and most meaningful seam.
|
||||
- Prior art: the existing `claude-code`, `gitea-axi`, `fish`, `tmux`, and `nvim` Modules are all verified this way — enabled on `neogaia`, exercised by the toplevel build.
|
||||
- The genuine end-to-end confirmation — launching `pi`, authenticating against the Claude subscription, and running the agent — is a manual post-build action on the real machine and is not automated, consistent with how interactive login is handled for Claude Code.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Any harvest/promote pipeline for sharing Pi's self-modifications across machines — rendering a repo-held skills or extensions source into Pi's state directory. Deferred until Pi earns a permanent slot.
|
||||
- A shared skill source between Pi and Claude Code for a fairer comparison, exploiting their common `SKILL.md` skill format.
|
||||
- A Pi-specific sudo guard (or any tool-permission guard) built as a Pi extension.
|
||||
- An `AGENTS.md` context carrying the operator's cross-agent house rules (commit conventions, no attribution trailer, markdown and filename rules). Its absence means Pi's commits will not automatically follow those conventions during the experiment; this is accepted.
|
||||
- Custom model providers (`models.json`), custom keybindings, and any non-Anthropic provider.
|
||||
- Moving Pi to the `unstable` overlay for head-of-channel freshness.
|
||||
- Wiring the credential through a secret store; sops is not yet wired on this Host, and Pi follows the same unmanaged-credential path as Claude Code until it is.
|
||||
- Enabling Pi on any Host other than `neogaia`.
|
||||
- An ADR recording the "promote agent self-modifications into the flake rather than sync mutable agent state" stance. It is not enacted by this deliverable; if Pi is promoted and the harvest pipeline is built, that becomes a real, repo-wide decision — covering Claude Code too — worth recording then.
|
||||
|
||||
## Further Notes
|
||||
|
||||
- **Why the single frozen file matters:** Pi's real self-modification surface (generated extensions, skills, prompt templates, installed package code, sessions, trust decisions) lives in paths under `~/.pi/agent/` that the upstream home-manager module never manages, regardless of what the Module declares. Freezing `settings.json` therefore constrains none of it, and disabling analytics removes the only runtime write that file would otherwise receive.
|
||||
- **Promotion path is left open by construction:** the directory-shaped Module and the untouched `configDir` mean that, if Pi sticks, a repo-held skills or extensions source can be rendered into the state directory with a writable-directory / read-only-leaf layout — the same mechanism this repo already uses for Claude Code skills — without restructuring anything decided here.
|
||||
- **Fair-comparison intent:** matching the model (Opus) and the account (the Claude subscription) is deliberate, so the experiment isolates the harness. Choosing a lighter model or a separate key would introduce a second variable and blur the read.
|
||||
- **Reversibility:** because only `settings.json` is frozen and the credential and all self-modification state live outside the flake, disabling the Module removes Pi cleanly, leaving Pi's own state directory as the only residue on disk.
|
||||
@@ -1,148 +0,0 @@
|
||||
## Problem Statement
|
||||
|
||||
`neogaia` is up and running the NixOS it builds, but its login password was set by hand through `nixos-enter` during the install and lives only on that laptop's disk.
|
||||
It is the one piece of the machine that is not declared, not reproducible, and not recoverable — a reimage loses it, and no other `Host` can inherit it.
|
||||
|
||||
The same gap blocks everything queued behind it.
|
||||
An Anthropic API key cannot be provisioned declaratively, a WireGuard key cannot be committed, and the three planned servers cannot carry service credentials.
|
||||
The repo is public and mirrored to GitHub, so none of that material can be committed in plaintext.
|
||||
|
||||
There is a second, subtler cost.
|
||||
SSH host keys are currently generated fresh by `sshd` on each install, so reimaging any machine invalidates its host identity and breaks `known_hosts` for every client that ever connected to it.
|
||||
|
||||
`ADR 0001` chose `sops-nix` for this, but its stated mechanism — age keys derived from each `Host`'s SSH host key — turns out to be the wrong topology, and its consequences no longer describe what should be built.
|
||||
|
||||
## Solution
|
||||
|
||||
Wire `sops-nix` into the `Skeleton` as unconditional plumbing, with a two-tier age identity model.
|
||||
|
||||
An **admin identity** stored outside the repo entirely, in Proton Pass, is a recipient of every secrets file.
|
||||
It is the durable recovery path: it outlives every machine, is reachable from any device including a live ISO, and is the credential that authorizes adding a new `Host` as a recipient.
|
||||
|
||||
A **host identity** — a dedicated age key on each machine's encrypted root — is a recipient of only that machine's own secrets plus the shared file.
|
||||
It is generated on the machine, never leaves it, and is deliberately not derived from the SSH host key, which is what frees the SSH host keys to become secrets in their own right.
|
||||
|
||||
Secrets are `sops`-encrypted into this same public repo.
|
||||
The ciphertext is safe to publish, and the only artifact that would not be — the admin private identity — is never committed at all.
|
||||
No second repository is introduced.
|
||||
|
||||
The first pass moves the login password off its hand-set value and makes `neogaia`'s SSH host keys stable across reimages.
|
||||
That exercises both decryption paths — the early one that runs before user creation, and the ordinary activation one — proving the machinery end to end on the two secrets that are actually needed today.
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As the operator, I want my login password declared as an encrypted secret rather than typed into a running machine, so that it is reproducible and survives a reimage like everything else in the flake.
|
||||
2. As the operator, I want secrets encrypted into the existing public repo rather than a separate private one, so that there is one repository to clone and no bootstrap credential is needed to reach my own configuration during an install.
|
||||
3. As the operator, I want an admin identity held in my password manager and never committed, so that nothing brute-forceable is published and I can recover every machine from a device I have never used before.
|
||||
4. As the operator, I want each `Host` to hold its own age identity, so that a compromised server cannot decrypt my laptop's secrets.
|
||||
5. As the operator, I want a shared secrets file alongside per-`Host` ones, so that material common to every machine is stored once rather than duplicated five times.
|
||||
6. As the operator, I want my workstation to be a recipient of only its own secrets, so that the admin identity stays a break-glass credential rather than something sitting unlocked on a laptop.
|
||||
7. As the operator, I want `neogaia`'s SSH host keys stored as secrets and restored at activation, so that reimaging the laptop does not invalidate its host identity or break `known_hosts` for clients.
|
||||
8. As the operator, I want the secrets machinery to live in the `Skeleton` rather than behind an `enable` flag, so that it reads as plumbing every `Host` depends on rather than an optional feature.
|
||||
9. As the operator, I want individual secrets declared next to the configuration that consumes them, so that a reader finds the secret where they find its use.
|
||||
10. As the operator, I want a mistyped secret name or a missing secrets file to fail the build, so that errors surface at `nix flake check` rather than at boot.
|
||||
11. As the operator, I want the procedure for provisioning a new `Host`'s identity written down, so that installing the desktop and the servers does not require rederiving the key ceremony under pressure.
|
||||
12. As the operator, I want the recovery path documented for a machine whose identity was provisioned wrongly, so that a failed first boot is a known procedure rather than an improvised one.
|
||||
13. As the operator, I want the editing workflow documented, so that I know which secrets I can change from my laptop and which require unlocking the admin identity.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
**Identity topology**
|
||||
|
||||
- Two tiers of recipient: one admin identity, plus one identity per `Host`.
|
||||
Every secrets file is encrypted to admin and to whichever `Host`s legitimately read it.
|
||||
- The admin identity is stored as a secure note in Proton Pass and is never committed in any form.
|
||||
Only its public recipient appears in the repo.
|
||||
No passphrase-encrypted copy is committed: the vault already provides passphrase protection with rate limiting, whereas a committed copy would be offline-brute-forceable by anyone who clones the repo, indefinitely.
|
||||
- Each `Host` identity is a dedicated age key on the LUKS-encrypted root, generated on that machine and never transmitted.
|
||||
It is *not* derived from the SSH host key.
|
||||
Decoupling them is what allows the SSH host keys to be secrets themselves; deriving one from the other would be circular.
|
||||
- An admin recipient on every file is structurally required, not a convenience.
|
||||
A file readable only by its own `Host` becomes permanently unrecoverable the moment that machine is wiped, and adding any new recipient must be done by someone who can already decrypt.
|
||||
|
||||
**Repository layout**
|
||||
|
||||
- One repository, public, unchanged.
|
||||
A private repository was considered and rejected: cloning it requires credentials that would themselves become bootstrap material at install time, reintroducing the hand-carried secret the design otherwise eliminates, in exchange for protecting content that is already safe to publish.
|
||||
- A `sops` configuration file and a secrets directory at the repo root.
|
||||
- One secrets file per `Host`, encrypted to admin plus that `Host`.
|
||||
- One shared secrets file encrypted to admin plus every `Host`.
|
||||
- `neogaia` is a recipient of its own file and the shared file only.
|
||||
Editing another machine's secrets requires unlocking the admin identity for that session, which is the intended friction.
|
||||
|
||||
**Secrets in this pass**
|
||||
|
||||
- The primary user's password hash lives in the shared file, consumed through `hashedPasswordFile`.
|
||||
It is marked as needed for users, which makes `sops-nix` decrypt it in an earlier activation stage than ordinary secrets, before accounts are created.
|
||||
This is the one ordering subtlety in the design and is the reason the `Host` identity must sit on the root filesystem rather than anywhere later-mounted.
|
||||
- Storing the password hash in the shared file rather than per-`Host` is deliberate.
|
||||
The same password will be used on every machine, so duplicating the identical hash across per-`Host` files would not reduce what an attacker learns — it would only make rotation a five-file edit.
|
||||
- `neogaia`'s SSH host **private** keys live in its own `Host` file, with `sshd`'s generated host keys disabled and pointed at the decrypted paths instead.
|
||||
- SSH host **public** keys are committed in plaintext.
|
||||
They are not secret — publishing them is their function — and encrypting them would impose a re-key cycle every time one changes.
|
||||
|
||||
**Placement in the flake**
|
||||
|
||||
- The machinery goes in the `Skeleton` as unconditional configuration, not behind an `enable` flag.
|
||||
This is a deliberate departure from the `Enable convention`, on the same grounds as the overlays and the flakes settings: every `Host` will carry secrets, so the flag would be permanently `true`, and the plumbing is not a feature a `Host` chooses.
|
||||
- The `Skeleton` carries only the machinery — the flake input, the identity file location, and the default secrets file.
|
||||
Individual secrets are declared wherever they are consumed, so the password secret sits beside the user declaration it feeds and the SSH host keys beside the `sshd` configuration.
|
||||
- `sops-nix` is added as a flake input following the base `nixpkgs`.
|
||||
|
||||
**Operational procedures**
|
||||
|
||||
- For `neogaia`, which is already installed and running, provisioning happens live: generate the identity on the machine, add its recipient, re-key the affected files with the admin identity, rebuild.
|
||||
No reimage and no live ISO are involved.
|
||||
- For a `Host` that does not yet exist, provisioning happens on the live ISO *before* the install: generate the identity, add its recipient, re-key, write the identity onto the target root, then install.
|
||||
The first boot then has everything it needs and cannot fail for want of a key.
|
||||
The install already builds from a local clone, so no push is required mid-procedure; the recipient change is committed afterward.
|
||||
- Both procedures, the editing workflow, and the live-ISO recovery path are documented in the existing install document rather than a new one.
|
||||
|
||||
**Decision record**
|
||||
|
||||
- A new ADR supersedes `ADR 0001`, which is marked superseded.
|
||||
`ADR 0001`'s choice of `sops-nix` over `agenix` still holds and carries forward in a sentence, but its key-derivation mechanism is replaced and its stated consequence — that each new `Host` registers its SSH host public key as a recipient — is inverted, since SSH host keys are now secrets rather than the root of trust.
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
- A good test here asserts externally-observable build and activation behaviour, not the internals of `sops-nix`.
|
||||
Nothing in this feature is our own logic to unit-test; it is configuration wiring, and the meaningful assertions are that the whole `Host` still evaluates and that the secrets actually materialize on a real machine.
|
||||
- **Primary seam (required):** `nix flake check` building the `neogaia` system toplevel, the same seam the laptop MVI established.
|
||||
It carries real weight for this feature rather than merely compiling: `sops-nix` validates secrets files at evaluation time by default, so a missing file, a file that is not valid `sops` output, or a declared secret whose key is absent from it all fail the build.
|
||||
Mistyped secret names surface here rather than at boot.
|
||||
- **Confirmation (manual):** a real activation on `neogaia`.
|
||||
This is what proves decryption itself — that the `Host` identity is readable at the right stage, that secrets appear with the declared ownership and mode, that `sshd` adopts the restored host keys, and that login works against `hashedPasswordFile`.
|
||||
It cannot be automated without a machine that holds a real identity, and is treated like the reimage in the laptop MVI: manual by nature.
|
||||
- No new seams are introduced.
|
||||
The existing whole-`Host` build remains the highest available point, and the config-merge model makes it the meaningful unit.
|
||||
- Prior art: the toplevel-build check established by the laptop MVI, already wired as the flake's `checks` output.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- The Anthropic API key.
|
||||
It is the natural next secret, but it is consumed as an environment variable rather than a file path, and conflating that shape with the bootstrap work would obscure both.
|
||||
- The WireGuard/ProtonVPN key, which has no `Module` to consume it yet.
|
||||
- Declarative wifi credentials.
|
||||
NetworkManager profile secrets are fiddly and joining from the console currently works.
|
||||
- Fleet-wide SSH host verification.
|
||||
With one `Host` there is nothing to verify against, and the choice of whether to identify machines by name or address should be made when a second machine exists and the answer is known rather than guessed.
|
||||
- Provisioning any identity for a `Host` that does not exist yet.
|
||||
The procedure is documented; no key is generated for `zeus` or the servers.
|
||||
- Rotating the LUKS passphrase or coupling it to secret decryption.
|
||||
- Hardware-token identities.
|
||||
A YubiKey can be added later as an additional admin recipient without changing any decision here.
|
||||
- Any change to how the flake is fetched during an install.
|
||||
|
||||
## Further Notes
|
||||
|
||||
- **The lockout risk is confined to fresh installs.**
|
||||
On `neogaia` the transition is safe: if activation fails, the rebuild fails and the running generation persists with the existing hand-set password intact.
|
||||
A machine being installed for the first time has no such fallback, because the password now arrives only from a decrypted secret — which is exactly why its identity is provisioned before the first boot rather than after it.
|
||||
- **The admin identity is the single point of recovery**, and its durability is now a property of Proton Pass rather than of any machine or repository.
|
||||
Losing the vault without a backup means losing the ability to add recipients or recover a wiped `Host`, even though every currently-running machine keeps working from its own identity.
|
||||
- **Stable SSH host keys were nearly given up** in favour of deriving identities from them, and were recovered by inverting the dependency.
|
||||
The rule that made it work generalizes: exactly one secret per machine must arrive out of band, and making that one thing a purpose-built key rather than a repurposed one keeps everything else declarable.
|
||||
- **Adding a `Host` is a re-key, not a re-encrypt.**
|
||||
A `sops` file holds a single data key encrypted once per recipient, so registering a new machine rewrites only that metadata, and the cost stays constant as the fleet grows to five.
|
||||
- **This is what `ADR 0001` chose `sops-nix` for.**
|
||||
The shared-plus-per-`Host` file split with overlapping recipients is precisely the multi-recipient, grouped-file model that decided against `agenix`; the topology change replaces how identities are obtained, not why the tool was picked.
|
||||
Reference in New Issue
Block a user