diff --git a/.secrets/hetzner/nextcloud-claude-bot.yaml b/.secrets/hetzner/nextcloud-claude-bot.yaml new file mode 100644 index 0000000..4d638f0 --- /dev/null +++ b/.secrets/hetzner/nextcloud-claude-bot.yaml @@ -0,0 +1,35 @@ +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 diff --git a/flake.lock b/flake.lock index da101c7..703e220 100644 --- a/flake.lock +++ b/flake.lock @@ -19,16 +19,16 @@ "brew-src": { "flake": false, "locked": { - "lastModified": 1763638478, - "narHash": "sha256-n/IMowE9S23ovmTkKX7KhxXC2Yq41EAVFR2FBIXPcT8=", + "lastModified": 1769363988, + "narHash": "sha256-BiGPeulrDVetXP+tjxhMcGLUROZAtZIhU5m4MqawCfM=", "owner": "Homebrew", "repo": "brew", - "rev": "fbfdbaba008189499958a7aeb1e2c36ab10c067d", + "rev": "d01011cac6d72032c75fd2cd9489909e95d9faf2", "type": "github" }, "original": { "owner": "Homebrew", - "ref": "5.0.3", + "ref": "5.0.12", "repo": "brew", "type": "github" } @@ -134,11 +134,11 @@ ] }, "locked": { - "lastModified": 1768923567, - "narHash": "sha256-GVJ0jKsyXLuBzRMXCDY6D5J8wVdwP1DuQmmvYL/Vw/Q=", + "lastModified": 1769524058, + "narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=", "owner": "nix-community", "repo": "disko", - "rev": "00395d188e3594a1507f214a2f15d4ce5c07cb28", + "rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d", "type": "github" }, "original": { @@ -421,11 +421,11 @@ ] }, "locked": { - "lastModified": 1768949235, - "narHash": "sha256-TtjKgXyg1lMfh374w5uxutd6Vx2P/hU81aEhTxrO2cg=", + "lastModified": 1769580047, + "narHash": "sha256-tNqCP/+2+peAXXQ2V8RwsBkenlfWMERb+Uy6xmevyhM=", "owner": "nix-community", "repo": "home-manager", - "rev": "75ed713570ca17427119e7e204ab3590cc3bf2a5", + "rev": "366d78c2856de6ab3411c15c1cb4fb4c2bf5c826", "type": "github" }, "original": { @@ -454,11 +454,11 @@ "homebrew-cask": { "flake": false, "locked": { - "lastModified": 1769077283, - "narHash": "sha256-alvFQmhX8POHxBP3/jResx6AJ06X+k6SF4/CiNndpPA=", + "lastModified": 1770127519, + "narHash": "sha256-wIpVsLhx1gaB2JYfpVipt9ZLAReKFO0kmVIOhieHfqs=", "owner": "homebrew", "repo": "homebrew-cask", - "rev": "4a8185e145fa4fc8326705c666d608c3ee761612", + "rev": "76e6c1bda247fe48dc30683203cce2b28b5d6eee", "type": "github" }, "original": { @@ -470,11 +470,11 @@ "homebrew-core": { "flake": false, "locked": { - "lastModified": 1769077518, - "narHash": "sha256-QtWC5CcY9xzfjcThSwZgise9RXbM2vZmw+Tot67RiJo=", + "lastModified": 1770130704, + "narHash": "sha256-95Jwssj3WbBwHO4nNB5uVIgIym/fuSDBb5vs6eKdgp0=", "owner": "homebrew", "repo": "homebrew-core", - "rev": "2ac083c750fa2a6999ad05a7352e8edbd7abd969", + "rev": "5369d45006ea107dead79ef8ef4b29b7c972f276", "type": "github" }, "original": { @@ -515,11 +515,11 @@ }, "mnw": { "locked": { - "lastModified": 1768701608, - "narHash": "sha256-kSvWF3Xt2HW9hmV5V7i8PqeWJIBUKmuKoHhOgj3Znzs=", + "lastModified": 1769981889, + "narHash": "sha256-ndI7AxL/6auelkLHngdUGVImBiHkG8w2N2fOTKZKn4k=", "owner": "Gerg-L", "repo": "mnw", - "rev": "20d63a8a1ae400557c770052a46a9840e768926b", + "rev": "332fed8f43b77149c582f1782683d6aeee1f07cf", "type": "github" }, "original": { @@ -562,11 +562,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1768906339, - "narHash": "sha256-iwkHIz2IYRcELkBoKXQUHlP0bFGmrHIz/roJUVYsyx8=", + "lastModified": 1770130359, + "narHash": "sha256-IfoT9oaeIE6XjXprMORG2qZFzGGZ0v6wJcOlQRdlpvY=", "owner": "NotAShelf", "repo": "nvf", - "rev": "18c55d3bebf2c704970b4ea6fd0261808bec8d94", + "rev": "92854bd0eaaa06914afba345741c372439b8e335", "type": "github" }, "original": { @@ -580,11 +580,11 @@ "brew-src": "brew-src" }, "locked": { - "lastModified": 1764473698, - "narHash": "sha256-C91gPgv6udN5WuIZWNehp8qdLqlrzX6iF/YyboOj6XI=", + "lastModified": 1769437432, + "narHash": "sha256-8d7KnCpT2LweRvSzZYEGd9IM3eFX+A78opcnDM0+ndk=", "owner": "zhaofengli-wip", "repo": "nix-homebrew", - "rev": "6a8ab60bfd66154feeaa1021fc3b32684814a62a", + "rev": "a5409abd0d5013d79775d3419bcac10eacb9d8c5", "type": "github" }, "original": { @@ -595,11 +595,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1768736227, - "narHash": "sha256-qgGq7CfrYKc3IBYQ7qp0Z/ZXndQVC5Bj0N8HW9mS2rM=", + "lastModified": 1769302137, + "narHash": "sha256-QEDtctEkOsbx8nlFh4yqPEOtr4tif6KTqWwJ37IM2ds=", "owner": "NixOS", "repo": "nixos-hardware", - "rev": "d447553bcbc6a178618d37e61648b19e744370df", + "rev": "a351494b0e35fd7c0b7a1aae82f0afddf4907aa8", "type": "github" }, "original": { @@ -642,11 +642,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1768940263, - "narHash": "sha256-sJERJIYTKPFXkoz/gBaBtRKke82h4DkX3BBSsKbfbvI=", + "lastModified": 1770056022, + "narHash": "sha256-yvCz+Qmci1bVucXEyac3TdoSPMtjqVJmVy5wro6j/70=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3ceaaa8bc963ced4d830e06ea2d0863b6490ff03", + "rev": "d04d8548aed39902419f14a8537006426dc1e4fa", "type": "github" }, "original": { @@ -748,11 +748,11 @@ ] }, "locked": { - "lastModified": 1769050281, - "narHash": "sha256-1H8DN4UZgEUqPUA5ecHOufLZMscJ4IlcGaEftaPtpBY=", + "lastModified": 1770088046, + "narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "6deef0585c52d9e70f96b6121207e1496d4b0c49", + "rev": "71f9daa4e05e49c434d08627e755495ae222bc34", "type": "github" }, "original": { @@ -835,11 +835,11 @@ ] }, "locked": { - "lastModified": 1768863606, - "narHash": "sha256-1IHAeS8WtBiEo5XiyJBHOXMzECD6aaIOJmpQKzRRl64=", + "lastModified": 1770110318, + "narHash": "sha256-NUVGVtYBTC96WhPh4Y3SVM7vf0o1z5W4uqRBn9v1pfo=", "owner": "Mic92", "repo": "sops-nix", - "rev": "c7067be8db2c09ab1884de67ef6c4f693973f4a2", + "rev": "f990b0a334e96d3ef9ca09d4bd92778b42fd84f9", "type": "github" }, "original": { @@ -857,11 +857,11 @@ "rust-overlay": "rust-overlay_3" }, "locked": { - "lastModified": 1768997903, - "narHash": "sha256-UpBfh3I4PhykVHqV74rrxufF3X1Z8z8sx/lFgMFfIP8=", + "lastModified": 1769829418, + "narHash": "sha256-ALZKPUa0eHP6HwETAJ9PsAnYQjNLF6eEpo1W2fmYqwA=", "owner": "haraldh", "repo": "ssh-tresor", - "rev": "dd45aed45f8d9b8729b7698ef43e7cc32fab97b6", + "rev": "2e1bfa29bd5ad5a60c3e0effd69851a67d455781", "type": "github" }, "original": { @@ -932,11 +932,11 @@ }, "unstable": { "locked": { - "lastModified": 1768886240, - "narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=", + "lastModified": 1770115704, + "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", "owner": "nixos", "repo": "nixpkgs", - "rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0", + "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", "type": "github" }, "original": { @@ -949,16 +949,16 @@ "xremap": { "flake": false, "locked": { - "lastModified": 1766606475, - "narHash": "sha256-FPZ4iQA/vVZGzbO8i8lTK8i9A3zs9BLqMvTMeAVv9rQ=", + "lastModified": 1769021727, + "narHash": "sha256-2wylBk3+Zu1pHa41dhKwvUtxOVyHSMRDfOD9fIp8x2I=", "owner": "k0kubun", "repo": "xremap", - "rev": "cdc744d873c19899ef21f329c4305b4b5e53d459", + "rev": "890e0a6ca92e90f3bcbd1e235abcf2192e233a46", "type": "github" }, "original": { "owner": "k0kubun", - "ref": "v0.14.8", + "ref": "v0.14.10", "repo": "xremap", "type": "github" } @@ -971,11 +971,11 @@ "xremap": "xremap" }, "locked": { - "lastModified": 1767318478, - "narHash": "sha256-h3oE50RedA8DRGrFU+Hv2kirt4rmzdaC9oSD+MSg9Ms=", + "lastModified": 1769636170, + "narHash": "sha256-X000Dgg053Dv9NIzm1b9QYSAHYtW2jHMVALQezui7L0=", "owner": "xremap", "repo": "nix-flake", - "rev": "9a2224aa01a3c86e94b398c33329c8ff6496dc5d", + "rev": "00bc6dd4275d4b003a17ef7f5f271ba87f73d698", "type": "github" }, "original": { diff --git a/modules/nixos/hardware/wooting/default.nix b/modules/nixos/hardware/wooting/default.nix new file mode 100644 index 0000000..e5cc8f5 --- /dev/null +++ b/modules/nixos/hardware/wooting/default.nix @@ -0,0 +1,25 @@ +{ + 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" + ''; + }; +} diff --git a/modules/nixos/services/acme-base/default.nix b/modules/nixos/services/acme-base/default.nix new file mode 100644 index 0000000..d572848 --- /dev/null +++ b/modules/nixos/services/acme-base/default.nix @@ -0,0 +1,41 @@ +{ + 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; + }; + }; + }; +} diff --git a/modules/nixos/services/nginx-base/default.nix b/modules/nixos/services/nginx-base/default.nix new file mode 100644 index 0000000..6b2dd52 --- /dev/null +++ b/modules/nixos/services/nginx-base/default.nix @@ -0,0 +1,42 @@ +{ + 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; + ''; + }; + }; +} diff --git a/modules/nixos/services/xremap/default.nix b/modules/nixos/services/xremap/default.nix new file mode 100644 index 0000000..6f22f38 --- /dev/null +++ b/modules/nixos/services/xremap/default.nix @@ -0,0 +1,44 @@ +{ + 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" ]; + }; +} diff --git a/modules/nixos/system/kernel-tweaks/default.nix b/modules/nixos/system/kernel-tweaks/default.nix new file mode 100644 index 0000000..0443809 --- /dev/null +++ b/modules/nixos/system/kernel-tweaks/default.nix @@ -0,0 +1,29 @@ +{ + 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; + }; +} diff --git a/modules/nixos/system/no-sleep/default.nix b/modules/nixos/system/no-sleep/default.nix new file mode 100644 index 0000000..9e12659 --- /dev/null +++ b/modules/nixos/system/no-sleep/default.nix @@ -0,0 +1,28 @@ +{ + 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"; + }; +} diff --git a/systems/aarch64-linux/m4nix/default.nix b/systems/aarch64-linux/m4nix/default.nix index d8cd570..f8a6934 100644 --- a/systems/aarch64-linux/m4nix/default.nix +++ b/systems/aarch64-linux/m4nix/default.nix @@ -9,7 +9,13 @@ 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; @@ -34,13 +40,6 @@ 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 @@ -60,16 +59,11 @@ 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 = { @@ -78,7 +72,5 @@ with lib.metacfg; allowReboot = false; }; - virtualisation.rosetta.enable = true; - system.stateVersion = "25.05"; } diff --git a/systems/aarch64-linux/rnix/default.nix b/systems/aarch64-linux/rnix/default.nix index d8cd570..f8a6934 100644 --- a/systems/aarch64-linux/rnix/default.nix +++ b/systems/aarch64-linux/rnix/default.nix @@ -9,7 +9,13 @@ 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; @@ -34,13 +40,6 @@ 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 @@ -60,16 +59,11 @@ 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 = { @@ -78,7 +72,5 @@ with lib.metacfg; allowReboot = false; }; - virtualisation.rosetta.enable = true; - system.stateVersion = "25.05"; } diff --git a/systems/x86_64-linux/amd/default.nix b/systems/x86_64-linux/amd/default.nix index 08b0b84..795fe5d 100644 --- a/systems/x86_64-linux/amd/default.nix +++ b/systems/x86_64-linux/amd/default.nix @@ -18,21 +18,17 @@ with lib.metacfg; 22000 ]; - services.tailscale.enable = true; - services.cratedocs-mcp.enable = true; services.openssh = { 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" - ''; + services.tailscale.enable = true; + services.resolved.enable = true; metacfg = { + hardware.wooting.enable = true; base.enable = true; gui.enable = true; nix-ld.enable = true; @@ -59,15 +55,21 @@ with lib.metacfg; "dialout" "tss" ]; + system.kernelTweaks.enable = true; + }; + + system.autoUpgrade = { + enable = true; + operation = "boot"; + allowReboot = false; }; nixpkgs.config.permittedInsecurePackages = [ "electron-27.3.11" ]; - # Kernel tuning + # Additional kernel tuning beyond the module defaults boot.kernel.sysctl = { - "power.pm_freeze_timeout" = 30000; # Reduce swap usage (you have zram) "vm.swappiness" = 10; # Prefer keeping directory/inode caches @@ -101,6 +103,7 @@ with lib.metacfg; kubectl kubectx logseq + nvtopPackages.amd obsidian piper-tts tipp10 @@ -111,32 +114,18 @@ 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 = { diff --git a/systems/x86_64-linux/amd/xremap.nix b/systems/x86_64-linux/amd/xremap.nix index 64a45c0..c28dd0a 100644 --- a/systems/x86_64-linux/amd/xremap.nix +++ b/systems/x86_64-linux/amd/xremap.nix @@ -1,33 +1,21 @@ -# In /etc/nixos/configuration.nix { ... }: { - 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 - ]; - - # 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"; - }; - } + metacfg.services.xremap = { + enable = true; + deviceNames = [ + "Hangsheng MonsGeek Keyboard" + "HS Galaxy100 Keyboard" ]; + config = { + keymap = [ + { + remap = { + LeftAlt-C = "COPY"; + LeftAlt-V = "PASTE"; + LeftAlt-X = "CUT"; + }; + } + ]; + }; }; } diff --git a/systems/x86_64-linux/mx/acme.nix b/systems/x86_64-linux/mx/acme.nix index 069bb2d..ee338c4 100644 --- a/systems/x86_64-linux/mx/acme.nix +++ b/systems/x86_64-linux/mx/acme.nix @@ -1,6 +1,4 @@ { - pkgs, - lib, config, ... }: @@ -9,14 +7,9 @@ sopsFile = ../../../.secrets/hetzner/internetbs.yaml; # bring your own password file }; - security.acme = { - acceptTerms = true; - defaults = { - email = "harald@hoyer.xyz"; - dnsProvider = "cloudflare"; - credentialsFile = config.sops.secrets.internetbs.path; - }; - certs = { + metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path; + + security.acme.certs = { "surfsite.org" = { extraDomainNames = [ "*.surfsite.org" ]; }; @@ -71,5 +64,4 @@ extraDomainNames = [ "*.harald-hoyer.de" ]; }; }; - }; } diff --git a/systems/x86_64-linux/mx/default.nix b/systems/x86_64-linux/mx/default.nix index e8ce185..3efb22c 100644 --- a/systems/x86_64-linux/mx/default.nix +++ b/systems/x86_64-linux/mx/default.nix @@ -12,6 +12,7 @@ ./mailserver.nix ./network.nix ./nextcloud.nix + ./nextcloud-claude-bot ./nginx.nix ./postgresql.nix ./rspamd.nix @@ -22,6 +23,8 @@ services.tailscale.enable = true; metacfg = { + services.nginxBase.enable = true; + services.acmeBase.enable = true; emailOnFailure.enable = true; base.enable = true; nix.enable = true; @@ -42,7 +45,6 @@ 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"; }; diff --git a/systems/x86_64-linux/mx/nextcloud-claude-bot/README.md b/systems/x86_64-linux/mx/nextcloud-claude-bot/README.md new file mode 100644 index 0000000..a16b3f8 --- /dev/null +++ b/systems/x86_64-linux/mx/nextcloud-claude-bot/README.md @@ -0,0 +1,146 @@ +# 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 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 diff --git a/systems/x86_64-linux/mx/nextcloud-claude-bot/bot.py b/systems/x86_64-linux/mx/nextcloud-claude-bot/bot.py new file mode 100644 index 0000000..3788d2a --- /dev/null +++ b/systems/x86_64-linux/mx/nextcloud-claude-bot/bot.py @@ -0,0 +1,355 @@ +#!/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= + 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: +- : Die letzten Nachrichten im Chatraum (User und deine Antworten) +- : 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("") + for role, msg in history[-MAX_HISTORY:]: + parts.append(f"{role}: {msg}") + parts.append("") + parts.append("") + + # Add current message + parts.append(f"") + parts.append(current_message) + parts.append("") + + 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) diff --git a/systems/x86_64-linux/mx/nextcloud-claude-bot/default.nix b/systems/x86_64-linux/mx/nextcloud-claude-bot/default.nix new file mode 100644 index 0000000..9bda0c7 --- /dev/null +++ b/systems/x86_64-linux/mx/nextcloud-claude-bot/default.nix @@ -0,0 +1,34 @@ +{ 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; + ''; + }; +} diff --git a/systems/x86_64-linux/mx/nextcloud-claude-bot/example-config.nix b/systems/x86_64-linux/mx/nextcloud-claude-bot/example-config.nix new file mode 100644 index 0000000..9560ff3 --- /dev/null +++ b/systems/x86_64-linux/mx/nextcloud-claude-bot/example-config.nix @@ -0,0 +1,80 @@ +# 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; + ''; + }; + }; +} diff --git a/systems/x86_64-linux/mx/nextcloud-claude-bot/module.nix b/systems/x86_64-linux/mx/nextcloud-claude-bot/module.nix new file mode 100644 index 0000000..6edcd71 --- /dev/null +++ b/systems/x86_64-linux/mx/nextcloud-claude-bot/module.nix @@ -0,0 +1,143 @@ +{ 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}/"; + # }; + }; +} diff --git a/systems/x86_64-linux/mx/nginx.nix b/systems/x86_64-linux/mx/nginx.nix index 26556bf..e71eb46 100644 --- a/systems/x86_64-linux/mx/nginx.nix +++ b/systems/x86_64-linux/mx/nginx.nix @@ -1,21 +1,6 @@ { ... }: { - 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 = { + services.nginx.virtualHosts = { "00000" = { useACMEHost = "hoyer.xyz"; serverName = "_"; @@ -157,5 +142,4 @@ forceSSL = true; }; }; - }; } diff --git a/systems/x86_64-linux/nixtee1/default.nix b/systems/x86_64-linux/nixtee1/default.nix index 4ca3282..6c879ae 100644 --- a/systems/x86_64-linux/nixtee1/default.nix +++ b/systems/x86_64-linux/nixtee1/default.nix @@ -6,8 +6,6 @@ { imports = [ ./hardware-configuration.nix ]; - services.tailscale.enable = true; - boot.kernelPackages = lib.mkOverride 0 pkgs.linuxPackages_latest; boot.loader.systemd-boot.enable = false; # Bootloader. @@ -18,6 +16,8 @@ security.tpm2.enable = false; security.tpm2.abrmd.enable = false; + services.tailscale.enable = true; + metacfg = { base.enable = true; nix-ld.enable = true; @@ -37,12 +37,6 @@ 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; @@ -66,5 +60,11 @@ } ]; + system.autoUpgrade = { + enable = true; + operation = "switch"; + allowReboot = true; + }; + system.stateVersion = "25.05"; } diff --git a/systems/x86_64-linux/sgx-attic/default.nix b/systems/x86_64-linux/sgx-attic/default.nix index 5cd7e1d..896c57c 100644 --- a/systems/x86_64-linux/sgx-attic/default.nix +++ b/systems/x86_64-linux/sgx-attic/default.nix @@ -1,7 +1,5 @@ { - pkgs, lib, - config, ... }: with lib; @@ -17,17 +15,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; diff --git a/systems/x86_64-linux/sgx/acme.nix b/systems/x86_64-linux/sgx/acme.nix index b3e1272..76df2ef 100644 --- a/systems/x86_64-linux/sgx/acme.nix +++ b/systems/x86_64-linux/sgx/acme.nix @@ -7,14 +7,9 @@ sopsFile = ../../../.secrets/sgx/internetbs.yaml; # bring your own password file }; - security.acme = { - acceptTerms = true; - defaults = { - email = "harald@hoyer.xyz"; - dnsProvider = "cloudflare"; - credentialsFile = config.sops.secrets.internetbs.path; - }; - certs = { + metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path; + + security.acme.certs = { "internal.hoyer.world" = { extraDomainNames = [ "openwebui.hoyer.world" @@ -23,5 +18,4 @@ ]; }; }; - }; } diff --git a/systems/x86_64-linux/sgx/default.nix b/systems/x86_64-linux/sgx/default.nix index 0a95eef..e4ecaa8 100644 --- a/systems/x86_64-linux/sgx/default.nix +++ b/systems/x86_64-linux/sgx/default.nix @@ -12,8 +12,6 @@ ./wyoming.nix ]; - services.tailscale.enable = true; - boot.tmp.useTmpfs = false; sops.secrets.pccs.sopsFile = ../../../.secrets/sgx/pccs.yaml; @@ -23,7 +21,16 @@ 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; @@ -58,13 +65,5 @@ 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"; } diff --git a/systems/x86_64-linux/sgx/nginx.nix b/systems/x86_64-linux/sgx/nginx.nix index 0c685c0..52f1cdc 100644 --- a/systems/x86_64-linux/sgx/nginx.nix +++ b/systems/x86_64-linux/sgx/nginx.nix @@ -3,22 +3,7 @@ ... }: { - 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 = { + services.nginx.virtualHosts = { "openwebui.hoyer.world" = { enableACME = false; useACMEHost = "internal.hoyer.world"; @@ -48,5 +33,4 @@ }; }; }; - }; } diff --git a/systems/x86_64-linux/t15/default.nix b/systems/x86_64-linux/t15/default.nix index cce5666..9e39ddc 100644 --- a/systems/x86_64-linux/t15/default.nix +++ b/systems/x86_64-linux/t15/default.nix @@ -2,6 +2,8 @@ { imports = [ ./hardware-configuration.nix ]; + services.resolved.enable = true; + metacfg = { base.enable = true; gui.enable = true; @@ -27,9 +29,6 @@ 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; diff --git a/systems/x86_64-linux/x1/default.nix b/systems/x86_64-linux/x1/default.nix index 96122d9..84fbaea 100644 --- a/systems/x86_64-linux/x1/default.nix +++ b/systems/x86_64-linux/x1/default.nix @@ -20,8 +20,6 @@ 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" ]; @@ -45,13 +43,11 @@ with lib.metacfg; ]; }; - 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" - ''; + services.tailscale.enable = true; + services.resolved.enable = true; metacfg = { + hardware.wooting.enable = true; base.enable = true; gui.enable = true; nix-ld.enable = true; @@ -77,17 +73,19 @@ 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 @@ -112,26 +110,12 @@ 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 = { diff --git a/systems/x86_64-linux/x1/xremap.nix b/systems/x86_64-linux/x1/xremap.nix index 64a45c0..c28dd0a 100644 --- a/systems/x86_64-linux/x1/xremap.nix +++ b/systems/x86_64-linux/x1/xremap.nix @@ -1,33 +1,21 @@ -# In /etc/nixos/configuration.nix { ... }: { - 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 - ]; - - # 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"; - }; - } + metacfg.services.xremap = { + enable = true; + deviceNames = [ + "Hangsheng MonsGeek Keyboard" + "HS Galaxy100 Keyboard" ]; + config = { + keymap = [ + { + remap = { + LeftAlt-C = "COPY"; + LeftAlt-V = "PASTE"; + LeftAlt-X = "CUT"; + }; + } + ]; + }; }; }