Compare commits

...
Sign in to create a new pull request.

20 commits
1 ... main

Author SHA1 Message Date
6597561ec4 feat(nix): add uwsgi configuration for search.hoyer.world
- Introduced `uwsgiConfig` section in the Searx configuration for `search.hoyer.world`.
- Configures uWSGI to listen on port `8081` for HTTP requests.
2026-02-06 12:55:36 +01:00
437cb4b606 feat(nginx): add new search.hoyer.world virtual host
- Introduced Nginx configuration for the `search.hoyer.world` domain.
- Ensures SSL enforcement and proper proxy settings for the new subdomain.
2026-02-06 12:51:24 +01:00
14c9a4f084 feat(nix): add new domain to ACME certificate
- Added `search.hoyer.world` to the `extraDomainNames` list for the `internal.hoyer.world` ACME certificate.
- Ensures proper SSL configuration for the new subdomain.
2026-02-06 11:43:37 +01:00
958175fb01 feat(nix): enable Searx and configure Nginx for domain
- Added Searx service with Nginx configuration for the domain `search.hoyer.world`.
2026-02-06 11:40:10 +01:00
4bc62866a8 feat(nix): update claude-code package and dependencies
- Upgraded `claude-code` to version 2.1.34 and updated associated npm dependencies and hash values.
- Refactored `update.sh` to use `nix shell` instead of `nix-shell` for improved compatibility.
- Added musl-based `sharp` dependencies and adjusted sandbox requirements in `package.nix`.
2026-02-06 08:57:49 +01:00
9b42e808d3 feat(bot): refactor system prompt and enhance CLI command
- Replaced `DEFAULT_SYSTEM_PROMPT` with `BOT_SYSTEM_PROMPT` for clarity and modularity.
- Introduced a `build_system_prompt` function to dynamically compose prompts.
- Enhanced `call_claude` CLI with new tool options and appendable prompts.
2026-02-04 09:11:08 +01:00
f25aab2441 feat(bot): improve prompt-building and help command handling
- Added a default system prompt and adjusted the structure to use XML for clarity.
- Improved help command handling by simplifying triggers and updating responses.
- Enhanced NixOS configuration with support for optional custom instructions.
2026-02-03 17:39:31 +01:00
9342933987 feat(bot): switch to in-memory conversation history
- Replaced Nextcloud chat history fetching with in-memory storage for conversation history.
- Added limits to history length based on an environment variable (`CONTEXT_MESSAGES`).
- Simplified prompt-building logic by removing async history fetching.
2026-02-03 17:26:46 +01:00
b35373b0ec feat(bot): replace maxTokens with contextMessages option
- Switched `maxTokens` to `contextMessages` to set chat history length instead of token limit.
- Updated environment variables, NixOS module, and prompt building logic for consistency.
- Removed in-memory conversation history, now fetching from Nextcloud for better scalability.
2026-02-03 17:00:50 +01:00
538d7623be refactor(bot): remove unused max-tokens argument handling
- Simplified the `call_claude` function by removing the unused `MAX_TOKENS` argument handling.
- Ensures cleaner and more maintainable command construction.
2026-02-03 16:30:47 +01:00
1f61a0d1ec feat(bot): refactor webhook parsing for better structure
- Updated webhook parsing to align with the latest Nextcloud Talk Bot format.
- Improved handling of actor, message, and conversation data for clarity and flexibility.
- Added robust JSON decoding with fallback for content extraction.
2026-02-03 16:29:10 +01:00
77cf4a0aed feat(bot): support random token in signature verification
- Enhanced signature verification by adding support for a `random` token included in webhook headers.
- Introduced logging to display signature variants for debugging purposes.
- Improved webhook handling to process new `X-Nextcloud-Talk-Random` header.
2026-02-03 16:26:37 +01:00
33937ab115 feat(bot): add signature verification logging
- Added info-level logging to provide details about signature verification, including secret length and partial hashes for expected and received signatures.
- Helps in debugging signature mismatches without exposing full sensitive data.
2026-02-03 16:23:14 +01:00
d5967cf392 feat(nix): improve Nextcloud Claude Bot security and user setup
- Set `User` and `Group` for the bot service to enhance security and isolation.
- Added system user and group for `claude-bot` with defined home directory.
- Modified secrets ownership to align with the new bot user.
2026-02-03 16:14:21 +01:00
b1370b5fc6 feat(bot): enhance group chat handling and mention detection
- Updated bot to only respond in group chats when explicitly mentioned.
- Added mention detection using regex for "Claude" patterns and cleaned up the message text for processing.
- Improved help message to clarify usage in direct messages and group chats.
2026-02-03 16:09:25 +01:00
8404f0998b refactor(nix): simplify Nextcloud Claude Bot packaging
- Replaced `buildPythonApplication` with `python3.withPackages` for a cleaner and more concise implementation.
- Adjusted service configuration to use the updated packaging structure, ensuring compatibility with the new setup.
- Simplifies the NixOS module by reducing redundancy and improving maintainability.
2026-02-03 15:54:01 +01:00
bc6091f63f feat(nix): add Nextcloud Claude Bot integration
- Added configuration for Nextcloud Claude Bot, including NixOS module, secrets management, and example setup files.
- Introduced a Python-based HTTP server for handling webhook events and interacting with Nextcloud Talk.
- Integrated necessary dependencies and systemd service for seamless operation.
2026-02-03 15:42:56 +01:00
eb10ad018f chore(nix): update flake.lock
- Updated flake.lock to incorporate the latest revisions for locked dependencies.
- Includes updates for `homebrew`, `sops-nix`, `nixos-hardware`, `rust-overlay`, and more.
- Ensures the system remains aligned with the most recent upstream changes.
2026-01-30 11:53:03 +01:00
0523639f2a feat(nix): add nvtop package to amd system
- Added `nvtopPackages.amd` to the package list for better GPU monitoring on AMD systems.
- Enhances system configuration by enabling real-time visualization of GPU usage.
2026-01-30 11:52:59 +01:00
4622c52d5b refactor(nix): extract common system configs into reusable modules
Create 6 new NixOS modules to reduce duplication across system configs:
- hardware/wooting: Wooting keyboard udev rules and Bluetooth compat
- services/nginx-base: Common nginx server settings
- services/acme-base: ACME certificate defaults
- services/xremap: Key remapping with sensible defaults
- system/no-sleep: Disable sleep/suspend/hibernate targets
- system/kernel-tweaks: PM freeze timeout and zram configuration

Update system configuration files to use these new modules.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 10:42:09 +01:00
32 changed files with 1324 additions and 311 deletions

View 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
View file

@ -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": {

View 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"
'';
};
}

View 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;
};
};
};
}

View 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;
'';
};
};
}

View 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" ];
};
}

View 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;
};
}

View 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";
};
}

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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 { }; };

View file

@ -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";
} }

View file

@ -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";
} }

View file

@ -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 = {

View file

@ -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 @@
} }
]; ];
}; };
};
} }

View file

@ -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" ];
}; };
}; };
};
} }

View file

@ -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";
}; };

View 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

View 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)

View 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;
'';
};
}

View file

@ -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;
'';
};
};
}

View 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}/";
# };
};
}

View file

@ -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;
}; };
}; };
};
} }

View file

@ -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";
} }

View file

@ -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;

View file

@ -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"
]; ];
}; };
}; };
};
} }

View file

@ -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";
} }

View file

@ -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;
}; };
}; };
} }

View file

@ -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;

View file

@ -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 = {

View file

@ -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 @@
} }
]; ];
}; };
};
} }