Compare commits

..

No commits in common. "main" and "1" have entirely different histories.
main ... 1

28 changed files with 268 additions and 1155 deletions

View file

@ -1,35 +0,0 @@
nextcloud-claude-bot:
secret: ENC[AES256_GCM,data:I0YxTjU89dDFnpF/TwZYBliLDyre0kNZbWvJD5Jdleihe1LGEptcLuTN0lkO9I8z9U7GDGxoAprb8W+5d2MQrA==,iv:m/q82cfbFID0aW3KfXCZSIa7FhtGx/3TCxv5x8GXVk0=,tag:+IuHUKVqdGrU0RS18NUlPg==,type:str]
sops:
age:
- recipient: age1qur4kh3gay9ryk3jh2snvjp6x9eq94zdrmgkrfcv4fzsu7l6lumq4tr3uy
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwUkZhbzhZbHZQRDJvZW1q
QjlIS2IxL1NaSnZ6L3JDU3hENEh4SFBBLzN3CnRvZkFmOUIzWUgybGdPblp4UmRH
U1JmUCt5WkNUc09EdktUdFBHY0lKUFkKLS0tIDZHRXRtZTBROGJJcFhMVDM4ZDJt
OGZOUElSNGJmaEtPalQ5MXBxQUFaRFkKu2EIbPsNMkejgc2rVC/nL5G2Hfp1IkiA
3CV36NHFXKRlo8Fxj+hl1Fi063TRlNW0TK5fc15u4En7tdMnCdfJ+A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1dwcz3fmp29ju4svy0t0wz4ylhpwlqa8xpw4l7t4gmgqr0ev37qrsfn840l
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPQTd4U0ZVOHJKMUgyLzF5
RkpiVmMzYTRZS21ZUnNERTB3K2pDSXpFMlVjClkxNDl6WlcyN0xBT3MzYWVOWnNL
UldRZER4YVFuSHZ0S3BMSVZLQm5pRWcKLS0tIEpZVlA2RFZGbElUQWVWb3c5OSt3
WlpSVGx4OEJGYU52L2xkdmNteWdGUE0KS0Xa9GmwTiAURgC72OhNLHW1/XgHyHFZ
4yQ2qri2m14E5oheB8ELzMMY9K/yQUs90UqdZIS8UoSeaG4GqjEuQA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1cpm9xhgue7sjvq7zyeeaxwr96c93sfzxxxj76sxsq7s7kgnygvcq5jxren
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA4YjNVSE9mdjhKY2hWVGJ3
d3VnVDBJODRyMkRyMUJUREwvT0ZUUkRtVUU0CjFOWCtFK05saHNTWGRoazQ2aVgw
bnlPMUNmdVVSUEFoVEtkaXcwVklETm8KLS0tIFBWMERoR0ZiMDJ1bW5May9RSWlv
VktQbU9STjNRVTh6TndIRVBLdFVFUVkKz0dBpDQ9+/Pp3FKsBpcmzuEROsZ65jkw
9LRQTMGF6kSrbLjRkBs21t5t2kunKgCriAmd8Nv+S/sG/NKqpQMJ6A==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2026-02-03T14:36:03Z"
mac: ENC[AES256_GCM,data:y0gOLMHjzv2ER+Bvo0glkY2EC28K2uWtPPhv0EDzb4PczGDNgQWGHhdyFsN07+JJIf2LMpKV1u7BMp4e/dF1wDgZsR6wErZLxuLrXfZ6B7mTDOPGUR1rGo5PhbNIO90LL5uQ/aRLl38efqxgU8fHCkuXJkUtM38UQ9+7JN4PVic=,iv:2AKgYujqxeGiiVMhqC8FGFiYbTcogxZx/uUgh+8XowQ=,tag:3RwH2AboBU9T25fWjecsMQ==,type:str]
unencrypted_suffix: _unencrypted
version: 3.11.0

100
flake.lock generated
View file

@ -19,16 +19,16 @@
"brew-src": {
"flake": false,
"locked": {
"lastModified": 1769363988,
"narHash": "sha256-BiGPeulrDVetXP+tjxhMcGLUROZAtZIhU5m4MqawCfM=",
"lastModified": 1763638478,
"narHash": "sha256-n/IMowE9S23ovmTkKX7KhxXC2Yq41EAVFR2FBIXPcT8=",
"owner": "Homebrew",
"repo": "brew",
"rev": "d01011cac6d72032c75fd2cd9489909e95d9faf2",
"rev": "fbfdbaba008189499958a7aeb1e2c36ab10c067d",
"type": "github"
},
"original": {
"owner": "Homebrew",
"ref": "5.0.12",
"ref": "5.0.3",
"repo": "brew",
"type": "github"
}
@ -134,11 +134,11 @@
]
},
"locked": {
"lastModified": 1769524058,
"narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=",
"lastModified": 1768923567,
"narHash": "sha256-GVJ0jKsyXLuBzRMXCDY6D5J8wVdwP1DuQmmvYL/Vw/Q=",
"owner": "nix-community",
"repo": "disko",
"rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d",
"rev": "00395d188e3594a1507f214a2f15d4ce5c07cb28",
"type": "github"
},
"original": {
@ -421,11 +421,11 @@
]
},
"locked": {
"lastModified": 1769580047,
"narHash": "sha256-tNqCP/+2+peAXXQ2V8RwsBkenlfWMERb+Uy6xmevyhM=",
"lastModified": 1768949235,
"narHash": "sha256-TtjKgXyg1lMfh374w5uxutd6Vx2P/hU81aEhTxrO2cg=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "366d78c2856de6ab3411c15c1cb4fb4c2bf5c826",
"rev": "75ed713570ca17427119e7e204ab3590cc3bf2a5",
"type": "github"
},
"original": {
@ -454,11 +454,11 @@
"homebrew-cask": {
"flake": false,
"locked": {
"lastModified": 1770127519,
"narHash": "sha256-wIpVsLhx1gaB2JYfpVipt9ZLAReKFO0kmVIOhieHfqs=",
"lastModified": 1769077283,
"narHash": "sha256-alvFQmhX8POHxBP3/jResx6AJ06X+k6SF4/CiNndpPA=",
"owner": "homebrew",
"repo": "homebrew-cask",
"rev": "76e6c1bda247fe48dc30683203cce2b28b5d6eee",
"rev": "4a8185e145fa4fc8326705c666d608c3ee761612",
"type": "github"
},
"original": {
@ -470,11 +470,11 @@
"homebrew-core": {
"flake": false,
"locked": {
"lastModified": 1770130704,
"narHash": "sha256-95Jwssj3WbBwHO4nNB5uVIgIym/fuSDBb5vs6eKdgp0=",
"lastModified": 1769077518,
"narHash": "sha256-QtWC5CcY9xzfjcThSwZgise9RXbM2vZmw+Tot67RiJo=",
"owner": "homebrew",
"repo": "homebrew-core",
"rev": "5369d45006ea107dead79ef8ef4b29b7c972f276",
"rev": "2ac083c750fa2a6999ad05a7352e8edbd7abd969",
"type": "github"
},
"original": {
@ -515,11 +515,11 @@
},
"mnw": {
"locked": {
"lastModified": 1769981889,
"narHash": "sha256-ndI7AxL/6auelkLHngdUGVImBiHkG8w2N2fOTKZKn4k=",
"lastModified": 1768701608,
"narHash": "sha256-kSvWF3Xt2HW9hmV5V7i8PqeWJIBUKmuKoHhOgj3Znzs=",
"owner": "Gerg-L",
"repo": "mnw",
"rev": "332fed8f43b77149c582f1782683d6aeee1f07cf",
"rev": "20d63a8a1ae400557c770052a46a9840e768926b",
"type": "github"
},
"original": {
@ -562,11 +562,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1770130359,
"narHash": "sha256-IfoT9oaeIE6XjXprMORG2qZFzGGZ0v6wJcOlQRdlpvY=",
"lastModified": 1768906339,
"narHash": "sha256-iwkHIz2IYRcELkBoKXQUHlP0bFGmrHIz/roJUVYsyx8=",
"owner": "NotAShelf",
"repo": "nvf",
"rev": "92854bd0eaaa06914afba345741c372439b8e335",
"rev": "18c55d3bebf2c704970b4ea6fd0261808bec8d94",
"type": "github"
},
"original": {
@ -580,11 +580,11 @@
"brew-src": "brew-src"
},
"locked": {
"lastModified": 1769437432,
"narHash": "sha256-8d7KnCpT2LweRvSzZYEGd9IM3eFX+A78opcnDM0+ndk=",
"lastModified": 1764473698,
"narHash": "sha256-C91gPgv6udN5WuIZWNehp8qdLqlrzX6iF/YyboOj6XI=",
"owner": "zhaofengli-wip",
"repo": "nix-homebrew",
"rev": "a5409abd0d5013d79775d3419bcac10eacb9d8c5",
"rev": "6a8ab60bfd66154feeaa1021fc3b32684814a62a",
"type": "github"
},
"original": {
@ -595,11 +595,11 @@
},
"nixos-hardware": {
"locked": {
"lastModified": 1769302137,
"narHash": "sha256-QEDtctEkOsbx8nlFh4yqPEOtr4tif6KTqWwJ37IM2ds=",
"lastModified": 1768736227,
"narHash": "sha256-qgGq7CfrYKc3IBYQ7qp0Z/ZXndQVC5Bj0N8HW9mS2rM=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "a351494b0e35fd7c0b7a1aae82f0afddf4907aa8",
"rev": "d447553bcbc6a178618d37e61648b19e744370df",
"type": "github"
},
"original": {
@ -642,11 +642,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1770056022,
"narHash": "sha256-yvCz+Qmci1bVucXEyac3TdoSPMtjqVJmVy5wro6j/70=",
"lastModified": 1768940263,
"narHash": "sha256-sJERJIYTKPFXkoz/gBaBtRKke82h4DkX3BBSsKbfbvI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d04d8548aed39902419f14a8537006426dc1e4fa",
"rev": "3ceaaa8bc963ced4d830e06ea2d0863b6490ff03",
"type": "github"
},
"original": {
@ -748,11 +748,11 @@
]
},
"locked": {
"lastModified": 1770088046,
"narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=",
"lastModified": 1769050281,
"narHash": "sha256-1H8DN4UZgEUqPUA5ecHOufLZMscJ4IlcGaEftaPtpBY=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "71f9daa4e05e49c434d08627e755495ae222bc34",
"rev": "6deef0585c52d9e70f96b6121207e1496d4b0c49",
"type": "github"
},
"original": {
@ -835,11 +835,11 @@
]
},
"locked": {
"lastModified": 1770110318,
"narHash": "sha256-NUVGVtYBTC96WhPh4Y3SVM7vf0o1z5W4uqRBn9v1pfo=",
"lastModified": 1768863606,
"narHash": "sha256-1IHAeS8WtBiEo5XiyJBHOXMzECD6aaIOJmpQKzRRl64=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "f990b0a334e96d3ef9ca09d4bd92778b42fd84f9",
"rev": "c7067be8db2c09ab1884de67ef6c4f693973f4a2",
"type": "github"
},
"original": {
@ -857,11 +857,11 @@
"rust-overlay": "rust-overlay_3"
},
"locked": {
"lastModified": 1769829418,
"narHash": "sha256-ALZKPUa0eHP6HwETAJ9PsAnYQjNLF6eEpo1W2fmYqwA=",
"lastModified": 1768997903,
"narHash": "sha256-UpBfh3I4PhykVHqV74rrxufF3X1Z8z8sx/lFgMFfIP8=",
"owner": "haraldh",
"repo": "ssh-tresor",
"rev": "2e1bfa29bd5ad5a60c3e0effd69851a67d455781",
"rev": "dd45aed45f8d9b8729b7698ef43e7cc32fab97b6",
"type": "github"
},
"original": {
@ -932,11 +932,11 @@
},
"unstable": {
"locked": {
"lastModified": 1770115704,
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
"lastModified": 1768886240,
"narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
"rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0",
"type": "github"
},
"original": {
@ -949,16 +949,16 @@
"xremap": {
"flake": false,
"locked": {
"lastModified": 1769021727,
"narHash": "sha256-2wylBk3+Zu1pHa41dhKwvUtxOVyHSMRDfOD9fIp8x2I=",
"lastModified": 1766606475,
"narHash": "sha256-FPZ4iQA/vVZGzbO8i8lTK8i9A3zs9BLqMvTMeAVv9rQ=",
"owner": "k0kubun",
"repo": "xremap",
"rev": "890e0a6ca92e90f3bcbd1e235abcf2192e233a46",
"rev": "cdc744d873c19899ef21f329c4305b4b5e53d459",
"type": "github"
},
"original": {
"owner": "k0kubun",
"ref": "v0.14.10",
"ref": "v0.14.8",
"repo": "xremap",
"type": "github"
}
@ -971,11 +971,11 @@
"xremap": "xremap"
},
"locked": {
"lastModified": 1769636170,
"narHash": "sha256-X000Dgg053Dv9NIzm1b9QYSAHYtW2jHMVALQezui7L0=",
"lastModified": 1767318478,
"narHash": "sha256-h3oE50RedA8DRGrFU+Hv2kirt4rmzdaC9oSD+MSg9Ms=",
"owner": "xremap",
"repo": "nix-flake",
"rev": "00bc6dd4275d4b003a17ef7f5f271ba87f73d698",
"rev": "9a2224aa01a3c86e94b398c33329c8ff6496dc5d",
"type": "github"
},
"original": {

View file

@ -1,25 +0,0 @@
{
config,
lib,
...
}:
with lib;
with lib.metacfg;
let
cfg = config.metacfg.hardware.wooting;
in
{
options.metacfg.hardware.wooting = with types; {
enable = mkBoolOpt false "Whether or not to enable Wooting keyboard support.";
enableBluetoothCompat = mkBoolOpt true "Disable ClassicBondedOnly for Bluetooth compatibility.";
};
config = mkIf cfg.enable {
hardware.bluetooth.input.General.ClassicBondedOnly = mkIf cfg.enableBluetoothCompat false;
services.udev.extraRules = ''
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e4c5", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e489", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
'';
};
}

View file

@ -1,41 +0,0 @@
{
config,
lib,
...
}:
with lib;
with lib.metacfg;
let
cfg = config.metacfg.services.acmeBase;
in
{
options.metacfg.services.acmeBase = with types; {
enable = mkBoolOpt false "Whether or not to enable ACME with common settings.";
email = mkOption {
type = types.str;
default = "harald@hoyer.xyz";
description = "Registration email for ACME.";
};
dnsProvider = mkOption {
type = types.str;
default = "cloudflare";
description = "DNS provider for ACME DNS-01 challenge.";
};
credentialsFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to the credentials file for the DNS provider.";
};
};
config = mkIf cfg.enable {
security.acme = {
acceptTerms = true;
defaults = {
email = cfg.email;
dnsProvider = cfg.dnsProvider;
credentialsFile = mkIf (cfg.credentialsFile != null) cfg.credentialsFile;
};
};
};
}

View file

@ -1,42 +0,0 @@
{
config,
lib,
...
}:
with lib;
with lib.metacfg;
let
cfg = config.metacfg.services.nginxBase;
in
{
options.metacfg.services.nginxBase = with types; {
enable = mkBoolOpt false "Whether or not to enable nginx with common settings.";
clientMaxBodySize = mkOption {
type = types.str;
default = "1000M";
description = "Maximum allowed size of the client request body.";
};
enableAcmeGroup = mkBoolOpt true "Add nginx user to acme group.";
enableVcombinedLog = mkBoolOpt true "Enable vcombined log format.";
};
config = mkIf cfg.enable {
users.users.nginx.extraGroups = mkIf cfg.enableAcmeGroup [ "acme" ];
services.nginx = {
enable = true;
clientMaxBodySize = cfg.clientMaxBodySize;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
appendHttpConfig = mkIf cfg.enableVcombinedLog ''
log_format vcombined '$host:$server_port '
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log vcombined;
'';
};
};
}

View file

@ -1,44 +0,0 @@
{
config,
lib,
...
}:
with lib;
with lib.metacfg;
let
cfg = config.metacfg.services.xremap;
in
{
options.metacfg.services.xremap = with types; {
enable = mkBoolOpt false "Whether or not to enable xremap key remapping.";
userName = mkOption {
type = types.str;
default = "harald";
description = "User to run xremap as.";
};
withGnome = mkBoolOpt true "Enable GNOME support.";
deviceNames = mkOption {
type = types.listOf types.str;
default = [ ];
description = "List of device names to remap.";
};
config = mkOption {
type = types.attrs;
default = { };
description = "Xremap configuration.";
};
};
config = {
services.xremap = {
enable = cfg.enable;
userName = mkIf cfg.enable cfg.userName;
serviceMode = mkIf cfg.enable "user";
withGnome = mkIf cfg.enable cfg.withGnome;
deviceNames = mkIf cfg.enable cfg.deviceNames;
config = mkIf cfg.enable cfg.config;
};
users.users.${cfg.userName}.extraGroups = mkIf cfg.enable [ "input" ];
};
}

View file

@ -1,29 +0,0 @@
{
config,
lib,
...
}:
with lib;
with lib.metacfg;
let
cfg = config.metacfg.system.kernelTweaks;
in
{
options.metacfg.system.kernelTweaks = with types; {
enable = mkBoolOpt false "Whether or not to enable desktop kernel optimizations.";
pmFreezeTimeout = mkOption {
type = types.int;
default = 30000;
description = "PM freeze timeout in milliseconds.";
};
enableZram = mkBoolOpt true "Enable zram swap.";
};
config = mkIf cfg.enable {
boot.kernel.sysctl = {
"power.pm_freeze_timeout" = cfg.pmFreezeTimeout;
};
zramSwap.enable = cfg.enableZram;
};
}

View file

@ -1,28 +0,0 @@
{
config,
lib,
...
}:
with lib;
with lib.metacfg;
let
cfg = config.metacfg.system.noSleep;
in
{
options.metacfg.system.noSleep = with types; {
enable = mkBoolOpt false "Whether or not to disable all sleep targets.";
disableGdmAutoSuspend = mkBoolOpt false "Disable GDM auto-suspend.";
ignoreLidSwitch = mkBoolOpt false "Ignore lid switch events.";
};
config = mkIf cfg.enable {
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
systemd.targets.hibernate.enable = false;
systemd.targets.hybrid-sleep.enable = false;
services.displayManager.gdm.autoSuspend = mkIf cfg.disableGdmAutoSuspend false;
services.logind.settings.Login.HandleLidSwitch = mkIf cfg.ignoreLidSwitch "ignore";
};
}

View file

@ -9,13 +9,7 @@ with lib.metacfg;
services.spice-autorandr.enable = true;
services.spice-vdagentd.enable = true;
services.resolved.enable = true;
services.resolved.extraConfig = ''
ResolveUnicastSingleLabel=yes
'';
metacfg = {
system.noSleep.enable = true;
base.enable = true;
gui.enable = true;
nix-ld.enable = true;
@ -40,6 +34,13 @@ with lib.metacfg;
];
};
# Disable the GNOME3/GDM auto-suspend feature that cannot be disabled in GUI!
# If no user is logged in, the machine will power down after 20 minutes.
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
systemd.targets.hibernate.enable = false;
systemd.targets.hybrid-sleep.enable = false;
environment.systemPackages = with pkgs; [
azure-cli
desktop-file-utils
@ -59,11 +60,16 @@ with lib.metacfg;
services.ratbagd.enable = true;
services.resolved.enable = true;
#services.resolved.dnssec = "allow-downgrade";
services.resolved.extraConfig = ''
ResolveUnicastSingleLabel=yes
'';
virtualisation = {
docker.enable = true;
podman.dockerCompat = false;
libvirtd.enable = false;
rosetta.enable = true;
};
system.autoUpgrade = {
@ -72,5 +78,7 @@ with lib.metacfg;
allowReboot = false;
};
virtualisation.rosetta.enable = true;
system.stateVersion = "25.05";
}

View file

@ -9,13 +9,7 @@ with lib.metacfg;
services.spice-autorandr.enable = true;
services.spice-vdagentd.enable = true;
services.resolved.enable = true;
services.resolved.extraConfig = ''
ResolveUnicastSingleLabel=yes
'';
metacfg = {
system.noSleep.enable = true;
base.enable = true;
gui.enable = true;
nix-ld.enable = true;
@ -40,6 +34,13 @@ with lib.metacfg;
];
};
# Disable the GNOME3/GDM auto-suspend feature that cannot be disabled in GUI!
# If no user is logged in, the machine will power down after 20 minutes.
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
systemd.targets.hibernate.enable = false;
systemd.targets.hybrid-sleep.enable = false;
environment.systemPackages = with pkgs; [
azure-cli
desktop-file-utils
@ -59,11 +60,16 @@ with lib.metacfg;
services.ratbagd.enable = true;
services.resolved.enable = true;
#services.resolved.dnssec = "allow-downgrade";
services.resolved.extraConfig = ''
ResolveUnicastSingleLabel=yes
'';
virtualisation = {
docker.enable = true;
podman.dockerCompat = false;
libvirtd.enable = false;
rosetta.enable = true;
};
system.autoUpgrade = {
@ -72,5 +78,7 @@ with lib.metacfg;
allowReboot = false;
};
virtualisation.rosetta.enable = true;
system.stateVersion = "25.05";
}

View file

@ -18,17 +18,21 @@ with lib.metacfg;
22000
];
services.tailscale.enable = true;
services.cratedocs-mcp.enable = true;
services.openssh = {
enable = true;
};
services.tailscale.enable = true;
services.resolved.enable = true;
hardware.bluetooth.input.General.ClassicBondedOnly = false;
services.udev.extraRules = ''
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e4c5", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e489", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
'';
metacfg = {
hardware.wooting.enable = true;
base.enable = true;
gui.enable = true;
nix-ld.enable = true;
@ -55,21 +59,15 @@ with lib.metacfg;
"dialout"
"tss"
];
system.kernelTweaks.enable = true;
};
system.autoUpgrade = {
enable = true;
operation = "boot";
allowReboot = false;
};
nixpkgs.config.permittedInsecurePackages = [
"electron-27.3.11"
];
# Additional kernel tuning beyond the module defaults
# Kernel tuning
boot.kernel.sysctl = {
"power.pm_freeze_timeout" = 30000;
# Reduce swap usage (you have zram)
"vm.swappiness" = 10;
# Prefer keeping directory/inode caches
@ -103,7 +101,6 @@ with lib.metacfg;
kubectl
kubectx
logseq
nvtopPackages.amd
obsidian
piper-tts
tipp10
@ -114,18 +111,32 @@ with lib.metacfg;
# zram swap with zstd compression for better performance
zramSwap = {
enable = true;
algorithm = "zstd";
memoryPercent = 50;
};
services.ratbagd.enable = true;
services.resolved.enable = true;
#services.resolved.dnssec = "allow-downgrade";
#services.resolved.extraConfig = ''
# ResolveUnicastSingleLabel=yes
#'';
virtualisation = {
libvirtd.enable = true;
docker.enable = true;
podman.dockerCompat = false;
};
system.autoUpgrade = {
enable = true;
operation = "boot";
allowReboot = false;
};
services.trezord.enable = true;
services.ollama = {

View file

@ -1,15 +1,28 @@
# In /etc/nixos/configuration.nix
{ ... }:
{
metacfg.services.xremap = {
enable = true;
deviceNames = [
users.users.harald.extraGroups = [ "input" ];
# Enable the xremap service
services.xremap.enable = true;
services.xremap.userName = "harald"; # Replace with your username
services.xremap.serviceMode = "user"; # Run as user service, not system-wide
services.xremap.withGnome = true;
# Add a specific configuration block to select your keyboard(s) by name
services.xremap.deviceNames = [
# Use the name found in the log output: "Hangsheng MonsGeek Keyboard System Control"
"Hangsheng MonsGeek Keyboard"
"HS Galaxy100 Keyboard"
# You can usually shorten the name slightly to match the device you want
];
config = {
# Define your remapping configuration using Nix's attribute set format
services.xremap.config = {
keymap = [
{
remap = {
# Map Alt+C (LeftAlt-C) to Ctrl+C (LeftControl-C)
LeftAlt-C = "COPY";
LeftAlt-V = "PASTE";
LeftAlt-X = "CUT";
@ -17,5 +30,4 @@
}
];
};
};
}

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}:
@ -7,9 +9,14 @@
sopsFile = ../../../.secrets/hetzner/internetbs.yaml; # bring your own password file
};
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
security.acme.certs = {
security.acme = {
acceptTerms = true;
defaults = {
email = "harald@hoyer.xyz";
dnsProvider = "cloudflare";
credentialsFile = config.sops.secrets.internetbs.path;
};
certs = {
"surfsite.org" = {
extraDomainNames = [ "*.surfsite.org" ];
};
@ -64,4 +71,5 @@
extraDomainNames = [ "*.harald-hoyer.de" ];
};
};
};
}

View file

@ -12,7 +12,6 @@
./mailserver.nix
./network.nix
./nextcloud.nix
./nextcloud-claude-bot
./nginx.nix
./postgresql.nix
./rspamd.nix
@ -23,8 +22,6 @@
services.tailscale.enable = true;
metacfg = {
services.nginxBase.enable = true;
services.acmeBase.enable = true;
emailOnFailure.enable = true;
base.enable = true;
nix.enable = true;
@ -45,6 +42,7 @@
dates = "04:00";
operation = "switch";
allowReboot = true;
# flake = lib.mkForce "git+file:///var/lib/gitea/repositories/harald/nixcfg.git#mx";
flake = lib.mkForce "/root/nixcfg/.#mx";
};

View file

@ -1,146 +0,0 @@
# Nextcloud Claude Bot Setup
## Voraussetzungen
- NixOS Server mit Nextcloud (Talk App aktiviert)
- Claude Code CLI installiert und authentifiziert
- Nextcloud Talk Version 17+ (Nextcloud 26+)
## 1. Bot Secret generieren
```bash
openssl rand -hex 32 > /var/secrets/nextcloud-claude-bot
chmod 600 /var/secrets/nextcloud-claude-bot
```
## 2. NixOS Konfiguration
Kopiere die Dateien nach `/etc/nixos/nextcloud-claude-bot/` oder in dein Flake:
```
/etc/nixos/
├── configuration.nix
└── nextcloud-claude-bot/
├── module.nix
└── bot.py
```
Füge das Modul zu deiner `configuration.nix` hinzu (siehe `example-config.nix`).
## 3. System rebuilden
```bash
nixos-rebuild switch
```
## 4. Bot bei Nextcloud registrieren
```bash
# Als root oder mit sudo
cd /var/www/nextcloud # oder wo dein Nextcloud liegt
# Bot secret auslesen
BOT_SECRET=$(cat /var/secrets/nextcloud-claude-bot)
# Bot installieren
sudo -u nextcloud php occ talk:bot:install \
"Claude" \
"Claude AI Assistant" \
"http://127.0.0.1:8085/webhook" \
"$BOT_SECRET"
```
Falls der Bot extern erreichbar sein muss:
```bash
sudo -u nextcloud php occ talk:bot:install \
"Claude" \
"Claude AI Assistant" \
"https://cloud.example.com/_claude-bot/webhook" \
"$BOT_SECRET"
```
## 5. Bot aktivieren
Nach der Installation musst du den Bot für Konversationen aktivieren:
```bash
# Liste alle Bots
sudo -u nextcloud php occ talk:bot:list
# Bot für alle User verfügbar machen (optional)
sudo -u nextcloud php occ talk:bot:state <bot-id> 1
```
## 6. Testen
1. Öffne Nextcloud Talk
2. Starte einen neuen Chat mit dem Bot (suche nach "Claude")
3. Schreibe eine Nachricht
### Health Check
```bash
curl http://127.0.0.1:8085/health
```
### Logs prüfen
```bash
journalctl -u nextcloud-claude-bot -f
```
## Troubleshooting
### Bot antwortet nicht
1. Prüfe ob der Service läuft:
```bash
systemctl status nextcloud-claude-bot
```
2. Prüfe die Logs:
```bash
journalctl -u nextcloud-claude-bot -n 50
```
3. Teste den Webhook manuell:
```bash
curl -X POST http://127.0.0.1:8085/webhook \
-H "Content-Type: application/json" \
-d '{"actor":{"type":"users","id":"harald"},"message":{"message":"test","id":1},"conversation":{"token":"abc123","type":1}}'
```
### Claude CLI Fehler
Stelle sicher, dass Claude CLI als der Service-User funktioniert:
```bash
# Teste als der User
sudo -u nextcloud-claude-bot claude --print "Hello"
```
Die Claude CLI Config liegt in `/var/lib/nextcloud-claude-bot/.config/claude/`.
### Signature Fehler
Prüfe ob das Bot Secret in Nextcloud und im Service übereinstimmt:
```bash
# Secret im Service
cat /var/secrets/nextcloud-claude-bot
# Secret in Nextcloud (verschlüsselt gespeichert)
sudo -u nextcloud php occ talk:bot:list
```
## Befehle im Chat
- `/help` oder `/hilfe` Hilfe anzeigen
- `/clear` oder `/reset` Konversation zurücksetzen
## Sicherheitshinweise
- Der Bot läuft nur auf localhost und ist nicht direkt erreichbar
- Nur in `allowedUsers` gelistete Nutzer können den Bot verwenden
- Webhook-Signaturen werden verifiziert
- DynamicUser isoliert den Service

View file

@ -1,355 +0,0 @@
#!/usr/bin/env python3
"""
Nextcloud Talk Claude Bot
Receives webhooks from Nextcloud Talk and responds using Claude CLI.
"""
import asyncio
import hashlib
import hmac
import json
import logging
import os
import re
import secrets
from datetime import datetime
from typing import Optional
import httpx
from fastapi import FastAPI, Request, HTTPException, Header
from fastapi.responses import JSONResponse
# Configuration from environment
NEXTCLOUD_URL = os.environ.get("NEXTCLOUD_URL", "").rstrip("/")
CLAUDE_PATH = os.environ.get("CLAUDE_PATH", "claude")
ALLOWED_USERS = [u.strip() for u in os.environ.get("ALLOWED_USERS", "").split(",") if u.strip()]
TIMEOUT = int(os.environ.get("TIMEOUT", "120"))
SYSTEM_PROMPT = os.environ.get("SYSTEM_PROMPT", "")
# Bot secret from systemd credential
def get_bot_secret() -> str:
cred_path = os.environ.get("CREDENTIALS_DIRECTORY", "")
if cred_path:
secret_file = os.path.join(cred_path, "bot-secret")
if os.path.exists(secret_file):
with open(secret_file) as f:
return f.read().strip()
# Fallback for development
return os.environ.get("BOT_SECRET", "")
BOT_SECRET = get_bot_secret()
# Logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s"
)
log = logging.getLogger(__name__)
app = FastAPI(title="Nextcloud Claude Bot")
# In-memory conversation history per conversation token
# Format: {token: [(user, message), ...]}
conversations: dict[str, list[tuple[str, str]]] = {}
MAX_HISTORY = int(os.environ.get("CONTEXT_MESSAGES", "6"))
def generate_bot_auth_headers(body: str = "") -> dict:
"""Generate authentication headers for bot requests to Nextcloud."""
random = secrets.token_hex(32)
digest = hmac.new(
BOT_SECRET.encode(),
(random + body).encode(),
hashlib.sha256
).hexdigest()
return {
"X-Nextcloud-Talk-Bot-Random": random,
"X-Nextcloud-Talk-Bot-Signature": digest,
"OCS-APIRequest": "true",
}
def verify_signature(body: bytes, signature: str, random: Optional[str] = None) -> bool:
"""Verify Nextcloud webhook signature."""
if not BOT_SECRET:
log.warning("No bot secret configured, skipping signature verification")
return True
# Nextcloud sends: sha256=<hex>
if signature.startswith("sha256="):
signature = signature[7:]
# Try different signature computation methods
# Method 1: Just body
expected1 = hmac.new(BOT_SECRET.encode(), body, hashlib.sha256).hexdigest()
# Method 2: random + body (if random header present)
if random:
expected2 = hmac.new(BOT_SECRET.encode(), (random.encode() + body), hashlib.sha256).hexdigest()
else:
expected2 = None
if hmac.compare_digest(expected1, signature):
return True
if expected2 and hmac.compare_digest(expected2, signature):
return True
return False
BOT_SYSTEM_PROMPT = """\
Du bist ein KI-Assistent im Nextcloud Talk Chat.
Deine Antworten werden direkt in den Chatraum gepostet.
Halte deine Antworten kurz und prägnant, da es ein Chat ist.
Nutze Markdown für Formatierung wenn sinnvoll.
Du erhältst:
- <chat_history>: Die letzten Nachrichten im Chatraum (User und deine Antworten)
- <current_message>: Die aktuelle Nachricht, auf die du antworten sollst"""
def build_system_prompt() -> str:
"""Build the full system prompt from hardcoded + optional custom parts."""
if SYSTEM_PROMPT:
return f"{BOT_SYSTEM_PROMPT}\n\n{SYSTEM_PROMPT.strip()}"
return BOT_SYSTEM_PROMPT
def build_prompt(conversation_token: str, current_message: str, current_user: str) -> str:
"""Build user prompt with in-memory conversation history using XML structure."""
parts = []
# Add chat history if available
history = conversations.get(conversation_token, [])
if history:
parts.append("<chat_history>")
for role, msg in history[-MAX_HISTORY:]:
parts.append(f"{role}: {msg}")
parts.append("</chat_history>")
parts.append("")
# Add current message
parts.append(f"<current_message user=\"{current_user}\">")
parts.append(current_message)
parts.append("</current_message>")
return "\n".join(parts)
async def call_claude(prompt: str) -> str:
"""Call Claude CLI and return response."""
cmd = [
CLAUDE_PATH, "--print",
"--tools", "WebSearch,WebFetch",
"--allowedTools", "WebSearch,WebFetch",
"--append-system-prompt", build_system_prompt(),
]
log.info(f"Calling Claude: {cmd[0]} --print --append-system-prompt ...")
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(
proc.communicate(prompt.encode()),
timeout=TIMEOUT
)
if proc.returncode != 0:
log.error(f"Claude CLI error: {stderr.decode()}")
return f"❌ Fehler beim Aufruf von Claude: {stderr.decode()[:200]}"
return stdout.decode().strip()
except asyncio.TimeoutError:
log.error(f"Claude CLI timeout after {TIMEOUT}s")
return f"⏱️ Timeout: Claude hat nicht innerhalb von {TIMEOUT}s geantwortet."
except Exception as e:
log.exception("Error calling Claude")
return f"❌ Fehler: {str(e)}"
async def send_reply(conversation_token: str, message: str, reply_to: int = None):
"""Send reply back to Nextcloud Talk."""
if not NEXTCLOUD_URL:
log.error("NEXTCLOUD_URL not configured")
return
url = f"{NEXTCLOUD_URL}/ocs/v2.php/apps/spreed/api/v1/bot/{conversation_token}/message"
# Bot authentication - signature is over the message being sent
headers = generate_bot_auth_headers(message)
headers["Content-Type"] = "application/json"
payload = {
"message": message,
"referenceId": hashlib.sha256(f"{conversation_token}-{datetime.now().isoformat()}".encode()).hexdigest()[:32],
}
if reply_to:
payload["replyTo"] = reply_to
async with httpx.AsyncClient() as client:
try:
resp = await client.post(url, json=payload, headers=headers)
if resp.status_code not in (200, 201):
log.error(f"Failed to send reply: {resp.status_code} {resp.text}")
else:
log.info(f"Reply sent to conversation {conversation_token}")
except Exception as e:
log.exception("Error sending reply to Nextcloud")
@app.post("/webhook")
async def handle_webhook(
request: Request,
x_nextcloud_talk_signature: Optional[str] = Header(None, alias="X-Nextcloud-Talk-Signature"),
x_nextcloud_talk_random: Optional[str] = Header(None, alias="X-Nextcloud-Talk-Random"),
):
"""Handle incoming webhook from Nextcloud Talk."""
body = await request.body()
# Verify signature
if x_nextcloud_talk_signature and not verify_signature(body, x_nextcloud_talk_signature, x_nextcloud_talk_random):
log.warning("Invalid webhook signature")
raise HTTPException(status_code=401, detail="Invalid signature")
try:
data = json.loads(body)
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid JSON")
log.info(f"Received webhook: {json.dumps(data, indent=2)[:500]}")
# Extract message info - Nextcloud Talk Bot webhook format
actor = data.get("actor", {})
actor_type = actor.get("type", "")
actor_id_full = actor.get("id", "") # e.g., "users/harald"
# Extract username from "users/username" format
if "/" in actor_id_full:
actor_id = actor_id_full.split("/", 1)[1]
else:
actor_id = actor_id_full
# Message is in object.content as JSON string
obj = data.get("object", {})
message_id = obj.get("id")
content_str = obj.get("content", "{}")
try:
content = json.loads(content_str)
message_text = content.get("message", "")
except json.JSONDecodeError:
message_text = content_str
# Conversation info is in target
target = data.get("target", {})
conversation_token = target.get("id", "")
# Only respond to user/person messages
if actor_type not in ("users", "Person"):
log.info(f"Ignoring non-user actor: {actor_type}")
return JSONResponse({"status": "ignored", "reason": "not a user message"})
# For now, treat all conversations the same (respond to mentions)
is_direct_message = False # We can't easily determine this from the webhook
# Check for bot mention in message (Nextcloud uses @"Bot Name" format)
bot_mentioned = False
clean_message = message_text
# Look for mention patterns: @Claude or @"Claude"
mention_patterns = [
r'@"?Claude"?\s*',
r'@"?claude"?\s*',
]
for pattern in mention_patterns:
if re.search(pattern, message_text, re.IGNORECASE):
bot_mentioned = True
clean_message = re.sub(pattern, '', message_text, flags=re.IGNORECASE).strip()
break
# In group chats, only respond if mentioned
if not is_direct_message and not bot_mentioned:
log.info(f"Ignoring message in group chat without mention")
return JSONResponse({"status": "ignored", "reason": "not mentioned in group chat"})
# Use clean message (without mention) for processing
if bot_mentioned:
message_text = clean_message
# Check allowed users
if ALLOWED_USERS and actor_id not in ALLOWED_USERS:
log.warning(f"User {actor_id} not in allowed list")
await send_reply(
conversation_token,
"🚫 Du bist nicht berechtigt, diesen Bot zu nutzen.",
reply_to=message_id
)
return JSONResponse({"status": "rejected", "reason": "user not allowed"})
if not message_text.strip():
return JSONResponse({"status": "ignored", "reason": "empty message"})
log.info(f"Processing message from {actor_id}: {message_text[:100]}")
if message_text.strip().lower() in ("hilfe", "help", "?"):
help_text = """🤖 **Claude Bot Hilfe**
Schreib mir einfach eine Nachricht und ich antworte dir.
**Nutzung:**
In Gruppenchats: @Claude gefolgt von deiner Frage
**Befehle:**
`hilfe` oder `?` Diese Hilfe anzeigen
Der Bot merkt sich die letzten Nachrichten pro Raum (bis zum Neustart)."""
await send_reply(conversation_token, help_text, reply_to=message_id)
return JSONResponse({"status": "ok", "action": "help"})
# Build prompt with chat history and call Claude
prompt = build_prompt(conversation_token, message_text, actor_id)
response = await call_claude(prompt)
# Store in history
if conversation_token not in conversations:
conversations[conversation_token] = []
conversations[conversation_token].append((f"User ({actor_id})", message_text))
conversations[conversation_token].append(("Assistant", response))
# Trim history
if len(conversations[conversation_token]) > MAX_HISTORY * 2:
conversations[conversation_token] = conversations[conversation_token][-MAX_HISTORY * 2:]
# Send response
await send_reply(conversation_token, response, reply_to=message_id)
return JSONResponse({"status": "ok"})
@app.get("/health")
async def health():
"""Health check endpoint."""
return {
"status": "ok",
"nextcloud_url": NEXTCLOUD_URL,
"claude_path": CLAUDE_PATH,
"allowed_users": ALLOWED_USERS if ALLOWED_USERS else "all",
"max_history": MAX_HISTORY,
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8085)

View file

@ -1,34 +0,0 @@
{ config, ... }:
{
imports = [ ./module.nix ];
services.nextcloud-claude-bot = {
enable = true;
nextcloudUrl = "https://nc.hoyer.xyz";
botSecretFile = config.sops.secrets."nextcloud-claude-bot/secret".path;
allowedUsers = []; # Allow all registered users
# Optional extra instructions (base prompt is hardcoded in bot.py)
# systemPrompt = "Additional custom instructions here";
};
sops.secrets."nextcloud-claude-bot/secret" = {
sopsFile = ../../../../.secrets/hetzner/nextcloud-claude-bot.yaml;
restartUnits = [ "nextcloud-claude-bot.service" ];
owner = "claude-bot";
};
# Nginx location for Nextcloud to send webhooks to the bot
services.nginx.virtualHosts."nc.hoyer.xyz".locations."/_claude-bot/" = {
proxyPass = "http://127.0.0.1:8085/";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Only allow from localhost (Nextcloud on same server)
allow 127.0.0.1;
deny all;
'';
};
}

View file

@ -1,80 +0,0 @@
# Example NixOS configuration for the Nextcloud Claude Bot
# Add this to your configuration.nix or a separate module
{ config, pkgs, ... }:
{
imports = [
./nextcloud-claude-bot/module.nix
];
# Install Claude Code CLI
# Note: You'll need to either:
# 1. Use the official package if available in nixpkgs
# 2. Package it yourself
# 3. Use a binary wrapper
# Option 1: If claude-code is in nixpkgs (check latest state)
# environment.systemPackages = [ pkgs.claude-code ];
# Option 2: Manual binary installation wrapper
nixpkgs.overlays = [
(final: prev: {
claude-code = final.writeShellScriptBin "claude" ''
# Assumes claude is installed via npm globally or similar
exec ${final.nodejs}/bin/node /opt/claude-code/cli.js "$@"
'';
})
];
# Create bot secret
# Generate with: openssl rand -hex 32
# Store in a file, e.g., /var/secrets/nextcloud-claude-bot
services.nextcloud-claude-bot = {
enable = true;
port = 8085;
host = "127.0.0.1";
nextcloudUrl = "https://cloud.example.com";
botSecretFile = "/var/secrets/nextcloud-claude-bot";
# Only allow specific users
allowedUsers = [ "harald" ];
# Claude settings
maxTokens = 4096;
timeout = 120;
# Optional system prompt
systemPrompt = ''
Du bist ein hilfreicher Assistent. Antworte auf Deutsch,
es sei denn der Nutzer schreibt auf Englisch.
'';
};
# Ensure secrets directory exists with proper permissions
systemd.tmpfiles.rules = [
"d /var/secrets 0750 root root -"
];
# If Nextcloud runs locally, bot can stay on localhost.
# If you need external access (e.g., Nextcloud on different server):
services.nginx.virtualHosts."cloud.example.com" = {
# ... your existing Nextcloud config ...
locations."/_claude-bot/" = {
proxyPass = "http://127.0.0.1:8085/";
extraConfig = ''
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Only allow from Nextcloud itself
allow 127.0.0.1;
deny all;
'';
};
};
}

View file

@ -1,143 +0,0 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.nextcloud-claude-bot;
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
fastapi
uvicorn
httpx
]);
botModule = pkgs.runCommand "nextcloud-claude-bot-module" {} ''
mkdir -p $out
cp ${./bot.py} $out/nextcloud_claude_bot.py
'';
in {
options.services.nextcloud-claude-bot = {
enable = mkEnableOption "Nextcloud Talk Claude Bot";
port = mkOption {
type = types.port;
default = 8085;
description = "Port for the webhook listener";
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Host to bind to";
};
nextcloudUrl = mkOption {
type = types.str;
example = "https://cloud.example.com";
description = "Base URL of your Nextcloud instance";
};
botSecretFile = mkOption {
type = types.path;
description = "Path to file containing the bot secret (shared with Nextcloud)";
};
claudePath = mkOption {
type = types.path;
default = "${pkgs.claude-code}/bin/claude";
description = "Path to claude CLI binary";
};
allowedUsers = mkOption {
type = types.listOf types.str;
default = [];
example = [ "harald" "admin" ];
description = "Nextcloud usernames allowed to talk to the bot (empty = all)";
};
contextMessages = mkOption {
type = types.int;
default = 6;
description = "Number of recent messages to fetch from chat for context";
};
timeout = mkOption {
type = types.int;
default = 120;
description = "Timeout in seconds for Claude CLI";
};
systemPrompt = mkOption {
type = types.nullOr types.str;
default = null;
example = "Du bist ein hilfreicher Assistent.";
description = "Optional system prompt for Claude";
};
};
config = mkIf cfg.enable {
systemd.services.nextcloud-claude-bot = {
description = "Nextcloud Talk Claude Bot";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
HOME = "/var/lib/nextcloud-claude-bot";
BOT_HOST = cfg.host;
BOT_PORT = toString cfg.port;
NEXTCLOUD_URL = cfg.nextcloudUrl;
CLAUDE_PATH = cfg.claudePath;
ALLOWED_USERS = concatStringsSep "," cfg.allowedUsers;
CONTEXT_MESSAGES = toString cfg.contextMessages;
TIMEOUT = toString cfg.timeout;
SYSTEM_PROMPT = cfg.systemPrompt or "";
PYTHONPATH = botModule;
};
serviceConfig = {
Type = "simple";
ExecStart = "${pythonEnv}/bin/uvicorn nextcloud_claude_bot:app --host ${cfg.host} --port ${toString cfg.port}";
Restart = "always";
RestartSec = 5;
User = "claude-bot";
Group = "claude-bot";
# Security hardening
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = "read-only";
PrivateTmp = true;
PrivateDevices = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
MemoryDenyWriteExecute = false; # Python needs this
LockPersonality = true;
# Bot secret
LoadCredential = "bot-secret:${cfg.botSecretFile}";
# Claude CLI needs home for config
StateDirectory = "nextcloud-claude-bot";
};
};
users.users.claude-bot = {
isSystemUser = true;
group = "claude-bot";
home = "/var/lib/nextcloud-claude-bot";
};
users.groups.claude-bot = {};
# Nginx reverse proxy config (optional, if you want external access)
# services.nginx.virtualHosts."cloud.example.com".locations."/claude-bot/" = {
# proxyPass = "http://${cfg.host}:${toString cfg.port}/";
# };
};
}

View file

@ -1,6 +1,21 @@
{ ... }:
{
services.nginx.virtualHosts = {
users.users.nginx.extraGroups = [ "acme" ];
services.nginx = {
enable = true;
clientMaxBodySize = "1000M";
appendHttpConfig = ''
log_format vcombined '$host:$server_port '
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log vcombined;
'';
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
virtualHosts = {
"00000" = {
useACMEHost = "hoyer.xyz";
serverName = "_";
@ -142,4 +157,5 @@
forceSSL = true;
};
};
};
}

View file

@ -6,6 +6,8 @@
{
imports = [ ./hardware-configuration.nix ];
services.tailscale.enable = true;
boot.kernelPackages = lib.mkOverride 0 pkgs.linuxPackages_latest;
boot.loader.systemd-boot.enable = false;
# Bootloader.
@ -16,8 +18,6 @@
security.tpm2.enable = false;
security.tpm2.abrmd.enable = false;
services.tailscale.enable = true;
metacfg = {
base.enable = true;
nix-ld.enable = true;
@ -37,6 +37,12 @@
podman.dockerCompat = false;
};
system.autoUpgrade = {
enable = true;
operation = "switch";
allowReboot = true;
};
networking.wireless.enable = false; # Enables wireless support via wpa_supplicant.
networking.firewall.allowPing = true;
@ -60,11 +66,5 @@
}
];
system.autoUpgrade = {
enable = true;
operation = "switch";
allowReboot = true;
};
system.stateVersion = "25.05";
}

View file

@ -1,5 +1,7 @@
{
pkgs,
lib,
config,
...
}:
with lib;
@ -15,17 +17,17 @@ with lib.metacfg;
nix.enable = true;
};
virtualisation = {
docker.enable = true;
podman.dockerCompat = false;
};
system.autoUpgrade = {
enable = true;
operation = "switch";
allowReboot = true;
};
virtualisation = {
docker.enable = true;
podman.dockerCompat = false;
};
security.tpm2.enable = false;
security.tpm2.abrmd.enable = false;

View file

@ -7,9 +7,14 @@
sopsFile = ../../../.secrets/sgx/internetbs.yaml; # bring your own password file
};
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
security.acme.certs = {
security.acme = {
acceptTerms = true;
defaults = {
email = "harald@hoyer.xyz";
dnsProvider = "cloudflare";
credentialsFile = config.sops.secrets.internetbs.path;
};
certs = {
"internal.hoyer.world" = {
extraDomainNames = [
"openwebui.hoyer.world"
@ -18,4 +23,5 @@
];
};
};
};
}

View file

@ -12,6 +12,8 @@
./wyoming.nix
];
services.tailscale.enable = true;
boot.tmp.useTmpfs = false;
sops.secrets.pccs.sopsFile = ../../../.secrets/sgx/pccs.yaml;
@ -21,16 +23,7 @@
claude-code
];
services.tailscale.enable = true;
metacfg = {
services.nginxBase.enable = true;
services.acmeBase.enable = true;
system.noSleep = {
enable = true;
disableGdmAutoSuspend = true;
ignoreLidSwitch = true;
};
emailOnFailure.enable = true;
base.enable = true;
gui.enable = true;
@ -65,5 +58,13 @@
allowReboot = true;
};
systemd.targets.sleep.enable = false;
systemd.targets.suspend.enable = false;
systemd.targets.hibernate.enable = false;
systemd.targets.hybrid-sleep.enable = false;
services.displayManager.gdm.autoSuspend = false;
services.logind.settings.Login.HandleLidSwitch = "ignore";
system.stateVersion = "23.11";
}

