diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f480c60 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# 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 # 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 + │ └── 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// + ├── configuration.nix # machine-specific NixOS settings + ├── environments.nix # enables profiles via my.profiles.* / my.hardware.* options + ├── disks.nix + └── hardware-configuration.nix +``` + +### Profile / Module Pattern + +Every module under `modules/` follows the same structure: + +```nix +{ config, lib, pkgs, ... }: +let cfg = config.my.profiles.; in +{ + options.my.profiles..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//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//default.nix` following the profile pattern above. +2. Add `./environments/` to `modules/environments/default.nix` (or the relevant `default.nix`). +3. Enable it in the target machine's `machines//environments.nix`. diff --git a/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md b/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md new file mode 100644 index 0000000..4f4af8c --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-zbt2-thread-otbr.md @@ -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 +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_-if00 -> ../../ttyACM0` + +- [ ] **Step 3: Record the by-id path** + +Capture the value `/dev/serial/by-id/usb-Nabu_Casa_Home_Assistant_Connect_ZBT-2_-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 = ""; + }; + +``` + +Replace `` 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_-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 +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 . The asset name will look like `ot-rcp-zbt-2-.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_-if00 \ +> flash --firmware ~/ot-rcp-zbt-2-.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 ` 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 "feat(home-assistant): enable ZHA for ZBT-2 Zigbee dongle" +4. `` — feat(home-assistant): import openthread-border-router module from unstable +5. `` — 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:** +- `` in Task 4 step 3 is intentional — it's a runtime parameter the operator fills in, captured in Task 3. +- ``, `` 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`). diff --git a/docs/superpowers/plans/2026-05-25-mibook-claude-code.md b/docs/superpowers/plans/2026-05-25-mibook-claude-code.md new file mode 100644 index 0000000..40e9ee3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-mibook-claude-code.md @@ -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-` + +- [ ] **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`. diff --git a/docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md b/docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md new file mode 100644 index 0000000..83cd144 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-zbt2-thread-otbr-design.md @@ -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_-... │ + │ │ │ + │ │ 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://?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_-..."; + # 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 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 `` 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_-...` 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_-... \ + flash --firmware ~/ot-rcp-zbt-2-.gbl +``` +Firmware download: latest ZBT-2 OpenThread RCP `.gbl` from . + +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. diff --git a/docs/superpowers/specs/2026-05-18-ha-push-notifications.md b/docs/superpowers/specs/2026-05-18-ha-push-notifications.md new file mode 100644 index 0000000..635ade8 --- /dev/null +++ b/docs/superpowers/specs/2026-05-18-ha-push-notifications.md @@ -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_` 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_` → 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_"; } + { service = "mobile_app_"; } + ]; + } + ]; + ``` + +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_` service + names from HA and update this spec + open Phase B PR. diff --git a/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md b/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md new file mode 100644 index 0000000..9e04ace --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-mibook-claude-code-design.md @@ -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` diff --git a/flake.lock b/flake.lock index fd055f3..da68131 100644 --- a/flake.lock +++ b/flake.lock @@ -21,11 +21,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1777988971, - "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { @@ -42,11 +42,11 @@ ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1778716662, + "narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb", "type": "github" }, "original": { @@ -69,11 +69,11 @@ ] }, "locked": { - "lastModified": 1776796298, - "narHash": "sha256-PcRvlWayisPSjd0UcRQbhG8Oqw78AcPE6x872cPRHN8=", + "lastModified": 1778507602, + "narHash": "sha256-kTwur1wV+01SdqskVMSo6JMEpg71ps3HpbFY2GsflKs=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad", + "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", "type": "github" }, "original": { @@ -92,11 +92,11 @@ "nixpkgs-regression": "nixpkgs-regression" }, "locked": { - "lastModified": 1778249748, - "narHash": "sha256-D+3kaW4NUUaiFTsEpmregtGZVniljBThg/O0JIjRL8k=", + "lastModified": 1780953488, + "narHash": "sha256-L6W9WD6QmaMHqZnO0OXO+B1bWvqBIkXcHQdvDfrL0ZI=", "owner": "NixOS", "repo": "nix", - "rev": "616df97974fd29b79f83502f63854d6d471ee055", + "rev": "5c38ac738636cf70537f02c30ab4ebdd0c7c4078", "type": "github" }, "original": { @@ -142,12 +142,15 @@ } }, "nixos-hardware": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, "locked": { - "lastModified": 1778143761, - "narHash": "sha256-lkesY6x2X2qxlqLM7CT2iM/0rP2JB7fruPN3h8POXmI=", + "lastModified": 1780310866, + "narHash": "sha256-fPBRVf6A5xlACYcOI59shGrjURuvwu0lRsDoSCEXt/I=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "3bcaa367d4c550d687a17ac792fd5cda214ee871", + "rev": "4ed851c979641e28597a05086332d75cdc9e395f", "type": "github" }, "original": { @@ -158,15 +161,15 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771903837, - "narHash": "sha256-jEA8WggGKtMFeNeCKq3NK8cLEjJmG6/RLUElYYbBZ0E=", - "rev": "e764fc9a405871f1f6ca3d1394fb422e0a0c3951", + "lastModified": 1780453794, + "narHash": "sha256-hhAl/iKiurXPn7rdzDgiSuRB8tqOB6f0buWkh8Y9mkY=", + "rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338", "type": "tarball", - "url": "https://releases.nixos.org/nixos/25.11/nixos-25.11.6495.e764fc9a4058/nixexprs.tar.xz" + "url": "https://releases.nixos.org/nixos/26.05/nixos-26.05.1183.6b316287bae2/nixexprs.tar.xz" }, "original": { "type": "tarball", - "url": "https://channels.nixos.org/nixos-25.11/nixexprs.tar.xz" + "url": "https://channels.nixos.org/nixos-26.05/nixexprs.tar.xz" } }, "nixpkgs-23-11": { @@ -218,11 +221,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1777954456, - "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "lastModified": 1780749050, + "narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "rev": "a799d3e3886da994fa307f817a6bc705ae538eeb", "type": "github" }, "original": { @@ -233,16 +236,29 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1778003029, - "narHash": "sha256-q/nkKLDtHIyLjZpKhWk3cSK5IYsFqtMd6UtXF3ddjgA=", + "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": 1780902259, + "narHash": "sha256-q8yYEC5f1mFlQO9RGna4LTc9QrcvWunX6FYp83munkQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "0c88e1f2bdb93d5999019e99cb0e61e1fe2af4c5", + "rev": "bd0ff2d3eac24699c3664d5966b9ef36f388e2ca", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-25.11", + "ref": "nixos-26.05", "type": "indirect" } }, @@ -252,7 +268,7 @@ "nix": "nix", "nixos-generators": "nixos-generators", "nixos-hardware": "nixos-hardware", - "nixpkgs": "nixpkgs_2", + "nixpkgs": "nixpkgs_3", "nixpkgs-unstable": "nixpkgs-unstable" } } diff --git a/flake.nix b/flake.nix index 8785dae..362e14f 100644 --- a/flake.nix +++ b/flake.nix @@ -3,7 +3,7 @@ inputs = { nix.url = "github:NixOS/nix"; - nixpkgs.url = "nixpkgs/nixos-25.11"; + 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"; @@ -11,7 +11,6 @@ url = "github:nix-community/nixos-generators"; inputs.nixpkgs.follows = "nixpkgs"; }; - # hyprland.url = "github:hyprwm/Hyprland"; }; diff --git a/machines/configuration.nix b/machines/configuration.nix index ac64523..c6af679 100644 --- a/machines/configuration.nix +++ b/machines/configuration.nix @@ -49,6 +49,7 @@ in # 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 @@ -56,6 +57,7 @@ in }; mibook = nixosSystem { system = "x86_64-linux"; + specialArgs = { inherit self; }; modules = defaultModules ++ [ # nixos-hardware.nixosModules.mibook ./mibook/configuration.nix diff --git a/machines/mibook/configuration.nix b/machines/mibook/configuration.nix index bfe3be6..c4d33e2 100644 --- a/machines/mibook/configuration.nix +++ b/machines/mibook/configuration.nix @@ -42,6 +42,8 @@ intelBusId = "PCI:00:2:0"; }; + services.openssh.enable = true; + system = { stateVersion = "23.05"; autoUpgrade.enable = true; diff --git a/machines/mibook/environments.nix b/machines/mibook/environments.nix index cd79646..8581e93 100644 --- a/machines/mibook/environments.nix +++ b/machines/mibook/environments.nix @@ -13,6 +13,7 @@ in }; development.enable = true; docker.enable = true; + claude-code.enable = true; }; my.hardware = { diff --git a/modules/environments/claude-code/default.nix b/modules/environments/claude-code/default.nix new file mode 100644 index 0000000..385b927 --- /dev/null +++ b/modules/environments/claude-code/default.nix @@ -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) + }; +} diff --git a/modules/environments/default.nix b/modules/environments/default.nix index 64ccba6..eb0d299 100644 --- a/modules/environments/default.nix +++ b/modules/environments/default.nix @@ -4,6 +4,7 @@ ./actual ./apps ./audiobookshelf + ./claude-code ./development ./home-assistant ./hyprland diff --git a/modules/environments/home-assistant/default.nix b/modules/environments/home-assistant/default.nix index e3e8fe4..07cbc0c 100644 --- a/modules/environments/home-assistant/default.nix +++ b/modules/environments/home-assistant/default.nix @@ -1,8 +1,9 @@ -# manages home automations +# manages home automations { config, lib, pkgs, + self, ... }: let @@ -10,6 +11,7 @@ let hostName = config.networking.hostName; in { + options.my.profiles.home-assistant = with lib; { enable = mkEnableOption "Home Automation"; @@ -24,12 +26,39 @@ in extraComponents = [ "matter" "mobile_app" + "otbr" + "thread" + "xiaomi_miio" ]; }; services.home-assistant.config = { - name = "Home - Rechberg"; - unit_system = "metric"; + 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 = [