Compare commits
No commits in common. "main" and "1" have entirely different histories.
28 changed files with 268 additions and 1155 deletions
|
|
@ -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
100
flake.lock
generated
|
|
@ -19,16 +19,16 @@
|
||||||
"brew-src": {
|
"brew-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769363988,
|
"lastModified": 1763638478,
|
||||||
"narHash": "sha256-BiGPeulrDVetXP+tjxhMcGLUROZAtZIhU5m4MqawCfM=",
|
"narHash": "sha256-n/IMowE9S23ovmTkKX7KhxXC2Yq41EAVFR2FBIXPcT8=",
|
||||||
"owner": "Homebrew",
|
"owner": "Homebrew",
|
||||||
"repo": "brew",
|
"repo": "brew",
|
||||||
"rev": "d01011cac6d72032c75fd2cd9489909e95d9faf2",
|
"rev": "fbfdbaba008189499958a7aeb1e2c36ab10c067d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "Homebrew",
|
"owner": "Homebrew",
|
||||||
"ref": "5.0.12",
|
"ref": "5.0.3",
|
||||||
"repo": "brew",
|
"repo": "brew",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
@ -134,11 +134,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769524058,
|
"lastModified": 1768923567,
|
||||||
"narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=",
|
"narHash": "sha256-GVJ0jKsyXLuBzRMXCDY6D5J8wVdwP1DuQmmvYL/Vw/Q=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d",
|
"rev": "00395d188e3594a1507f214a2f15d4ce5c07cb28",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -421,11 +421,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769580047,
|
"lastModified": 1768949235,
|
||||||
"narHash": "sha256-tNqCP/+2+peAXXQ2V8RwsBkenlfWMERb+Uy6xmevyhM=",
|
"narHash": "sha256-TtjKgXyg1lMfh374w5uxutd6Vx2P/hU81aEhTxrO2cg=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "366d78c2856de6ab3411c15c1cb4fb4c2bf5c826",
|
"rev": "75ed713570ca17427119e7e204ab3590cc3bf2a5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -454,11 +454,11 @@
|
||||||
"homebrew-cask": {
|
"homebrew-cask": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770127519,
|
"lastModified": 1769077283,
|
||||||
"narHash": "sha256-wIpVsLhx1gaB2JYfpVipt9ZLAReKFO0kmVIOhieHfqs=",
|
"narHash": "sha256-alvFQmhX8POHxBP3/jResx6AJ06X+k6SF4/CiNndpPA=",
|
||||||
"owner": "homebrew",
|
"owner": "homebrew",
|
||||||
"repo": "homebrew-cask",
|
"repo": "homebrew-cask",
|
||||||
"rev": "76e6c1bda247fe48dc30683203cce2b28b5d6eee",
|
"rev": "4a8185e145fa4fc8326705c666d608c3ee761612",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -470,11 +470,11 @@
|
||||||
"homebrew-core": {
|
"homebrew-core": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770130704,
|
"lastModified": 1769077518,
|
||||||
"narHash": "sha256-95Jwssj3WbBwHO4nNB5uVIgIym/fuSDBb5vs6eKdgp0=",
|
"narHash": "sha256-QtWC5CcY9xzfjcThSwZgise9RXbM2vZmw+Tot67RiJo=",
|
||||||
"owner": "homebrew",
|
"owner": "homebrew",
|
||||||
"repo": "homebrew-core",
|
"repo": "homebrew-core",
|
||||||
"rev": "5369d45006ea107dead79ef8ef4b29b7c972f276",
|
"rev": "2ac083c750fa2a6999ad05a7352e8edbd7abd969",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -515,11 +515,11 @@
|
||||||
},
|
},
|
||||||
"mnw": {
|
"mnw": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769981889,
|
"lastModified": 1768701608,
|
||||||
"narHash": "sha256-ndI7AxL/6auelkLHngdUGVImBiHkG8w2N2fOTKZKn4k=",
|
"narHash": "sha256-kSvWF3Xt2HW9hmV5V7i8PqeWJIBUKmuKoHhOgj3Znzs=",
|
||||||
"owner": "Gerg-L",
|
"owner": "Gerg-L",
|
||||||
"repo": "mnw",
|
"repo": "mnw",
|
||||||
"rev": "332fed8f43b77149c582f1782683d6aeee1f07cf",
|
"rev": "20d63a8a1ae400557c770052a46a9840e768926b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -562,11 +562,11 @@
|
||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770130359,
|
"lastModified": 1768906339,
|
||||||
"narHash": "sha256-IfoT9oaeIE6XjXprMORG2qZFzGGZ0v6wJcOlQRdlpvY=",
|
"narHash": "sha256-iwkHIz2IYRcELkBoKXQUHlP0bFGmrHIz/roJUVYsyx8=",
|
||||||
"owner": "NotAShelf",
|
"owner": "NotAShelf",
|
||||||
"repo": "nvf",
|
"repo": "nvf",
|
||||||
"rev": "92854bd0eaaa06914afba345741c372439b8e335",
|
"rev": "18c55d3bebf2c704970b4ea6fd0261808bec8d94",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -580,11 +580,11 @@
|
||||||
"brew-src": "brew-src"
|
"brew-src": "brew-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769437432,
|
"lastModified": 1764473698,
|
||||||
"narHash": "sha256-8d7KnCpT2LweRvSzZYEGd9IM3eFX+A78opcnDM0+ndk=",
|
"narHash": "sha256-C91gPgv6udN5WuIZWNehp8qdLqlrzX6iF/YyboOj6XI=",
|
||||||
"owner": "zhaofengli-wip",
|
"owner": "zhaofengli-wip",
|
||||||
"repo": "nix-homebrew",
|
"repo": "nix-homebrew",
|
||||||
"rev": "a5409abd0d5013d79775d3419bcac10eacb9d8c5",
|
"rev": "6a8ab60bfd66154feeaa1021fc3b32684814a62a",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -595,11 +595,11 @@
|
||||||
},
|
},
|
||||||
"nixos-hardware": {
|
"nixos-hardware": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769302137,
|
"lastModified": 1768736227,
|
||||||
"narHash": "sha256-QEDtctEkOsbx8nlFh4yqPEOtr4tif6KTqWwJ37IM2ds=",
|
"narHash": "sha256-qgGq7CfrYKc3IBYQ7qp0Z/ZXndQVC5Bj0N8HW9mS2rM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixos-hardware",
|
"repo": "nixos-hardware",
|
||||||
"rev": "a351494b0e35fd7c0b7a1aae82f0afddf4907aa8",
|
"rev": "d447553bcbc6a178618d37e61648b19e744370df",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -642,11 +642,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770056022,
|
"lastModified": 1768940263,
|
||||||
"narHash": "sha256-yvCz+Qmci1bVucXEyac3TdoSPMtjqVJmVy5wro6j/70=",
|
"narHash": "sha256-sJERJIYTKPFXkoz/gBaBtRKke82h4DkX3BBSsKbfbvI=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d04d8548aed39902419f14a8537006426dc1e4fa",
|
"rev": "3ceaaa8bc963ced4d830e06ea2d0863b6490ff03",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -748,11 +748,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770088046,
|
"lastModified": 1769050281,
|
||||||
"narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=",
|
"narHash": "sha256-1H8DN4UZgEUqPUA5ecHOufLZMscJ4IlcGaEftaPtpBY=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "71f9daa4e05e49c434d08627e755495ae222bc34",
|
"rev": "6deef0585c52d9e70f96b6121207e1496d4b0c49",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -835,11 +835,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770110318,
|
"lastModified": 1768863606,
|
||||||
"narHash": "sha256-NUVGVtYBTC96WhPh4Y3SVM7vf0o1z5W4uqRBn9v1pfo=",
|
"narHash": "sha256-1IHAeS8WtBiEo5XiyJBHOXMzECD6aaIOJmpQKzRRl64=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "f990b0a334e96d3ef9ca09d4bd92778b42fd84f9",
|
"rev": "c7067be8db2c09ab1884de67ef6c4f693973f4a2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -857,11 +857,11 @@
|
||||||
"rust-overlay": "rust-overlay_3"
|
"rust-overlay": "rust-overlay_3"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769829418,
|
"lastModified": 1768997903,
|
||||||
"narHash": "sha256-ALZKPUa0eHP6HwETAJ9PsAnYQjNLF6eEpo1W2fmYqwA=",
|
"narHash": "sha256-UpBfh3I4PhykVHqV74rrxufF3X1Z8z8sx/lFgMFfIP8=",
|
||||||
"owner": "haraldh",
|
"owner": "haraldh",
|
||||||
"repo": "ssh-tresor",
|
"repo": "ssh-tresor",
|
||||||
"rev": "2e1bfa29bd5ad5a60c3e0effd69851a67d455781",
|
"rev": "dd45aed45f8d9b8729b7698ef43e7cc32fab97b6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -932,11 +932,11 @@
|
||||||
},
|
},
|
||||||
"unstable": {
|
"unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1770115704,
|
"lastModified": 1768886240,
|
||||||
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
"narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
"rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -949,16 +949,16 @@
|
||||||
"xremap": {
|
"xremap": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769021727,
|
"lastModified": 1766606475,
|
||||||
"narHash": "sha256-2wylBk3+Zu1pHa41dhKwvUtxOVyHSMRDfOD9fIp8x2I=",
|
"narHash": "sha256-FPZ4iQA/vVZGzbO8i8lTK8i9A3zs9BLqMvTMeAVv9rQ=",
|
||||||
"owner": "k0kubun",
|
"owner": "k0kubun",
|
||||||
"repo": "xremap",
|
"repo": "xremap",
|
||||||
"rev": "890e0a6ca92e90f3bcbd1e235abcf2192e233a46",
|
"rev": "cdc744d873c19899ef21f329c4305b4b5e53d459",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "k0kubun",
|
"owner": "k0kubun",
|
||||||
"ref": "v0.14.10",
|
"ref": "v0.14.8",
|
||||||
"repo": "xremap",
|
"repo": "xremap",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
@ -971,11 +971,11 @@
|
||||||
"xremap": "xremap"
|
"xremap": "xremap"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769636170,
|
"lastModified": 1767318478,
|
||||||
"narHash": "sha256-X000Dgg053Dv9NIzm1b9QYSAHYtW2jHMVALQezui7L0=",
|
"narHash": "sha256-h3oE50RedA8DRGrFU+Hv2kirt4rmzdaC9oSD+MSg9Ms=",
|
||||||
"owner": "xremap",
|
"owner": "xremap",
|
||||||
"repo": "nix-flake",
|
"repo": "nix-flake",
|
||||||
"rev": "00bc6dd4275d4b003a17ef7f5f271ba87f73d698",
|
"rev": "9a2224aa01a3c86e94b398c33329c8ff6496dc5d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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" ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -9,13 +9,7 @@ with lib.metacfg;
|
||||||
services.spice-autorandr.enable = true;
|
services.spice-autorandr.enable = true;
|
||||||
services.spice-vdagentd.enable = true;
|
services.spice-vdagentd.enable = true;
|
||||||
|
|
||||||
services.resolved.enable = true;
|
|
||||||
services.resolved.extraConfig = ''
|
|
||||||
ResolveUnicastSingleLabel=yes
|
|
||||||
'';
|
|
||||||
|
|
||||||
metacfg = {
|
metacfg = {
|
||||||
system.noSleep.enable = true;
|
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
gui.enable = true;
|
gui.enable = true;
|
||||||
nix-ld.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; [
|
environment.systemPackages = with pkgs; [
|
||||||
azure-cli
|
azure-cli
|
||||||
desktop-file-utils
|
desktop-file-utils
|
||||||
|
|
@ -59,11 +60,16 @@ with lib.metacfg;
|
||||||
|
|
||||||
services.ratbagd.enable = true;
|
services.ratbagd.enable = true;
|
||||||
|
|
||||||
|
services.resolved.enable = true;
|
||||||
|
#services.resolved.dnssec = "allow-downgrade";
|
||||||
|
services.resolved.extraConfig = ''
|
||||||
|
ResolveUnicastSingleLabel=yes
|
||||||
|
'';
|
||||||
|
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
docker.enable = true;
|
docker.enable = true;
|
||||||
podman.dockerCompat = false;
|
podman.dockerCompat = false;
|
||||||
libvirtd.enable = false;
|
libvirtd.enable = false;
|
||||||
rosetta.enable = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
system.autoUpgrade = {
|
system.autoUpgrade = {
|
||||||
|
|
@ -72,5 +78,7 @@ with lib.metacfg;
|
||||||
allowReboot = false;
|
allowReboot = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
virtualisation.rosetta.enable = true;
|
||||||
|
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,7 @@ with lib.metacfg;
|
||||||
services.spice-autorandr.enable = true;
|
services.spice-autorandr.enable = true;
|
||||||
services.spice-vdagentd.enable = true;
|
services.spice-vdagentd.enable = true;
|
||||||
|
|
||||||
services.resolved.enable = true;
|
|
||||||
services.resolved.extraConfig = ''
|
|
||||||
ResolveUnicastSingleLabel=yes
|
|
||||||
'';
|
|
||||||
|
|
||||||
metacfg = {
|
metacfg = {
|
||||||
system.noSleep.enable = true;
|
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
gui.enable = true;
|
gui.enable = true;
|
||||||
nix-ld.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; [
|
environment.systemPackages = with pkgs; [
|
||||||
azure-cli
|
azure-cli
|
||||||
desktop-file-utils
|
desktop-file-utils
|
||||||
|
|
@ -59,11 +60,16 @@ with lib.metacfg;
|
||||||
|
|
||||||
services.ratbagd.enable = true;
|
services.ratbagd.enable = true;
|
||||||
|
|
||||||
|
services.resolved.enable = true;
|
||||||
|
#services.resolved.dnssec = "allow-downgrade";
|
||||||
|
services.resolved.extraConfig = ''
|
||||||
|
ResolveUnicastSingleLabel=yes
|
||||||
|
'';
|
||||||
|
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
docker.enable = true;
|
docker.enable = true;
|
||||||
podman.dockerCompat = false;
|
podman.dockerCompat = false;
|
||||||
libvirtd.enable = false;
|
libvirtd.enable = false;
|
||||||
rosetta.enable = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
system.autoUpgrade = {
|
system.autoUpgrade = {
|
||||||
|
|
@ -72,5 +78,7 @@ with lib.metacfg;
|
||||||
allowReboot = false;
|
allowReboot = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
virtualisation.rosetta.enable = true;
|
||||||
|
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,21 @@ with lib.metacfg;
|
||||||
22000
|
22000
|
||||||
];
|
];
|
||||||
|
|
||||||
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
services.cratedocs-mcp.enable = true;
|
services.cratedocs-mcp.enable = true;
|
||||||
|
|
||||||
services.openssh = {
|
services.openssh = {
|
||||||
enable = true;
|
enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
services.tailscale.enable = true;
|
hardware.bluetooth.input.General.ClassicBondedOnly = false;
|
||||||
services.resolved.enable = true;
|
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 = {
|
metacfg = {
|
||||||
hardware.wooting.enable = true;
|
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
gui.enable = true;
|
gui.enable = true;
|
||||||
nix-ld.enable = true;
|
nix-ld.enable = true;
|
||||||
|
|
@ -55,21 +59,15 @@ with lib.metacfg;
|
||||||
"dialout"
|
"dialout"
|
||||||
"tss"
|
"tss"
|
||||||
];
|
];
|
||||||
system.kernelTweaks.enable = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
system.autoUpgrade = {
|
|
||||||
enable = true;
|
|
||||||
operation = "boot";
|
|
||||||
allowReboot = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nixpkgs.config.permittedInsecurePackages = [
|
nixpkgs.config.permittedInsecurePackages = [
|
||||||
"electron-27.3.11"
|
"electron-27.3.11"
|
||||||
];
|
];
|
||||||
|
|
||||||
# Additional kernel tuning beyond the module defaults
|
# Kernel tuning
|
||||||
boot.kernel.sysctl = {
|
boot.kernel.sysctl = {
|
||||||
|
"power.pm_freeze_timeout" = 30000;
|
||||||
# Reduce swap usage (you have zram)
|
# Reduce swap usage (you have zram)
|
||||||
"vm.swappiness" = 10;
|
"vm.swappiness" = 10;
|
||||||
# Prefer keeping directory/inode caches
|
# Prefer keeping directory/inode caches
|
||||||
|
|
@ -103,7 +101,6 @@ with lib.metacfg;
|
||||||
kubectl
|
kubectl
|
||||||
kubectx
|
kubectx
|
||||||
logseq
|
logseq
|
||||||
nvtopPackages.amd
|
|
||||||
obsidian
|
obsidian
|
||||||
piper-tts
|
piper-tts
|
||||||
tipp10
|
tipp10
|
||||||
|
|
@ -114,18 +111,32 @@ with lib.metacfg;
|
||||||
|
|
||||||
# zram swap with zstd compression for better performance
|
# zram swap with zstd compression for better performance
|
||||||
zramSwap = {
|
zramSwap = {
|
||||||
|
enable = true;
|
||||||
algorithm = "zstd";
|
algorithm = "zstd";
|
||||||
memoryPercent = 50;
|
memoryPercent = 50;
|
||||||
};
|
};
|
||||||
|
|
||||||
services.ratbagd.enable = true;
|
services.ratbagd.enable = true;
|
||||||
|
|
||||||
|
services.resolved.enable = true;
|
||||||
|
|
||||||
|
#services.resolved.dnssec = "allow-downgrade";
|
||||||
|
#services.resolved.extraConfig = ''
|
||||||
|
# ResolveUnicastSingleLabel=yes
|
||||||
|
#'';
|
||||||
|
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
libvirtd.enable = true;
|
libvirtd.enable = true;
|
||||||
docker.enable = true;
|
docker.enable = true;
|
||||||
podman.dockerCompat = false;
|
podman.dockerCompat = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
system.autoUpgrade = {
|
||||||
|
enable = true;
|
||||||
|
operation = "boot";
|
||||||
|
allowReboot = false;
|
||||||
|
};
|
||||||
|
|
||||||
services.trezord.enable = true;
|
services.trezord.enable = true;
|
||||||
|
|
||||||
services.ollama = {
|
services.ollama = {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,33 @@
|
||||||
|
# In /etc/nixos/configuration.nix
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
metacfg.services.xremap = {
|
users.users.harald.extraGroups = [ "input" ];
|
||||||
enable = true;
|
|
||||||
deviceNames = [
|
# Enable the xremap service
|
||||||
"Hangsheng MonsGeek Keyboard"
|
services.xremap.enable = true;
|
||||||
"HS Galaxy100 Keyboard"
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
];
|
];
|
||||||
config = {
|
|
||||||
keymap = [
|
|
||||||
{
|
|
||||||
remap = {
|
|
||||||
LeftAlt-C = "COPY";
|
|
||||||
LeftAlt-V = "PASTE";
|
|
||||||
LeftAlt-X = "CUT";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
{
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
config,
|
config,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
|
@ -7,9 +9,14 @@
|
||||||
sopsFile = ../../../.secrets/hetzner/internetbs.yaml; # bring your own password file
|
sopsFile = ../../../.secrets/hetzner/internetbs.yaml; # bring your own password file
|
||||||
};
|
};
|
||||||
|
|
||||||
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
security.acme.certs = {
|
defaults = {
|
||||||
|
email = "harald@hoyer.xyz";
|
||||||
|
dnsProvider = "cloudflare";
|
||||||
|
credentialsFile = config.sops.secrets.internetbs.path;
|
||||||
|
};
|
||||||
|
certs = {
|
||||||
"surfsite.org" = {
|
"surfsite.org" = {
|
||||||
extraDomainNames = [ "*.surfsite.org" ];
|
extraDomainNames = [ "*.surfsite.org" ];
|
||||||
};
|
};
|
||||||
|
|
@ -64,4 +71,5 @@
|
||||||
extraDomainNames = [ "*.harald-hoyer.de" ];
|
extraDomainNames = [ "*.harald-hoyer.de" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
./mailserver.nix
|
./mailserver.nix
|
||||||
./network.nix
|
./network.nix
|
||||||
./nextcloud.nix
|
./nextcloud.nix
|
||||||
./nextcloud-claude-bot
|
|
||||||
./nginx.nix
|
./nginx.nix
|
||||||
./postgresql.nix
|
./postgresql.nix
|
||||||
./rspamd.nix
|
./rspamd.nix
|
||||||
|
|
@ -23,8 +22,6 @@
|
||||||
services.tailscale.enable = true;
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
metacfg = {
|
metacfg = {
|
||||||
services.nginxBase.enable = true;
|
|
||||||
services.acmeBase.enable = true;
|
|
||||||
emailOnFailure.enable = true;
|
emailOnFailure.enable = true;
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
nix.enable = true;
|
nix.enable = true;
|
||||||
|
|
@ -45,6 +42,7 @@
|
||||||
dates = "04:00";
|
dates = "04:00";
|
||||||
operation = "switch";
|
operation = "switch";
|
||||||
allowReboot = true;
|
allowReboot = true;
|
||||||
|
# flake = lib.mkForce "git+file:///var/lib/gitea/repositories/harald/nixcfg.git#mx";
|
||||||
flake = lib.mkForce "/root/nixcfg/.#mx";
|
flake = lib.mkForce "/root/nixcfg/.#mx";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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}/";
|
|
||||||
# };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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" = {
|
"00000" = {
|
||||||
useACMEHost = "hoyer.xyz";
|
useACMEHost = "hoyer.xyz";
|
||||||
serverName = "_";
|
serverName = "_";
|
||||||
|
|
@ -142,4 +157,5 @@
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
{
|
{
|
||||||
imports = [ ./hardware-configuration.nix ];
|
imports = [ ./hardware-configuration.nix ];
|
||||||
|
|
||||||
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
boot.kernelPackages = lib.mkOverride 0 pkgs.linuxPackages_latest;
|
boot.kernelPackages = lib.mkOverride 0 pkgs.linuxPackages_latest;
|
||||||
boot.loader.systemd-boot.enable = false;
|
boot.loader.systemd-boot.enable = false;
|
||||||
# Bootloader.
|
# Bootloader.
|
||||||
|
|
@ -16,8 +18,6 @@
|
||||||
security.tpm2.enable = false;
|
security.tpm2.enable = false;
|
||||||
security.tpm2.abrmd.enable = false;
|
security.tpm2.abrmd.enable = false;
|
||||||
|
|
||||||
services.tailscale.enable = true;
|
|
||||||
|
|
||||||
metacfg = {
|
metacfg = {
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
nix-ld.enable = true;
|
nix-ld.enable = true;
|
||||||
|
|
@ -37,6 +37,12 @@
|
||||||
podman.dockerCompat = false;
|
podman.dockerCompat = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
system.autoUpgrade = {
|
||||||
|
enable = true;
|
||||||
|
operation = "switch";
|
||||||
|
allowReboot = true;
|
||||||
|
};
|
||||||
|
|
||||||
networking.wireless.enable = false; # Enables wireless support via wpa_supplicant.
|
networking.wireless.enable = false; # Enables wireless support via wpa_supplicant.
|
||||||
|
|
||||||
networking.firewall.allowPing = true;
|
networking.firewall.allowPing = true;
|
||||||
|
|
@ -60,11 +66,5 @@
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
system.autoUpgrade = {
|
|
||||||
enable = true;
|
|
||||||
operation = "switch";
|
|
||||||
allowReboot = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
|
config,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
with lib;
|
with lib;
|
||||||
|
|
@ -15,17 +17,17 @@ with lib.metacfg;
|
||||||
nix.enable = true;
|
nix.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
virtualisation = {
|
||||||
|
docker.enable = true;
|
||||||
|
podman.dockerCompat = false;
|
||||||
|
};
|
||||||
|
|
||||||
system.autoUpgrade = {
|
system.autoUpgrade = {
|
||||||
enable = true;
|
enable = true;
|
||||||
operation = "switch";
|
operation = "switch";
|
||||||
allowReboot = true;
|
allowReboot = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
virtualisation = {
|
|
||||||
docker.enable = true;
|
|
||||||
podman.dockerCompat = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
security.tpm2.enable = false;
|
security.tpm2.enable = false;
|
||||||
security.tpm2.abrmd.enable = false;
|
security.tpm2.abrmd.enable = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,14 @@
|
||||||
sopsFile = ../../../.secrets/sgx/internetbs.yaml; # bring your own password file
|
sopsFile = ../../../.secrets/sgx/internetbs.yaml; # bring your own password file
|
||||||
};
|
};
|
||||||
|
|
||||||
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
|
security.acme = {
|
||||||
|
acceptTerms = true;
|
||||||
security.acme.certs = {
|
defaults = {
|
||||||
|
email = "harald@hoyer.xyz";
|
||||||
|
dnsProvider = "cloudflare";
|
||||||
|
credentialsFile = config.sops.secrets.internetbs.path;
|
||||||
|
};
|
||||||
|
certs = {
|
||||||
"internal.hoyer.world" = {
|
"internal.hoyer.world" = {
|
||||||
extraDomainNames = [
|
extraDomainNames = [
|
||||||
"openwebui.hoyer.world"
|
"openwebui.hoyer.world"
|
||||||
|
|
@ -18,4 +23,5 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@
|
||||||
./wyoming.nix
|
./wyoming.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
boot.tmp.useTmpfs = false;
|
boot.tmp.useTmpfs = false;
|
||||||
|
|
||||||
sops.secrets.pccs.sopsFile = ../../../.secrets/sgx/pccs.yaml;
|
sops.secrets.pccs.sopsFile = ../../../.secrets/sgx/pccs.yaml;
|
||||||
|
|
@ -21,16 +23,7 @@
|
||||||
claude-code
|
claude-code
|
||||||
];
|
];
|
||||||
|
|
||||||
services.tailscale.enable = true;
|
|
||||||
|
|
||||||
metacfg = {
|
metacfg = {
|
||||||
services.nginxBase.enable = true;
|
|
||||||
services.acmeBase.enable = true;
|
|
||||||
system.noSleep = {
|
|
||||||
enable = true;
|
|
||||||
disableGdmAutoSuspend = true;
|
|
||||||
ignoreLidSwitch = true;
|
|
||||||
};
|
|
||||||
emailOnFailure.enable = true;
|
emailOnFailure.enable = true;
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
gui.enable = true;
|
gui.enable = true;
|
||||||
|
|
@ -65,5 +58,13 @@
|
||||||
allowReboot = true;
|
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";
|
system.stateVersion = "23.11";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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" = {
|
"openwebui.hoyer.world" = {
|
||||||
enableACME = false;
|
enableACME = false;
|
||||||
useACMEHost = "internal.hoyer.world";
|
useACMEHost = "internal.hoyer.world";
|
||||||
|
|
@ -33,4 +48,5 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
{
|
{
|
||||||
imports = [ ./hardware-configuration.nix ];
|
imports = [ ./hardware-configuration.nix ];
|
||||||
|
|
||||||
services.resolved.enable = true;
|
|
||||||
|
|
||||||
metacfg = {
|
metacfg = {
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
gui.enable = true;
|
gui.enable = true;
|
||||||
|
|
@ -29,6 +27,9 @@
|
||||||
|
|
||||||
system.stateVersion = "23.11";
|
system.stateVersion = "23.11";
|
||||||
|
|
||||||
|
services.resolved.enable = true;
|
||||||
|
#services.resolved.dnssec = "allow-downgrade";
|
||||||
|
|
||||||
sops.age.sshKeyPaths = [ "/persist/ssh/ssh_host_ed25519_key" ];
|
sops.age.sshKeyPaths = [ "/persist/ssh/ssh_host_ed25519_key" ];
|
||||||
sops.secrets.backup-s3.sopsFile = ../../../.secrets/t15/backup-s3.yaml;
|
sops.secrets.backup-s3.sopsFile = ../../../.secrets/t15/backup-s3.yaml;
|
||||||
sops.secrets.backup-pw.sopsFile = ../../../.secrets/t15/backup-s3.yaml;
|
sops.secrets.backup-pw.sopsFile = ../../../.secrets/t15/backup-s3.yaml;
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ with lib.metacfg;
|
||||||
programs.ccache.enable = true;
|
programs.ccache.enable = true;
|
||||||
nix.settings.extra-sandbox-paths = [ config.programs.ccache.cacheDir ];
|
nix.settings.extra-sandbox-paths = [ config.programs.ccache.cacheDir ];
|
||||||
|
|
||||||
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
services.cratedocs-mcp.enable = true;
|
services.cratedocs-mcp.enable = true;
|
||||||
|
|
||||||
sops.age.sshKeyPaths = [ "/var/lib/secrets/ssh_host_ed25519_key" ];
|
sops.age.sshKeyPaths = [ "/var/lib/secrets/ssh_host_ed25519_key" ];
|
||||||
|
|
@ -43,11 +45,13 @@ with lib.metacfg;
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.tailscale.enable = true;
|
hardware.bluetooth.input.General.ClassicBondedOnly = false;
|
||||||
services.resolved.enable = true;
|
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 = {
|
metacfg = {
|
||||||
hardware.wooting.enable = true;
|
|
||||||
base.enable = true;
|
base.enable = true;
|
||||||
gui.enable = true;
|
gui.enable = true;
|
||||||
nix-ld.enable = true;
|
nix-ld.enable = true;
|
||||||
|
|
@ -73,19 +77,17 @@ with lib.metacfg;
|
||||||
"dialout"
|
"dialout"
|
||||||
"tss"
|
"tss"
|
||||||
];
|
];
|
||||||
system.kernelTweaks.enable = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
system.autoUpgrade = {
|
|
||||||
enable = true;
|
|
||||||
operation = "boot";
|
|
||||||
allowReboot = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
nixpkgs.config.permittedInsecurePackages = [
|
nixpkgs.config.permittedInsecurePackages = [
|
||||||
"electron-27.3.11"
|
"electron-27.3.11"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# increase freezing timeout
|
||||||
|
boot.kernel.sysctl = {
|
||||||
|
"power.pm_freeze_timeout" = 30000;
|
||||||
|
};
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
attic-client
|
attic-client
|
||||||
azure-cli
|
azure-cli
|
||||||
|
|
@ -110,12 +112,26 @@ with lib.metacfg;
|
||||||
vscode
|
vscode
|
||||||
];
|
];
|
||||||
|
|
||||||
|
zramSwap.enable = true;
|
||||||
|
|
||||||
services.ratbagd.enable = true;
|
services.ratbagd.enable = true;
|
||||||
|
|
||||||
|
services.resolved.enable = true;
|
||||||
|
#services.resolved.dnssec = "allow-downgrade";
|
||||||
|
#services.resolved.extraConfig = ''
|
||||||
|
# ResolveUnicastSingleLabel=yes
|
||||||
|
#'';
|
||||||
|
|
||||||
virtualisation = {
|
virtualisation = {
|
||||||
libvirtd.enable = true;
|
libvirtd.enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
system.autoUpgrade = {
|
||||||
|
enable = true;
|
||||||
|
operation = "boot";
|
||||||
|
allowReboot = false;
|
||||||
|
};
|
||||||
|
|
||||||
services.trezord.enable = true;
|
services.trezord.enable = true;
|
||||||
|
|
||||||
services.ollama = {
|
services.ollama = {
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,33 @@
|
||||||
|
# In /etc/nixos/configuration.nix
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
metacfg.services.xremap = {
|
users.users.harald.extraGroups = [ "input" ];
|
||||||
enable = true;
|
|
||||||
deviceNames = [
|
# Enable the xremap service
|
||||||
"Hangsheng MonsGeek Keyboard"
|
services.xremap.enable = true;
|
||||||
"HS Galaxy100 Keyboard"
|
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";
|
||||||
|
};
|
||||||
|
}
|
||||||
];
|
];
|
||||||
config = {
|
|
||||||
keymap = [
|
|
||||||
{
|
|
||||||
remap = {
|
|
||||||
LeftAlt-C = "COPY";
|
|
||||||
LeftAlt-V = "PASTE";
|
|
||||||
LeftAlt-X = "CUT";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue