Compare commits

..
Author SHA1 Message Date
finn.markwitzandClaude Sonnet 5 202eca2d03 fix(homepage): merge service groups by name to stop manual entries being dropped
homepage-dashboard resolves services.yaml groups by name using first-match
lookup, so the module-contributed "Services" group and manual-services.nix's
"Services" group (two separate top-level YAML list entries) caused the
second one to be silently discarded. Merge by group name before handing the
list to services.homepage-dashboard.services.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbLevsLozTCcCWe1SoUGe1
2026-09-21 15:27:07 +02:00
finn.markwitz 2ff3042cd9 Merge pull request 'feat(mibook): add swapfile and nodejs dev tooling' (#13) from feat/mibook-swapfile into main
Reviewed-on: #13
2026-09-21 15:21:22 +02:00
finn.markwitzandClaude Sonnet 5 d219f2b61c feat(mibook): add swapfile and nodejs dev tooling
Add an 8G swapfile for headroom during nix builds (7.7G RAM, no prior
swap) and bring nodejs (bundles npm) into the development profile.
Also picks up latest flake.lock input updates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5fRUDLRzDhoKhEqa69cyR
2026-09-21 15:13:03 +02:00
finn.markwitz 5a2a649244 feat: Sops for Jupiter Setup 2026-09-19 18:48:53 +02:00
finn.markwitz 4691eb1aa4 Merge pull request 'Feat/sops nix' (#12) from feat/sops-nix into main
Reviewed-on: #12
2026-09-14 13:06:45 +02:00
finn.markwitz 9dbd6ff71d Merge branch 'main' into feat/sops-nix 2026-09-14 13:06:33 +02:00
finn.markwitzandClaude Sonnet 5 807eea7e4f feat(homepage): add Athena Dashboard link to jupiter's homepage
Hermes Agent's dashboard runs on mibook, but the homepage-dashboard
service only runs on jupiter, so wire it in as a manual cross-host
entry rather than via my.homepage.services (which is scoped per-host
and would be inert on mibook).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sux8hmeb2qadqPqbJ1zNzL
2026-09-10 11:09:48 +02:00
finn.markwitzandClaude Sonnet 5 67973559c0 feat(hermes-agent): expose dashboard on LAN with basic auth
Bind the backend to 0.0.0.0:9119 in dashboard mode so the web UI is
reachable from the local network, open the port in the firewall, and
add dashboard username/password credentials to the mibook hermes-env
secret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sux8hmeb2qadqPqbJ1zNzL
2026-09-10 10:56:50 +02:00
finn.markwitz c4b29e1e06 feat(mibook): populate Hermes Agent credentials 2026-09-08 21:10:28 +02:00
finn.markwitzandClaude Sonnet 5 0756eb94cd docs: correct spec+plan for self-vs-inputs in imports
The fix subagent found that inputs.hermes-agent.nixosModules.default
inside `imports` causes infinite recursion in this repo's wiring
(inputs comes from _module.args, which merges after imports resolve;
self comes from specialArgs, which is available at import-time).
Tested both ways. Update docs to show self.inputs, matching commit
31e1910.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 17:51:31 +02:00
finn.markwitzandClaude Sonnet 5 31e1910d75 fix(mibook): import hermes-agent module from the profile, not machines/configuration.nix
machines/configuration.nix wired hermes-agent.nixosModules.default into
mibook's mibook-only modules list. modules/environments/default.nix is
machine-agnostic, so jupiter still evaluated a
services.hermes-agent = lib.mkIf false { ... } definition for an option it
never declared as existing — NixOS errors on that regardless of mkIf's
condition, breaking nixosConfigurations.jupiter (and the repo's sole CI
job) entirely.

Fix: import the upstream module unconditionally from inside
modules/environments/hermes-agent/default.nix itself, so the option is
declared everywhere modules/environments/default.nix loads, and only ever
turned on where my.profiles.hermes-agent.enable = true (mibook). Also adds
users.users.finn.extraGroups = [ "hermes" ], required for the hermes CLI to
be usable per the corrected design. machines/configuration.nix no longer
mentions hermes-agent.

Deviates from the reconciled spec/plan text in one respect: uses `self`
(from specialArgs) instead of `inputs` (from _module.args) to reach the
flake input inside `imports`. Using `inputs` there reproduced infinite
recursion — NixOS resolves a module's imports before _module.args
assignments are merged, so an import list cannot depend on an arg that
itself comes from config. `self` is available at import-time via
specialArgs, `inputs` is not, in this repo's wiring. See report for detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 17:50:11 +02:00
finn.markwitzandClaude Sonnet 5 0249aae2ae docs: correct spec+plan per final whole-plan review findings
Final review (opus) found a Critical bug (jupiter's nixosConfigurations
stopped evaluating because hermes-agent.nixosModules.default was wired
through machines/configuration.nix's mibook-only module list, while
the profile module itself is loaded on both machines) plus three
Important gaps: secrets persist to /var/lib/hermes/.hermes/.env, not
/run/secrets; Telegram/Email need explicit ALLOWED_USERS or they're
wide open / silently drop messages; addToSystemPackages's CLI needs
group membership finn didn't have. All four corrected in the docs
here; the actual code fix is a separate dispatched task.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 17:44:39 +02:00
finn.markwitzandClaude Sonnet 5 ac32aca351 docs: correct plan's sops build-check assumption
Task 1's implementer found sops.validateSopsFiles makes secret-key
existence a build-time check (via manifest.json.drv), not an
activation-time one as the plan assumed. Corrects Step 10 to reflect
reality rather than leave future readers misled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 17:22:09 +02:00
finn.markwitz 731678ecac feat(mibook): add Hermes Agent profile module 2026-09-08 17:20:40 +02:00
finn.markwitzandClaude Sonnet 5 32e0faa82c chore: ignore .superpowers/ scratch workspace
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 17:01:54 +02:00
finn.markwitzandClaude Sonnet 5 a83f7e63a0 docs: implementation plan for Hermes Agent on mibook
Task 1 is agent-executable repo wiring (flake input, profile module,
secret declaration), build-checked with nix build. Tasks 2-5 are an
operator-run runbook: OAuth login, Anthropic billing check, Telegram
bot setup, and typing real secrets into $EDITOR via sops - none of
that is meant to pass through an agent session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 17:00:38 +02:00
finn.markwitzandClaude Sonnet 5 6212b4fcf7 docs: design spec for Hermes Agent on mibook
Locks in host (mibook, not jupiter), deployment mode (NixOS native
module), and auth (claude setup-token / ANTHROPIC_TOKEN, Max extra
credits) after a grilling round. Builds on feat/sops-nix as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbvwg3Qt2u9nbkEA3wL17S
2026-09-08 16:56:26 +02:00
finn.markwitz 621ca5dde6 docs: sops-nix secrets workflow
Operator runbook for adding a secret, adding a new host, and rotating
keys, plus a pointer from CLAUDE.md's architecture overview.
2026-09-08 15:43:50 +02:00
finn.markwitz a00916864c feat(mibook): decrypt placeholder secret via host age key
Proves the sops-nix pipeline end to end on mibook: encrypts a
throwaway secret to the operator + mibook host keys, declares it in
machines/mibook/secrets.nix, and verifies it decrypts to
/run/secrets/placeholder (root:root, 0400) on nixos-rebuild switch.

Also removes the dead `let secrets = config.sops.secrets;` binding
from both environments.nix files — leftover from an earlier abandoned
plan, never referenced.
2026-09-08 15:43:45 +02:00
finn.markwitz 3ab8bfbb91 feat(sops): add sops-nix input, core module and devShell tooling
Wires sops-nix into every machine via machines/core/sops.nix so hosts
decrypt with their existing SSH host key. Adds a devShell exposing
sops/age/ssh-to-age for the operator workflow, which also fixes .envrc's
previously no-op `use flake`.
2026-09-08 15:43:36 +02:00
finn.markwitz b96d30edd7 config: Futher Poweroptions to deny hibernate 2026-09-03 08:19:07 +02:00
finn.markwitz a7ffcc4648 feat: Enable Claude on Jupiter 2026-09-03 08:18:39 +02:00
finn.markwitz 70deb25080 chore: Flake Update 2026-08-21 10:30:01 +02:00
finn.markwitz 652630cfe4 config: Jupiter Domain 2026-08-13 17:22:58 +02:00
finn.markwitz abe2431dbb config: Local DNS for Homepage Dashboard 2026-08-13 17:21:08 +02:00
finn.markwitz ac57abf255 chore: Flake Update 2026-08-11 15:40:07 +02:00
finn.markwitz 83ee518091 Merge pull request 'feat(jupiter): native Immich profile module (docker → NixOS)' (#10) from feat/immich-nixos-module into main
Reviewed-on: #10
2026-08-07 11:13:40 +02:00
finn.markwitzandClaude Opus 4.8 3ee2b1a9d8 feat(immich): declare QSV transcoding via config-file settings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-07 11:02:50 +02:00
finn.markwitz e9a5781944 feat(jupiter): add native Immich profile module 2026-08-05 16:39:07 +02:00
finn.markwitzandClaude Opus 4.8 1f8a34d0e8 docs: Immich NixOS module implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-05 16:35:29 +02:00
finn.markwitzandClaude Opus 4.8 1176132627 docs: resolve Immich version check (2.7.5 == stable, no vector migration)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-05 16:33:53 +02:00
finn.markwitzandClaude Opus 4.8 01b31a3493 docs: Immich docker→NixOS module migration design
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016e2PKH5yN31h6JgHWCQb32
2026-08-05 16:32:46 +02:00
finn.markwitz 28fc71dbbe Merge pull request 'feat: Athena Firewall' (#9) from athena-introduction into main
Reviewed-on: #9
2026-08-05 16:16:31 +02:00
finn.markwitz c7392f2dd9 feat: Athena Firewall 2026-08-05 16:15:49 +02:00
finn.markwitz f5f8e88966 Merge pull request 'revert(mibook): remove boot-time terminal/desktop choice feature' (#7) from revert/mibook-boot-choice into main
Reviewed-on: #7
2026-07-28 15:21:01 +02:00
finn.markwitzandClaude Opus 4.8 bf874d962a revert(mibook): remove boot-time terminal/desktop choice feature
Back out the entire "choose terminal-only vs KDE at boot" work. The first
attempt (default multi-user.target + on-demand desktop) locked mibook out —
it is WiFi-only with credentials in KWallet, so a headless boot has no
network and no SSH. The follow-up specialisation redesign is no longer
wanted either.

mibook returns to plain always-KDE boot (the pre-feature baseline):
- machines/mibook/configuration.nix: drop specialisation.terminal and the
  NetworkManager-wait-online override.
- machines/mibook/environments.nix: drop kde-desktop.startOnBoot.
- modules/environments/kde-desktop/default.nix: drop the startOnBoot option.
- remove the boot-desktop-choice design spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1wvPy5kbGp8pUQX61WBCY
2026-07-28 14:51:31 +02:00
finn.markwitz f01b1f2c4f Merge pull request 'Feat/jellyfin hw transcoding' (#3) from feat/jellyfin-hw-transcoding into main
Reviewed-on: #3
2026-07-27 09:56:11 +02:00
finn.markwitz ff34fe762e Merge pull request 'Feat/mibook boot desktop choice' (#4) from feat/mibook-boot-desktop-choice into main
Reviewed-on: #4
2026-07-27 09:55:47 +02:00
finn.markwitzandClaude Opus 4.8 7e4407a1f8 feat(mibook): boot to terminal, launch KDE on demand
Add my.profiles.kde-desktop.startOnBoot (default true, no change for
existing consumers). When false, boot to multi-user.target, keep SDDM
out of any boot target's wants, and provide a 'desktop' command that
starts the display manager on demand. Enable this on mibook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfozKLQdUh5TzqyjSigLUx
2026-07-27 09:48:13 +02:00
finn.markwitzandClaude Opus 4.8 539fb26791 docs: spec for mibook boot-time terminal/desktop choice
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CfozKLQdUh5TzqyjSigLUx
2026-07-27 09:27:37 +02:00
marthsincemeleeandClaude Sonnet 5 003a2f77dd docs: record post-deploy QSV/OpenCL runtime fixes in spec
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:42:41 +02:00
marthsincemeleeandClaude Sonnet 5 86e7f9c1a8 fix(jupiter): add Intel compute-runtime for OpenCL HDR tone-mapping
HDR HEVC transcodes were failing with "Failed to get number of OpenCL
platforms: -1001" (CL_PLATFORM_NOT_FOUND_KHR). The tonemap_opencl
filter jellyfin-ffmpeg uses for HDR-to-SDR tone-mapping needs an OpenCL
ICD, which intel-media-driver/vpl-gpu-rt don't provide on their own.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:34:36 +02:00
marthsincemeleeandClaude Sonnet 5 adb7fcfad7 fix(jupiter): add oneVPL/MFX runtime for QSV hardware transcoding
HEVC HDR transcodes were failing with "Error creating a MFX session: -9"
because intel-media-driver only provides VAAPI, not the separate
oneVPL/MFX runtime QSV needs to create a hardware session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 17:27:31 +02:00
marthsincemelee 5235f5abcb feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding 2026-07-26 14:51:01 +02:00
marthsincemeleeandClaude Sonnet 5 f53f2331d0 docs: add implementation plan for Jellyfin hardware transcoding
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:31:20 +02:00
marthsincemeleeandClaude Sonnet 5 84cec9e935 docs: add design spec for Jellyfin hardware transcoding on jupiter
Jupiter's Intel iGPU is configured at the OS level but Jellyfin has no
access to it, so transcodes run on CPU only and stutter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 14:27:30 +02:00
finn.markwitz 79b26ddfda chore: Flake Update 2026-07-24 12:01:15 +02:00
finn.markwitz b8961fb47d Merge pull request 'feat(mibook): do nothing on lid close while on AC power' (#2) from feat/mibook-lid-close-ac into main
Reviewed-on: #2
2026-07-24 11:56:02 +02:00
finn.markwitzandClaude Opus 4.8 4851f745d8 feat(mibook): do nothing on lid close while on AC power
Ship a system-wide PowerDevil default (/etc/xdg/powerdevilrc) setting the
AC lid-close action to "do nothing" (LidAction=0). Battery behavior is left
at the default (suspend).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbZoyXq6J8v2eqC9yn5Vcy
2026-07-21 17:42:47 +02:00
finn.markwitz 59b5cfb47c chore: Flake update 2026-07-20 11:08:47 +02:00
finn.markwitz a8364f21ca chore: Flake update 2026-07-14 17:33:45 +02:00
finn.markwitz f135c1646f Merge pull request 'feat(home-assistant): add Apple TV and Tuya integrations' (#1) from feat/ha-appletv-tuya-irrigation into main
Reviewed-on: #1
2026-07-05 12:15:22 +02:00
marthsincemeleeandClaude Opus 4.8 7a1b0541c2 feat(home-assistant): add Apple TV and Tuya integrations
Enable the apple_tv (pyatv) and tuya (SmartLife cloud) components on
jupiter. Tuya covers the Unistyle WLAN irrigation computer; both are
config-flow integrations paired through the HA web UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 17:19:53 +02:00
marthsincemelee 6c5f61997d fix: Homepage Domain 2026-06-27 15:45:06 +02:00
finn.markwitz 763253693c chore: Flake Update 2026-06-27 15:28:11 +02:00
finn.markwitz b6f4bb7955 chore: Flake Update 2026-06-23 16:55:42 +02:00
finn.markwitz 74d65a0d75 Revert "chore: flake update"
This reverts commit 85bd12778a.
2026-06-15 11:55:29 +02:00
finn.markwitz 85bd12778a chore: flake update 2026-06-15 11:51:59 +02:00
finn.markwitz d9565448b9 Merge branch 'feature/ha-zbt-2-thread' into 'master'
feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle

See merge request finn.markwitz/nixos!18
2026-06-09 06:38:56 +00:00
finn.markwitz af2655f9e2 Merge branch 'master' into 'feature/ha-zbt-2-thread'
# Conflicts:
#   machines/mibook/configuration.nix
2026-06-09 06:37:07 +00:00
finn.markwitz 6d0684610e chore: Flake Update 2026-06-09 08:28:11 +02:00
finn.markwitz 17c3a3189f chore(NixOS Version): Update to 26.05 2026-06-09 08:16:08 +02:00
finn.markwitz 18d1ce711d chore(HA): Removed uneccessary Unstable Overlay import 2026-06-09 08:10:45 +02:00
finn.markwitz cbdb42f333 chore: Flake Update 2026-06-01 10:02:27 +02:00
finn.markwitzandClaude Sonnet 4.6 b44775e3e5 docs: add CLAUDE.md with repo architecture and development commands
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:34:24 +02:00
marthsincemelee 9ed0fb6f07 feat: SSH for mibook 2026-05-25 12:33:40 +02:00
marthsincemelee ff6e25b708 fix: Duplicated NVIDIA PCI 2026-05-25 12:16:59 +02:00
marthsincemeleeandClaude Sonnet 4.6 2e5568611a feat(mibook): add claude-code profile module
Installs claude-code (stable), ripgrep, fd, gh, and jq for user finn.
Includes annotation for future headless service upgrade path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:03:54 +02:00
marthsincemeleeandClaude Sonnet 4.6 b44220adf1 docs(plan): mibook claude-code module implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:56:53 +02:00
marthsincemeleeandClaude Sonnet 4.6 45c6d978f2 docs(spec): add pkgs.unstable contingency note to claude-code spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:54:09 +02:00
marthsincemeleeandClaude Sonnet 4.6 62875c0b1a docs(spec): mibook claude-code execution machine design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 11:53:36 +02:00
marthsincemeleeandClaude Opus 4.7 d2775e35d9 feat(home-assistant): set internal/external URLs for mobile_app push
The HA Companion app needs an external_url for clickable notification
deep-links to resolve when the phone is off the home Wi-Fi. Reach is via
Tailscale (Headscale tailnet solar.internal), so external_url points at
the FQDN jupiter.solar.internal:8123; internal_url stays on the bare
hostname for LAN-attached devices.

Phase A only: device registration + notify group + smoke-test land in a
follow-up commit once Companion has registered real mobile_app_<slug>
service names. See docs/superpowers/specs/2026-05-18-ha-push-notifications.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 12:07:31 +02:00
marthsincemeleeandClaude Opus 4.7 4cb4455d37 fix(home-assistant): use xiaomi_miio for Mi Home devices
The bare "xiaomi" component only ships the legacy IP-camera platform.
Mi Home Wi-Fi devices (vacuums, air purifiers, Yeelight, fans, etc.)
are provided by the xiaomi_miio integration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 18:23:16 +02:00
marthsincemelee 933c2f8b41 feat: HA Xiaomi Support 2026-05-17 18:01:16 +02:00
finn.markwitz 1444912326 config: Disable Prime Sync MiBook 2026-05-17 14:23:55 +02:00
marthsincemeleeandClaude Opus 4.7 d1299ed112 fix(home-assistant): wire automation/script/scene !include directives
UI-saved automations land in /var/lib/hass/automations.yaml, but HA
won't load them unless configuration.yaml has \`automation: !include
automations.yaml\`. The Nix-managed config didn't have it, so HA's
post-save reload found no automations and timed out.

The NixOS HA module's renderYAMLFile post-processes the generated YAML
to convert quoted bang-strings into real YAML tags (see the sed step
in nixos/modules/services/home-automation/home-assistant.nix), so a
plain Nix string is enough — no escape-hatch needed.

Pre-create the three include targets via systemd.tmpfiles so HA
doesn't fail at startup if the user hasn't saved anything yet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 21:50:41 +02:00
marthsincemeleeandClaude Opus 4.7 96fbeb04ef fix(home-assistant): nest name/unit_system under homeassistant block
HA expects \`name\` and \`unit_system\` as keys of the top-level
\`homeassistant:\` block, not as top-level integration names. Recent HA
versions tightened config validation, so this surfaced as
"Integration 'unit_system' not found" / "Integration 'name' not found"
warnings, blocking the automation reload that runs after saving an
automation in the UI.

Pre-existing bug, unrelated to OTBR — surfaced now because automation
edits trigger strict validation. Bonus: NixOS auto-populates
\`time_zone\` from the system locale once \`homeassistant\` is a real block.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 21:35:44 +02:00
marthsincemeleeandClaude Opus 4.7 68f2c65246 fix(home-assistant): set OTBR radio.baudRate to 460800 for ZBT-2 RCP firmware
Nabu Casa's prebuilt OpenThread RCP image for the ZBT-2
(zbt2_openthread_rcp_2.7.2.0_GitHub-fb0446f53_gsdk_2025.6.2.gbl)
runs at 460800 baud, not the module's 115200 default. Aligns the
radio URL with the firmware so otbr-agent can actually open the
Spinel link after rebuild.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 20:41:49 +02:00
marthsincemeleeandClaude Opus 4.7 787427e7c8 feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components
Brings up otbr-agent against the ZBT-2 over Spinel/UART, opens the
REST API on :8081, and wires HA's otbr + thread integrations so
Matter-over-Thread devices can commission through the existing
matter-server.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 20:21:30 +02:00
marthsincemeleeandClaude Opus 4.7 6251c8edef feat(home-assistant): import openthread-border-router module from unstable
Pulls the services.openthread-border-router NixOS module directly from
nixpkgs-unstable since it isn't in 25.11 yet. Service stays disabled
in this commit; configuration follows.

Also promotes `self` from `_module.args` to `specialArgs` in
machines/configuration.nix, since `imports` are evaluated before
`config` and so can't reach `_module.args.self`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:52:43 +02:00
marthsincemeleeandClaude Opus 4.7 311e358d88 docs(plan): correct Task 2 scope — specialArgs needed for self in imports
The original plan claimed no flake-level changes were needed because
machines/configuration.nix:21 already passes `_module.args.self = self;`.
That's only true for `config`-time evaluation; `imports` are collected
before `config` is available, so referencing `self` in `imports` causes
infinite recursion. Fix: promote `self` to `specialArgs` on each
nixosSystem call. The implementer of Task 2 caught this on first
dispatch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:52:20 +02:00
marthsincemelee 9ff3603d40 Revert "feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle"
This reverts commit e8d09f40f6.
2026-05-10 15:39:10 +02:00
marthsincemeleeandClaude Opus 4.7 6d12940205 docs(home-assistant): implementation plan for ZBT-2 Thread + OTBR
Task-by-task plan covering: revert of prior ZHA commit, unstable
OTBR module import, OTBR enablement against the ZBT-2, firmware
flash via universal-silabs-flasher, rebuild on jupiter, and
end-to-end smoke test through the HA UI.

Designed for execution via superpowers:subagent-driven-development
or superpowers:executing-plans, with operator handoffs marked
explicitly (per the 'no SSH' workflow rule).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:36:12 +02:00
marthsincemeleeandClaude Opus 4.7 dbeda276e1 docs(home-assistant): design spec for ZBT-2 Thread + OTBR setup
Captures the architecture, operator workflow, and verification for
running the Connect ZBT-2 as an OpenThread Border Router on jupiter
(via nixos-unstable's services.openthread-border-router module),
with HA's otbr + thread integrations driving the Thread network
and the existing matter-server consuming credentials for
Matter-over-Thread device commissioning.

Supersedes the ZHA-direction commit on this branch (e8d09f4),
which will be reverted at the start of implementation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 15:29:21 +02:00
marthsincemeleeandClaude Opus 4.7 e8d09f40f6 feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle
Adds the `zha` extra component so Home Assistant can drive the
Nabu Casa Connect ZBT-2 radio, and puts the `hass` service user in
`dialout` so it can open `/dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_*`.

Pairing is then handled through the standard ZHA wizard in the HA UI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 14:55:55 +02:00
finn.markwitz 098e632405 chores: Update Flake 2026-05-10 14:40:22 +02:00
finn.markwitz ede28c8bb4 Merge branch 'home-assistant-matter' into 'master'
fix: enable mobile_app integration in HA config

See merge request finn.markwitz/nixos!15
2026-05-05 11:37:32 +00:00
finn.markwitz db1f44c4ea Merge branch 'feature/homepage-widgets' into 'master'
Feature/homepage widgets

See merge request finn.markwitz/nixos!14
2026-05-05 11:27:44 +00:00
finn.markwitz df05fe451e feature/homepage widgets 2026-05-05 11:27:44 +00:00
marthsincemeleeandClaude Sonnet 4.6 74abfe35a2 fix: enable mobile_app integration in HA config
extraComponents only installs packages; mobile_app needs to be declared
in configuration.yaml to actually load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-30 16:28:24 +02:00
marthsincemelee c51a658b18 feat: Mobile-App Component 2026-04-30 16:10:12 +02:00
marthsincemelee 1b64ea5930 feat: Matter Server 2026-04-30 16:05:29 +02:00
finn.markwitz eb21cb998f Merge branch 'feature/homepage-icons' into 'master'
feat: add homepage icons and Home Assistant entry

See merge request finn.markwitz/nixos!13
2026-04-22 09:55:34 +00:00
marthsincemelee 9342809975 feat: add manual homepage services file 2026-04-20 18:19:49 +02:00
marthsincemelee 2d43ed36ea feat: add homepage icons and Home Assistant entry 2026-04-20 16:34:27 +02:00
finn.markwitz 004f04eca5 Merge branch 'homeassistant' into 'master'
Homeassistant

See merge request finn.markwitz/nixos!11
2026-04-20 14:08:40 +00:00
finn.markwitz 4296dda7f8 Merge branch 'nvidia' into 'master'
feat(Hardware): Nvidia GPU Support

See merge request finn.markwitz/nixos!9
2026-04-20 14:07:05 +00:00
marthsincemelee b316b16a7f fix: AI Stuff in gitignore 2026-04-20 16:05:05 +02:00
finn.markwitz ca1fa3810b Merge branch 'feature/dashboard' into 'master'
Feature/dashboard

See merge request finn.markwitz/nixos!10
2026-04-20 14:03:38 +00:00
marthsincemelee 337ba6c847 fix: Hompage internal URL 2026-04-20 16:01:21 +02:00
marthsincemelee 191e860455 feat: Homepage 2026-04-20 14:46:44 +02:00
finn.markwitz 93a36dbf2b chore(flake): Updates 2026-04-07 15:54:33 +02:00
finn.markwitz dc65b044cf feat(Nvidia): PRIME Sync 2026-02-12 16:07:30 +01:00
finn.markwitz 3ce8459904 feat(Nvidia): PRIME Sync 2026-02-09 13:35:00 +01:00
finn.markwitz 78f540a776 feat(Hardware): Nvidia GPU Support 2026-02-09 13:34:48 +01:00
finn.markwitz 9757b987bb feat: Zed Editor 2026-02-09 13:34:43 +01:00
finn.markwitz f833439e0f feat: MiBook Config Update 2026-02-09 13:34:25 +01:00
finn.markwitz e1ea5b03dc chore: Flake Update 2026-01-08 16:37:52 +01:00
finn.markwitz 9de69b7dcc config: Home-Assistant 2026-01-08 16:27:46 +01:00
finn.markwitz 9974bd49ac refactor(Home-Assistant): module naming 2026-01-08 16:24:28 +01:00
finn.markwitz b300f39c03 feat: Homeassistant 2026-01-08 16:22:24 +01:00
finn.markwitz 64eedeaac9 Merge branch '25-11-update' into 'master'
Draft: chore(OS Update): 25.11

See merge request finn.markwitz/nixos!8


chore: Replaced scikitlearn with scikit-learn
chore: removed gitAndTools
chore: Flake Update
chore: Replaced vaapiVdpau with libva-vdpau-driver
chore: Update Nixpkgs to 25.11
2026-01-05 18:53:52 +00:00
finn.markwitz 97633af170 chore(OS Update): 25.11
chore: Replaced scikitlearn with scikit-learn
chore: removed gitAndTools
chore: Flake Update
chore: Replaced vaapiVdpau with libva-vdpau-driver
chore: Update Nixpkgs to 25.11
2026-01-05 18:53:52 +00:00
marthsincemelee a4ad1e0754 feat(Homepage): Booksmarks 2025-12-23 17:59:48 +01:00
marthsincemelee 9ec2df1afa feat: Homepage 2025-12-23 17:55:17 +01:00
marthsincemelee bc87844e76 feat: Homepage 2025-12-23 17:38:02 +01:00
finn.markwitz 39263f65f9 chore(Update): Flake 2025-11-17 20:55:11 +01:00
finn.markwitz a1e0dc499e Merge branch 'kernel-modules-troubleshoot' into 'master'
Kernel modules troubleshoot

See merge request finn.markwitz/nixos!7
2025-10-27 07:40:23 +00:00
finn.markwitz 24a211330a Kernel modules troubleshoot 2025-10-27 07:40:22 +00:00
finn.markwitz 6f84bdefb4 Merge branch 'moonlight' into 'master'
feat: Moonlight Setup & OS Update

See merge request finn.markwitz/nixos!5
2025-08-12 20:24:57 +00:00
finn.markwitz 49371f2e2b feat: fwupd 2025-08-12 21:54:20 +02:00
finn.markwitz b802659462 feat: Nixos 25.05 2025-06-17 22:10:21 +02:00
finn.markwitz 256fdc850b feat: Updated Drive IDs for new M2 SSD 2025-03-24 13:00:39 +01:00
finn.markwitz ce4e8225f9 feat: Tailscale VPN Setup 2025-02-18 20:53:02 +01:00
finn.markwitz dc2abe6ff1 refactor: Formatting 2025-02-07 22:13:20 +01:00
finn.markwitz c3f4470f80 refactor: Apps & Development 2025-02-07 22:01:18 +01:00
finn.markwitz c407e1e9ee config: Swap to KDE 2025-02-04 21:05:49 +01:00
finn.markwitz 6e73e9362d config: Hyprland & Ghostty 2025-02-04 21:03:49 +01:00
finn.markwitz 1fefc23c1b update: Flake 2025-01-28 23:46:50 +01:00
finn.markwitz 2b13f15711 fix: Development Env 2025-01-28 23:45:58 +01:00
finn.markwitz 514ee3c137 feat: Developent Environment 2025-01-28 23:32:53 +01:00
finn.markwitz 325ce5969c feat: Sensors, SystemCTL TUI & Headset Options 2025-01-28 23:26:24 +01:00
finn.markwitz 2945601aa3 fix: Hyprland & Update 2025-01-28 23:25:38 +01:00
finn.markwitz b15fe38d2f feat: Logitech Drivers 2025-01-16 11:57:05 +01:00
finn.markwitz 495f1a0645 Merge branch 'master' of ssh://gitlab.smoothcloud.de:2222/finn.markwitz/nixos 2025-01-04 11:46:15 +01:00
finn.markwitz bb238e3369 Merge branch 'master' of ssh://gitlab.smoothcloud.de:2222/finn.markwitz/nixos 2025-01-04 11:45:37 +01:00
finn.markwitz fa72edfc05 config: Hyprland Desktop 2025-01-04 11:45:29 +01:00
finn.markwitz d8341c3e38 config: Hyprland Desktop 2025-01-04 11:42:25 +01:00
finn.markwitz 204c358cea feat: Moonlight-qt 2025-01-04 11:38:12 +01:00
finn.markwitz 7c6dc43a41 Merge branch 'kde-desktop' into 'master'
feat: kde-desktop

See merge request finn.markwitz/nixos!4
2025-01-04 10:20:39 +00:00
finn.markwitz 76955ad6b3 config: disable desktop 2024-12-12 20:13:53 +01:00
finn.markwitz 74f1471bb9 update: 24.11 - flake 2024-12-10 21:59:47 +01:00
marthsincemelee 8ce14dc6e0 update: 24.11 2024-12-10 20:50:55 +01:00
finn.markwitz 619c6c6ed4 Merge remote-tracking branch 'origin/kde-desktop' into kde-desktop
# Conflicts:
#	flake.nix
2024-12-10 19:30:40 +01:00
finn.markwitz 6086ace431 test: nix-pkg: 24.11 2024-12-10 19:27:22 +01:00
finn.markwitz c300b55e8f Config: KDE-Desktop with VS Code enabled 2024-10-28 20:54:51 +01:00
finn.markwitz 232fa2ead9 test: disable Paperless, since docker service is more stable 2024-09-30 22:53:25 +02:00
finn.markwitz 97981637a3 feat: Disable Powersaving 2024-08-14 19:04:10 +02:00
marthsincemelee 42d2702bad conf: Paperless Data Location 2024-08-14 19:02:49 +02:00
finn.markwitz af2d9af641 feat: Enable Readarr 2024-07-21 21:58:25 +02:00
finn.markwitz c2a645a5b5 update: Major System & Flake 2024-07-15 11:43:31 +02:00
finn.markwitz 71f160c3e5 feat: Enable Arr Suite 2024-06-12 21:51:23 +02:00
finn.markwitz 284ec2e841 feat: Thunar File Manager 2024-06-12 21:51:23 +02:00
finn.markwitz a2e1831cc7 feat: Voyager hubble 2024-06-12 21:51:23 +02:00
finn.markwitz a3f2922017 refactor: Jupiter Server Edition Removal 2024-06-12 21:50:27 +02:00
finn.markwitz 0c393d81c8 test: hyprland & jellyfin 2024-06-12 21:50:27 +02:00
finn.markwitz 6018134b12 feat: Numix Icons 2024-06-12 21:50:27 +02:00
finn.markwitz 192c1f5e56 feat: zsh Configuration 2024-06-12 21:50:27 +02:00
finn.markwitz c9e725ff85 fix: Desktop Rebuild Fix
Removed Arr Suite
Temporarily Removed NFS Mount
2024-06-12 21:50:27 +02:00
finn.markwitz eac2c87e2d feat: Latte Dock 2024-06-12 21:50:27 +02:00
finn.markwitz b3b3bf9f4b Installed Pipewire and wireplumber for screen sharing 2024-06-12 21:50:27 +02:00
finn.markwitz f9906a9e96 Flake Update 2024-06-12 21:50:27 +02:00
marthsincemelee 3ec4a7135e feat: KDE Desktop Module 2024-06-12 21:50:27 +02:00
finn.markwitz cfedf8aa17 Merge branch 'jupiter-2' into 'master'
Feature: Paperless

See merge request finn.markwitz/nixos!3
2024-03-10 21:19:23 +00:00
marthsincemelee 6ee162822e Feature: Paperless 2024-03-10 22:12:15 +01:00
finn.markwitz 0899f5ae70 Merge branch 'jup-1' into 'master'
Jupiter-01: OS Update

See merge request finn.markwitz/nixos!2
2024-03-09 11:50:31 +00:00
finn.markwitz fecaa7dea9 Bug: zsh Theme 2024-03-09 12:44:37 +01:00
finn.markwitz 9696357a24 OS-Update: 23.11 2024-03-09 12:31:34 +01:00
marthsincemelee f252a997da Configuration: Disable xserver 2024-03-09 12:08:51 +01:00
marthsincemelee 1565cee96b Fix: Prowlarr Firewallrule 2024-01-20 11:10:18 +01:00
finn.markwitz 45535629a8 Updated TODOs 2024-01-20 09:55:48 +00:00
finn.markwitz 6ea77c298c Merge branch 'SSH' into 'master'
Backup Suite

See merge request finn.markwitz/nixos!1
2024-01-20 09:22:46 +00:00
marthsincemelee 5ce59a0983 Feature: Prowlarr module 2024-01-20 10:18:08 +01:00
marthsincemelee 5d003e0b27 Feature: Paperless Module 2024-01-20 10:18:08 +01:00
marthsincemelee 925ba42182 Feature: QNAP NAS 2024-01-20 10:18:08 +01:00
marthsincemelee de0e3f81bc Feature: QNAP NAS 2024-01-20 10:18:08 +01:00
marthsincemelee 5a869457af Feature: QNAP NAS 2024-01-20 10:18:08 +01:00
marthsincemelee d5a6e3a235 Feature: QNAP NAS 2024-01-20 10:18:08 +01:00
marthsincemelee 0aded836ac Feature: QNAP NAS 2024-01-20 10:18:08 +01:00
finn.markwitz 66c8eca980 Config: Disable Desktop Apps 2024-01-20 10:18:08 +01:00
finn.markwitz a529352343 Feature: Hardware acceleration 2024-01-20 10:18:08 +01:00
finn.markwitz c65044ac75 Feature: Jellyseerr Service 2024-01-20 10:18:08 +01:00
marthsincemelee 02b44230f5 Bug: Import statements for Sonarr Service 2024-01-20 10:18:08 +01:00
marthsincemelee 3df1fb2c0e Feature: idea config 2024-01-20 10:18:08 +01:00
marthsincemelee fa60eabd35 Feature: Sonarr Service 2024-01-20 10:18:08 +01:00
finn.markwitz 82e177d41b Removed User from Radarr setup 2024-01-20 10:18:08 +01:00
finn.markwitz 0b6c123099 Update User for Radarr Service 2024-01-20 10:18:08 +01:00
marthsincemelee 082c07bbdd Feature: Added Jellyfin + Readarr Service 2024-01-20 10:18:08 +01:00
marthsincemelee a304d2fd9a Feature: Docker Module 2024-01-20 10:18:08 +01:00
marthsincemelee 9eb41081c3 Fix: Firewallrules for arr suite 2024-01-20 10:18:08 +01:00
marthsincemelee 6de80ac4a4 Feature: radrarr module 2024-01-20 10:18:08 +01:00
marthsincemelee c7071b4bf0 Feature: Prowlarr module 2024-01-20 10:18:05 +01:00
finn.markwitz fd2b310d92 Feature: Enable ssh 2024-01-20 10:17:50 +01:00
finn.markwitz 6030e88a35 added go 2024-01-20 10:15:45 +01:00
finn.markwitz 02f2deb219 Installed Pipewire and wireplumber for screen sharing 2024-01-20 10:15:45 +01:00
marthsincemelee e22eb62d2b Feature: Prowlarr module 2024-01-20 10:13:51 +01:00
marthsincemelee 2985cd7ab2 Feature: New Development Server Environment 2024-01-20 10:12:49 +01:00
marthsincemelee 5aae4ebf13 Feature: New Jupiter Server machine 2024-01-20 10:12:22 +01:00
marthsincemelee 3174128bab Feature: Paperless Module 2024-01-20 10:03:31 +01:00
marthsincemelee 5b2d8ce8b6 Feature: QNAP NAS 2024-01-17 11:41:52 +01:00
marthsincemelee 9fb7e97366 Feature: QNAP NAS 2024-01-17 11:38:17 +01:00
marthsincemelee b73f1f49a2 Feature: QNAP NAS 2024-01-17 11:22:33 +01:00
marthsincemelee 7019c9476c Feature: QNAP NAS 2024-01-17 10:59:41 +01:00
marthsincemelee 29b51380f1 Feature: QNAP NAS 2024-01-17 09:00:28 +01:00
finn.markwitz d7d23b1dd7 Config: Disable Desktop Apps 2023-11-24 20:29:06 +01:00
finn.markwitz 2c1b19052e Feature: Hardware acceleration 2023-11-24 20:26:36 +01:00
finn.markwitz 5ab19e678c Feature: Jellyseerr Service 2023-11-22 19:18:31 +01:00
marthsincemelee 246927fc11 Bug: Import statements for Sonarr Service 2023-11-20 16:57:01 +01:00
marthsincemelee 1da5a581f1 Feature: idea config 2023-11-20 16:52:54 +01:00
marthsincemelee c6763f4976 Feature: Sonarr Service 2023-11-20 16:52:54 +01:00
finn.markwitz c1ee956e02 Removed User from Radarr setup 2023-11-04 10:33:54 +01:00
finn.markwitz 5af5a1e300 Update User for Radarr Service 2023-11-04 10:17:48 +01:00
marthsincemelee ef7fe9abc2 Feature: Added Jellyfin + Readarr Service 2023-11-01 11:06:16 +01:00
marthsincemelee 70076accf1 Feature: Docker Module 2023-10-30 21:53:18 +01:00
marthsincemelee 6ba6b3dca6 Fix: Firewallrules for arr suite 2023-10-16 22:36:37 +02:00
marthsincemelee dac0e346e7 Feature: radrarr module 2023-10-16 22:33:02 +02:00
marthsincemelee 5e40f81c99 Feature: Prowlarr module 2023-10-16 22:21:49 +02:00
finn.markwitz c1d83736eb Feature: Enable ssh 2023-10-16 22:06:38 +02:00
finn.markwitz e8c653bf75 added go 2023-10-16 21:59:07 +02:00
finn.markwitz 05da1808de Installed Pipewire and wireplumber for screen sharing 2023-10-16 21:58:41 +02:00
marthsincemelee 7f601985bf Feature: New Development Server Environment 2023-10-13 22:45:57 +02:00
marthsincemelee fa7587edf0 Feature: New Jupiter Server machine 2023-10-13 22:45:35 +02:00
finn.markwitz 718a1d97d1 Created Sound Configuration 2023-09-25 10:16:08 +02:00
finn.markwitz e86a16c932 Sound Options using pulseaudio 2023-09-25 10:15:35 +02:00
finn.markwitz 34bac6a33e Added Project to intellij 2023-09-25 10:14:54 +02:00
finn.markwitz 5a4745e8db activated bluetooth for jupiter 2023-09-25 09:52:16 +02:00
finn.markwitz e56039f7bb Enabled bluetooth for jupiter 2023-09-25 09:44:05 +02:00
finn.markwitz f200837f0e added networkmanager for working wifi 2023-09-25 09:41:31 +02:00
finn.markwitz 4366b8612c Added hyprpaper to Hyprland Module 2023-09-19 22:22:21 +02:00
finn.markwitz 73e4b2e7fb added Dolphin and kitty to hyprland config 2023-09-16 11:02:33 +02:00
finn.markwitz f716e7c197 New Drives 2023-09-16 11:02:09 +02:00
finn.markwitz 3533957964 Added SDDM Dispalymanager 2023-09-15 09:50:42 +00:00
finn.markwitz 28da6ce7ee Updated Kernel 2023-09-11 15:20:38 +00:00
finn.markwitz 76b89fb0d2 ci 2023-09-11 15:02:33 +00:00
finn.markwitz 97ff99cd6d removed hardware stuff 2023-09-11 15:00:44 +00:00
finn.markwitz 3279870a36 Commented out sound file 2023-09-11 14:57:16 +00:00
finn.markwitz 13d66ca328 added siing ; 2023-09-11 14:55:46 +00:00
finn.markwitz 483f7f50b7 Typo Changes CI 2023-09-11 14:54:26 +00:00
finn.markwitz 718f722e54 Updaet CI 2023-09-11 14:53:09 +00:00
finn.markwitz 182ad4c11e Fixed Typo in CI 2023-09-11 14:51:04 +00:00
finn.markwitz b8ac8301d4 Added mising hyprland import 2023-09-11 14:49:47 +00:00
finn.markwitz 6d47fb5a66 Updated Jupiter Test in CI 2023-09-11 14:44:34 +00:00
finn.markwitz a4e51a59c0 added more hyperland options 2023-09-07 18:02:05 +02:00
finn.markwitz a602737262 Added hardware profiel for wifi 2023-07-25 13:13:50 +02:00
finn.markwitz b1784be493 Changed Kernel to 5.15 2023-07-25 13:10:21 +02:00
fmarkwitz 56eed9210c Added Packages to Hyprland config 2023-07-06 16:04:36 +02:00
fmarkwitz de1b89071f Merge branch 'master' of https://gitlab.smoothcloud.de/finn.markwitz/nixos 2023-07-06 13:33:00 +02:00
fmarkwitz d04f30bf83 Added Hyprland Flake 2023-07-06 13:32:41 +02:00
fmarkwitz c883dfb0e8 Added Hyprland Flake 2023-07-06 13:31:29 +02:00
fmarkwitz 3163f69b28 test 2023-07-06 13:23:53 +02:00
fmarkwitz f2eded1341 Added Heapbyzes Theme to the zsh 2023-07-06 13:09:19 +02:00
fmarkwitz 13f418fef5 Merge branch 'master' of https://gitlab.smoothcloud.de/finn.markwitz/nixos 2023-07-04 16:22:14 +02:00
fmarkwitz 59270b9a76 Formatted Packages + Readme Update 2023-07-04 16:22:10 +02:00
finn.markwitz 9db8fc644a Added Apps PAcakge for Gnome Config and Extentions 2023-07-04 14:49:01 +02:00
finn.markwitz 537da46c40 Added hardware flag to jupiter 2023-07-04 13:33:34 +02:00
marthsincemelee dd14951d5a Removed Grub Bootloader from Core 2023-07-04 12:46:40 +02:00
finn.markwitz 7b4d03d15a Generated Hardware Infor for Jupiter 2023-07-04 12:44:04 +02:00
marthsincemelee 069711977d Updated Jupiter Bootloader 2023-07-04 12:40:23 +02:00
marthsincemelee a146ff81c8 Added gnome config to jupiter 2023-07-04 12:32:48 +02:00
fmarkwitz aecb93b24f xServer + audio settings for mibook 2023-07-03 13:43:32 +02:00
finn.markwitz c02360f9dc Update MiBook with generated config 2023-07-03 13:42:28 +02:00
fmarkwitz c7f68162da added boot loader config for mibook 2023-07-03 12:04:25 +02:00
fmarkwitz 3f43d418e7 added Mibook to configuration.nix machines 2023-07-03 11:54:32 +02:00
fmarkwitz bd0d018748 created second machine "mibook" for testing 2023-07-03 11:46:07 +02:00
finn.markwitz beddbee7c3 enable grub 2023-07-03 09:37:48 +00:00
finn.markwitz c90ae14937 typo cf cfg 2023-07-03 09:31:45 +00:00
finn.markwitz 80422ab0f4 pkgs to root folder 2023-07-03 09:30:52 +00:00
finn.markwitz c9acad680a rename user to users 2023-07-03 09:29:46 +00:00
finn.markwitz 1c939f4049 ; 2023-07-03 09:28:58 +00:00
finn.markwitz 9dd64d8a30 ; 2023-07-03 09:28:34 +00:00
finn.markwitz 2572d4317f missing ; 2023-07-03 09:27:59 +00:00
finn.markwitz c20bc9b377 Fix error 2023-07-03 09:25:51 +00:00
finn.markwitz 72d1362676 Reverted nix commands Changes in CI 2023-07-03 08:48:49 +00:00
finn.markwitz 4cdbc642ac Updated CI COmmand 2023-07-03 08:46:19 +00:00
finn.markwitz fe4b2bdee9 Typo 2023-07-03 08:40:50 +00:00
finn.markwitz 86e4ea6903 Added juipiter flag to CI 2023-07-03 08:37:01 +00:00
fmarkwitz ba81de27da Added hardware url and generator url 2023-07-03 10:31:09 +02:00
finn.markwitz 83d9d979f6 added envrc 2023-07-03 07:58:41 +00:00
finn.markwitz b546dad014 added flake-parts url 2023-07-03 07:30:16 +00:00
finn.markwitz 02fd6092e4 allow nix-command 2023-07-03 07:27:02 +00:00
finn.markwitz 5e55e00c64 Moved Locale Settings 2023-07-03 07:26:22 +00:00
finn.markwitz d5724379ee Added pAckages to systems 2023-07-03 07:20:15 +00:00
marthsincemelee 11575dc441 Added link to nixos config to readme 2023-07-02 23:09:35 +02:00
marthsincemelee e4d62003f2 Added Default imports for modules and environments 2023-07-02 23:07:51 +02:00
finn.markwitz d1138eee25 Reverted flake name 2023-07-02 21:00:05 +00:00
finn.markwitz 4f10febcdf Added Flake name 2023-07-02 13:14:46 +02:00
finn.markwitz 578cd0b23f Updated Import 2023-07-01 13:58:34 +02:00
finn.markwitz b6b022dd02 Fixed Syntax 2023-07-01 13:49:11 +02:00
finn.markwitz f8cee7df14 Update Flake 2023-07-01 13:45:39 +02:00
finn.markwitz 9d0f057e8c Revert CI 2023-07-01 13:41:52 +02:00
finn.markwitz bc5863280d CI 2023-07-01 13:41:17 +02:00
finn.markwitz c24026dad0 Updated Nix Config 2023-07-01 13:39:40 +02:00
finn.markwitz 7c1aa9a808 Fixed Syntax v2 2023-07-01 13:25:44 +02:00
finn.markwitz 2845e94cee Fixed Syntax 2023-07-01 13:25:08 +02:00
finn.markwitz 1e77d89666 Enabled Feature flakes 2023-07-01 13:21:32 +02:00
finn.markwitz 09ff767c08 No Cd in CI 2023-07-01 13:20:34 +02:00
finn.markwitz 4fd9c70283 Removed unknon stuff from flake.nix 2023-07-01 13:19:46 +02:00
finn.markwitz d76325230c Imported Felix flake.nix 2023-07-01 11:12:50 +00:00
finn.markwitz 4eb538648e enable build command 2023-07-01 11:08:51 +00:00
finn.markwitz 97c1d0e1d3 Updated CI and added Flake.nix 2023-07-01 11:07:39 +00:00
finn.markwitz 466720f02e Changed to use Jupiter 2023-07-01 11:00:46 +00:00
finn.markwitz 55127d29d5 Updated CI 2023-07-01 10:59:07 +00:00
finn.markwitz 7bea34b915 Update .gitlab-ci.yml file 2023-07-01 10:34:41 +00:00
finn.markwitz fe58b4a77a Update Ci 2023-06-30 12:48:18 +00:00
finn.markwitz 5121b50d36 Add CI File 2023-06-30 12:46:00 +00:00
finn.markwitz 498328fea7 Packages, Disks, & Hardware 2023-06-30 11:01:10 +00:00
finn.markwitz 2003e17fa4 Felix Erklaerbaer Termin 2023-06-30 09:02:00 +00:00
finn.markwitz 92ed683b43 Created basic config for machines 2023-06-27 11:40:02 +02:00
finn.markwitz 6173c533f2 Added Readme File 2023-06-27 11:35:22 +02:00
80 changed files with 5737 additions and 2 deletions
+7
View File
@@ -0,0 +1,7 @@
export DIRENV_WARN_TIMEOUT=5m
if ! has nix_direnv_version || ! nix_direnv_version 2.2.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.0/direnvrc" "sha256-5EwyKnkJNQeXrRkYbwwRBcXbibosCJqyIUuz9Xq+LRc="
fi
use flake
+4
View File
@@ -0,0 +1,4 @@
AGENTS.md
*.dec
keys.txt
.superpowers/
+13
View File
@@ -0,0 +1,13 @@
stages:
- build
jupiter-build:
image: nixos/nix
stage: build
script:
- ls -la
- nix-build --version
- nix flake show --extra-experimental-features 'nix-command flakes'
- nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' --extra-experimental-features 'nix-command flakes'
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+5
View File
@@ -0,0 +1,5 @@
<project version="4">
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/nixos.iml" filepath="$PROJECT_DIR$/.idea/nixos.iml" />
</modules>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+19
View File
@@ -0,0 +1,19 @@
keys:
- &admin_finn age1a0v442ct2qfjcdhyktn25spw02yq4fl08plrrvuradwwxzsle4mq8thln4
- &host_mibook age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p
- &host_jupiter age15uxuaxhn2hr4ypmgll36fwnkdkvv6gynvlxuc2p8ek0enmxmr5rsvlq0r2
creation_rules:
- path_regex: ^secrets/mibook\.yaml$
key_groups:
- age:
- *admin_finn
- *host_mibook
- path_regex: ^secrets/jupiter\.yaml$
key_groups:
- age:
- *admin_finn
- *host_jupiter
# - path_regex: ^secrets/common\.yaml$ # TODO: only if/when a secret needs to be shared across both hosts
# key_groups:
# - age: [ *admin_finn, *host_mibook, *host_jupiter ]
+104
View File
@@ -0,0 +1,104 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
# Apply configuration (switch/boot/test)
sudo nixos-rebuild switch --flake '.#jupiter'
sudo nixos-rebuild switch --flake '.#mibook'
# Build without switching (CI-style check)
nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'
# Update all flake inputs
nix flake update
# Format Nix files
nixfmt-rfc-style <file> # or: find . -name '*.nix' | xargs nixfmt-rfc-style
```
## Architecture
This is a flake-parts NixOS configuration for two machines:
- **jupiter** — home server running media/automation services
- **mibook** — laptop running KDE desktop + development tools
### Module Loading Chain
```
flake.nix
└── machines/configuration.nix # flake-parts module; defines nixosConfigurations
├── machines/core/ # base modules applied to every machine
│ ├── core.nix # system packages, locale, timezone
│ ├── network.nix
│ ├── nix.nix
│ ├── sops.nix # sops-nix decryption config, sops/age/ssh-to-age on PATH
│ └── users.nix
├── modules/ # custom NixOS option modules (my.profiles.*, my.hardware.*, my.services.*)
│ ├── environments/ # per-service/app profiles
│ ├── hardware/ # hardware profiles (nvidia, bluetooth, sound, wifi)
│ └── services/ # infrastructure services (vpn, webserver)
└── machines/<name>/
├── configuration.nix # machine-specific NixOS settings
├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options
├── secrets.nix # sops.secrets declared for this host
├── disks.nix
└── hardware-configuration.nix
```
### Secrets (sops-nix)
Secrets are encrypted with sops, decrypted per-host from that host's SSH host
key. `.sops.yaml` defines who can decrypt what; `secrets/<host>.yaml` holds
the encrypted values; `machines/<host>/secrets.nix` declares which secrets
that host exposes and consumes them via `config.sops.secrets.<name>.path`.
Always `git add` a secrets file before `nix build` — untracked files are
invisible to flake evaluation. Full workflow (adding a secret, adding a host,
rotating keys): see [`docs/secrets.md`](docs/secrets.md).
### Profile / Module Pattern
Every module under `modules/` follows the same structure:
```nix
{ config, lib, pkgs, ... }:
let cfg = config.my.profiles.<name>; in
{
options.my.profiles.<name>.enable = lib.mkEnableOption "...";
config = lib.mkIf cfg.enable { ... };
}
```
Namespaces in use:
- `my.profiles.*` — application/service profiles
- `my.hardware.*` — hardware profiles
- `my.services.*` — infrastructure services
Profiles are enabled per-machine in `machines/<name>/environments.nix`.
### Unstable Packages
`pkgs.unstable` is available everywhere via an overlay defined in `machines/configuration.nix`. Use it when a package isn't in the pinned stable channel (`nixpkgs/nixos-25.11`).
### Homepage Dashboard Integration
Modules that expose a web UI can self-register with the homepage dashboard by adding to `my.homepage.services`:
```nix
my.homepage.services = [{
group = "Services";
name = "My Service";
description = "...";
href = "http://${hostName}:PORT";
icon = "si-iconname"; # optional
}];
```
### Adding a New Service Module
1. Create `modules/environments/<name>/default.nix` following the profile pattern above.
2. Add `./environments/<name>` to `modules/environments/default.nix` (or the relevant `default.nix`).
3. Enable it in the target machine's `machines/<name>/environments.nix`.
+30 -2
View File
@@ -1,3 +1,31 @@
# nixos # Private NixOS Configuration
Private NixOS configuration [Felix Nixos Config](https://github.com/Stunkymonkey/nixos)
## Structure
```
.
├── images # custom image generations
├── machines # machine definitions
├── modules # own nix-options, to modularize services/hardware/...
├── overlays # overlays
├── pkgs # own packages, which are not available in nixpkgs
└── environments # summarize module collections into single options
```
## Commands
```bash
# Rebuild (switch/boot/test)
sudo nixos-rebuild switch --flake '.#jupiter'
# Update Flake
nix flake update
# Channel list
sudo nix-channel --list
# Channel update
sudo nix-channel --update
```
+92
View File
@@ -0,0 +1,92 @@
# Secrets (sops-nix)
Secrets are encrypted with [sops](https://github.com/getsops/sops) and decrypted
at activation by [sops-nix](https://github.com/Mic92/sops-nix). Each host decrypts
with its own SSH host key (converted to an age key in-memory — no separate host
private key is ever stored on disk).
Tooling (`sops`, `age`, `ssh-to-age`) is available via `nix develop` in this repo,
and installed system-wide on every machine via `machines/core/sops.nix`.
## Adding a secret to an existing host
```
nix develop -c sops secrets/<host>.yaml
```
This opens `$EDITOR` on the decrypted content. Add your key, save, quit — sops
re-encrypts on write. Then declare it in `machines/<host>/secrets.nix`:
```nix
sops.secrets.my-secret = {
owner = "someuser"; # defaults to root
group = "someuser";
mode = "0400";
restartUnits = [ "foo.service" ]; # if a running service needs to pick it up
};
```
Consume it in the owning module as `config.sops.secrets.my-secret.path`
(resolves to `/run/secrets/my-secret`) — never inline the decrypted value into
a Nix string, since that copies it into the world-readable `/nix/store`.
Then:
```
nix build '.#nixosConfigurations.<host>.config.system.build.toplevel'
sudo nixos-rebuild test --flake '.#<host>'
# verify, then:
sudo nixos-rebuild switch --flake '.#<host>'
```
**The secrets file must be `git add`ed before `nix build`** — untracked files
are invisible to flake evaluation, and `sops.validateSopsFiles` needs it in the
Nix store.
## Adding a new host
1. On the new host: `ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub` to get its
age public key.
2. In `.sops.yaml`: add a `&host_<name>` anchor with that key, and a
`creation_rules` entry for `secrets/<name>.yaml` including both the new
host key and `*admin_finn`.
3. Create `machines/<name>/secrets.nix`, add it to that machine's
`configuration.nix` imports.
4. If the host needs to read an existing shared file, run
`sops updatekeys secrets/<file>.yaml` after step 2 so it's re-encrypted to
include the new recipient.
## Rotating keys
```
# after editing .sops.yaml (new/removed recipient):
nix develop -c sops updatekeys secrets/<file>.yaml
```
for every file the change affects. `updatekeys` requires you to already be
able to decrypt the file — this is why `*admin_finn` is a recipient on every
rule, as the standing fallback.
Reinstalling a host regenerates its SSH host key, which changes its age
identity. Any file previously encrypted to that host must be `updatekeys`'d
with the new key, or the host's next activation fails to decrypt.
## Where private keys live
- **Operator** (you): `~/.config/sops/age/keys.txt` — sops' default lookup
path, so `SOPS_AGE_KEY_FILE` never needs to be set. Never committed. Back
this up out-of-band (password manager, offline copy) — if it's lost, only
the surviving host keys can still decrypt their own files, and any file that
was only ever readable by an operator-only rule becomes permanently
unreadable.
- **Hosts**: `/etc/ssh/ssh_host_ed25519_key` — already managed by NixOS,
converted to age in-memory at activation time. Nothing extra to back up.
## Layout
```
.sops.yaml # age recipients + creation rules (which keys can decrypt which files)
secrets/<host>.yaml # per-host encrypted secrets (default sopsFile for that host)
machines/core/sops.nix # applied to every machine: decryption config, sops/age/ssh-to-age on PATH
machines/<host>/secrets.nix # per-host: which secrets exist, owner/group/mode
```
@@ -0,0 +1,607 @@
# ZBT-2 Thread + OTBR Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Run the Home Assistant Connect ZBT-2 as an OpenThread Border Router on `jupiter`, fully integrated with the existing native `services.home-assistant` + `services.matter-server` stack so Matter-over-Thread devices commission through the dongle.
**Architecture:** Single NixOS module file (`modules/environments/home-assistant/default.nix`) is edited to import the `services.openthread-border-router` module from `nixos-unstable` (not yet in 25.11 stable), enable it against the ZBT-2's `/dev/serial/by-id/...` path, and add HA's `otbr` + `thread` extra components. The previous ZHA-direction commit on this branch is reverted first. The dongle is one-time-flashed from Zigbee NCP firmware to OpenThread RCP firmware via `universal-silabs-flasher` outside the NixOS lifecycle (per design decision: option B, CLI-only).
**Tech Stack:** Nix flakes (flake-parts), NixOS 25.11 stable + nixos-unstable, `services.openthread-border-router`, `services.home-assistant`, `services.matter-server`, `python313Packages.universal-silabs-flasher`.
**Spec:** [`docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md`](../specs/2026-05-10-zbt2-thread-otbr-design.md) — read this before starting.
**User feedback rules in force:**
- Never commit to `master`; this branch is `feature/ha-zbt-2-thread`. Final merge happens at the end via PR or operator-driven merge.
- Do not SSH to `jupiter`. All commands targeting jupiter are operator handoffs — present the command, the user runs it and pastes output back.
---
## File Map
| Action | File | Responsibility |
|--------|------|----------------|
| Modify | `modules/environments/home-assistant/default.nix` | Import unstable OTBR module; enable OTBR for the ZBT-2; add `otbr` + `thread` HA components |
| Modify | `machines/configuration.nix` | Pass `self` via `specialArgs` so it's available during NixOS module **imports** evaluation (not just config) |
| Create (auto) | _(no new files)_ | All work fits in the two modules |
The `git revert` of `e8d09f4` automatically un-modifies the home-assistant module (drops `"zha"` and the `dialout` line). No host-level (`machines/jupiter/`) changes.
**Why the flake-level edit is needed:** the existing `_module.args.self = self;` in `machines/configuration.nix:21` makes `self` available in module bodies (option definitions, `config` blocks). It does **not** make `self` available during `imports` evaluation — `_module.args` is resolved from `config`, but `imports` are collected **before** `config` is evaluated, so `self` in `imports` causes an infinite recursion error. Promoting `self` to `specialArgs` short-circuits that and is the conventional fix.
---
## Validation Approach (instead of unit tests)
This is a NixOS configuration change; there's no test framework. We use `nix eval` against `nixosConfigurations.jupiter.config.*` as the equivalent of unit tests — assert option resolution **before** the change (red), then **after** the change (green). Functional / smoke tests happen post-`nixos-rebuild` on jupiter via systemctl, mDNS, and the HA UI.
All `nix eval` commands run on the dev Mac. All `systemctl` / `journalctl` / `nixos-rebuild` commands run on jupiter (operator handoff).
---
### Task 1: Revert the prior ZHA commit
**Files:**
- Modify: `modules/environments/home-assistant/default.nix` (via `git revert`)
- [ ] **Step 1: Verify pre-state**
On dev Mac, in the repo root:
```bash
git log --oneline -3
```
Expected: `dbeda27` (design spec) on top of `e8d09f4` (the ZHA commit) on top of `098e632`.
Also confirm current `extraComponents` includes `"zha"`:
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app","zha"]`
- [ ] **Step 2: Revert**
```bash
git revert --no-edit e8d09f4
```
Expected: revert commit created cleanly (no merge conflicts), single file changed.
- [ ] **Step 3: Verify post-state**
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app"]` — `zha` is gone.
```bash
nix eval --json .#nixosConfigurations.jupiter.config.users.users.hass.extraGroups
```
Expected: `[]` — `dialout` is gone.
```bash
git log --oneline -4
```
Expected: revert commit on top of `dbeda27` on top of `e8d09f4`.
(No explicit `git commit` step — `git revert` produced its own commit.)
---
### Task 2: Wire the unstable OTBR module import (still disabled)
This task gets the module into scope so options become available, but leaves `services.openthread-border-router.enable = false` (the default). The point is to confirm the import path works before adding device-specific config.
**Files:**
- Modify: `machines/configuration.nix` (add `specialArgs = { inherit self; };` to each `nixosSystem` call)
- Modify: `modules/environments/home-assistant/default.nix`
- [ ] **Step 1: Write the failing eval check**
On dev Mac:
```bash
nix eval --json .#nixosConfigurations.jupiter.options.services.openthread-border-router.enable.description 2>&1 | head -3
```
Expected: error containing `attribute 'openthread-border-router' missing` or similar — the option doesn't exist yet because the module isn't imported.
- [ ] **Step 1a: Promote `self` to `specialArgs` in `machines/configuration.nix`**
`self` must be reachable during `imports` evaluation (not just `config` evaluation). The existing `_module.args.self = self;` only covers `config`-time access. Edit each `nixosSystem` call (`jupiter` and `mibook`) to add `specialArgs`.
Current shape (lines 50–56 and 57–63):
```nix
jupiter = nixosSystem {
system = "x86_64-linux";
modules = defaultModules ++ [
# nixos-hardware.nixosModules.bmax-b7-power
./jupiter/configuration.nix
];
};
mibook = nixosSystem {
system = "x86_64-linux";
modules = defaultModules ++ [
# nixos-hardware.nixosModules.mibook
./mibook/configuration.nix
];
};
```
Add `specialArgs = { inherit self; };` to each:
```nix
jupiter = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.bmax-b7-power
./jupiter/configuration.nix
];
};
mibook = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.mibook
./mibook/configuration.nix
];
};
```
- [ ] **Step 2: Add `self` to the module's argument list and add the `imports` block**
Current header (`modules/environments/home-assistant/default.nix` lines 1–11):
```nix
# manages home automations
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
```
Replace lines 1–11 with:
```nix
# manages home automations
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
```
Then, immediately after the opening brace on line 12 of the modified file (i.e. at the top of the attribute set body, before `options.my.profiles.home-assistant`), add:
```nix
imports = [
# services.openthread-border-router isn't in nixos-25.11; pull from
# nixpkgs-unstable. Package comes from the existing unstable overlay.
"${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix"
];
```
- [ ] **Step 3: Re-run the eval check**
```bash
nix eval --json .#nixosConfigurations.jupiter.options.services.openthread-border-router.enable.description 2>&1 | head -3
```
Expected: a JSON string describing the option (e.g. `"Whether to enable the OpenThread Border Router."`).
- [ ] **Step 4: Verify the service is currently disabled**
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable
```
Expected: `false`.
- [ ] **Step 5: Verify whole config still evaluates**
```bash
nix eval .#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath
```
Expected: a `/nix/store/...drv` path. Pre-existing trace warnings (the `*.service ordered after network-online.target` ones) are fine; no errors.
- [ ] **Step 6: Commit**
```bash
git add machines/configuration.nix modules/environments/home-assistant/default.nix
git commit -m "$(cat <<'EOF'
feat(home-assistant): import openthread-border-router module from unstable
Pulls the services.openthread-border-router NixOS module directly from
nixpkgs-unstable since it isn't in 25.11 yet. Service stays disabled
in this commit; configuration follows.
Also promotes `self` from `_module.args` to `specialArgs` in
machines/configuration.nix, since `imports` are evaluated before
`config` and so can't reach `_module.args.self`.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
---
### Task 3: Operator handoff — get the ZBT-2 device path from jupiter
This task has no code. It collects the runtime parameter (USB serial number) that Task 4 needs.
**Files:** _(none)_
- [ ] **Step 1: Hand off**
Tell the operator:
> "Plug the ZBT-2 into a USB-2 port on jupiter (it's still on stock Zigbee firmware — that's fine for this step). Then run `ls -l /dev/serial/by-id/` on jupiter and paste the full output back. We're after the line that contains `Nabu_Casa_Home_Assistant_Connect_ZBT-2`."
- [ ] **Step 2: Wait for the operator's pasted output**
Expected shape: a line like
`lrwxrwxrwx 1 root root 13 May 10 14:30 usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial-string>-if00 -> ../../ttyACM0`
- [ ] **Step 3: Record the by-id path**
Capture the value `/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial-string>-if00` for use in Task 4. Use the **by-id** path (not `/dev/ttyACM0`) so USB renumbering can't break OTBR.
---
### Task 4: Enable OTBR + add HA otbr/thread components
**Files:**
- Modify: `modules/environments/home-assistant/default.nix`
- [ ] **Step 1: Write the failing eval checks**
On dev Mac:
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable
```
Expected: `false` (still disabled from Task 2).
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app"]` — no `otbr`, no `thread` yet.
- [ ] **Step 2: Add `"otbr"` and `"thread"` to `extraComponents`**
In `modules/environments/home-assistant/default.nix`, locate the `extraComponents` list (currently `[ "matter" "mobile_app" ]`) and replace it with:
```nix
extraComponents = [
"matter"
"mobile_app"
"otbr"
"thread"
];
```
- [ ] **Step 3: Add the `services.openthread-border-router` block**
In the same file, **after** the `services.home-assistant.config = { ... };` block and **before** `my.homepage.services`, add:
```nix
services.openthread-border-router = {
enable = true;
package = pkgs.unstable.openthread-border-router;
openFirewall = true;
backboneInterfaces = [ "enp3s0" ];
radio.device = "<PASTE-BY-ID-PATH-FROM-TASK-3>";
};
```
Replace `<PASTE-BY-ID-PATH-FROM-TASK-3>` with the literal string captured in Task 3 step 3 (e.g. `"/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_AB12CD34-if00"`).
- [ ] **Step 4: Run the green eval checks**
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: `["matter","mobile_app","otbr","thread"]`.
```bash
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.enable
```
Expected: `true`.
```bash
nix eval --raw .#nixosConfigurations.jupiter.config.services.openthread-border-router.radio.url
```
Expected: a string like `spinel+hdlc+uart:///dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_<serial>-if00?uart-baudrate=115200` (the module composes this from `radio.device` automatically).
- [ ] **Step 5: Full eval — system derivation must build**
```bash
nix eval .#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath
```
Expected: a `/nix/store/...drv` path with no eval errors.
- [ ] **Step 6: `nix flake check` for good measure**
```bash
nix flake check
```
Expected: no errors. (Same pre-existing trace warnings as before are acceptable.)
- [ ] **Step 7: Commit**
```bash
git add modules/environments/home-assistant/default.nix
git commit -m "$(cat <<'EOF'
feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components
Brings up otbr-agent against the ZBT-2 over Spinel/UART, opens the
REST API on :8081, and wires HA's otbr + thread integrations so
Matter-over-Thread devices can commission through the existing
matter-server.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
EOF
)"
```
---
### Task 5: Operator handoff — flash OpenThread RCP firmware on the dongle
The dongle is currently running Zigbee NCP firmware and won't speak Spinel until reflashed. This must happen **before** Task 6's rebuild (otherwise `otbr-agent` will try to talk to a Zigbee-firmware dongle and fail).
**Files:** _(none on dev Mac)_
- [ ] **Step 1: Hand off — fetch firmware**
Tell the operator:
> "On any machine with a browser: download the latest **ZBT-2 OpenThread RCP** `.gbl` from <https://github.com/NabuCasa/silabs-firmware-builder/releases>. The asset name will look like `ot-rcp-zbt-2-<version>.gbl`. Get it onto jupiter — `scp` it over, or just `curl` from jupiter's shell. Confirm by running `ls ~/ot-rcp-zbt-2-*.gbl` on jupiter and pasting the result."
- [ ] **Step 2: Wait for confirmation**
Expected: a single matching path, e.g. `/home/finn/ot-rcp-zbt-2-2025.10.0.gbl`.
- [ ] **Step 3: Hand off — flash**
Tell the operator:
> "On jupiter, run (substituting the actual by-id path from Task 3 and the actual `.gbl` filename):
>
> ```bash
> nix shell nixpkgs#python313Packages.universal-silabs-flasher -c \
> universal-silabs-flasher \
> --device /dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-if00 \
> flash --firmware ~/ot-rcp-zbt-2-<version>.gbl
> ```
>
> Paste the full output. Expected duration: ~30 seconds. The tool detects the running firmware, drops the dongle into bootloader mode, writes the `.gbl`, and reboots back to RCP."
- [ ] **Step 4: Verify the flash succeeded**
Expected output ends with something like `Firmware update complete` (or equivalent success message). If the tool reports CRC failure / partial write — re-run; the bootloader stays addressable.
If the operator reports `--help` shows different subcommand syntax (universal-silabs-flasher's CLI has changed across versions), have them check `universal-silabs-flasher --help` and adapt — but the `flash --firmware <path>` form has been stable since 1.0.x.
---
### Task 6: Operator handoff — `nixos-rebuild switch` on jupiter
**Files:** _(none on dev Mac)_
- [ ] **Step 1: Push the branch so jupiter can fetch it**
On dev Mac:
```bash
git push -u origin feature/ha-zbt-2-thread
```
(If the operator pulls via a different mechanism — local checkout, fileshare — adapt accordingly. The standard pattern in this repo is `git pull` on jupiter.)
- [ ] **Step 2: Hand off — pull + rebuild**
Tell the operator:
> "On jupiter:
>
> ```bash
> cd ~/development/nixos # or wherever the flake lives on jupiter
> git fetch origin
> git checkout feature/ha-zbt-2-thread
> sudo nixos-rebuild switch --flake .#jupiter
> ```
>
> Paste the tail of the output (everything from the first `building ...` line onward). Expected: build completes, switch to the new generation, no errors."
- [ ] **Step 3: Verify the switch succeeded**
If the operator's pasted output includes `error:` or the switch failed mid-activation, **stop here**. Common failure: option name mismatch with whatever version of nixos-unstable is locked in the flake. Fix on dev Mac, push, ask operator to pull + rebuild again.
If the rebuild succeeded, proceed to Task 7.
---
### Task 7: Operator handoff — service-level verification on jupiter
**Files:** _(none)_
- [ ] **Step 1: Hand off — service health**
Tell the operator:
> "On jupiter, run each command and paste output:
>
> ```bash
> systemctl status otbr-agent.service --no-pager
> journalctl -u otbr-agent.service -n 50 --no-pager
> ip link show wpan0
> ```"
- [ ] **Step 2: Verify**
Expected:
- `systemctl status` reports `active (running)`.
- `journalctl` shows OTBR startup messages, no repeated restart loops.
- `ip link show wpan0` shows the interface exists; state DOWN is correct (HA hasn't formed a network yet).
If `otbr-agent` is in restart loop with `Failed to open device`: device path mismatch. Re-check Task 3's path.
- [ ] **Step 3: Hand off — mDNS publication**
Tell the operator:
> "On jupiter:
>
> ```bash
> avahi-browse -r -t _meshcop._udp
> ```"
Expected: one entry whose hostname matches jupiter, advertising port 8081.
If empty: `backboneInterfaces` is wrong. On jupiter, run `ip link show` and tell operator to paste; pick the actual primary LAN interface, update `backboneInterfaces`, re-rebuild.
- [ ] **Step 4: Hand off — REST API reachability**
Tell the operator:
> "On jupiter:
>
> ```bash
> curl -s http://127.0.0.1:8081/node/state
> ```"
Expected: a JSON state string, most likely `"disabled"` (HA hasn't formed a network yet).
If connection refused: OTBR isn't actually listening — re-check `journalctl`.
---
### Task 8: Operator handoff — HA UI smoke test
**Files:** _(none)_
- [ ] **Step 1: Hand off — confirm discovery**
Tell the operator:
> "Open `http://jupiter:8123` in a browser. Go to **Settings → Devices & Services**. Within ~30s of the rebuild, you should see **'Open Thread Border Router'** under 'Discovered'. Click **Configure**. Let HA form a new Thread network (or import existing dataset if you have one). Tell me when that's done — and paste any errors if it doesn't work."
- [ ] **Step 2: Wait for confirmation**
Expected: HA reports the Thread network is formed; the OTBR integration appears under 'Configured'.
If discovery doesn't happen: cross-check with Task 7 step 3 (`avahi-browse`). HA reads from the system's avahi cache.
- [ ] **Step 3: Hand off — Matter-over-Thread pairing**
Tell the operator:
> "Pick one Matter-over-Thread device. Use the HA Companion app, scan its Matter QR code, and follow the prompts. Tell me when it's paired — or paste any errors. Pairing should complete in 30–90s."
- [ ] **Step 4: Wait for confirmation**
Expected: device appears under both Matter and Thread integrations in HA, and is controllable from the dashboard.
If pairing times out: see "Failure modes" table in the spec — most likely Thread mesh prefix isn't routed back to LAN. Operator runs `nft list ruleset` and `ip -6 route` on jupiter; debug from there.
---
### Task 9: Merge to master
**Files:** _(none)_
- [ ] **Step 1: Final branch state**
On dev Mac:
```bash
git log --oneline master..feature/ha-zbt-2-thread
```
Expected (in chronological order from oldest to newest):
1. `e8d09f4` — original ZHA commit
2. `dbeda27` — design spec
3. `<revert hash>` — Revert "feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle"
4. `<task-2 hash>` — feat(home-assistant): import openthread-border-router module from unstable
5. `<task-4 hash>` — feat(home-assistant): enable OTBR for ZBT-2 + add HA otbr/thread components
That's a fine history to merge as-is (the ZHA→revert pair is honest about the pivot).
- [ ] **Step 2: Hand off — merge**
The user runs the merge themselves (per repo policy: never commit to master without explicit consent). Tell the operator:
> "If the smoke tests in Task 8 worked, merge with:
>
> ```bash
> git switch master
> git merge --no-ff feature/ha-zbt-2-thread
> git push origin master
> ```
>
> Or open a merge request / PR if you prefer review first."
- [ ] **Step 3: Optional cleanup**
After merge:
```bash
git branch -d feature/ha-zbt-2-thread
git push origin --delete feature/ha-zbt-2-thread
```
---
## Self-Review
**Spec coverage:**
- Goals (4 bullets) → Tasks 2 (OTBR module wiring), 4 (OTBR enable + HA components), 5 (firmware flash), 8 (Matter-over-Thread smoke test) ✓
- Non-goals → respected; no multipan, no auto-flash, no fallback paths ✓
- Architecture diagram → Task 4 produces the wiring shown; Tasks 6–8 verify it ✓
- File changes (one module) → Tasks 1, 2, 4 ✓
- Reverts of prior ZHA commit → Task 1 ✓
- Operator workflow steps 0–7 → Tasks 1, 2, 3, 4, 5, 6, 7, 8 ✓
- Verification (eval-only / service-level / functional) → Tasks 2/4/6/7/8 ✓
- Failure-mode table → referenced in Tasks 6, 7, 8 for triage ✓
**Placeholder scan:**
- `<PASTE-BY-ID-PATH-FROM-TASK-3>` in Task 4 step 3 is intentional — it's a runtime parameter the operator fills in, captured in Task 3.
- `<serial>`, `<version>` in shell commands are intentional placeholders for operator substitution.
- No "TBD", "TODO", "implement later", or vague "handle errors" steps.
**Type / name consistency:**
- `services.openthread-border-router` used consistently (matches the unstable module's option path).
- `pkgs.unstable.openthread-border-router` matches the overlay (`machines/configuration.nix:11`).
- `extraComponents` strings (`"otbr"`, `"thread"`) match HA Core integration names.
- `radio.device` ↔ `radio.url` relationship documented (module composes `url` from `device`).
@@ -0,0 +1,232 @@
# MiBook Claude Code Module Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add a `my.profiles.claude-code` NixOS module that installs the Claude Code CLI and companion tooling on the MiBook.
**Architecture:** A single new environment module (`modules/environments/claude-code/default.nix`) declares the `my.profiles.claude-code.enable` option and installs packages for user `finn`. It is registered in `modules/environments/default.nix` and toggled on in `machines/mibook/environments.nix`.
**Tech Stack:** Nix flakes, NixOS module system, `pkgs.unstable` overlay (already present in repo)
---
## File Map
| Action | Path | Responsibility |
|---|---|---|
| Create | `modules/environments/claude-code/default.nix` | Declares option + installs packages |
| Modify | `modules/environments/default.nix` | Registers the new module so NixOS loads it |
| Modify | `machines/mibook/environments.nix` | Enables the profile for the MiBook host |
---
### Task 1: Verify `claude-code` exists in `nixpkgs-unstable`
**Files:**
- Read-only check — no file changes
- [ ] **Step 1: Check if the package is available**
Run:
```bash
nix eval --extra-experimental-features 'nix-command flakes' 'github:NixOS/nixpkgs/nixos-unstable#claude-code.version' 2>&1
```
Expected (success): prints a version string like `"0.2.x"`
Expected (failure): `error: attribute 'claude-code' missing`
- [ ] **Step 2: If the package exists — note the attribute path and continue to Task 2**
The module will use `pkgs.unstable.claude-code`.
- [ ] **Step 3: If the package does NOT exist — add a custom derivation first**
Create `pkgs/claude-code/default.nix`:
```nix
{ lib, buildNpmPackage, fetchFromGitHub }:
buildNpmPackage rec {
pname = "claude-code";
version = "0.2.116"; # update to latest release tag
src = fetchFromGitHub {
owner = "anthropics";
repo = "claude-code";
rev = "v${version}";
hash = lib.fakeHash; # run nix build to get real hash
};
npmDepsHash = lib.fakeHash; # run nix build to get real hash
meta = {
description = "Claude Code CLI by Anthropic";
homepage = "https://github.com/anthropics/claude-code";
license = lib.licenses.unfree;
mainProgram = "claude";
};
}
```
Then register it in `pkgs/default.nix`:
```nix
final: prev: {
claude-code = final.callPackage ./claude-code { };
}
```
And use `pkgs.claude-code` (not `pkgs.unstable.claude-code`) in the module.
---
### Task 2: Create the `claude-code` module
**Files:**
- Create: `modules/environments/claude-code/default.nix`
- [ ] **Step 1: Create the module file**
Create `modules/environments/claude-code/default.nix` with this exact content:
```nix
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.claude-code;
in
{
options.my.profiles.claude-code = with lib; {
enable = mkEnableOption "Claude Code CLI";
};
config = lib.mkIf cfg.enable {
users.users.finn.packages = with pkgs; [
unstable.claude-code
ripgrep
fd
gh
jq
];
# Future: headless Claude Code service
# A natural next step is exposing Claude Code as a persistent background service —
# e.g. a systemd user service that accepts work via an HTTP API or Unix socket,
# triggerable over SSH or a local network endpoint. This would turn the MiBook
# into a true remote execution node without requiring an interactive session.
# See: my.profiles.claude-code.service.enable (not yet implemented)
};
}
```
> Note: if Task 1 Step 3 was taken (custom derivation), replace `unstable.claude-code` with `claude-code`.
---
### Task 3: Register the module
**Files:**
- Modify: `modules/environments/default.nix`
- [ ] **Step 1: Add the import**
In `modules/environments/default.nix`, add `./claude-code` to the imports list (alphabetical order puts it between `./audiobookshelf` and `./development`):
```nix
{ ... }:
{
imports = [
./actual
./apps
./audiobookshelf
./claude-code
./development
./home-assistant
./hyprland
./zsh
./paperless
./prowlarr
./radarr
./docker
./homepage
./kde-desktop
./readarr
./sonarr
./jellyfin
./jellyseerr
];
}
```
- [ ] **Step 2: Verify the option is now defined (without enabling it)**
Run:
```bash
nix eval --extra-experimental-features 'nix-command flakes' '.#nixosConfigurations.mibook.options.my.profiles.claude-code.enable.description'
```
Expected: `"Whether to enable Claude Code CLI."`
If this errors, the module isn't loading — re-check the import path.
---
### Task 4: Enable on MiBook and verify the build
**Files:**
- Modify: `machines/mibook/environments.nix`
- [ ] **Step 1: Enable the profile**
In `machines/mibook/environments.nix`, add `claude-code.enable = true` inside the `my.profiles` block:
```nix
my.profiles = {
kde-desktop.enable = true;
zsh.enable = true;
apps = {
desktop_apps = true;
dev_apps = true;
};
development.enable = true;
docker.enable = true;
claude-code.enable = true;
};
```
- [ ] **Step 2: Verify the package appears in finn's user packages**
Run:
```bash
nix eval --extra-experimental-features 'nix-command flakes' '.#nixosConfigurations.mibook.config.users.users.finn.packages' --apply 'builtins.map (p: p.name)' 2>&1 | grep -i claude
```
Expected: a line containing `claude-code-<version>`
- [ ] **Step 3: Dry-run build to confirm the full config evaluates**
Run:
```bash
nix build '.#nixosConfigurations.mibook.config.system.build.toplevel' --extra-experimental-features 'nix-command flakes' --dry-run 2>&1 | tail -5
```
Expected: exits 0, output lists derivations to build (or "nothing to do" if already cached). No evaluation errors.
---
### Task 5: Commit
**Files:**
- All changed files from Tasks 1–4
- [ ] **Step 1: Stage and commit**
```bash
git add modules/environments/claude-code/default.nix \
modules/environments/default.nix \
machines/mibook/environments.nix
git commit -m "feat(mibook): add claude-code profile module"
```
If Task 1 Step 3 was taken, also stage `pkgs/claude-code/default.nix` and `pkgs/default.nix`.
@@ -0,0 +1,154 @@
# Jellyfin Hardware Transcoding (jupiter) Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give jupiter's Jellyfin service access to the Intel iGPU's VAAPI render node so Quick Sync hardware transcoding can be enabled, instead of every transcode falling back to CPU.
**Architecture:** One NixOS module change (`modules/environments/jellyfin/default.nix`) grants the `jellyfin` systemd service supplementary access to the `video`/`render` groups and installs `libva-utils` for verification. This is declarative and build-verifiable from the Mac. Enabling Quick Sync inside Jellyfin's own dashboard, and the on-machine verification, is a manual step run by the user on jupiter after deploy — the NixOS module has no option for it and this environment's convention is that the assistant never SSHes into jupiter directly (see `docs/superpowers/specs/2026-07-26-jellyfin-hw-transcoding.md`).
**Tech Stack:** NixOS (flake-parts), nixpkgs `services.jellyfin` module, VAAPI/`intel-media-driver`, `libva-utils`.
## Global Constraints
- No SSH from the assistant into jupiter — all on-machine commands are given to the user to run and paste back.
- Follow the existing profile pattern in `modules/environments/jellyfin/default.nix` (`config = lib.mkIf cfg.enable { ... }`); don't introduce a new toggle option — hardcode the hardware-acceleration wiring on, per the approved spec.
- Verify locally via `nix eval` / `nix build` before asking the user to deploy.
---
### Task 1: Grant Jellyfin access to the iGPU and verify the build
**Files:**
- Modify: `modules/environments/jellyfin/default.nix`
**Interfaces:**
- Produces: `systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [ "video" "render" ];` — verified via `nix eval` in Step 2.
- [ ] **Step 1: Add the device-access config and `libva-utils` package**
Read the current file first (`modules/environments/jellyfin/default.nix`), then edit the `config = lib.mkIf cfg.enable { ... }` block so it reads:
```nix
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
name = "Jellyfin";
description = "Media server";
href = "http://${hostName}:${toString port}";
icon = "jellyfin.png";
}
];
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
```
Note the two existing `systemd.services.jellyfin` keys (`after`) and the new `serviceConfig.SupplementaryGroups` now live in the same attrset — don't create a second `systemd.services.jellyfin = { ... }` block, it would overwrite the first.
- [ ] **Step 2: Verify the rendered config with `nix eval`**
Run (from the repo root on the Mac):
```bash
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expected output: `[ "video" "render" ]`
- [ ] **Step 3: Verify the machine still builds**
Run:
```bash
nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel' \
--extra-experimental-features 'nix-command flakes' --no-link
```
Expected: build succeeds with no errors (may take a while; watch for any evaluation error mentioning `jellyfin` or `libva-utils`).
- [ ] **Step 4: Format and commit**
```bash
nixfmt-rfc-style modules/environments/jellyfin/default.nix
git add modules/environments/jellyfin/default.nix
git commit -m "feat(jellyfin): grant iGPU access for Quick Sync hardware transcoding"
```
---
### Task 2: Deploy on jupiter and enable Quick Sync (user-executed)
**Files:** none (on-machine deploy + Jellyfin dashboard UI)
**Interfaces:**
- Consumes: the `SupplementaryGroups` change from Task 1, already merged into the flake.
These steps run **on jupiter**, by the user — paste the output back so we can confirm each one before moving to the next.
- [ ] **Step 1: Deploy**
```bash
sudo nixos-rebuild switch --flake '.#jupiter'
```
Expected: switch succeeds, no errors mentioning `jellyfin`.
- [ ] **Step 2: Confirm the service picked up the new groups**
```bash
systemctl show jellyfin -p SupplementaryGroups
systemctl status jellyfin --no-pager
```
Expected: `SupplementaryGroups=video render` (order may vary) and the service is `active (running)`.
- [ ] **Step 3: Confirm VAAPI driver loads**
```bash
vainfo
```
Expected: output starts with something like `vainfo: VA-API version: 1.x` and `Driver version: Intel iHD driver`, followed by a list of supported VAProfiles/VAEntrypoints (e.g. `VAProfileH264Main : VAEntrypointVLD`, `VAEntrypointEncSlice`).
If this instead prints a permissions or "no VA display" error, paste it back — that means the group grant isn't reaching the process and Task 1 needs a follow-up fix (e.g. the jellyfin service may be more sandboxed than expected, requiring an explicit `DeviceAllow=char-drm rw` in `serviceConfig` as well).
- [ ] **Step 4: Enable Quick Sync in the Jellyfin dashboard**
In the Jellyfin web UI:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library uses (H264 at minimum).
5. If the library has HDR content, enable tone-mapping.
6. Save.
- [ ] **Step 5: Functional check**
Play a file that requires transcoding (or force a lower quality in the client's playback settings to trigger one), then:
```bash
journalctl -u jellyfin -n 50 --no-pager
```
Look for a line referencing `qsv` or `vaapi` in the transcode command. Separately, watch CPU usage (`htop`) during playback — it should stay low on the core doing the transcode, rather than pegging at 100%, since the iGPU is now doing the encode/decode work.
## Self-Review Notes
- Spec coverage: NixOS change (Task 1) ✓, manual dashboard step (Task 2 Step 4) ✓, verification via `vainfo`/build (Task 1 Step 2-3, Task 2 Step 3) ✓, functional check (Task 2 Step 5) ✓. Toggle option explicitly excluded per approved spec — not present, correctly.
- No placeholders — every step has literal commands/code.
- `SupplementaryGroups` key/value matches exactly between Task 1 (produced) and Task 2 (consumed/checked).
@@ -0,0 +1,299 @@
# Immich NixOS Module Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> **Note on nature:** Task 1 is repo work verifiable with `nix build` (no runtime tests exist for declarative config). Tasks 2–6 are a **manual migration runbook executed on jupiter by the operator** — they are destructive and cannot be run from the dev machine (mibook). Do not attempt to automate or execute Tasks 2–6 from an agent session; present them for the operator to run and confirm.
**Goal:** Replace jupiter's docker-compose Immich with the native `services.immich` NixOS module, preserving all data (albums, faces, shares, library).
**Architecture:** A standard `my.profiles.immich` module wraps `services.immich` (native Postgres+VectorChord over unix socket, Redis, server, machine-learning). Media stays at the default local `/var/lib/immich`. The existing docker Postgres dump is restored same-version (2.7.5 → 2.7.5, no schema/vector migration). GPU is exposed for VAAPI/QSV transcoding.
**Tech Stack:** NixOS (flake-parts), `services.immich` from nixpkgs 25.11, PostgreSQL, Intel QSV/VAAPI, docker (source only).
## Global Constraints
- Machine: **jupiter** only. Do not enable on mibook.
- Immich version: source docker == target nixpkgs == **2.7.5** (stable). No `package` override. Do NOT bump nixpkgs Immich during this work.
- Media location: default `/var/lib/immich` (local disk). Do not point at the NAS.
- Database: local PostgreSQL over **unix socket + peer auth** — no password, no sops secret.
- HW accel: **video transcoding only**. ML stays on CPU (`machine-learning.enable = true`, no OpenVINO).
- Access: LAN + VPN, `openFirewall = true`, port **2283**. No reverse proxy/TLS.
- Rebuild command: `sudo nixos-rebuild switch --flake '.#jupiter'`.
- Build-check command: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`.
- Format Nix with `nixfmt-rfc-style` before committing.
- Do not delete docker DB or upload data until Task 6 sign-off.
---
## File Structure
- **Create** `modules/environments/immich/default.nix` — the `my.profiles.immich` module (single responsibility: declare Immich).
- **Modify** `modules/environments/default.nix` — add `./environments/immich` to the import list.
- **Modify** `machines/jupiter/environments.nix` — set `immich.enable = true`.
No other files change. The DB/media migration touches only runtime state on jupiter, not the repo.
---
### Task 1: Author the `immich` profile module
**Files:**
- Create: `modules/environments/immich/default.nix`
- Modify: `modules/environments/default.nix` (import list)
- Modify: `machines/jupiter/environments.nix` (`my.profiles.immich.enable`)
**Interfaces:**
- Produces: NixOS option `my.profiles.immich.enable` (bool). When true, configures `services.immich`, adds `immich` user to `video`/`render` groups, and appends an entry to `my.homepage.services`.
- Consumes: existing `my.homepage.services` aggregator; `config.networking.hostName`.
- [ ] **Step 1: Read a reference module to match repo style**
Read `modules/environments/jellyfin/default.nix` (same shape: `cfg`, `hostName`, `port`, `mkIf`, `my.homepage.services`). Match its formatting and header-comment convention.
- [ ] **Step 2: Create the module file**
Create `modules/environments/immich/default.nix`:
```nix
# Immich self-hosted photo & video server
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.immich;
hostName = config.networking.hostName;
port = 2283;
in
{
options.my.profiles.immich = with lib; {
enable = mkEnableOption "Immich photo server";
};
config = lib.mkIf cfg.enable {
services.immich = {
enable = true;
host = "0.0.0.0";
inherit port;
openFirewall = true;
mediaLocation = "/var/lib/immich";
machine-learning.enable = true;
accelerationDevices = [ "/dev/dri/renderD128" ];
settings.server.externalDomain = "http://${hostName}:${toString port}";
};
# The native module does not add GPU groups; required for VAAPI/QSV transcoding.
users.users.immich.extraGroups = [
"video"
"render"
];
my.homepage.services = [
{
group = "Media";
name = "Immich";
description = "Photo & video server";
href = "http://${hostName}:${toString port}";
icon = "immich.png";
}
];
};
}
```
- [ ] **Step 3: Register the module in the environments import list**
Open `modules/environments/default.nix` and add `./environments/immich` (or `./immich`, matching the exact relative style already used in that file — check how `jellyfin` is listed and mirror it).
- [ ] **Step 4: Enable it on jupiter**
In `machines/jupiter/environments.nix`, inside the `my.profiles = { ... }` block, add:
```nix
immich.enable = true;
```
- [ ] **Step 5: Format**
Run: `nixfmt-rfc-style modules/environments/immich/default.nix`
- [ ] **Step 6: Build-check (this is the "test")**
Run: `nix build '.#nixosConfigurations.jupiter.config.system.build.toplevel'`
Expected: builds successfully. If it fails on an unknown option (e.g. `accelerationDevices`, `settings.server.externalDomain`), reconcile against the module at `$(nix eval --raw '.#nixosConfigurations.jupiter.pkgs.path')/nixos/modules/services/web-apps/immich.nix` and fix.
- [ ] **Step 7: Confirm the option evaluates on**
Run: `nix eval '.#nixosConfigurations.jupiter.config.services.immich.enable'`
Expected: `true`
- [ ] **Step 8: Commit**
```bash
git add modules/environments/immich/default.nix modules/environments/default.nix machines/jupiter/environments.nix
git commit -m "feat(jupiter): add native Immich profile module"
```
---
### Task 2: Pre-flight & backup on jupiter (operator-run)
**Files:** none (runtime state on jupiter). Run all commands on jupiter.
**Interfaces:**
- Produces: `immich-db.sql` dump file and a known-good copy/snapshot of the docker upload folder; recorded `UPLOAD_LOCATION` path and DB container name.
- [ ] **Step 1: Record docker facts**
From the docker-compose dir on jupiter, note `UPLOAD_LOCATION`, the DB service/container name, and `POSTGRES_USER`/`POSTGRES_DB` from `.env`/compose. Confirm server version is **2.7.5** (web UI footer or `docker exec <server> immich --version`). If it is not 2.7.5, STOP — this plan assumes a same-version restore.
- [ ] **Step 2: Stop the docker stack (DB may stay up for the dump)**
Run: `docker compose stop immich-server immich-machine-learning` (leave the DB container running).
- [ ] **Step 3: Dump the database**
Run: `docker exec -t <db-container> pg_dumpall --clean --if-exists --username=<POSTGRES_USER> > ~/immich-db.sql`
Expected: a non-trivial `immich-db.sql` (check it is not near-empty: `wc -l ~/immich-db.sql`).
- [ ] **Step 4: Stop the DB and record the media size**
Run: `docker compose down` then `du -sh <UPLOAD_LOCATION>` and note the size. Do NOT copy yet. Do NOT delete anything.
---
### Task 3: First switch — let the module create empty state (operator-run)
**Files:** none at runtime (repo change already committed in Task 1). Run on jupiter after pulling the committed branch.
**Interfaces:**
- Consumes: `immich-db.sql`, `UPLOAD_LOCATION` from Task 2.
- Produces: an `immich` system user, an empty `immich` Postgres DB + role, and `/var/lib/immich` created with correct ownership, with services then stopped.
- [ ] **Step 1: Deploy the config**
On jupiter, check out the branch containing Task 1's commit and run:
`sudo nixos-rebuild switch --flake '.#jupiter'`
Expected: `immich-server`, `immich-machine-learning`, postgres, and redis units come up; UI reachable at `http://jupiter:2283` showing a fresh/empty instance.
- [ ] **Step 2: Stop immich so data can be swapped underneath**
Run: `sudo systemctl stop immich-server immich-machine-learning`
Expected: both inactive. PostgreSQL and Redis stay running.
- [ ] **Step 3: Verify the DB and user exist**
Run: `sudo -u postgres psql -c '\l' | grep immich` and `sudo -u postgres psql -c '\du' | grep immich`
Expected: an `immich` database and `immich` role are present.
---
### Task 4: Restore database and media (operator-run, destructive)
**Files:** none in repo. Run on jupiter. This overwrites the freshly-created empty DB.
**Interfaces:**
- Consumes: `immich-db.sql`, `<UPLOAD_LOCATION>`, the running NixOS PostgreSQL.
- Produces: the migrated DB and populated `/var/lib/immich`.
- [ ] **Step 1: Restore the dump into the NixOS Postgres**
`pg_dumpall` output includes role/DB creation. Load it as the `postgres` superuser over the unix socket:
Run: `sudo -u postgres psql -f ~/immich-db.sql`
Expected: completes without fatal errors. Harmless "role already exists"/"database already exists" notices are OK because of `--clean --if-exists`. If the immich DB ends up owned by the wrong role, reassign: `sudo -u postgres psql -c 'ALTER DATABASE immich OWNER TO immich;'`.
- [ ] **Step 2: Sanity-check the restored data**
Run: `sudo -u postgres psql -d immich -c 'SELECT count(*) FROM assets;'`
Expected: a count matching your library size (non-zero). If the table name differs by version, list tables with `\dt` and check an obviously-populated one.
- [ ] **Step 3: Move the media into the default location**
Immich's upload folder holds subdirs `library/ upload/ thumbs/ encoded-video/ profile/ backups/`. Move (not copy, if same filesystem) the contents of `<UPLOAD_LOCATION>` into `/var/lib/immich`:
Run: `sudo rsync -aHAX --info=progress2 <UPLOAD_LOCATION>/ /var/lib/immich/`
(Use `rsync` — safe if partially interrupted. Keep the source until Task 6 sign-off.)
- [ ] **Step 4: Fix ownership**
Run: `sudo chown -R immich:immich /var/lib/immich`
Expected: everything under `/var/lib/immich` owned by `immich`.
---
### Task 5: Start and verify (operator-run)
**Files:** none. Run on jupiter.
**Interfaces:**
- Consumes: migrated DB + media from Task 4.
- Produces: a running, verified native Immich.
- [ ] **Step 1: Start the server and watch logs**
Run: `sudo systemctl start immich-server && journalctl -u immich-server -f`
Expected: it connects to the DB, runs same-version startup checks (no destructive migration since 2.7.5==2.7.5), and reports listening on 2283. Leave the follow running through the next step.
- [ ] **Step 2: Start machine-learning**
Run: `sudo systemctl start immich-machine-learning`
Expected: active, no crash loop in `journalctl -u immich-machine-learning`.
- [ ] **Step 3: Functional spot-check in the web UI**
At `http://jupiter:2283`: log in with an existing account; confirm the timeline loads; open an **album**; open the **People/faces** view; open a **shared link**; open one photo so a **thumbnail and its full original both load** (this proves DB↔file paths align after the media move).
Expected: all present, images render.
- [ ] **Step 4: Confirm homepage dashboard tile**
Open the homepage dashboard; confirm the Immich tile appears under "Media" and links to `http://jupiter:2283`.
- [ ] **Step 5: Enable and verify hardware transcoding**
In Immich **Administration → Settings → Video Transcoding**, set hardware acceleration to **Quick Sync** (QSV) (or VAAPI). Trigger a transcode (upload/play a video that needs transcoding, or run the transcoding job). Then:
Run: `journalctl -u immich-server | grep -iE 'qsv|vaapi|hwaccel|transcode'`
Expected: log shows the hardware path in use, not a CPU-fallback error. Confirm `/dev/dri/renderD128` is accessible to the service (the `video`/`render` groups + `accelerationDevices` from Task 1 handle this).
---
### Task 6: Sign-off and cleanup (operator-run)
**Files:** none in repo. Merge the branch; then, only after a confidence window, remove docker.
**Interfaces:**
- Consumes: a verified running instance (Task 5).
- [ ] **Step 1: Merge the feature branch**
Open a PR from `feat/immich-nixos-module` into `main` and merge it (repo convention: PRs via the Gitea remote).
- [ ] **Step 2: Confidence window**
Use Immich normally for a few days. Keep the docker `<UPLOAD_LOCATION>` source copy and `~/immich-db.sql` untouched as the rollback path.
- [ ] **Step 3: Rollback (only if needed, before cleanup)**
If something is wrong: `sudo systemctl stop immich-server immich-machine-learning`, set `immich.enable = false` (or check out the pre-migration commit), `sudo nixos-rebuild switch --flake '.#jupiter'`, then `docker compose up -d` in the old stack. Original docker DB + upload folder are intact until Step 4.
- [ ] **Step 4: Cleanup (after sign-off)**
Remove the docker Immich stack (`docker compose down --rmi all --volumes` in the old dir if the DB volume is dedicated — verify first), delete the now-duplicated `<UPLOAD_LOCATION>` source, and remove `~/immich-db.sql`. Optionally disable the `docker` profile on jupiter if Immich was its only consumer (check other services first — jupiter's `docker.enable` may still be needed).
---
## Self-Review
**Spec coverage:**
- Native `services.immich` → Task 1. ✓
- Version target 2.7.5==stable, no override → Global Constraints + Task 2 Step 1. ✓
- Media at default `/var/lib/immich` → Task 1 + Task 4 Step 3. ✓
- DB migrate keep-everything → Tasks 2–4. ✓
- HW transcoding only → Task 1 (`accelerationDevices`, groups) + Task 5 Step 5. ✓
- LAN+VPN, port 2283, openFirewall, homepage tile → Task 1 + Task 5 Steps 3–4. ✓
- Rollback path → Task 6 Step 3. ✓
- Deferred (OpenVINO/NAS/proxy) → correctly absent. ✓
**Placeholder scan:** No TBD/TODO; every command is concrete. Placeholders like `<db-container>`, `<UPLOAD_LOCATION>`, `<POSTGRES_USER>` are runtime values the operator reads in Task 2 Step 1 — intentional, not gaps.
**Type consistency:** Option name `my.profiles.immich.enable` and path `/var/lib/immich` used consistently across all tasks. Media subfolder list matches between Task 4 Step 3 and the spec.
@@ -0,0 +1,384 @@
# Hermes Agent on mibook Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
>
> **Note on nature:** Task 1 is repo-only work verifiable with `nix build` — no runtime tests exist for declarative config, matching this repo's existing convention (see the Immich plan). Tasks 2–5 are an **operator-run runbook**, not agent-automatable: Task 2 needs an interactive `claude setup-token` OAuth login, an Anthropic billing console check, and a Telegram BotFather chat; Task 3 requires typing real secret material into the `$EDITOR` sops opens; Task 4 runs `nixos-rebuild` on mibook, the operator's live daily-driver desktop. **Never paste secret values (tokens, passwords) into an agent session or chat** — Task 3 has the operator type them directly into the editor sops opens. Present Tasks 2–5 for the operator to run and confirm; do not attempt to execute them from an agent session.
**Goal:** Run Hermes Agent as a hardened NixOS systemd service on mibook, reachable via Telegram and Email, authenticated to Anthropic via a Claude subscription (`claude setup-token`) rather than a pay-per-token API key.
**Architecture:** A standard `my.profiles.hermes-agent` module wraps the upstream `services.hermes-agent` module (from `NousResearch/hermes-agent`'s `nixosModules.default`), imported from inside the profile module itself so both machines can evaluate it — only mibook ever turns it on (see Task 1 Step 3). Credentials (Anthropic token, Telegram bot token, email creds, both `*_ALLOWED_USERS` access-control keys) live in one sops secret (`hermes-env`) consumed via `environmentFiles`. No network exposure — Telegram long-polling and Email IMAP/SMTP are both outbound-initiated from mibook.
**Tech Stack:** NixOS (flake-parts), sops-nix, upstream `hermes-agent` flake (Tier 2/best-effort Nix support), Anthropic Claude (subscription auth).
**Spec:** `docs/superpowers/specs/2026-09-08-hermes-agent-nixos-design.md`
## Global Constraints
- Machine: **mibook** only. Do not enable on jupiter.
- Module source: official `github:NousResearch/hermes-agent` (`nixosModules.default`). Not the community `0xrsydn/nix-hermes-agent` wrapper.
- Auth: `claude setup-token` output goes into `ANTHROPIC_TOKEN` in the `hermes-env` secret — **not** `ANTHROPIC_API_KEY`. Confirmed supported by the locked revision's source (Task 4 Step 2); no longer conditional.
- Billing dependency: the setup-token/OAuth path only draws from **Claude Max "extra usage credits"** — confirm this pool is funded before Task 2's token generation, or auth will fail even with a valid token.
- Module wiring: the upstream `hermes-agent.nixosModules.default` import lives **inside** `modules/environments/hermes-agent/default.nix` (via the `inputs` module arg), never in `machines/configuration.nix`'s per-machine module lists — wiring it there breaks jupiter's evaluation even with the profile disabled (see Task 1 Step 3's correction).
- Access control: `TELEGRAM_ALLOWED_USERS` and `EMAIL_ALLOWED_USERS` are required in the `hermes-env` secret, not optional — see Task 2 Steps 3–4.
- Secret persistence: credentials end up in plaintext at `/var/lib/hermes/.hermes/.env` (mode 0640), not sops-nix's `/run/secrets` tmpfs — rollback (Task 5 Step 3) accounts for this.
- Secret: one sops secret named `hermes-env`, a full env-file blob (matches `environmentFiles` — list of paths, not a single-value secret).
- No network exposure: do not touch `modules/services/webserver` or open any inbound port. Telegram + Email are both outbound-only.
- Platforms: Telegram + Email only, first pass. No Discord/Slack/etc.
- Rebuild command: `sudo nixos-rebuild switch --flake '.#mibook'` (use `nixos-rebuild test` first, per Task 4).
- Build-check command: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`.
- Format Nix with `nixfmt-rfc-style` before committing.
- Secret material never enters an agent session, chat, or the git history in plaintext — only via `sops secrets/mibook.yaml` and `$EDITOR`.
- Base branch: `feat/sops-nix` (already pushed; do not merge or rebase it as part of this work).
---
## File Structure
- **Modify** `flake.nix` — add the `hermes-agent` flake input.
- **Not modified** `machines/configuration.nix` — Task 1 originally wired the upstream module import here (mibook-only); this broke jupiter's evaluation and was corrected to import from inside the profile module instead. See Task 1 Step 3.
- **Create** `modules/environments/hermes-agent/default.nix` — the `my.profiles.hermes-agent` module (single responsibility: declare Hermes Agent).
- **Modify** `modules/environments/default.nix` — add `./hermes-agent` to the import list.
- **Modify** `machines/mibook/environments.nix` — set `hermes-agent.enable = true`.
- **Modify** `machines/mibook/secrets.nix` — replace the `placeholder` secret with `hermes-env`.
- **Modify (encrypted, operator-only)** `secrets/mibook.yaml` — real credential values, never touched by an agent.
No other files change.
---
### Task 1: Wire up the module (flake input, profile module, secret declaration)
**Files:**
- Modify: `flake.nix`
- Create: `modules/environments/hermes-agent/default.nix`
- Modify: `modules/environments/default.nix`
- Modify: `machines/mibook/environments.nix`
- Modify: `machines/mibook/secrets.nix`
- **Not modified:** `machines/configuration.nix` — see Step 3's correction.
**Interfaces:**
- Produces: NixOS option `my.profiles.hermes-agent.enable` (bool). When true, configures `services.hermes-agent` with `environmentFiles = [ config.sops.secrets."hermes-env".path ]` and grants `finn` the `hermes` group. Declares `sops.secrets.hermes-env` (owner/group `hermes`, path resolves to `/run/secrets/hermes-env` at runtime — but note the *service* re-copies it to `/var/lib/hermes/.hermes/.env`, per the spec's persistence note).
- Consumes: `hermes-agent.nixosModules.default` from the new flake input, imported directly inside the profile module via `self.inputs` (the `self` module arg, from `specialArgs` — see Step 3 and Step 5's note on why not the `inputs` module arg) — not through `machines/configuration.nix`; existing `sops.secrets.<name>` mechanism from `machines/core/sops.nix` (already applied to every machine, unaffected by this task).
- [ ] **Step 1: Add the flake input**
In `flake.nix`, inside `inputs = { ... }`, add after the `sops-nix` block:
```nix
hermes-agent = {
url = "github:NousResearch/hermes-agent";
inputs.nixpkgs.follows = "nixpkgs";
};
```
- [ ] **Step 2: Lock the new input**
Run: `nix flake lock`
Expected: `flake.lock` gains a `hermes-agent` entry (and its transitive inputs). No other entries change.
- [ ] **Step 3: Do NOT wire the module import through `machines/configuration.nix`**
**Correction (found during final whole-plan review, 2026-09-08):** an
earlier version of this step added `hermes-agent` to the
`inherit (self.inputs) ...` destructure and `hermes-agent.nixosModules.default`
to mibook's `modules` list in `machines/configuration.nix`. That broke
`nixosConfigurations.jupiter` evaluation — confirmed by direct `nix eval`.
`modules/environments/default.nix` (Step 6) is machine-agnostic, so jupiter
loads the profile module too; NixOS's module system errors on a definition
for an undeclared option **regardless of `lib.mkIf`'s condition**. Since
jupiter never imported `hermes-agent.nixosModules.default`, it never
declared `services.hermes-agent` as an option at all, so even the disabled
(`mkIf false`) config definition in Step 5 crashed evaluation.
Skip this step — do not touch `machines/configuration.nix` at all. The fix
lives in Step 5: the upstream module is imported from inside the profile
module itself, which works correctly on both machines (declares the option
everywhere, only ever turns it on where `my.profiles.hermes-agent.enable = true`).
- [ ] **Step 4: Read a reference module to match repo style**
Read `modules/environments/immich/default.nix` (same shape: `cfg`, `lib.mkIf`, single-purpose options block). Match its formatting.
- [ ] **Step 5: Create the profile module**
Create `modules/environments/hermes-agent/default.nix`:
```nix
# Hermes Agent — Telegram + Email messaging gateway (Claude subscription auth)
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.my.profiles.hermes-agent;
in
{
imports = [ self.inputs.hermes-agent.nixosModules.default ];
options.my.profiles.hermes-agent.enable =
lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)";
config = lib.mkIf cfg.enable {
services.hermes-agent = {
enable = true;
# Deliberate pin, matches the approved design — bump on purpose, not by drift.
settings.model.default = "anthropic/claude-sonnet-4-5";
environmentFiles = [ config.sops.secrets."hermes-env".path ];
addToSystemPackages = true;
};
# addToSystemPackages only puts `hermes` on $PATH; using it needs group
# access to /var/lib/hermes/.hermes (mode 2770 hermes:hermes). finn is
# this machine's one human user and already knows hermes-env's contents
# (typed them into sops directly), so this is not new exposure.
users.users.finn.extraGroups = [ "hermes" ];
};
}
```
**Use `self`, not `inputs`, in the `imports` line.** `inputs` is supplied
via `_module.args.inputs = self.inputs;`, set inside a config block — but
NixOS resolves a module's `imports` before `_module.args` assignments
merge, so anything referenced inside `imports` must come from
`specialArgs` (fixed up front) instead. `self` is threaded via
`specialArgs = { inherit self; };` in `machines/configuration.nix` and is
available at import-time; `inputs` is not. Using `inputs` here reproduces
`infinite recursion encountered` — confirmed by testing it directly, not
theoretical. `self.inputs.hermes-agent` is the same flake input either way.
- [ ] **Step 6: Register the module in the environments import list**
In `modules/environments/default.nix`, add `./hermes-agent` after `./immich`:
```nix
./immich
./hermes-agent
];
}
```
- [ ] **Step 7: Enable it on mibook**
In `machines/mibook/environments.nix`, inside the `my.profiles = { ... }` block, add:
```nix
hermes-agent.enable = true;
```
- [ ] **Step 8: Replace the placeholder secret declaration**
In `machines/mibook/secrets.nix`, replace the whole file body:
```nix
# Secrets decrypted on mibook. sopsFile defaults to secrets/mibook.yaml
# via machines/core/sops.nix.
{ ... }:
{
sops.secrets.hermes-env = {
owner = "hermes";
group = "hermes";
mode = "0400";
restartUnits = [ "hermes-agent.service" ]; # confirmed against the locked revision's generated unit
};
}
```
This removes the `placeholder` secret declared here — it was an explicit throwaway (comment: "Safe to delete once a real secret is in place"). Its entry in the encrypted `secrets/mibook.yaml` content itself is cleaned up by the operator in Task 3, not here.
- [ ] **Step 9: Format**
Run: `nixfmt-rfc-style modules/environments/hermes-agent/default.nix machines/mibook/secrets.nix machines/mibook/environments.nix flake.nix`
- [ ] **Step 10: Build-check (this is the "test", with a known-expected failure until Task 3)**
Run: `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'`
**Correction (found during Task 1 implementation, 2026-09-08):** this does **not** build cleanly yet, and that's expected — `sops.validateSopsFiles = true` (set repo-wide in `machines/core/sops.nix`) makes secret-key existence a **build-time** dependency of `config.system.build.toplevel` (via a `manifest.json.drv` step), not an activation-time check as originally assumed here. Since `secrets/mibook.yaml` still only has the `placeholder` key at this point in the plan (Task 3 adds `hermes-env`), the build correctly fails with:
```
> .../sops-install-secrets: manifest is not valid: secret hermes-env in .../mibook.yaml is not valid: the key 'hermes-env' cannot be found
```
Do **not** treat this as a wiring defect, and do not weaken `sops.validateSopsFiles` to work around it — that's a repo-wide safety invariant, not this task's to change. Instead, confirm the wiring is correct by two other means: `nix build ... --dry-run` (should complete with no eval errors and the full derivation list) and Step 11 below. A fully green (non-dry-run) `nix build` of the toplevel is only reachable after Task 3 populates the real secret — treat that as the actual end-to-end checkpoint, revisited in Task 4 Step 1.
If `--dry-run` itself fails on an unknown `services.hermes-agent.*` option, reconcile against the actual module: the error will name the bad option; cross-check against `website/docs/getting-started/nix-setup.md` in the `NousResearch/hermes-agent` repo at the locked revision.
- [ ] **Step 11: Build-check jupiter too — this is the regression gate**
Run: `nix eval '.#nixosConfigurations.jupiter.config.system.build.toplevel.drvPath'`
Expected: succeeds, printing a `.drv` path. **Any change under `modules/` is a two-machine change even when it's "for one host"** — `modules/environments/default.nix` (Step 6) is loaded by every machine, so a mistake here can break jupiter's eval even though jupiter never enables this profile. This is exactly the failure Step 3's correction fixes; this step is what catches it if it ever regresses.
- [ ] **Step 12: Confirm the option evaluates on mibook**
Run: `nix eval '.#nixosConfigurations.mibook.config.services.hermes-agent.enable'`
Expected: `true`
- [ ] **Step 13: Commit**
```bash
git add flake.nix flake.lock modules/environments/hermes-agent/default.nix modules/environments/default.nix machines/mibook/environments.nix machines/mibook/secrets.nix
git commit -m "feat(mibook): add Hermes Agent profile module"
```
**Post-review note, 2026-09-08:** Task 1 was originally committed as
`731678e` with the jupiter-breaking bug Step 3 describes and fixes — the
upstream module was wired through `machines/configuration.nix`'s
mibook-only module list instead of imported inside the profile module. The
final whole-plan review caught it (jupiter's `nixosConfigurations` stopped
evaluating, breaking the repo's sole CI job even though jupiter never
enables this profile). It was corrected in a follow-up commit on the same
branch — run `git log --oneline -- modules/environments/hermes-agent/ machines/configuration.nix`
if you need both commit hashes. The steps above already reflect the
corrected, final approach — executing this plan from scratch would never
hit the bug.
---
### Task 2: Gather credentials (operator-run, not agent-automatable)
**Files:** none. All values are held by the operator for Task 3 — do not write them into any file an agent can read, and do not paste them into a chat session.
**Interfaces:**
- Produces: an Anthropic setup-token, a Telegram bot token, and email (IMAP/SMTP) credentials — ready to type into `$EDITOR` in Task 3.
- [ ] **Step 1: Confirm the billing pool is funded**
In the Anthropic console, confirm the Claude Max plan has **extra usage credits purchased**. The `claude setup-token` auth path bills against this pool only — it does not draw from the base Max allowance, and Claude Pro cannot use this path at all. If this isn't funded, stop here and decide whether to fund it or fall back to a plain `ANTHROPIC_API_KEY` (see Global Constraints).
- [ ] **Step 2: Generate the setup-token**
On mibook, run: `claude setup-token`
This opens an interactive OAuth flow. On completion it prints a long-lived token. Copy it somewhere only you control (e.g. your password manager) — you'll type it into `sops` in Task 3, not into this session.
- [ ] **Step 3: Create the Telegram bot, and get your own user ID**
In Telegram, message `@BotFather`, run `/newbot`, follow the prompts. Copy the resulting bot token (format `123456:ABC-...`). Also get your own numeric Telegram user ID (e.g. via `@userinfobot`) — Task 3 needs it for `TELEGRAM_ALLOWED_USERS`. Telegram defaults to open DM pairing (any stranger who messages the bot gets a pairing code and can request approval); without this allowlist, that's the default posture, not a hardened one.
- [ ] **Step 4: Gather email credentials**
Decide which mailbox Hermes Agent should use, and get its IMAP/SMTP host, username, and password (or app-specific password if the provider requires one, e.g. Gmail). The exact env var names (confirmed against the locked `hermes-agent` revision, not guessed) are `EMAIL_ADDRESS`, `EMAIL_PASSWORD`, `EMAIL_IMAP_HOST`, `EMAIL_SMTP_HOST`, plus optional `EMAIL_IMAP_PORT` (default 993) / `EMAIL_SMTP_PORT` (default 587) if your provider differs — see `website/docs/user-guide/messaging/email.md` and `website/docs/reference/environment-variables.md` in the `NousResearch/hermes-agent` repo for more detail. Also decide `EMAIL_ALLOWED_USERS` (your own sending address) — without it, mail from unrecognized senders is **silently ignored** with no log line, which Task 4 Step 4 depends on knowing.
---
### Task 3: Populate the secret (operator-run, secret material)
**Files:**
- Modify (encrypted): `secrets/mibook.yaml`
**Interfaces:**
- Consumes: the three credentials gathered in Task 2, and the `hermes-env` declaration from Task 1.
- Produces: a decryptable `hermes-env` entry that Task 1's `environmentFiles` reference resolves at activation.
- [ ] **Step 1: Open the encrypted file**
Run: `nix develop -c sops secrets/mibook.yaml`
This decrypts and opens `$EDITOR` on the plaintext.
- [ ] **Step 2: Remove the placeholder key, add `hermes-env`**
Delete the `placeholder: ...` entry (Task 1 already removed its Nix declaration; this removes the matching content). Add:
```yaml
hermes-env: |
ANTHROPIC_TOKEN=<paste the claude setup-token output>
TELEGRAM_BOT_TOKEN=<paste the BotFather token>
TELEGRAM_ALLOWED_USERS=<your numeric Telegram user id, from Task 2 Step 3>
EMAIL_ADDRESS=<mailbox address>
EMAIL_PASSWORD=<password or app-password>
EMAIL_IMAP_HOST=<imap host>
EMAIL_SMTP_HOST=<smtp host>
EMAIL_ALLOWED_USERS=<your own sending address, from Task 2 Step 4>
```
(Exact YAML block-scalar syntax per whatever your `$EDITOR` shows — sops re-encrypts on save regardless of formatting, as long as it's valid YAML. Both `*_ALLOWED_USERS` keys are access control, not optional — see Task 2 Steps 3–4 for why.)
- [ ] **Step 3: Save and let sops re-encrypt**
Save and quit `$EDITOR`. Run: `git diff --stat secrets/mibook.yaml`
Expected: the file shows as changed (still fully encrypted — confirm by `git diff secrets/mibook.yaml | head` and seeing sops's `ENC[...]` ciphertext, never plaintext).
- [ ] **Step 4: Stage it**
Run: `git add secrets/mibook.yaml`
Required before the next `nix build` — untracked sops files are invisible to flake evaluation (repo rule, `docs/secrets.md`).
- [ ] **Step 5: Commit**
```bash
git commit -m "feat(mibook): populate Hermes Agent credentials"
```
---
### Task 4: Deploy and verify (operator-run, on mibook)
**Files:** none in repo. Run all commands on mibook.
**Interfaces:**
- Consumes: the built config from Task 1, the secret from Task 3.
- Produces: a running, verified `hermes-agent.service` reachable via Telegram and Email.
- [ ] **Step 1: Test-activate without persisting**
Run: `sudo nixos-rebuild test --flake '.#mibook'`
Expected: activates without reboot-persisting. If it fails at secret decryption ("hermes-env not found in sops file"), Task 3 wasn't saved/staged correctly — redo Task 3.
- [ ] **Step 2: Watch the service start**
`hermes-agent.service` is the confirmed unit name (verified against the locked revision's generated unit, not a guess). Run: `journalctl -u hermes-agent.service -f`
Expected: starts, reads the env file (lands at `/var/lib/hermes/.hermes/.env` — see spec Part 2), and does **not** crash-loop on auth. `ANTHROPIC_TOKEN` is confirmed supported by the module's credential resolver, so an auth failure here points at the token itself (expired, or the extra-credits pool from Task 2 Step 1 isn't actually funded) rather than the module rejecting the variable name.
- [ ] **Step 3: Verify Telegram connects**
Send your bot a message from Telegram. Expected: it responds (confirms both the bot token and the LLM auth work end-to-end).
- [ ] **Step 4: Verify Email connects**
Send a test email to the configured mailbox. Expected: Hermes Agent picks it up (check `journalctl -u hermes-agent.service` for an inbound-message log line) and can send a reply.
- [ ] **Step 5: Persist across reboot**
Run: `sudo nixos-rebuild switch --flake '.#mibook'`
Expected: same running state as Step 1–4, now persisted.
---
### Task 5: Sign-off (operator-run)
**Files:** none in repo beyond what Tasks 1 and 3 already committed.
**Interfaces:**
- Consumes: a verified running instance (Task 4).
- [ ] **Step 1: Open a PR**
`feat/sops-nix` is already pushed to `origin` but not merged; this work sits on top of it. Push this branch (or continue on `feat/sops-nix` directly, per your preference) and open a PR per the repo's usual Gitea workflow. Note in the PR description that it depends on `feat/sops-nix` landing first (or is stacked on it).
- [ ] **Step 2: Confidence window**
Use it for a few days — send yourself Telegram/Email messages periodically, including after mibook has slept and woken up, to see how the accepted availability gap actually behaves in practice.
- [ ] **Step 3: Rollback (if needed)**
`my.profiles.hermes-agent.enable = false` in `machines/mibook/environments.nix`, `sudo nixos-rebuild switch --flake '.#mibook'` stops the service — but the plaintext credential file survives on disk (see spec's Rollback section for why). Also run `sudo rm -rf /var/lib/hermes` and revoke the Telegram bot token via `@BotFather` → `/revoke`. No data migration is involved, so this is still cheap, just not a single command.
---
## Self-Review
**Spec coverage:**
- Host mibook, not jupiter → Global Constraints + File Structure. ✓
- NixOS native module, official upstream source → Task 1. ✓
- Home Manager rejected → not present anywhere (correctly absent, matches "deliberately deferred" reasoning — no task references it). ✓
- `claude setup-token` → `ANTHROPIC_TOKEN`, Max extra-credits caveat → Task 2 Steps 1–2, Task 4 Step 2, Global Constraints. ✓
- `hermes-env` single env-file secret → Task 1 Step 8, Task 3. ✓
- No sops prerequisite (mibook already registered) → confirmed by reusing existing `machines/mibook/secrets.nix` rather than any `.sops.yaml`/Phase-9-style bootstrapping. ✓
- No network exposure → Global Constraints; no task touches `modules/services/webserver`. ✓
- Telegram + Email only → Task 2 Steps 3–4, Task 4 Steps 3–4. ✓
- Rollback path → Task 5 Step 3. ✓
- Deferred (other platforms, availability-gap workarounds, public exposure) → correctly absent. ✓
**Placeholder scan:** No bare TBD/TODO. The bracketed values (`<paste the claude setup-token output>`, etc.) are operator-supplied secret material by design — Task 2 explicitly produces them, Task 3 explicitly consumes them; they're never meant to be concrete in this document since they're credentials, not code.
**Type consistency:** `my.profiles.hermes-agent.enable`, `services.hermes-agent.*`, and the `hermes-env` secret name are used identically across Tasks 1, 3, and 4. The `hermes-agent.service` unit name and `ANTHROPIC_TOKEN` var are confirmed (not guessed) consistently in both Task 1's declaration and Task 4's verification.
**Post-final-review addendum (2026-09-08):** the final whole-plan review found the module-wiring bug described in Task 1 Step 3, plus three access/persistence gaps now folded into Global Constraints, Task 2 Steps 3–4, Task 3's template, and Task 5 Step 3's rollback (TELEGRAM_ALLOWED_USERS/EMAIL_ALLOWED_USERS required; secrets persist to `/var/lib/hermes/.hermes/.env`, not `/run/secrets`; `hermes` CLI needs `finn` in the `hermes` group to be usable). All four are corrected in place above, not left as separate addenda, so the plan reads as a single coherent document rather than a stack of patches.
@@ -0,0 +1,241 @@
# ZBT-2 as a Thread Border Router for Home Assistant on `jupiter`
**Date:** 2026-05-10
**Branch:** `feature/ha-zbt-2-thread`
**Status:** Design — pending implementation plan
## Context
Home Assistant on `jupiter` already runs natively (`services.home-assistant`) with the Matter integration and `services.matter-server` enabled, but has no Zigbee or Thread radio. The user has acquired a **Home Assistant Connect ZBT-2** (Nabu Casa's Silicon Labs EFR32MG24‑based USB Zigbee/Thread radio).
The user wants the dongle running as an **OpenThread Border Router (OTBR)** — Thread only, not Zigbee — so Matter-over-Thread devices can be onboarded through the existing HA Matter integration.
A previous iteration of this work shipped `zha` enablement on the same branch (commit `e8d09f4`). That commit will be reverted as part of implementation; this design supersedes it.
## Goals
- Bring up `otbr-agent` on jupiter against the ZBT-2.
- Have Home Assistant auto-discover the OTBR via mDNS and use its REST API to manage the Thread network.
- Have `services.matter-server` (already enabled) consume Thread credentials from HA so Matter-over-Thread devices commission through the ZBT-2.
- One-time, manual firmware flash from Zigbee NCP to OpenThread RCP via `universal-silabs-flasher` (option B from brainstorming — no HA-driven update flow).
## Non-goals
- **Multipan / multiprotocol** (Zigbee + Thread on one radio). Out of scope; the dongle will be Thread-only.
- **Falling back to ZHA** if Thread misbehaves. Thread-only by choice; if it fails the response is to debug, not to dual-stack.
- **HA-UI-driven firmware updates.** The HAOS "Silicon Labs Multiprotocol" add-on workflow doesn't translate to native NixOS without faking a supervisor; the user explicitly accepted CLI-only flashing.
- **Thread network credential backups.** HA owns the dataset; standard HA backup hygiene (separate concern) covers it.
## Architecture
```
┌────────────────────────── jupiter (NixOS) ──────────────────────────┐
│ │
ZBT-2 USB ──►│ /dev/serial/by-id/usb-Nabu_Casa_..._ZBT-2_<serial>-... │
│ │ │
│ │ spinel+hdlc+uart, 115200 baud │
│ ▼ │
│ ┌───────────────┐ REST :8081 (loopback) ┌──────────────────┐ │
│ │ otbr-agent │ ◄─────────────────────►│ home-assistant │ │
│ │ (systemd) │ │ + matter-server │ │
│ │ wpan0 ───────┼── advertises via ─┐ │ extraComponents:│ │
│ └───────────────┘ avahi (_meshcop) │ │ matter, │ │
│ ▼ │ mobile_app, │ │
│ enp3s0 (LAN — backbone) │ otbr, thread │ │
│ └──────────────────┘ │
└────────────────────────────────────┬──────────────────────────────────┘
│
home LAN ◄─┘
(Matter-over-Thread devices join here)
```
### Components
1. **The radio.** ZBT-2, USB-attached, running OpenThread RCP firmware after a one-time flash.
2. **`otbr-agent`** (systemd). Managed by the unstable `services.openthread-border-router` NixOS module imported via `inputs.nixpkgs-unstable`. Owns `wpan0`, talks Spinel to the dongle, exposes the OTBR REST API on `127.0.0.1:8081`, advertises `_meshcop._udp` over `enp3s0` via avahi.
3. **Home Assistant** (already running). Gains the `otbr` and `thread` extra components. Discovers OTBR via mDNS, drives the REST API, supplies Thread operational datasets to `matter-server` during Matter commissioning.
### Data flows
- **OTBR ↔ ZBT-2:** Spinel-over-HDLC over UART. Built automatically by the module from `radio.device` as `spinel+hdlc+uart://<device>?uart-baudrate=115200`.
- **HA ↔ OTBR:** mDNS discovery (`_meshcop._udp`) → REST calls to `127.0.0.1:8081` for network management.
- **Matter commissioning:** HA scans QR → `matter-server` does BLE commissioning → asks HA for Thread dataset → HA fetches from OTBR → ships to device → device joins Thread mesh through the ZBT-2.
HA never opens the serial port directly; `matter-server` never talks to OTBR directly. HA brokers between them — that's why all four extra components are needed.
## NixOS-side changes
All changes live in **`modules/environments/home-assistant/default.nix`**. No host-level changes in `machines/jupiter/` (the existing profile activation handles that), no flake-level changes (the existing `_module.args.self = self;` wiring is sufficient).
### Edited module sketch
```nix
{ config, lib, pkgs, self, ... }:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
{
imports = [
# OTBR module isn't in 25.11 yet; use unstable's directly. Package
# comes from the existing `unstable` overlay.
"${self.inputs.nixpkgs-unstable}/nixos/modules/services/home-automation/openthread-border-router.nix"
];
options.my.profiles.home-assistant.enable = lib.mkEnableOption "Home Automation";
config = lib.mkIf cfg.enable {
services.matter-server.enable = true;
services.home-assistant = {
enable = true;
openFirewall = true;
extraComponents = [
"matter"
"mobile_app"
"otbr"
"thread"
];
};
services.home-assistant.config = {
name = "Home - Rechberg";
unit_system = "metric";
mobile_app = { };
};
services.openthread-border-router = {
enable = true;
package = pkgs.unstable.openthread-border-router;
openFirewall = true;
backboneInterfaces = [ "enp3s0" ]; # verify with `ip link` post-deploy
radio.device = "/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-...";
# web.enable left default (off) — HA UI is the management surface
};
my.homepage.services = [
{
group = "Services";
name = "Home Assistant";
description = "Home automation";
href = "http://${hostName}:8123";
icon = "si-homeassistant";
}
];
};
}
```
### Reverts of the prior ZHA commit
Drop both lines from commit `e8d09f4`:
- `"zha"` from `extraComponents` (replaced by `"otbr"` + `"thread"`).
- `users.users.hass.extraGroups = [ "dialout" ];` — `otbr-agent` runs as root and owns the device directly; HA never opens the serial port itself.
Done by `git revert e8d09f4` at the start of implementation, before applying the new diff.
### Decisions captured
- **No `universal-silabs-flasher` in `environment.systemPackages`.** Flashing is a once-or-twice-a-year operation; `nix shell nixpkgs#python313Packages.universal-silabs-flasher` is sufficient when needed and avoids a perma-dep on a tool that's idle most of the time.
- **No firmware pinning in the flake.** Consistent with option B (CLI-only manual flashing). The user fetches the `.gbl` from <https://github.com/NabuCasa/silabs-firmware-builder/releases> at update time.
- **`backboneInterfaces = [ "enp3s0" ]`** as a starting value (per `machines/jupiter/hardware-configuration.nix:64`). To be verified against `ip link` after first deploy; correctable in a follow-up commit if the actual primary interface differs.
## Operator workflow
All commands the user runs themselves; nothing is SSH'd from the dev session.
### Step 0 — branch hygiene (dev Mac)
```
git switch feature/ha-zbt-2-thread # already renamed
git revert --no-edit e8d09f4 # drops ZHA + dialout commit
```
### Step 1 — apply the module changes (dev Mac)
Edit `modules/environments/home-assistant/default.nix` per the sketch above. Leave `<serial>` as a placeholder; fill after Step 3.
### Step 2 — eval-only sanity check (dev Mac)
```
nix flake check
```
or, equivalently,
```
nixos-rebuild dry-build --flake .#jupiter
```
Catches: bad import path, option typos, version skew between unstable and stable.
### Step 3 — plug ZBT-2 into jupiter (still on stock Zigbee firmware)
On jupiter:
```
ls -l /dev/serial/by-id/
```
Then on dev Mac: copy the full `usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-...` path into `radio.device`, commit on the feature branch.
### Step 4 — flash OpenThread RCP firmware (one-time, on jupiter)
```
nix shell nixpkgs#python313Packages.universal-silabs-flasher -c \
universal-silabs-flasher \
--device /dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_<serial>-... \
flash --firmware ~/ot-rcp-zbt-2-<version>.gbl
```
Firmware download: latest ZBT-2 OpenThread RCP `.gbl` from <https://github.com/NabuCasa/silabs-firmware-builder/releases>.
OTBR isn't running yet at this point, so there's no contention on the device.
### Step 5 — rebuild (on jupiter)
```
sudo nixos-rebuild switch --flake .#jupiter
```
Brings up `otbr-agent.service`, opens TCP/8081, loads `otbr` + `thread` integrations in HA.
### Step 6 — confirm HA discovered it
- `http://jupiter:8123` → Settings → Devices & Services → "Open Thread Border Router" appears as auto-discovered within ~30 s.
- Click "Configure", form a new Thread network (or import an existing dataset).
- "Matter" integration page now shows Thread credentials available.
### Step 7 — Matter-over-Thread smoke test
Pair one Matter-over-Thread device end-to-end via the HA Companion app. Pairing should complete in 30–90 s. If it does, merge `feature/ha-zbt-2-thread` into `master`.
### Future updates
Identical to Step 4: stop `otbr-agent.service`, run the flasher with a new `.gbl`, start the service.
## Failure modes
| Symptom | Likely cause | Mitigation |
|---|---|---|
| `otbr-agent.service` fails: "Failed to open device" | Dongle unplugged or `radio.device` path stale (e.g. after replacement) | Module sets `Restart = "on-failure"`; check `systemctl status otbr-agent`, re-check `/dev/serial/by-id/`, update path. |
| OTBR up but HA never discovers it | mDNS not propagating on `enp3s0` (most often: `backboneInterfaces` wrong) | `avahi-browse -r _meshcop._udp` should show one entry. If not: `ip link`, fix `backboneInterfaces`, rebuild. |
| HA shows OTBR but Matter pairing times out | Thread mesh prefix not routed to LAN, or matter-server can't reach the device's IPv6 ULA | `nft list ruleset` should show OTBR's forwarding rules; `ip -6 route` should include the Thread mesh prefix. |
| Dongle stuck after a half-completed flash | Flasher interrupted mid-write | Re-run the flash; bootloader stays addressable even if RCP firmware is corrupt. The tool detects bootloader-mode automatically. |
| `nixos-rebuild` fails: "option `services.openthread-border-router` does not exist" | Unstable module import path wrong / not in scope | Caught by Step 2 (eval-only). Fix before deploy. |
## Verification
### Eval-only (dev Mac, before deploy)
```
nix flake check
nix eval --json .#nixosConfigurations.jupiter.config.services.openthread-border-router.radio.url
nix eval --json .#nixosConfigurations.jupiter.config.services.home-assistant.extraComponents
```
Expected: flake check passes; `radio.url` is a `spinel+hdlc+uart://...` string built from the by-id path; `extraComponents` includes `"otbr"` and `"thread"`.
### Service-level (jupiter, after rebuild)
```
systemctl status otbr-agent.service
journalctl -u otbr-agent.service -n 50 --no-pager
ip link show wpan0
avahi-browse -r -t _meshcop._udp
curl -s http://127.0.0.1:8081/node/state
```
Expected: service active; `wpan0` exists (DOWN until HA forms a network — correct); one `_meshcop._udp` entry; REST returns a JSON state string.
### Functional (HA UI)
- "Open Thread Border Router" appears under auto-discovered integrations.
- Forming a Thread network from the integration UI succeeds.
- Pairing one Matter-over-Thread device end-to-end succeeds.
## Open questions / risks
- **Unstable module ABI.** The `services.openthread-border-router` module is in `nixos-unstable` and may change shape before landing in 26.05. If options rename, the eval-only step catches it before deploy. Acceptable risk; we can pin the unstable input revision if churn becomes annoying.
- **Backbone interface name.** `enp3s0` is a best guess from `hardware-configuration.nix:64`'s commented-out line. Definitive answer comes from `ip link` on the actual host. Trivial to correct if wrong.
- **First-flash chicken-and-egg.** Deferred to `nix shell` rather than baked into the system, because the dongle must be flashed *before* `otbr-agent` claims it. This is documented in Step 4.
@@ -0,0 +1,111 @@
# Smartphone push notifications from Home Assistant
## Goal
Send push notifications from jupiter's Home Assistant to the user's
smartphones over the home Tailscale (Headscale) tailnet `solar.internal`.
## Architecture
- HA's `mobile_app` integration is enabled (already in `extraComponents`
and present as `mobile_app = {}` in config).
- Each smartphone runs the **HA Companion app**, signs in to HA, and
auto-registers as a `notify.mobile_app_<device_slug>` service.
- Reach: phones connect to HA via Tailscale, so HA's `external_url` is
set to the Headscale FQDN `http://jupiter.solar.internal:8123`. The
`internal_url` is `http://jupiter:8123` for LAN-attached devices.
- No public exposure, no reverse proxy, no TLS termination in scope.
## Phase A — done in this branch
NixOS module change in `modules/environments/home-assistant/default.nix`:
```nix
homeassistant = {
name = "Home - Rechberg";
unit_system = "metric";
internal_url = "http://${hostName}:8123";
external_url = "http://jupiter.solar.internal:8123";
};
```
After deploy, perform the user-side registration:
1. Install the Companion app:
- iOS: search "Home Assistant" in the App Store.
- Android: search "Home Assistant" in Google Play.
2. Ensure Tailscale is running and connected on the phone.
3. Open the Companion app. When asked to connect, enter
`http://jupiter.solar.internal:8123` and sign in with the HA account.
4. Approve the registration prompt in HA.
5. In HA, go to **Settings → Devices & Services → Mobile App** and
confirm the phone appears as a device.
6. In HA, go to **Developer Tools → Services**, type `notify.mobile_app_`
and note the exact service slug for each phone (e.g.
`notify.mobile_app_iphone_finn`). These slugs are needed for Phase B.
### Verifying Phase A end-to-end
Build-time:
```
nix eval '.#nixosConfigurations.jupiter.config.services.home-assistant.config.homeassistant' \
--extra-experimental-features 'nix-command flakes'
```
Expect the rendered attrset to contain both `external_url` and
`internal_url`.
Deploy on jupiter:
```
sudo nixos-rebuild switch --flake '.#jupiter'
systemctl status home-assistant
journalctl -u home-assistant -n 50 --no-pager
```
Functional check after Companion sign-in:
- HA UI → **Developer Tools → Services** → choose
`notify.mobile_app_<your_device>` → service data
`{ "message": "Phase A test" }` → **Call Service** → push arrives on
the phone.
## Phase B — follow-up commit (after registration)
Once device slugs are known, a separate commit adds:
1. A `notify` group fanning out to every registered phone:
```nix
notify = [
{
name = "all_phones";
platform = "group";
services = [
{ service = "mobile_app_<slug_1>"; }
{ service = "mobile_app_<slug_2>"; }
];
}
];
```
2. A smoke-test mechanism. Approach to be decided in Phase B based on
whether future Nix-managed automations are expected:
- Pragmatic: document a one-time UI call to `notify.all_phones` from
Developer Tools (no automation in YAML).
- Compromise: switch `automation = "!include automations.yaml"` to
`!include_dir_merge_list automations/` so a Nix-managed
`00-smoke-test.yaml` can coexist with UI-editable automations.
### Verifying Phase B end-to-end
- Restart HA, watch `journalctl -u home-assistant` for YAML schema errors.
- Call `notify.all_phones` from Developer Tools — every registered phone
receives the push.
## Open items
- After Companion registration, collect the `mobile_app_<slug>` service
names from HA and update this spec + open Phase B PR.
@@ -0,0 +1,61 @@
# MiBook Claude Code Execution Machine
**Date:** 2026-05-25
**Branch:** feature/ha-zbt-2-thread
**Status:** Approved
## Goal
Configure the MiBook as an interactive Claude Code execution machine by installing the Claude Code CLI and its companion tooling via a new, independently-toggleable NixOS profile module.
## Scope
Phase 1 (this spec): interactive CLI session — user logs in or SSHs in and runs `claude` directly.
Phase 2 (future): headless systemd user service exposing Claude Code over an HTTP API or Unix socket, turning the MiBook into a remotely-triggerable execution node without an interactive session.
## Module Structure
```
modules/environments/claude-code/default.nix ← new module
modules/environments/default.nix ← add import
machines/mibook/environments.nix ← my.profiles.claude-code.enable = true
```
Follows the exact same pattern as every other environment module in this repo.
## Option Declaration
```
my.profiles.claude-code.enable (mkEnableOption)
```
No sub-options for Phase 1. Phase 2 would add `my.profiles.claude-code.service.enable`.
## Packages (users.users.finn.packages)
| Package | Source | Purpose |
|---|---|---|
| `claude-code` | `pkgs.unstable` | Claude Code CLI |
| `ripgrep` | `pkgs` | Primary file search tool used by Claude Code |
| `fd` | `pkgs` | Fast `find` replacement used by Claude Code |
| `gh` | `pkgs` | GitHub CLI for PR/issue operations |
| `jq` | `pkgs` | JSON processing in shell pipelines |
`git`, `nodejs`, and `docker` are already provided by existing modules and are not duplicated.
**Contingency:** If `pkgs.unstable.claude-code` does not exist at eval time, the fallback is a custom derivation in `pkgs/claude-code/default.nix` added to the local overlay — the same mechanism used for other absent packages.
## No Secrets Management
The Anthropic API key is handled manually by the user (browser login / `claude` interactive setup). No sops-nix or shell-profile injection needed.
## Future: Headless Service
A commented block in the module documents the upgrade path: a systemd user service that accepts work via an HTTP API or Unix socket, triggerable over SSH or a local network endpoint, without requiring an interactive session.
## Files Changed
1. `modules/environments/claude-code/default.nix` — new profile module
2. `modules/environments/default.nix` — add `./claude-code` import
3. `machines/mibook/environments.nix` — set `my.profiles.claude-code.enable = true`
@@ -0,0 +1,145 @@
# Intel Quick Sync hardware transcoding for Jellyfin on jupiter
## Problem
Jellyfin streams stutter on jupiter whenever a client needs a transcode
(unsupported codec/container, bitrate cap, or a client that can't
direct-play). Transcoding currently runs entirely on CPU.
jupiter's Intel iGPU is already usable at the OS level:
- `hardware.graphics.enable = true` with `intel-media-driver` (the `iHD`
VAAPI driver) is configured in
`machines/jupiter/hardware-configuration.nix:20-27`.
- The commented-out `i915.force_probe = "9a49"` kernel param there
corresponds to a Quick-Sync-capable Intel UHD iGPU, confirming the
hardware supports it.
But `modules/environments/jellyfin/default.nix` never grants the
`jellyfin` systemd service access to `/dev/dri`, so Jellyfin has no path
to the GPU and silently falls back to software transcoding.
## Goal
Give the Jellyfin service access to the iGPU's VAAPI render node, so
Quick Sync can be enabled in Jellyfin's own dashboard and transcodes are
offloaded from the CPU.
## Non-goals
- Remote/external access or reverse-proxy tuning.
- General CPU/RAM headroom review of jupiter.
- A toggle option (`my.profiles.jellyfin.hardwareAcceleration.enable`) —
jupiter only has the one iGPU, so this is hardcoded on rather than
made configurable.
## Design
### NixOS change (declarative)
In `modules/environments/jellyfin/default.nix`, inside the existing
`config = lib.mkIf cfg.enable { ... }` block, grant the systemd service
supplementary access to the `video` and `render` groups (the groups that
own `/dev/dri/card*` and `/dev/dri/renderD*`):
```nix
systemd.services.jellyfin.serviceConfig.SupplementaryGroups = [
"video"
"render"
];
```
This is additive to the existing `systemd.services.jellyfin.after = [
"network-online.target" ];` block already in the file — both apply to
the same service.
Also add `libva-utils` to `environment.systemPackages` (or scoped to
this module) so `vainfo` is available on jupiter to verify the driver
loads correctly.
### Manual step (not declarative)
Jellyfin stores its transcoding/hardware-acceleration choice in its own
internal `encoding.xml`, which the NixOS module does not expose as an
option. After deploying the Nix change, one-time manual configuration in
the Jellyfin dashboard is required:
1. **Dashboard → Playback**.
2. Hardware acceleration: **Intel QuickSync (QSV)**.
3. VA-API device: `/dev/dri/renderD128`.
4. Enable hardware decoding for the codecs your library actually uses
(H264 at minimum; HEVC/VP9 depending on iGPU generation).
5. If any HDR content exists in the library, enable tone-mapping — this
is one of the more CPU-expensive operations Quick Sync can offload.
## Verification
Build-time (from the Mac, no SSH needed):
```
nix eval '.#nixosConfigurations.jupiter.config.systemd.services.jellyfin.serviceConfig.SupplementaryGroups' \
--extra-experimental-features 'nix-command flakes'
```
Expect `[ "video" "render" ]`.
On jupiter after `sudo nixos-rebuild switch --flake '.#jupiter'`:
```
systemctl status jellyfin
journalctl -u jellyfin -n 50 --no-pager
vainfo
```
`vainfo` should list the `iHD` driver and print supported VAEntrypoints
(VLD decode / encode profiles for H264/HEVC).
Functional check: play a file on a client that forces transcoding (or
force it manually via Jellyfin's playback quality setting), then in
Jellyfin's dashboard **Activity/Now Playing** panel confirm the
transcode reason and check that CPU usage on jupiter (`htop`) stays low
during playback rather than pegging a core — Quick Sync offload should
show up as low CPU, some GPU (`intel_gpu_top`) activity instead.
## Open items
- Exact supported codec list depends on the iGPU generation (device ID
`9a49`) — confirm via `vainfo` output once run, and enable only the
hardware decode paths it actually reports.
## Post-deploy fix: two additional runtime packages required
After the initial deploy (Task 1's `SupplementaryGroups` grant) and
enabling QSV in the dashboard, HEVC HDR playback hung indefinitely
(Direct Play worked for some titles; titles that needed a real
transcode+tonemap never produced output). Root-caused via
`journalctl -u jellyfin` and the per-session ffmpeg transcode log
(`find / -xdev -iname '*ffmpeg-transcode*'`) — two separate runtimes
were missing beyond `intel-media-driver` (which only provides VAAPI):
1. **QSV session creation failed:** `Error creating a MFX session: -9`
/ `Error initializing an MFX session: -3` on
`-init_hw_device qsv=qs@va`. VAAPI and QSV are separate runtimes on
Linux — QSV needs the oneVPL/MFX GPU implementation. Fix: added
`pkgs.vpl-gpu-rt` ("oneAPI Video Processing Library Intel GPU
implementation"; note `onevpl-intel-gpu` is the old, renamed
attribute) to `hardware.graphics.extraPackages` in
`machines/jupiter/hardware-configuration.nix`.
2. **OpenCL device creation failed:** `Failed to get number of OpenCL
platforms: -1001` (`CL_PLATFORM_NOT_FOUND_KHR`) on
`-init_hw_device opencl=ocl@va`. The `tonemap_opencl` filter jellyfin
uses for HDR→SDR tone-mapping needs a working OpenCL ICD, which
nothing installed so far provides. Fix: added
`pkgs.intel-compute-runtime` ("Intel Graphics Compute Runtime oneAPI
Level Zero and OpenCL, supporting 12th Gen and newer" — matches
jupiter's Tiger Lake/Xe iGPU) to the same `extraPackages` list.
Confirmed working end-to-end: HEVC HDR transcode with QSV encode +
OpenCL tone-map runs at `speed=2.68x` realtime on jupiter's iGPU, and
plays smoothly on Apple TV (JellyTV app).
Both packages live in `machines/jupiter/hardware-configuration.nix`
(`hardware.graphics.extraPackages`), alongside `intel-media-driver`,
rather than in the jellyfin module itself — they're iGPU runtime
capabilities, not something specific to the jellyfin service.
@@ -0,0 +1,121 @@
# Immich: Docker → NixOS module migration
**Date:** 2026-08-05
**Machine:** jupiter (home server, Intel iGPU)
**Status:** Design approved, pending implementation plan
## Goal
Replace the existing docker-compose Immich deployment on jupiter with the
native `services.immich` NixOS module, wrapped in the repo's standard
`my.profiles.*` pattern. Preserve all existing data (albums, faces, shared
links, metadata) and photo/video library.
## Decisions
| Topic | Decision |
|-------|----------|
| Approach | Native `services.immich` (nixpkgs), not `oci-containers` |
| Version target | **Resolved: docker runs 2.7.5 == stable nixpkgs 2.7.5.** Use the stable module as-is; no `package` override. Same-version restore, no forward schema migration |
| Media location | Default local path `/var/lib/immich`. NAS deferred to a future read-only external library |
| Database | Migrate via dump/restore — keep everything |
| HW acceleration | Video transcoding only (VAAPI/QSV via existing Intel graphics stack). ML on CPU |
| Access | LAN + VPN only: open port 2283, register on homepage dashboard. No reverse proxy/TLS |
### Deliberately deferred (YAGNI)
- OpenVINO ML acceleration
- NAS-backed external library
- Reverse proxy / TLS / public hostname
## Part 1 — The module
New file `modules/environments/immich/default.nix` following the profile
pattern; add `./environments/immich` to `modules/environments/default.nix`;
enable `my.profiles.immich.enable = true` in
`machines/jupiter/environments.nix`.
```nix
{ config, lib, pkgs, ... }:
let
cfg = config.my.profiles.immich;
hostName = config.networking.hostName;
port = 2283;
in {
options.my.profiles.immich.enable = lib.mkEnableOption "Immich photo server";
config = lib.mkIf cfg.enable {
services.immich = {
enable = true;
# package = pkgs.unstable.immich; # only if docker :release is > 2.7.5
host = "0.0.0.0";
inherit port;
openFirewall = true;
mediaLocation = "/var/lib/immich";
machine-learning.enable = true;
accelerationDevices = [ "/dev/dri/renderD128" ];
settings.server.externalDomain = "http://${hostName}:${toString port}";
};
# native module does not add GPU groups; needed for VAAPI/QSV transcoding
users.users.immich.extraGroups = [ "video" "render" ];
my.homepage.services = [{
group = "Media";
name = "Immich";
description = "Photo & video server";
href = "http://${hostName}:${toString port}";
icon = "immich.png";
}];
};
}
```
**Provided for free by the native module:** local PostgreSQL with the required
vector extension over a **unix socket + peer auth** (so no DB password / sops
secret needed), Redis, `immich-server` and `immich-machine-learning` systemd
units, the `immich` system user, and `mediaLocation` created via tmpfiles.
**Transcoding is two parts:** (a) NixOS exposes the GPU device + `video`/`render`
groups (above); (b) the hwaccel backend (QSV/VAAPI) is chosen in Immich's
**admin → video transcoding** settings after cutover — a UI toggle, not Nix.
## Part 2 — Migration runbook (on jupiter)
### Pre-flight (hard blocker)
1. Get running docker Immich version (`docker exec <server> immich --version` or web UI footer).
2. **Resolved 2026-08-05: running version is 2.7.5, equal to stable nixpkgs.**
Use the stable module as-is (no `package` override). Kept for reference:
- running ≤ 2.7.5 → stable module as-is ← **this case**
- 2.7.6–3.0.3 → set `package = pkgs.unstable.immich`
- `> 3.0.3` → bump nixpkgs first; **stop and re-plan**
3. Record docker `UPLOAD_LOCATION` and DB container name/credentials.
### Backup (before touching anything)
4. `docker compose down` (DB may stay up for the dump).
5. Dump DB: `docker exec -t <db> pg_dumpall --clean --if-exists --username=postgres > immich-db.sql`
6. Verify upload folder intact; note size (no copy yet).
### Cutover
7. Add the module to jupiter's `environments.nix` (leave `database.createDB` default).
8. `sudo nixos-rebuild switch --flake '.#jupiter'` → creates user, empty DB + role, `mediaLocation`. Then `systemctl stop immich-server immich-machine-learning`.
9. Restore the DB into the NixOS Postgres (drop the freshly-created empty `immich` DB, load `immich-db.sql`) per Immich's restore docs.
10. Move media into `/var/lib/immich` (subfolders `library/`, `upload/`, `thumbs/`, `encoded-video/`, `profile/`); `chown -R immich:immich /var/lib/immich`.
11. `systemctl start immich-server`; it runs schema migrations forward. Watch `journalctl -u immich-server -f`.
### Verify
12. UI at `http://jupiter:2283` loads; log in; spot-check albums, faces, a shared link, and that thumbnails/originals actually load.
13. Homepage tile works.
14. Enable QSV/VAAPI in admin settings; transcode one video; confirm `journalctl` shows the hw path, not a CPU fallback error.
### Rollback
Before deleting any docker data: `systemctl stop immich-*`, disable the profile,
`nixos-rebuild switch`, `docker compose up -d`. Original docker DB + upload
folder remain untouched until explicitly removed after a few days of confidence.
## Known risk — RESOLVED
The main risk was step 9 crossing the **pgvecto.rs → VectorChord** vector-extension
boundary. With source and target both at **2.7.5**, both use VectorChord — no
boundary crossing and no forward schema migration. The restore is a same-version
dump/load. Residual risk is limited to routine dump/restore mechanics
(roles, extension availability in the NixOS Postgres, ownership on restore).
@@ -0,0 +1,205 @@
# Hermes Agent on mibook
**Date:** 2026-09-08
**Machine:** mibook (laptop, KDE desktop, current daily driver)
**Status:** Design approved, pending implementation plan
**Base branch:** `feat/sops-nix` (pushed to `origin`, not yet merged to `main`)
## Goal
Deploy [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent)
as a NixOS-native systemd service on mibook, reachable via Telegram and Email,
using an Anthropic Claude subscription (`claude setup-token`) rather than a
pay-per-token API key. Design + implementation plan only — no execution in
this round; a later session applies the plan.
## Decisions
| Topic | Decision |
|-------|----------|
| Host | mibook, not jupiter. Availability gap (laptop sleeps/lid-closes) is accepted, not engineered around |
| Use case | Messaging gateway (Telegram + Email), not CLI-only |
| Deployment mode | NixOS native module (`services.hermes-agent.*` from upstream `nixosModules.default`), wrapped in `my.profiles.hermes-agent`. Home Manager was considered and rejected — not wired into this flake anywhere; adding it for one service is a new framework dependency, not proportionate |
| Module source | Official `github:NousResearch/hermes-agent`, not the community `0xrsydn/nix-hermes-agent` wrapper |
| Nix support tier | Docs explicitly call Nix/NixOS "Tier 2, best-effort" — Docker/FHS is upstream's primary supported path. Accepted tradeoff, not a blocker |
| LLM provider auth | Anthropic via `claude setup-token` (long-lived, headless-friendly token) → `ANTHROPIC_TOKEN`, not `ANTHROPIC_API_KEY` |
| Billing caveat | Setup-token/OAuth path only draws from a **Claude Max "extra usage credits"** pool, never the base Max allowance. Claude Pro cannot use this path at all. User intends to use `claude setup-token`; not independently re-verified that extra credits are purchased — confirm in Anthropic console before/during implementation |
| Secrets | One sops secret, `hermes-env`, containing a full env-file blob (matches the module's `environmentFiles` list-of-paths interface — not a single-value secret) |
| sops prerequisite | **None.** mibook is already a registered sops recipient (`host_mibook` in `.sops.yaml`, `secrets/mibook.yaml` exists, proven via a live `nixos-rebuild switch` in the sops-nix work). No "Phase 9"-style bootstrapping needed — that deferred work is about *jupiter*, irrelevant now that mibook is the target |
| Network exposure | None. Telegram (long-polling) and Email (IMAP/SMTP) are both outbound-initiated from mibook. No Caddy/webserver module changes, no public endpoint |
| Messaging platforms | Telegram + Email only, first pass. Hermes Agent supports 27+ platforms total (Discord, Slack, WhatsApp, Matrix, etc.) — others deliberately out of scope, same pattern applies later if wanted |
### Deliberately deferred (YAGNI)
- Any workaround for mibook's availability gap (cloud relay, wake scheduling, HA failover to jupiter)
- Discord, Slack, or any other messaging platform beyond Telegram + Email
- Public/internet exposure of any kind
- Merging or rebasing `feat/sops-nix` — this design builds on top of it as-is
## Part 1 — The module
New flake input in `flake.nix`, following the exact shape `sops-nix` already
uses:
```nix
hermes-agent = {
url = "github:NousResearch/hermes-agent";
inputs.nixpkgs.follows = "nixpkgs";
};
```
Import the upstream module **from inside the profile module itself**, via
`imports = [ self.inputs.hermes-agent.nixosModules.default ];` — using
`self` (available via `specialArgs = { inherit self; };` in
`machines/configuration.nix`), not the `inputs` module arg. **Correction
(found during implementation, 2026-09-08):** `inputs` is supplied via
`_module.args.inputs = self.inputs;`, set inside a config block — but
NixOS resolves a module's `imports` list before `_module.args` assignments
merge, so anything referenced inside `imports` must come from
`specialArgs` (fixed up front) instead. Using `inputs` here reproduces
`infinite recursion encountered`; `self.inputs.hermes-agent` is the same
flake input, reached the way that actually works. Separately, this is
deliberately *not* wired through `machines/configuration.nix`'s per-machine
module lists. NixOS's module system errors on a definition for an
undeclared option regardless of `lib.mkIf`'s condition — so declaring
`services.hermes-agent` only on mibook while jupiter still loads the
profile module (because `modules/environments/default.nix` is
machine-agnostic) breaks jupiter's evaluation outright, even with the
profile disabled there. Importing the upstream module unconditionally, and
gating only `config` behind `lib.mkIf cfg.enable`, is what actually
respects "mibook only, not jupiter": the option exists everywhere
(harmless), but only ever turns on where `enable = true`.
New file `modules/environments/hermes-agent/default.nix` following the
repo's standard profile pattern:
```nix
# Hermes Agent — Telegram + Email messaging gateway (Claude subscription auth)
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.my.profiles.hermes-agent;
in
{
imports = [ self.inputs.hermes-agent.nixosModules.default ];
options.my.profiles.hermes-agent.enable =
lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)";
config = lib.mkIf cfg.enable {
services.hermes-agent = {
enable = true;
settings.model.default = "anthropic/claude-sonnet-4-5"; # deliberate pin, see Open risks
environmentFiles = [ config.sops.secrets."hermes-env".path ];
addToSystemPackages = true;
};
# addToSystemPackages only puts `hermes` on $PATH; using it needs group
# access to /var/lib/hermes/.hermes (mode 2770 hermes:hermes). finn is
# this machine's one human user and already knows hermes-env's contents
# (typed them into sops directly), so this is not new exposure.
users.users.finn.extraGroups = [ "hermes" ];
};
}
```
Add `./environments/hermes-agent` to `modules/environments/default.nix`;
enable `my.profiles.hermes-agent.enable = true` in
`machines/mibook/environments.nix`. `machines/configuration.nix` is **not**
touched by this design.
**Provided for free by the upstream module:** `hermes` system user/group
(`createUser` default true), hardened systemd unit (`NoNewPrivileges`,
`ProtectSystem=strict`, `PrivateTmp` — not independently user-configurable
per the docs), `hermes` CLI on `$PATH` via `addToSystemPackages` (usable by
finn once group membership is granted, above).
## Part 2 — Secret
Add to `machines/mibook/secrets.nix` (existing file, per `docs/secrets.md`):
```nix
sops.secrets.hermes-env = {
owner = "hermes";
group = "hermes";
mode = "0400";
restartUnits = [ "hermes-agent.service" ]; # confirm exact unit name at implementation
};
```
Populate via `nix develop -c sops secrets/mibook.yaml`, adding a
multi-line `hermes-env` block:
```
ANTHROPIC_TOKEN=<output of `claude setup-token`>
TELEGRAM_BOT_TOKEN=<from @BotFather>
TELEGRAM_ALLOWED_USERS=<your numeric Telegram user id>
EMAIL_ADDRESS=<mailbox address>
EMAIL_PASSWORD=<password or app-password>
EMAIL_IMAP_HOST=<imap host>
EMAIL_SMTP_HOST=<smtp host>
EMAIL_ALLOWED_USERS=<your own sending address>
```
(`EMAIL_IMAP_PORT`/`EMAIL_SMTP_PORT` default to 993/587 — only set if the
provider differs. See `website/docs/reference/environment-variables.md` for
the full key list.)
Both `*_ALLOWED_USERS` keys are access control, not optional: Telegram
defaults to open DM pairing (any stranger who messages the bot gets a
pairing code and can request approval), and email with no
`EMAIL_ALLOWED_USERS` set **silently ignores** messages from unrecognized
senders — no log line, no error. For an agent with terminal access on the
daily driver, treat both as required.
**Resolved (confirmed against the locked `hermes-agent` revision's source —
`agent/anthropic_credentials.py`, `hermes_cli/providers.py`):** the module
reads `ANTHROPIC_TOKEN` for the setup-token path. No longer an open
question; see Open risks below for what replaced it.
**Where this actually lands at runtime:** the upstream module does not use
systemd `EnvironmentFile=`. An activation script concatenates every
`environmentFiles` entry into **`/var/lib/hermes/.hermes/.env`**, mode
`0640`, owned `hermes:hermes` — plaintext on mibook's plain ext4 root (no
LUKS, per `machines/mibook/disks.nix`), not sops-nix's `/run/secrets`
tmpfs. This is upstream behavior, not something this design controls; see
Rollback below for why it matters.
**Must `git add secrets/mibook.yaml` before `nix build`** — untracked
files are invisible to flake evaluation (existing repo rule, `docs/secrets.md`).
## Part 3 — Rollout runbook (on mibook)
1. Confirm Claude Max extra-usage-credits are actually purchased (Anthropic
console) — the whole auth design depends on this pool being funded.
2. Run `claude setup-token` on mibook; capture the token.
3. Create/gather Telegram bot token (`@BotFather`) and email credentials.
4. Add the `hermes-agent` flake input; add the module wrapper file; wire it
into `modules/environments/default.nix` and `machines/mibook/environments.nix`.
5. Add `sops.secrets.hermes-env` to `machines/mibook/secrets.nix`.
6. `nix develop -c sops secrets/mibook.yaml`, populate `hermes-env`, `git add`.
7. `nix build '.#nixosConfigurations.mibook.config.system.build.toplevel'` — CI-style check before touching the running system.
8. `sudo nixos-rebuild test --flake '.#mibook'` — verify before persisting across reboot.
9. `journalctl -u hermes-agent.service -f` — confirm it starts, decrypts the secret, connects to Telegram (and doesn't loop-crash on the `ANTHROPIC_TOKEN` question from Part 2).
10. Send a test Telegram message; send a test email; confirm both round-trip.
11. `sudo nixos-rebuild switch --flake '.#mibook'` once verified.
### Rollback
`my.profiles.hermes-agent.enable = false`, `nixos-rebuild switch` stops the
service, but does **not** clean up `/var/lib/hermes` — the plaintext
`.env` (Anthropic token, Telegram token, email password) stays on disk
indefinitely, per the persistence note in Part 2. A real rollback is:
`sudo rm -rf /var/lib/hermes`, plus revoking the Telegram bot token
(`@BotFather` → `/revoke`) since it can't be un-leaked once exposed. No
data migration is involved, so this is still cheap — just not one command.
## Open risks (carried forward, not resolved by design)
1. ~~`ANTHROPIC_TOKEN` vs `ANTHROPIC_API_KEY`~~ — **Resolved**: confirmed supported by reading the locked `hermes-agent` revision's source (Part 2).
2. **Extra-credits billing pool funding** — not independently verified, user-asserted intent (Part 3, step 1).
3. **Tier 2 Nix support** — expect some manual troubleshooting; upstream doesn't guarantee the flake stays working across their commits.
4. **Availability gap** — accepted by design, not a bug to fix later unless priorities change.
5. **`ANTHROPIC_TOKEN` cannot refresh unattended.** The setup-token is a terminal value to the module's credential resolver; the refresh path needs a `~/.claude/.credentials.json` with a refresh token that the `hermes` service user won't have. When the token expires, the gateway silently stops authenticating and the operator has to re-run `claude setup-token` and re-populate the secret by hand — watch for this during the confidence window, not something this design solves.
Generated
+459
View File
@@ -0,0 +1,459 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
"repo": "flake-compat",
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1788450739,
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_3": {
"inputs": {
"nixpkgs-lib": [
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1785627969,
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"git-hooks-nix": {
"inputs": {
"flake-compat": [
"nix"
],
"nixpkgs": [
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1784288435,
"narHash": "sha256-ReRHaLgr/uVqdD8afFSn+myXIfpHeOhP0yYe0TJqAA8=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "43b3c1ab9d40fb1dbb008f451988a91e375825e9",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"hermes-agent": {
"inputs": {
"flake-parts": "flake-parts_2",
"home-manager": "home-manager",
"nixpkgs": [
"nixpkgs"
],
"npm-lockfile-fix": "npm-lockfile-fix",
"pyproject-build-systems": "pyproject-build-systems",
"pyproject-nix": "pyproject-nix",
"uv2nix": "uv2nix"
},
"locked": {
"lastModified": 1789361774,
"narHash": "sha256-ashkMxyQelHq7T4AcJ0Zwuzm9/jMR3hTCWW+2FPwlDg=",
"owner": "NousResearch",
"repo": "hermes-agent",
"rev": "5eb99eb2844b22ebb723711b8e6a0bbb80bb5f04",
"type": "github"
},
"original": {
"owner": "NousResearch",
"repo": "hermes-agent",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1786487128,
"narHash": "sha256-ad60hrRVhH/bo3Jl1YLzO+QcS3mUNZr9LNJdzhMO2P4=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "f404edbfa4117810c96b97048299242fc50e5362",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"flake-parts": "flake-parts_3",
"git-hooks-nix": "git-hooks-nix",
"nixpkgs": "nixpkgs",
"nixpkgs-23-11": "nixpkgs-23-11",
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1789352761,
"narHash": "sha256-jAcDj4iXkbxa5AlooMRZj7H3E81Cz/q7yN/bX7GPFSU=",
"owner": "NixOS",
"repo": "nix",
"rev": "920f6956361496578b8a97ae04ec5f400d729ab4",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nix",
"type": "github"
}
},
"nixlib": {
"locked": {
"lastModified": 1736643958,
"narHash": "sha256-tmpqTSWVRJVhpvfSN9KXBvKEXplrwKnSZNAoNPf/S/s=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "1418bc28a52126761c02dd3d89b2d8ca0f521181",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixos-generators": {
"inputs": {
"nixlib": "nixlib",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1769813415,
"narHash": "sha256-nnVmNNKBi1YiBNPhKclNYDORoHkuKipoz7EtVnXO50A=",
"owner": "nix-community",
"repo": "nixos-generators",
"rev": "8946737ff703382fda7623b9fab071d037e897d5",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixos-generators",
"type": "github"
}
},
"nixos-hardware": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1789127271,
"narHash": "sha256-vL/Fpo7Zxa7wvO8V5FAToUBvP+K3XsMB1wUglTtd/J0=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "24cfdc1f9344b90a1eee329a3906e2f39f4d0f1e",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixos-hardware",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1788921488,
"narHash": "sha256-JLopN/XSodSsJIZ7v7tEjpPZ+QXc883s3HoUIxHD8Io=",
"rev": "6aefcda9401be8acc2b74244fb3b37520ea1f0a8",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.9440.6aefcda9401b/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz"
}
},
"nixpkgs-23-11": {
"locked": {
"lastModified": 1717159533,
"narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1788057806,
"narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1789286504,
"narHash": "sha256-3i80JnZ9pZ7tWReb/Sqpc32cfzffCu8TWLnbnH0hN9c=",
"rev": "ef34387ddd751e1ab8857adf4676492d32eb24ec",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.11pre1073009.ef34387ddd75/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1767892417,
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1789114715,
"narHash": "sha256-eeH+/1GxEjw8NCObjaHVIy3YvCQ4Qckrx4LZj5xBaU4=",
"rev": "21a67dc470149f337cecafbe965d8d252a390518",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.9592.21a67dc47014/nixexprs.tar.xz"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-26.05",
"type": "indirect"
}
},
"npm-lockfile-fix": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1775903712,
"narHash": "sha256-2GV79U6iVH4gKAPWYrxUReB0S41ty/Y3dBLquU8AlaA=",
"owner": "jeslie0",
"repo": "npm-lockfile-fix",
"rev": "c6093acb0c0548e0f9b8b3d82918823721930fe8",
"type": "github"
},
"original": {
"owner": "jeslie0",
"repo": "npm-lockfile-fix",
"type": "github"
}
},
"pyproject-build-systems": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
],
"pyproject-nix": [
"hermes-agent",
"pyproject-nix"
],
"uv2nix": [
"hermes-agent",
"uv2nix"
]
},
"locked": {
"lastModified": 1785115949,
"narHash": "sha256-8AM37BfyGaL2v/SZyg4PupRxJ01Y4htvM+WrTjWrPpo=",
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"rev": "62c0d86027edb1c4f39a5facc09876348144f7c9",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "build-system-pkgs",
"type": "github"
}
},
"pyproject-nix": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
]
},
"locked": {
"lastModified": 1784591072,
"narHash": "sha256-zP/WaDxrRu8GANZM61+V2LT/7ycEEdoyLWn7M6WzU7M=",
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"rev": "e3b599ca2e7fcf93d4edf65d7f19bbf6491724f3",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "pyproject.nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"hermes-agent": "hermes-agent",
"nix": "nix",
"nixos-generators": "nixos-generators",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs_3",
"nixpkgs-unstable": "nixpkgs-unstable",
"sops-nix": "sops-nix"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1788914643,
"narHash": "sha256-4GuMPW90JSxXWDPUB9M+1m7fYbe3H0apOd86/zBQ2Kw=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "13616fff713a9f94055c66f15687ebdc17a335df",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"uv2nix": {
"inputs": {
"nixpkgs": [
"hermes-agent",
"nixpkgs"
],
"pyproject-nix": [
"hermes-agent",
"pyproject-nix"
]
},
"locked": {
"lastModified": 1785277507,
"narHash": "sha256-9Tq3UDX2hD/aveW/HvkBlAmEwJTOlY5HQXJM+L5BGmE=",
"owner": "pyproject-nix",
"repo": "uv2nix",
"rev": "5a836d395cbf5fc22670eb98dd4aa4fc4d406977",
"type": "github"
},
"original": {
"owner": "pyproject-nix",
"repo": "uv2nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+60
View File
@@ -0,0 +1,60 @@
{
description = "NixOS configuration";
inputs = {
nix.url = "github:NixOS/nix";
nixpkgs.url = "nixpkgs/nixos-26.05";
nixpkgs-unstable.url = "nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
nixos-hardware.url = "github:NixOS/nixos-hardware";
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
sops-nix = {
url = "github:Mic92/sops-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
hermes-agent = {
url = "github:NousResearch/hermes-agent";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
inputs@{ self, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
./machines/configuration.nix
];
perSystem =
{
self',
inputs',
config,
pkgs,
system,
...
}:
{
# make pkgs available to all `perSystem` functions
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
sops
age
ssh-to-age
nixfmt
];
};
};
systems = [ "x86_64-linux" ];
};
}
+69
View File
@@ -0,0 +1,69 @@
{ self, ... }:
let
inherit (self.inputs)
nixpkgs
nixpkgs-unstable
nixos-hardware
sops-nix
#hyprland
;
nixosSystem = nixpkgs.lib.makeOverridable nixpkgs.lib.nixosSystem;
overlay-unstable = final: prev: {
unstable = import nixpkgs-unstable {
inherit (final) system;
config.allowUnfree = true;
};
};
customModules = import ./core/default.nix;
baseModules = [
# make flake inputs accessible in NixOS
{
_module.args.self = self;
_module.args.inputs = self.inputs;
}
{
imports = [
(
{ pkgs, ... }:
{
nixpkgs.overlays = [
overlay-unstable
(import ../pkgs)
];
nix.nixPath = [
"nixpkgs=${pkgs.path}"
];
documentation.info.enable = false;
}
)
#hyprland.nixosModules.default
];
}
../modules
sops-nix.nixosModules.sops
# ../profiles
];
defaultModules = baseModules ++ customModules;
in
{
flake.nixosConfigurations = {
# use your hardware- model from this list: https://github.com/NixOS/nixos-hardware/blob/master/flake.nix
jupiter = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.bmax-b7-power
./jupiter/configuration.nix
];
};
mibook = nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit self; };
modules = defaultModules ++ [
# nixos-hardware.nixosModules.mibook
./mibook/configuration.nix
];
};
};
}
+158
View File
@@ -0,0 +1,158 @@
# Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running ‘nixos-help’).
{ config, pkgs, ... }:
{
imports = [
# Include the results of the hardware scan.
./hardware-configuration.nix
];
# Bootloader.
boot.loader.grub.enable = true;
boot.loader.grub.device = "/dev/sda";
boot.loader.grub.useOSProber = true;
networking.hostName = "nixos"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Configure network proxy if necessary
# networking.proxy.default = "http://user:password@proxy:port/";
# networking.proxy.noProxy = "127.0.0.1,localhost,internal.domain";
# Enable networking
networking.networkmanager.enable = true;
# Set your time zone.
time.timeZone = "Europe/Berlin";
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "de_DE.UTF-8";
LC_IDENTIFICATION = "de_DE.UTF-8";
LC_MEASUREMENT = "de_DE.UTF-8";
LC_MONETARY = "de_DE.UTF-8";
LC_NAME = "de_DE.UTF-8";
LC_NUMERIC = "de_DE.UTF-8";
LC_PAPER = "de_DE.UTF-8";
LC_TELEPHONE = "de_DE.UTF-8";
LC_TIME = "de_DE.UTF-8";
};
# Enable the X11 windowing system.
services.xserver.enable = true;
# Enable the GNOME Desktop Environment.
services.xserver.displayManager.gdm.enable = true;
services.xserver.desktopManager.gnome.enable = true;
# Configure keymap in X11
services.xserver = {
layout = "de";
xkbVariant = "";
};
# Configure console keymap
console.keyMap = "de";
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound with pipewire.
hardware.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
# use the example session manager (no others are packaged yet so this is enabled by default,
# no need to redefine it in your config for now)
#media-session.enable = true;
};
# Enable touchpad support (enabled default in most desktopManager).
# services.xserver.libinput.enable = true;
# Define a user account. Don't forget to set a password with ‘passwd’.
users.users.finn = {
isNormalUser = true;
description = "Finn Markwitz";
extraGroups = [
"networkmanager"
"wheel"
];
packages = with pkgs; [
firefox
jetbrains.webstorm
jetbrains.goland
jetbrains.pycharm-professional
jetbrains.idea-ultimate
thunderbird
google-chrome
discord
spotify
keepassxc
nextcloud-client
zsh
oh-my-zsh
neovim
];
};
# Enable automatic login for the user.
services.xserver.displayManager.autoLogin.enable = true;
services.xserver.displayManager.autoLogin.user = "finn";
# Workaround for GNOME autologin: https://github.com/NixOS/nixpkgs/issues/103746#issuecomment-945091229
systemd.services."getty@tty1".enable = false;
systemd.services."autovt@tty1".enable = false;
# Allow unfree packages
nixpkgs.config.allowUnfree = true;
# List packages installed in system profile. To search, run:
# $ nix search wget
environment.systemPackages = with pkgs; [
vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
wget
git
python312
nodejs_18
];
# Some programs need SUID wrappers, can be configured further or are
# started in user sessions.
# programs.mtr.enable = true;
# programs.gnupg.agent = {
# enable = true;
# enableSSHSupport = true;
# };
# List services that you want to enable:
# Enable the OpenSSH daemon.
# services.openssh.enable = true;
# Open ports in the firewall.
# networking.firewall.allowedTCPPorts = [ ... ];
# networking.firewall.allowedUDPPorts = [ ... ];
# Or disable the firewall altogether.
# networking.firewall.enable = false;
# This value determines the NixOS release from which the default
# settings for stateful data, like file locations and database versions
# on your system were taken. It‘s perfectly fine and recommended to leave
# this value at the release version of the first install of this system.
# Before changing this value read the documentation for this option
# (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
system.stateVersion = "23.05"; # Did you read the comment?
}
+71
View File
@@ -0,0 +1,71 @@
{
config,
pkgs,
lib,
...
}:
{
# Packages
environment.systemPackages = with pkgs; [
bandwhich
bind # dig
borgbackup
cryptsetup
file
fwupd
fzf
gettext
git
gptfdisk
htop
jq
killall
lsof
mosh
multipath-tools # kpartx
mtr
nmap
nmon
ouch # de-/compress
pciutils
progress
pv
reptyr
rsync
screen
stress-ng
usbutils
tmux
vim
wget
whois
zip
unzip
networkmanager
lm_sensors
systemctl-tui
nixfmt-rfc-style
];
time.timeZone = "Europe/Berlin";
services.timesyncd.enable = true;
# Enable networking
networking.networkmanager.enable = true;
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "de_DE.UTF-8";
LC_IDENTIFICATION = "de_DE.UTF-8";
LC_MEASUREMENT = "de_DE.UTF-8";
LC_MONETARY = "de_DE.UTF-8";
LC_NAME = "de_DE.UTF-8";
LC_NUMERIC = "de_DE.UTF-8";
LC_PAPER = "de_DE.UTF-8";
LC_TELEPHONE = "de_DE.UTF-8";
LC_TIME = "de_DE.UTF-8";
};
}
+8
View File
@@ -0,0 +1,8 @@
[
./core.nix
./modules.nix
./network.nix
./nix.nix
./sops.nix
./users.nix
]
+8
View File
@@ -0,0 +1,8 @@
# Generate via nix-generate
{
config,
lib,
pkgs,
...
}:
{ }
+20
View File
@@ -0,0 +1,20 @@
{
config,
lib,
pkgs,
...
}:
{
networking.networkmanager = {
enable = true;
unmanaged = [
"interface-name:br-*" # Ignore docker compose network bridges
"interface-name:docker?" # Ignore docker default bridge
"interface-name:veth*" # Ignore docker compose network devices
"interface-name:virbr?" # Ignore libvirt default bridge
];
};
}
+33
View File
@@ -0,0 +1,33 @@
{
config,
lib,
pkgs,
inputs,
...
}:
{
nix = {
daemonCPUSchedPolicy = "idle";
daemonIOSchedClass = "idle";
settings = {
auto-optimise-store = true;
};
gc = {
automatic = true;
options = "--delete-older-than 30d";
};
extraOptions = ''
experimental-features = nix-command flakes
warn-dirty = false
'';
registry = {
nixpkgs.flake = inputs.nixpkgs;
unstable.flake = inputs.nixpkgs-unstable;
};
};
nixpkgs.config.allowUnfree = true;
}
+31
View File
@@ -0,0 +1,31 @@
# sops-nix plumbing — applied to every machine.
# See docs/secrets.md for the day-to-day workflow.
{
config,
lib,
pkgs,
...
}:
{
# Each host defaults to its own secrets/<hostname>.yaml. Override per-secret
# with sops.secrets.<name>.sopsFile for shared files.
sops.defaultSopsFile = lib.mkDefault (../../secrets + "/${config.networking.hostName}.yaml");
sops.defaultSopsFormat = "yaml";
sops.validateSopsFiles = true;
# Decrypt with the host's own SSH host key, converted to age.
# Explicit rather than inheriting the openssh-derived default, so this does
# not silently break if services.openssh is ever disabled.
sops.age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
sops.age.generateKey = false;
# Age only. The default here is the RSA host key imported as a GPG key,
# which is deprecated and an extra failure surface.
sops.gnupg.sshKeyPaths = [ ];
environment.systemPackages = with pkgs; [
sops
age
ssh-to-age
];
}
+32
View File
@@ -0,0 +1,32 @@
{
config,
pkgs,
lib,
...
}:
{
users.users.finn = {
isNormalUser = true;
home = "/home/finn";
group = "finn";
extraGroups = [
"adbusers" # adb control
"audio" # sound control
"dialout" # serial-console
"docker" # usage of `docker` socket
"input" # mouse control
"libvirtd" # kvm control
"networkmanager" # wireless configuration
"podman" # usage of `podman` socket
"video" # screen control
"wheel" # `sudo` for the user.
];
# openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOFx6OLwL9MbkD3mnMsv+xrzZHN/rwCTgVs758SCLG0h finn@thinkman" ];
};
users.groups.finn = {
gid = 1000;
};
}
+77
View File
@@ -0,0 +1,77 @@
{
config,
pkgs,
lib,
...
}:
{
imports = [
./disks.nix
./hardware-configuration.nix
./environments.nix
./network.nix
];
networking.hostName = "jupiter";
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.kernelPackages = pkgs.linuxPackages;
# Shitfuck SOnar Dotnet dependency
nixpkgs.config = {
permittedInsecurePackages = [
"aspnetcore-runtime-6.0.36"
"aspnetcore-runtime-wrapped-6.0.36"
"dotnet-sdk-6.0.428"
"dotnet-sdk-wrapped-6.0.428"
];
};
services.openssh.enable = true;
# Configure keymap in X11
services.xserver.xkb = {
layout = "de";
variant = "";
};
# Configure console keymap
console.keyMap = "de";
# Enable CUPS to print documents.
services.printing.enable = true;
services.fwupd.enable = true;
security.rtkit.enable = true;
# Try fix wifi disconnect
networking.networkmanager.wifi.powersave = false;
# Disable hibernate and suspend completely - this is a headless server.
# The board exposes a phantom ACPI lid switch (PNP0C0D) that fires a
# "closed" event when the HDMI cable is unplugged, which used to trigger
# a real suspend via logind's default HandleLidSwitch=suspend.
powerManagement.enable = true;
systemd.targets."hibernate".enable = false;
systemd.targets."hybrid-sleep".enable = false;
systemd.targets."suspend-then-hibernate".enable = false;
systemd.targets."suspend".enable = false;
systemd.targets."sleep".enable = false;
services.logind.settings.Login = {
HandleLidSwitch = "ignore";
HandleLidSwitchExternalPower = "ignore";
HandleLidSwitchDocked = "ignore";
};
# Optional: kernel parameter to fully disable hibernation
boot.kernelParams = [ "nohibernate" ];
system = {
stateVersion = "23.05";
autoUpgrade.enable = true;
};
}
+30
View File
@@ -0,0 +1,30 @@
{
config,
lib,
pkgs,
...
}:
#TODO: CHECK THIS OUT: https://github.com/nix-community/disko its kinda better but felix does not support it
{
fileSystems."/" =
{ device = "/dev/disk/by-uuid/8e3ac6ce-6865-49d8-b6d0-d7c008de391e";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/F193-2A35";
fsType = "vfat";
};
fileSystems."/data/nfs" = {
device = "voyager:/";
fsType = "nfs";
options = [
"x-systemd.automount"
"noauto"
];
};
}
+73
View File
@@ -0,0 +1,73 @@
# enabled profiles
_: {
my.profiles = {
hyprland.enable = false;
zsh.enable = true;
apps = {
desktop_apps = false;
dev_apps = false;
gnome_apps = false;
};
actual.enable = false;
audiobookshelf.enable = true;
claude-code.enable = true;
kde-desktop.enable = false;
radarr.enable = true;
docker.enable = true;
readarr.enable = true;
sonarr.enable = true;
jellyfin.enable = true;
jellyseerr.enable = true;
immich.enable = true;
development.enable = true;
home-assistant.enable = true;
homepage.enable = true;
paperless = {
enable = true;
port = 28981; # Optional, to override the default port
extraConfig = {
PAPERLESS_ADMIN_USER = "finn";
PAPERLESS_OCR_LANGUAGE = "deu+eng";
};
};
prowlarr.enable = true;
};
# my.homepage.widgets = [
# {
# search = {
# provider = "google";
# target = "_blank";
# };
# }
# {
# resources = {
# cpu = true;
# memory = true;
# disk = "/";
# cacheInterval = 5000;
# };
# }
# {
# datetime = {
# text_size = "xl";
# format = {
# timeStyle = "short";
# dateStyle = "short";
# hour12 = false;
# };
# };
# }
# ];
my.hardware = {
bluetooth.enable = true;
sound.enable = false;
};
my.services = {
vpn.enable = true;
webserver.enable = false;
};
}
@@ -0,0 +1,71 @@
# Do not modify this file! It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"nvme"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
#boot.kernelParams = [ "i915.force_probe=9a49" ];
# 9a49
nixpkgs.config.packageOverrides = pkgs: {
vaapiIntel = pkgs.vaapiIntel.override { enableHybridCodec = true; };
};
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver # LIBVA_DRIVER_NAME=iHD
#vaapiIntel # LIBVA_DRIVER_NAME=i965 (older but works better for Firefox/Chromium)
libva-vdpau-driver
libvdpau-va-gl
vpl-gpu-rt # oneVPL/MFX runtime, required for QSV (h264_qsv/hevc_qsv) session creation
intel-compute-runtime # OpenCL runtime, required for tonemap_opencl (HDR tone-mapping)
];
};
hardware.logitech.wireless = {
enable = true;
enableGraphical = true;
};
fileSystems."/" =
{ device = "/dev/disk/by-uuid/8e3ac6ce-6865-49d8-b6d0-d7c008de391e";
fsType = "ext4";
};
fileSystems."/boot" =
{ device = "/dev/disk/by-uuid/F193-2A35";
fsType = "vfat";
options = [ "fmask=0077" "dmask=0077" ];
};
swapDevices = [ { device = "/dev/disk/by-uuid/7ffb2d2b-c03d-474b-8513-fee26e1e5e2f"; } ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp3s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
powerManagement.cpuFreqGovernor = lib.mkDefault "performance";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
+10
View File
@@ -0,0 +1,10 @@
_: {
networking.firewall.allowedTCPPorts = [
8080 # aria
];
networking = {
domain = "jupiter";
search = [ "jupiter.solar.internal" ];
};
}
+66
View File
@@ -0,0 +1,66 @@
{
config,
pkgs,
lib,
...
}:
{
imports = [
./disks.nix
./hardware-configuration.nix
./environments.nix
./network.nix
./secrets.nix
# ./system.nix use docker here
];
networking.hostName = "mibook";
swapDevices = [
{
device = "/var/lib/swapfile";
size = 8 * 1024; # 8G, headroom for nix builds (7.7G RAM, no prior swap)
}
];
boot.loader.grub = {
enable = true;
device = "/dev/nvme0n1";
useOSProber = true;
};
# Configure keymap in X11
services.xserver.xkb = {
layout = "de";
variant = "";
};
# Configure console keymap
console.keyMap = "de";
# Enable CUPS to print documents.
services.printing.enable = true;
nixpkgs.config.allowUnfree = true;
hardware.nvidia.prime = {
sync.enable = false;
nvidiaBusId = "PCI:01:00:0";
intelBusId = "PCI:00:2:0";
};
services.openssh.enable = true;
# KDE (PowerDevil) power settings: do nothing on lid close while on AC power.
# Shipped as a system-wide default; KConfig cascades so a user's own
# ~/.config/powerdevilrc will override this if present.
environment.etc."xdg/powerdevilrc".text = ''
[AC][SuspendAndShutdown]
LidAction=0
'';
system = {
stateVersion = "23.05";
autoUpgrade.enable = true;
};
}
+15
View File
@@ -0,0 +1,15 @@
{
config,
lib,
pkgs,
...
}:
#TODO: CHECK THIS OUT: https://github.com/nix-community/disko its kinda better but felix does not support it
{
fileSystems."/" = {
device = "/dev/disk/by-uuid/46e36a07-e75a-480a-a2f1-be103c6a3881";
fsType = "ext4";
};
}
+25
View File
@@ -0,0 +1,25 @@
# enabled profiles
_: {
my.profiles = {
kde-desktop.enable = true;
zsh.enable = true;
apps = {
desktop_apps = true;
dev_apps = true;
};
development.enable = true;
docker.enable = true;
claude-code.enable = true;
hermes-agent.enable = true;
};
my.hardware = {
bluetooth.enable = true;
sound.enable = false;
nvidia.enable = false;
};
my.services = {
vpn.enable = true;
};
}
@@ -0,0 +1,43 @@
# Do not modify this file! It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{
config,
lib,
pkgs,
modulesPath,
...
}:
{
imports = [
(modulesPath + "/installer/scan/not-detected.nix")
];
boot.initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"nvme"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ];
fileSystems."/" = {
device = "/dev/disk/by-uuid/46e36a07-e75a-480a-a2f1-be103c6a3881";
fsType = "ext4";
};
swapDevices = [ ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp2s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
+8
View File
@@ -0,0 +1,8 @@
_: {
# Athena (local AI): allow LAN access to the Hermes web dashboard.
# Bound to 0.0.0.0:9119 in the athena docker stack; NixOS default-deny
# firewall otherwise blocks inbound connections from other devices.
networking.firewall.allowedTCPPorts = [
9119 # athena hermes dashboard
];
}
+11
View File
@@ -0,0 +1,11 @@
# Secrets decrypted on mibook. sopsFile defaults to secrets/mibook.yaml
# via machines/core/sops.nix.
{ ... }:
{
sops.secrets.hermes-env = {
owner = "hermes";
group = "hermes";
mode = "0400";
restartUnits = [ "hermes-agent.service" ]; # confirmed/fixed in Task 4 if the unit name differs
};
}
+8
View File
@@ -0,0 +1,8 @@
{ ... }:
{
imports = [
./environments
./hardware
./services
];
}
+47
View File
@@ -0,0 +1,47 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.actual;
hostName = config.networking.hostName;
port = 40465;
in
{
options.my.profiles.actual = with lib; {
enable = mkEnableOption "Actual budget service";
};
config = lib.mkIf cfg.enable {
services.actual = {
enable = true;
openFirewall = true;
settings = {
port = port;
hostname = "0.0.0.0";
};
};
environment.systemPackages = with pkgs; [
actual-server
];
my.homepage.services = [
{
group = "Finance";
name = "Actual";
description = "Budgeting";
href = "http://${hostName}:${toString port}";
icon = "mdi-cash";
}
];
systemd.services.actual = {
after = [ "network-online.target" ];
};
};
}
+35
View File
@@ -0,0 +1,35 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.apps;
in
{
options.my.profiles.apps = with lib; {
desktop_apps = mkEnableOption "Basic Apps";
dev_apps = mkEnableOption "Development Apps";
gnome_apps = mkEnableOption "Gnome Extentions and Configuration";
};
config = lib.mkIf cfg.desktop_apps {
users.users.finn.packages = with pkgs; [
firefox
thunderbird
google-chrome
vscode
discord
spotify
keepassxc
moonlight-qt
nextcloud-client
pipewire
wireplumber
vlc
ghostty
];
};
}
@@ -0,0 +1,45 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.audiobookshelf;
hostName = config.networking.hostName;
domain = config.networking.domain;
in
{
options.my.profiles.audiobookshelf = with lib; {
enable = mkEnableOption "Audio Book Service";
};
config = lib.mkIf cfg.enable {
services.audiobookshelf = {
enable = true;
openFirewall = true;
port = 63834;
host = "0.0.0.0";
};
environment.systemPackages = with pkgs; [
audiobookshelf
];
my.homepage.services = [
{
group = "Media";
name = "Audiobookshelf";
description = "Audiobooks and podcasts";
href = "http://${domain}:63834";
icon = "audiobookshelf.png";
}
];
systemd.services.audiobookshelf = {
after = [ "network-online.target" ];
};
};
}
@@ -0,0 +1,31 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.claude-code;
in
{
options.my.profiles.claude-code = with lib; {
enable = mkEnableOption "Claude Code CLI";
};
config = lib.mkIf cfg.enable {
users.users.finn.packages = with pkgs; [
claude-code
ripgrep
fd
gh
jq
];
# Future: headless Claude Code service
# A natural next step is exposing Claude Code as a persistent background service —
# e.g. a systemd user service that accepts work via an HTTP API or Unix socket,
# triggerable over SSH or a local network endpoint. This would turn the MiBook
# into a true remote execution node without requiring an interactive session.
# See: my.profiles.claude-code.service.enable (not yet implemented)
};
}
+25
View File
@@ -0,0 +1,25 @@
{ ... }:
{
imports = [
./actual
./apps
./audiobookshelf
./claude-code
./development
./home-assistant
./hyprland
./zsh
./paperless
./prowlarr
./radarr
./docker
./homepage
./kde-desktop
./readarr
./sonarr
./jellyfin
./jellyseerr
./immich
./hermes-agent
];
}
@@ -0,0 +1,41 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.development;
in
{
options.my.profiles.development = with lib; {
enable = mkEnableOption "Development Tools";
};
config = lib.mkIf cfg.enable {
users.users.finn.packages = with pkgs; [
google-chrome
vscode
zed-editor
neovim
# jetbrains.idea-ultimate
go
nodejs # bundles npm
(python3.withPackages (
ps: with ps; [
jupyter # notebooks
matplotlib
numpy
pandas
pillow
plotly
scikit-learn
scipy
tqdm # progressbar in pandas
wheel # python development
]
))
];
};
}
+29
View File
@@ -0,0 +1,29 @@
# Docker related settings
{
config,
inputs,
lib,
options,
pkgs,
...
}:
let
cfg = config.my.profiles.docker;
in
{
options.my.profiles.docker = with lib; {
enable = mkEnableOption "docker configuration";
};
config = lib.mkIf cfg.enable {
environment.systemPackages = with pkgs; [
docker
docker-compose
];
virtualisation.docker = {
enable = true;
autoPrune.enable = true;
};
};
}
@@ -0,0 +1,45 @@
# Hermes Agent — Telegram + Email messaging gateway (Claude subscription auth)
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.my.profiles.hermes-agent;
in
{
imports = [ self.inputs.hermes-agent.nixosModules.default ];
options.my.profiles.hermes-agent.enable =
lib.mkEnableOption "Hermes Agent (Telegram + Email gateway)";
config = lib.mkIf cfg.enable {
services.hermes-agent = {
enable = true;
# Deliberate pin, matches the approved design — bump on purpose, not by drift.
settings.model.default = "anthropic/claude-sonnet-4-5";
environmentFiles = [ config.sops.secrets."hermes-env".path ];
addToSystemPackages = true;
backend = {
# "dashboard" serves the browser admin panel, not just the API.
mode = "dashboard";
# Non-loopback bind opens the dashboard's auth gate — username/password
# comes from HERMES_DASHBOARD_BASIC_AUTH_* in hermes-env (see secrets.nix).
host = "0.0.0.0";
port = 9119;
};
};
# NixOS default-deny firewall otherwise blocks inbound LAN connections.
networking.firewall.allowedTCPPorts = [ config.services.hermes-agent.backend.port ];
# addToSystemPackages only puts `hermes` on $PATH; using it needs group
# access to /var/lib/hermes/.hermes (mode 2770 hermes:hermes). finn is
# this machine's one human user and already knows hermes-env's contents
# (typed them into sops directly), so this is not new exposure.
users.users.finn.extraGroups = [ "hermes" ];
};
}
@@ -0,0 +1,76 @@
# manages home automations
{
config,
lib,
pkgs,
self,
...
}:
let
cfg = config.my.profiles.home-assistant;
hostName = config.networking.hostName;
in
{
options.my.profiles.home-assistant = with lib; {
enable = mkEnableOption "Home Automation";
};
config = lib.mkIf cfg.enable {
services.matter-server.enable = true;
services.home-assistant = {
enable = true;
openFirewall = true;
extraComponents = [
"matter"
"mobile_app"
"otbr"
"thread"
"xiaomi_miio"
"apple_tv" # Apple TV (pyatv); pair via UI PIN flow
"tuya" # Tuya/SmartLife cloud; Unistyle WLAN irrigation computer
];
};
services.home-assistant.config = {
homeassistant = {
name = "Home - Rechberg";
unit_system = "metric";
internal_url = "http://${hostName}:8123";
external_url = "http://jupiter.solar.internal:8123";
};
mobile_app = { };
automation = "!include automations.yaml";
script = "!include scripts.yaml";
scene = "!include scenes.yaml";
};
# `!include` targets must exist or HA fails at startup. Create them empty
# so HA's UI editor can write to them; `f` only acts if the file is absent.
systemd.tmpfiles.rules = [
"f /var/lib/hass/automations.yaml 0644 hass hass - []"
"f /var/lib/hass/scripts.yaml 0644 hass hass - {}"
"f /var/lib/hass/scenes.yaml 0644 hass hass - []"
];
services.openthread-border-router = {
enable = true;
package = pkgs.unstable.openthread-border-router;
openFirewall = true;
backboneInterfaces = [ "enp3s0" ];
radio.device = "/dev/serial/by-id/usb-Nabu_Casa_ZBT-2_DCB4D9149C7C-if00";
radio.baudRate = 460800;
};
my.homepage.services = [
{
group = "Services";
name = "Home Assistant";
description = "Home automation";
href = "http://${hostName}:8123";
icon = "si-homeassistant";
}
];
};
}
@@ -0,0 +1,39 @@
[
{
Developer = [
{
Github = [
{
abbr = "GitLab - Smoothcloud";
href = "https://gitlab.smoothcloud.de/";
}
];
}
];
}
{
Social = [
{
Reddit = [
{
icon = "reddit.png";
href = "https://reddit.com/";
description = "The front page of the internet";
}
];
}
];
}
{
Entertainment = [
{
YouTube = [
{
abbr = "YT";
href = "https://youtube.com/";
}
];
}
];
}
]
+126
View File
@@ -0,0 +1,126 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.homepage;
dashboardPort = 8082;
dashboardHost = config.networking.domain;
dashboardUrl = "http://${dashboardHost}:${toString dashboardPort}";
manualServices = import ./manual-services.nix;
manualWidgets = import ./manual-widgets.nix;
groupedServices = lib.foldl' (
acc: entry:
acc
// {
${entry.group} = (acc.${entry.group} or [ ]) ++ [ entry ];
}
) { } config.my.homepage.services;
homepageServices = lib.mapAttrsToList (group: entries: {
${group} = map (
entry:
let
service = builtins.removeAttrs entry [
"group"
"name"
];
in
{
${entry.name} = lib.filterAttrs (_: value: value != null) service;
}
) entries;
}) groupedServices;
# homepage-dashboard keeps only the first services.yaml list entry with a
# given group name (first match wins), so module-contributed and manual
# groups sharing a name (e.g. "Services") must be merged here rather than
# simply concatenated, or the later ones are silently dropped.
mergeGroups =
lists:
let
merged = lib.foldl' (
acc: groupAttrs:
let
groupName = builtins.head (builtins.attrNames groupAttrs);
in
acc // { ${groupName} = (acc.${groupName} or [ ]) ++ groupAttrs.${groupName}; }
) { } lists;
in
lib.mapAttrsToList (name: entries: { ${name} = entries; }) merged;
in
{
options.my.homepage.services =
with lib;
mkOption {
type = types.listOf (
types.submodule {
options = {
group = mkOption {
type = types.str;
description = "Homepage service group";
};
name = mkOption {
type = types.str;
description = "Homepage service name";
};
description = mkOption {
type = types.str;
description = "Homepage service description";
};
href = mkOption {
type = types.str;
description = "Homepage service URL";
};
icon = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional Homepage service icon";
};
};
}
);
default = [ ];
description = "Merged homepage service metadata contributed by repo modules.";
};
options.my.homepage.widgets =
with lib;
mkOption {
type = types.listOf types.attrs;
default = [ ];
description = "Widget definitions passed directly to services.homepage-dashboard.widgets. Each entry is an attrset like { resources = { cpu = true; memory = true; }; }.";
};
options.my.profiles.homepage = with lib; {
enable = mkEnableOption "getHomepage.dev Dashboard";
};
config = lib.mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [ dashboardPort ];
services.homepage-dashboard = {
enable = true;
listenPort = dashboardPort;
allowedHosts = "${dashboardHost}:${toString dashboardPort},192.168.178.65:${toString dashboardPort},localhost:${toString dashboardPort},127.0.0.1:${toString dashboardPort},jupiter:${toString dashboardPort},jupiter.solar.internal:${toString dashboardPort}";
bookmarks = import ./bookmarks.nix;
services = mergeGroups (homepageServices ++ manualServices);
widgets = config.my.homepage.widgets ++ manualWidgets;
};
users.users.finn.packages = with pkgs; [
homepage-dashboard
];
programs.chromium.homepageLocation = dashboardUrl;
};
}
@@ -0,0 +1,35 @@
[
# Example:
{
"Services" = [
{
"Athena Dashboard" = {
href = "http://mibook:9119";
description = "Hermes Agent messaging gateway";
icon = "mdi-robot-outline";
};
}
{
"Tesla - Dashboard" = {
href = "https://grafana.smoothcloud.de";
description = "Tesla Information";
icon = "tesla.png";
};
}
{
"GitLab" = {
href = "https://gitlab.smoothcloud.de";
description = "Selfhosted GitLab";
icon = "gitlab.png";
};
}
{
"Nextcloud" = {
href = "https://next.smoothcloud.de";
description = "Selfhosted Cloud Service";
icon = "nextcloud.png";
};
}
];
}
]
@@ -0,0 +1,6 @@
[
# Example:
{ resources = { cpu = true; memory = true; disk = "/"; }; }
{ datetime = { text_size = "xl"; format = { timeStyle = "short"; }; }; }
{ search = {provider = "google"; focus = true; target = "_blank"; }; }
]
+46
View File
@@ -0,0 +1,46 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.hyprland;
in
{
options.my.profiles.hyprland = with lib; {
enable = mkEnableOption "hyprland env";
};
config = lib.mkIf cfg.enable {
programs.hyprland = {
enable = true;
xwayland.enable = true;
};
services.displayManager.sddm.wayland.enable = true;
services.xserver = {
layout = "de";
xkbVariant = "";
};
users.users.finn.packages = with pkgs; [
wofi
mako
kitty
hyprpaper
waybar
libsForQt5.dolphin
#libsForQt5.systemsettings
libsForQt5.polkit-kde-agent
#xdg-desktop-portal-hyprland
];
hardware = {
# Opengl
opengl.enable = true;
# Most wayland compositors need this
nvidia.modesetting.enable = true;
};
};
}
+52
View File
@@ -0,0 +1,52 @@
# Immich self-hosted photo & video server
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.immich;
hostName = config.networking.hostName;
port = 2283;
in
{
options.my.profiles.immich = with lib; {
enable = mkEnableOption "Immich photo server";
};
config = lib.mkIf cfg.enable {
services.immich = {
enable = true;
host = "0.0.0.0";
inherit port;
openFirewall = true;
mediaLocation = "/var/lib/immich";
machine-learning.enable = true;
accelerationDevices = [ "/dev/dri/renderD128" ];
# Setting `settings` puts Immich in config-file mode: the admin settings
# UI becomes read-only and system config is managed declaratively here.
settings = {
server.externalDomain = "http://${hostName}:${toString port}";
# Intel Quick Sync hardware transcoding (jupiter's iGPU).
ffmpeg.accel = "qsv";
};
};
# The native module does not add GPU groups; required for VAAPI/QSV transcoding.
users.users.immich.extraGroups = [
"video"
"render"
];
my.homepage.services = [
{
group = "Media";
name = "Immich";
description = "Photo & video server";
href = "http://${hostName}:${toString port}";
icon = "immich.png";
}
];
};
}
+45
View File
@@ -0,0 +1,45 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.jellyfin;
hostName = config.networking.hostName;
port = 8096;
in
{
options.my.profiles.jellyfin = with lib; {
enable = mkEnableOption "Media Service";
};
config = lib.mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
};
environment.systemPackages = [ pkgs.libva-utils ];
my.homepage.services = [
{
group = "Media";
name = "Jellyfin";
description = "Media server";
href = "http://${hostName}:${toString port}";
icon = "jellyfin.png";
}
];
systemd.services.jellyfin = {
after = [ "network-online.target" ];
serviceConfig.SupplementaryGroups = [
"video"
"render"
];
};
};
}
@@ -0,0 +1,33 @@
# manages and downloads films
{ config, lib, ... }:
let
cfg = config.my.profiles.jellyseerr;
hostName = config.networking.hostName;
port = 5055;
in
{
options.my.profiles.jellyseerr = with lib; {
enable = mkEnableOption "JellySeerr Media Service";
};
config = lib.mkIf cfg.enable {
services.jellyseerr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Jellyseerr";
description = "Media requests";
href = "http://${hostName}:${toString port}";
icon = "mdi-television-guide";
}
];
systemd.services.jellyseerr = {
after = [ "network-online.target" ];
};
};
}
@@ -0,0 +1,27 @@
# KDE Desktio Environment
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.kde-desktop;
in
{
options.my.profiles.kde-desktop = with lib; {
enable = mkEnableOption "KDE Desktop Environment";
};
config = lib.mkIf cfg.enable {
services = {
displayManager.sddm.enable = true;
displayManager.sddm.wayland.enable = true;
desktopManager.plasma6.enable = true;
};
users.users.finn.packages = with pkgs; [
# Programms can be added here...
numix-icon-theme
];
};
}
@@ -0,0 +1,48 @@
# document management system
{ config, lib, ... }:
let
cfg = config.my.profiles.paperless;
hostName = config.networking.hostName;
in
{
options.my.profiles.paperless = with lib; {
enable = mkEnableOption "Paperless Server";
port = mkOption {
type = types.port;
default = 28981;
example = 8080;
description = "Internal port for webui";
};
extraConfig = mkOption {
type = types.attrs;
default = { };
example = {
PAPERLESS_OCR_LANGUAGE = "deu+eng";
};
description = "Extra configuration options";
};
};
config = lib.mkIf cfg.enable {
services.paperless = {
enable = true;
address = "0.0.0.0";
port = cfg.port;
# settings = cfg.extraConfig;
};
my.homepage.services = [
{
group = "Services";
name = "Paperless";
description = "Document management";
href = "http://${hostName}:${toString cfg.port}";
icon = "paperless-ngx.png";
}
];
networking.firewall.allowedTCPPorts = [ cfg.port ];
};
}
+51
View File
@@ -0,0 +1,51 @@
# manages indexes
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.prowlarr;
hostName = config.networking.hostName;
port = 9696;
in
# domain = config.networking.domain;
# port = 9696;
{
options.my.profiles.prowlarr = with lib; {
enable = mkEnableOption "Prowlarr for indexing";
};
config = lib.mkIf cfg.enable {
services.prowlarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Prowlarr";
description = "Indexer manager";
href = "http://${hostName}:${toString port}";
icon = "prowlarr.png";
}
];
# # ugly fix for service not having a homedirectory
# users.users.prowlarr = {
# isSystemUser = true;
# home = "/var/lib/prowlarr";
# group = "prowlarr";
# uid = 61654;
# };
# users.groups.prowlarr = {
# gid = 61654;
# };
systemd.services.prowlarr = {
after = [ "network-online.target" ];
};
};
}
+43
View File
@@ -0,0 +1,43 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.radarr;
hostName = config.networking.hostName;
port = 7878;
in
# domain = config.networking.domain;
# port = 7878;
{
options.my.profiles.radarr = with lib; {
enable = mkEnableOption "Sonarr for films management";
};
config = lib.mkIf cfg.enable {
services.radarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Radarr";
description = "Movie management";
href = "http://${hostName}:${toString port}";
icon = "radarr.png";
}
];
my.profiles.prowlarr.enable = true;
systemd.services.radarr = {
after = [ "network-online.target" ];
};
};
}
+43
View File
@@ -0,0 +1,43 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.readarr;
hostName = config.networking.hostName;
port = 8787;
in
# domain = config.networking.domain;
# port = 7878;
{
options.my.profiles.readarr = with lib; {
enable = mkEnableOption "Readarr for ebook management";
};
config = lib.mkIf cfg.enable {
services.readarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Readarr";
description = "Book management";
href = "http://${hostName}:${toString port}";
icon = "readarr.png";
}
];
my.profiles.prowlarr.enable = true;
systemd.services.readarr = {
after = [ "network-online.target" ];
};
};
}
+41
View File
@@ -0,0 +1,41 @@
# manages and downloads films
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.sonarr;
hostName = config.networking.hostName;
port = 8989;
in
{
options.my.profiles.sonarr = with lib; {
enable = mkEnableOption "sonarr for series management";
};
config = lib.mkIf cfg.enable {
services.sonarr = {
enable = true;
openFirewall = true;
};
my.homepage.services = [
{
group = "Media";
name = "Sonarr";
description = "Series management";
href = "http://${hostName}:${toString port}";
icon = "sonarr.png";
}
];
my.profiles.prowlarr.enable = true;
systemd.services.sonarr = {
after = [ "network-online.target" ];
};
};
}
+27
View File
@@ -0,0 +1,27 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.profiles.zsh;
in
{
options.my.profiles.zsh = with lib; {
enable = mkEnableOption "zsh env";
};
config = lib.mkIf cfg.enable {
users.defaultUserShell = pkgs.zsh;
programs.zsh = {
enable = true;
ohMyZsh = {
enable = true;
theme = "agnoster";
};
};
};
}
+30
View File
@@ -0,0 +1,30 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.bluetooth;
in
{
options.my.hardware.bluetooth = with lib; {
enable = mkEnableOption "bluetooth configuration";
};
config = lib.mkIf cfg.enable {
hardware.bluetooth = {
enable = true;
package = pkgs.bluez;
settings = {
General = {
Enable = "Source,Sink,Media,Socket";
};
};
};
services.blueman.enable = true;
environment.systemPackages = with pkgs; [
sony-headphones-client
];
};
}
+18
View File
@@ -0,0 +1,18 @@
# Hardware-related modules
{ ... }:
{
imports = [
./bluetooth
#./debug
#./drive-monitor
#./firmware
#./graphics
#./keychron
./nvidia
#./yubikey
./sound
#./thunderbolt
# ./wifi
];
}
+53
View File
@@ -0,0 +1,53 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.nvidia;
in
{
options.my.hardware.nvidia = with lib; {
enable = mkEnableOption "NVIDIA GPU support";
};
config = lib.mkIf cfg.enable {
# Enable OpenGL
hardware.graphics = {
enable = true;
};
# Load nvidia driver for Xorg and Wayland
services.xserver.videoDrivers = ["nvidia"];
hardware.nvidia = {
# Modesetting is required.
modesetting.enable = true;
# Nvidia power management. Experimental, and can cause sleep/suspend to fail.
# Enable this if you have graphical corruption issues or application crashes after waking
# up from sleep. This fixes it by saving the entire VRAM memory to /tmp/ instead
# of just the bare essentials.
powerManagement.enable = false;
# Fine-grained power management. Turns off GPU when not in use.
# Experimental and only works on modern Nvidia GPUs (Turing or newer).
powerManagement.finegrained = false;
# Use the NVidia open source kernel module (not to be confused with the
# independent third-party "nouveau" open source driver).
# Support is limited to the Turing and later architectures. Full list of
# supported GPUs is at:
# https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus
# Only available from driver 515.43.04+
open = false;
# Enable the Nvidia settings menu,
# accessible via `nvidia-settings`.
nvidiaSettings = true;
# Optionally, you may need to select the appropriate driver version for your specific GPU.
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
};
}
+26
View File
@@ -0,0 +1,26 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.bluetooth;
in
{
options.my.hardware.sound = with lib; {
enable = mkEnableOption "Sound configuration";
};
config = lib.mkIf cfg.enable {
services.pulseaudio = {
enable = false;
support32Bit = true;
};
users.extraUsers.finn.extraGroups = [ "audio" ];
environment.systemPackages = with pkgs; [
headsetcontrol
];
};
}
+21
View File
@@ -0,0 +1,21 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.hardware.wifi;
in
{
options.my.hardware.wifi = with lib; {
enable = mkEnableOption "wifi configuration";
};
config = lib.mkIf cfg.enable {
hardware.wifi = {
enable = true;
package = pkgs.rtw89-firmware;
};
};
}
+9
View File
@@ -0,0 +1,9 @@
# Service-related modules
{ ... }:
{
imports = [
./vpn
./webserver
];
}
+18
View File
@@ -0,0 +1,18 @@
# self-hosted vpn
{ config, lib, ... }:
let
cfg = config.my.services.vpn;
in
{
options.my.services.vpn = with lib; {
enable = mkEnableOption "Headscale VPN Setup";
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
services.tailscale.enable = true;
}
]
);
}
+247
View File
@@ -0,0 +1,247 @@
# public webserver with reverseproxy
{
config,
lib,
pkgs,
...
}:
let
cfg = config.my.services.webserver;
inherit (config.networking) domain;
virtualHostOption = lib.types.submodule {
options = {
subdomain = lib.mkOption {
type = lib.types.str;
example = "dev";
description = ''
Which subdomain, under config.networking.domain, to use
for this virtual host.
'';
};
homepage = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to expose this virtual host on homepage-dashboard.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "Web";
description = ''
Homepage service group for this virtual host.
'';
};
name = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Optional display name for homepage-dashboard. Defaults to the subdomain.
'';
};
description = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Optional homepage-dashboard description for this virtual host.
'';
};
icon = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Optional homepage-dashboard icon for this virtual host.
'';
};
};
port = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
example = 8080;
description = ''
Which port to proxy to, through localhost, for this virtual host.
This option is incompatible with `root`.
'';
};
root = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
example = "/var/www/blog";
description = ''
The root folder for this virtual host. This option is incompatible
with `port`.
'';
};
extraConfig = lib.mkOption {
type = with lib.types; nullOr lines;
example = lib.literalExpression ''
{
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
}
'';
default = null;
description = ''
Any extra configuration that should be applied to this virtual host.
'';
};
};
};
in
{
options.my.services.webserver = {
enable = lib.mkEnableOption "webserver";
virtualHosts = lib.mkOption {
type = lib.types.listOf virtualHostOption;
default = [ ];
example = lib.literalExpression ''
[
{
subdomain = "gitea";
port = 8080;
homepage.description = "Git forge";
}
{
subdomain = "dev";
root = "/var/www/dev";
homepage.description = "Static site";
}
{
subdomain = "jellyfin";
port = 8096;
homepage.group = "Media";
homepage.description = "Media server";
extraConfig = {
locations."/socket" = {
proxyPass = "http://localhost:8096/";
proxyWebsockets = true;
};
};
}
]
'';
description = ''
List of virtual hosts to set-up using default settings.
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = lib.allUnique (builtins.filter (p: p != null) (map (v: v.port) cfg.virtualHosts));
message =
let
portsWithSubdomains = builtins.filter (v: v.port != null) cfg.virtualHosts;
duplicates = lib.filter (
p: builtins.length (lib.filter (x: x.port == p.port) portsWithSubdomains) > 1
) portsWithSubdomains;
in
if duplicates == [ ] then
""
else
"Duplicate ports found in my.services.webserver.virtualHosts: "
+ builtins.concatStringsSep ", " (map (v: v.subdomain + ":" + builtins.toString v.port) duplicates);
}
];
my.homepage.services = map (
vhost:
{
group = vhost.homepage.group;
name = if vhost.homepage.name != "" then vhost.homepage.name else vhost.subdomain;
description =
if vhost.homepage.description != "" then
vhost.homepage.description
else if vhost.root != null then
"Static site"
else if vhost.port != null then
"Reverse proxied service"
else
"Web service";
href = "https://${vhost.subdomain}.${domain}";
icon = vhost.homepage.icon;
}
) (builtins.filter (vhost: vhost.homepage.enable) cfg.virtualHosts);
services = {
nginx.enable = false;
caddy = {
enable = true;
email = "jupiter@solar.internal";
globalConfig = ''
servers{
}
'';
extraConfig = ''
(compress) {
encode gzip zstd
}
(headers) {
header {
# enable CORS
Access-Control-Allow-Origin "https://${config.networking.domain}"
# disable FLoC tracking
Permissions-Policy interest-cohort=()
# enable HSTS
Strict-Transport-Security max-age=31536000;
# disable clients from sniffing the media type
X-Content-Type-Options "nosniff"
# clickjacking protection
X-Frame-Options "DENY"
# enable XSS protection
X-XSS-Protection "1; mode=block"
# referrer policy
Referrer-Policy "strict-origin-when-cross-origin"
}
}
(common) {
import headers
import compress
}
'';
virtualHosts =
let
mkVHost =
{ subdomain, ... }@args:
lib.nameValuePair "${subdomain}.${domain}" (
lib.foldl lib.recursiveUpdate { } [
{
useACMEHost = domain;
extraConfig = ''
import common
${lib.optionalString (args.root != null) ''
root * ${args.root}
file_server
''}
${lib.optionalString (args.port != null) ''
reverse_proxy localhost:${toString args.port} {
# remove CORS headers from proxied server, because duplicate headers are not allowed
# remove after new release: https://github.com/navidrome/navidrome/commit/657fe11f5327ff7a3cb6aa9308b0bb7c71eea5c6
header_down -Access-Control-Allow-Origin
}
''}
${lib.optionalString (args.extraConfig != null) args.extraConfig}
'';
}
]
);
in
lib.listToAttrs (map mkVHost cfg.virtualHosts);
};
};
networking.firewall.allowedTCPPorts = [
80
443
];
};
}
+3
View File
@@ -0,0 +1,3 @@
final: prev: {
# homer = final.callPackage ./homer { };
}
+25
View File
@@ -0,0 +1,25 @@
hermes-env: ENC[AES256_GCM,data:du6bieWblEAhAYdZtCqnbNy0yWjFj13djw6co5nrNt1J8zWrmjdXo+UzvwKVpH5EMnxIEuzq9VrqsDPBLDAy1/SQRiWUuK5V7bRSVn88Hs/c7zJ1wo57i1DTVU5gwygzZHmEZbn6gKnFzNBx3aqZV+dLk3+Q4s6LpXbtRgZc63WeuMHqjlUBFeZoGIPZZ9FzqWjVx/FIgWRFm56utyBsnWhMK/2ZD2E6aOFNCZaXZp0QVXGRmiLtx7MLBY8oqJUBl16wytjOB8Y5khf5pJ5AV2NkXx9h27ycZbEQwouq1P6ty/OjQfmkZn7Jbt/oL9lG2FiOsR1x6f4bmVYrTrhjXmFoWvvDSlPJ7noBg2L+J0UrAZpDnOr2DCxZXlLnBCKaMdqvq27yTyATeWAW1BE5gCLlYnT5iE2vlr9GttY4LjfXW8HL3ieQ4bL/PB/PBZSZVKl5QrLfVrou1i8cxtM5cGfFKi9zCaDYI3G282961u1VHuVYVKZoU73WiR2BEuWqXn+1F3YOWEkOg7lM4xMoy9Q9qx7rSv3M/zOF7gjI0spCQv5RiuUOgoHbiERXgTAEfIaDRTYL30IqnHMS3O9SsTTINwDXBNk4cDxI2Ctyfy2IeAucBWAd4OhEqnji8bJ7tO4At8UIxjZ3oYGJiu0uqaDFS/Jdre4uYrSk,iv:CyO15YBh6BKNpZnsp+LAizND9rRgIjWj8SyT33Nii2c=,tag:WLlmMBIcvmYceSY4RXGUuw==,type:str]
sops:
age:
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4ZUk5cjhGTlVveUFRdHdS
elpVV0pZVU54U3plYjNBai9ZZVBxTDhyRFFnCmpGWW81VGJoMnd4eWxCWnovQnFF
WXR1MVBTZ3VRUnNMUjI5Rko4NHpBdjQKLS0tIDkxYWNxTmhUS2dYTkFnTWZXaS9V
ejdtSmF2M1ZVdFNiWENMWlNraUYxVzAKdlz4xLhTKjJfFESDcym2xCOa71rXj6BE
mdeaCDhE11CAXqrpKU+j0/kwTsZRzyqlwQsfeI4ratu1K1vY66ONsw==
-----END AGE ENCRYPTED FILE-----
recipient: age1a0v442ct2qfjcdhyktn25spw02yq4fl08plrrvuradwwxzsle4mq8thln4
- enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYT1hRSGt6TDRSMklQNDA3
Z2lYS2pMRWxxeEtGRGJSeXRDSlY5UTFBNEZvCm5RK0huT282MXpwWjdrTHByY3Ev
bTBMczR1MzVEcnNDVUxadlREbGg3UUUKLS0tIHhydUlmRExkbmNNNkVTcW1OSnpZ
aEtLZkRTRE9GZ0I1bEN0ZEl3RjVMeDgKoCY7NyxFMS+WuBUez2Irf91Xuy9QpM5g
RTmhK6mw2wd8oUMdgPOUESNwh1xkCz4hDHPrDQnfd4sPhSAvh28jwg==
-----END AGE ENCRYPTED FILE-----
recipient: age1n3uqtee5ymykrls6k04dznq50cwmj99hw7ny83yxjndt4y2ncf5qhjwa2p
lastmodified: "2026-09-10T06:28:38Z"
mac: ENC[AES256_GCM,data:t9oIZWrBtwkGyyb9hGlfWzAguI/xlqtRceTyyYE2y99xmGIl+pXHJkFF2pP9T09/NWHcXV0gRmV7LuKfF1zX1ZPNY7dba0AwVtU5ezoUdUuL2i47S7Ty1Onk9y6PqtgsRE1SkRAdzAr7s0csBzGXAn8I4z2rqZa43obRwtx7XV4=,iv:Lw6UmeeSNsfNftho423VM+htR0VzQFrCw3sDQ9OLQP0=,tag:SDQ0jJLf1jQIhBWPkm2WRg==,type:str]
unencrypted_suffix: _unencrypted
version: 3.13.3