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