Commit Graph
100 Commits
Author SHA1 Message Date
ryan4yin | 二花 3acb5d4ee5 fix(nfs,libvirt): restrict the golden export and bind VNC to loopback (#400)
* fix(nfs): restrict the golden export to its real clients

* fix(libvirt): bind VNC consoles to loopback

* fix(firewall): drop VNC ports on every host

* docs(security): record the NFS and VNC boundaries
2026-10-07 15:10:52 +08:00
ryan4yin | 二花 43f39e8e9f fix(k3s): deny credential-bearing host ports to pod sources (#399)
* fix(k3s): deny credential-bearing host ports to pod sources

* test: assert the k3s pod firewall drops sensitive ports

* docs(security): record the pod firewall boundary
2026-10-07 15:03:56 +08:00
ryan4yin | 二花 b79c5807ae ci: harden workflows and run the runtime firewall VM test (#398)
* ci: pin actions, scope tokens, run the runtime security VM test

* test: add a just recipe for the runtime firewall VM test
2026-10-07 14:57:34 +08:00
ryan4yin | 二花 5f6a1dadf4 docs: fix stale claims and AI-flavored prose (#397) 2026-10-07 14:48:36 +08:00
ryan4yin | 二花 76c4566e9b fix(k8s): pass inputs to the MicroVM guests (#396)
Host builds have been broken since #375 - function called without argument inputs
2026-10-07 13:20:08 +08:00
ryan4yin | 二花 95b849420e feat(gaming): declarative Proton and dGPU game rendering (#393) 2026-10-07 13:14:42 +08:00
ryan4yin | 二花 90bbe8bc4f fix(monitoring): stop recurring false alerts (#395)
The Telegram channel was dominated by alerts that can never be actionable:

- DriveTemperatureHigh/Critical did not filter temperature_type, so
  max(smartctl_device_temperature) picked the drive's rated
  op_limit_max/critical_limit_max instead of the actual reading. ai reported
  95 C and youko 130 C while the drives actually ran at 40-48 C.
- HostPhysicalComponentTooHot excluded label!="tctl", but the AMD k10temp
  hwmon sensor labels itself Tctl, so the CPU control temperature (up to
  ~100 C under load) tripped the alert on every AMD host.
- MicroVM guests share the host's read-only /nix/store, so their
  nix-gc.service always failed and tripped HostSystemdServiceCrashed on all
  six k3s guests. Disable automatic GC in the guest; the host's nix.gc
  reclaims the shared store.

The remaining k3s alert noise comes from workloads running on the masters
that belong on the workers; that is fixed in k8s-gitops, not by resizing
the control-plane VMs.
2026-10-07 12:51:30 +08:00
ryan4yin | 二花 e653ec3306 docs(hosts): add mixed-memory notes for idols-ai (#391)
Document the 4-DIMM kit mix (2×48G JUHOR + 2×16G GLOWY) on the Colorful
CVN Z890 ARK FROZEN V20:

- hardware (CPU, board, and both kits)
- the install order that trains (16G pair first)
- the only stable settings (XMP off, DDR5-4800 @ 1.25 V)
- the stress-test recipe and results
- the XMP @ 5600 failure and how to recover a board that will not POST
- re-enrolling the LUKS TPM unlock after a BIOS change
2026-10-06 21:39:23 +08:00
ryan4yin | 二花 dc65254511 fix(ssh): make sandboxed ssh work on Linux (#390) 2026-10-06 15:20:53 +08:00
ryan4yin | 二花 187577b2da docs(agents): fix the dsh private model route recipe (#389)
`dsh-llm-pi-ai` registers its built-in providers once, so a second instance
in the profile patch fails on mount with `provider "..." is already declared`.
Extend the base `llm-pi-ai` row instead.
2026-10-06 14:10:54 +08:00
ryan4yin | 二花 28e0b156ce refactor(agents): link dsh shared policy as a home-layer patch (#388)
dsh rewrites a profile's cordis.patch.yml at runtime, so linking the web
profile put GUI state in the checkout and let runtime writes clobber manual
edits. Link the stable policy as the $DSH_HOME home layer instead: dsh applies
it after every profile and only reads it.

The host-plane compaction-basic row is disabled on the web surface and each
agent preset owns its own copy, so the top-level override was inert. Drop it;
the model contextWindow/maxTokens are the effective knobs.

- add home/base/tui/agents/dsh/cordis.patch.yml, linked from default.nix
- drop the ~/.dsh/profiles/web link and the tracked web/ profile
- keep runtime state and private/local model routes in the profile patch
- update the eval test and README
2026-10-06 14:04:55 +08:00
ryan4yin | 二花 205f7c622b feat(agents): version the dsh web profile and add a proxied launcher #387 2026-10-06 00:54:30 +08:00
ryan4yin | 二花 85c51f5769 feat(rime): share the Rime config and user dictionary across Linux and macOS (#385)
* feat(rime): share the Rime config and user dictionary across Linux and macOS

Move the Rime data, preferences, and user dictionary into home/base/gui/rime so fcitx5-rime and Squirrel read the same files.

* feat(vinput): open the command palette with right Ctrl

Pin keyd's right-hand modifiers (rightcontrol/rightshift/rightmeta) to their own keycodes; keyd otherwise folds them into the left keys. Set Vinput's MenuKey to right Ctrl and clear CommandKeys, which would shadow it.

* feat(fcitx5): switch CN/EN with Ctrl+Space and right Shift

Clear AltTriggerKeys, whose Shift_L default toggled on left Shift. Left Shift is now a plain modifier and right Shift switches through the shared Rime config.
2026-10-05 22:43:20 +08:00
ryan4yin | 二花 0ec5b8032c fix(desktop): align Qt app fonts with GTK (#386)
* fix(desktop): align Qt app fonts with GTK

Qt apps kept their built-in default font ("Sans Serif" 9) while GTK apps use
`gtk-font-name` (Noto Sans 11), so Telegram and other Qt apps rendered
noticeably smaller than GTK apps like Firefox. Set
QT_QPA_PLATFORMTHEME=gtk3; the plugin ships with qtbase, so it needs no
extra package and also gives Qt apps GTK file dialogs.

* chore(desktop): drop unused Xft.dpi from the Wayland GTK profile

Xft.dpi/ *.dpi are X11 X-resource settings. This file is only imported by
the niri (Wayland) hosts, where xwayland-satellite sets its own Xft.dpi
(96 x output scale, currently 144), and the i3 hosts are headless 1080p
computer-use sessions that do not import it. So the setting had no effect
anywhere; remove it instead of keeping a misleading knob.
2026-10-05 20:01:51 +08:00
ryan4yin | 二花 6b8c5bfae4 feat(vinput): drop NPU/OpenVINO support (#384)
Vinput's streaming Zipformer models have unbounded dynamic shapes, so
OpenVINO cannot compile them for the GPU or the NPU; the `openvino`
provider and ai's Intel NPU enablement could never run on the NPU (noted
as unverified in WORKAROUNDS WA-017). Remove the backend option, the
daemon's OpenVINO library path, and the intel-npu module enable, and
update the eval test, README and WORKAROUNDS (drop WA-016 and WA-017).

Keep Vinput's stock package and its bundled sherpa-onnx (1.13.4): the
fork-based and nixpkgs-master sherpa overrides existed only to provide the
`openvino` provider, so drop package.nix with them.

Assisted-by: opencode (DeepSeek V4.1 Flash)
2026-10-05 15:42:48 +08:00
ryan4yin | 二花 50a4182898 feat(mihomo): show hostname in metacubexd page title (#383)
metacubexd hardcodes its tab title in the SPA, so dashboards on
different hosts are indistinguishable. Patch the static <title> and
re-apply the hostname from config.js, since Nuxt's useHead resets
document.title on every route change.
2026-10-05 10:05:12 +08:00
ryan4yin | 二花 b8348bf276 fix(mihomo): order the service after its config mount (#382)
The config deliberately stays out of the store, under the home directory, which is a persistent bind mount on tmpfs-root hosts. When the service and the mount are introduced in the same activation, mihomo can start before the mount is up and fail with "Failed to set up credentials: No such file or directory", making the switch report status 4. Add RequiresMountsFor so the mount is brought up and ordered before the service.
2026-10-05 01:08:08 +08:00
ryan4yin | 二花 502d0c85c7 fix(preservation): keep vinput config and models across reboots (#381)
Vinput stores its core config in ~/.config/vinput/config.json and its
downloaded sherpa-onnx models in ~/.local/share/vinput/models/. Neither
was bind-mounted from /persistent on the tmpfs-root hosts, so a reboot
wiped both and vinput-daemon started with "Local ASR provider model is
not configured". Persist both directories and document the one-shot CLI
model setup in the vinput README.
2026-10-05 00:18:01 +08:00
ryan4yin | 二花 8f0961d9c2 fix(desktop): tie ai's link DNS takeover to mihomo's lifecycle (#380)
* fix(desktop): tie ai's link DNS takeover to mihomo's lifecycle

sing-tun normally points systemd-resolved at the TUN DNS by running
resolvectl (dns/domain/default-route). Under nixpkgs' hardened
DynamicUser mihomo unit that hand-off fails silently: the unit's
RestrictAddressFamilies blocks resolvectl's D-Bus socket and resolve1
requires privileged authorization, so resolved keeps querying the
physical-link DNS, fake-ip is bypassed, and domain rules degrade to
MATCH.

Do the hand-off as '+' ExecStartPost/ExecStopPost hooks instead: '+'
commands run outside the sandbox (systemd.exec: address-family
restrictions do not apply to them) and ExecStopPost reverts even on
crashes, so a dead mihomo leaves DNS usable.

Signed-off-by: Ryan Yin <xiaoyin_c@qq.com>

* docs(mihomo): record sing-tun's resolvectl hand-off as the leak cause

Signed-off-by: Ryan Yin <xiaoyin_c@qq.com>

* refactor(desktop): keep mihomo only on ai and shoukei

ruby/kana never ran the core (service inactive live), and their
resolved + TUN combo would reintroduce the resolver leak once enabled.
Drop the enable and the explicit module import from computer-use; the
eval test now pins mihomo=false for both guests.

---------

Signed-off-by: Ryan Yin <xiaoyin_c@qq.com>
2026-10-05 00:07:32 +08:00
ryan4yin | 二花 bbfe1fa53b feat(desktop): replace Clash Verge with native mihomo on all NixOS hosts (#379) 2026-10-04 21:04:41 +08:00
ryan4yin | 二花 0a672060a4 feat(voice): add NPU voice input on ai (#377)
* feat(vinput): add NPU speech input on ai

* docs(agents): scope nixpkgs review guidance
2026-10-04 19:52:44 +08:00
ryan4yin | 二花 fdafca930b feat(agents): add dsh from nur-packages (#378) 2026-10-04 17:00:17 +08:00
ryan4yin | 二花 44454022a7 Merge pull request #376 from ryan4yin/refactor/rime-data-flypy-to-nur
refactor(fcitx5): ship flypy rime data from NUR
2026-10-04 13:38:40 +08:00
ryan4yin | 二花 cf6c657e4a refactor: consume computer-use packages from NUR (#375)
* refactor: consume computer-use packages from NUR

* fix: expose flake inputs to package overlays

Package overlays are imported from two places with different argument
shapes: outputs/default.nix passes a set containing `inputs`, while
modules/base/overlays.nix passes the NixOS module args, where genSpecialArgs
spreads each flake input as a top-level argument. The new NUR-backed
overlays read `inputs.nur-ryan4yin`, so evaluation failed on the module
path. Also expose the whole flake input set as `inputs` in genSpecialArgs.

* chore: bump nur-ryan4yin input

Update the locked nur-packages revision to the current main (nixpkgs
26.05, unused packages removed).
2026-10-04 13:11:34 +08:00
ryan4yin | 二花 2ee0b9c7eb chore(tui): remove standalone herdr, superseded by tuios (#374) 2026-10-04 11:23:16 +08:00
ryan4yin | 二花 6259359945 security(monitoring): bind exporters to static LAN addresses (#373)
node_exporter listened on 0.0.0.0, so access control rested entirely on
the nftables allowlist. Bind node/smartctl exporters to the host static
LAN IPv4 (the same address VictoriaMetrics scrapes), keeping the socket
off tailscale/bridge interfaces even if the firewall regresses.

Hosts without a static LAN address (shoukei, akane) now run no exporters
at all instead of loopback-bound ones nobody scrapes; akane, a UTM VM,
previously ran a loopback smartctl exporter with no physical disks.

LAN-bound exporters also get the boot-race treatment from #372
(network-online ordering plus patient restart); loopback-bound hosts
must not pull in network-online because NM wait-online would stall
their offline boots.
2026-10-03 19:07:21 +08:00
ryan4yin | 二花 1b31730db4 fix(monitoring): order smartctl exporter after network-online.target (#372)
The exporter binds to the host LAN address, which systemd-networkd
assigns during boot. Without ordering, the service starts before the
address exists, `bind: cannot assign requested address` trips the
start limit, and the unit stays failed until restarted manually.

Order the unit after network-online.target, and since wait-online is
best-effort (120s timeout), also retry patiently so the unit self-heals
when the link comes up late.
2026-10-03 18:54:08 +08:00
ryan4yin | 二花 3a5b9ec4c6 docs: README audit fixes and de-duplication (#371) 2026-10-03 18:28:58 +08:00
ryan4yin | 二花 6a1b4c699a chore(docs): prune stale TODOs (#370) 2026-10-03 17:50:28 +08:00
ryan4yin | 二花 37d79fbb08 chore(hardening): consolidate system hardening and fix stale docs (#368) 2026-10-03 17:11:51 +08:00
ryan4yin | 二花 afdaa3b869 refactor(agents): manage agent config in a home-manager module (#367) 2026-10-03 16:57:38 +08:00
ryan4yin | 二花 f03907c6a4 feat(security): add balanced kernel and exporter baseline (#365) 2026-10-03 16:33:50 +08:00
ryan4yin | 二花 3fdeedaa78 docs(security): document homelab architecture and agent-first audits
Add SECURITY.md covering the threat model, trust boundaries,
proportionate kernel/process hardening, the update/reboot routine and
remaining prioritized work. Record the dated read-only audit findings
(listeners, privileged workloads, effective RBAC, secret metadata and
sandbox/SSH boundaries) separately from planned controls and runtime
verification.

Define agent-first diagnosis: scoped root read-only access on homelab
hosts, and normal-user diagnostics on core desktops with no new sudo
grants. Add canonical domain-based k3s API endpoint regression tests,
record the unverified Darwin secret-ownership concern as WA-015, and
correct comments claiming mode 0000 blocks privileged root.
2026-10-03 16:26:32 +08:00
ryan4yin | 二花 d5fa86dbc7 fix(qemu-guest): let the NixOS test framework override serial-getty@ttyS0 (#362)
test-instrumentation.nix sets serial-getty@ttyS0.enable = false at normal priority to keep ttyS0 free for the test backdoor console, while qemu-guest.nix set it to true at normal priority. Two conflicting normal-priority definitions made `nix flake check` fail while evaluating packages.x86_64-linux.ruby-nixos-tests.

Use lib.mkDefault so the test framework wins in tests while ordinary QEMU guests still enable the serial getty.
2026-10-03 12:18:46 +08:00
ryan4yin | 二花 18925eac8b fix(colmena): expose colmenaHive for the nix-community evaluator (#361)
* fix(colmena): expose colmenaHive for the new evaluator

nixpkgs now ships the nix-community Colmena rewrite, whose default
evaluator reads an evaluated colmenaHive flake output instead of the raw
colmena hive. Without it every colmena apply recipe failed with
"does not provide attribute colmenaHive".

Add a colmena input pinned to the same v0.5.0 as nixpkgs and expose
colmenaHive = colmena.lib.makeHive <raw hive>, with the raw hive kept as
an internal binding rather than a flake output of its own.

* chore: clean up stale colmena and aquamarine references

- point the Colmena links at nix-community/colmena after the move
- rename the security-aquamarine-metrics test to youko-metrics (it
  already tested youko) and update the outputs README
- drop the commented aquamarine remote-builder example
2026-10-03 12:14:13 +08:00
ryan4yin | 二花 d056aece28 chore(workarounds): re-evaluate stale hardware/kernel fixes (#360)
* chore(workarounds): re-evaluate stale hardware/kernel fixes

Prune workarounds whose root cause is gone, fix one vendor typo, and
document the ones we keep:

- drop `transparent_hugepage=madvise` on shoryu/shushou/youko: it was tied to
  the old fixed 1G hugetlb pool, and the VMs now use ordinary memory
- shoryu: `hardware.cpu.intel.updateMicrocode` -> `amd` (it is an AMD APU)
- ai: drop `i915.enable_guc=3` (GuC/HuC submission is the default on modern
  i915/Xe)
- kernel-hardening: name the Dirty Frag CVEs (CVE-2026-43284 / CVE-2026-43500)
  and the re-evaluation condition
- document re-eval conditions for the Insta360 resume fix, the darwin
  auto-optimise-store workaround, and the youko USB-SATA bridge mitigations

* chore(workarounds): keep the Dirty Frag module blacklist as defence in depth

* chore(ai): correct the i915.enable_guc rationale (verified against the kernel)

* chore(darwin): drop the auto-optimise-store workaround (nix#7273 fixed)
2026-10-03 11:25:34 +08:00
ryan4yin | 二花 dc3ec8c159 feat(storage): relax ai NVMe workarounds and add drive SMART monitoring (#359)
* fix(ai): relax NVMe power-management workarounds and enable smartd

Logs show no NVMe errors, so allow APST again (capped at 100 ms), restore
the default io_timeout, and drop pcie_aspm=off. Add smartd for drive
health visibility.

* feat(storage): add drive/SSD SMART monitoring on physical hosts

node_exporter's nvme collector only exposes node_nvme_info and namespace
capacity, not SMART health, so SSD wear/lifespan and drive temperature were
not observable anywhere.

- enable services.prometheus.exporters.smartctl on physical hosts, gated on
  modules.hardwareTools (off on QEMU/MicroVM guests), bound to the host's LAN
  address on port 9633
- firewall: drop 9633 from anything but the monitoring host
- youko: scrape smartctl-<host> for the physical hosts
- alert rules (storage.yml): SMART health, NVMe wear, available spare, media
  errors, critical warning, and drive temperature (warn 70 / crit 80 C)
- Grafana dashboard "Drive SMART (smartctl)"
2026-10-03 11:13:46 +08:00
ryan4yin | 二花 8bee7d93a7 chore: bump agenix to 0.18.0 and follow nix-darwin/home-manager (#358)
agenix was pinned to a May 2025 revision, so `just up` could never move
it. Move to the 0.18.0 release tag and collapse its duplicate
nix-darwin/home-manager lock nodes via follows.
2026-10-03 10:40:48 +08:00
ryan4yin | 二花 51261645ca chore: small audit cleanups (#357)
Remove a dead .gitattributes rule, bump the mirror workflow's checkout,
document the daemon test settings, drop unused JACK, and align the
installer's disko version.
2026-10-03 10:32:08 +08:00
ryan4yin | 二花 885dabb49d refactor: scope host tools, locate and mDNS by purpose (#356) 2026-10-03 10:23:21 +08:00
ryan4yin | 二花 ce416240b2 fix: avoid mkForce for allowUnfree and boot timeout (#355)
Let hosts override these again and drop the forced priorities.
2026-10-03 10:14:50 +08:00
ryan4yin | 二花 2506e0e462 refactor: use cached nixpkgs for hugo and the rust toolchain (#354)
Drop the pkgs-master variants so a master bump no longer forces a local
build of these tools.
2026-10-03 10:13:00 +08:00
ryan4yin | 二花 2bacb5c138 refactor: drop redundant package declarations (#353)
Remove no-op overrides and packages already provided elsewhere in the
same profile.
2026-10-03 10:11:30 +08:00
ryan4yin | 二花 8b37fc0a7d chore: remove dead configuration (#352)
Drop declarations that nothing references, including the incorrect
`attrs.listToAttrs = lib.genAttrs` alias.
2026-10-03 10:09:38 +08:00
ryan4yin | 二花 a902678522 docs(flake): document currently-unused inputs (#351)
Three inputs are intentionally present but not consumed by any package
today. Document each so a future reader knows why they are kept.
2026-10-03 10:07:57 +08:00
ryan4yin | 二花 dafd16f447 fix(ci): fail the eval-test run when the suite does not pass (#350)
* fix(ci): fail the eval-test run when the suite does not pass

`nix eval .#evalTests` prints the suite result but exits 0 either way, so
both `just test` and the CI workflow reported success even when a test
failed. Compare the raw boolean and fail instead.

* fix(ci): use --json for the boolean eval result

`nix eval --raw` cannot coerce a Boolean to a string, and Just runs each
recipe line as a separate `nu -c`, so the result variable must stay on one
line.
2026-10-03 10:06:22 +08:00
ryan4yin | 二花 42641e2fdf feat!: replace zellij with tuios, default to ghostty + kitty (#349)
* feat!: replace zellij with tuios and switch to ghostty + kitty

Terminal environment overhaul:

- add the tuios flake input (upstream 0.8.5; nixpkgs has 0.8.1) and link
  the checked-in config out-of-store; auto-start tuios in nushell instead
  of zellij
- remove zellij entirely: both modules, config, README and nushell
  completions
- remove foot and alacritty, which lack the Kitty graphics protocol;
2026-10-03 02:34:31 +08:00
ryan4yin | 二花 9254b63a18 chore(idols-ai): move tmp state off RAM onto btrfs and harden tmp mounts (#348)
* feat(idols-ai): persist /var/tmp and small state dirs, wipe /var/tmp at boot

Move the strata model staging area (~3G) off the root tmpfs onto the
persistent btrfs volume to free RAM, preserve btrbk/cups/logrotate
state, and wipe /var/tmp at boot so it only accumulates between
reboots.

* chore(idols-ai): add nosuid,nodev to root tmpfs and tmp btrfs mounts

Hardens /, /tmp and /snapshots; safe because /dev, /run, /nix and
/run/setuid-programs are separate mounts, so nothing there needs suid
bits or device nodes.

* chore(idols-ai): add noatime to the @tmp btrfs mount

Temp files do not need access times; matches the @nix subvolume style
and saves btrfs metadata writes.

* chore(idols-ai): harden the /var/tmp bind mount with nosuid,nodev

World-writable staging data should not honor suid bits or device
nodes, same as /tmp and /snapshots.

* fix(idols-ai): preserve logrotate state as a file, fix persisted dir ownership

- /var/lib/logrotate.status is a regular file, not a directory: move it from
  preservation directories to files. A missing state entry makes logrotate
  treat every log as freshly rotated, so it must survive reboots.
- keep /var/lib/btrbk owned by btrbk:btrbk 0750 so preservation does not
  override the btrbk module's rule with root:root.
- set /var/tmp mode 1777 explicitly instead of relying on systemd's tmp.conf
  ordering.
- rewrite comments to describe the paths generically.

* chore(idols-ai): cap journal, wipe coredumps at boot, persist upower/noctalia

- services.journald.settings.Journal: SystemMaxUse=10G, RuntimeMaxUse=256M,
  matching the cap already set on idols-kana/idols-ruby.
- wipe /var/lib/systemd/coredump at boot: it lives in the persisted
  /var/lib/systemd and crash dumps can contain secrets copied from memory.
- persist /var/lib/upower (device battery history) and
  ~/.local/state/noctalia (shell settings, notification/clipboard history).
2026-10-02 21:58:21 +08:00
ryan4yin | 二花 a958ddf8ed refactor(secrets): drop the redundant work gitconfig from core (#347) 2026-10-02 13:53:15 +08:00
ryan4yin | 二花 0c8f0fe0c3 feat(secrets): add work gitconfig secret and a no-sudo replace flow (#346)
* feat(secrets): add work gitconfig secret, symlink it in home-manager

* feat(secrets): add a no-sudo replace recipe for new secrets

* refactor(secrets): move work gitconfig symlink into the desktop layer
2026-10-02 13:51:17 +08:00
ryan4yin | 二花 9c44117f75 refactor(editors): standardize on helix, keep neovim as backup (#345) 2026-10-02 13:33:17 +08:00
ryan4yin | 二花 1a10e65221 refactor(secrets): follow the reorganized nix-secrets layout (#344) 2026-10-02 13:07:40 +08:00
ryan4yin | 二花 969a20ac7b docs: refresh repository images (#343) 2026-10-02 12:30:53 +08:00
ryan4yin | 二花 51e86d1168 feat(agents): add project-scoped Grafana MCP for opencode and codex (#339) 2026-10-02 12:01:01 +08:00
ryan4yin | 二花 c913813ac6 refactor(shells): let nushell own PATH and aliases (#342) 2026-10-02 11:49:56 +08:00
ryan4yin | 二花 3a0afe5499 chore(youko): drop stale KubeVirt references (#341)
KubeVirt is gone: remove its Grafana dashboard and the commented
Homepage service entry.
2026-10-02 11:16:45 +08:00
ryan4yin | 二花 2b0f01d99c chore(agents): workspace layout, related repositories, and ~/src checkouts (#340)
* docs(skills): point nix-config-secrets at the private repo under ~/codes

Record where the private nix-secrets checkout lives and that agents should
do the mechanical secret work themselves, handing back only the
interactive/sudo agenix operations and pushes.

* docs(agents): add related repositories to nix-config

* chore(agents): adopt ~/src for source checkouts

Move third-party/source checkouts from ~/codes/src to ~/src so the home
layout reads as three siblings: ~/codes (personal), ~/work (work),
~/src (checkouts). Preserve and back up the new path, and update the
global rule and the two nixpkgs skill references.

* docs(agents): tighten the code layout rule

* docs(agents): fold the work-confidentiality note into the layout line

* docs(agents): base the confidentiality rule on repo visibility

* docs(agents): drop the stale KubeVirt mention
2026-10-02 11:16:24 +08:00
ryan4yin | 二花 68ea04f173 fix(docs): correct stale and mismatched documentation (#338) 2026-10-02 10:21:55 +08:00
ryan4yin | 二花 b2ec56db66 docs(skills): add repo-scoped task skills and give each fact one home (#337) 2026-10-02 10:13:03 +08:00
ryan4yin | 二花 739cd171ba feat(neovim): replace nixvim with programs.neovim (#336)
nixvim has been disabled for a long time (programs.nixvim.enable = false), so
drop the flake input (and prune flake.lock). Port the settings it carried to
Home Manager's programs.neovim: opts and per-plugin setup become an initLua
block plus a plugin list. The catppuccin module already provides the
colorscheme, vi stays unmapped, and the plain system neovim package is
untouched so sudo/nvim --clean keep using it.
2026-10-01 21:19:18 +08:00
ryan4yin | 二花 4909f6357b security: scope privileges and stop a world-readable secret copy (#335) 2026-10-01 21:18:34 +08:00
ryan4yin | 二花 d690f45183 feat: add CanoKey hardware security key support (#334) 2026-10-01 20:50:13 +08:00
ryan4yin | 二花 ab2fd0fee9 feat: add plocate filesystem search and cap podman log size (#333) 2026-10-01 20:00:53 +08:00
ryan4yin | 二花 8f9fdda2bb refactor: trim unused virtualisation config and packages (#332) 2026-10-01 19:42:20 +08:00
ryan4yin | 二花 111929023d docs(agents): clarify tool selection and authorization boundaries (#331) 2026-09-30 20:51:42 +08:00
ryan4yin | 二花 9ab5eca60c feat(monitoring): GPU-aware btop variants and GPU metrics (#330) 2026-09-30 00:16:50 +08:00
ryan4yin | 二花 523f4940a7 Merge pull request #329 from ryan4yin/youko-shushou-ssd-swap
hosts: swap youko/shushou roles to fix Jellyfin HDR transcoding
2026-09-29 22:02:11 +08:00
ryan4yin | 二花 8ef52e2cf7 fix(youko): add ROCm OpenCL so Jellyfin can tone-map HDR (#328)
* fix(youko): add ROCm OpenCL so Jellyfin can tone-map HDR

Jellyfin's HDR/DoVi transcodes use ffmpeg's `tonemap_opencl`, but only
Mesa was installed via `hardware.graphics.enable`, so no OpenCL ICD was
registered. ffmpeg then failed to create an OpenCL device ("Failed to
get number of OpenCL platforms: -1001") and aborted with exit code 237,
leaving HDR titles unplayable.

Add `rocmPackages.clr.icd` to `hardware.graphics.extraPackages`; it
ships the ICD for the Barceló iGPU (reported as `gfx90c`) under
/run/opengl-driver/etc/OpenCL/vendors, where `ocl-icd` (built with
`--enable-custom-vendordir`) finds it.

The Barceló iGPU only manages ~0.8x realtime for a 4K HDR -> 1080p
tone-mapped transcode, so playback can still buffer; this only makes
such titles playable at all.

* fix(youko): allow /dev/kfd so Jellyfin OpenCL can reach the GPU

The jellyfin module's `DeviceAllow` whitelist only permits the VA-API
render node (`/dev/dri/renderD128`). ROCm's OpenCL runtime opens
`/dev/kfd`, so even with the ICD installed OpenCL device creation failed
("No matching devices found") and HDR transcodes still aborted with exit
code 237.

Add `/dev/kfd rw` to the whitelist; list definitions merge with the
module's entry. Verified under the unit's sandbox (PrivateUsers,
PrivateTmp) that OpenCL init then succeeds as the jellyfin user.
2026-09-29 19:52:27 +08:00
ryan4yin | 二花 c1b590b31b chore(codex): use upstream 0.157.1 and drop the temporary patch (#327) 2026-09-26 23:24:39 +08:00
ryan4yin | 二花 1256570a84 feat(youko): firewall and tune transmission, stop homepage RPC probes (#326)
* fix(youko): stop homepage from probing the authenticated transmission RPC

The siteMonitor hit /transmission/rpc unauthenticated, so every probe returned 401 and counted against transmission's anti-brute-force threshold.

* feat(youko): tune transmission and firewall its network namespace

Raise the upload limit and queues, relax required encryption to preferred, and add a default-drop nftables input chain inside the netns so the peer port stays open while everything else is closed. Allowlist the nftables 'iif' keyword for typos.

* feat(youko): export transmission metrics and add a Grafana dashboard

nixpkgs has no transmission exporter, so add a small stdlib Python exporter that queries the authenticated netns RPC on loopback, scrape it from VictoriaMetrics, and ship a Transmission dashboard.

* fix(youko): cap transmission upload at 2500 kB/s

30 MB/s ignored the 50 Mbps uplink; keep seeding around 20 Mbps.

* Revert "feat(youko): export transmission metrics and add a Grafana dashboard"

This reverts commit 9df7a26526.
2026-09-26 22:40:02 +08:00
ryan4yin | 二花 e5b95affa6 feat(nixos): default-deny host inbound, trust the LAN/tailnet (#325)
Hosts used to trust the router as the only boundary (servers ran with the
firewall off; service ports were opened globally), so any LAN device could
reach every host service. Move filtering to the hosts as defence in depth
behind the router, which keeps its firewall on and uses a port mapping for
the few ports we actually need.

- base: nftables backend; deny inbound by default; trust the home LAN by
  source and the tailnet by interface (restrict the tailnet with ACLs);
  suppress per-service `openFirewall` global ports; allow node_exporter only
  from the monitoring host. The local podman bridge, the tailnet and the pod
  CIDRs are accepted only after the node_exporter drop.
- servers: stop disabling the firewall; drop redundant global port opens
  (caddy 80/443, NFS 2049, Jellyfin, localsend/test ports); delete the
  redundant desktop firewall module; aarch64/riscv64 import the firewall
  explicitly (akane was missing it).
- k3s: Cilium pods (10.0.0.0/8, fd00::/8) may reach host services; drop the
  flannel-era cni0/flannel.1 trust.
- youko: postgres listens on localhost + the LAN with pg_hba limited to the
  LAN; the NFS export stays LAN-scoped; remove the unauthenticated v2ray
  proxy.
- tests: `security-firewall` asserts every host runs the shared firewall, has
  the LAN CIDR rule and no global ports.
2026-09-26 14:02:03 +08:00
ryan4yin | 二花 2314754655 fix(youko): run transmission in its own network namespace (#324) 2026-09-26 12:48:51 +08:00
ryan4yin | 二花 f326dcb139 docs(agents): Simplifies global rules so agents can understand and follow it easily (#323) 2026-09-26 09:51:42 +08:00
ryan4yin | 二花 9fe6166fd1 feat(homepage): add Jellyfin, alerting, files and k3s-test-1 services (#322) 2026-09-26 00:15:06 +08:00
ryan4yin | 二花 a111676ede feat(youko): add Jellyfin media server (#321)
* feat(youko): add Jellyfin media server

Jellyfin reads the existing Transmission download tree on the public HDD
(via the shared `fileshare` group) and transcodes with the AMD Barceló
iGPU through VA-API. Encoding settings are declared in Nix
(`forceEncodingConfig`), so the web UI cannot silently drift.

Replace the per-service public ACME certificates with a single
`*.writefor.fun` wildcard (plus the apex) shared by every public vhost,
so adding a service needs no new ACME entry; a wildcard also keeps
individual hostnames out of public CT logs.

Pin the jellyfin uid/gid to avoid the dynamic-id drift fixed for the
other services in #320.

* fix(youko): enable graphics for Jellyfin VA-API

VA-API needs Mesa's radeonsi VA driver under /run/opengl-driver, but
youko is headless so hardware.graphics was off. Without it Jellyfin's
ffmpeg fails with 'va_openDriver() returns -1'. Enable it so the iGPU
can be used for hardware transcoding.

* fix(youko): use VA-API hardware encoding for Jellyfin

Enabling hardware decoding alone left encoding on the CPU (libx264), which
pins ~3 cores per 720p stream. Set transcoding.enableHardwareEncoding so
the iGPU (Vega VCN) does the H.264 encode too.

* feat(youko): harden Jellyfin proxy trust, groups, and HEVC encode

Add jellyfin to the render/video groups for robust VA-API access (was
relying on the world-writable render node), enable HEVC hardware encoding
(Vega's VCN supports it; AV1 does not), and patch network.xml on start to
trust Caddy as KnownProxies and advertise the public URL.

* fix(youko): run Jellyfin network.xml patch every start; allow LAN discovery

The module's preStart exits early once encoding.xml matches, so the
mkAfter network.xml patch only ran when encoding settings changed. Run it
with mkBefore instead. Also allow Jellyfin app auto-discovery (UDP 7359 /
TCP 8096) from the home network only.
2026-09-25 23:22:04 +08:00
ryan4yin | 二花 9187e4d920 fix(youko): pin dynamically-allocated service uid/gid (#320)
Service users/groups get a dynamically allocated uid/gid, which shifts when the user set changes: adding `immich` moved gitea 993:985 -> 996:993 and rustfs 987:977 -> 988:983. Files on the persistent volumes keep the old numeric owner, so gitea and rustfs lost access to their data and failed to start after a restart. Pin every dynamically allocated service id on youko to its current value. The ids are host-specific, so this is a host file.
2026-09-25 20:55:02 +08:00
ryan4yin | 二花 5ee8b728b5 fix(restic): use the correct systemd timer option names (#319)
timerConfig was set with lowercase onCalendar/randomizedDelaySec, which systemd treats as unknown directives. The generated timer had no schedule, so `systemctl list-timers` showed NEXT=- and no backup ever ran. Use the real keys OnCalendar/RandomizedDelaySec.
2026-09-25 20:39:59 +08:00
ryan4yin | 二花 4bd463a784 chore(eval): resolve catppuccin and rust-overlay deprecation warnings (#318) 2026-09-25 20:36:26 +08:00
ryan4yin | 二花 793b636fa5 feat(youko): add Immich photo platform (#317)
Add Immich via the native nixpkgs services.immich module on youko, reusing the host PostgreSQL 16 (pgvector + vectorchord) and a shared Valkey instance for Redis-compatible caching and BullMQ queues (Dragonfly rejected BullMQ's dynamic Lua keys). Media lives on the encrypted HDD at /data/apps/immich; Caddy publishes immich.writefor.fun with a publicly-trusted Let's Encrypt cert (security.acme + Cloudflare DNS-01) so the mobile app connects. OTEL metrics are scraped by VictoriaMetrics, and /var/cache is now preserved on the tmpfs-root hosts so the ML model cache survives reboots.
2026-09-25 20:30:01 +08:00
ryan4yin | 二花 6a6950e7eb fix(youko): don't redirect rustfs console STS/S3 calls (#316)
The s3-console Caddy vhost redirected every request to / to /rustfs/console/. The RustFS console frontend signs its own S3/STS requests to / (POST / for STS, GET /?x-id=ListBuckets), so the 302 broke the console session and login failed server-side with SignatureDoesNotMatch ("missing signed header: content-type"). Restrict the redirect to unsigned browser GET /; signed requests now reach RustFS unchanged.
2026-09-25 16:21:12 +08:00
ryan4yin | 二花 7d8d6ec8ad chore(youko): drop redundant loki-gateway Caddy vhost (#315) 2026-09-25 16:09:44 +08:00
ryan4yin | 二花 069e081fec fix(k3s): bind control-plane metrics to node addresses (#311)
* fix(k3s): bind control-plane metrics to node addresses

* docs(microvm): document remote deployment workflow

* fix(microvm): deploy runner before guest switch

* docs(microvm): standardize on just deployment commands

* docs(k3s): explain control-plane metrics address

* docs(hosts): move generic build commands to host guide

* docs: simplify command operation guidance
2026-09-24 11:13:59 +08:00
ryan4yin | 二花 3b13291216 feat(computer-use): sync the X clipboard and document the input-level design (#308)
- run two autocutsel instances so the X PRIMARY/CLIPBOARD selections stay in
  sync, making copy/paste work in the session and through VNC
- drop --disable-blink-features=AutomationControlled: nothing drives the
  browser over CDP, so navigator.webdriver is already false and the flag only
  marked the setup as automated
- document why the driver stays on real X11/AT-SPI input and is never granted
  a CDP attach, and note the SSH-tunnel path for manual VNC access
- extend the computer-use eval test
2026-09-22 20:45:09 +08:00
ryan4yin | 二花 b19818ef44 feat(k8s): add the k3s-test-1 worker nodes (#307)
Add worker-1 (4 vCPU / 16 GiB) on shoryu, and worker-2 (4 vCPU / 16 GiB) plus worker-3 (2 vCPU / 8 GiB) on shushou, as microVMs. They come with a genK3sAgentModule and the per-host/output/home-manager wiring, and are labelled node-role.kubernetes.io/worker=true. The masters keep their control-plane:NoSchedule taint, so workloads land on the workers.
2026-09-21 23:09:13 +08:00
ryan4yin | 二花 ca00d26642 chore: drop the unused darwin proxy set script (#306)
The script pointed the macOS nix-daemon at the homelab v2ray HTTP proxy (192.168.5.101:7890), but that proxy was never running. Remove it together with the just darwin-set-proxy recipe and the README step that referenced it.
2026-09-21 22:33:33 +08:00
ryan4yin | 二花 242c607ce6 feat(k8s): reserve a VIP block and narrow the tailscale routes (#305)
Reserve 192.168.5.192/27 as a dedicated kube-vip block, outside the router's DHCP pool (.2-.99): one /29 per cluster, k3s-test-1 gets .192/29 with the API VIP at .193.

Advertise only youko itself (.183/32) and that block over Tailscale, instead of the whole /24. The /24 route shadowed hosts that are already reachable directly, e.g. idols-ai at 192.168.5.100.

Deploy prerequisites (not in this repo): approve the new Tailscale routes; point kube-vip at the new VIP; move the *.writefor.fun wildcard from .101 to .183 and update scripts/darwin_set_proxy.py, since .101 is no longer advertised.
2026-09-21 22:28:36 +08:00
ryan4yin | 二花 97c2d1325d docs: refresh stale docs after the VM and backup refactors 2026-09-21 22:03:21 +08:00
ryan4yin | 二花 ef00eb31e0 docs: warn that user-readable decrypted secrets must not be read (#303)
Only three secrets are readable as the login user (nix-access-tokens,
ssh-key-romantic and the nushell one); everything else is root-only and
the filesystem already keeps agents out.
2026-09-21 21:18:06 +08:00
ryan4yin | 二花 260da1ee20 chore: rename the nushell secret, and forbid reading decrypted secrets 2026-09-21 21:13:50 +08:00
ryan4yin | 二花 bc6748d021 feat(backup): notify on failure, and 404 unconfigured hostnames (#301)
* feat(backup): notify on failure, and 404 unconfigured hostnames

A failed backup was silent. Post to alertmanager (which routes above-info
to telegram), and have caddy answer 404 instead of its empty 200 for
*.writefor.fun names that are not services.

* docs(backup): say 'cloud copy' rather than 'offsite'

Clearer for a reader who does not live in backup jargon; the requirement
(not in the homelab, immutable) is spelled out in the text.

* docs(secrets): state the recipient invariant

A recipient set must answer two questions: who may read it, and how it is
recovered once those hosts are gone. Dropping recovery_key while narrowing
a secret is easy and silent.

* docs(secrets): state that every secret must be decryptable by the desktops

The desktops hold the trusted admin keys and are where secrets are edited and
rekeyed from; narrowing a set to the servers alone breaks agenix -r and drops
the offline recovery key.

* docs(secrets): desktops and recovery_key have identical access

recovery_key is a member of desktop_keys, so every group written as
desktop_keys ++ ... covers both.

* feat(monitoring): scrape the restic rest-server

Expose /metrics on the loopback-bound server, scrape it with
victoriametrics, and block /metrics on the public vhost.

* docs(backup): record what the metrics actually expose

rest-server only ships the go/promhttp defaults, so repository growth is
charted from node-exporter's filesystem metrics instead.
2026-09-21 21:01:18 +08:00
ryan4yin | 二花 e37042f61c feat(youko): restic backup of the service state to the HDD (part 1) (#299)
* feat(hosts): per-host restic backups to the SMB remote

Replace the disabled aggregate job (which rsynced every host's /persistent
into /tmp - a tmpfs - and silently produced incomplete backups) with one
restic repo per host, backing up its own data directly:

- shared module lib/genResticBackup.nix: secrets, rclone/SMB target
  (rclone:smb-downloads:/Downloads/homelab-backup/<host>), retention, and
  excludes for the regenerable/bulky paths (podman overlay layers, microvm
  and libvirt disk images, nfs golden images, logs, caches)
- youko additionally backs up the service state under /persistent/var/lib
  and dumps all postgres databases first (a consistent SQL artifact beside
  the crash-consistent data dir)
- shoryu/shushou back up /etc/agenix, /etc/ssh and the k3s token

Needs three agenix secrets before it can run: restic-password,
ssh-key-for-restic-backup and rclone-conf-for-restic-backup.

* refactor(hosts): only restic does off-host; btrbk stays local

btrbk works at the subvolume level, so a  would ship the whole
@persistent - including podman's overlay storage, the microVM/libvirt
images and the nfs golden images - which is exactly the regenerable bulk
we want out of the backups.

So: btrbk keeps making the cheap local CoW snapshots (quick rollbacks),
and restic owns the off-host copies with file-level excludes. Back up the
whole /persistent minus those excludes, so nothing is missed.

Update the btrbk module's comment to say so.

* fix(restic): never back up keys; drop the implicit /etc/agenix

restic's own password lives in /etc/agenix, so backing that up would store
the repository's password inside the repository. Exclude agenix, the ssh
host keys and the usual credential dirs (.ssh, .gnupg, .aws, gcloud).

* refactor(restic): make the backup a proper module

Replace lib/genResticBackup.nix with modules/nixos/base/restic-backup.nix,
which declares modules.restic-backup options (enable, repository, paths,
exclude, requiresMountsFor, postgresDump) like the btrbk module does.
youko just sets the options now.

* refactor(restic): exclude the whole podman storage, trim retention

Exclude /persistent/var/lib/containers wholesale: the image layers are
re-pullable and podman's storage DB is not consistent when copied from a
running podman. uptime-kuma's named volume lives there, but its data is
not wanted anyway.

Retention: daily 3, weekly 2, monthly 2 - no yearly.

* refactor(restic): back up a short-lived btrfs snapshot, drop pg_dump

A live copy of a running postgres data dir is only crash-consistent. Instead
of a daily pg_dumpall (which duplicates the data in the repository and gets
slower as the DB grows), take a short-lived read-only btrfs snapshot in
backupPrepareCommand, back that up, and delete it right after - a
consistency step, not a second backup. The excludes are relative now so they
work for both the snapshot and a direct path.

* feat(secrets): restic-password for modules.restic-backup

With the rekeyed secret in nix-secrets (storage group, alongside
hdd-luks-crypt-key), the module's passwordFile resolves.

* fix(restic): agenix's secret is under /run/agenix, not /etc/agenix

* fix(restic): take the password path from agenix, not a hardcoded one

config.age.secrets."restic-password".path follows agenix's secretsDir, so it
stays correct if that changes.
2026-09-21 19:00:35 +08:00
ryan4yin | 二花 bb33cca83a refactor(youko): let preservation own the service state dirs (#298)
* refactor(youko): let preservation own the service state dirs

The services' data now sits on the SSD, so the hand-written dataDir +
systemd.tmpfiles rules are unnecessary: /var/lib is already preserved by
preservation, and the upstream modules create and own their default
state dirs (StateDirectory). Use the defaults:

- gitea /var/lib/gitea, grafana, sftpgo, caddy, victoriametrics,
  postgresql (/var/lib/postgresql/<version>), uptime-kuma, homepage
- drop the per-service tmpfiles rules and victoriametrics' self-bind

Data was moved in place with mv (same filesystem): /persistent/apps/<svc>
(== /var/lib/<svc> via the preservation bind) so nothing was copied.

* refactor(youko): drop caddy's redundant tmpfiles rules

The activation script already creates the fileserver tree with mkdir -p and
the module creates caddy's dataDir, so the rules - and the unused vms
directory they created - are redundant.

* refactor(youko): use a podman named volume for uptime-kuma

podman creates the volume and takes the ownership from the image, so the
kuma user, the tmpfiles rule and the host directory are no longer needed;
the volume lives under /var/lib/containers, which preservation keeps.
2026-09-21 18:06:40 +08:00
ryan4yin | 二花 9ef2fefc25 Merge pull request #295 from ryan4yin/fix/homepage-allowed-hosts
fix(homepage): allow the home.writefor.fun host
2026-09-21 17:37:35 +08:00
ryan4yin | 二花 621da40081 Merge pull request #293 from ryan4yin/feat/genpass-nu-command
feat(shells): add genpass nu command to generate random passwords
2026-09-21 17:14:48 +08:00
ryan4yin | 二花 787b51df25 feat(vms): migrate the idol desktops from KubeVirt to libvirt (#287)
* wip(kubevirt-shoryu): enable libvirt/qemu

* feat(kubevirt-shoryu): enable libvirt/qemu with br0 + no NAT network

* chore: lock the microvm flake input

* feat(kubevirt-shoryu): define the libvirt domains declaratively

* fix(kubevirt-shoryu): pin the kana domain UUID

* feat(kubevirt-shoryu): migrate idols-ruby to libvirt
2026-09-21 15:12:37 +08:00
ryan4yin | 二花 3753396a01 docs(k8s): note the flaky USB bridge and the aquamarine microVM plan (#286) 2026-09-21 13:46:39 +08:00
ryan4yin | 二花 7cedea6161 feat(upload-vm): write the image into the VM's own disk (#285)
The VMs now declare their root disk as a DataVolume with `source: upload`, so
upload straight into `<vm>-disk` (CDI only accepts an upload into a DataVolume)
instead of a shared golden PVC. Takes the disk size as an argument.
2026-09-21 13:42:54 +08:00
ryan4yin | 二花 d40ed42f6c feat(k3s-test-1): add three worker nodes and taint the control plane (#283)
* feat(k3s-test-1): add three worker nodes and taint the control plane

- hosts/k8s/k3s-test-1-worker-{1,2,3} (k3s agents) + flake outputs
- vars/networking: .117/.118/.119 (the .111-.113 range is still held by the
  never-deployed k3s-prod-1 workers)
- k3s-test-1-master-{1,2,3}: nodeTaints=node-role.kubernetes.io/control-plane:NoSchedule
- Justfile: upload the worker images too

* chore(k3s-test-1): move the workers to .111-.113

The k3s-prod-1 entries that held those addresses are gone (nix-config#284).
2026-09-21 12:56:49 +08:00
ryan4yin | 二花 01c6b3290a chore: remove the never-deployed k3s-prod-1 cluster (#284)
It was never bootstrapped (no flux deploy key, VMs commented out since
2024-10) and only carried a stale duplicate of the test cluster's config.

- hosts/k8s/k3s-prod-1-{master,worker}-{1,2,3} + flake outputs + the home module
- the k3s-prod-1-token agenix entry (the .age file lives in the private secrets repo)
- the k3s-prod-1-* IPs, the offlineHosts entries, the Justfile recipes and the docs
2026-09-21 12:53:43 +08:00
ryan4yin | 二花 a4609501c4 feat(kubevirt-youko): serve VM golden images from a shared NFS store (#282)
* feat(kubevirt-youko): export a shared golden-image store over NFS

* feat(vm-images): upload golden images into the cluster

Replace the rsync-to-orangepi5 step with a CDI upload into the golden-image
PVCs (vms/golden-images in k8s-gitops), so VM images no longer depend on the
single-board file server.
2026-09-21 11:21:33 +08:00