View file

@ -3,7 +3,22 @@
...
}:
{
services.nginx.virtualHosts = {
users.users.nginx.extraGroups = [ "acme" ];
services.nginx = {
enable = true;
clientMaxBodySize = "1000M";
appendHttpConfig = ''
log_format vcombined '$host:$server_port '
'$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log vcombined;
'';
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
virtualHosts = {
"openwebui.hoyer.world" = {
enableACME = false;
useACMEHost = "internal.hoyer.world";
@ -33,4 +48,5 @@
};
};
};
};
}

View file

@ -2,8 +2,6 @@
{
imports = [ ./hardware-configuration.nix ];
services.resolved.enable = true;
metacfg = {
base.enable = true;
gui.enable = true;
@ -29,6 +27,9 @@
system.stateVersion = "23.11";
services.resolved.enable = true;
#services.resolved.dnssec = "allow-downgrade";
sops.age.sshKeyPaths = [ "/persist/ssh/ssh_host_ed25519_key" ];
sops.secrets.backup-s3.sopsFile = ../../../.secrets/t15/backup-s3.yaml;
sops.secrets.backup-pw.sopsFile = ../../../.secrets/t15/backup-s3.yaml;

View file

@ -20,6 +20,8 @@ with lib.metacfg;
programs.ccache.enable = true;
nix.settings.extra-sandbox-paths = [ config.programs.ccache.cacheDir ];
services.tailscale.enable = true;
services.cratedocs-mcp.enable = true;
sops.age.sshKeyPaths = [ "/var/lib/secrets/ssh_host_ed25519_key" ];
@ -43,11 +45,13 @@ with lib.metacfg;
];
};
services.tailscale.enable = true;
services.resolved.enable = true;
hardware.bluetooth.input.General.ClassicBondedOnly = false;
services.udev.extraRules = ''
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e4c5", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e489", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
'';
metacfg = {
hardware.wooting.enable = true;
base.enable = true;
gui.enable = true;
nix-ld.enable = true;
@ -73,19 +77,17 @@ with lib.metacfg;
"dialout"
"tss"
];
system.kernelTweaks.enable = true;
};
system.autoUpgrade = {
enable = true;
operation = "boot";
allowReboot = false;
};
nixpkgs.config.permittedInsecurePackages = [
"electron-27.3.11"
];
# increase freezing timeout
boot.kernel.sysctl = {
"power.pm_freeze_timeout" = 30000;
};
environment.systemPackages = with pkgs; [
attic-client
azure-cli
@ -110,12 +112,26 @@ with lib.metacfg;
vscode
];
zramSwap.enable = true;
services.ratbagd.enable = true;
services.resolved.enable = true;
#services.resolved.dnssec = "allow-downgrade";
#services.resolved.extraConfig = ''
# ResolveUnicastSingleLabel=yes
#'';
virtualisation = {
libvirtd.enable = true;
};
system.autoUpgrade = {
enable = true;
operation = "boot";
allowReboot = false;
};
services.trezord.enable = true;
services.ollama = {

View file

@ -1,15 +1,28 @@
# In /etc/nixos/configuration.nix
{ ... }:
{
metacfg.services.xremap = {
enable = true;
deviceNames = [
users.users.harald.extraGroups = [ "input" ];
# Enable the xremap service
services.xremap.enable = true;
services.xremap.userName = "harald"; # Replace with your username
services.xremap.serviceMode = "user"; # Run as user service, not system-wide
services.xremap.withGnome = true;
# Add a specific configuration block to select your keyboard(s) by name
services.xremap.deviceNames = [
# Use the name found in the log output: "Hangsheng MonsGeek Keyboard System Control"
"Hangsheng MonsGeek Keyboard"
"HS Galaxy100 Keyboard"
# You can usually shorten the name slightly to match the device you want
];
config = {
# Define your remapping configuration using Nix's attribute set format
services.xremap.config = {
keymap = [
{
remap = {
# Map Alt+C (LeftAlt-C) to Ctrl+C (LeftControl-C)
LeftAlt-C = "COPY";
LeftAlt-V = "PASTE";
LeftAlt-X = "CUT";
@ -17,5 +30,4 @@
}
];
};
};
}