Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b42e808d3 | |||
| f25aab2441 | |||
| 9342933987 | |||
| b35373b0ec | |||
| 538d7623be | |||
| 1f61a0d1ec | |||
| 77cf4a0aed | |||
| 33937ab115 | |||
| d5967cf392 | |||
| b1370b5fc6 | |||
| 8404f0998b | |||
| bc6091f63f | |||
| eb10ad018f | |||
| 0523639f2a | |||
| 4622c52d5b |
28 changed files with 1155 additions and 268 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": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1763638478,
|
||||
"narHash": "sha256-n/IMowE9S23ovmTkKX7KhxXC2Yq41EAVFR2FBIXPcT8=",
|
||||
"lastModified": 1769363988,
|
||||
"narHash": "sha256-BiGPeulrDVetXP+tjxhMcGLUROZAtZIhU5m4MqawCfM=",
|
||||
"owner": "Homebrew",
|
||||
"repo": "brew",
|
||||
"rev": "fbfdbaba008189499958a7aeb1e2c36ab10c067d",
|
||||
"rev": "d01011cac6d72032c75fd2cd9489909e95d9faf2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "Homebrew",
|
||||
"ref": "5.0.3",
|
||||
"ref": "5.0.12",
|
||||
"repo": "brew",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
@ -134,11 +134,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768923567,
|
||||
"narHash": "sha256-GVJ0jKsyXLuBzRMXCDY6D5J8wVdwP1DuQmmvYL/Vw/Q=",
|
||||
"lastModified": 1769524058,
|
||||
"narHash": "sha256-zygdD6X1PcVNR2PsyK4ptzrVEiAdbMqLos7utrMDEWE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "disko",
|
||||
"rev": "00395d188e3594a1507f214a2f15d4ce5c07cb28",
|
||||
"rev": "71a3fc97d80881e91710fe721f1158d3b96ae14d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -421,11 +421,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768949235,
|
||||
"narHash": "sha256-TtjKgXyg1lMfh374w5uxutd6Vx2P/hU81aEhTxrO2cg=",
|
||||
"lastModified": 1769580047,
|
||||
"narHash": "sha256-tNqCP/+2+peAXXQ2V8RwsBkenlfWMERb+Uy6xmevyhM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "75ed713570ca17427119e7e204ab3590cc3bf2a5",
|
||||
"rev": "366d78c2856de6ab3411c15c1cb4fb4c2bf5c826",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -454,11 +454,11 @@
|
|||
"homebrew-cask": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1769077283,
|
||||
"narHash": "sha256-alvFQmhX8POHxBP3/jResx6AJ06X+k6SF4/CiNndpPA=",
|
||||
"lastModified": 1770127519,
|
||||
"narHash": "sha256-wIpVsLhx1gaB2JYfpVipt9ZLAReKFO0kmVIOhieHfqs=",
|
||||
"owner": "homebrew",
|
||||
"repo": "homebrew-cask",
|
||||
"rev": "4a8185e145fa4fc8326705c666d608c3ee761612",
|
||||
"rev": "76e6c1bda247fe48dc30683203cce2b28b5d6eee",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -470,11 +470,11 @@
|
|||
"homebrew-core": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1769077518,
|
||||
"narHash": "sha256-QtWC5CcY9xzfjcThSwZgise9RXbM2vZmw+Tot67RiJo=",
|
||||
"lastModified": 1770130704,
|
||||
"narHash": "sha256-95Jwssj3WbBwHO4nNB5uVIgIym/fuSDBb5vs6eKdgp0=",
|
||||
"owner": "homebrew",
|
||||
"repo": "homebrew-core",
|
||||
"rev": "2ac083c750fa2a6999ad05a7352e8edbd7abd969",
|
||||
"rev": "5369d45006ea107dead79ef8ef4b29b7c972f276",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -515,11 +515,11 @@
|
|||
},
|
||||
"mnw": {
|
||||
"locked": {
|
||||
"lastModified": 1768701608,
|
||||
"narHash": "sha256-kSvWF3Xt2HW9hmV5V7i8PqeWJIBUKmuKoHhOgj3Znzs=",
|
||||
"lastModified": 1769981889,
|
||||
"narHash": "sha256-ndI7AxL/6auelkLHngdUGVImBiHkG8w2N2fOTKZKn4k=",
|
||||
"owner": "Gerg-L",
|
||||
"repo": "mnw",
|
||||
"rev": "20d63a8a1ae400557c770052a46a9840e768926b",
|
||||
"rev": "332fed8f43b77149c582f1782683d6aeee1f07cf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -562,11 +562,11 @@
|
|||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768906339,
|
||||
"narHash": "sha256-iwkHIz2IYRcELkBoKXQUHlP0bFGmrHIz/roJUVYsyx8=",
|
||||
"lastModified": 1770130359,
|
||||
"narHash": "sha256-IfoT9oaeIE6XjXprMORG2qZFzGGZ0v6wJcOlQRdlpvY=",
|
||||
"owner": "NotAShelf",
|
||||
"repo": "nvf",
|
||||
"rev": "18c55d3bebf2c704970b4ea6fd0261808bec8d94",
|
||||
"rev": "92854bd0eaaa06914afba345741c372439b8e335",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -580,11 +580,11 @@
|
|||
"brew-src": "brew-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1764473698,
|
||||
"narHash": "sha256-C91gPgv6udN5WuIZWNehp8qdLqlrzX6iF/YyboOj6XI=",
|
||||
"lastModified": 1769437432,
|
||||
"narHash": "sha256-8d7KnCpT2LweRvSzZYEGd9IM3eFX+A78opcnDM0+ndk=",
|
||||
"owner": "zhaofengli-wip",
|
||||
"repo": "nix-homebrew",
|
||||
"rev": "6a8ab60bfd66154feeaa1021fc3b32684814a62a",
|
||||
"rev": "a5409abd0d5013d79775d3419bcac10eacb9d8c5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -595,11 +595,11 @@
|
|||
},
|
||||
"nixos-hardware": {
|
||||
"locked": {
|
||||
"lastModified": 1768736227,
|
||||
"narHash": "sha256-qgGq7CfrYKc3IBYQ7qp0Z/ZXndQVC5Bj0N8HW9mS2rM=",
|
||||
"lastModified": 1769302137,
|
||||
"narHash": "sha256-QEDtctEkOsbx8nlFh4yqPEOtr4tif6KTqWwJ37IM2ds=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "d447553bcbc6a178618d37e61648b19e744370df",
|
||||
"rev": "a351494b0e35fd7c0b7a1aae82f0afddf4907aa8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -642,11 +642,11 @@
|
|||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1768940263,
|
||||
"narHash": "sha256-sJERJIYTKPFXkoz/gBaBtRKke82h4DkX3BBSsKbfbvI=",
|
||||
"lastModified": 1770056022,
|
||||
"narHash": "sha256-yvCz+Qmci1bVucXEyac3TdoSPMtjqVJmVy5wro6j/70=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3ceaaa8bc963ced4d830e06ea2d0863b6490ff03",
|
||||
"rev": "d04d8548aed39902419f14a8537006426dc1e4fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -748,11 +748,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769050281,
|
||||
"narHash": "sha256-1H8DN4UZgEUqPUA5ecHOufLZMscJ4IlcGaEftaPtpBY=",
|
||||
"lastModified": 1770088046,
|
||||
"narHash": "sha256-4hfYDnUTvL1qSSZEA4CEThxfz+KlwSFQ30Z9jgDguO0=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "6deef0585c52d9e70f96b6121207e1496d4b0c49",
|
||||
"rev": "71f9daa4e05e49c434d08627e755495ae222bc34",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -835,11 +835,11 @@
|
|||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768863606,
|
||||
"narHash": "sha256-1IHAeS8WtBiEo5XiyJBHOXMzECD6aaIOJmpQKzRRl64=",
|
||||
"lastModified": 1770110318,
|
||||
"narHash": "sha256-NUVGVtYBTC96WhPh4Y3SVM7vf0o1z5W4uqRBn9v1pfo=",
|
||||
"owner": "Mic92",
|
||||
"repo": "sops-nix",
|
||||
"rev": "c7067be8db2c09ab1884de67ef6c4f693973f4a2",
|
||||
"rev": "f990b0a334e96d3ef9ca09d4bd92778b42fd84f9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -857,11 +857,11 @@
|
|||
"rust-overlay": "rust-overlay_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1768997903,
|
||||
"narHash": "sha256-UpBfh3I4PhykVHqV74rrxufF3X1Z8z8sx/lFgMFfIP8=",
|
||||
"lastModified": 1769829418,
|
||||
"narHash": "sha256-ALZKPUa0eHP6HwETAJ9PsAnYQjNLF6eEpo1W2fmYqwA=",
|
||||
"owner": "haraldh",
|
||||
"repo": "ssh-tresor",
|
||||
"rev": "dd45aed45f8d9b8729b7698ef43e7cc32fab97b6",
|
||||
"rev": "2e1bfa29bd5ad5a60c3e0effd69851a67d455781",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -932,11 +932,11 @@
|
|||
},
|
||||
"unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1768886240,
|
||||
"narHash": "sha256-C2TjvwYZ2VDxYWeqvvJ5XPPp6U7H66zeJlRaErJKoEM=",
|
||||
"lastModified": 1770115704,
|
||||
"narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "80e4adbcf8992d3fd27ad4964fbb84907f9478b0",
|
||||
"rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -949,16 +949,16 @@
|
|||
"xremap": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1766606475,
|
||||
"narHash": "sha256-FPZ4iQA/vVZGzbO8i8lTK8i9A3zs9BLqMvTMeAVv9rQ=",
|
||||
"lastModified": 1769021727,
|
||||
"narHash": "sha256-2wylBk3+Zu1pHa41dhKwvUtxOVyHSMRDfOD9fIp8x2I=",
|
||||
"owner": "k0kubun",
|
||||
"repo": "xremap",
|
||||
"rev": "cdc744d873c19899ef21f329c4305b4b5e53d459",
|
||||
"rev": "890e0a6ca92e90f3bcbd1e235abcf2192e233a46",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "k0kubun",
|
||||
"ref": "v0.14.8",
|
||||
"ref": "v0.14.10",
|
||||
"repo": "xremap",
|
||||
"type": "github"
|
||||
}
|
||||
|
|
@ -971,11 +971,11 @@
|
|||
"xremap": "xremap"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1767318478,
|
||||
"narHash": "sha256-h3oE50RedA8DRGrFU+Hv2kirt4rmzdaC9oSD+MSg9Ms=",
|
||||
"lastModified": 1769636170,
|
||||
"narHash": "sha256-X000Dgg053Dv9NIzm1b9QYSAHYtW2jHMVALQezui7L0=",
|
||||
"owner": "xremap",
|
||||
"repo": "nix-flake",
|
||||
"rev": "9a2224aa01a3c86e94b398c33329c8ff6496dc5d",
|
||||
"rev": "00bc6dd4275d4b003a17ef7f5f271ba87f73d698",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
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";
|
||||
};
|
||||
}
|
||||
|
|
@ -9,7 +9,13 @@ with lib.metacfg;
|
|||
services.spice-autorandr.enable = true;
|
||||
services.spice-vdagentd.enable = true;
|
||||
|
||||
services.resolved.enable = true;
|
||||
services.resolved.extraConfig = ''
|
||||
ResolveUnicastSingleLabel=yes
|
||||
'';
|
||||
|
||||
metacfg = {
|
||||
system.noSleep.enable = true;
|
||||
base.enable = true;
|
||||
gui.enable = true;
|
||||
nix-ld.enable = true;
|
||||
|
|
@ -34,13 +40,6 @@ with lib.metacfg;
|
|||
];
|
||||
};
|
||||
|
||||
# Disable the GNOME3/GDM auto-suspend feature that cannot be disabled in GUI!
|
||||
# If no user is logged in, the machine will power down after 20 minutes.
|
||||
systemd.targets.sleep.enable = false;
|
||||
systemd.targets.suspend.enable = false;
|
||||
systemd.targets.hibernate.enable = false;
|
||||
systemd.targets.hybrid-sleep.enable = false;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
azure-cli
|
||||
desktop-file-utils
|
||||
|
|
@ -60,16 +59,11 @@ with lib.metacfg;
|
|||
|
||||
services.ratbagd.enable = true;
|
||||
|
||||
services.resolved.enable = true;
|
||||
#services.resolved.dnssec = "allow-downgrade";
|
||||
services.resolved.extraConfig = ''
|
||||
ResolveUnicastSingleLabel=yes
|
||||
'';
|
||||
|
||||
virtualisation = {
|
||||
docker.enable = true;
|
||||
podman.dockerCompat = false;
|
||||
libvirtd.enable = false;
|
||||
rosetta.enable = true;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
|
|
@ -78,7 +72,5 @@ with lib.metacfg;
|
|||
allowReboot = false;
|
||||
};
|
||||
|
||||
virtualisation.rosetta.enable = true;
|
||||
|
||||
system.stateVersion = "25.05";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,13 @@ with lib.metacfg;
|
|||
services.spice-autorandr.enable = true;
|
||||
services.spice-vdagentd.enable = true;
|
||||
|
||||
services.resolved.enable = true;
|
||||
services.resolved.extraConfig = ''
|
||||
ResolveUnicastSingleLabel=yes
|
||||
'';
|
||||
|
||||
metacfg = {
|
||||
system.noSleep.enable = true;
|
||||
base.enable = true;
|
||||
gui.enable = true;
|
||||
nix-ld.enable = true;
|
||||
|
|
@ -34,13 +40,6 @@ with lib.metacfg;
|
|||
];
|
||||
};
|
||||
|
||||
# Disable the GNOME3/GDM auto-suspend feature that cannot be disabled in GUI!
|
||||
# If no user is logged in, the machine will power down after 20 minutes.
|
||||
systemd.targets.sleep.enable = false;
|
||||
systemd.targets.suspend.enable = false;
|
||||
systemd.targets.hibernate.enable = false;
|
||||
systemd.targets.hybrid-sleep.enable = false;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
azure-cli
|
||||
desktop-file-utils
|
||||
|
|
@ -60,16 +59,11 @@ with lib.metacfg;
|
|||
|
||||
services.ratbagd.enable = true;
|
||||
|
||||
services.resolved.enable = true;
|
||||
#services.resolved.dnssec = "allow-downgrade";
|
||||
services.resolved.extraConfig = ''
|
||||
ResolveUnicastSingleLabel=yes
|
||||
'';
|
||||
|
||||
virtualisation = {
|
||||
docker.enable = true;
|
||||
podman.dockerCompat = false;
|
||||
libvirtd.enable = false;
|
||||
rosetta.enable = true;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
|
|
@ -78,7 +72,5 @@ with lib.metacfg;
|
|||
allowReboot = false;
|
||||
};
|
||||
|
||||
virtualisation.rosetta.enable = true;
|
||||
|
||||
system.stateVersion = "25.05";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,21 +18,17 @@ with lib.metacfg;
|
|||
22000
|
||||
];
|
||||
|
||||
services.tailscale.enable = true;
|
||||
|
||||
services.cratedocs-mcp.enable = true;
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
hardware.bluetooth.input.General.ClassicBondedOnly = false;
|
||||
services.udev.extraRules = ''
|
||||
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e4c5", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
|
||||
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e489", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
|
||||
'';
|
||||
services.tailscale.enable = true;
|
||||
services.resolved.enable = true;
|
||||
|
||||
metacfg = {
|
||||
hardware.wooting.enable = true;
|
||||
base.enable = true;
|
||||
gui.enable = true;
|
||||
nix-ld.enable = true;
|
||||
|
|
@ -59,15 +55,21 @@ with lib.metacfg;
|
|||
"dialout"
|
||||
"tss"
|
||||
];
|
||||
system.kernelTweaks.enable = true;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
operation = "boot";
|
||||
allowReboot = false;
|
||||
};
|
||||
|
||||
nixpkgs.config.permittedInsecurePackages = [
|
||||
"electron-27.3.11"
|
||||
];
|
||||
|
||||
# Kernel tuning
|
||||
# Additional kernel tuning beyond the module defaults
|
||||
boot.kernel.sysctl = {
|
||||
"power.pm_freeze_timeout" = 30000;
|
||||
# Reduce swap usage (you have zram)
|
||||
"vm.swappiness" = 10;
|
||||
# Prefer keeping directory/inode caches
|
||||
|
|
@ -101,6 +103,7 @@ with lib.metacfg;
|
|||
kubectl
|
||||
kubectx
|
||||
logseq
|
||||
nvtopPackages.amd
|
||||
obsidian
|
||||
piper-tts
|
||||
tipp10
|
||||
|
|
@ -111,32 +114,18 @@ with lib.metacfg;
|
|||
|
||||
# zram swap with zstd compression for better performance
|
||||
zramSwap = {
|
||||
enable = true;
|
||||
algorithm = "zstd";
|
||||
memoryPercent = 50;
|
||||
};
|
||||
|
||||
services.ratbagd.enable = true;
|
||||
|
||||
services.resolved.enable = true;
|
||||
|
||||
#services.resolved.dnssec = "allow-downgrade";
|
||||
#services.resolved.extraConfig = ''
|
||||
# ResolveUnicastSingleLabel=yes
|
||||
#'';
|
||||
|
||||
virtualisation = {
|
||||
libvirtd.enable = true;
|
||||
docker.enable = true;
|
||||
podman.dockerCompat = false;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
operation = "boot";
|
||||
allowReboot = false;
|
||||
};
|
||||
|
||||
services.trezord.enable = true;
|
||||
|
||||
services.ollama = {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,15 @@
|
|||
# In /etc/nixos/configuration.nix
|
||||
{ ... }:
|
||||
{
|
||||
users.users.harald.extraGroups = [ "input" ];
|
||||
|
||||
# Enable the xremap service
|
||||
services.xremap.enable = true;
|
||||
services.xremap.userName = "harald"; # Replace with your username
|
||||
services.xremap.serviceMode = "user"; # Run as user service, not system-wide
|
||||
services.xremap.withGnome = true;
|
||||
|
||||
# Add a specific configuration block to select your keyboard(s) by name
|
||||
services.xremap.deviceNames = [
|
||||
# Use the name found in the log output: "Hangsheng MonsGeek Keyboard System Control"
|
||||
metacfg.services.xremap = {
|
||||
enable = true;
|
||||
deviceNames = [
|
||||
"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 = {
|
||||
config = {
|
||||
keymap = [
|
||||
{
|
||||
remap = {
|
||||
# Map Alt+C (LeftAlt-C) to Ctrl+C (LeftControl-C)
|
||||
LeftAlt-C = "COPY";
|
||||
LeftAlt-V = "PASTE";
|
||||
LeftAlt-X = "CUT";
|
||||
|
|
@ -30,4 +17,5 @@
|
|||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
|
@ -9,14 +7,9 @@
|
|||
sopsFile = ../../../.secrets/hetzner/internetbs.yaml; # bring your own password file
|
||||
};
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults = {
|
||||
email = "harald@hoyer.xyz";
|
||||
dnsProvider = "cloudflare";
|
||||
credentialsFile = config.sops.secrets.internetbs.path;
|
||||
};
|
||||
certs = {
|
||||
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
|
||||
|
||||
security.acme.certs = {
|
||||
"surfsite.org" = {
|
||||
extraDomainNames = [ "*.surfsite.org" ];
|
||||
};
|
||||
|
|
@ -71,5 +64,4 @@
|
|||
extraDomainNames = [ "*.harald-hoyer.de" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
./mailserver.nix
|
||||
./network.nix
|
||||
./nextcloud.nix
|
||||
./nextcloud-claude-bot
|
||||
./nginx.nix
|
||||
./postgresql.nix
|
||||
./rspamd.nix
|
||||
|
|
@ -22,6 +23,8 @@
|
|||
services.tailscale.enable = true;
|
||||
|
||||
metacfg = {
|
||||
services.nginxBase.enable = true;
|
||||
services.acmeBase.enable = true;
|
||||
emailOnFailure.enable = true;
|
||||
base.enable = true;
|
||||
nix.enable = true;
|
||||
|
|
@ -42,7 +45,6 @@
|
|||
dates = "04:00";
|
||||
operation = "switch";
|
||||
allowReboot = true;
|
||||
# flake = lib.mkForce "git+file:///var/lib/gitea/repositories/harald/nixcfg.git#mx";
|
||||
flake = lib.mkForce "/root/nixcfg/.#mx";
|
||||
};
|
||||
|
||||
|
|
|
|||
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 = {
|
||||
enable = true;
|
||||
clientMaxBodySize = "1000M";
|
||||
appendHttpConfig = ''
|
||||
log_format vcombined '$host:$server_port '
|
||||
'$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent"';
|
||||
access_log /var/log/nginx/access.log vcombined;
|
||||
'';
|
||||
recommendedGzipSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedProxySettings = true;
|
||||
recommendedTlsSettings = true;
|
||||
virtualHosts = {
|
||||
services.nginx.virtualHosts = {
|
||||
"00000" = {
|
||||
useACMEHost = "hoyer.xyz";
|
||||
serverName = "_";
|
||||
|
|
@ -157,5 +142,4 @@
|
|||
forceSSL = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@
|
|||
{
|
||||
imports = [ ./hardware-configuration.nix ];
|
||||
|
||||
services.tailscale.enable = true;
|
||||
|
||||
boot.kernelPackages = lib.mkOverride 0 pkgs.linuxPackages_latest;
|
||||
boot.loader.systemd-boot.enable = false;
|
||||
# Bootloader.
|
||||
|
|
@ -18,6 +16,8 @@
|
|||
security.tpm2.enable = false;
|
||||
security.tpm2.abrmd.enable = false;
|
||||
|
||||
services.tailscale.enable = true;
|
||||
|
||||
metacfg = {
|
||||
base.enable = true;
|
||||
nix-ld.enable = true;
|
||||
|
|
@ -37,12 +37,6 @@
|
|||
podman.dockerCompat = false;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
operation = "switch";
|
||||
allowReboot = true;
|
||||
};
|
||||
|
||||
networking.wireless.enable = false; # Enables wireless support via wpa_supplicant.
|
||||
|
||||
networking.firewall.allowPing = true;
|
||||
|
|
@ -66,5 +60,11 @@
|
|||
}
|
||||
];
|
||||
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
operation = "switch";
|
||||
allowReboot = true;
|
||||
};
|
||||
|
||||
system.stateVersion = "25.05";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
with lib;
|
||||
|
|
@ -17,17 +15,17 @@ with lib.metacfg;
|
|||
nix.enable = true;
|
||||
};
|
||||
|
||||
virtualisation = {
|
||||
docker.enable = true;
|
||||
podman.dockerCompat = false;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
operation = "switch";
|
||||
allowReboot = true;
|
||||
};
|
||||
|
||||
virtualisation = {
|
||||
docker.enable = true;
|
||||
podman.dockerCompat = false;
|
||||
};
|
||||
|
||||
security.tpm2.enable = false;
|
||||
security.tpm2.abrmd.enable = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,14 +7,9 @@
|
|||
sopsFile = ../../../.secrets/sgx/internetbs.yaml; # bring your own password file
|
||||
};
|
||||
|
||||
security.acme = {
|
||||
acceptTerms = true;
|
||||
defaults = {
|
||||
email = "harald@hoyer.xyz";
|
||||
dnsProvider = "cloudflare";
|
||||
credentialsFile = config.sops.secrets.internetbs.path;
|
||||
};
|
||||
certs = {
|
||||
metacfg.services.acmeBase.credentialsFile = config.sops.secrets.internetbs.path;
|
||||
|
||||
security.acme.certs = {
|
||||
"internal.hoyer.world" = {
|
||||
extraDomainNames = [
|
||||
"openwebui.hoyer.world"
|
||||
|
|
@ -23,5 +18,4 @@
|
|||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@
|
|||
./wyoming.nix
|
||||
];
|
||||
|
||||
services.tailscale.enable = true;
|
||||
|
||||
boot.tmp.useTmpfs = false;
|
||||
|
||||
sops.secrets.pccs.sopsFile = ../../../.secrets/sgx/pccs.yaml;
|
||||
|
|
@ -23,7 +21,16 @@
|
|||
claude-code
|
||||
];
|
||||
|
||||
services.tailscale.enable = true;
|
||||
|
||||
metacfg = {
|
||||
services.nginxBase.enable = true;
|
||||
services.acmeBase.enable = true;
|
||||
system.noSleep = {
|
||||
enable = true;
|
||||
disableGdmAutoSuspend = true;
|
||||
ignoreLidSwitch = true;
|
||||
};
|
||||
emailOnFailure.enable = true;
|
||||
base.enable = true;
|
||||
gui.enable = true;
|
||||
|
|
@ -58,13 +65,5 @@
|
|||
allowReboot = true;
|
||||
};
|
||||
|
||||
systemd.targets.sleep.enable = false;
|
||||
systemd.targets.suspend.enable = false;
|
||||
systemd.targets.hibernate.enable = false;
|
||||
systemd.targets.hybrid-sleep.enable = false;
|
||||
services.displayManager.gdm.autoSuspend = false;
|
||||
|
||||
services.logind.settings.Login.HandleLidSwitch = "ignore";
|
||||
|
||||
system.stateVersion = "23.11";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,7 @@
|
|||
...
|
||||
}:
|
||||
{
|
||||
users.users.nginx.extraGroups = [ "acme" ];
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
clientMaxBodySize = "1000M";
|
||||
appendHttpConfig = ''
|
||||
log_format vcombined '$host:$server_port '
|
||||
'$remote_addr - $remote_user [$time_local] '
|
||||
'"$request" $status $body_bytes_sent '
|
||||
'"$http_referer" "$http_user_agent"';
|
||||
access_log /var/log/nginx/access.log vcombined;
|
||||
'';
|
||||
recommendedGzipSettings = true;
|
||||
recommendedOptimisation = true;
|
||||
recommendedProxySettings = true;
|
||||
recommendedTlsSettings = true;
|
||||
virtualHosts = {
|
||||
services.nginx.virtualHosts = {
|
||||
"openwebui.hoyer.world" = {
|
||||
enableACME = false;
|
||||
useACMEHost = "internal.hoyer.world";
|
||||
|
|
@ -48,5 +33,4 @@
|
|||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
{
|
||||
imports = [ ./hardware-configuration.nix ];
|
||||
|
||||
services.resolved.enable = true;
|
||||
|
||||
metacfg = {
|
||||
base.enable = true;
|
||||
gui.enable = true;
|
||||
|
|
@ -27,9 +29,6 @@
|
|||
|
||||
system.stateVersion = "23.11";
|
||||
|
||||
services.resolved.enable = true;
|
||||
#services.resolved.dnssec = "allow-downgrade";
|
||||
|
||||
sops.age.sshKeyPaths = [ "/persist/ssh/ssh_host_ed25519_key" ];
|
||||
sops.secrets.backup-s3.sopsFile = ../../../.secrets/t15/backup-s3.yaml;
|
||||
sops.secrets.backup-pw.sopsFile = ../../../.secrets/t15/backup-s3.yaml;
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ with lib.metacfg;
|
|||
programs.ccache.enable = true;
|
||||
nix.settings.extra-sandbox-paths = [ config.programs.ccache.cacheDir ];
|
||||
|
||||
services.tailscale.enable = true;
|
||||
|
||||
services.cratedocs-mcp.enable = true;
|
||||
|
||||
sops.age.sshKeyPaths = [ "/var/lib/secrets/ssh_host_ed25519_key" ];
|
||||
|
|
@ -45,13 +43,11 @@ with lib.metacfg;
|
|||
];
|
||||
};
|
||||
|
||||
hardware.bluetooth.input.General.ClassicBondedOnly = false;
|
||||
services.udev.extraRules = ''
|
||||
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e4c5", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
|
||||
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="342d", ATTRS{idProduct}=="e489", MODE="0660", GROUP="users", TAG+="uaccess", TAG+="udev-acl"
|
||||
'';
|
||||
services.tailscale.enable = true;
|
||||
services.resolved.enable = true;
|
||||
|
||||
metacfg = {
|
||||
hardware.wooting.enable = true;
|
||||
base.enable = true;
|
||||
gui.enable = true;
|
||||
nix-ld.enable = true;
|
||||
|
|
@ -77,17 +73,19 @@ with lib.metacfg;
|
|||
"dialout"
|
||||
"tss"
|
||||
];
|
||||
system.kernelTweaks.enable = true;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
operation = "boot";
|
||||
allowReboot = false;
|
||||
};
|
||||
|
||||
nixpkgs.config.permittedInsecurePackages = [
|
||||
"electron-27.3.11"
|
||||
];
|
||||
|
||||
# increase freezing timeout
|
||||
boot.kernel.sysctl = {
|
||||
"power.pm_freeze_timeout" = 30000;
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
attic-client
|
||||
azure-cli
|
||||
|
|
@ -112,26 +110,12 @@ with lib.metacfg;
|
|||
vscode
|
||||
];
|
||||
|
||||
zramSwap.enable = true;
|
||||
|
||||
services.ratbagd.enable = true;
|
||||
|
||||
services.resolved.enable = true;
|
||||
#services.resolved.dnssec = "allow-downgrade";
|
||||
#services.resolved.extraConfig = ''
|
||||
# ResolveUnicastSingleLabel=yes
|
||||
#'';
|
||||
|
||||
virtualisation = {
|
||||
libvirtd.enable = true;
|
||||
};
|
||||
|
||||
system.autoUpgrade = {
|
||||
enable = true;
|
||||
operation = "boot";
|
||||
allowReboot = false;
|
||||
};
|
||||
|
||||
services.trezord.enable = true;
|
||||
|
||||
services.ollama = {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,15 @@
|
|||
# In /etc/nixos/configuration.nix
|
||||
{ ... }:
|
||||
{
|
||||
users.users.harald.extraGroups = [ "input" ];
|
||||
|
||||
# Enable the xremap service
|
||||
services.xremap.enable = true;
|
||||
services.xremap.userName = "harald"; # Replace with your username
|
||||
services.xremap.serviceMode = "user"; # Run as user service, not system-wide
|
||||
services.xremap.withGnome = true;
|
||||
|
||||
# Add a specific configuration block to select your keyboard(s) by name
|
||||
services.xremap.deviceNames = [
|
||||
# Use the name found in the log output: "Hangsheng MonsGeek Keyboard System Control"
|
||||
metacfg.services.xremap = {
|
||||
enable = true;
|
||||
deviceNames = [
|
||||
"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 = {
|
||||
config = {
|
||||
keymap = [
|
||||
{
|
||||
remap = {
|
||||
# Map Alt+C (LeftAlt-C) to Ctrl+C (LeftControl-C)
|
||||
LeftAlt-C = "COPY";
|
||||
LeftAlt-V = "PASTE";
|
||||
LeftAlt-X = "CUT";
|
||||
|
|
@ -30,4 +17,5 @@
|
|||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue