Compare commits

..

1 commit

Author SHA1 Message Date
DannyDannyDanny
eccd9ee7dd phantom-ship + vps-relay: Forgejo on git.dannydannydanny.me
Phase 1 of the de-platform-from-GitHub roadmap (vimwiki/diary/2026-05-03.md).

- phantom-ship: services.forgejo bound to 0.0.0.0:3000, sqlite, lfs on,
  registration disabled, sign-in required.
- phantom-ship: open port 3000 only on the ZT interface (matches the
  bbbot pattern on sunken-ship — never exposed on WAN/Wi-Fi).
- vps-relay: Caddy vhost git.dannydannydanny.me reverse-proxies over ZT
  to phantom-ship:3000.

Manual steps still needed before this is reachable:
1. GoDaddy A record git.dannydannydanny.me -> 89.167.39.251
2. clan machines update phantom-ship && clan machines update vps-relay
3. On phantom-ship: bootstrap admin user (DISABLE_REGISTRATION is on):
     forgejo admin user create --admin --username danny \
       --email <addr> --password <pw>
2026-05-04 19:55:04 +02:00
65 changed files with 187 additions and 1933 deletions

View file

@ -1,53 +0,0 @@
// 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"
}
}

View file

@ -94,45 +94,16 @@ sudo dd if=result/iso/nixos-minimal-*.iso of=/dev/sdX status=progress bs=4M
## Live-system WiFi (optional, custom ISO only)
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):
Create `nixos/installer-wifi.nix` (gitignored):
```nix
{
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";
};
networking.wireless.enable = true;
networking.wireless.networks."YourSSID".psk = "your-password";
}
```
`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.
Add to flake's installer-iso modules, rebuild ISO on Linux.
## Installed-system WiFi (optional)

View file

@ -21,8 +21,6 @@ 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/<machine>.clan/... bootstrap multiaddrs resolve over ZT.
@ -31,8 +29,6 @@ let
"${sunkenShipZTv6}" = [ "sunken-ship.clan" ];
"${phantomShipZTv6}" = [ "phantom-ship.clan" ];
"${vpsRelayZTv6}" = [ "vps-relay.clan" ];
"${distantShoreZTv6}" = [ "distant-shore.clan" ];
"${foreignPortZTv6}" = [ "foreign-port.clan" ];
};
};
in {
@ -51,8 +47,6 @@ 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
@ -64,8 +58,6 @@ 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).
@ -78,8 +70,6 @@ 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 = { };
};
@ -97,8 +87,6 @@ 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
@ -123,18 +111,6 @@ 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,
@ -149,9 +125,8 @@ 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";
@ -171,56 +146,6 @@ 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";
@ -240,8 +165,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";

View file

@ -1,13 +1,9 @@
{ inputs, self, ... }: {
# Custom minimal installer ISO (build with: nix build .#installer-iso).
# nixos/installer-wifi.nix (gitignored) is auto-included when present, to
# preconfigure live-system WiFi. See docs/server-installer-usb.md.
# Optional: add ./installer-wifi.nix (gitignored) to modules for live WiFi.
flake.nixosConfigurations.installer-iso = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [ ../nixos/installer-iso.nix ]
++ inputs.nixpkgs.lib.optional
(builtins.pathExists ../nixos/installer-wifi.nix)
../nixos/installer-wifi.nix;
modules = [ ../nixos/installer-iso.nix ];
};
flake.packages.x86_64-linux.installer-iso =

View file

@ -1,9 +1,8 @@
# Expose reusable NixOS modules via `flake.nixosModules`.
#
# Consume from a host's flake-module via:
# modules = [ config.flake.nixosModules.server-debug-tools ];
# modules = [ config.flake.nixosModules.dotfiles-rebuild ];
{ ... }: {
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;
}

192
flake.lock generated
View file

@ -9,18 +9,22 @@
"nixpkgs": [
"nixpkgs"
],
"systems": "systems",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"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"
"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"
},
"original": {
"type": "tarball",
"url": "https://git.clan.lol/clan/clan-community/archive/main.tar.gz"
"ref": "fix/dm-pull-deploy-hyphen-hostnames",
"type": "git",
"url": "https://git.clan.lol/dannydannydanny/clan-community.git"
}
},
"clan-core": {
@ -36,15 +40,15 @@
"nixpkgs"
],
"sops-nix": "sops-nix",
"systems": "systems",
"systems": "systems_2",
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1778462753,
"narHash": "sha256-/9qWZbrwoVWP0YWuC1Z5HMEb/oy6rNsjypUKTuk1PB4=",
"rev": "09551fdb27a7e5712bef371e9271034d503242ed",
"lastModified": 1776557977,
"narHash": "sha256-j+UWg3fR6jWKPqkPoqRf1a6nR1b/AnZXDuh04H+voUc=",
"rev": "e9ced950bedc726492e5cb52139bf5f17258dc69",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/09551fdb27a7e5712bef371e9271034d503242ed.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/clan-core/archive/e9ced950bedc726492e5cb52139bf5f17258dc69.tar.gz"
},
"original": {
"type": "tarball",
@ -67,11 +71,11 @@
]
},
"locked": {
"lastModified": 1776654564,
"narHash": "sha256-5bpzOOXsaAr4g25/ghtKdYO17xg0l+MieCcWgqx24eY=",
"rev": "ad23733ebc47284dc1158db43218cf4027824aee",
"lastModified": 1776506822,
"narHash": "sha256-WlxAhXEoDHbkfFw3uNYra0CXce7pBk314x9chPu7ycE=",
"rev": "c3f48f5931b27bb9cc58de8799d36ecefb867d98",
"type": "tarball",
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/ad23733ebc47284dc1158db43218cf4027824aee.tar.gz"
"url": "https://git.clan.lol/api/v1/repos/clan/data-mesher/archive/c3f48f5931b27bb9cc58de8799d36ecefb867d98.tar.gz"
},
"original": {
"type": "tarball",
@ -86,11 +90,11 @@
]
},
"locked": {
"lastModified": 1776613567,
"narHash": "sha256-gC9Cp5ibBmGD5awCA9z7xy6MW6iJufhazTYJOiGlCUI=",
"lastModified": 1773889306,
"narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=",
"owner": "nix-community",
"repo": "disko",
"rev": "32f4236bfc141ae930b5ba2fb604f561fed5219d",
"rev": "5ad85c82cc52264f4beddc934ba57f3789f28347",
"type": "github"
},
"original": {
@ -106,11 +110,11 @@
]
},
"locked": {
"lastModified": 1777713215,
"narHash": "sha256-8GzXDOXckDWwST8TY5DbwYFjdvQLlP7K9CLSVx6iTTo=",
"lastModified": 1773889306,
"narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=",
"owner": "nix-community",
"repo": "disko",
"rev": "63b4e7e6cf75307c1d26ac3762b886b5b0247267",
"rev": "5ad85c82cc52264f4beddc934ba57f3789f28347",
"type": "github"
},
"original": {
@ -163,11 +167,11 @@
]
},
"locked": {
"lastModified": 1777988971,
"narHash": "sha256-qIoWPDs+0/8JecyYgE3gpKQxW/4bLW/gp45vow9ioCQ=",
"lastModified": 1775087534,
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "0678d8986be1661af6bb555f3489f2fdfc31f6ff",
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
"type": "github"
},
"original": {
@ -178,7 +182,7 @@
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
"systems": "systems_3"
},
"locked": {
"lastModified": 1731533236,
@ -196,7 +200,7 @@
},
"flake-utils_2": {
"inputs": {
"systems": "systems_3"
"systems": "systems_4"
},
"locked": {
"lastModified": 1681202837,
@ -219,11 +223,11 @@
]
},
"locked": {
"lastModified": 1778444552,
"narHash": "sha256-f18pIiR9q/p1vHY93gmAum7aHhQOG49oGvAB9+lptRo=",
"lastModified": 1776184304,
"narHash": "sha256-No6QGBmIv5ChiwKCcbkxjdEQ/RO2ZS1gD7SFy6EZ7rc=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "dcebe66f958673729896eec2de4abfd86ef22d21",
"rev": "3c7524c68348ef79ce48308e0978611a050089b2",
"type": "github"
},
"original": {
@ -261,11 +265,11 @@
]
},
"locked": {
"lastModified": 1777594677,
"narHash": "sha256-h90sHwoRJLRvaTpZroTvU2JRHDFj0czUafM8eqLe1RI=",
"lastModified": 1774991950,
"narHash": "sha256-kScKj3qJDIWuN9/6PMmgy5esrTUkYinrO5VvILik/zw=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "899c08a15beae5da51a5cecd6b2b994777a948da",
"rev": "f2d3e04e278422c7379e067e323734f3e8c585a7",
"type": "github"
},
"original": {
@ -317,11 +321,11 @@
]
},
"locked": {
"lastModified": 1777780666,
"narHash": "sha256-8wURyQMdDkGUarSTKOGdCuFfYiwa3HbzwscUfn3STDE=",
"lastModified": 1775037210,
"narHash": "sha256-KM2WYj6EA7M/FVZVCl3rqWY+TFV5QzSyyGE2gQxeODU=",
"owner": "nix-darwin",
"repo": "nix-darwin",
"rev": "8c62fba0854ba15c8917aed18894dbccb48a3777",
"rev": "06648f4902343228ce2de79f291dd5a58ee12146",
"type": "github"
},
"original": {
@ -335,18 +339,17 @@
"inputs": {
"flake-utils": "flake-utils",
"home-manager": "home-manager_2",
"nix-openclaw-tools": "nix-openclaw-tools",
"nix-steipete-tools": "nix-steipete-tools",
"nixpkgs": [
"nixpkgs"
],
"qmd": "qmd"
]
},
"locked": {
"lastModified": 1778353239,
"narHash": "sha256-g0yC+loN19X3Xyn6RuBHeWzevH7Qymt0REW+kyGuCLY=",
"lastModified": 1776183358,
"narHash": "sha256-uRWaRXGhkyGWMbNgQcmx0+RPzPLenVGopkNHgAEfmBQ=",
"owner": "openclaw",
"repo": "nix-openclaw",
"rev": "e2ea91056fdd0836bef96326a2b687277dbe3e1c",
"rev": "53aac0dce0810c40c75793fdad3d41b0f7e7baaf",
"type": "github"
},
"original": {
@ -355,24 +358,6 @@
"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,
@ -386,17 +371,35 @@
"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": 1777732699,
"narHash": "sha256-2uX/XtOWZ/oy2rerRynVhqVA//ZXZ3Fo60PikLHEPQc=",
"lastModified": 1776255237,
"narHash": "sha256-LQjlc0VEn55WAT4BiI8sIsokb/2FNlcbBD+Xr3MTE24=",
"owner": "nix-community",
"repo": "NixOS-WSL",
"rev": "5482f113fd31ebac131d1ebeb2ae90bf0d5e41f5",
"rev": "9a8c2a85f1ffdcecfb0f9c52c5a73c49ceb43911",
"type": "github"
},
"original": {
@ -424,11 +427,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1776169885,
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"lastModified": 1773734432,
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
"type": "github"
},
"original": {
@ -440,11 +443,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1778274207,
"narHash": "sha256-I4puXmX1iovcCHZlRmztO3vW0mAbbRvq4F8wgIMQ1MM=",
"lastModified": 1776255774,
"narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b3da656039dc7a6240f27b2ef8cc6a3ef3bccae7",
"rev": "566acc07c54dc807f91625bb286cb9b321b5f42a",
"type": "github"
},
"original": {
@ -468,32 +471,6 @@
"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",
@ -532,6 +509,21 @@
}
},
"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=",
@ -547,7 +539,7 @@
"type": "github"
}
},
"systems_2": {
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@ -562,7 +554,7 @@
"type": "github"
}
},
"systems_3": {
"systems_4": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
@ -646,11 +638,11 @@
]
},
"locked": {
"lastModified": 1778394798,
"narHash": "sha256-/jR8bModWv0ji305ecMgAB+2eaXLZiYdH+9Z4JIRkuA=",
"lastModified": 1776317517,
"narHash": "sha256-JP1XVRabZquf7pnXvRUjp7DV+EBrB6Qmp3+vG3HMy/k=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
"rev": "45bc54456044b96492923739bfae633e1a4352e1",
"rev": "0a7be59e988bb2cb452080f59aaabae70bc415ae",
"type": "github"
},
"original": {

View file

@ -29,9 +29,10 @@
clan-core.inputs.nixpkgs.follows = "nixpkgs";
clan-core.inputs.flake-parts.follows = "flake-parts";
# 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: 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.inputs.nixpkgs.follows = "nixpkgs";
clan-community.inputs.clan-core.follows = "clan-core";
};

View file

@ -0,0 +1,44 @@
# 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 `<dotfilesDir>#<host>`.
#
# 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";
};
}

View file

@ -1,12 +0,0 @@
# 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 ];
}

View file

@ -1,134 +0,0 @@
# Prometheus + Alertmanager + Grafana on sunken-ship.
#
# Scrape targets are the clan ZeroTier IPv6s — kept in sync with
# vars/per-machine/<host>/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 ];
}

View file

@ -1,37 +0,0 @@
# 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 = "/";
};
};
};
};
};
};
}

View file

@ -1,36 +0,0 @@
# 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 = "/";
};
};
};
};
};
};
}

View file

@ -24,38 +24,6 @@
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 &

View file

@ -88,35 +88,6 @@
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'
'';
}
];
};
@ -171,11 +142,6 @@
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;
@ -262,10 +228,9 @@
# alacritty # TODO: configured via programs.alacritty above, so not needed here
# warp-terminal # TODO: Bloat
# vscodium # TODO: Bloat
zed-editor
# zed-editor # TODO: Bloat
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

View file

@ -1,18 +0,0 @@
# 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;
}

View file

@ -1,114 +0,0 @@
# 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 ];
}

View file

@ -1,18 +0,0 @@
# 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;
}

View file

@ -1,111 +0,0 @@
# 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 ];
}

View file

@ -48,13 +48,10 @@ 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 ];
# Forgejo's HTTP backend is only allowed on the ZeroTier interface so
# vps-relay's Caddy can reach it via the ZT mesh. Same pattern as
# bbbot on sunken-ship — port 3000 is never exposed on WAN/Wi-Fi.
networking.firewall.interfaces."zt+".allowedTCPPorts = [ 3000 ];
hardware.enableRedistributableFirmware = true; # iwlwifi (Intel 8260) + GPU + BT firmware
@ -111,7 +108,7 @@ in
# Passwordless sudo for wheel.
security.sudo.wheelNeedsPassword = false;
environment.systemPackages = with pkgs; [
git # clone/bootstrap and dm-pull-deploy
git # clone/bootstrap and dotfiles-rebuild timer
nodejs # npm for openclaw plugin installs
python3 # node-gyp dependency for openclaw plugins
wakeonlan # wake rusty-anchor: wakeonlan 00:16:cb:87:20:ba
@ -168,20 +165,10 @@ in
};
};
# OpenClaw gateway needs write access to its config dir and repo clones;
# shelfish wants its DB outside the rsynced code dir.
# OpenClaw gateway needs write access to its config dir and repo clones.
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
@ -242,41 +229,20 @@ 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, 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.
# Data (feedback.jsonl, pointer cache): ~danny/.local/share/shipyard/
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
pkgs.ffmpeg # video/animation posters, sticker decode
tesseractWithLangs # photo OCR
pkgs.whisper-cpp # voice/audio transcription
pkgs.poppler-utils # pdftotext (document handling)
];
path = [ pythonEnv ];
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";
@ -287,347 +253,6 @@ in
};
};
# 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/<user_id>/
# 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.
@ -658,7 +283,6 @@ in
};
};
# 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.
# Auto-rebuild service/timer + safe.directory provided by the
# shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix).
}

View file

@ -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, repo-pull timers, dm-pull-deploy push
git # clone/bootstrap and dotfiles-rebuild timer
brightnessctl # manual backlight; replaces removed `light` from nixpkgs
uxplay # AirPlay mirroring receiver
alsa-utils # aplay, amixer, arecord for audio debugging
@ -95,10 +95,7 @@
networking.firewall = {
allowedTCPPorts = [ 7000 7001 7100 4533 ];
allowedUDPPorts = [ 5353 6000 6001 7011 ];
# 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 ];
interfaces."zt+".allowedTCPPorts = [ 8080 ];
};
# Navidrome — self-hosted music streaming server (Subsonic API).
@ -110,25 +107,9 @@
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";
@ -158,29 +139,23 @@
};
};
# BigBiggerBiggestBot — Mini App backend (no Telegram polling).
# BigBiggerBiggestBot — Telegram fitness tracker with Mini App.
# Code: https://github.com/DannyDannyDanny/bigbiggerbiggestbot cloned at /home/danny/tg_fitness_bot
# Bot token (used only for validating Telegram WebApp initData HMACs):
# ~danny/.secrets/bigbiggerbiggestbot
# Bot token: ~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).
# 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).
# 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.
systemd.services.fitness-bot = let
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
python-telegram-bot
python-dotenv
aiohttp
]);
in {
description = "BigBiggerBiggestBot Mini App backend";
description = "BigBiggerBiggestBot Telegram fitness tracker";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
@ -191,7 +166,6 @@
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";
@ -226,278 +200,6 @@
timerConfig.RandomizedDelaySec = "2min";
};
# ── 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=<shipyard_poc_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 <branch>:staging → wait ~15 min → tap B3Bot
# beta in shipyard_poc_bot's launcher → test → git push <branch>: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 `[<id>]` 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.
# Auto-rebuild service/timer + safe.directory provided by the
# shared dotfiles-rebuild NixOS module (see nixos/modules/dotfiles-rebuild.nix).
}

