diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 1ed6ffd..9041a2b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,32 @@ "mcp__web-reader__webReader", "Bash(python3 -c \"import json; json.load\\(open\\('modules/openclaw-config.json'\\)\\); print\\('JSON is valid'\\)\")", "mcp__zread__search_doc", - "mcp__zread__read_file" + "mcp__zread__read_file", + "Bash(sudo podman:*)", + "Bash(curl:*)", + "Bash(sudo ls:*)", + "Bash(sudo systemctl:*)", + "Bash(sudo mkdir:*)", + "Bash(sudo chown:*)", + "Bash(sudo chmod:*)", + "Read(//var/lib/**)", + "Bash(sudo cat:*)", + "Bash(sudo journalctl:*)", + "Bash(sudo nginx:*)", + "Bash(systemctl status:*)", + "Bash(nix-instantiate:*)", + "Bash(sudo head:*)", + "Bash(ls:*)", + "Bash(systemctl show:*)", + "Bash(nix-store:*)", + "Read(//etc/**)", + "Bash(sudo iptables:*)", + "Bash(sudo find:*)", + "Bash(sudo grep:*)", + "Bash(find:*)", + "Bash(systemctl cat:*)", + "Bash(readlink:*)", + "Bash(nixos-option:*)" ] } } diff --git a/custom/public/assets/img/favicon.svg b/custom/public/assets/img/favicon.svg new file mode 100644 index 0000000..24518a9 --- /dev/null +++ b/custom/public/assets/img/favicon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/custom/public/assets/img/logo.svg b/custom/public/assets/img/logo.svg new file mode 100644 index 0000000..8b07c90 --- /dev/null +++ b/custom/public/assets/img/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/modules/adguard.nix b/modules/adguard.nix index 816c5e6..add1199 100644 --- a/modules/adguard.nix +++ b/modules/adguard.nix @@ -185,12 +185,18 @@ in iptables -t nat -D PREROUTING -i tailscale0 -p tcp --dport 53 -j REDIRECT --to-ports 5353 || true ''; - # Nginx reverse proxy for AdGuard Home Web UI and DoH + # Nginx reverse proxy for AdGuard Home Web UI and DoH (Tailscale only) myModules.nginx.domains."${cfg.domain}" = { port = cfg.port; + internalOnly = true; # Restrict access to Tailscale network and localhost only contentSecurityPolicy = null; # AdGuard Home handles its own CSP }; + # Allow direct Tailscale access to AdGuard Home dashboard + networking.firewall.extraCommands = lib.mkAfter '' + iptables -I ts-input 3 -p tcp --dport ${toString cfg.port} -s 100.64.0.0/10 -j ACCEPT + ''; + # Ensure nginx user can access ACME certs users.users.nginx.extraGroups = [ "acme" ]; }; diff --git a/modules/forgejo.nix b/modules/forgejo.nix index 9f0f003..169a40d 100644 --- a/modules/forgejo.nix +++ b/modules/forgejo.nix @@ -67,7 +67,7 @@ enable = true; database.type = "postgres"; - customDir = toString (pkgs.copyPathToStore ../custom); + customDir = "/var/lib/forgejo/custom"; settings = { server = { DOMAIN = cfg.domain; @@ -116,6 +116,13 @@ config.services.forgejo.stateDir ]; + # Copy branding assets to custom directory (Forgejo serves from /assets/img/) + systemd.tmpfiles.rules = [ + "d /var/lib/forgejo/custom/public/assets/img 0755 forgejo forgejo -" + "C+ /var/lib/forgejo/custom/public/assets/img/logo.svg - - - - ${toString ../custom/public/assets/img/logo.svg}" + "C+ /var/lib/forgejo/custom/public/assets/img/favicon.svg - - - - ${toString ../custom/public/assets/img/favicon.svg}" + ]; + services.gitea-actions-runner = lib.mkIf cfg.runner.enable { package = pkgs.forgejo-runner; instances.default = { diff --git a/modules/netdata.nix b/modules/netdata.nix index 1e2b6b7..4616e74 100644 --- a/modules/netdata.nix +++ b/modules/netdata.nix @@ -56,7 +56,7 @@ in # Allow direct Tailscale access to Netdata port networking.firewall.extraCommands = lib.mkAfter '' - iptables -I INPUT 1 -p tcp --dport ${toString cfg.port} -s 100.64.0.0/10 -j ACCEPT + iptables -I ts-input 3 -p tcp --dport ${toString cfg.port} -s 100.64.0.0/10 -j ACCEPT ''; }; diff --git a/modules/nginx.nix b/modules/nginx.nix index 717451a..6289430 100644 --- a/modules/nginx.nix +++ b/modules/nginx.nix @@ -89,6 +89,10 @@ }; }; + websockets = { + enable = lib.mkEnableOption "WebSocket proxy support for this domain"; + }; + extraLocations = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { options = { @@ -135,6 +139,12 @@ commonHttpConfig = lib.optionalString cfg.rateLimit.enable '' limit_req_zone $binary_remote_addr zone=global:10m rate=${toString cfg.rateLimit.requests}r/s; limit_conn_zone $binary_remote_addr zone=connlimit:10m; + '' + + '' + map $http_upgrade $connection_upgrade { + default upgrade; + ''' close; + } ''; virtualHosts = lib.mapAttrs' (domain: opts: { @@ -161,7 +171,13 @@ locations = { "/" = { proxyPass = "http://127.0.0.1:${toString opts.port}"; - extraConfig = opts.extraConfig + lib.optionalString (if opts.rateLimit.enable != null then + extraConfig = opts.extraConfig + + lib.optionalString opts.websockets.enable '' + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + '' + + lib.optionalString (if opts.rateLimit.enable != null then opts.rateLimit.enable else cfg.rateLimit.enable) '' limit_req zone=global burst=${toString (if opts.rateLimit.burst != null then opts.rateLimit.burst else cfg.rateLimit.burst)} nodelay; diff --git a/modules/open-webui.nix b/modules/open-webui.nix index 769b5df..0f43035 100644 --- a/modules/open-webui.nix +++ b/modules/open-webui.nix @@ -50,6 +50,7 @@ in environment = { OLLAMA_API_BASE_URL = cfg.ollamaUrl; WEBUI_URL = "https://${cfg.domain}"; + OPENAI_API_BASE_URL = "https://api.tng-chimera.ai/v1/"; }; environmentFile = config.sops.templates."openwebui.env".path; }; @@ -58,10 +59,12 @@ in sops.templates."openwebui.env" = { content = '' WEBUI_SECRET_KEY=${config.sops.placeholder.openwebui_secret_key} + OPENAI_API_KEY=${config.sops.placeholder.tng_api_key} ''; }; sops.secrets.openwebui_secret_key = { }; + sops.secrets.tng_api_key = { }; # Nginx configuration myModules.nginx.domains.${cfg.domain} = { @@ -69,8 +72,12 @@ in extraConfig = '' client_max_body_size 100M; ''; + # Disable rate limiting for OpenWebUI (loads many assets at once) + rateLimit.enable = false; + # Enable WebSocket support for Socket.IO + websockets.enable = true; # Relaxed CSP for OpenWeb UI — needs unsafe-eval for some JS, WebSockets, external images - contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https:; frame-ancestors 'self'"; + contentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' wss: https: ws:; frame-ancestors 'self'"; }; }; } diff --git a/modules/openclaw-podman.nix b/modules/openclaw-podman.nix index b938579..1ea31a8 100644 --- a/modules/openclaw-podman.nix +++ b/modules/openclaw-podman.nix @@ -52,6 +52,7 @@ in # Create directory for OpenClaw data systemd.tmpfiles.rules = [ "d /var/lib/openclaw 0755 1000 1000 -" # Assuming node user is uid 1000 + "d /var/lib/openclaw/local 0755 1000 1000 -" # For Go toolchain ]; # OpenClaw container (bridge network — isolated from host services) @@ -63,6 +64,7 @@ in ]; volumes = [ "/var/lib/openclaw:/home/node/.openclaw" + "/var/lib/openclaw/local:/home/node/.local" ] ++ lib.optionals cfg.superpowers.enable [ "${cfg.superpowers.src}:/home/node/superpowers-src:ro" ]; @@ -71,7 +73,7 @@ in # Copy the declarative config before starting the container # This allows OpenClaw to safely write/rename the file at runtime without EBUSY errors systemd.services."podman-openclaw".preStart = lib.mkBefore '' - mkdir -p /var/lib/openclaw + mkdir -p /var/lib/openclaw/local cp -f ${./openclaw-config.json} /var/lib/openclaw/openclaw.json chown -R 1000:1000 /var/lib/openclaw chmod -R u+rwX /var/lib/openclaw @@ -100,13 +102,56 @@ in ''} ''; + # Go toolchain installation script + # Stored in /var/lib/openclaw and executed inside the container + environment.etc."openclaw/install-go.sh".source = pkgs.writeScript "install-go.sh" '' + #!/bin/bash + set -e + GO_URL="https://go.dev/dl/go1.24.1.linux-amd64.tar.gz" + GO_DIR="/home/node/.local/go" + + if [ -d "$GO_DIR" ]; then + echo "Go already installed at $GO_DIR" + exit 0 + fi + + echo "Installing Go toolchain" + mkdir -p /home/node/.local + + if command -v curl &> /dev/null; then + curl -fsSL "$GO_URL" | tar -C /home/node/.local -xzf - + elif command -v wget &> /dev/null; then + wget -qO- "$GO_URL" | tar -C /home/node/.local -xzf - + else + echo "ERROR - Neither curl nor wget available" + exit 1 + fi + + echo "Go installed successfully" + "$GO_DIR/bin/go" version + ''; + + # Go toolchain installation + # Downloads Go to a persistent volume for use inside the container + systemd.services."openclaw-go-setup" = { + description = "Install Go toolchain for OpenClaw"; + after = [ "podman-openclaw.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + # Copy script to container-accessible location, then execute + ExecStart = "${pkgs.bash}/bin/bash -c 'cp /etc/openclaw/install-go.sh /var/lib/openclaw/ && chmod +x /var/lib/openclaw/install-go.sh && ${pkgs.podman}/bin/podman exec -u node openclaw /home/node/.openclaw/install-go.sh'"; + RemainAfterExit = true; + }; + }; + # Optional: Install PyYAML inside the container on startup - # We do this as a postStart or a simple background loop if needed, - # but a better way is to ensure the image has it. + # We do this as a postStart or a simple background loop if needed, + # but a better way is to ensure the image has it. # Since we can't easily change the image here, we'll try to run a one-time pip install. systemd.services."openclaw-superpowers-setup" = lib.mkIf cfg.superpowers.enable { description = "One-time setup for OpenClaw superpowers (PyYAML and Cron)"; - after = [ "podman-openclaw.service" ]; + after = [ "podman-openclaw.service" "openclaw-go-setup.service" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; diff --git a/modules/podman.nix b/modules/podman.nix index 971a21d..dd2e7c9 100644 --- a/modules/podman.nix +++ b/modules/podman.nix @@ -28,5 +28,11 @@ in # Give main user access to podman users.users.${mainUser}.extraGroups = [ "podman" ]; + + # Enable IP forwarding for container networking + boot.kernel.sysctl = { + "net.ipv4.ip_forward" = 1; + "net.ipv6.conf.all.forwarding" = 1; + }; }; } diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index 91abc18..5138062 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -15,6 +15,7 @@ forgejo_token: ENC[AES256_GCM,data:/ugEQr0bqai0QPbW9FqdqQEBrF8bSqTtGtCswRP7BYfMR forgejo_user: ENC[AES256_GCM,data:Ralbwu6+6Htc2+I=,iv:dicPMRRriz6MVp0PtbezI6UcxdCmszp9PnTsJaHAzKk=,tag:g2mUMYmnDJZ42F+KVsvHhg==,type:str] github_token: ENC[AES256_GCM,data:OAh6v6xrLr47ZdytdSR4uBpj6vJB8kJa8qc3eFBByK53nkrb3SUuBQ==,iv:GM/DpFSVl1CeQLX4tH2WxBuWBbI1YWzSsmvZK+2jdWM=,tag:lPQSjIlYbe3AczfDN3Nk+g==,type:str] openwebui_secret_key: ENC[AES256_GCM,data:c1Mnc0juYBAmHap3f0G5vwRDUymYWb92nIk78Rw5ApkhhW4k4ifRccCsv0fU2TXNSwHCc7d6OeP60kJYEpr5ZA==,iv:wCqfwhn6WFv3A0asZnbPdBdmw24QdKbJE0BplWzq9CE=,tag:WU8Rlyf+CIL2uG8yWKHjcw==,type:str] +tng_api_key: ENC[AES256_GCM,data:KxfGd46SSp13zo1IxchDVq8dY7wTReyAS58JTIqJbv10YoXKY2fq4R4o6EaWSqyJC+k=,iv:QBVLbDDV94pwsHSngABiL17wVcHYNTUBtoOvQgg8Fcc=,tag:A/4Ieeb1nIKVEJ6FVL6gJQ==,type:str] sops: age: - recipient: age1rz4eyzmmtmua6s9cny3pjjwv80n4fpvhkwc4jzdd8vpre8zc5vtqfjtuy0 @@ -26,7 +27,7 @@ sops: SnI3Z3p6U0x1YzVUTHZ0RVh0cHBDSGMKbDXwp9MM9cL/9DgWPV/btH6iYgaVXmvw Gk4IsH7zEWbS1kxIEapzBpIINTSQKZ30aPqwuspVKdSa8lsfi1X1jA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-03-20T21:46:27Z" - mac: ENC[AES256_GCM,data:EnwV1ghvueIVy0zSfBiJYITz79/WgtABjY+UC8l7IdUUppymWMpU1AotqxZmBwGuVmheffGNxjmeJGyq4vjBssz8AKagtqNO7l3MAkqDfcFBJbBip5zR7b6kh2izhWUfokB73Fvp7nwnYFsaGKDVPMzsCZ2hWSxhTzdzn4/iv9w=,iv:kZSS5F3l1nId8gcZmHbLKIIyquwbnUrdjHVWDnwV+yI=,tag:yTbpzExgwmGQKELbriyzPw==,type:str] + lastmodified: "2026-03-20T23:38:03Z" + mac: ENC[AES256_GCM,data:B9s77SPIEI22K1QVyE4iUx9sDlPph7jbGVP2R5ulA+LU0ctjVqybvQQIepY1lN7OmnOVteK1Ed3B+BlqxlYhotZgTU14Sjh0fy5NKViLG2eQOJRbMoYPSXL7aLugGkfhTWr0qvdR9O4S4e6jR2AFWfp07x+HRSM95hKKCKgOFYI=,iv:TBAICXdmp0L4iAbAbST1K9/eysqNq3gqRy1mrMOap9o=,tag:eqBRamo+TdE6wleiiZC3eQ==,type:str] unencrypted_suffix: _unencrypted version: 3.12.1