diff --git a/assets/zed/settings.json b/assets/zed/settings.json new file mode 100644 index 0000000..e48db6f --- /dev/null +++ b/assets/zed/settings.json @@ -0,0 +1,53 @@ +// Zed settings — tracked in dotfiles, symlinked into ~/.config/zed/settings.json +// by home-manager (xdg.configFile in nixos/home/danny/home.nix). +// +// Because this is a symlink to a nix-store file, editing it from inside Zed +// will fail (read-only). Edit THIS file in dotfiles, commit, and rebuild +// (`darwin-rebuild switch --flake .`). To see Zed's full default settings, +// run `zed: open default settings` from the command palette. +{ + "sticky_scroll": { + "enabled": true + }, + "edit_predictions": { + "provider": "ollama" + }, + "buffer_font_family": "JetBrains Mono", + "cli_default_open_behavior": "existing_window", + "project_panel": { + "dock": "left" + }, + "outline_panel": { + "dock": "left" + }, + "collaboration_panel": { + "dock": "left" + }, + "git_panel": { + "dock": "left" + }, + "agent": { + "dock": "right", + "default_model": { + "provider": "ollama", + "model": "llama3.2:latest" + } + }, + "disable_ai": false, + "minimap": { + "show": "auto" + }, + "telemetry": { + "diagnostics": false, + "metrics": false + }, + "base_keymap": "VSCode", + "vim_mode": true, + "ui_font_size": 16, + "buffer_font_size": 15, + "theme": { + "mode": "system", + "light": "One Light", + "dark": "One Dark" + } +} diff --git a/docs/server-installer-usb.md b/docs/server-installer-usb.md index 4c69d53..295f227 100644 --- a/docs/server-installer-usb.md +++ b/docs/server-installer-usb.md @@ -94,16 +94,45 @@ sudo dd if=result/iso/nixos-minimal-*.iso of=/dev/sdX status=progress bs=4M ## Live-system WiFi (optional, custom ISO only) -Create `nixos/installer-wifi.nix` (gitignored): +The minimal installer ISO runs NetworkManager, so live-system WiFi must be a +declarative NetworkManager profile. `networking.wireless` / wpa_supplicant does +**not** work here — NixOS asserts you cannot combine `networking.networkmanager` +with `networking.wireless.networks`. + +Create `nixos/installer-wifi.nix` (gitignored — it holds the PSK): ```nix { - networking.wireless.enable = true; - networking.wireless.networks."YourSSID".psk = "your-password"; + networking.networkmanager.ensureProfiles.profiles.installer-wifi = { + connection = { + id = "installer-wifi"; + type = "wifi"; + }; + wifi = { + mode = "infrastructure"; + ssid = "YourSSID"; + }; + wifi-security = { + auth-alg = "open"; + key-mgmt = "wpa-psk"; + psk = "your-password"; + }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; } ``` -Add to flake's installer-iso modules, rebuild ISO on Linux. +`flake-modules/installer-iso.nix` auto-includes this file when present (via a +`builtins.pathExists` check) — no flake edit needed. Because the file is +gitignored, the flake only sees it once it is staged: + +- **`build-installer-iso-on-server.sh`** copies the file to the build host and + runs `git add -f` automatically. +- For a **direct `nix build`**, run `git add -f nixos/installer-wifi.nix` first + (staging is enough — never commit it; it contains the PSK). + +Then rebuild the ISO on Linux. ## Installed-system WiFi (optional) diff --git a/flake-modules/clan.nix b/flake-modules/clan.nix index f8b1293..e4a7944 100644 --- a/flake-modules/clan.nix +++ b/flake-modules/clan.nix @@ -21,6 +21,8 @@ let sunkenShipZTv6 = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33"; phantomShipZTv6 = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc"; vpsRelayZTv6 = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3"; + distantShoreZTv6 = "fdd5:53a2:de33:d269:6499:93b6:ef1a:c3b3"; + foreignPortZTv6 = "fdd5:53a2:de33:d269:6499:9389:9b18:6c52"; # Shared across both servers: /etc/hosts entries so data-mesher's # libp2p /dns/.clan/... bootstrap multiaddrs resolve over ZT. @@ -29,6 +31,8 @@ let "${sunkenShipZTv6}" = [ "sunken-ship.clan" ]; "${phantomShipZTv6}" = [ "phantom-ship.clan" ]; "${vpsRelayZTv6}" = [ "vps-relay.clan" ]; + "${distantShoreZTv6}" = [ "distant-shore.clan" ]; + "${foreignPortZTv6}" = [ "foreign-port.clan" ]; }; }; in { @@ -47,6 +51,8 @@ in { inventory.machines.sunken-ship = { }; inventory.machines.phantom-ship = { }; inventory.machines.vps-relay = { }; + inventory.machines.distant-shore = { }; + inventory.machines.foreign-port = { }; # ZeroTier mesh VPN. sunken-ship is the controller (manages network # membership); phantom-ship is a peer. The mac joins manually as an @@ -58,6 +64,8 @@ in { roles.peer.machines.phantom-ship = { }; roles.peer.machines.sunken-ship = { }; roles.peer.machines.vps-relay = { }; + roles.peer.machines.distant-shore = { }; + roles.peer.machines.foreign-port = { }; }; # data-mesher — signed-file gossip protocol over libp2p (port 7946). @@ -70,6 +78,8 @@ in { module.input = "clan-core"; roles.default.machines.sunken-ship = { }; roles.default.machines.phantom-ship = { }; + roles.default.machines.distant-shore = { }; + roles.default.machines.foreign-port = { }; roles.bootstrap.machines.sunken-ship = { }; }; @@ -87,6 +97,8 @@ in { }; roles.default.machines.sunken-ship.settings.action = "switch"; roles.default.machines.phantom-ship.settings.action = "switch"; + roles.default.machines.distant-shore.settings.action = "switch"; + roles.default.machines.foreign-port.settings.action = "switch"; }; # `clan machines update` connection target. Priority 2000 > ZT's 900 @@ -111,6 +123,18 @@ in { host = "89.167.39.251"; user = "danny"; }; + # distant-shore: LAN IP for the first update (not yet on ZT). Swap to + # its generated ZT IPv6 after it joins the mesh, like the others. + roles.default.machines.distant-shore.settings = { + host = "192.168.1.182"; + user = "danny"; + }; + # foreign-port: WiFi-only LAN IP for the first update; swap to its + # generated ZT IPv6 after it joins the mesh. + roles.default.machines.foreign-port.settings = { + host = "192.168.1.223"; + user = "danny"; + }; }; # Preserve current network / init stack (no systemd-networkd/resolved, @@ -125,8 +149,9 @@ in { } clanHostsModule ../nixos/hosts/sunken-ship.nix - config.flake.nixosModules.dotfiles-rebuild config.flake.nixosModules.server-debug-tools + config.flake.nixosModules.monitoring-node-exporter + config.flake.nixosModules.monitoring-prometheus-server inputs.home-manager.nixosModules.home-manager (hmModule { user = "danny"; @@ -146,6 +171,56 @@ in { } clanHostsModule ../nixos/hosts/vps-relay.nix + config.flake.nixosModules.monitoring-node-exporter + inputs.home-manager.nixosModules.home-manager + (hmModule { + user = "danny"; + homeDirectory = "/home/danny"; + stateVersion = "25.11"; + }) + ]; + }; + + # distant-shore — ThinkPad X13 Gen 2, WiFi, Secure Boot via shim+MOK + # (installed standalone, then migrated into clan). targetHost is the LAN + # IP for the first `clan machines update`; switch to its ZT IPv6 once the + # mesh is up. buildHost = sunken-ship: it's an x86_64 builder whose key is + # already in distant-shore's authorized_keys, so the closure copy works + # (building on distant-shore itself needs a fragile self-SSH). + machines.distant-shore = { + imports = [ + { + clan.core.enableRecommendedDefaults = false; + clan.core.networking.targetHost = "danny@192.168.1.182"; + clan.core.networking.buildHost = "danny@sunken-ship"; + } + clanHostsModule + ../nixos/hosts/distant-shore.nix + config.flake.nixosModules.monitoring-node-exporter + inputs.home-manager.nixosModules.home-manager + (hmModule { + user = "danny"; + homeDirectory = "/home/danny"; + stateVersion = "25.11"; + }) + ]; + }; + + # foreign-port — WiFi-only laptop server, locally-signed boot chain + # (installed standalone, migrated into clan). targetHost is the LAN IP + # for the first `clan machines update`; switch to its ZT IPv6 once the + # mesh is up. buildHost = sunken-ship for the closure copy (avoids + # self-SSH). + machines.foreign-port = { + imports = [ + { + clan.core.enableRecommendedDefaults = false; + clan.core.networking.targetHost = "danny@192.168.1.223"; + clan.core.networking.buildHost = "danny@sunken-ship"; + } + clanHostsModule + ../nixos/hosts/foreign-port.nix + config.flake.nixosModules.monitoring-node-exporter inputs.home-manager.nixosModules.home-manager (hmModule { user = "danny"; @@ -165,8 +240,8 @@ in { clanHostsModule inputs.nix-openclaw.nixosModules.openclaw-gateway ../nixos/hosts/phantom-ship.nix - config.flake.nixosModules.dotfiles-rebuild config.flake.nixosModules.server-debug-tools + config.flake.nixosModules.monitoring-node-exporter inputs.home-manager.nixosModules.home-manager (hmModule { user = "danny"; diff --git a/flake-modules/installer-iso.nix b/flake-modules/installer-iso.nix index fc18929..03609ab 100644 --- a/flake-modules/installer-iso.nix +++ b/flake-modules/installer-iso.nix @@ -1,9 +1,13 @@ { inputs, self, ... }: { # Custom minimal installer ISO (build with: nix build .#installer-iso). - # Optional: add ./installer-wifi.nix (gitignored) to modules for live WiFi. + # nixos/installer-wifi.nix (gitignored) is auto-included when present, to + # preconfigure live-system WiFi. See docs/server-installer-usb.md. flake.nixosConfigurations.installer-iso = inputs.nixpkgs.lib.nixosSystem { system = "x86_64-linux"; - modules = [ ../nixos/installer-iso.nix ]; + modules = [ ../nixos/installer-iso.nix ] + ++ inputs.nixpkgs.lib.optional + (builtins.pathExists ../nixos/installer-wifi.nix) + ../nixos/installer-wifi.nix; }; flake.packages.x86_64-linux.installer-iso = diff --git a/flake-modules/nixos-modules.nix b/flake-modules/nixos-modules.nix index a466a58..3dd7929 100644 --- a/flake-modules/nixos-modules.nix +++ b/flake-modules/nixos-modules.nix @@ -1,8 +1,9 @@ # Expose reusable NixOS modules via `flake.nixosModules`. # # Consume from a host's flake-module via: -# modules = [ config.flake.nixosModules.dotfiles-rebuild ]; +# modules = [ config.flake.nixosModules.server-debug-tools ]; { ... }: { - flake.nixosModules.dotfiles-rebuild = ../modules/dotfiles-rebuild.nix; flake.nixosModules.server-debug-tools = ../modules/server-debug-tools.nix; + flake.nixosModules.monitoring-node-exporter = ../modules/monitoring-node-exporter.nix; + flake.nixosModules.monitoring-prometheus-server = ../modules/monitoring-prometheus-server.nix; } diff --git a/flake.lock b/flake.lock index 106a124..59c41ec 100644 --- a/flake.lock +++ b/flake.lock @@ -9,22 +9,18 @@ "nixpkgs": [ "nixpkgs" ], - "systems": "systems", "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1776708356, - "narHash": "sha256-Smv2algQmojsu0m9EEXs+Oy0Tg/SjwI5WN66u/BaxYs=", - "ref": "fix/dm-pull-deploy-hyphen-hostnames", - "rev": "796ee625b60941bb959039924bfc39e5d13481cc", - "revCount": 46, - "type": "git", - "url": "https://git.clan.lol/dannydannydanny/clan-community.git" + "lastModified": 1779453564, + "narHash": "sha256-q7iVGGhZYtAwsjf7sIKcYD5IgsTTTobWP/EStaDCUZc=", + "rev": "81e4c9cded645d0384812dd6b8f05bd2475ffe64", + "type": "tarball", + "url": "https://git.clan.lol/api/v1/repos/clan/clan-community/archive/81e4c9cded645d0384812dd6b8f05bd2475ffe64.tar.gz" }, "original": { - "ref": "fix/dm-pull-deploy-hyphen-hostnames", - "type": "git", - "url": "https://git.clan.lol/dannydannydanny/clan-community.git" + "type": "tarball", + "url": "https://git.clan.lol/clan/clan-community/archive/main.tar.gz" } }, "clan-core": { @@ -40,15 +36,15 @@ "nixpkgs" ], "sops-nix": "sops-nix", - "systems": "systems_2", + "systems": "systems", "treefmt-nix": "treefmt-nix_2" }, "locked": { - "lastModified": 1776557977, - "narHash": "sha256-j+UWg3fR6jWKPqkPoqRf1a6nR1b/AnZXDuh04H+voUc=", - "rev": "e9ced950bedc726492e5cb52139bf5f17258dc69", + "lastModified": 1778462753, + "narHash": "sha256-/9qWZbrwoVWP0YWuC1Z5HMEb/oy6rNsjypUKTuk1PB4=", + "rev": "09551fdb27a7e5712bef371e9271034d503242ed", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/e9ced950bedc726492e5cb52139bf5f17258dc69.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/09551fdb27a7e5712bef371e9271034d503242ed.tar.gz" }, "original": { "type": "tarball", @@ -71,11 +67,11 @@ ] }, "locked": { - "lastModified": 1776506822, - "narHash": "sha256-WlxAhXEoDHbkfFw3uNYra0CXce7pBk314x9chPu7ycE=", - "rev": "c3f48f5931b27bb9cc58de8799d36ecefb867d98", + "lastModified": 1776654564, + "narHash": "sha256-5bpzOOXsaAr4g25/ghtKdYO17xg0l+MieCcWgqx24eY=", + "rev": "ad23733ebc47284dc1158db43218cf4027824aee", "type": "tarball", - "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/c3f48f5931b27bb9cc58de8799d36ecefb867d98.tar.gz" + "url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/ad23733ebc47284dc1158db43218cf4027824aee.tar.gz" }, "original": { "type": "tarball", @@ -90,11 +86,11 @@ ] }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1776613567, + "narHash": "sha256-gC9Cp5ibBmGD5awCA9z7xy6MW6iJufhazTYJOiGlCUI=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "32f4236bfc141ae930b5ba2fb604f561fed5219d", "type": "github" }, "original": { @@ -110,11 +106,11 @@ ] }, "locked": { - "lastModified": 1773889306, - "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "lastModified": 1777713215, + "narHash": "sha256-8GzXDOXckDWwST8TY5DbwYFjdvQLlP7K9CLSVx6iTTo=", "owner": "nix-community", "repo": "disko", - "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "rev": "63b4e7e6cf75307c1d26ac3762b886b5b0247267", "type": "github" }, "original": { @@ -167,11 +163,11 @@ ] }, "locked": { - "lastModified": 1775087534, - "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", + "lastModified": 1777988971, + "narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", + "rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff", "type": "github" }, "original": { @@ -182,7 +178,7 @@ }, "flake-utils": { "inputs": { - "systems": "systems_3" + "systems": "systems_2" }, "locked": { "lastModified": 1731533236, @@ -200,7 +196,7 @@ }, "flake-utils_2": { "inputs": { - "systems": "systems_4" + "systems": "systems_3" }, "locked": { "lastModified": 1681202837, @@ -223,11 +219,11 @@ ] }, "locked": { - "lastModified": 1776184304, - "narHash": "sha256-No6QGBmIv5ChiwKCcbkxjdEQ/RO2ZS1gD7SFy6EZ7rc=", + "lastModified": 1778444552, + "narHash": "sha256-f18pIiR9q/p1vHY93gmAum7aHhQOG49oGvAB9+lptRo=", "owner": "nix-community", "repo": "home-manager", - "rev": "3c7524c68348ef79ce48308e0978611a050089b2", + "rev": "dcebe66f958673729896eec2de4abfd86ef22d21", "type": "github" }, "original": { @@ -265,11 +261,11 @@ ] }, "locked": { - "lastModified": 1774991950, - "narHash": "sha256-kScKj3qJDIWuN9/6PMmgy5esrTUkYinrO5VvILik/zw=", + "lastModified": 1777594677, + "narHash": "sha256-h90sHwoRJLRvaTpZroTvU2JRHDFj0czUafM8eqLe1RI=", "owner": "nix-community", "repo": "home-manager", - "rev": "f2d3e04e278422c7379e067e323734f3e8c585a7", + "rev": "899c08a15beae5da51a5cecd6b2b994777a948da", "type": "github" }, "original": { @@ -321,11 +317,11 @@ ] }, "locked": { - "lastModified": 1775037210, - "narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=", + "lastModified": 1777780666, + "narHash": "sha256-8wURyQMdDkGUarSTKOGdCuFfYiwa3HbzwscUfn3STDE=", "owner": "nix-darwin", "repo": "nix-darwin", - "rev": "06648f4902343228ce2de79f291dd5a58ee12146", + "rev": "8c62fba0854ba15c8917aed18894dbccb48a3777", "type": "github" }, "original": { @@ -339,17 +335,18 @@ "inputs": { "flake-utils": "flake-utils", "home-manager": "home-manager_2", - "nix-steipete-tools": "nix-steipete-tools", + "nix-openclaw-tools": "nix-openclaw-tools", "nixpkgs": [ "nixpkgs" - ] + ], + "qmd": "qmd" }, "locked": { - "lastModified": 1776183358, - "narHash": "sha256-uRWaRXGhkyGWMbNgQcmx0+RPzPLenVGopkNHgAEfmBQ=", + "lastModified": 1778353239, + "narHash": "sha256-g0yC+loN19X3Xyn6RuBHeWzevH7Qymt0REW+kyGuCLY=", "owner": "openclaw", "repo": "nix-openclaw", - "rev": "53aac0dce0810c40c75793fdad3d41b0f7e7baaf", + "rev": "e2ea91056fdd0836bef96326a2b687277dbe3e1c", "type": "github" }, "original": { @@ -358,6 +355,24 @@ "type": "github" } }, + "nix-openclaw-tools": { + "inputs": { + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1778060041, + "narHash": "sha256-tXWkN1VnwFG8XlRqW/e7VwbKnUfyU9tB7YDm9QHJXTY=", + "owner": "openclaw", + "repo": "nix-openclaw-tools", + "rev": "4c1cee3c7eaf68f9de0f756be1484534f5bb5f34", + "type": "github" + }, + "original": { + "owner": "openclaw", + "repo": "nix-openclaw-tools", + "type": "github" + } + }, "nix-select": { "locked": { "lastModified": 1763303120, @@ -371,35 +386,17 @@ "url": "https://git.clan.lol/clan/nix-select/archive/main.tar.gz" } }, - "nix-steipete-tools": { - "inputs": { - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1773561580, - "narHash": "sha256-wT0bKTp45YnMkc4yXQvk943Zz/rksYiIjEXGdWzxnic=", - "owner": "openclaw", - "repo": "nix-steipete-tools", - "rev": "cd4c429ff3b3aaef9f92e59812cf2baf5704b86f", - "type": "github" - }, - "original": { - "owner": "openclaw", - "repo": "nix-steipete-tools", - "type": "github" - } - }, "nixos-wsl": { "inputs": { "flake-compat": "flake-compat", "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1776255237, - "narHash": "sha256-LQjlc0VEn55WAT4BiI8sIsokb/2FNlcbBD+Xr3MTE24=", + "lastModified": 1777732699, + "narHash": "sha256-2uX/XtOWZ/oy2rerRynVhqVA//ZXZ3Fo60PikLHEPQc=", "owner": "nix-community", "repo": "NixOS-WSL", - "rev": "9a8c2a85f1ffdcecfb0f9c52c5a73c49ceb43911", + "rev": "5482f113fd31ebac131d1ebeb2ae90bf0d5e41f5", "type": "github" }, "original": { @@ -427,11 +424,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1773734432, - "narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=", + "lastModified": 1776169885, + "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cda48547b432e8d3b18b4180ba07473762ec8558", + "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9", "type": "github" }, "original": { @@ -443,11 +440,11 @@ }, "nixpkgs_3": { "locked": { - "lastModified": 1776255774, - "narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=", + "lastModified": 1778274207, + "narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "566acc07c54dc807f91625bb286cb9b321b5f42a", + "rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7", "type": "github" }, "original": { @@ -471,6 +468,32 @@ "type": "indirect" } }, + "qmd": { + "inputs": { + "flake-utils": [ + "nix-openclaw", + "flake-utils" + ], + "nixpkgs": [ + "nix-openclaw", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775429264, + "narHash": "sha256-bqIVaNRTa8H5vrw3RwsD7QdtTa0xNvRuEVzlzE1hIBQ=", + "owner": "tobi", + "repo": "qmd", + "rev": "65cd1b3fd02891d1ee0eefa751620918664fa321", + "type": "github" + }, + "original": { + "owner": "tobi", + "ref": "v2.1.0", + "repo": "qmd", + "type": "github" + } + }, "root": { "inputs": { "clan-community": "clan-community", @@ -509,21 +532,6 @@ } }, "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { "locked": { "lastModified": 1774449309, "narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=", @@ -539,7 +547,7 @@ "type": "github" } }, - "systems_3": { + "systems_2": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -554,7 +562,7 @@ "type": "github" } }, - "systems_4": { + "systems_3": { "locked": { "lastModified": 1681028828, "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", @@ -638,11 +646,11 @@ ] }, "locked": { - "lastModified": 1776317517, - "narHash": "sha256-JP1XVRabZquf7pnXvRUjp7DV+EBrB6Qmp3+vG3HMy/k=", + "lastModified": 1778394798, + "narHash": "sha256-/jR8bModWv0ji305ecMgAB+2eaXLZiYdH+9Z4JIRkuA=", "owner": "0xc000022070", "repo": "zen-browser-flake", - "rev": "0a7be59e988bb2cb452080f59aaabae70bc415ae", + "rev": "45bc54456044b96492923739bfae633e1a4352e1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 30e7d71..2970510 100644 --- a/flake.nix +++ b/flake.nix @@ -29,10 +29,9 @@ clan-core.inputs.nixpkgs.follows = "nixpkgs"; clan-core.inputs.flake-parts.follows = "flake-parts"; - # clan-community: dm-pull-deploy etc. Pinned to our fork's fix branch - # until clan/clan-community#25 (machine.name hyphen sanitization) lands. - # Swap back to `archive/main.tar.gz` when merged. - clan-community.url = "git+https://git.clan.lol/dannydannydanny/clan-community.git?ref=fix/dm-pull-deploy-hyphen-hostnames"; + # clan-community: dm-pull-deploy etc. Back on upstream main since + # clan/clan-community#25 (machine.name hyphen sanitization) merged. + clan-community.url = "https://git.clan.lol/clan/clan-community/archive/main.tar.gz"; clan-community.inputs.nixpkgs.follows = "nixpkgs"; clan-community.inputs.clan-core.follows = "clan-core"; }; diff --git a/modules/dotfiles-rebuild.nix b/modules/dotfiles-rebuild.nix deleted file mode 100644 index de6ac87..0000000 --- a/modules/dotfiles-rebuild.nix +++ /dev/null @@ -1,44 +0,0 @@ -# Shared auto-rebuild-from-git service for homelab hosts. -# -# Every 15 min: git fetch origin, fast-forward main, and if there were any -# new commits run nixos-rebuild switch against `#`. -# -# Assumes /etc/dotfiles is an already-cloned checkout of the dotfiles repo. -{ config, lib, pkgs, ... }: -let - dotfilesDir = "/etc/dotfiles"; - flakeRef = "${dotfilesDir}#${config.networking.hostName}"; -in { - environment.systemPackages = [ pkgs.git ]; - - # Trust /etc/dotfiles as root even though it's owned by `danny`. - # nix/libgit2 reads safe.directory from /etc/gitconfig; the GIT_CONFIG_* - # env vars on the service only affect the git CLI, not nix. - programs.git.enable = true; - programs.git.config.safe.directory = [ dotfilesDir ]; - - systemd.services.dotfiles-rebuild = { - description = "Pull dotfiles and run nixos-rebuild if repo changed"; - path = with pkgs; [ git nix nixos-rebuild ]; - environment.GIT_CONFIG_COUNT = "1"; - environment.GIT_CONFIG_KEY_0 = "safe.directory"; - environment.GIT_CONFIG_VALUE_0 = dotfilesDir; - script = '' - set -euo pipefail - cd ${dotfilesDir} - git fetch origin - if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then - exit 0 - fi - git pull origin main - exec nixos-rebuild switch --flake ${flakeRef} - ''; - serviceConfig.Type = "oneshot"; - }; - - systemd.timers.dotfiles-rebuild = { - wantedBy = [ "timers.target" ]; - timerConfig.OnCalendar = "*-*-* *:00/15:00"; # every 15 minutes - timerConfig.RandomizedDelaySec = "2min"; - }; -} diff --git a/modules/monitoring-node-exporter.nix b/modules/monitoring-node-exporter.nix new file mode 100644 index 0000000..7e08ae0 --- /dev/null +++ b/modules/monitoring-node-exporter.nix @@ -0,0 +1,12 @@ +# Prometheus node_exporter — exposes host metrics on :9100, scoped to the +# ZeroTier mesh so only sunken-ship (the Prometheus server) can scrape it. +{ ... }: { + services.prometheus.exporters.node = { + enable = true; + port = 9100; + listenAddress = "[::]"; + enabledCollectors = [ "systemd" ]; + }; + + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 9100 ]; +} diff --git a/modules/monitoring-prometheus-server.nix b/modules/monitoring-prometheus-server.nix new file mode 100644 index 0000000..9aedc14 --- /dev/null +++ b/modules/monitoring-prometheus-server.nix @@ -0,0 +1,134 @@ +# Prometheus + Alertmanager + Grafana on sunken-ship. +# +# Scrape targets are the clan ZeroTier IPv6s — kept in sync with +# vars/per-machine//zerotier/zerotier-ip/value. +# +# Telegram receiver uses the existing @HarakatBot. Drop the bot token at +# /etc/alertmanager/telegram-token (mode 0400, root) before rebuild — same +# manual-secret pattern as the other Telegram bots in the repo. +# +# Routing: critical alerts repeat every 1h, everything else every 4h. +{ ... }: +let + sunkenShipZTv6 = "fdd5:53a2:de33:d269:6499:93d5:53a2:de33"; + phantomShipZTv6 = "fdd5:53a2:de33:d269:6499:936c:48a:bbdc"; + vpsRelayZTv6 = "fdd5:53a2:de33:d269:6499:9305:339f:2ed3"; + + target = ip: "[${ip}]:9100"; +in { + services.prometheus = { + enable = true; + port = 9090; + listenAddress = "[::1]"; + + globalConfig = { + scrape_interval = "30s"; + evaluation_interval = "30s"; + }; + + scrapeConfigs = [{ + job_name = "node"; + static_configs = [{ + targets = [ + (target sunkenShipZTv6) + (target phantomShipZTv6) + (target vpsRelayZTv6) + ]; + labels.job = "node"; + }]; + }]; + + ruleFiles = [ + (builtins.toFile "host-rules.yml" (builtins.toJSON { + groups = [{ + name = "hosts"; + rules = [{ + alert = "HostDown"; + expr = ''up{job="node"} == 0''; + for = "5m"; + labels.severity = "critical"; + annotations = { + summary = "{{ $labels.instance }} is down"; + description = "{{ $labels.instance }} has been unreachable for 5 minutes."; + }; + }]; + }]; + })) + ]; + + alertmanagers = [{ + static_configs = [{ targets = [ "[::1]:9093" ]; }]; + }]; + + alertmanager = { + enable = true; + port = 9093; + listenAddress = "[::1]"; + configuration = { + route = { + receiver = "telegram-default"; + group_by = [ "alertname" ]; + group_wait = "30s"; + group_interval = "5m"; + repeat_interval = "4h"; + routes = [{ + matchers = [ ''severity="critical"'' ]; + receiver = "telegram-critical"; + group_wait = "10s"; + group_interval = "1m"; + repeat_interval = "1h"; + }]; + }; + receivers = [ + { + name = "telegram-default"; + telegram_configs = [{ + bot_token_file = "/etc/alertmanager/telegram-token"; + chat_id = 66070351; + api_url = "https://api.telegram.org"; + parse_mode = ""; + }]; + } + { + name = "telegram-critical"; + telegram_configs = [{ + bot_token_file = "/etc/alertmanager/telegram-token"; + chat_id = 66070351; + api_url = "https://api.telegram.org"; + parse_mode = ""; + message = '' + CRITICAL: {{ .CommonLabels.alertname }} + {{ range .Alerts }}{{ .Annotations.summary }} + {{ .Annotations.description }} + {{ end }}''; + }]; + } + ]; + }; + }; + }; + + services.grafana = { + enable = true; + settings.server = { + http_addr = "::"; + http_port = 3000; + domain = "sunken-ship.clan"; + }; + # Drop a random 32+ char string at /etc/grafana/secret-key (mode 0400, + # owned by grafana:grafana) before rebuild — same manual-secret pattern + # as /etc/alertmanager/telegram-token. Used to encrypt secrets stored + # in Grafana's DB; nothing to rotate on a fresh install. + settings.security.secret_key = "$__file{/etc/grafana/secret-key}"; + provision.datasources.settings.datasources = [{ + name = "Prometheus"; + type = "prometheus"; + url = "http://[::1]:9090"; + isDefault = true; + }]; + }; + + # Grafana on the ZeroTier mesh only. Prometheus + Alertmanager bind to + # localhost so they're not reachable off-host. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 ]; +} diff --git a/nixos/disko-distant-shore.nix b/nixos/disko-distant-shore.nix new file mode 100644 index 0000000..ab35aac --- /dev/null +++ b/nixos/disko-distant-shore.nix @@ -0,0 +1,37 @@ +# Declarative disk layout for distant-shore (ThinkPad X13 Gen 2 — 256 GB +# SK Hynix NVMe). UEFI/systemd-boot, no encryption: it's a headless, +# WiFi-only server that must reboot unattended (clan dm-pull-deploy), so +# a LUKS passphrase prompt at boot would hang it. Mirrors sunken-ship's +# plain-ext4 choice. Device is wiped + repartitioned at install time by +# clan/nixos-anywhere. +{ + disko.devices = { + disk.main = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "fmask=0022" "dmask=0022" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; +} diff --git a/nixos/disko-foreign-port.nix b/nixos/disko-foreign-port.nix new file mode 100644 index 0000000..a928620 --- /dev/null +++ b/nixos/disko-foreign-port.nix @@ -0,0 +1,36 @@ +# Declarative disk layout for distant-shore. UEFI/systemd-boot, no +# encryption: it's a headless, WiFi-only server that must reboot +# unattended (clan dm-pull-deploy), so a LUKS passphrase prompt at boot +# would hang it. Mirrors sunken-ship's plain-ext4 choice. Device is wiped +# + repartitioned at install time by clan/nixos-anywhere. +{ + disko.devices = { + disk.main = { + type = "disk"; + device = "/dev/nvme0n1"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "fmask=0022" "dmask=0022" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; +} diff --git a/nixos/fish.nix b/nixos/fish.nix index 9d04f51..1d72d26 100644 --- a/nixos/fish.nix +++ b/nixos/fish.nix @@ -24,6 +24,38 @@ set fish_greeting 🐟: (set_color yellow; date +%T; set_color green; date --iso-8601 2>/dev/null; or date +%F; set_color normal) + # gco: smart `git checkout` — if the branch is checked out in another + # worktree, cd there instead of failing with "already used by worktree at". + function gco --description 'git checkout, but cd into worktree if the branch lives there' + if test (count $argv) -eq 0 + git checkout + return $status + end + + set -l branch $argv[1] + set -l target_ref "refs/heads/$branch" + set -l wt_path "" + set -l current_wt "" + for line in (git worktree list --porcelain 2>/dev/null) + switch $line + case 'worktree *' + set current_wt (string replace -r '^worktree ' "" -- $line) + case "branch $target_ref" + set wt_path $current_wt + break + end + end + + set -l here (git rev-parse --show-toplevel 2>/dev/null) + if test -n "$wt_path"; and test "$wt_path" != "$here" + echo "→ cd $wt_path (branch '$branch' is checked out in another worktree)" + cd $wt_path + return $status + end + + git checkout $argv + end + # Alacritty palette follows macOS appearance; refresh when opening a shell (LaunchAgent also polls). if test (uname -s) = Darwin bash ~/dotfiles/scripts/alacritty-sync-system-theme.sh >/dev/null 2>&1 & diff --git a/nixos/home/danny/home.nix b/nixos/home/danny/home.nix index 16d9adf..b57def5 100644 --- a/nixos/home/danny/home.nix +++ b/nixos/home/danny/home.nix @@ -88,6 +88,35 @@ catppuccin tmux-fzf extrakto + # tmux-resurrect: prefix + Ctrl-s saves, prefix + Ctrl-r restores. + # Snapshot lives at ~/.tmux/resurrect/last (window layout, working + # dirs, pane contents if enabled). Survives force-quits / reboots + # / kernel panics. + # + # @resurrect-processes: programs to restart on restore. Default + # list covers vim/emacs/less/top/etc. but NOT nvim, claude, or + # ssh. The "~name->cmd" form re-runs the original argv; bare + # names match argv-less invocations. Without this, restored panes + # come back as plain fish prompts in the right directory. + { + plugin = resurrect; + extraConfig = '' + set -g @resurrect-capture-pane-contents 'on' + set -g @resurrect-strategy-nvim 'session' + set -g @resurrect-processes 'nvim "~nvim->nvim *" claude "~claude->claude --continue" ssh "~ssh->ssh *"' + ''; + } + # tmux-continuum: auto-saves every 15min and auto-restores on + # tmux server start. With this, the next force-quit just costs + # you up to 15min of recent terminal activity, not the whole + # workspace. + { + plugin = continuum; + extraConfig = '' + set -g @continuum-restore 'on' + set -g @continuum-save-interval '15' + ''; + } ]; }; @@ -142,6 +171,11 @@ xdg.configFile."alacritty/catppuccin-mocha-colors.toml".source = ../../../assets/alacritty/catppuccin-mocha-colors.toml; + # Zed: settings.json is a read-only symlink to assets/zed/settings.json. + # To change a setting, edit the asset file and rebuild — editing via Zed's + # UI will fail because the target is in the nix store. + xdg.configFile."zed/settings.json".source = ../../../assets/zed/settings.json; + # Alacritty: base config + imported active-colors.toml (updated without rebuild) programs.alacritty = { enable = true; @@ -228,9 +262,10 @@ # alacritty # TODO: configured via programs.alacritty above, so not needed here # warp-terminal # TODO: Bloat # vscodium # TODO: Bloat - # zed-editor # TODO: Bloat + zed-editor code-cursor cursor-cli + cinny-desktop # Matrix client (Tauri wrapper around the Cinny web app) dfu-util # USB DFU firmware flasher (Flipper Zero etc.) discord mapscii diff --git a/nixos/hosts/distant-shore-hardware.nix b/nixos/hosts/distant-shore-hardware.nix new file mode 100644 index 0000000..3c52633 --- /dev/null +++ b/nixos/hosts/distant-shore-hardware.nix @@ -0,0 +1,18 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/nixos/hosts/distant-shore.nix b/nixos/hosts/distant-shore.nix new file mode 100644 index 0000000..33a7026 --- /dev/null +++ b/nixos/hosts/distant-shore.nix @@ -0,0 +1,114 @@ +# NixOS server on a ThinkPad X13 Gen 2 (Intel i5-1145G7, 16 GB). +# WiFi-only, headless, unattended auto-rebuild via clan dm-pull-deploy. +# No LUKS (mirrors sunken-ship) so reboots don't block on a passphrase. +# +# Blank-slate server for now — no application services. Give it a purpose +# later (just add services here and let dm-pull-deploy roll it out). +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./distant-shore-hardware.nix + ../disko-distant-shore.nix + ]; + + boot.loader.systemd-boot.enable = true; + # Secure Boot is enforced and the BIOS supervisor password is unknown, so + # we can't enrol our own SB keys. Instead, shim (MS-signed) is placed on + # the ESP and chain-loads systemd-boot; the NVRAM boot entry points at + # shim. We manage that entry imperatively via efibootmgr; letting bootctl + # touch EFI variables would replace it on every rebuild. + boot.loader.efi.canTouchEfiVariables = false; + boot.loader.efi.efiSysMountPoint = "/boot"; # matches disko ESP mountpoint + + # --- Secure Boot via shim + MOK (no firmware key enrolment possible) ------ + # The firmware trusts Microsoft-signed shim; shim trusts our enrolled MOK. + # On every bootloader install we: (1) sign systemd-boot with the MOK and + # drop it where shim chain-loads it (grubx64.efi), (2) install shim as the + # firmware-booted binary (+ MokManager), (3) MOK-sign every kernel image + # systemd-boot will hand off to (shim verifies them via the shim-lock + # protocol). Re-runs on each nixos-rebuild so auto-deployed generations + # stay bootable. Keys + shim live in /etc/secrets (outside the repo). + boot.loader.systemd-boot.extraInstallCommands = '' + # NixOS's bootloader-install systemd unit runs with a minimal PATH that + # doesn't include coreutils, so use absolute paths for cp/mv. + KEY=/etc/secrets/MOK.key + CRT=/etc/secrets/MOK.crt + sb() { ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$2" "$1"; } + # systemd-boot -> shim's chain-load target + sb /boot/EFI/systemd/systemd-bootx64.efi /boot/EFI/BOOT/grubx64.efi + # shim (MS-signed) is what the firmware boots; MokManager beside it + ${pkgs.coreutils}/bin/cp -f /etc/secrets/shimx64.efi /boot/EFI/BOOT/BOOTX64.EFI + ${pkgs.coreutils}/bin/cp -f /etc/secrets/mmx64.efi /boot/EFI/BOOT/mmx64.efi + # MOK-sign each kernel (skip already-signed; never touch initrds) + for k in /boot/EFI/nixos/*bzImage.efi; do + [ -e "$k" ] || continue + if ! ${pkgs.sbsigntool}/bin/sbverify --cert "$CRT" "$k" >/dev/null 2>&1; then + ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$k.tmp" "$k" \ + && ${pkgs.coreutils}/bin/mv -f "$k.tmp" "$k" + fi + done + ''; + + networking.hostName = "distant-shore"; + # WiFi via NetworkManager. The wpa_supplicant stack hit two issues on this + # box: (1) it strips CAP_CHOWN so wpa couldn't create its ctrl_interface, + # and (2) dhcpcd didn't grab a lease after the (late) association at boot, + # needing a manual restart — fatal for an unattended headless server. NM + # handles association + DHCP atomically and connected first-try here. + # The PSK stays out of the repo: it's substituted from /etc/secrets/nm.env + # ($PSK_INTENO) into the declared profile at activation. + networking.networkmanager.enable = true; + networking.networkmanager.ensureProfiles.environmentFiles = [ "/etc/secrets/nm.env" ]; + networking.networkmanager.ensureProfiles.profiles."Inteno-89FE" = { + connection = { id = "Inteno-89FE"; type = "wifi"; autoconnect = true; }; + wifi = { ssid = "Inteno-89FE"; mode = "infrastructure"; }; + wifi-security = { key-mgmt = "wpa-psk"; psk = "$PSK_INTENO"; }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; + hardware.enableRedistributableFirmware = true; # iwlwifi for the Intel AX201 WiFi + time.timeZone = "Europe/Copenhagen"; + + # It's a laptop acting as a server: keep running with the lid shut. + services.logind.settings.Login.HandleLidSwitch = "ignore"; + services.logind.settings.Login.HandleLidSwitchExternalPower = "ignore"; + + # Reduce screen burn-in / power: blank the TTY after a minute. + boot.kernelParams = [ "consoleblank=60" ]; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + programs.nix-ld.enable = true; # run dynamically linked binaries (e.g. Claude Code remote CLI) + nix.settings.trusted-users = [ "root" "danny" ]; + system.stateVersion = "25.11"; + + users.users.danny = { + isNormalUser = true; + extraGroups = [ "wheel" "video" "audio" ]; + openssh.authorizedKeys.keys = [ + # Mac admin / fleet key (~/.ssh/id_ed25519_sunken_ship) — the key the + # Mac uses to reach the fleet; clan machines update relies on it. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@mac-admin" + # Per-host key (~/.ssh/id_ed25519_distant_shore) — plain `ssh distant-shore`. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH61JOiOOPrAXekakAwTJg5yCSDfOIjlSvMYkpXrarAf distant-shore" + # sunken-ship (dm-pull-deploy push node) — reach distant-shore over ZT. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship" + ]; + }; + users.users.root.openssh.authorizedKeys.keys = + config.users.users.danny.openssh.authorizedKeys.keys; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + }; + }; + + security.sudo.wheelNeedsPassword = false; + + # mokutil — manage MOK enrolment for the shim chain; sbsigntool — inspect + # signatures on bootloader/kernel images when debugging. + environment.systemPackages = with pkgs; [ git mokutil sbsigntool ]; +} diff --git a/nixos/hosts/foreign-port-hardware.nix b/nixos/hosts/foreign-port-hardware.nix new file mode 100644 index 0000000..3c52633 --- /dev/null +++ b/nixos/hosts/foreign-port-hardware.nix @@ -0,0 +1,18 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "xhci_pci" "thunderbolt" "nvme" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/nixos/hosts/foreign-port.nix b/nixos/hosts/foreign-port.nix new file mode 100644 index 0000000..9705b6e --- /dev/null +++ b/nixos/hosts/foreign-port.nix @@ -0,0 +1,111 @@ +# NixOS laptop server. WiFi-only, headless, unattended auto-rebuild via +# clan dm-pull-deploy. No LUKS (mirrors sunken-ship) so reboots don't +# block on a passphrase. +# +# Blank-slate server for now — no application services. Give it a purpose +# later (just add services here and let dm-pull-deploy roll it out). +{ config, lib, pkgs, ... }: + +{ + imports = [ + ./foreign-port-hardware.nix + ../disko-foreign-port.nix + ]; + + boot.loader.systemd-boot.enable = true; + # Firmware-locked Secure Boot: we can't enrol our own keys into the + # firmware key DB, so a vendor-signed shim is the firmware-booted binary + # and chain-loads a locally-signed systemd-boot + kernel. The NVRAM + # entry points at shim; bootctl is kept away from EFI variables so + # rebuilds don't clobber the entry. + boot.loader.efi.canTouchEfiVariables = false; + boot.loader.efi.efiSysMountPoint = "/boot"; # matches disko ESP mountpoint + + # --- Locally-signed boot chain -------------------------------------------- + # On every bootloader install: re-sign systemd-boot and every kernel + # image, refresh the shim binary on the ESP, and place the helper binary + # beside it. Re-runs on each nixos-rebuild so auto-deployed generations + # stay bootable. Signing material lives in /etc/secrets, never the repo. + boot.loader.systemd-boot.extraInstallCommands = '' + # NixOS's bootloader-install systemd unit runs with a minimal PATH that + # doesn't include coreutils, so use absolute paths for cp/mv. + KEY=/etc/secrets/MOK.key + CRT=/etc/secrets/MOK.crt + sb() { ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$2" "$1"; } + # systemd-boot -> shim's chain-load target + sb /boot/EFI/systemd/systemd-bootx64.efi /boot/EFI/BOOT/grubx64.efi + # shim is the firmware-booted binary; helper binary sits beside it + ${pkgs.coreutils}/bin/cp -f /etc/secrets/shimx64.efi /boot/EFI/BOOT/BOOTX64.EFI + ${pkgs.coreutils}/bin/cp -f /etc/secrets/mmx64.efi /boot/EFI/BOOT/mmx64.efi + # sign each kernel (skip if already signed; leave initrds untouched) + for k in /boot/EFI/nixos/*bzImage.efi; do + [ -e "$k" ] || continue + if ! ${pkgs.sbsigntool}/bin/sbverify --cert "$CRT" "$k" >/dev/null 2>&1; then + ${pkgs.sbsigntool}/bin/sbsign --key "$KEY" --cert "$CRT" --output "$k.tmp" "$k" \ + && ${pkgs.coreutils}/bin/mv -f "$k.tmp" "$k" + fi + done + ''; + + networking.hostName = "foreign-port"; + # WiFi via NetworkManager. The wpa_supplicant stack hit two issues on this + # box: (1) it strips CAP_CHOWN so wpa couldn't create its ctrl_interface, + # and (2) dhcpcd didn't grab a lease after the (late) association at boot, + # needing a manual restart — fatal for an unattended headless server. NM + # handles association + DHCP atomically and connected first-try here. + # The PSK stays out of the repo: it's substituted from /etc/secrets/nm.env + # ($PSK_INTENO) into the declared profile at activation. + networking.networkmanager.enable = true; + networking.networkmanager.ensureProfiles.environmentFiles = [ "/etc/secrets/nm.env" ]; + networking.networkmanager.ensureProfiles.profiles."Inteno-89FE-5GHz" = { + connection = { id = "Inteno-89FE-5GHz"; type = "wifi"; autoconnect = true; }; + wifi = { ssid = "Inteno-89FE-5GHz"; mode = "infrastructure"; }; + wifi-security = { key-mgmt = "wpa-psk"; psk = "$PSK_INTENO"; }; + ipv4.method = "auto"; + ipv6.method = "auto"; + }; + hardware.enableRedistributableFirmware = true; # WiFi firmware blobs + time.timeZone = "Europe/Copenhagen"; + + # It's a laptop acting as a server: keep running with the lid shut. + services.logind.settings.Login.HandleLidSwitch = "ignore"; + services.logind.settings.Login.HandleLidSwitchExternalPower = "ignore"; + + # Reduce screen burn-in / power: blank the TTY after a minute. + boot.kernelParams = [ "consoleblank=60" ]; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + programs.nix-ld.enable = true; # run dynamically linked binaries (e.g. Claude Code remote CLI) + nix.settings.trusted-users = [ "root" "danny" ]; + system.stateVersion = "25.11"; + + users.users.danny = { + isNormalUser = true; + extraGroups = [ "wheel" "video" "audio" ]; + openssh.authorizedKeys.keys = [ + # Mac admin / fleet key (~/.ssh/id_ed25519_sunken_ship) — the key the + # Mac uses to reach the fleet; clan machines update relies on it. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@mac-admin" + # TODO: add a per-host key (~/.ssh/id_ed25519_foreign_port) for + # plain `ssh foreign-port`. Generate when convenient. + # sunken-ship (dm-pull-deploy push node) — reach foreign-port over ZT. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship" + ]; + }; + users.users.root.openssh.authorizedKeys.keys = + config.users.users.danny.openssh.authorizedKeys.keys; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + KbdInteractiveAuthentication = false; + }; + }; + + security.sudo.wheelNeedsPassword = false; + + # mokutil + sbsigntool — manage the shim trust chain and inspect signed + # bootloader/kernel images when debugging. + environment.systemPackages = with pkgs; [ git mokutil sbsigntool ]; +} diff --git a/nixos/hosts/phantom-ship.nix b/nixos/hosts/phantom-ship.nix index a298360..26b3e90 100644 --- a/nixos/hosts/phantom-ship.nix +++ b/nixos/hosts/phantom-ship.nix @@ -48,6 +48,14 @@ in }; networking.firewall.trustedInterfaces = [ "enp0s31f6" ]; + # KomTolk (:8080), Shelfish (:8081), Scuttle (:8082), Bananasimulator + # (:8083), Forgejo (:3000), Escape Hormuz (:8090), bon (:8091), + # notes (:8092), TDPixi (:8093) are reachable only over the ZeroTier mesh — + # the vps-relay Caddy reverse-proxies into them. Same pattern as + # sunken-ship's bbbot. Not in global allowedTCPPorts, so the WAN side + # stays closed. + networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 8080 8081 8082 8083 8084 8090 8091 8092 8093 ]; + hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware boot.kernelParams = [ "consoleblank=60" ]; # blank TTY after 60s to reduce burn-in @@ -103,7 +111,7 @@ in # Passwordless sudo for wheel. security.sudo.wheelNeedsPassword = false; environment.systemPackages = with pkgs; [ - git # clone/bootstrap and dotfiles-rebuild timer + git # clone/bootstrap and dm-pull-deploy nodejs # npm for openclaw plugin installs python3 # node-gyp dependency for openclaw plugins wakeonlan # wake rusty-anchor: wakeonlan 00:16:cb:87:20:ba @@ -160,10 +168,20 @@ in }; }; - # OpenClaw gateway needs write access to its config dir and repo clones. + # OpenClaw gateway needs write access to its config dir and repo clones; + # shelfish wants its DB outside the rsynced code dir. systemd.tmpfiles.rules = [ "d /etc/openclaw 0775 root openclaw - -" "d /var/lib/openclaw/repos 0750 openclaw openclaw - -" + "d /home/danny/.local/share/shelfish 0755 danny users - -" + "d /home/danny/.local/share/scuttle 0755 danny users - -" + "d /home/danny/.local/share/bananasimulator 0755 danny users - -" + "d /home/danny/.local/share/bananasimulator-beta 0755 danny users - -" + "d /home/danny/.local/share/komtolk 0755 danny users - -" + "d /home/danny/.local/share/escape_hormuz 0755 danny users - -" + "d /home/danny/.local/share/scuttle/tiles 0755 danny users - -" + "d /home/danny/.local/share/bon 0755 danny users - -" + "d /home/danny/.local/share/bon/images 0755 danny users - -" ]; # Hara Gmail MCP server (path 1: IMAP+SMTP). Replaced by an OAuth2 @@ -224,20 +242,41 @@ in # Code deployed out-of-band via rsync to /home/danny/shipyard/ # (staying in-tree in ~/python-projects/26_shipyard/ until spun out to its own repo). # Bot token (not in repo): ~danny/.secrets/telegram-bot-token-shipyard - # Data (feedback.jsonl, pointer cache): ~danny/.local/share/shipyard/ + # Data (feedback.jsonl, feedback.db, pointer cache, feedback_media/): + # ~danny/.local/share/shipyard/ + # + # Feedback now accepts photos / voice / video / docs / stickers etc. + # Phase A captures + stores raw files; Phase B derives OCR text + # (tesseract), speech transcripts (whisper-cpp), poster frames + # (ffmpeg) and PDF text (pdftotext) — all via subprocess, so each + # tool degrades gracefully if missing. systemd.services.shipyard = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ python-telegram-bot httpx + pillow # EXIF strip on captured photos ]); + # tesseract with English + Russian tessdata — vyscul writes in + # Russian, screenshots can land in either language. + tesseractWithLangs = pkgs.tesseract.override { + enableLanguages = [ "eng" "rus" ]; + }; in { description = "Shipyard Telegram bot (mini-app launcher + feedback)"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; - path = [ pythonEnv ]; + path = [ + pythonEnv + pkgs.ffmpeg # video/animation posters, sticker decode + tesseractWithLangs # photo OCR + pkgs.whisper-cpp # voice/audio transcription + pkgs.poppler-utils # pdftotext (document handling) + ]; environment = { SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + # Owner-only commands (/admin, /grant, /revoke) — anyone else gets ignored. + SHIPYARD_OWNER_ID = "66070351"; # @DannyDannyDanny }; serviceConfig = { WorkingDirectory = "/home/danny/shipyard"; @@ -248,6 +287,378 @@ in }; }; - # Auto-rebuild service/timer + safe.directory provided by the - # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). + # Shelfish — Goodreads-flavoured book club Mini App. + # Public traffic comes through vps-relay's Caddy → ZeroTier → here. + # See vps-relay.nix for the public-facing virtualHost. We never expose + # this host's IP directly. + # Code deployed out-of-band via rsync to /home/danny/shelfish/ + # (staying in-tree in ~/python-projects/27_shelfish/ until spun out). + # Auth: validates Telegram WebApp initData against shipyard's bot token + # (the bot that publishes shelfish via shipyard's project list). + # DB lives outside the rsynced code dir so deploys don't clobber state. + # (tmpfiles rule for the DB dir is bundled into the OpenClaw block above.) + systemd.services.shelfish = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "Shelfish FastAPI server (book club Mini App)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + SH_DB_PATH = "/home/danny/.local/share/shelfish/shelfish.db"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/shelfish"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8081"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # Scuttle — topdown tilt-to-move multiplayer Mini App. + # Same vps-relay-fronted ZT path as shelfish; binds to :: so the + # ZeroTier IPv6 address can reach it. + # Code rsync'd from ~/python-projects/26_scuttle/ to /home/danny/scuttle/ + # DB at ~/.local/share/scuttle/scuttle.db. + systemd.services.scuttle = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + websockets + python-telegram-bot + ]); + in { + description = "Scuttle FastAPI + WebSocket game server (geo: Østerbro)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + SC_DB_PATH = "/home/danny/.local/share/scuttle/scuttle.db"; + SC_TILES_DIR = "/home/danny/.local/share/scuttle/tiles"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/scuttle"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8082"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # Bananasimulator — the actual project at https://bananasimulator.dannydannydanny.me + # (was a placeholder in shipyard's apps.json for ages). You ARE a banana. + # Code rsync'd from ~/python-projects/26_bananasimulator/ to /home/danny/bananasimulator/ + systemd.services.bananasimulator = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "Bananasimulator FastAPI server"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + BS_DB_PATH = "/home/danny/.local/share/bananasimulator/bananasimulator.db"; + BS_RIPE_MIN_PER_STAGE = "2"; # 2 min/stage → 30 min to compost in production + }; + serviceConfig = { + WorkingDirectory = "/home/danny/bananasimulator"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8083"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # Bananasimulator BETA — cheat-instance for testing the full progression + # end-to-end. Separate DB, exposes /api/cheat/* (gated by BS_BETA_MODE=1) + # so the frontend cheat menu can seed canonical states and reset. + # Faster ripening (0.2 min/stage = ~3 min to compost) so cycles are + # testable in real time. Same code base; deploy to a sibling dir. + # vhost in vps-relay.nix → bananasimulator-beta.dannydannydanny.me. + systemd.services.bananasimulator-beta = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "Bananasimulator BETA (cheat instance) FastAPI server"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + BS_DB_PATH = "/home/danny/.local/share/bananasimulator-beta/bananasimulator.db"; + BS_RIPE_MIN_PER_STAGE = "0.2"; # ~3 min to compost — testable in real time + BS_BETA_MODE = "1"; # exposes /api/cheat/* + flips beta=true in /api/me + }; + serviceConfig = { + WorkingDirectory = "/home/danny/bananasimulator-beta"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8084"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # Escape Hormuz — turn-based boat-race Mini App (Hara's first build). + # Code lives at /home/danny/escape_hormuz/. Same vps-relay-fronted ZT path + # as the others; binds :: so the ZeroTier IPv6 address is reachable. + systemd.services.escape-hormuz = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + python-telegram-bot + ]); + in { + description = "Escape Hormuz FastAPI server (turn-based boat race)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + DB_PATH = "/home/danny/.local/share/escape_hormuz/escape_hormuz.db"; + MINIAPP_URL = "https://escapehormuz.dannydannydanny.me"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/escape_hormuz"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8090"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # Ollama — local LLM runtime, used by bon's structured-data extraction + # step. Listens on 127.0.0.1:11434 only (not exposed over ZT). + # 3B is bon's default — 7B was tested but ran ~3.6 min/receipt vs ~30s + # for 3B on phantom-ship CPU, with no real accuracy gain (still picked + # line items as merchant on header-less OCR; that's an OCR problem, + # not a model problem). Both kept loaded so we can A/B without a pull. + services.ollama = { + enable = true; + host = "127.0.0.1"; + port = 11434; + loadModels = [ + "qwen2.5:3b-instruct" # ~2.5 GB — current default + "qwen2.5:7b-instruct" # ~4.7 GB — A/B testing only + ]; + }; + + # bon — receipt scanner Mini App (camera capture + gallery + OCR + extract). + # Code rsync'd from ~/python-projects/26_bon/ to /home/danny/bon/ + # Images on disk under /home/danny/.local/share/bon/images// + # OCR via tesseract (binary on PATH; server uses subprocess directly). + # Structured extraction via local Ollama (qwen2.5:3b-instruct). + systemd.services.bon = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + python-telegram-bot + python-multipart + pillow + httpx # for the Ollama HTTP call from extract.py + ]); + # English-only for now — Danish receipts in DK are mostly English chars + # plus prices, which `eng` handles fine. Add more languages later if + # vyscul or other testers report missed text. + tesseractEng = pkgs.tesseract.override { + enableLanguages = [ "eng" ]; + }; + in { + description = "bon FastAPI server (receipt scanner)"; + after = [ "network-online.target" "ollama.service" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv tesseractEng ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + BON_DB_PATH = "/home/danny/.local/share/bon/bon.db"; + BON_IMAGES_DIR = "/home/danny/.local/share/bon/images"; + BON_OLLAMA_URL = "http://127.0.0.1:11434"; + BON_OLLAMA_MODEL = "qwen2.5:3b-instruct"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/bon"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8091"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # KomTolk (formerly translate-platform) — Copenhagen translation gigs Mini App. + # Code rsync'd from ~/python-projects/26_komtolk/ to /home/danny/komtolk/ + systemd.services.komtolk = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + httpx + python-telegram-bot + ]); + in { + description = "KomTolk FastAPI server (Copenhagen translation gigs)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment = { + SHIPYARD_BOT_TOKEN_FILE = "/home/danny/.secrets/telegram-bot-token-shipyard"; + KT_DB_PATH = "/home/danny/.local/share/komtolk/komtolk.db"; + }; + serviceConfig = { + WorkingDirectory = "/home/danny/komtolk"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8080"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # notes — tiny markdown blog + apex landing page. + # One service serves two hostnames via Host-header switch: + # notes.dannydannydanny.me → blog + # dannydannydanny.me → landing + # Code rsync'd from ~/python-projects/26_notes/ to /home/danny/notes/ + systemd.services.notes = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + markdown + jinja2 + ]); + in { + description = "notes — markdown blog + landing page"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + serviceConfig = { + WorkingDirectory = "/home/danny/notes"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8092"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # TDPixi — Idle Tower Defence Telegram Mini App by @plasmagoat. + # Pure static serve, no DB. Code rsync'd to /home/danny/tdpixi/. + # Upstream: https://github.com/plasmagoat/TDPixi + systemd.services.tdpixi = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + fastapi + uvicorn + ]); + in { + description = "tdpixi — Idle Tower Defence Mini App"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + serviceConfig = { + WorkingDirectory = "/home/danny/tdpixi"; + ExecStart = "${pythonEnv}/bin/python -m uvicorn server:app --host :: --port 8093"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + # Hara morning heartbeat — daily email check + Telegram good-morning ping. + # Runs claude in print mode with the Gmail MCP, then sends output via Bot API. + # Token lives in ~/.claude/channels/telegram/.env (managed by the telegram plugin). + systemd.services.hara-heartbeat = { + description = "Hara morning heartbeat (email check + Telegram ping)"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + path = [ pkgs.claude-code pkgs.curl pkgs.jq pkgs.gnused ]; + environment = { + HOME = "/home/danny"; + }; + serviceConfig = { + Type = "oneshot"; + User = "danny"; + Group = "users"; + WorkingDirectory = "/home/danny"; + EnvironmentFile = "/etc/claude-channels/env"; + }; + script = '' + set -euo pipefail + CHAT_ID="66070351" + BOT_TOKEN=$(grep '^TELEGRAM_BOT_TOKEN=' /home/danny/.claude/channels/telegram/.env | cut -d= -f2-) + MSG=$(${pkgs.claude-code}/bin/claude -p \ + "You are Hara, a concise cat-energy AI assistant. Read ~/.hara/HEARTBEAT.md. Check Gmail for all three accounts (danielth95, powerhouseplayer, wildstylewarrior) for urgent unread emails — security alerts, invoices, anything requiring a decision; skip newsletters and marketing. Compose a short message for Danny: flag urgent emails if any, otherwise just a brief check-in. One message, very short, cat energy." \ + --mcp-config /etc/hara/mcp-servers.json \ + 2>/dev/null | ${pkgs.gnused}/bin/sed 's/\*\*//g; s/\*//g; s/__//g; s/_//g') + ${pkgs.curl}/bin/curl -sf -X POST \ + "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \ + -H "Content-Type: application/json" \ + -d "{\"chat_id\": $CHAT_ID, \"text\": $(echo "$MSG" | ${pkgs.jq}/bin/jq -Rs .)}" \ + > /dev/null + ''; + }; + + systemd.timers.hara-heartbeat = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "06:07"; + Timezone = "Europe/Copenhagen"; + Persistent = true; + }; + }; + + # Forgejo — self-hosted Git forge. Phase 1 of the de-platform-from-GitHub + # roadmap (vimwiki/diary/2026-05-03.md). Public URL git.dannydannydanny.me + # is fronted by Caddy on vps-relay reverse-proxying over ZT to :3000 here. + # Auth for now: HTTPS + PAT (osxkeychain credential helper on the Mac). + # SSH disabled in Phase 1; revisit if push-via-https gets annoying. + # Backups: TODO — snapshot /var/lib/forgejo/ once it's up. + services.forgejo = { + enable = true; + database.type = "sqlite3"; # personal scale; one user, plenty + lfs.enable = true; + settings = { + DEFAULT.APP_NAME = "git.dannydannydanny.me"; + server = { + DOMAIN = "git.dannydannydanny.me"; + ROOT_URL = "https://git.dannydannydanny.me/"; + # Bind to all interfaces — firewall above scopes inbound to ZT. + HTTP_ADDR = "0.0.0.0"; + HTTP_PORT = 3000; + DISABLE_SSH = true; + }; + service = { + DISABLE_REGISTRATION = true; # admin-bootstrapped only + REQUIRE_SIGNIN_VIEW = true; # no anonymous browsing + }; + session.COOKIE_SECURE = true; + log.LEVEL = "Info"; + repository.DEFAULT_BRANCH = "main"; + }; + }; + + # Deploys flow through clan dm-pull-deploy: the dm-pull-deploy.path + # watcher rebuilds when sunken-ship announces a new origin/main rev. + # The legacy pull-based dotfiles-rebuild module was retired 2026-05-19. } diff --git a/nixos/hosts/sunken-ship.nix b/nixos/hosts/sunken-ship.nix index e305c03..c929d84 100644 --- a/nixos/hosts/sunken-ship.nix +++ b/nixos/hosts/sunken-ship.nix @@ -72,7 +72,7 @@ # x86_64-linux builds here via ssh-ng://danny@sunken-ship-zt). nix.settings.trusted-users = [ "root" "danny" ]; environment.systemPackages = with pkgs; [ - git # clone/bootstrap and dotfiles-rebuild timer + git # clone/bootstrap, repo-pull timers, dm-pull-deploy push brightnessctl # manual backlight; replaces removed `light` from nixpkgs uxplay # AirPlay mirroring receiver alsa-utils # aplay, amixer, arecord for audio debugging @@ -95,7 +95,10 @@ networking.firewall = { allowedTCPPorts = [ 7000 7001 7100 4533 ]; allowedUDPPorts = [ 5353 6000 6001 7011 ]; - interfaces."zt+".allowedTCPPorts = [ 8080 ]; + # 8080: bbbot HTTP backend. 8081: bbbot SHIPYARD STAGING (B3Bot beta). + # 8091: mulbo-server companion service. All ZT-only — see vps-relay.nix + # for the reverse proxies that expose them publicly. + interfaces."zt+".allowedTCPPorts = [ 8080 8081 8091 ]; }; # Navidrome — self-hosted music streaming server (Subsonic API). @@ -107,9 +110,25 @@ Address = "0.0.0.0"; Port = 4533; MusicFolder = "/srv/music"; + # Auto-delete `missing=1` rows during scan so transient files + # (e.g. mulbo dedupe quarantine ones) don't accumulate as stale + # track IDs that Substreamer caches and then 500s on. Without + # this, Navidrome keeps missing rows forever (default behaviour + # preserves play history; we trade that for client-cache hygiene). + # Valid values: never | always | full. `always` purges on every + # scan (selective + full); risk on transient missing is fine + # here (stable local disk). + Scanner.PurgeMissing = "always"; }; }; + # Navidrome's Subsonic API path field is tag-virtual; only the internal + # SQLite has real fs paths. mulbo-server reads navidrome.db ro to + # power /folders + POST /tracks resolution. UMask=0027 makes new DB + # files (and WAL rotations) group-readable; the tmpfile rule fixes the + # existing files written under the previous 0600 umask. + systemd.services.navidrome.serviceConfig.UMask = lib.mkForce "0027"; + # Persist the bind mount so navidrome can read music outside ProtectHome. fileSystems."/srv/music" = { device = "/home/danny/music"; @@ -139,23 +158,29 @@ }; }; - # BigBiggerBiggestBot — Telegram fitness tracker with Mini App. + # BigBiggerBiggestBot — Mini App backend (no Telegram polling). # Code: https://github.com/DannyDannyDanny/bigbiggerbiggestbot cloned at /home/danny/tg_fitness_bot - # Bot token: ~danny/.secrets/bigbiggerbiggestbot + # Bot token (used only for validating Telegram WebApp initData HMACs): + # ~danny/.secrets/bigbiggerbiggestbot # Deployment: fitness-bot-pull timer below runs every 15 min, git pulls, restarts service on changes. # # Mini App URL is fronted by Caddy on the vps-relay host at # https://bbbot.dannydannydanny.me (VPS → ZeroTier → localhost:8080). - # The bot's start.py honors WEBAPP_URL to skip starting its own - # cloudflared Quick Tunnel when we've got a stable URL from the VPS. + # start.py honors WEBAPP_URL to skip starting its own cloudflared + # Quick Tunnel when the stable URL from the VPS is already set. + # + # The slash-command bot (bot.py) was removed in May 2026 — the Mini App + # is now the only interface. No python-telegram-bot dependency required. + # ExecStartPost re-publishes the bot's chat-side presence (menu button, + # description, cleared command list) every time the service starts. + # Idempotent against the Telegram API. Errors are non-fatal (`-` prefix). systemd.services.fitness-bot = let pythonEnv = pkgs.python3.withPackages (ps: with ps; [ - python-telegram-bot python-dotenv aiohttp ]); in { - description = "BigBiggerBiggestBot Telegram fitness tracker"; + description = "BigBiggerBiggestBot Mini App backend"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; @@ -166,6 +191,7 @@ serviceConfig = { WorkingDirectory = "/home/danny/tg_fitness_bot"; ExecStart = "${pythonEnv}/bin/python start.py"; + ExecStartPost = "-${pythonEnv}/bin/python scripts/set-bot-presence.py"; Restart = "on-failure"; RestartSec = 10; User = "danny"; @@ -200,6 +226,278 @@ timerConfig.RandomizedDelaySec = "2min"; }; - # Auto-rebuild service/timer + safe.directory provided by the - # shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix). + # ── Shipyard staging — B3Bot beta tenant under shipyard_poc_bot ────── + # Mini-App-only HTTP server (no Telegram polling — shipyard_poc_bot on + # phantom-ship owns the polling loop; this service only validates Telegram + # WebApp initData HMACs against the shared bot token). + # + # Working dir: /home/danny/tg_fitness_bot_shipyard (separate clone of the + # same repo, gitignored workouts.db kept across pulls). + # Branch: origin/staging (push there to deploy here; push to origin/main for prod). + # Token file: /home/danny/.secrets/shipyard_poc_bot.env + # File contents: BOT_TOKEN= + # Service won't start until this file exists (ConditionPathExists). + # Mini App URL: https://b3.dannydannydanny.me (vps-relay Caddy → + # ZT IPv6 → here:8081). Stable across restarts — listed in + # ~/python-projects/26_shipyard/apps.json. + # Workflow: git push origin :staging → wait ~15 min → tap B3Bot + # beta in shipyard_poc_bot's launcher → test → git push :main. + systemd.services.fitness-bot-shipyard = let + pythonEnv = pkgs.python3.withPackages (ps: with ps; [ + python-dotenv + aiohttp + ]); + in { + description = "BigBiggerBiggestBot — SHIPYARD STAGING instance"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ pythonEnv ]; + environment.API_HOST = "::"; + environment.API_PORT = "8081"; + # Stable URL fronted by vps-relay's Caddy → ZT → here:8081. + # WEBAPP_URL set tells start.py to skip cloudflared entirely. + environment.WEBAPP_URL = "https://b3.dannydannydanny.me"; + unitConfig.ConditionPathExists = "/home/danny/.secrets/shipyard_poc_bot.env"; + serviceConfig = { + WorkingDirectory = "/home/danny/tg_fitness_bot_shipyard"; + EnvironmentFile = "/home/danny/.secrets/shipyard_poc_bot.env"; + ExecStart = "${pythonEnv}/bin/python start.py"; + Restart = "on-failure"; + RestartSec = 10; + User = "danny"; + }; + }; + + systemd.services.fitness-bot-shipyard-pull = { + description = "Pull shipyard fitness bot from origin/staging and restart if changed"; + path = with pkgs; [ git systemd ]; + environment.GIT_CONFIG_COUNT = "1"; + environment.GIT_CONFIG_KEY_0 = "safe.directory"; + environment.GIT_CONFIG_VALUE_0 = "/home/danny/tg_fitness_bot_shipyard"; + script = '' + set -euo pipefail + if [ ! -d /home/danny/tg_fitness_bot_shipyard/.git ]; then + echo "Shipyard working dir not bootstrapped yet — skipping pull." + exit 0 + fi + cd /home/danny/tg_fitness_bot_shipyard + git fetch origin + if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/staging)" ]; then + exit 0 + fi + git pull origin staging + systemctl restart fitness-bot-shipyard + ''; + serviceConfig.Type = "oneshot"; + }; + + systemd.timers.fitness-bot-shipyard-pull = { + wantedBy = [ "timers.target" ]; + # Offset from prod (07/15), mulbo (11/15), and dotfiles-rebuild. + timerConfig.OnCalendar = "*-*-* *:13/15:00"; + timerConfig.RandomizedDelaySec = "2min"; + }; + + # Mulbo companion service (Phase 5: uploads + dedup index + folders). + # Wire spec: ~danny/python-projects/20_mulbo/SERVER_API.md. + # Bootstrap (one-time): git clone git@github.com:DannyDannyDanny/python-projects.git /home/danny/python-projects + # (uses sunken-ship's id_ed25519 as a read-only deploy key on the repo) + # ZT-only via the firewall rule above (port 8091). Runs as `danny` so + # writes go through to /home/danny/music/mulbo-uploads, which Navidrome + # reads via the existing /srv/music ro bind-mount with no mount changes. + systemd.tmpfiles.rules = [ + "d /home/danny/music/mulbo-uploads 0755 danny users -" + # One-time fix for the existing navidrome.db (+ WAL/SHM) created + # under the old 0600 umask. UMask=0027 above keeps future writes + # group-readable. + "z /var/lib/navidrome/navidrome.db 0640 navidrome navidrome -" + "z /var/lib/navidrome/navidrome.db-wal 0640 navidrome navidrome -" + "z /var/lib/navidrome/navidrome.db-shm 0640 navidrome navidrome -" + ]; + + systemd.services.mulbo-server = let + pythonEnv = pkgs.python312.withPackages (ps: with ps; [ + fastapi + uvicorn + python-multipart + mutagen # tag writeback (enrich.write_tags); needed by the + # /enrich/revert endpoint which reuses enrich.py. + numpy # FFT for spectral-rolloff analysis (quality.py); used + # by chromaprint-dupe winner picker in --spectral mode. + ]); + in { + description = "Mulbo companion service (uploads, dedup, folders)"; + after = [ "network-online.target" "navidrome.service" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + # ffmpeg: PCM extraction for quality.py's spectral-rolloff probe + # (chromaprint-dupe winner picker in --spectral mode). Without it, + # the subprocess silently fails and rolloff returns 0Hz. + path = with pkgs; [ ffmpeg ]; + environment = { + MULBO_UPLOADS_DIR = "/home/danny/music/mulbo-uploads"; + MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_MUSIC_ROOT = "/srv/music"; # ro view via bind-mount; reads + hashing + MULBO_MUSIC_WRITE_ROOT = "/home/danny/music"; # underlying rw path; deletes + quarantines + MULBO_NAVIDROME_URL = "http://localhost:4533"; + MULBO_BIND_HOST = "::"; + MULBO_BIND_PORT = "8091"; + PYTHONUNBUFFERED = "1"; # immediate journal output + }; + serviceConfig = { + WorkingDirectory = "/home/danny/python-projects/20_mulbo"; + ExecStart = "${pythonEnv}/bin/python mulbo_server/app.py"; + Restart = "on-failure"; + RestartSec = 5; + User = "danny"; + # Read-only access to navidrome.db (+WAL/SHM) — see UMask override + # on the navidrome service above. + SupplementaryGroups = [ "navidrome" ]; + StateDirectory = "mulbo-server"; # /var/lib/mulbo-server, owned by danny + # Navidrome credentials — file format: KEY=value lines. + # Required keys: MULBO_NAVIDROME_USER, MULBO_NAVIDROME_PASS. + # Created manually on sunken-ship (mode 600, owned by danny): + # echo -e "MULBO_NAVIDROME_USER=DannyDannyDanny\nMULBO_NAVIDROME_PASS=..." > ~/.secrets/mulbo-server-navidrome + # chmod 600 ~/.secrets/mulbo-server-navidrome + EnvironmentFile = "/home/danny/.secrets/mulbo-server-navidrome"; + }; + }; + + # Pull mulbo (python-projects repo) and restart service if repo changed. + # Repo lives at /home/danny/python-projects (must be cloned manually first + # — see bootstrap note above). DBs/state live in /var/lib/mulbo-server, + # not in the repo, so they survive pulls. + systemd.services.mulbo-pull = { + description = "Pull mulbo repo and restart mulbo-server if changed"; + # openssh: `git fetch origin` over an SSH remote forks `ssh`; without + # it git dies with "cannot run ssh: No such file or directory" and the + # unit fails (shows up as system `degraded`). + path = with pkgs; [ git openssh systemd ]; + environment = { + GIT_CONFIG_COUNT = "1"; + GIT_CONFIG_KEY_0 = "safe.directory"; + GIT_CONFIG_VALUE_0 = "/home/danny/python-projects"; + }; + script = '' + set -euo pipefail + cd /home/danny/python-projects + git fetch origin + if [ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ]; then + exit 0 + fi + git pull origin main + systemctl restart mulbo-server + ''; + serviceConfig.Type = "oneshot"; + }; + + systemd.timers.mulbo-pull = { + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = "*-*-* *:11/15:00"; # every 15 min, offset from fitness-bot-pull and dotfiles-rebuild + timerConfig.RandomizedDelaySec = "2min"; + }; + + # dm-pull-deploy push automation. sunken-ship is the push node for the + # clan dm-pull-deploy instance (wired in flake-modules/clan.nix), but + # the upstream module only ships a manual `dm-send-deploy` binary — no + # scheduler. This timer announces the latest origin/main rev over + # data-mesher gossip; the watchers (dm-pull-deploy.path on sunken + + # phantom) compare and only rebuild when the rev actually changes, so + # re-announcing the same rev is a cheap no-op. This is the replacement + # for the legacy dotfiles-rebuild pull timer (being retired). + # + # dm-send-deploy self-discovers the rev via `git ls-remote` and signs + # with /run/secrets/vars/dm-pull-deploy-signing-key — needs root. + systemd.services.dm-pull-deploy-push = { + description = "Announce latest origin/main rev via data-mesher (dm-pull-deploy push)"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "/run/current-system/sw/bin/dm-send-deploy"; + User = "root"; + }; + }; + + systemd.timers.dm-pull-deploy-push = { + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = "*-*-* *:04/15:00"; # every 15 min, offset from the other pull timers + timerConfig.RandomizedDelaySec = "2min"; + timerConfig.Persistent = true; + }; + + # One-shot backfill: walks Navidrome's media_file, computes + # (sha256, chromaprint) per file, populates mulbo-server's tracks_index + # with the corresponding navidrome_track_id. Idempotent — existing rows + # left alone. Without this, /tracks/by-hash misses for every existing + # offshore track and `mulbo reconcile-local` duplicates content. + # + # Trigger manually: sudo systemctl start mulbo-server-backfill + # Follow progress: journalctl -fu mulbo-server-backfill + systemd.services.mulbo-server-backfill = let + pythonEnv = pkgs.python312.withPackages (ps: with ps; [ ]); + in { + description = "Backfill mulbo-server tracks_index from Navidrome catalog"; + after = [ "mulbo-server.service" ]; + requires = [ "mulbo-server.service" ]; + path = [ pkgs.chromaprint ]; # provides fpcalc + environment = { + MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_NAVIDROME_DB = "/var/lib/navidrome/navidrome.db"; + MULBO_MUSIC_ROOT = "/srv/music"; + PYTHONUNBUFFERED = "1"; + }; + serviceConfig = { + Type = "oneshot"; + WorkingDirectory = "/home/danny/python-projects/20_mulbo"; + ExecStart = "${pythonEnv}/bin/python mulbo_server/backfill.py"; + User = "danny"; + SupplementaryGroups = [ "navidrome" ]; # ro access to navidrome.db + StateDirectory = "mulbo-server"; # so /var/lib/mulbo-server/index.db stays writable + TimeoutSec = "8h"; # full backfill on 274 GB ≈ 1h, leave headroom + }; + }; + + # Phase 7.5 enrichment one-shot. For tracks where Navidrome's tags + # are empty/Unknown, runs three sources (filename heuristics, yt-dlp + # for SoundCloud `[]` patterns, AcoustID+MusicBrainz), votes the + # results, and writes back via mutagen with strict-replacement + # (never touches user-set tags). + # + # Trigger: sudo systemctl start mulbo-server-enrich + # Follow progress: journalctl -fu mulbo-server-enrich + systemd.services.mulbo-server-enrich = let + pythonEnv = pkgs.python312.withPackages (ps: with ps; [ + mutagen # tag writeback + ]); + in { + description = "Enrich Navidrome tracks with empty/Unknown metadata"; + after = [ "mulbo-server.service" ]; + requires = [ "mulbo-server.service" ]; + path = with pkgs; [ yt-dlp chromaprint ]; # yt-dlp for SC/YT lookups, chromaprint for AcoustID's -plain fingerprint + environment = { + MULBO_INDEX_DB = "/var/lib/mulbo-server/index.db"; + MULBO_NAVIDROME_DB = "/var/lib/navidrome/navidrome.db"; + MULBO_MUSIC_ROOT = "/srv/music"; + MULBO_MUSIC_WRITE_ROOT = "/home/danny/music"; + PYTHONUNBUFFERED = "1"; + }; + serviceConfig = { + Type = "oneshot"; + WorkingDirectory = "/home/danny/python-projects/20_mulbo"; + ExecStart = "${pythonEnv}/bin/python mulbo_server/enrich.py"; + User = "danny"; + SupplementaryGroups = [ "navidrome" ]; + StateDirectory = "mulbo-server"; + # Add MULBO_ACOUSTID_KEY to the secrets file to enable the + # AcoustID source. yt-dlp source needs no key. Filename source + # needs nothing. + EnvironmentFile = "/home/danny/.secrets/mulbo-server-navidrome"; + TimeoutSec = "8h"; + }; + }; + + # Deploys now flow through clan dm-pull-deploy: the dm-pull-deploy-push + # timer above announces origin/main, and the dm-pull-deploy.path watcher + # rebuilds on change. The legacy pull-based dotfiles-rebuild module was + # retired 2026-05-19. } diff --git a/nixos/hosts/vps-relay.nix b/nixos/hosts/vps-relay.nix index f7bf7b0..cedcbfa 100644 --- a/nixos/hosts/vps-relay.nix +++ b/nixos/hosts/vps-relay.nix @@ -46,8 +46,13 @@ isNormalUser = true; extraGroups = [ "wheel" ]; openssh.authorizedKeys.keys = [ - # Same pubkey used to reach sunken-ship; set at install via clan. - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@sunken-ship" + # Mac admin key (~/.ssh/id_ed25519_sunken_ship on the laptop — the + # key the Mac uses to reach the fleet). Used for `clan machines + # update vps-relay` from the Mac and at install via clan. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@mac-admin" + # sunken-ship's own key, so the push node can SSH into vps-relay + # over ZeroTier for mesh introspection / debugging. + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB9t4YAaoHvVouqp+qyFOq8o3SAtXMiAmjF6J0ldyx4g danny@sunken-ship" ]; }; users.users.root.openssh.authorizedKeys.keys = @@ -101,6 +106,74 @@ "bbbot.dannydannydanny.me".extraConfig = '' reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:8080 ''; + # B3Bot beta — bbbot's staging tenant under shipyard_poc_bot. + # Same backend host as bbbot prod, port 8081. + "b3.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:93d5:53a2:de33]:8081 + ''; + # Shelfish — phantom-ship's ZT IPv6. + "shelfish.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8081 + ''; + # Scuttle — same backend, different port. WebSocket upgrade is + # transparent under reverse_proxy. + "scuttle.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8082 + ''; + # Bananasimulator — same backend, port 8083. + "bananasimulator.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8083 + ''; + # Bananasimulator BETA — separate service on port 8084 with + # BS_BETA_MODE=1 (cheat menu + faster ripening for testing). + "bananasimulator-beta.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8084 + ''; + # KomTolk (formerly translate-platform) — same backend, port 8080. + "komtolk.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8080 + ''; + # Forgejo on phantom-ship — Phase 1 of the de-platform-from-GitHub + # roadmap (vimwiki/diary/2026-05-03.md). + "git.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:3000 + ''; + # Escape Hormuz — turn-based boat-race Mini App, port 8090. + "escapehormuz.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8090 + ''; + # bon — receipt scanner Mini App, port 8091. Camera capture in + # the WebView needs HTTPS, which Caddy terminates here. + "bon.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8091 + ''; + # TDPixi — Idle Tower Defence Mini App by @plasmagoat, port 8093. + "tdpixi.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8093 + ''; + # notes — markdown blog (notes.X) + apex landing (X). Same backend + # service on phantom :8092 routes by Host header. + "notes.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; + "dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; + # kf — Kyranna Fardi architecture portfolio. Same notes service on + # phantom :8092, routed by Host header (PORTFOLIO_HOST). + "kf.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; + # map — curated-architecture world map by Kyranna. Same notes + # service on phantom :8092, routed by Host header (MAP_HOST). + "map.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; + # studio — Kyranna's private art-learning archive. Same notes + # service on phantom :8092, routed by Host header (STUDIO_HOST). + "studio.dannydannydanny.me".extraConfig = '' + reverse_proxy http://[fdd5:53a2:de33:d269:6499:936c:48a:bbdc]:8092 + ''; }; }; diff --git a/nixos/neovim.nix b/nixos/neovim.nix index 59a6f85..51ae100 100644 --- a/nixos/neovim.nix +++ b/nixos/neovim.nix @@ -41,6 +41,7 @@ end -- General options + vim.opt.cursorline = true vim.opt.mouse = "a" vim.opt.listchars = { tab = "→ ", space = "·", nbsp = "␣", trail = "•", eol = "¶", precedes = "«", extends = "»" } vim.opt.clipboard:append("unnamedplus") @@ -57,6 +58,39 @@ end, }) + -- Treesitter highlighting: parser-driven syntax highlighting (richer + -- than the regex-based default). Leaving `indent` off — it's still + -- buggy in several languages (python, yaml). + require'nvim-treesitter.configs'.setup { + highlight = { enable = true }, + } + + -- Sticky scroll: pin enclosing scopes (functions, classes, YAML keys, + -- etc.) to the top of the window as you scroll deeper. Same idea as + -- Zed/VS Code's "Sticky Scroll". `mode = 'topline'` matches Zed's + -- "scrolled past" feel; switch to 'cursor' if you'd rather it track + -- the cursor instead of the viewport. + require'treesitter-context'.setup { + enable = true, + max_lines = 5, + mode = 'topline', + trim_scope = 'outer', + } + + -- Fish: expand tabs to spaces. Fish renders raw \t in the commandline + -- as the Unicode glyph ␉ (U+2409) and wrap-indents each line to the + -- column of the opening quote, which mangles Alt-E multiline edits. + -- Using spaces sidesteps the issue entirely. + vim.api.nvim_create_autocmd("FileType", { + pattern = "fish", + callback = function() + vim.opt_local.expandtab = true + vim.opt_local.tabstop = 2 + vim.opt_local.shiftwidth = 2 + vim.opt_local.softtabstop = 2 + end, + }) + -- Keymaps vim.keymap.set("n", "S", ":%s//g", { desc = "Replace all" }) vim.keymap.set("n", "w", ":w", { desc = "Save file" }) @@ -72,6 +106,8 @@ catppuccin-nvim # theme goyo-vim # write prose limelight-vim # prose paragraph highlighter + nvim-treesitter.withAllGrammars # parsers (also makes vim.treesitter.foldexpr work for markdown) + nvim-treesitter-context # sticky scroll: pin parent scopes at top of window ]; }; } diff --git a/nixos/pkgs/hara-gmail-mcp/default.nix b/nixos/pkgs/hara-gmail-mcp/default.nix index e82523e..6d62d10 100644 --- a/nixos/pkgs/hara-gmail-mcp/default.nix +++ b/nixos/pkgs/hara-gmail-mcp/default.nix @@ -6,7 +6,7 @@ python3Packages.buildPythonApplication { pname = "hara-gmail-mcp"; - version = "0.1.0"; + version = "0.2.0"; pyproject = true; src = ./.; nativeBuildInputs = [ python3Packages.setuptools ]; diff --git a/nixos/pkgs/hara-gmail-mcp/pyproject.toml b/nixos/pkgs/hara-gmail-mcp/pyproject.toml index b2a985e..fb1db6d 100644 --- a/nixos/pkgs/hara-gmail-mcp/pyproject.toml +++ b/nixos/pkgs/hara-gmail-mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "hara-gmail-mcp" -version = "0.1.0" +version = "0.2.0" description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)" requires-python = ">=3.11" dependencies = [ diff --git a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py index 34f2e29..de204e6 100644 --- a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py +++ b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/imap_client.py @@ -153,6 +153,34 @@ def read_email( ) +def mark_read( + store: AccountStore, + email_addr: str, + uid: str, + mailbox: str = "INBOX", +) -> None: + """Mark a message as read by adding the \\Seen flag.""" + account = store.get(email_addr) + password = store.password_for(email_addr) + with _open(account, password, mailbox) as conn: + conn.uid("store", uid, "+FLAGS", r"(\Seen)") + + +def archive( + store: AccountStore, + email_addr: str, + uid: str, + mailbox: str = "INBOX", +) -> None: + """Archive a message: copy to All Mail then delete from INBOX.""" + account = store.get(email_addr) + password = store.password_for(email_addr) + with _open(account, password, mailbox) as conn: + conn.uid("copy", uid, "[Gmail]/All Mail") + conn.uid("store", uid, "+FLAGS", r"(\Deleted)") + conn.expunge() + + def _fetch_summary(conn: imaplib.IMAP4_SSL, uid: str) -> MessageSummary: typ, data = conn.uid( "fetch", diff --git a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py index 797d41c..0310786 100644 --- a/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py +++ b/nixos/pkgs/hara-gmail-mcp/src/hara_gmail_mcp/server.py @@ -1,14 +1,15 @@ """Hara Gmail MCP server. -Exposes a small toolset for reading and (later) replying to mail across -the configured Gmail accounts. v1 ships read-only tools; reply/archive/label -follow once Hara is using these reliably. +Exposes a small toolset for reading and writing mail across the configured +Gmail accounts. Tools: list_accounts() list configured accounts list_inbox(email, limit) recent messages from an account search(email, query, limit) IMAP SEARCH wrapper read_email(email, uid) full body of one message + mark_read(email, uid) mark a message as read + archive(email, uid) archive a message (remove from INBOX) """ from __future__ import annotations @@ -21,7 +22,7 @@ from dataclasses import asdict from mcp.server.fastmcp import FastMCP from .accounts import AccountStore -from .imap_client import list_inbox, read_email, search +from .imap_client import archive, list_inbox, mark_read, read_email, search logger = logging.getLogger("hara_gmail_mcp") @@ -92,6 +93,36 @@ def gmail_read_email(email: str, uid: str) -> str: return json.dumps(asdict(msg), ensure_ascii=False) +@mcp.tool() +def gmail_mark_read(email: str, uid: str) -> str: + """Mark a message as read (sets the \\Seen flag). + + Args: + email: which configured account + uid: the message UID (returned by gmail_list_inbox or gmail_search) + + Returns: + JSON object with ok and uid. + """ + mark_read(_get_store(), email, uid=uid) + return json.dumps({"ok": True, "uid": uid}) + + +@mcp.tool() +def gmail_archive(email: str, uid: str) -> str: + """Archive a message (copies to All Mail, removes from INBOX). + + Args: + email: which configured account + uid: the message UID (returned by gmail_list_inbox or gmail_search) + + Returns: + JSON object with ok and uid. + """ + archive(_get_store(), email, uid=uid) + return json.dumps({"ok": True, "uid": uid}) + + def main() -> None: logging.basicConfig( level=os.environ.get("HARA_GMAIL_LOG_LEVEL", "INFO"), diff --git a/scripts/build-installer-iso-on-server.sh b/scripts/build-installer-iso-on-server.sh index d969b68..e7bd002 100755 --- a/scripts/build-installer-iso-on-server.sh +++ b/scripts/build-installer-iso-on-server.sh @@ -5,12 +5,17 @@ # host: SSH host (default: sunken-ship) # output_dir: where to save the ISO on your Mac (default: .) # Override SSH key: SSH_KEY=~/.ssh/my_key ./scripts/build-installer-iso-on-server.sh +# +# If nixos/installer-wifi.nix exists locally (gitignored), it is copied into +# the build and the ISO gets preconfigured live-system WiFi. flake-modules/ +# installer-iso.nix auto-includes it via a builtins.pathExists check. set -euo pipefail HOST="${1:-sunken-ship}" OUT="${2:-.}" +REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) -# Use sunken-ship key if not set (AGENTS.md) +# Default to the sunken-ship SSH key when targeting that host. if [[ -n "${SSH_KEY:-}" ]]; then SSH_OPTS=(-i "$SSH_KEY") elif [[ "$HOST" == "sunken-ship" ]] && [[ -f ~/.ssh/id_ed25519_sunken_ship ]]; then @@ -19,23 +24,37 @@ else SSH_OPTS=() fi -echo "Pushing branch so server can pull..." -git push origin server-installer-usb 2>/dev/null || true +echo "Pushing main so the server can clone the latest..." +git -C "$REPO_ROOT" push origin main 2>/dev/null || true -echo "On $HOST: clone branch, build ISO..." +echo "On $HOST: clone main into ~/dotfiles-iso-build..." ssh "${SSH_OPTS[@]}" "$HOST" 'set -e BUILD_DIR=~/dotfiles-iso-build rm -rf "$BUILD_DIR" - git clone --branch server-installer-usb https://github.com/DannyDannyDanny/dotfiles.git "$BUILD_DIR" - cd "$BUILD_DIR/nixos" + git clone --branch main https://github.com/DannyDannyDanny/dotfiles.git "$BUILD_DIR" +' + +# Optional live-system WiFi: the module is gitignored, so a fresh clone never +# has it. Copy it in and stage it (git add -f) so the flake sees it -- a flake +# build only includes git-tracked files. +if [[ -f "$REPO_ROOT/nixos/installer-wifi.nix" ]]; then + echo "Found nixos/installer-wifi.nix - including live-system WiFi in the ISO." + scp "${SSH_OPTS[@]}" "$REPO_ROOT/nixos/installer-wifi.nix" \ + "$HOST:dotfiles-iso-build/nixos/installer-wifi.nix" + ssh "${SSH_OPTS[@]}" "$HOST" 'cd ~/dotfiles-iso-build && git add -f nixos/installer-wifi.nix' +fi + +echo "On $HOST: build ISO (flake is at the repo root)..." +ssh "${SSH_OPTS[@]}" "$HOST" 'set -e + cd ~/dotfiles-iso-build nix build .#installer-iso ls -la result/iso/ ' -ISO_NAME=$(ssh "${SSH_OPTS[@]}" "$HOST" 'ls ~/dotfiles-iso-build/nixos/result/iso/*.iso 2>/dev/null | head -1') +ISO_NAME=$(ssh "${SSH_OPTS[@]}" "$HOST" 'ls ~/dotfiles-iso-build/result/iso/*.iso 2>/dev/null | head -1') ISO_NAME=$(basename "$ISO_NAME") echo "Copying $ISO_NAME to $OUT ..." -scp "${SSH_OPTS[@]}" "$HOST:~/dotfiles-iso-build/nixos/result/iso/$ISO_NAME" "$OUT/" +scp "${SSH_OPTS[@]}" "$HOST:dotfiles-iso-build/result/iso/$ISO_NAME" "$OUT/" echo "Done. ISO at $OUT/$ISO_NAME" echo "Write to USB: diskutil unmountDisk diskN && sudo dd if=$OUT/$ISO_NAME of=/dev/rdiskN bs=4m" diff --git a/sops/machines/distant-shore/key.json b/sops/machines/distant-shore/key.json new file mode 100755 index 0000000..f580056 --- /dev/null +++ b/sops/machines/distant-shore/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "type": "age" + } +] \ No newline at end of file diff --git a/sops/machines/foreign-port/key.json b/sops/machines/foreign-port/key.json new file mode 100755 index 0000000..6aa3307 --- /dev/null +++ b/sops/machines/foreign-port/key.json @@ -0,0 +1,6 @@ +[ + { + "publickey": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "type": "age" + } +] \ No newline at end of file diff --git a/sops/secrets/distant-shore-age.key/secret b/sops/secrets/distant-shore-age.key/secret new file mode 100644 index 0000000..27a5780 --- /dev/null +++ b/sops/secrets/distant-shore-age.key/secret @@ -0,0 +1,14 @@ +{ + "data": "ENC[AES256_GCM,data:WTerGWNmve9/q+TLYi8HoGUQI0UgYMZN2zuC/FABX0MC6VuUsz9Doz36X8lsy+MRJzcHNPqdaHmAHopY/hODHLBirfUPLVZjEKI=,iv:ilp+cJivxY2us1jO85dWUHAqLJSsJ7ZKpmYMyi2476I=,tag:H0k2CZDhcH9lYSxz6BAPrg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZS2ZhQlJacmR4R3JMek5l\nZlFRajM4VllmK2R6NFlRMEkwNUJOL05OUUhzCmpWQ0gxQ1BHUkZOVm80QzRUc1BY\nTDNRZDZOL3EyS1FWK1A4UUd6MTFaTjAKLS0tIEUxU3hBSkZqRmc5d0dXZm0rNTYw\nQ0hrZUF5dDJLN0MvM2RQZlVFZkNPc28Kvq8yV+VwqQIuG1SPI/mMYbGwuD7oUOeR\nCzAuZvqGtludjW7+wX5uIwRzHMudU/yP/iME8vsDC3dL6sf75+arHg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:36Z", + "mac": "ENC[AES256_GCM,data:g35f5YmoneVewxmTh3E8ECDGAl0OwUj4v/2bjFs9Dd7MaT3in7PHvu30jJ4WHalYC8pkT5IlpBwsp1nVUnKsgh+2V+jN4JiGizlvTwByaYoalOoGZStIyQa+k8XRQqoUDbV3ESdI5q+dwS5PCWYIOH3MoA0o5b42iQPghrViaeY=,iv:v0UUy4LtQ5SRLB01vbcfNpcm8zgs1Vp3KCK552peXlA=,tag:45b8czXYtNh02q7P42FJmQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/sops/secrets/distant-shore-age.key/users/danny b/sops/secrets/distant-shore-age.key/users/danny new file mode 120000 index 0000000..215639b --- /dev/null +++ b/sops/secrets/distant-shore-age.key/users/danny @@ -0,0 +1 @@ +../../../users/danny \ No newline at end of file diff --git a/sops/secrets/foreign-port-age.key/secret b/sops/secrets/foreign-port-age.key/secret new file mode 100644 index 0000000..2ba1f0f --- /dev/null +++ b/sops/secrets/foreign-port-age.key/secret @@ -0,0 +1,14 @@ +{ + "data": "ENC[AES256_GCM,data:MH/ib8WAbzucbm2dhhoo6ESSSLKtKMWmjUwtpAOZhU7KyhOoechpJRSkBBmFV4LzbSP1qeaFbid6USJBnRsxkoz6XvhMzP0kzS0=,iv:9sPwc/JIlo5mzxelNzLCB26k2f+n2C9tB8Y/HEdPvHw=,tag:hJBhzTMsTWd9PDydS4aosg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6L2JGejlzQlhiNFhES1lT\nNTZsNFFMT1NzZEV0T28rNDA3STJ1UXNRcUZFCkxoenpFYWJicHpGVDhtMUdwNXBo\nS29EazVsRGFST2ZodDJMTkxQN2I1RjAKLS0tIFpib0RoOTJ6bkU0b1F6NnRaV3lF\nVHhvYjNOUUtMbGF5ejdaVk5WdGt2d1kKNU5JR1nIYPQLALUM3wRO945Sk6GLxJpn\nTVmVUEgXcwHcSij10a/cQOyPXNNnsfIC+WJFMJcjHfsjBnwS5W/Bgw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:mVobAhXUhbs49+g0bXfi4TjPG667F7pM8Kk518a7kRZ/HtN2kLYcSyl3XpspTosAs4x3QbFQUbFCgsBgqx+gS6xlw3OAJXM3iG2fNu2qoj9Q7viAEHoWVHwT+ftjA0qVTUf0BDD1r4ow6BNhe6kQy5bQqVu0MhjDfsK9BNTXAu4=,iv:aFHo3bQKgr1XSnwGUajkSFa4UftTWdZbPtXY05N7qOM=,tag:VymYJf4XFLaEGvxQmvF6rA==,type:str]", + "version": "3.12.2" + } +} diff --git a/sops/secrets/foreign-port-age.key/users/danny b/sops/secrets/foreign-port-age.key/users/danny new file mode 120000 index 0000000..215639b --- /dev/null +++ b/sops/secrets/foreign-port-age.key/users/danny @@ -0,0 +1 @@ +../../../users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/machines/distant-shore b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/secret b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/secret new file mode 100644 index 0000000..58ead86 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:esTlopK7VkLLnWvxsLoZtAGgbYKWKfu0XJde2fzxDuOaf9yUCU6NHpnyRAZnChceEZ3frwS7Lh/LWqX9CTKQ1LHTV8HrJERSERDzrQDHbIXFLtDbeF+qN7M1wYFEwCUa8PVAg4XHMN/ZGy6H71+J8UrktcbxcHUr+8L3pj4DZb5930kT3U02rzSoan8zb4zMhGqA0keq9QJ04uNJEN2Bly1kCBvdgc7kVUBNHwS78s+jfsa3PyOiLy5AI4CEbQ5r/xBjNgY/aSEOzRMoZtVWUFlh5Kxc47gz7MlK2x/2iXyCIAw3qeTIxor30GIL,iv:QbSPukR5aMrhBfYOM6lOb0qSEPm4oEqqQp59WDv0p6Y=,tag:KrMyGleLjIhT1LTlS3S63g==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyWlpRS1hhQThqaFUyNE4y\nYU1YVDFtazZnSHpTOWRFQkZYVThsRk9RQ1RZCnI4ZlFacTRRSHlub2hWVTNSSkhN\ndWExR202RG1nZ2dQTzQ5LzBNNW1kc2sKLS0tIHZlZXNhSm9wdElTZzRXZjQxaDAx\nRXpvcEkwK3dMNHZ2M21OSFluWnhDOFEKv0/yC/Llmhsm3+kV3AUJ2PPF817rOyL5\n6GkqTrb/gB8q8jnQabDr2sHUz7AB4w7zlQaNLRSo3Ba8KFbW7GZNRg==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvcEdVdTBsUThiOE1EVWZW\nYXh2NUNCZTVieUZKbjByY2dSZVU2c0Q4L2hBCm1mNzVrcTRTTFpUTkJDZlArYTBZ\nWXhEMERmd1J3VTYxa2dWTlFxOW45N00KLS0tIHNLVzRCdDJGdWk2K0JoY1dJbzIz\nQU9DR2tXU3l2aU9YMGd1RjBGbUJYM0UKYmdAj535wvaGxN6m2VBBVtWRAD5RzQ7K\nbiJjvf8NH4A0aO9RVTFCevqRXUOKBu7jNIpFFfEyUEGHEUCWOuVSlA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:36Z", + "mac": "ENC[AES256_GCM,data:QVkNUsAO6BsVoPAL5GG1/DProapF8ryaUGDr8Y8mYPpD1Y2YXAF2sBRJ4FWkFZkWl4L2sp5DLXfqs+z0tpvi6rpG0jfpgJzy3Du2QKnk5W78WENlK+M74tSzAUfCUPn6RodykLJ8ik+EvxR+yxRmfjStAWsS6eqoTYowa4TGeJ0=,iv:qousMcaNKMtl8hGcfiS1WYBe0ftyb9ohHdBG+gqTio0=,tag:j64zgZpB7cmDfPcq4csjMQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/users/danny b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.cert/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/machines/distant-shore b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/secret b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/secret new file mode 100644 index 0000000..50edf19 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:kAzaF+nxyux0zwjoqC5QYrx5UyEhMPW0v9hGcYUXExZl6ShMMgCWhKN82al2jY6OnU/CQ7UT9USH6PC+eecimyM6A5YXQ0GvvU3uus0t46GKqXqcGVl4BdgVO6tm8ienIcfjF6ml3LyvMXirjDdIluVkrH/P0vM=,iv:BSQrtg9kgBHRkCV8+nODNyPX3PchkTEjPPTYy5JZrfo=,tag:dPtjxWqDh1Bce9rlW6czyw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMNkR1YTZNU1FyU2VWaUJR\nbjBOc0RSMW1SL1ZkZ1ozVHRmcVdkS01sdkZnCndTbGJlOVFrdDJHVDUxS1JFUmcy\nZS9jWGhRbWFCeGZOMHQwdzYxTFlrSjgKLS0tIEFaZmFzOXdXVjVOeUJuMDdpQ3hK\ndnRkUytmZk1zaXhUTSt1OTljUkNYK2MKpe6f3GHGCfduiidzYh0qaKEBaKyBZY4s\ne/f5QvZVApMiI4HFkOwFmNITOv6JdjGMQOw+OI6po0nqg0mqVnNIVA==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpa0p1NCtUeGFLRzJFOFQv\nVXh4MThqOVR4TU9SK3Mrc21Ga1BPdUZrM1c0CmJxQzNyam56aTdUVVB5NVhEenlV\nOTkwb2YyRWdoVXc4K2VEaXhwZXM3TEUKLS0tIEFBajAycEQzelNoR2tCU3l6cVJo\nc3lHbWJZQWFQTkVxd0lBamxlQStZWlkKopG1Z2E0Smt/z/y1+cQeTKUKyJKBXzZr\nCQNkGfi1Dk/7n/WeKwePHWVF/19WqVfOIZW0E3tOKOIqDQZa0Io1Nw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:36Z", + "mac": "ENC[AES256_GCM,data:B6UAFOrK0QIngkf5OA3+BnLAouBvsr0AbW8lKI8RH7ylGQNOyXfnN06fYshi+jQyu5EAZBqovfSZzgcTDm7MDRAjzzmTToT5ekHPZnquleU/F7pF/D7JF78M6rQyw3uG0nwhnJcRqlCAXy+56++kTJhoKEW+B5fUsbvlHTmxwLk=,iv:BXDbLObPBXsL3Uj+TRwIFtNDRzWYJeM0mJyDDluz70s=,tag:eTANaLmNaUUSYBNcIhuIFQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/users/danny b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/identity.pub/value b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.pub/value new file mode 100644 index 0000000..8f2058b --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/identity.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAABhcRTNvFEyWkyRBX17KkM5nDuqOvR1xTY5vDqTygvk= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/distant-shore/data-mesher-node-identity/peer.id/value b/vars/per-machine/distant-shore/data-mesher-node-identity/peer.id/value new file mode 100644 index 0000000..f748b68 --- /dev/null +++ b/vars/per-machine/distant-shore/data-mesher-node-identity/peer.id/value @@ -0,0 +1 @@ +12D3KooW9pjiKnqmnHSwGRhgyUqKeFydDUE8RvYJDAqHb5PZvzue \ No newline at end of file diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/machines/distant-shore b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/secret b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/secret new file mode 100644 index 0000000..7c824e7 --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:tLR5iZ7Iro3BuBJlpvkKO7RrA9X2pO2H9Isi6jc8y8krh+a89Eug0PCNb4U/aSASjQgDfZgwg9+SU1y4iIoc3qC4sxw3f4uTdjCWRDEgfAvY3DVWiWI/EbWcfX7bVvl/GCQtHSwBW5z3KwhJV2McLK6Fpblx6fM=,iv:exFXncN3SA9zqSTFxX6o3kstwMGL9y8x0IOqJVNqK+I=,tag:dEkDG3meaWoq74hkRHbplg==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3YWliNzFKWnZqdnZYeHRs\nVy8wdk1ZZWpJTlg2WWc4eEpUM2diZEFoamg0CnJnVDZJT3lWaUZlV3NHY1NpN0tW\nMXdRTnNGSjBhSFpLY0xvaER6UDI5RlEKLS0tIDloRHJFV2I1RVN6TXh6dmd5dzV0\nZ1AvZmpOM0VkaVZjNHlFdFBNd0FhTVkKEVFjtN66i+8f7P03ODYgoWZsTUiEcPiL\nYaV4UZKbjnp3SKTAeWk1P/lEj5DkicW3hq0ONQf2xrYriCpAc3/gKw==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0VG1wZE52RFQ2ekl1R1ZY\nN2pmYWd1cStGZzU0Tk1LTmNuMnc5c1UwMnlVCmJrMjB6Qzc5ZUE3aXhmUmVuTTN1\neVFWbGhOUUNYUFJJVHF0OGtaR1FvVG8KLS0tIFp4aHgwN1QvWVVnaTNDVG42SXVB\nYVlLRndmQ1ovQjFMcHZYU0dqNS9ML2sKtHjmgLODafDcmrpYQyXRc/ajAR2saGs8\nlh4NVYYYwoXE6sNKSXwzgXXSjGEXGTVLxVvp9OKnSloI5/LsbrxANQ==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:37Z", + "mac": "ENC[AES256_GCM,data:20RiSc6b3o3xy23NDQRw4pcSf/akdcUMO6ciSFSZMQrhreYPBEa+Tb85qqqZ0dqQHRQFanzE3Usomp+Ux4FhFfSsCxljxdOjkQCAfkQKrg+GQ7/NOUhgdVQtep2+gT7MFrEzo5Jv8kctNuT18kqUjv5CwCOR35QJ98yHeAUULoo=,iv:ocUDNN4vhOX9pCUJKqQiBRhjTHbdRdw96csN6EWFdUg=,tag:Lps0aJy0ctWU5ilCUn9Uww==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/users/danny b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.pub/value b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.pub/value new file mode 100644 index 0000000..ea47b41 --- /dev/null +++ b/vars/per-machine/distant-shore/dm-pull-deploy-status-key/signing.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAPVF7m/+s1YroGdvSMxPwKmenJjk4yNrP8tNtZGHEhJI= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/machines/distant-shore b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/machines/distant-shore new file mode 120000 index 0000000..2f4e8ad --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/machines/distant-shore @@ -0,0 +1 @@ +../../../../../../sops/machines/distant-shore \ No newline at end of file diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/secret b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/secret new file mode 100644 index 0000000..ed43256 --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:9BN/+IBbsAmgABYuTEZvgB3cJOwiZ1aKu5GqcBEvCBoY3K4T5lDPqHrwdH48msu9/KD435SSz336+Stq8bQB87AXdfDMEhVIUwi8SV/CQg3urXvyqp0+lkbbrP9xyFzcH16L7NDmfD/SlZeFXQoPA3YHLvoYSsWnfjzHqrt0600IhAgq0TK+c+5hCzke9k89pgOrO6ypueHV+6GMx0g4JMcwq17bqT3fOQZ+hHSp9uOWDP1kJrO2TktwR/9AWAN+IG1sjUcaKYg+W34pG4XDkNPnp30NPfXSGMXjrM++MkIxyow1zFeSRI+bP5iLQEFpm1AvFFRdYIGN66hQVCgv0kxaOEJknlrG4QT4TyEJ,iv:MUsdjMEBvuaFkJJ6t3NNDrgECjheLJ0FtdrBsztOKZ8=,tag:lTcmyWAoKYPUhDjkHTd+Iw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLQ0Y3Y1N5aEo5ZDQwZ2g4\neXBDMldtWU42cUFaaTBmS1B6YW5QWktNcVZvCjBMYmNKWjR6cmVIRjhNK2Y2aWg5\nM093ZFhFWW0yZnVrOUxGQ3MzSGY5UkUKLS0tICtTbHFTMUtGQWEycGNDNFlXcTBS\nWmNWbDZSNE5sWUpzQ0dTNTgyemhNdzgKdPZIFY/m3IpEMH1PGsYToyLe9Qzj6LpW\nJhOTJbT9L0dTfE3OzdaG8BkwCkb8XCWxzveLPTLPCOvbP8DmOpjjHA==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvcXQ4NjE4d00ramdsemhI\nb1dxcldHS040TkVyL2lxUjdxL3J1WUlCdEZNCnExMDRqcmh5MGUxNFpJd3k4MzZT\nMXljSW5ncWxlSGRsYlJBdkoxQjIyZHMKLS0tIEhUSkRpeXhOM3BnTEsrNEpDb1I2\nUlhvZzFjRVNCcng2c3lsYS8vZHVHN00KFMMGm6BJY7/cn5WSP/RgjK6bVo4r7ps2\nkMcPoyMyenPiZrzWdL4iIb5azFB3CI8DAQS84Mt6KPR/wkYNoErxJg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T16:36:38Z", + "mac": "ENC[AES256_GCM,data:Cy3KGFXu58LAWSCUYJGpMeJxBboQxEPS1TzoK8iCFUyTT7Xfak9M9omaBd2r2fEel61iuSDVoDvQbZgNy2RwuiG0HhTXliMXR6G4oOheQIsSQix81tOWoPipu77qoeVkOSUDRhBzHdQVQQmiN7VJvw1kHvCq20u2ZM0057vf91g=,iv:uAmwqd0gpCD7pTFWwgKdkKjjxVadnHeRYUEv+vUgvL8=,tag:iDbx80+08AqhvdZIXJzdgQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/users/danny b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-identity-secret/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/distant-shore/zerotier/zerotier-ip/value b/vars/per-machine/distant-shore/zerotier/zerotier-ip/value new file mode 100644 index 0000000..1de93b8 --- /dev/null +++ b/vars/per-machine/distant-shore/zerotier/zerotier-ip/value @@ -0,0 +1 @@ +fdd5:53a2:de33:d269:6499:93b6:ef1a:c3b3 \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/machines/foreign-port b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/secret b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/secret new file mode 100644 index 0000000..768ab41 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:wnNPCB0+f3dcxMW1/pcFZFauUVYTC1mfWoWBV2EJmyRzZS3Uux5Un3R/GbYQeDSFZDLzLH+zCZFaxq3mpb3NGTTUzF8vnGMk/OnjlolA8OjAfiODI0mahTiQA7WcWSk1hkkZ15Ri1o+uyumx9hmvJU3dIsKIJe7AizCzwP5bHg1jgRhG2wPKKyIDWKoh4JTlR6SxK6/tOaUPx2gb2ddz2Lk56Xdw7GCbb/9I9D6sRwxdWMCoWFKdTllLsdsD48b8Jfq4ewD+LudYEtiVByk5SpyOjQoAmMLYaGlD+nxFgZz53hePRIXnp0fL0pm4,iv:fA607yxD/yHJatEiGh1SVGDcqKxB+EFeyCUQeF/Z5hA=,tag:glaq+MBCp6ptKqDsw4RM/Q==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0MklDUkpWbEdFcFgxTVJZ\ndEs1OTJtZFhVaEsyb2pobGlUOGhtcTY4RWpVCjFDV3lqRmNGclZMbTR3UXlhcjJv\nVEY1Tjk1YWR4Tmt0SmgvR3laZnNIRUkKLS0tIHB1TURnYmVzZW4xSERMR0ZrRXl5\nbWVJbW1keGkyUkhuQXE0MEFTaXFsS1EKHlsS3FDr9RuMBRU5r4T3bCZWZn38V3k+\nfLUfuZK2IF+xyD7kEiBuATB57wwfd8RzZ1lBwz4fD4jlb+fz0BXoJQ==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TnNqc284WkZqdXNLVFBU\nRTJndEFmNjY0Q1YyUnRPLy9jWllpSy9ZaFNFCkFkNmpYenQ2dk1Fb2dRZTNvM0Jl\nemNqUmdjQmpJQUF4M3ZNRmo4UEhXOHcKLS0tIFp4OTZJTGR1algxTEVWemdkQTB5\nME4xTTdlelN6bXJiTGRSM1VSWG5vZUEKOYc71rLx7RTq4DR6ZggrtgllK58sYJ6h\ngw156OTQl3fKWxlrKDd1l4o72M1qmfAIQ1z5YJJ+CfNPk/iMz/R3rQ==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:AkcOoNTxMNkpF0SrwFlNujBrB8fxL1diu+mGq/kbsiWIj6UqvVD+dimDSvTgVqvnU4HF7/7b9zKriC6SbG42Kz8zScFv7m3idD2tHr+7SE/iR7CowDQs70CRMo1b85wLq8WAxhfQb93NHdum6I2biNVIf0ZXs1+kZ2iNBxtjqfQ=,iv:kWOCWCe953ekq0n0HLe3S2JprIBnBe9QXwIzDFyQMH8=,tag:tLz7VZwj7RrbpJ7QTrBqcg==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/users/danny b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.cert/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/machines/foreign-port b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/secret b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/secret new file mode 100644 index 0000000..a79bb25 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:1Hq98rN3U+8DcxIFJpYkvv31gUpSm0WBjfZxivYn7/ZkH6zbJ57fzeU+9PH9SRF6QBuekZKZNIBup3fteI5VqQ/moEyQE9aSvnqGCrkcamDwDQfN5GwKX+rb7W96atESRm/VqhgDWC2KTc3892515gBPpkDG+nc=,iv:tAlghG1jpDPcYgTvEzAlnB2upAetl8mz8IIQercHe4k=,tag:mz3fvVlKolg5JzrjhBNPaw==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlaEk2Z1NMZnVtNlJPdXgw\nTWNaTFBCRXo3T2JRUEY2Q2hBY0xpMVV6ckE0ClJOVUpKNDZTcEhGS2RzQm1tSjNp\ndmxQWjl5aHord0RUMHRvTlhyMkVqc1UKLS0tIHlDRXlReUgzZVdLcE9kMFhsTDRq\nOGxpZE9KcUR0VEhyOE9VUkVUVlIyRlEKsnU17famN/qr2M8BdvVpRl5bSWseegrZ\nnB9yljvm+pxsE55xM1WyguNfUwXtHj0YTiVgBl5PIUolj3/J8R76sg==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjTGdDVlB0RGlTcng3M3pE\nNzEvNFpBUzF0aDJDaUJFTFFGWlB3bEVVdHhFClZOZGNDanlMTkxIMk9lbzVGRzAv\nZG93NUFFL3NIM3Z0TlhucFlMTTYwc3MKLS0tIFFQcTIwekNEM0k0MElGZys2QldS\nMDZpRVk5OVNZYVVWSWJDTFZqVFdiRWcKgwuwZgKhKx1PiQwH2CgMoCl0WUQR5Rv9\nx4mpZgkoD5pkEx896117CyAy2BRzrDWo+4SsjEijSMlDynYsbxLReA==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:DX9+9MH8ZPtc6sPbYSc+54soAIXJWWEoEWBZdbJ6gT5RhVdzUjMHuEbmb9eMcb+nVu4KSUCoXiJOT9XActSU2dcTNIIiLX1lqpw0aWRS2sAWM+Go4hT4/P98z/0vcsdN/uQOBl3cDlygqKhN9GSoPfJTMT+QTSZsVjxwYxW1pPM=,iv:B9RiMMX+yS1Y+3E1ifTJI30pvLrah5SCPwW6CZKZGNU=,tag:MA007hv+nMIMutOdl5ewkQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/users/danny b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/identity.pub/value b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.pub/value new file mode 100644 index 0000000..b450f2b --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/identity.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAZqy+mwYOfJy3GSHfeC80TFn1c0kYte5zzzbwrP8xww0= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/foreign-port/data-mesher-node-identity/peer.id/value b/vars/per-machine/foreign-port/data-mesher-node-identity/peer.id/value new file mode 100644 index 0000000..f9982a8 --- /dev/null +++ b/vars/per-machine/foreign-port/data-mesher-node-identity/peer.id/value @@ -0,0 +1 @@ +12D3KooWGjAXheQGEfy13JQJP8pSrwcivxoXw5ijRzesfXVDFuyW \ No newline at end of file diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/machines/foreign-port b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/secret b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/secret new file mode 100644 index 0000000..cd27ea5 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:dDO6hu8prxHvoP41Oxky0mGGbrwqcCcrrkg0tbr/Sv8K16gNoQaX2wvaRDExOmt0BZkv5Oe8p5pvKudmm5JN0AS7oaPexW0lE+vFJ+zrRpq01c5BbCYZ0SuuafJ3VmRS/dlYU0/SZ4MyK3eijLzX3rGHPOi3b0g=,iv:hbh49ExGMYyshxcus/5sTIs/ZcOL9pod/3H/oHG1Qs8=,tag:fjHnl2uunGEU0i2FtgZB+g==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBZNTlkTWVleld5K3Q5Vklm\nMlphdVduQ0RKY0pEVGdVTm5scHRWR0lNVjAwClV4V3drQnFLUkhpUVk1ZElGcFM1\ncit3UTdURExTRDVjVW1ZdklTZzRINDAKLS0tIHFMYnNycmh1Y0h4OC9UNUtHUmMw\nVXdpVk9QWHlBYmtCS3FOam9SWnRFZG8KDnggBRH/wSh1tfiCGOn1sF/Fdfxkf1us\n7Lzxexrmh+lllns/KY2of9L2HUgDavp+ju/5QVFfT7O3SuSTB6aoow==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5eVpNUmV5QlllaXlPaGgr\ndXZYMURzT3I4UWxWSHBSbnAyZVNsOWNaZ2xJClhkRmZ2ejBYVCtkTVBZZE82YXE5\nWkdZWFJFM0lVQXFFYm5rYnRVZDFEdlkKLS0tIHZ5OUgzcFRLZnFWK3pDUUtWUUJj\nWFF4Zk5IeDl5VFNQWlVsTk1lQWlLQmMKJzaOm0cwOshmwoO+eHovf6i6mGkezjIP\ncXJlDaJyxfPKJxc36XlJ5KT9c4RqTX7WFOifHoKRh4EN58KnvtFj+A==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:18Z", + "mac": "ENC[AES256_GCM,data:UX7265pubBBssugQk4pZsQH5WedsmnqFa77bJQZwu2ixNUTkO9VfR8r9CUiugDOmbDj9Y7TJtoN4JR+v6hBmDOnjHO5w0WO5dONNJebGmO+pGU7r/K6WwSGi5nPANiYjGuHqYZwq7PJe8ZCF/vu/ZI8q7iJijw6xGWuGHaP/Gvw=,iv:Ezo1z5n+pHPdhjh9l+HvmsgElEwJR4eoMPtZKdDhHAI=,tag:57yLRXReSRz098sDxyiQZQ==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/users/danny b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.key/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.pub/value b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.pub/value new file mode 100644 index 0000000..6131304 --- /dev/null +++ b/vars/per-machine/foreign-port/dm-pull-deploy-status-key/signing.pub/value @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA6xYjcIT5B5NDduIARf2EAoE+vsnZK+NWcyiI0fQc0Fg= +-----END PUBLIC KEY----- diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/machines/foreign-port b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/machines/foreign-port new file mode 120000 index 0000000..96f5ba3 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/machines/foreign-port @@ -0,0 +1 @@ +../../../../../../sops/machines/foreign-port \ No newline at end of file diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/secret b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/secret new file mode 100644 index 0000000..5fd8d79 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/secret @@ -0,0 +1,18 @@ +{ + "data": "ENC[AES256_GCM,data:PO0Thn6D7kcIGWr7MwmS8H58+9JYSDDGQZlx28B7T6noXTA6tWqMJlqY4aMn1dXJ1CKAqV4q5VZpd/kP9KQvSL4DRnRrFteRe0C+k/mlLfwsWVqLGFY7eqoG1QTZwc4w8cw3FB7R0YUfxRlHq3mIyrbf+8POX2Rq2r5L5GNWVkGTKZOPRtNawPxTrUgfVM4B9ksc1vtTZeWn1GymSwevnt4KPX/8efFAgIclTUHh+Eh+F9xSU9efnkT+Phsh3QLf+3+UHiXQXlpMgwuKrvBJdHWLxJz/3aTpU2+nByqv0IANhGhR8ut0EbFXr8Zr1pIYrt4mWCAyYJvnwxR6iljQ1zyhI0GXUNAHJPQ7wRYq,iv:yDOBYu2+HK/KfS/hbR5QgOi2QHp9RzGPiKxojQX2s8c=,tag:q6s6LemFyoFBEq+ojd4D6A==,type:str]", + "sops": { + "age": [ + { + "recipient": "age1g6y8gvcampqj5y3yzdajke2h5n7k6ckdg6a424cghy5325px7cmqjmmd28", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1bEkwSUhxR0JQR2psZ3Uy\nUlFpWi93NTBhZ0s0TlpkQ2VkVUdDWHIyNmhFClBhVFBnb1h0c2o2cm9OODZpZWMy\nQTB2YmxnWmN5Ylo4M1JHMVVVdklWeWMKLS0tIDBSY2NQdmRTZnA1QUtnaHloUFJJ\nb0VvZGlwSko0UitTa2t6TDZ4bnhsSWMKt5awUoFdny/Qg5krgUAzHeqIoIhprPmF\nBNleiSJdAvSsK53a7CT2rGInnl3dcrtpkEWluK7WJlFTJBdekMwQuA==\n-----END AGE ENCRYPTED FILE-----\n" + }, + { + "recipient": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TFZjRjk4Wm8vcEcxN0ZP\na05yd282UmR3NllXM2MyeUpSb0VuWURvTHp3CmJzL0cwcU5WWGJuME1KcmtxSFVw\nL1lFdzg3Z2t4TXBiaWduZ2tSZXc3bjAKLS0tIEp6NWpIMlhoSEtvQ3IyNXJNVnE1\nb1lSczR2eG1JY1NScnkyNWMxWWN0aWcKrnfv9dGrWpmBjt8u+FdtwojU5hLDyV/Z\n6vgaW35SvFYLYR53Zo18MPkYbqGcaNldyr68qbYMLxqVdQUJwv3LSg==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2026-06-07T19:41:19Z", + "mac": "ENC[AES256_GCM,data:joT4cUsVDxTVJqF9OJyETkC0lxQ6sT3XonBIjy80/PZ6cs7lcEyboWWSVuBcG+CTPzcUv1uXmdNjUBNc/TDdF8P0vEGnMBgmNRnSrxb0OwENW+c08GOB+c4AJev58H+V1wmzmyr9NJAKxpvQaE/cWIS1wS7c5QdiKAj8HsYd2ns=,iv:H2xSAU0jTH0bKS+P5W+FwbOtzl/Wb5xTfirkZMmtPq8=,tag:o+b9ESO3d8XnIU/bcH09zw==,type:str]", + "version": "3.12.2" + } +} diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/users/danny b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/users/danny new file mode 120000 index 0000000..48e5c60 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-identity-secret/users/danny @@ -0,0 +1 @@ +../../../../../../sops/users/danny \ No newline at end of file diff --git a/vars/per-machine/foreign-port/zerotier/zerotier-ip/value b/vars/per-machine/foreign-port/zerotier/zerotier-ip/value new file mode 100644 index 0000000..79b3db0 --- /dev/null +++ b/vars/per-machine/foreign-port/zerotier/zerotier-ip/value @@ -0,0 +1 @@ +fdd5:53a2:de33:d269:6499:9389:9b18:6c52 \ No newline at end of file