View file

@ -46,13 +46,8 @@
isNormalUser = true;
extraGroups = [ "wheel" ];
openssh.authorizedKeys.keys = [
# 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"
# Same pubkey used to reach sunken-ship; set at install via clan.
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKW/akfIiVU5o63YrTAJVZhMj7kXfYHOnXDtlpVFW7pf danny@sunken-ship"
];
};
users.users.root.openssh.authorizedKeys.keys =
@ -106,74 +101,11 @@
"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
'';
};
};

View file

@ -41,7 +41,6 @@
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")
@ -58,39 +57,6 @@
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<Left><Left>", { desc = "Replace all" })
vim.keymap.set("n", "<leader>w", ":w<CR>", { desc = "Save file" })
@ -106,8 +72,6 @@
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
];
};
}

View file

@ -6,7 +6,7 @@
python3Packages.buildPythonApplication {
pname = "hara-gmail-mcp";
version = "0.2.0";
version = "0.1.0";
pyproject = true;
src = ./.;
nativeBuildInputs = [ python3Packages.setuptools ];

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hara-gmail-mcp"
version = "0.2.0"
version = "0.1.0"
description = "Gmail MCP server for Hara (IMAP+SMTP, throwaway pre-OAuth2)"
requires-python = ">=3.11"
dependencies = [

View file

@ -153,34 +153,6 @@ 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",

View file

@ -1,15 +1,14 @@
"""Hara Gmail MCP server.
Exposes a small toolset for reading and writing mail across the configured
Gmail accounts.
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.
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
@ -22,7 +21,7 @@ from dataclasses import asdict
from mcp.server.fastmcp import FastMCP
from .accounts import AccountStore
from .imap_client import archive, list_inbox, mark_read, read_email, search
from .imap_client import list_inbox, read_email, search
logger = logging.getLogger("hara_gmail_mcp")
@ -93,36 +92,6 @@ 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"),

View file

@ -5,17 +5,12 @@
# 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)
# Default to the sunken-ship SSH key when targeting that host.
# Use sunken-ship key if not set (AGENTS.md)
if [[ -n "${SSH_KEY:-}" ]]; then
SSH_OPTS=(-i "$SSH_KEY")
elif [[ "$HOST" == "sunken-ship" ]] && [[ -f ~/.ssh/id_ed25519_sunken_ship ]]; then
@ -24,37 +19,23 @@ else
SSH_OPTS=()
fi
echo "Pushing main so the server can clone the latest..."
git -C "$REPO_ROOT" push origin main 2>/dev/null || true
echo "Pushing branch so server can pull..."
git push origin server-installer-usb 2>/dev/null || true
echo "On $HOST: clone main into ~/dotfiles-iso-build..."
echo "On $HOST: clone branch, build ISO..."
ssh "${SSH_OPTS[@]}" "$HOST" 'set -e
BUILD_DIR=~/dotfiles-iso-build
rm -rf "$BUILD_DIR"
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
git clone --branch server-installer-usb https://github.com/DannyDannyDanny/dotfiles.git "$BUILD_DIR"
cd "$BUILD_DIR/nixos"
nix build .#installer-iso
ls -la result/iso/
'
ISO_NAME=$(ssh "${SSH_OPTS[@]}" "$HOST" 'ls ~/dotfiles-iso-build/result/iso/*.iso 2>/dev/null | head -1')
ISO_NAME=$(ssh "${SSH_OPTS[@]}" "$HOST" 'ls ~/dotfiles-iso-build/nixos/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/result/iso/$ISO_NAME" "$OUT/"
scp "${SSH_OPTS[@]}" "$HOST:~/dotfiles-iso-build/nixos/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"

View file

@ -1,6 +0,0 @@
[
{
"publickey": "age1hjhqyuvcjuh62xh9m5ek3aa2rluaz8c28hgh2pm435jkqtpry9ssdn2l0z",
"type": "age"
}
]

View file

@ -1,6 +0,0 @@
[
{
"publickey": "age1lwl2z6ymqjshknr79277qnr7hvffcc8n7qdqt98sz3t709a5yutq8d7gka",
"type": "age"
}
]

View file

@ -1,14 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../users/danny

View file

@ -1,14 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../users/danny

View file

@ -1 +0,0 @@
../../../../../../sops/machines/distant-shore

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1 +0,0 @@
../../../../../../sops/machines/distant-shore

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1,3 +0,0 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAABhcRTNvFEyWkyRBX17KkM5nDuqOvR1xTY5vDqTygvk=
-----END PUBLIC KEY-----

View file

@ -1 +0,0 @@
12D3KooW9pjiKnqmnHSwGRhgyUqKeFydDUE8RvYJDAqHb5PZvzue

View file

@ -1 +0,0 @@
../../../../../../sops/machines/distant-shore

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1,3 +0,0 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAPVF7m/+s1YroGdvSMxPwKmenJjk4yNrP8tNtZGHEhJI=
-----END PUBLIC KEY-----

View file

@ -1 +0,0 @@
../../../../../../sops/machines/distant-shore

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1 +0,0 @@
fdd5:53a2:de33:d269:6499:93b6:ef1a:c3b3

View file

@ -1 +0,0 @@
../../../../../../sops/machines/foreign-port

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1 +0,0 @@
../../../../../../sops/machines/foreign-port

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1,3 +0,0 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAZqy+mwYOfJy3GSHfeC80TFn1c0kYte5zzzbwrP8xww0=
-----END PUBLIC KEY-----

View file

@ -1 +0,0 @@
12D3KooWGjAXheQGEfy13JQJP8pSrwcivxoXw5ijRzesfXVDFuyW

View file

@ -1 +0,0 @@
../../../../../../sops/machines/foreign-port

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1,3 +0,0 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA6xYjcIT5B5NDduIARf2EAoE+vsnZK+NWcyiI0fQc0Fg=
-----END PUBLIC KEY-----

View file

@ -1 +0,0 @@
../../../../../../sops/machines/foreign-port

View file

@ -1,18 +0,0 @@
{
"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"
}
}

View file

@ -1 +0,0 @@
../../../../../../sops/users/danny

View file

@ -1 +0,0 @@
fdd5:53a2:de33:d269:6499:9389:9b18:6c52