Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6597561ec4 | |||
| 437cb4b606 | |||
| 14c9a4f084 | |||
| 958175fb01 | |||
| 4bc62866a8 | |||
| 9b42e808d3 | |||
| f25aab2441 | |||
| 9342933987 | |||
| b35373b0ec | |||
| 538d7623be | |||
| 1f61a0d1ec | |||
| 77cf4a0aed | |||
| 33937ab115 | |||
| d5967cf392 | |||
| b1370b5fc6 | |||
| 8404f0998b | |||
| bc6091f63f | |||
| eb10ad018f | |||
| 0523639f2a | |||
| 4622c52d5b |
32 changed files with 1324 additions and 311 deletions
35
.secrets/hetzner/nextcloud-claude-bot.yaml
Normal file
35
.secrets/hetzner/nextcloud-claude-bot.yaml
Normal file
|
|
@ -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
|
||||||
100
flake.lock
generated
100
flake.lock
generated
|
|
@ -19,16 +19,16 @@
|
||||||
"brew-src": {
|
"brew-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1763638478,
|
"lastModified": 1769363988,
|
||||||
"narHash": "sha256-n/IMowE9S23ovmTkKX7KhxXC2Yq41EAVFR2FBIXPcT8=",
|
"narHash": "sha256-BiGPeulrDVetXP+tjxhMcGLUROZAtZIhU5m4MqawCfM=",
|
||||||
"owner": "Homebrew",
|
"owner": "Homebrew",
|
||||||
"repo": "brew",
|
"repo": "brew",
|
||||||
"rev": "fbfdbaba008189499958a7aeb1e2c36ab10c067d",
|
"rev": "d01011cac6d72032c75fd2cd9489909e95d9faf2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "Homebrew",
|
"owner": "Homebrew",
|
||||||
"ref": "5.0.3",
|
"ref": "5.0.12",
|
||||||
"repo": "brew",
|
"repo": "brew",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
@ -134,11 +134,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768923567,
|
"lastModified": 1769524058,
|
||||||
"narHash": "sha256-GVJ0jKsyXLuBzRMXCDY6D5J8wVdwP1DuQmmvYL/Vw/Q=",
|
"narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "disko",
|
"repo": "disko",
|
||||||
"rev": "00395d188e3594a1507f214a2f15d4ce5c07cb28",
|
"rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -421,11 +421,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768949235,
|
"lastModified": 1769580047,
|
||||||
"narHash": "sha256-TtjKgXyg1lMfh374w5uxutd6Vx2P/hU81aEhTxrO2cg=",
|
"narHash": "sha256-tNqCP/+2+peAXXQ2V8RwsBkenlfWMERb+Uy6xmevyhM=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "75ed713570ca17427119e7e204ab3590cc3bf2a5",
|
"rev": "366d78c2856de6ab3411c15c1cb4fb4c2bf5c826",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -454,11 +454,11 @@
|
||||||
"homebrew-cask": {
|
"homebrew-cask": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769077283,
|
"lastModified": 1770127519,
|
||||||
"narHash": "sha256-alvFQmhX8POHxBP3/jResx6AJ06X+k6SF4/CiNndpPA=",
|
"narHash": "sha256-wIpVsLhx1gaB2JYfpVipt9ZLAReKFO0kmVIOhieHfqs=",
|
||||||
"owner": "homebrew",
|
"owner": "homebrew",
|
||||||
"repo": "homebrew-cask",
|
"repo": "homebrew-cask",
|
||||||
"rev": "4a8185e145fa4fc8326705c666d608c3ee761612",
|
"rev": "76e6c1bda247fe48dc30683203cce2b28b5d6eee",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -470,11 +470,11 @@
|
||||||
"homebrew-core": {
|
"homebrew-core": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769077518,
|
"lastModified": 1770130704,
|
||||||
"narHash": "sha256-QtWC5CcY9xzfjcThSwZgise9RXbM2vZmw+Tot67RiJo=",
|
"narHash": "sha256-95Jwssj3WbBwHO4nNB5uVIgIym/fuSDBb5vs6eKdgp0=",
|
||||||
"owner": "homebrew",
|
"owner": "homebrew",
|
||||||
"repo": "homebrew-core",
|
"repo": "homebrew-core",
|
||||||
"rev": "2ac083c750fa2a6999ad05a7352e8edbd7abd969",
|
"rev": "5369d45006ea107dead79ef8ef4b29b7c972f276",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -515,11 +515,11 @@
|
||||||
},
|
},
|
||||||
"mnw": {
|
"mnw": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768701608,
|
"lastModified": 1769981889,
|
||||||
"narHash": "sha256-kSvWF3Xt2HW9hmV5V7i8PqeWJIBUKmuKoHhOgj3Znzs=",
|
"narHash": "sha256-ndI7AxL/6auelkLHngdUGVImBiHkG8w2N2fOTKZKn4k=",
|
||||||
"owner": "Gerg-L",
|
"owner": "Gerg-L",
|
||||||
"repo": "mnw",
|
"repo": "mnw",
|
||||||
"rev": "20d63a8a1ae400557c770052a46a9840e768926b",
|
"rev": "332fed8f43b77149c582f1782683d6aeee1f07cf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -562,11 +562,11 @@
|
||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768906339,
|
"lastModified": 1770130359,
|
||||||
"narHash": "sha256-iwkHIz2IYRcELkBoKXQUHlP0bFGmrHIz/roJUVYsyx8=",
|
"narHash": "sha256-IfoT9oaeIE6XjXprMORG2qZFzGGZ0v6wJcOlQRdlpvY=",
|
||||||
"owner": "NotAShelf",
|
"owner": "NotAShelf",
|
||||||
"repo": "nvf",
|
"repo": "nvf",
|
||||||
"rev": "18c55d3bebf2c704970b4ea6fd0261808bec8d94",
|
"rev": "92854bd0eaaa06914afba345741c372439b8e335",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -580,11 +580,11 @@
|
||||||
"brew-src": "brew-src"
|
"brew-src": "brew-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1764473698,
|
"lastModified": 1769437432,
|
||||||
"narHash": "sha256-C91gPgv6udN5WuIZWNehp8qdLqlrzX6iF/YyboOj6XI=",
|
"narHash": "sha256-8d7KnCpT2LweRvSzZYEGd9IM3eFX+A78opcnDM0+ndk=",
|
||||||
"owner": "zhaofengli-wip",
|
"owner": "zhaofengli-wip",
|
||||||
"repo": "nix-homebrew",
|
"repo": "nix-homebrew",
|
||||||
"rev": "6a8ab60bfd66154feeaa1021fc3b32684814a62a",
|
"rev": "a5409abd0d5013d79775d3419bcac10eacb9d8c5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -595,11 +595,11 @@
|
||||||
},
|
},
|
||||||
"nixos-hardware": {
|
"nixos-hardware": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768736227,
|
"lastModified": 1769302137,
|
||||||
"narHash": "sha256-qgGq7CfrYKc3IBYQ7qp0Z/ZXndQVC5Bj0N8HW9mS2rM=",
|
"narHash": "sha256-QEDtctEkOsbx8nlFh4yqPEOtr4tif6KTqWwJ37IM2ds=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixos-hardware",
|
"repo": "nixos-hardware",
|
||||||
"rev": "d447553bcbc6a178618d37e61648b19e744370df",
|
"rev": "a351494b0e35fd7c0b7a1aae82f0afddf4907aa8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -642,11 +642,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768940263,
|
"lastModified": 1770056022,
|
||||||
"narHash": "sha256-sJERJIYTKPFXkoz/gBaBtRKke82h4DkX3BBSsKbfbvI=",
|
"narHash": "sha256-yvCz+Qmci1bVucXEyac3TdoSPMtjqVJmVy5wro6j/70=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "3ceaaa8bc963ced4d830e06ea2d0863b6490ff03",
|
"rev": "d04d8548aed39902419f14a8537006426dc1e4fa",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -748,11 +748,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1769050281,
|
"lastModified": 1770088046,
|
||||||
"narHash": "sha256-1H8DN4UZgEUqPUA5ecHOufLZMscJ4IlcGaEftaPtpBY=",
|
"narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=",
|
||||||
"owner": "oxalica",
|
"owner": "oxalica",
|
||||||
"repo": "rust-overlay",
|
"repo": "rust-overlay",
|
||||||
"rev": "6deef0585c52d9e70f96b6121207e1496d4b0c49",
|
"rev": "71f9daa4e05e49c434d08627e755495ae222bc34",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -835,11 +835,11 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768863606,
|
"lastModified": 1770110318,
|
||||||
"narHash": "sha256-1IHAeS8WtBiEo5XiyJBHOXMzECD6aaIOJmpQKzRRl64=",
|
"narHash": "sha256-NUVGVtYBTC96WhPh4Y3SVM7vf0o1z5W4uqRBn9v1pfo=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "c7067be8db2c09ab1884de67ef6c4f693973f4a2",
|
"rev": "f990b0a334e96d3ef9ca09d4bd92778b42fd84f9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -857,11 +857,11 @@
|
||||||
"rust-overlay": "rust-overlay_3"
|
"rust-overlay": "rust-overlay_3"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768997903,
|
"lastModified": 1769829418,
|
||||||
"narHash": "sha256-UpBfh3I4PhykVHqV74rrxufF3X1Z8z8sx/lFgMFfIP8=",
|
"narHash": "sha256-ALZKPUa0eHP6HwETAJ9PsAnYQjNLF6eEpo1W2fmYqwA=",
|
||||||
"owner": "haraldh",
|
"owner": "haraldh",
|
||||||
"repo": "ssh-tresor",
|
"repo": "ssh-tresor",
|
||||||
"rev": "dd45aed45f8d9b8729b7698ef43e7cc32fab97b6",
|
"rev": "2e1bfa29bd5ad5a60c3e0effd69851a67d455781",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -932,11 +932,11 @@
|
||||||
},
|
},
|
||||||
"unstable": {
|
"unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1768886240,
|
"lastModified": 1770115704,
|
||||||
"narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=",
|
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0",
|
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -949,16 +949,16 @@
|
||||||
"xremap": {
|
"xremap": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766606475,
|
"lastModified": 1769021727,
|
||||||
"narHash": "sha256-FPZ4iQA/vVZGzbO8i8lTK8i9A3zs9BLqMvTMeAVv9rQ=",
|
"narHash": "sha256-2wylBk3+Zu1pHa41dhKwvUtxOVyHSMRDfOD9fIp8x2I=",
|
||||||
"owner": "k0kubun",
|
"owner": "k0kubun",
|
||||||
"repo": "xremap",
|
"repo": "xremap",
|
||||||
"rev": "cdc744d873c19899ef21f329c4305b4b5e53d459",
|
"rev": "890e0a6ca92e90f3bcbd1e235abcf2192e233a46",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "k0kubun",
|
"owner": "k0kubun",
|
||||||
"ref": "v0.14.8",
|
"ref": "v0.14.10",
|
||||||
"repo": "xremap",
|
"repo": "xremap",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
@ -971,11 +971,11 @@
|
||||||
"xremap": "xremap"
|
"xremap": "xremap"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767318478,
|
"lastModified": 1769636170,
|
||||||
"narHash": "sha256-h3oE50RedA8DRGrFU+Hv2kirt4rmzdaC9oSD+MSg9Ms=",
|
"narHash": "sha256-X000Dgg053Dv9NIzm1b9QYSAHYtW2jHMVALQezui7L0=",
|
||||||
"owner": "xremap",
|
"owner": "xremap",
|
||||||
"repo": "nix-flake",
|
"repo": "nix-flake",
|
||||||
"rev": "9a2224aa01a3c86e94b398c33329c8ff6496dc5d",
|
"rev": "00bc6dd4275d4b003a17ef7f5f271ba87f73d698",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
25
modules/nixos/hardware/wooting/default.nix
Normal file
25
modules/nixos/hardware/wooting/default.nix
Normal file
|
|
@ -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"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
41
modules/nixos/services/acme-base/default.nix
Normal file
41
modules/nixos/services/acme-base/default.nix
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
42
modules/nixos/services/nginx-base/default.nix
Normal file
42
modules/nixos/services/nginx-base/default.nix
Normal file
|
|
@ -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;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
44
modules/nixos/services/xremap/default.nix
Normal file
44
modules/nixos/services/xremap/default.nix
Normal file
|
|
@ -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" ];
|
||||||
|
};
|
||||||
|
}
|
||||||
29
modules/nixos/system/kernel-tweaks/default.nix
Normal file
29
modules/nixos/system/kernel-tweaks/default.nix
Normal file
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
28
modules/nixos/system/no-sleep/default.nix
Normal file
28
modules/nixos/system/no-sleep/default.nix
Normal file
|
|
@ -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";
|
||||||
|
};
|
||||||
|
}
|
||||||
82
overlays/unstable/claude-code/package-lock.json
generated
82
overlays/unstable/claude-code/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "@anthropic-ai/claude-code",
|
"name": "@anthropic-ai/claude-code",
|
||||||
"version": "2.0.50",
|
"version": "2.1.34",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@anthropic-ai/claude-code",
|
"name": "@anthropic-ai/claude-code",
|
||||||
"version": "2.0.50",
|
"version": "2.1.34",
|
||||||
"license": "SEE LICENSE IN README.md",
|
"license": "SEE LICENSE IN README.md",
|
||||||
"bin": {
|
"bin": {
|
||||||
"claude": "cli.js"
|
"claude": "cli.js"
|
||||||
|
|
@ -20,6 +20,8 @@
|
||||||
"@img/sharp-linux-arm": "^0.33.5",
|
"@img/sharp-linux-arm": "^0.33.5",
|
||||||
"@img/sharp-linux-arm64": "^0.33.5",
|
"@img/sharp-linux-arm64": "^0.33.5",
|
||||||
"@img/sharp-linux-x64": "^0.33.5",
|
"@img/sharp-linux-x64": "^0.33.5",
|
||||||
|
"@img/sharp-linuxmusl-arm64": "^0.33.5",
|
||||||
|
"@img/sharp-linuxmusl-x64": "^0.33.5",
|
||||||
"@img/sharp-win32-x64": "^0.33.5"
|
"@img/sharp-win32-x64": "^0.33.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -147,6 +149,38 @@
|
||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "LGPL-3.0-or-later",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-linux-arm": {
|
"node_modules/@img/sharp-linux-arm": {
|
||||||
"version": "0.33.5",
|
"version": "0.33.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
|
||||||
|
|
@ -213,6 +247,50 @@
|
||||||
"@img/sharp-libvips-linux-x64": "1.0.4"
|
"@img/sharp-libvips-linux-x64": "1.0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||||
|
"version": "0.33.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
|
||||||
|
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://opencollective.com/libvips"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@img/sharp-win32-x64": {
|
"node_modules/@img/sharp-win32-x64": {
|
||||||
"version": "0.33.5",
|
"version": "0.33.5",
|
||||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,37 @@
|
||||||
|
# NOTE: Use the following command to update the package
|
||||||
|
# ```sh
|
||||||
|
# nix-shell maintainers/scripts/update.nix --argstr commit true --arg predicate '(path: pkg: builtins.elem path [["claude-code"] ["claude-code-bin"] ["vscode-extensions" "anthropic" "claude-code"]])'
|
||||||
|
# ```
|
||||||
{
|
{
|
||||||
lib,
|
lib,
|
||||||
|
stdenv,
|
||||||
buildNpmPackage,
|
buildNpmPackage,
|
||||||
fetchzip,
|
fetchzip,
|
||||||
writableTmpDirAsHomeHook,
|
|
||||||
versionCheckHook,
|
versionCheckHook,
|
||||||
|
writableTmpDirAsHomeHook,
|
||||||
|
bubblewrap,
|
||||||
|
procps,
|
||||||
|
socat,
|
||||||
}:
|
}:
|
||||||
buildNpmPackage (finalAttrs: {
|
buildNpmPackage (finalAttrs: {
|
||||||
pname = "claude-code";
|
pname = "claude-code";
|
||||||
version = "2.0.51";
|
version = "2.1.34";
|
||||||
|
|
||||||
src = fetchzip {
|
src = fetchzip {
|
||||||
url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${finalAttrs.version}.tgz";
|
url = "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-${finalAttrs.version}.tgz";
|
||||||
hash = "sha256-rfJZaACY+Kbm+0lWOPwAfl/x2yFxskLKZpJJhqlccSY=";
|
hash = "sha256-J3kltFY5nR3PsRWbW310VqD/6hhfMbVSvynv0eaIi3M=";
|
||||||
};
|
};
|
||||||
|
|
||||||
npmDepsHash = "sha256-/5Qh99vAcTiFz6FrzJgm26RserqxVjLYqOOx5q5hkgc=";
|
npmDepsHash = "sha256-n762einDxLUUXWMsfdPVhA/kn0ywlJgFQ2ZGoEk3E68=";
|
||||||
|
|
||||||
|
strictDeps = true;
|
||||||
|
|
||||||
postPatch = ''
|
postPatch = ''
|
||||||
cp ${./package-lock.json} package-lock.json
|
cp ${./package-lock.json} package-lock.json
|
||||||
|
|
||||||
|
# https://github.com/anthropics/claude-code/issues/15195
|
||||||
|
substituteInPlace cli.js \
|
||||||
|
--replace-fail '#!/bin/sh' '#!/usr/bin/env sh'
|
||||||
'';
|
'';
|
||||||
|
|
||||||
dontNpmBuild = true;
|
dontNpmBuild = true;
|
||||||
|
|
@ -30,7 +44,21 @@ buildNpmPackage (finalAttrs: {
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
wrapProgram $out/bin/claude \
|
wrapProgram $out/bin/claude \
|
||||||
--set DISABLE_AUTOUPDATER 1 \
|
--set DISABLE_AUTOUPDATER 1 \
|
||||||
--unset DEV
|
--set DISABLE_INSTALLATION_CHECKS 1 \
|
||||||
|
--unset DEV \
|
||||||
|
--prefix PATH : ${
|
||||||
|
lib.makeBinPath (
|
||||||
|
[
|
||||||
|
# claude-code uses [node-tree-kill](https://github.com/pkrumins/node-tree-kill) which requires procps's pgrep(darwin) or ps(linux)
|
||||||
|
procps
|
||||||
|
]
|
||||||
|
# the following packages are required for the sandbox to work (Linux only)
|
||||||
|
++ lib.optionals stdenv.hostPlatform.isLinux [
|
||||||
|
bubblewrap
|
||||||
|
socat
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
doInstallCheck = true;
|
doInstallCheck = true;
|
||||||
|
|
@ -39,7 +67,6 @@ buildNpmPackage (finalAttrs: {
|
||||||
versionCheckHook
|
versionCheckHook
|
||||||
];
|
];
|
||||||
versionCheckKeepEnvironment = [ "HOME" ];
|
versionCheckKeepEnvironment = [ "HOME" ];
|
||||||
versionCheckProgramArg = "--version";
|
|
||||||
|
|
||||||
passthru.updateScript = ./update.sh;
|
passthru.updateScript = ./update.sh;
|
||||||
|
|
||||||
|
|
@ -49,6 +76,7 @@ buildNpmPackage (finalAttrs: {
|
||||||
downloadPage = "https://www.npmjs.com/package/@anthropic-ai/claude-code";
|
downloadPage = "https://www.npmjs.com/package/@anthropic-ai/claude-code";
|
||||||
license = lib.licenses.unfree;
|
license = lib.licenses.unfree;
|
||||||
maintainers = with lib.maintainers; [
|
maintainers = with lib.maintainers; [
|
||||||
|
adeci
|
||||||
malo
|
malo
|
||||||
markus1189
|
markus1189
|
||||||
omarjatoi
|
omarjatoi
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env nix-shell
|
#!/usr/bin/env nix
|
||||||
#!nix-shell --pure --keep NIX_PATH -i bash --packages nodejs nix-update git cacert
|
#!nix shell --ignore-environment .#cacert .#nodejs .#git .#nix-update .#nix .#gnused .#findutils .#bash --command bash
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
|
@ -7,4 +7,9 @@ version=$(npm view @anthropic-ai/claude-code version)
|
||||||
|
|
||||||
# Update version and hashes
|
# Update version and hashes
|
||||||
AUTHORIZED=1 NIXPKGS_ALLOW_UNFREE=1 nix-update claude-code --version="$version" --generate-lockfile
|
AUTHORIZED=1 NIXPKGS_ALLOW_UNFREE=1 nix-update claude-code --version="$version" --generate-lockfile
|
||||||
nix-update vscode-extensions.anthropic.claude-code --use-update-script --version "$version"
|
|
||||||
|
# nix-update can't update package-lock.json along with npmDepsHash
|
||||||
|
# TODO: Remove this workaround if nix-update can update package-lock.json along with npmDepsHash.
|
||||||
|
(nix-build --expr '((import ./.) { system = builtins.currentSystem; }).claude-code.npmDeps.overrideAttrs { outputHash = ""; outputHashAlgo = "sha256"; }' 2>&1 || true) \
|
||||||
|
| sed -nE '$s/ *got: *(sha256-[A-Za-z0-9+/=-]+).*/\1/p' \
|
||||||
|
| xargs -I{} sed -i 's|npmDepsHash = "sha256-[^"]*";|npmDepsHash = "{}";|' pkgs/by-name/cl/claude-code/package.nix
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ final: prev: {
|
||||||
gemini-cli
|
gemini-cli
|
||||||
# opencode
|
# opencode
|
||||||
tailscale
|
tailscale
|
||||||
claude-code
|
# claude-code
|
||||||
# open-webui
|
# open-webui
|
||||||
# vscode
|
# vscode
|
||||||
# nodejs_20
|
# nodejs_20
|
||||||
|
|
@ -19,7 +19,7 @@ final: prev: {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
# goose-cli = channels.unstable.callPackage ./goose.nix { };
|
# goose-cli = channels.unstable.callPackage ./goose.nix { };
|
||||||
# claude-code = channels.unstable.callPackage ./claude-code/package.nix { };
|
claude-code = channels.unstable.callPackage ./claude-code/package.nix { };
|
||||||
# gemini-cli = channels.unstable.callPackage ./gemini-cli/package.nix { };
|
# gemini-cli = channels.unstable.callPackage ./gemini-cli/package.nix { };
|
||||||
# vscode-extensions = channels.unstable.vscode-extensions // {
|
# vscode-extensions = channels.unstable.vscode-extensions // {
|
||||||
# rooveterinaryinc = { roo-cline = channels.unstable.callPackage ./roo-code.nix { }; };
|
# rooveterinaryinc = { roo-cline = channels.unstable.callPackage ./roo-code.nix { }; };
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,13 @@ 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;
|
||||||
|
|
@ -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; [
|
environment.systemPackages = with pkgs; [
|
||||||
azure-cli
|
azure-cli
|
||||||
desktop-file-utils
|
desktop-file-utils
|
||||||
|
|
@ -60,16 +59,11 @@ 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 = {
|
||||||
|
|
@ -78,7 +72,5 @@ with lib.metacfg;
|
||||||
allowReboot = false;
|
allowReboot = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
virtualisation.rosetta.enable = true;
|
|
||||||
|
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,13 @@ 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;
|
||||||
|
|
@ -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; [
|
environment.systemPackages = with pkgs; [
|
||||||
azure-cli
|
azure-cli
|
||||||
desktop-file-utils
|
desktop-file-utils
|
||||||
|
|
@ -60,16 +59,11 @@ 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 = {
|
||||||
|
|
@ -78,7 +72,5 @@ with lib.metacfg;
|
||||||
allowReboot = false;
|
allowReboot = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
virtualisation.rosetta.enable = true;
|
|
||||||
|
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,21 +18,17 @@ 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
hardware.bluetooth.input.General.ClassicBondedOnly = false;
|
services.tailscale.enable = true;
|
||||||
services.udev.extraRules = ''
|
services.resolved.enable = true;
|
||||||
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;
|
||||||
|
|
@ -59,15 +55,21 @@ 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"
|
||||||
];
|
];
|
||||||
|
|
||||||
# Kernel tuning
|
# Additional kernel tuning beyond the module defaults
|
||||||
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
|
||||||
|
|
@ -101,6 +103,7 @@ with lib.metacfg;
|
||||||
kubectl
|
kubectl
|
||||||
kubectx
|
kubectx
|
||||||
logseq
|
logseq
|
||||||
|
nvtopPackages.amd
|
||||||
obsidian
|
obsidian
|
||||||
piper-tts
|
piper-tts
|
||||||
tipp10
|
tipp10
|
||||||
|
|
@ -111,32 +114,18 @@ 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,28 +1,15 @@
|
||||||
# In /etc/nixos/configuration.nix
|
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
users.users.harald.extraGroups = [ "input" ];
|
metacfg.services.xremap = {
|
||||||
|
enable = true;
|
||||||
# Enable the xremap service
|
deviceNames = [
|
||||||
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"
|
"Hangsheng MonsGeek Keyboard"
|
||||||
"HS Galaxy100 Keyboard"
|
"HS Galaxy100 Keyboard"
|
||||||
# You can usually shorten the name slightly to match the device you want
|
|
||||||
];
|
];
|
||||||
|
config = {
|
||||||
# Define your remapping configuration using Nix's attribute set format
|
|
||||||
services.xremap.config = {
|
|
||||||
keymap = [
|
keymap = [
|
||||||
{
|
{
|
||||||
remap = {
|
remap = {
|
||||||
# Map Alt+C (LeftAlt-C) to Ctrl+C (LeftControl-C)
|
|
||||||
LeftAlt-C = "COPY";
|
LeftAlt-C = "COPY";
|
||||||
LeftAlt-V = "PASTE";
|
LeftAlt-V = "PASTE";
|
||||||
LeftAlt-X = "CUT";
|
LeftAlt-X = "CUT";
|
||||||
|
|
@ -30,4 +17,5 @@
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
{
|
{
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
config,
|
config,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
|
@ -9,14 +7,9 @@
|
||||||
sopsFile = ../../../.secrets/hetzner/internetbs.yaml; # bring your own password file
|
sopsFile = ../../../.secrets/hetzner/internetbs.yaml; # bring your own password file
|
||||||
};
|
};
|
||||||
|
|
||||||
security.acme = {
|
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
|
||||||
acceptTerms = true;
|
|
||||||
defaults = {
|
security.acme.certs = {
|
||||||
email = "harald@hoyer.xyz";
|
|
||||||
dnsProvider = "cloudflare";
|
|
||||||
credentialsFile = config.sops.secrets.internetbs.path;
|
|
||||||
};
|
|
||||||
certs = {
|
|
||||||
"surfsite.org" = {
|
"surfsite.org" = {
|
||||||
extraDomainNames = [ "*.surfsite.org" ];
|
extraDomainNames = [ "*.surfsite.org" ];
|
||||||
};
|
};
|
||||||
|
|
@ -71,5 +64,4 @@
|
||||||
extraDomainNames = [ "*.harald-hoyer.de" ];
|
extraDomainNames = [ "*.harald-hoyer.de" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
./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
|
||||||
|
|
@ -22,6 +23,8 @@
|
||||||
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;
|
||||||
|
|
@ -42,7 +45,6 @@
|
||||||
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";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
146
systems/x86_64-linux/mx/nextcloud-claude-bot/README.md
Normal file
146
systems/x86_64-linux/mx/nextcloud-claude-bot/README.md
Normal file
|
|
@ -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 <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
|
||||||
355
systems/x86_64-linux/mx/nextcloud-claude-bot/bot.py
Normal file
355
systems/x86_64-linux/mx/nextcloud-claude-bot/bot.py
Normal file
|
|
@ -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=<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)
|
||||||
34
systems/x86_64-linux/mx/nextcloud-claude-bot/default.nix
Normal file
34
systems/x86_64-linux/mx/nextcloud-claude-bot/default.nix
Normal file
|
|
@ -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;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
143
systems/x86_64-linux/mx/nextcloud-claude-bot/module.nix
Normal file
143
systems/x86_64-linux/mx/nextcloud-claude-bot/module.nix
Normal file
|
|
@ -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}/";
|
||||||
|
# };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,6 @@
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
users.users.nginx.extraGroups = [ "acme" ];
|
services.nginx.virtualHosts = {
|
||||||
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 = "_";
|
||||||
|
|
@ -157,5 +142,4 @@
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@
|
||||||
{
|
{
|
||||||
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.
|
||||||
|
|
@ -18,6 +16,8 @@
|
||||||
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,12 +37,6 @@
|
||||||
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;
|
||||||
|
|
@ -66,5 +60,11 @@
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
system.autoUpgrade = {
|
||||||
|
enable = true;
|
||||||
|
operation = "switch";
|
||||||
|
allowReboot = true;
|
||||||
|
};
|
||||||
|
|
||||||
system.stateVersion = "25.05";
|
system.stateVersion = "25.05";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
{
|
{
|
||||||
pkgs,
|
|
||||||
lib,
|
lib,
|
||||||
config,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
with lib;
|
with lib;
|
||||||
|
|
@ -17,17 +15,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,21 +7,16 @@
|
||||||
sopsFile = ../../../.secrets/sgx/internetbs.yaml; # bring your own password file
|
sopsFile = ../../../.secrets/sgx/internetbs.yaml; # bring your own password file
|
||||||
};
|
};
|
||||||
|
|
||||||
security.acme = {
|
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
|
||||||
acceptTerms = true;
|
|
||||||
defaults = {
|
security.acme.certs = {
|
||||||
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"
|
||||||
"syncthing.hoyer.world"
|
"syncthing.hoyer.world"
|
||||||
"home.hoyer.world"
|
"home.hoyer.world"
|
||||||
|
"search.hoyer.world"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@
|
||||||
./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;
|
||||||
|
|
@ -23,7 +21,25 @@
|
||||||
claude-code
|
claude-code
|
||||||
];
|
];
|
||||||
|
|
||||||
|
services.tailscale.enable = true;
|
||||||
|
|
||||||
|
services.searx = {
|
||||||
|
enable = true;
|
||||||
|
configureNginx = true;
|
||||||
|
domain = "search.hoyer.world";
|
||||||
|
uwsgiConfig = {
|
||||||
|
http = ":8081";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
@ -58,13 +74,5 @@
|
||||||
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,22 +3,7 @@
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
{
|
{
|
||||||
users.users.nginx.extraGroups = [ "acme" ];
|
services.nginx.virtualHosts = {
|
||||||
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";
|
||||||
|
|
@ -47,6 +32,10 @@
|
||||||
proxyWebsockets = true;
|
proxyWebsockets = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
"search.hoyer.world" = {
|
||||||
|
enableACME = false;
|
||||||
|
useACMEHost = "search.hoyer.world";
|
||||||
|
forceSSL = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
{
|
{
|
||||||
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;
|
||||||
|
|
@ -27,9 +29,6 @@
|
||||||
|
|
||||||
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,8 +20,6 @@ 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" ];
|
||||||
|
|
@ -45,13 +43,11 @@ with lib.metacfg;
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
hardware.bluetooth.input.General.ClassicBondedOnly = false;
|
services.tailscale.enable = true;
|
||||||
services.udev.extraRules = ''
|
services.resolved.enable = true;
|
||||||
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;
|
||||||
|
|
@ -77,17 +73,19 @@ 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
|
||||||
|
|
@ -112,26 +110,12 @@ 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,28 +1,15 @@
|
||||||
# In /etc/nixos/configuration.nix
|
|
||||||
{ ... }:
|
{ ... }:
|
||||||
{
|
{
|
||||||
users.users.harald.extraGroups = [ "input" ];
|
metacfg.services.xremap = {
|
||||||
|
enable = true;
|
||||||
# Enable the xremap service
|
deviceNames = [
|
||||||
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"
|
"Hangsheng MonsGeek Keyboard"
|
||||||
"HS Galaxy100 Keyboard"
|
"HS Galaxy100 Keyboard"
|
||||||
# You can usually shorten the name slightly to match the device you want
|
|
||||||
];
|
];
|
||||||
|
config = {
|
||||||
# Define your remapping configuration using Nix's attribute set format
|
|
||||||
services.xremap.config = {
|
|
||||||
keymap = [
|
keymap = [
|
||||||
{
|
{
|
||||||
remap = {
|
remap = {
|
||||||
# Map Alt+C (LeftAlt-C) to Ctrl+C (LeftControl-C)
|
|
||||||
LeftAlt-C = "COPY";
|
LeftAlt-C = "COPY";
|
||||||
LeftAlt-V = "PASTE";
|
LeftAlt-V = "PASTE";
|
||||||
LeftAlt-X = "CUT";
|
LeftAlt-X = "CUT";
|
||||||
|
|
@ -30,4 +17,5 @@
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue