This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

AGENTS.md: mention PRs from git.lassul.us forks #1

Test PR from the hermes account (fork on knot.lassul.us, no merge rights). Fine to close instead of merging.

Labels

None yet.

Participants 1
AT URI
at://did:plc:vhearieesyugq7ortfgncciv/sh.tangled.repo.pull/3mycakhkdy222
+1024 -16
Diff #2
+3 -2
configs/ssh-redirect.nix
··· 13 13 } 14 14 # Runs before nixos-fw, which accepts 22 everywhere. Connections that 15 15 # came in on 45621 have been redirected to 22 already, so match the 16 - # port the client asked for. 16 + # port the client asked for. tcp dport 22 spares connections to 22 17 + # that got redirected elsewhere (the tangled knot on neoprism). 17 18 chain input { 18 19 type filter hook input priority filter - 1; policy accept; 19 - ct state new meta l4proto tcp ct original proto-dst 22 iifname != { "lo", "retiolum", "wiregrill" } reject with tcp reset 20 + ct state new tcp dport 22 ct original proto-dst 22 iifname != { "lo", "retiolum", "wiregrill" } reject with tcp reset 20 21 } 21 22 ''; 22 23 };
+16 -14
configs/dns/knot.nix
··· 8 8 ip4 = "95.217.192.59"; 9 9 ip6 = "2a01:4f9:4a:4f1a::2"; 10 10 # Delegated from lassul.us (NS ns1 only, the Hetzner secondaries never see 11 - # it) so lego can do DNS-01 for *.review.lassul.us (configs/crit-proxy.nix) 12 - # with RFC2136 updates, without touching the signed main zone. 13 - acmeReviewZone = pkgs.writeText "_acme-challenge.review.lassul.us.zone" '' 14 - @ 3600 IN SOA ns1.lassul.us. acme.lassul.us. 1 7200 3600 86400 60 15 - @ 3600 IN NS ns1.lassul.us. 16 - ''; 11 + # them) so lego can do DNS-01 for *.review.lassul.us (configs/crit-proxy.nix) 12 + # and *.pds.lassul.us (configs/services/git/pds.nix) with RFC2136 updates, 13 + # without touching the signed main zone. 14 + acmeZone = domain: { 15 + inherit domain; 16 + file = "${pkgs.writeText "${domain}.zone" '' 17 + @ 3600 IN SOA ns1.lassul.us. acme.lassul.us. 1 7200 3600 86400 60 18 + @ 3600 IN NS ns1.lassul.us. 19 + ''}"; 20 + acl = [ "acme_review" ]; 21 + zonefile-sync = "-1"; 22 + zonefile-load = "difference-no-serial"; 23 + journal-content = "all"; 24 + }; 17 25 # dyndns = domain: 18 26 # pkgs.writeText "${domain}.zone" '' 19 27 # @ 3600 IN SOA ${domain}. ns1.lassul.us. 2023101213 7200 3600 86400 3600 ··· 205 213 serial-policy = "dateserial"; 206 214 journal-content = "all"; 207 215 } 208 - { 209 - domain = "_acme-challenge.review.lassul.us"; 210 - file = "${acmeReviewZone}"; 211 - acl = [ "acme_review" ]; 212 - zonefile-sync = "-1"; 213 - zonefile-load = "difference-no-serial"; 214 - journal-content = "all"; 215 - } 216 + (acmeZone "_acme-challenge.review.lassul.us") 217 + (acmeZone "_acme-challenge.pds.lassul.us") 216 218 ]; 217 219 }; 218 220 };
+7
configs/dns/lassul.us.zone
··· 67 67 *.review IN CNAME neoprism 68 68 ;; DNS-01 for the *.review wildcard cert, dynamic zone in knot.nix 69 69 _acme-challenge.review IN NS ns1 70 + pds IN CNAME neoprism 71 + *.pds IN CNAME neoprism 72 + ;; DNS-01 for the *.pds wildcard cert (PDS handles), dynamic zone in knot.nix 73 + _acme-challenge.pds IN NS ns1 74 + git IN CNAME neoprism 75 + knot IN CNAME neoprism 76 + mirror IN CNAME neoprism 70 77 when IN CNAME neoprism 71 78 paperless IN CNAME neoprism 72 79
+3
machines/neoprism/config.nix
··· 106 106 # "Your own provider" logins for zitadel (byoidc.lassul.us) 107 107 ../../configs/byoidc.nix 108 108 109 + # ATProto PDS (pds.lassul.us) 110 + ../../configs/services/git 111 + 109 112 # radicle public seed (radicle.lassul.us) 110 113 ../../configs/radicle.lassul.us.nix 111 114 # radicle identity + node for this machine (/var/lib/rad)
+11
pkgs/tangled/appview-knot-members-did-web.patch
··· 1 + --- a/appview/knotacl/client.go 2 + +++ b/appview/knotacl/client.go 3 + @@ -49,7 +49,7 @@ func (c *Client) GetKnotMembers(ctx context.Context, host string) ([]string, error) { 4 + "", 5 + make(map[string]struct{}), 6 + func(cursor string) ([]*tangled.KnotListMembers_ListItem, *string, error) { 7 + - out, err := tangled.KnotListMembers(ctx, xc, cursor, listPageLimit, "", host) 8 + + out, err := tangled.KnotListMembers(ctx, xc, cursor, listPageLimit, "", "did:web:"+host) 9 + if err != nil { 10 + return nil, nil, err 11 + }
+58
pkgs/tangled/assets.json
··· 1 + { 2 + "actor-typeahead-src": { 3 + "type": "git", 4 + "url": "https://tangled.org/@jakelazaroff.com/actor-typeahead", 5 + "rev": "677fe7f743050a4e7f09d4a6f87bbf1325a06f6b", 6 + "hash": "sha256-heizoWUKDdar6ymfZTnj3ytcEv/L4d4fzSmtr0HlXsQ=" 7 + }, 8 + "hls-src": { 9 + "type": "file", 10 + "url": "https://cdn.jsdelivr.net/npm/hls.js@1.5.13/dist/hls.min.js", 11 + "hash": "sha256-mmxgaQRXXWzRG6/OlOn5efV+NmzhvmXTQaFbODqh0b8=" 12 + }, 13 + "htmx-src": { 14 + "type": "file", 15 + "url": "https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js", 16 + "hash": "sha256-2D8I0L0u6qb57QpeVn88RrT0xatDwQwG/lWOTjuEFPc=" 17 + }, 18 + "htmx-ws-src": { 19 + "type": "file", 20 + "url": "https://cdn.jsdelivr.net/npm/htmx-ext-ws@2.0.4", 21 + "hash": "sha256-woiluaeAoxH/CyDkpk6LYNWCzxH8PHgwgnrlBFGJxgk=" 22 + }, 23 + "ibm-plex-mono-src": { 24 + "type": "tarball", 25 + "url": "https://github.com/IBM/plex/releases/download/@ibm%2Fplex-mono@1.1.0/ibm-plex-mono.zip", 26 + "hash": "sha256-OwUmrPfEehLDz0fl2ChYLK8FQM2p0G1+EMrGsYEq+6g=" 27 + }, 28 + "inter-fonts-src": { 29 + "type": "tarball", 30 + "url": "https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip", 31 + "hash": "sha256-5vdKKvHAeZi6igrfpbOdhZlDX2/5+UvzlnCQV6DdqoQ=" 32 + }, 33 + "lucide-src": { 34 + "type": "tarball", 35 + "url": "https://github.com/lucide-icons/lucide/releases/download/0.536.0/lucide-icons-0.536.0.zip", 36 + "hash": "sha256-+exBR2OToB1iv7ZQI2S4B0lXA/QRvC9n6U99UxGpJGs=" 37 + }, 38 + "mathjax-src": { 39 + "type": "file", 40 + "url": "https://cdn.jsdelivr.net/npm/mathjax@4.1.2/tex-svg.js", 41 + "hash": "sha256-eqg25bKfiDzynRMR2RfeWNfo0EefHF/eRRFafl9RnNI=" 42 + }, 43 + "mermaid-src": { 44 + "type": "file", 45 + "url": "https://cdn.jsdelivr.net/npm/mermaid@11.12.3/dist/mermaid.min.js", 46 + "hash": "sha256-/YOdECG2V5c3kJ1QfGvhziTT6K/Dx/4mOk2mr3Fs/do=" 47 + }, 48 + "sqlite-lib-src": { 49 + "type": "tarball", 50 + "url": "https://sqlite.org/2024/sqlite-amalgamation-3450100.zip", 51 + "hash": "sha256-bJoMjirsBjm2Qk9KPiy3yV3+8b/POlYe76/FQbciHro=" 52 + }, 53 + "tailwindcss-animated-src": { 54 + "type": "tarball", 55 + "url": "https://registry.npmjs.org/tailwindcss-animated/-/tailwindcss-animated-1.1.2.tgz", 56 + "hash": "sha256-l3esIcsAwj0PHOhnHBLWiLzzRiOo1yOk9VYEGpn6fyQ=" 57 + } 58 + }
+183
pkgs/tangled/package.nix
··· 1 + { 2 + lib, 3 + stdenv, 4 + buildGoModule, 5 + rustPlatform, 6 + fetchgit, 7 + fetchurl, 8 + fetchzip, 9 + linkFarm, 10 + runCommandLocal, 11 + symlinkJoin, 12 + writeShellScriptBin, 13 + cmake, 14 + perl, 15 + tailwindcss, 16 + }: 17 + 18 + # Tangled (https://tangled.org) without its flake: the appview, knot 2 19 + # (knot-rs) and knotmirror, built the way its nix/pkgs files do. tap and goat 20 + # come from nixpkgs (tap, atproto-goat). 21 + let 22 + version = "0-unstable-2026-10-18"; 23 + 24 + src = fetchgit { 25 + url = "https://tangled.org/tangled.org/core"; 26 + rev = "dad751949f865d6189f7602e1409c5f951e75771"; 27 + hash = "sha256-pehXSw17JRgWA/d4t/x9ZcdxOHJaY3I0+H+3p1hVZxk="; 28 + }; 29 + 30 + # The *-src inputs of the flake at that rev, generated by update.sh from 31 + # its flake.lock; the hashes are the lock's narHashes. 32 + assets = lib.mapAttrs ( 33 + name: a: 34 + { 35 + file = fetchurl { 36 + inherit name; 37 + inherit (a) url hash; 38 + recursiveHash = true; 39 + }; 40 + tarball = fetchzip { 41 + inherit name; 42 + inherit (a) url hash; 43 + # flake tarballs only drop a single top-level directory 44 + stripRoot = name != "inter-fonts-src"; 45 + }; 46 + git = fetchgit { inherit (a) url rev hash; }; 47 + } 48 + .${a.type} 49 + ) (lib.importJSON ./assets.json); 50 + 51 + # The Go programs share the root go.mod, so one vendor directory. 52 + buildTangledGo = 53 + pname: args: 54 + buildGoModule ( 55 + { 56 + inherit pname version src; 57 + vendorHash = "sha256-6eDNOcnMHMpdQS7M7A8O+wZOnP2/968Re7Iqn8XgGNM="; 58 + overrideModAttrs = _: { name = "tangled-${version}-go-modules"; }; 59 + subPackages = [ "cmd/${pname}" ]; 60 + doCheck = false; 61 + meta.mainProgram = pname; 62 + } 63 + // args 64 + ); 65 + 66 + sqlite-lib = stdenv.mkDerivation { 67 + name = "sqlite-lib"; 68 + src = assets.sqlite-lib-src; 69 + buildPhase = '' 70 + $CC -c sqlite3.c 71 + $AR rcs libsqlite3.a sqlite3.o 72 + $RANLIB libsqlite3.a 73 + ''; 74 + installPhase = '' 75 + mkdir -p $out/include $out/lib 76 + cp *.h $out/include 77 + cp libsqlite3.a $out/lib 78 + ''; 79 + }; 80 + 81 + dolly-unwrapped = buildTangledGo "dolly" { }; 82 + dolly = writeShellScriptBin "dolly" '' 83 + exec ${dolly-unwrapped}/bin/dolly \ 84 + -template ${src}/appview/pages/templates/fragments/dolly \ 85 + "$@" 86 + ''; 87 + 88 + tailwindcss-plugins = linkFarm "tailwindcss-plugins" [ 89 + { 90 + name = "tailwindcss-animated"; 91 + path = assets.tailwindcss-animated-src; 92 + } 93 + ]; 94 + 95 + static-files = runCommandLocal "appview-static-files" { } '' 96 + mkdir -p $out/{fonts,icons,logos} && cd $out 97 + cp -f ${assets.htmx-src} htmx.min.js 98 + cp -f ${assets.htmx-ws-src} htmx-ext-ws.min.js 99 + cp -f ${assets.mermaid-src} mermaid.min.js 100 + cp -f ${assets.hls-src} hls.min.js 101 + cp -f ${assets.mathjax-src} mathjax.min.js 102 + cp -rf ${assets.lucide-src}/*.svg icons/ 103 + cp -rf ${src}/icons/*.svg icons/ 104 + cp -f ${assets.inter-fonts-src}/web/InterVariable*.woff2 fonts/ 105 + cp -f ${assets.inter-fonts-src}/web/InterDisplay*.woff2 fonts/ 106 + cp -f ${assets.inter-fonts-src}/InterVariable*.ttf fonts/ 107 + cp -f ${assets.ibm-plex-mono-src}/fonts/complete/woff2/IBMPlexMono*.woff2 fonts/ 108 + cp -f ${assets.actor-typeahead-src}/actor-typeahead.js . 109 + cp -rf ${src}/appview/pages/profile-fx . 110 + 111 + ${dolly}/bin/dolly -output logos/dolly.png -size 180 112 + ${dolly}/bin/dolly -output logos/dolly.ico -size 48 113 + ${dolly}/bin/dolly -output logos/dolly.svg -color currentColor -favicon 114 + export NODE_PATH=${tailwindcss-plugins} 115 + cd ${src} && ${tailwindcss}/bin/tailwindcss -i input.css -o $out/tw.css 116 + ''; 117 + 118 + appview = buildTangledGo "appview" { 119 + # knot2 wants the knot's did:web as listMembers subject, not its hostname 120 + patches = [ ./appview-knot-members-did-web.patch ]; 121 + postUnpack = '' 122 + mkdir -p $sourceRoot/appview/pages/static 123 + cp -fr ${static-files}/* $sourceRoot/appview/pages/static 124 + ''; 125 + tags = [ "libsqlite3" ]; 126 + env = { 127 + CGO_ENABLED = 1; 128 + CGO_CFLAGS = "-I ${sqlite-lib}/include "; 129 + CGO_LDFLAGS = "-L ${sqlite-lib}/lib"; 130 + }; 131 + }; 132 + 133 + knotmirror = buildTangledGo "knotmirror" { }; 134 + 135 + knot-rs = rustPlatform.buildRustPackage { 136 + pname = "knot-server"; 137 + inherit version src; 138 + cargoHash = "sha256-3YPqGQS1yVF+Pmj0Xe2z6yITLl7RgwPMPY389MB8lw8="; 139 + nativeBuildInputs = [ 140 + cmake 141 + perl 142 + ]; 143 + dontUseCmakeConfigure = true; 144 + cargoBuildFlags = [ 145 + "--bin" 146 + "knot-server" 147 + "--package" 148 + "knot-server" 149 + ]; 150 + doCheck = false; 151 + meta.mainProgram = "knot-server"; 152 + }; 153 + in 154 + symlinkJoin { 155 + pname = "tangled"; 156 + inherit version; 157 + paths = [ 158 + appview 159 + knot-rs 160 + knotmirror 161 + ]; 162 + 163 + passthru = { 164 + inherit 165 + src 166 + assets 167 + sqlite-lib 168 + dolly 169 + static-files 170 + appview 171 + knot-rs 172 + knotmirror 173 + ; 174 + updateScript = ./update.sh; 175 + }; 176 + 177 + meta = { 178 + description = "Git collaboration platform on atproto: appview, knot and knotmirror"; 179 + homepage = "https://tangled.org/tangled.org/core"; 180 + license = lib.licenses.mit; 181 + platforms = lib.platforms.linux; 182 + }; 183 + }
+69
pkgs/tangled/update.sh
··· 1 + #!/usr/bin/env bash 2 + set -euo pipefail 3 + 4 + # Tangled has no releases for the knot-rs era and isn't on GitHub, so 5 + # nix-update can't follow it: bump src to the head of master, take the 6 + # frontend assets (the flake's *-src inputs) from its flake.lock and probe the 7 + # Go vendor and cargo hashes. 8 + attr=tangled 9 + url=https://tangled.org/tangled.org/core 10 + 11 + # Run by update-pkgs from the flake root ($0 is a read-only store copy). 12 + dir=pkgs/tangled 13 + pkg=$dir/package.nix 14 + # Linux-only package: always evaluate/build the x86_64-linux variant. 15 + build_attr=".#packages.x86_64-linux.$attr" 16 + 17 + new_rev=$(git ls-remote "$url" refs/heads/master | cut -f1) 18 + old_rev=$(sed -n 's|^ *rev = "\([^"]*\)";|\1|p' "$pkg") 19 + if [ "$old_rev" = "$new_rev" ]; then 20 + echo "$attr: already up to date ($new_rev)" 21 + exit 0 22 + fi 23 + 24 + # The flake fetcher's narHash is fetchgit's hash, and its store path has the 25 + # flake.lock with the asset pins. 26 + prefetch=$(nix flake prefetch --json "git+$url?rev=$new_rev") 27 + src_hash=$(jq -r .hash <<<"$prefetch") 28 + src_path=$(jq -r .storePath <<<"$prefetch") 29 + new_date=$(date -u -d "@$(jq -r .locked.lastModified <<<"$prefetch")" +%F) 30 + new_version="0-unstable-$new_date" 31 + 32 + jq '.nodes as $n 33 + | [.nodes.root.inputs | to_entries[] | select(.key | endswith("-src")) 34 + | { key, value: ($n[.value].locked 35 + | { type, url } + (if .rev then { rev } else {} end) + { hash: .narHash }) }] 36 + | sort_by(.key) | from_entries' "$src_path/flake.lock" >"$dir/assets.json" 37 + 38 + # Bump rev + version and blank the derived hashes: with the old ones in place 39 + # nix reuses the cached FODs and the build "succeeds" on stale code. 40 + sed -i \ 41 + -e "s|^\( *\)rev = \".*\";|\1rev = \"$new_rev\";|" \ 42 + -e "s|^\( *\)version = \".*\";|\1version = \"$new_version\";|" \ 43 + -e "s|^\( *\)hash = \"sha256-.*\";|\1hash = \"$src_hash\";|" \ 44 + -e "s|^\( *\)vendorHash = \".*\";|\1vendorHash = \"\";|" \ 45 + -e "s|^\( *\)cargoHash = \".*\";|\1cargoHash = \"\";|" \ 46 + "$pkg" 47 + 48 + # The probe build is *expected* to fail (hash mismatch); `|| true` keeps 49 + # pipefail/errexit from turning that into an empty `got`. awk must not 50 + # `exit` early or tee dies of SIGPIPE and takes the pipeline with it. 51 + probe_hash() { 52 + { nix build --no-link "$build_attr$1" 2>&1 || true; } | tee /dev/stderr | 53 + awk '/got:/ && !seen { print $2; seen = 1 }' || true 54 + } 55 + 56 + # Probe each FOD on its own so a hash is never attributed to the wrong field. 57 + for spec in vendorHash:.appview.goModules cargoHash:.knot-rs.cargoDeps; do 58 + field=${spec%%:*} 59 + got=$(probe_hash "${spec#*:}") 60 + if [ -z "$got" ]; then 61 + echo "$attr: no hash reported for $field" >&2 62 + exit 1 63 + fi 64 + sed -i "s|^\( *\)$field = \"[^\"]*\";|\1$field = \"$got\";|" "$pkg" 65 + echo "$attr: $field → $got" 66 + done 67 + 68 + echo "$attr: updated to $new_rev ($new_version)" 69 + nix build --no-link "$build_attr"
+33
pkgs/tranquil-pds/package.nix
··· 1 + { 2 + tranquil-pds, 3 + tranquil-pds-frontend, 4 + fetchFromTangled, 5 + rustPlatform, 6 + }: 7 + let 8 + version = "0.7.0"; 9 + src = fetchFromTangled { 10 + did = "did:plc:jj6ajj6duxnlthwtnob4qyuv"; 11 + tag = "v${version}"; 12 + hash = "sha256-QbncR2z9GhxKKbbCf6a5Ph4lLi4OYWjRN49s7+5q7Uc="; 13 + }; 14 + in 15 + tranquil-pds.overrideAttrs ( 16 + finalAttrs: old: { 17 + inherit version src; 18 + cargoDeps = rustPlatform.fetchCargoVendor { 19 + inherit (finalAttrs) pname version src; 20 + hash = "sha256-fjT0cAt7lPD0O2PGNT604dRIczerKjExJvU/L3Yul3c="; 21 + }; 22 + passthru = old.passthru // { 23 + # web UI, served from settings.frontend.dir 24 + frontend = tranquil-pds-frontend.overrideAttrs (old: { 25 + inherit version src; 26 + pnpmDeps = old.pnpmDeps.overrideAttrs { 27 + inherit version src; 28 + outputHash = "sha256-+P4UUkZKQJVfGbDFKR0gRMU+wYK9K7NBYo1s/ebRK9I="; 29 + }; 30 + }); 31 + }; 32 + } 33 + )
+132
configs/services/git/appview.nix
··· 1 + { 2 + self, 3 + config, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + domain = "git.lassul.us"; 9 + port = 3010; 10 + redisPort = 6380; 11 + listenAddr = "127.0.0.1:${toString port}"; 12 + in 13 + { 14 + # Tangled appview: the web UI for repos on knot.lassul.us (knot.nix) and 15 + # other knots. Users log in with their atproto account via OAuth to their 16 + # PDS. Records come from Bluesky's jetstream (upstream default) and repo 17 + # contents from our knotmirror (knotmirror.nix). Follows tangled's 18 + # nix/modules/appview.nix, without its flake (the module files are in a 19 + # fetched source, importing them would be IFD). 20 + 21 + clan.core.vars.generators.tangled-appview = { 22 + files."appview.env" = { }; 23 + runtimeInputs = with pkgs; [ 24 + atproto-goat 25 + coreutils 26 + gawk 27 + gnugrep 28 + openssl 29 + ]; 30 + script = '' 31 + { 32 + echo "TANGLED_COOKIE_SECRET=$(openssl rand -hex 32)" 33 + echo "TANGLED_OAUTH_CLIENT_KID=$(date +%s)" 34 + echo "TANGLED_OAUTH_CLIENT_SECRET=$(goat key generate -t P-256 | grep -A1 'Secret Key' | tail -n1 | awk '{print $1}')" 35 + } > "$out/appview.env" 36 + ''; 37 + }; 38 + 39 + # 6379 is docuseal's 40 + services.redis.servers.appview = { 41 + enable = true; 42 + port = redisPort; 43 + }; 44 + 45 + systemd.services.appview = { 46 + description = "tangled appview service"; 47 + wantedBy = [ "multi-user.target" ]; 48 + after = [ 49 + "redis-appview.service" 50 + "network-online.target" 51 + ]; 52 + requires = [ "redis-appview.service" ]; 53 + wants = [ "network-online.target" ]; 54 + path = [ pkgs.diffutils ]; 55 + serviceConfig = { 56 + Type = "simple"; 57 + ExecStart = "${self.packages.${pkgs.stdenv.hostPlatform.system}.tangled.appview}/bin/appview"; 58 + Restart = "always"; 59 + RestartSec = "10s"; 60 + EnvironmentFile = config.clan.core.vars.generators.tangled-appview.files."appview.env".path; 61 + # upstream runs it as root 62 + DynamicUser = true; 63 + StateDirectory = "appview"; 64 + WorkingDirectory = "/var/lib/appview"; 65 + NoNewPrivileges = true; 66 + PrivateTmp = true; 67 + ProtectSystem = "strict"; 68 + ProtectHome = true; 69 + ReadWritePaths = [ "/var/lib/appview" ]; 70 + }; 71 + # upstream's defaults, except where commented 72 + environment = { 73 + TANGLED_DB_PATH = "/var/lib/appview/appview.db"; 74 + TANGLED_LISTEN_ADDR = listenAddr; 75 + TANGLED_METRICS_LISTEN_ADDR = "127.0.0.1:${toString (port + 1)}"; 76 + TANGLED_APPVIEW_HOST = domain; 77 + TANGLED_APPVIEW_NAME = domain; 78 + TANGLED_DEV = "false"; 79 + TANGLED_REDIS_ADDR = "127.0.0.1:${toString redisPort}"; 80 + TANGLED_REDIS_DB = "0"; 81 + TANGLED_JETSTREAM_ENDPOINT = "wss://jetstream1.us-east.bsky.network/subscribe"; 82 + TANGLED_KNOTSTREAM_RETRY_INTERVAL = "60s"; 83 + TANGLED_KNOTSTREAM_MAX_RETRY_INTERVAL = "120m"; 84 + TANGLED_KNOTSTREAM_CONNECTION_TIMEOUT = "5s"; 85 + TANGLED_KNOTSTREAM_WORKER_COUNT = "64"; 86 + TANGLED_KNOTSTREAM_QUEUE_SIZE = "100"; 87 + TANGLED_SPINDLESTREAM_RETRY_INTERVAL = "60s"; 88 + TANGLED_SPINDLESTREAM_MAX_RETRY_INTERVAL = "120m"; 89 + TANGLED_SPINDLESTREAM_CONNECTION_TIMEOUT = "5s"; 90 + TANGLED_SPINDLESTREAM_WORKER_COUNT = "64"; 91 + TANGLED_SPINDLESTREAM_QUEUE_SIZE = "100"; 92 + TANGLED_RESEND_SENT_FROM = "noreply@notifs.tangled.sh"; 93 + # sends logins, searches and pushes even without an API key 94 + TANGLED_POSTHOG_ENDPOINT = "http://127.0.0.1:9"; 95 + TANGLED_CAMO_HOST = "https://camo.tangled.sh"; 96 + TANGLED_AVATAR_HOST = "https://avatar.tangled.sh"; 97 + TANGLED_PLC_URL = "https://plc.directory"; 98 + TANGLED_PDS_HOST = "https://tngl.sh"; 99 + # records of tangled's account, fetched from its PDS at startup; repos 100 + # start without labels 101 + TANGLED_LABEL_DEFAULTS = ""; 102 + TANGLED_LABEL_GFI = "at://did:plc:wshs7t2adsemcrrd4snkeqli/sh.tangled.label.definition/good-first-issue"; 103 + # probed at every login, upstream's is knot1.tangled.sh 104 + TANGLED_KNOT_DEFAULT = "knot.lassul.us"; 105 + # OG images of repos, issues and pulls, rendered by tangled 106 + TANGLED_OGRE_HOST = "http://127.0.0.1:9"; 107 + # browsers get raw blob links to it, so not localhost 108 + TANGLED_KNOTMIRROR_URL = "https://mirror.lassul.us"; 109 + }; 110 + }; 111 + 112 + # redis only caches 113 + lass.backup.state.tangled-appview = { 114 + preBackup = '' 115 + ${pkgs.sqlite}/bin/sqlite3 -cmd ".timeout 60000" /var/lib/private/appview/appview.db ".backup /var/backup/tangled-appview.db" 116 + ''; 117 + # WAL-mode SQLite readers need to write the -shm file 118 + readWritePaths = [ "/var/lib/private/appview" ]; 119 + }; 120 + 121 + services.nginx.virtualHosts.${domain} = { 122 + enableACME = true; 123 + forceSSL = true; 124 + locations."/" = { 125 + proxyPass = "http://${listenAddr}"; 126 + recommendedProxySettings = true; 127 + proxyWebsockets = true; 128 + }; 129 + # creates tngl.sh accounts, which needs tangled's PDS admin secret 130 + locations."/signup".return = "404"; 131 + }; 132 + }
+8
configs/services/git/default.nix
··· 1 + { 2 + imports = [ 3 + ./appview.nix 4 + ./knot.nix 5 + ./knotmirror.nix 6 + ./pds.nix 7 + ]; 8 + }
+216
configs/services/git/knot.nix
··· 1 + { 2 + self, 3 + config, 4 + lib, 5 + pkgs, 6 + ... 7 + }: 8 + let 9 + domain = "knot.lassul.us"; 10 + # DID of the owner's account on pds.lassul.us 11 + ownerDid = "did:plc:6nk6tk76up7254wawocoekjr"; # lass.pds.lassul.us 12 + user = "tangled-knot"; 13 + stateDir = "/var/lib/tangled-knot"; 14 + listenAddr = "127.0.0.1:5555"; 15 + sshPort = 2222; 16 + package = self.packages.${pkgs.stdenv.hostPlatform.system}.tangled.knot-rs; 17 + 18 + # `knot-server config-template` lists the keys; upstream's defaults, except 19 + # where commented 20 + rendered = (pkgs.formats.toml { }).generate "knot.toml" { 21 + server = { 22 + hostname = domain; 23 + admins = [ ownerDid ]; 24 + appview_endpoint = "https://git.lassul.us"; 25 + listen_addr = listenAddr; 26 + internal_listen_addr = "[::1]:5444"; 27 + ssh_listen_addr = "[::]:${toString sshPort}"; 28 + ssh_host_key_file = "${stateDir}/ssh_host_key"; 29 + }; 30 + atproto.plc_directory = "https://plc.directory"; 31 + acl.admission = "closed"; 32 + repo.scan_path = "${stateDir}/repos"; 33 + # sha256 repos can't exchange objects with sha1 ones (github, cgit) 34 + git.object_format = "sha1"; 35 + secrets = { 36 + sealed_key_file = "${stateDir}/sealed-keys"; 37 + master_key_env = "KNOT_MASTER_KEY"; 38 + }; 39 + tls = { 40 + http3 = true; 41 + acme_enabled = false; 42 + acme_cache_dir = "${stateDir}/acme"; 43 + acme_staging = false; 44 + mtls_enabled = false; 45 + }; 46 + xrpc = { 47 + trusted_proxy_header = "x-forwarded-for"; 48 + trusted_proxies = [ 49 + "127.0.0.1" 50 + "::1" 51 + ]; 52 + }; 53 + }; 54 + configFile = pkgs.runCommandLocal "knot-config.toml" { nativeBuildInputs = [ package ]; } '' 55 + knot-server validate --config-only ${rendered} 56 + ln -s ${rendered} $out 57 + ''; 58 + in 59 + { 60 + # Tangled knot 2 (knot-rs): git hosting for git.lassul.us, following 61 + # tangled's nix/modules/knot-rs.nix without its flake (the module files are 62 + # in a fetched source, importing them would be IFD): plain HTTP behind 63 + # nginx, unprivileged ports, keys in stateDir. The Go knot's unit and user 64 + # are both named knot, which the DNS server already uses. 65 + # Knot 2 serves ssh itself on [::]:2222 (it binds a single address, and 66 + # sshd keeps 22). Port 22 on the public addresses is redirected to it, so 67 + # git@knot.lassul.us:<owner>/<repo> works as the appview shows it. Admin 68 + # ssh stays on 22 via retiolum/wiregrill and on public 45621 69 + # (ssh-redirect.nix). 70 + 71 + clan.core.vars.generators.tangled-knot = { 72 + files."knot.env" = { }; 73 + runtimeInputs = [ pkgs.openssl ]; 74 + script = '' 75 + echo "KNOT_MASTER_KEY=$(openssl rand -base64 32)" > "$out/knot.env" 76 + ''; 77 + }; 78 + 79 + environment.systemPackages = [ package ]; 80 + environment.etc."knot/config.toml".source = configFile; 81 + 82 + users.users.${user} = { 83 + isSystemUser = true; 84 + home = stateDir; 85 + group = user; 86 + }; 87 + users.groups.${user} = { }; 88 + 89 + systemd.tmpfiles.settings."10-knot-rs" = { 90 + ${stateDir}.d = { 91 + mode = "0750"; 92 + inherit user; 93 + group = user; 94 + }; 95 + "${stateDir}/repos".d = { 96 + mode = "0750"; 97 + inherit user; 98 + group = user; 99 + }; 100 + }; 101 + 102 + systemd.services.knot-rs = { 103 + description = "knot git server"; 104 + after = [ "network-online.target" ]; 105 + wants = [ "network-online.target" ]; 106 + wantedBy = [ "multi-user.target" ]; 107 + restartTriggers = [ configFile ]; 108 + startLimitIntervalSec = 60; 109 + startLimitBurst = 5; 110 + serviceConfig = { 111 + User = user; 112 + Group = user; 113 + UMask = "0077"; 114 + WorkingDirectory = stateDir; 115 + EnvironmentFile = config.clan.core.vars.generators.tangled-knot.files."knot.env".path; 116 + ExecStart = "${lib.getExe package} /etc/knot/config.toml"; 117 + Restart = "on-failure"; 118 + RestartSec = 5; 119 + TimeoutStopSec = 120; 120 + LimitNOFILE = 65536; 121 + # without a cgroup limit the knot budgets for MemTotal and refuses 122 + # pushes whose base cache (40% of it) exceeds MemAvailable 123 + MemoryMax = "4G"; 124 + CapabilityBoundingSet = [ ]; 125 + ReadWritePaths = [ 126 + stateDir 127 + "${stateDir}/repos" 128 + ]; 129 + NoNewPrivileges = true; 130 + ProtectProc = "invisible"; 131 + ProtectSystem = "strict"; 132 + ProtectHome = true; 133 + PrivateTmp = true; 134 + PrivateDevices = true; 135 + PrivateUsers = true; 136 + ProtectHostname = true; 137 + ProtectClock = true; 138 + ProtectKernelTunables = true; 139 + ProtectKernelModules = true; 140 + ProtectKernelLogs = true; 141 + ProtectControlGroups = true; 142 + RestrictAddressFamilies = [ 143 + "AF_INET" 144 + "AF_INET6" 145 + "AF_NETLINK" 146 + "AF_UNIX" 147 + ]; 148 + RestrictNamespaces = true; 149 + LockPersonality = true; 150 + MemoryDenyWriteExecute = true; 151 + RestrictRealtime = true; 152 + RestrictSUIDSGID = true; 153 + RemoveIPC = true; 154 + PrivateMounts = true; 155 + SystemCallFilter = [ 156 + "@system-service" 157 + "~@privileged @resources" 158 + ]; 159 + SystemCallArchitectures = "native"; 160 + }; 161 + }; 162 + 163 + networking.firewall.allowedTCPPorts = [ sshPort ]; 164 + 165 + # knot 2 doesn't request crawls (no KNOT_MIRRORS), so subscribe our 166 + # mirror (knotmirror.nix) to the knot's event stream whenever either 167 + # starts; the mirror marks a knot offline after it was unreachable for a 168 + # while 169 + systemd.services.knotmirror-request-crawl = { 170 + wantedBy = [ 171 + "knot-rs.service" 172 + "knotmirror.service" 173 + ]; 174 + after = [ 175 + "knot-rs.service" 176 + "knotmirror.service" 177 + ]; 178 + serviceConfig.Type = "oneshot"; 179 + script = '' 180 + ${pkgs.curl}/bin/curl -fsS --retry 10 --retry-connrefused \ 181 + -H 'Content-Type: application/json' -d '{"hostname":"${domain}"}' \ 182 + http://127.0.0.1:7000/xrpc/sh.tangled.sync.requestCrawl 183 + ''; 184 + }; 185 + 186 + networking.nftables.tables.knot-ssh = { 187 + family = "inet"; 188 + content = '' 189 + chain prerouting { 190 + type nat hook prerouting priority dstnat; policy accept; 191 + ip daddr 95.217.192.59 tcp dport 22 redirect to :${toString sshPort} 192 + ip6 daddr 2a01:4f9:4a:4f1a::2 tcp dport 22 redirect to :${toString sshPort} 193 + } 194 + ''; 195 + }; 196 + 197 + # repos (with the ACL data), sealed keys and ssh host key; the master key 198 + # is in vars 199 + lass.backup.state.tangled-knot.folders = [ stateDir ]; 200 + 201 + services.nginx.virtualHosts.${domain} = { 202 + enableACME = true; 203 + forceSSL = true; 204 + locations."/" = { 205 + proxyPass = "http://${listenAddr}"; 206 + recommendedProxySettings = true; 207 + # /events 208 + proxyWebsockets = true; 209 + extraConfig = '' 210 + proxy_read_timeout 1d; 211 + client_max_body_size 0; 212 + proxy_request_buffering off; 213 + ''; 214 + }; 215 + }; 216 + }
+133
configs/services/git/knotmirror.nix
··· 1 + { 2 + self, 3 + lib, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + domain = "mirror.lassul.us"; 9 + listenAddr = "127.0.0.1:7000"; 10 + tapPort = 7480; 11 + redisPort = 6377; 12 + package = self.packages.${pkgs.stdenv.hostPlatform.system}.tangled.knotmirror; 13 + in 14 + { 15 + # Tangled knotmirror: the appview reads repo contents from here instead of 16 + # mirror.tangled.network. It mirrors a repo the first time the appview asks 17 + # for it (resolving the knot from the repo's DID) and follows the event 18 + # streams of knots that requested a crawl (ours, see knot.nix). Public, 19 + # since the appview links browsers to raw blobs and images on it. Follows 20 + # tangled's nix/modules/knotmirror.nix, without its flake (the module files 21 + # are in a fetched source, importing them would be IFD); tap is nixpkgs'. 22 + 23 + environment.systemPackages = [ 24 + pkgs.git 25 + package 26 + ]; 27 + 28 + services.redis.servers.knotmirror = { 29 + enable = true; 30 + port = redisPort; 31 + }; 32 + 33 + systemd.services.tap-knotmirror = { 34 + description = "knotmirror tap service"; 35 + after = [ "network.target" ]; 36 + wantedBy = [ "multi-user.target" ]; 37 + # outbox only: no firehose subscription to the bsky relay for nothing 38 + environment.TAP_OUTBOX_ONLY = "true"; 39 + serviceConfig = { 40 + # upstream runs it as root 41 + DynamicUser = true; 42 + LogsDirectory = "knotmirror-tap"; 43 + StateDirectory = "knotmirror-tap"; 44 + Environment = [ 45 + "TAP_BIND=:${toString tapPort}" 46 + "TAP_PLC_URL=https://plc.directory" 47 + "TAP_RELAY_URL=https://relay1.us-east.bsky.network" 48 + "TAP_RESYNC_PARALLELISM=10" 49 + "TAP_DATABASE_URL=sqlite:///var/lib/knotmirror-tap/tap.db" 50 + "TAP_RETRY_TIMEOUT=60s" 51 + "TAP_COLLECTION_FILTERS=sh.tangled.repo" 52 + # nothing calls tap's /repos/add without the full network, so it 53 + # tracks no repos 54 + "TAP_FULL_NETWORK=false" 55 + ]; 56 + ExecStart = "${lib.getExe pkgs.tap} run"; 57 + }; 58 + }; 59 + 60 + systemd.services.knotmirror = { 61 + description = "knotmirror service"; 62 + after = [ 63 + "postgresql.target" 64 + "redis-knotmirror.service" 65 + "network.target" 66 + "tap-knotmirror.service" 67 + ]; 68 + requires = [ 69 + "postgresql.target" 70 + "redis-knotmirror.service" 71 + ]; 72 + wantedBy = [ "multi-user.target" ]; 73 + path = [ pkgs.git ]; 74 + # pinged after indexing languages, upstream's is tangled.org 75 + environment.MIRROR_APPVIEW_URL = "https://git.lassul.us"; 76 + serviceConfig = { 77 + # upstream runs it as root; the name matches the postgres role for peer 78 + # auth 79 + DynamicUser = true; 80 + User = "knotmirror"; 81 + LogsDirectory = "knotmirror"; 82 + StateDirectory = "knotmirror"; 83 + Environment = [ 84 + "MIRROR_LISTEN=${listenAddr}" 85 + "MIRROR_HOSTNAME=${domain}" 86 + "MIRROR_TAP_URL=http://localhost:${toString tapPort}" 87 + "MIRROR_DB_URL=postgres:///knotmirror?host=/run/postgresql&user=knotmirror" 88 + "MIRROR_REDIS_ADDR=localhost:${toString redisPort}" 89 + "MIRROR_GIT_BASEPATH=/var/lib/knotmirror/repos" 90 + "MIRROR_KNOT_USE_SSL=true" 91 + "MIRROR_KNOT_SSRF=true" 92 + "MIRROR_RESYNC_PARALLELISM=12" 93 + "MIRROR_METRICS_LISTEN=127.0.0.1:7100" 94 + "MIRROR_ADMIN_LISTEN=127.0.0.1:7200" 95 + "MIRROR_SLURPER_CONCURRENCY=4" 96 + "MIRROR_SEARCH_ZOEKT_URL=" 97 + ]; 98 + ExecStart = "${lib.getExe package} serve"; 99 + Restart = "always"; 100 + }; 101 + }; 102 + 103 + services.postgresql = { 104 + enable = true; 105 + ensureDatabases = [ "knotmirror" ]; 106 + ensureUsers = [ 107 + { 108 + name = "knotmirror"; 109 + ensureDBOwnership = true; 110 + } 111 + ]; 112 + }; 113 + 114 + # no backup: postgres, redis, tap and the repos only cache what the knots 115 + # serve, and the mirror refetches it 116 + 117 + services.nginx.virtualHosts.${domain} = { 118 + enableACME = true; 119 + forceSSL = true; 120 + locations."/" = { 121 + proxyPass = "http://${listenAddr}"; 122 + recommendedProxySettings = true; 123 + # reads only: sh.tangled.sync.requestCrawl (POST) would let anyone 124 + # subscribe us to any knot, ours requests it via localhost 125 + extraConfig = '' 126 + limit_except GET { 127 + deny all; 128 + } 129 + proxy_read_timeout 10m; 130 + ''; 131 + }; 132 + }; 133 + }
+152
configs/services/git/pds.nix
··· 1 + { 2 + self, 3 + config, 4 + pkgs, 5 + ... 6 + }: 7 + let 8 + domain = "pds.lassul.us"; 9 + cfg = config.services.tranquil-pds; 10 + package = self.packages.${pkgs.stdenv.hostPlatform.system}.tranquil-pds; 11 + zitadelEnv = config.lass.zitadel.projects.pds.apps.web.envFile.path; 12 + in 13 + { 14 + # ATProto PDS (Tranquil). Accounts get handles <name>.pds.lassul.us and 15 + # log in via id.lassul.us. Accounts are created only by the SSO signup 16 + # (no invite code), nginx rejects all other account-creation routes. 17 + clan.core.vars.generators.tranquil-pds = { 18 + files."secrets.env" = { }; 19 + runtimeInputs = [ pkgs.openssl ]; 20 + script = '' 21 + for var in JWT_SECRET DPOP_SECRET MASTER_KEY; do 22 + echo "$var=$(openssl rand -hex 32)" 23 + done > "$out/secrets.env" 24 + ''; 25 + }; 26 + 27 + # send-only mailbox on this host's mailserver (configs/mailserver.nix) for 28 + # email verification and password resets 29 + mailserver.loginAccounts."pds@lassul.us" = { 30 + hashedPasswordFile = config.clan.core.vars.generators.mailserver-pds.files."password-hash".path; 31 + sendOnly = true; 32 + }; 33 + 34 + # smarthost.env is an environmentFile of tranquil-pds 35 + clan.core.vars.generators.mailserver-pds = { 36 + files."smarthost.env" = { }; 37 + files."password-hash" = { }; 38 + runtimeInputs = with pkgs; [ 39 + coreutils 40 + mkpasswd 41 + pwgen 42 + ]; 43 + script = '' 44 + password=$(pwgen -s 32 1) 45 + echo "MAIL_SMARTHOST_PASSWORD=$password" > "$out/smarthost.env" 46 + mkpasswd -sm bcrypt <<< "$password" > "$out/password-hash" 47 + ''; 48 + }; 49 + 50 + services.tranquil-pds = { 51 + enable = true; 52 + inherit package; 53 + database.createLocally = true; 54 + environmentFiles = [ 55 + config.clan.core.vars.generators.tranquil-pds.files."secrets.env".path 56 + config.clan.core.vars.generators.mailserver-pds.files."smarthost.env".path 57 + zitadelEnv 58 + ]; 59 + settings = { 60 + server = { 61 + hostname = domain; 62 + invite_code_required = false; 63 + # certs come from nginx, not caddy 64 + enable_caddy_on_demand_tls = false; 65 + }; 66 + frontend.dir = package.frontend; 67 + # owner's PLC recovery key (public part) 68 + secrets.plc_rotation_key = "did:key:zDnaepKZiYEbndTH9NNqoZUa7mvbhdiF3M9g2MCjtnxhijtUS"; 69 + email = { 70 + from_address = "pds@lassul.us"; 71 + from_name = "lassul.us PDS"; 72 + smarthost = { 73 + host = "mail.lassul.us"; 74 + port = 465; 75 + tls = "implicit"; 76 + }; 77 + }; 78 + sso.oidc = { 79 + issuer = "https://id.lassul.us"; 80 + display_name = "lassul.us ID"; 81 + }; 82 + }; 83 + }; 84 + 85 + systemd.services.tranquil-pds = { 86 + wants = [ "zitadel-reconcile.service" ]; 87 + after = [ "zitadel-reconcile.service" ]; 88 + # not in settings: the config file is validated at build time, without 89 + # the secrets from environmentFiles that these require 90 + environment = { 91 + MAIL_SMARTHOST_USERNAME = "pds@lassul.us"; 92 + SSO_OIDC_ENABLED = "true"; 93 + }; 94 + }; 95 + 96 + # only the users listed here get a project grant and can log in 97 + lass.zitadel.projects.pds = { 98 + access = "declarative"; 99 + users = [ "lass@lassul.us" ]; 100 + apps.web = { 101 + redirectUris = [ "https://${domain}/oauth/sso/callback" ]; 102 + authMethod = "post"; 103 + envFile = { 104 + clientIdVariable = "SSO_OIDC_CLIENT_ID"; 105 + clientSecretVariable = "SSO_OIDC_CLIENT_SECRET"; 106 + }; 107 + }; 108 + }; 109 + 110 + # repos, accounts and keys in postgres, blobs on disk 111 + lass.backup.state.tranquil-pds = { 112 + folders = [ cfg.dataDir ]; 113 + preBackup = '' 114 + mkdir -p /var/backup/postgresql 115 + ${pkgs.sudo}/bin/sudo -u postgres ${config.services.postgresql.package}/bin/pg_dump \ 116 + tranquil-pds | ${pkgs.gzip}/bin/gzip > /var/backup/postgresql/tranquil-pds.sql.gz 117 + ''; 118 + }; 119 + 120 + # handles are served /.well-known/atproto-did on <handle>.pds.lassul.us; 121 + # DNS-01 against our own knot, see _acme-challenge.pds.lassul.us in 122 + # configs/dns/knot.nix 123 + security.acme.certs.${domain} = { 124 + extraDomainNames = [ "*.${domain}" ]; 125 + dnsProvider = "rfc2136"; 126 + # recursive resolvers may have cached the *.lassul.us wildcard for 127 + # _acme-challenge.pds, which makes lego pick the lassul.us zone 128 + dnsResolver = "ns1.lassul.us:53"; 129 + environmentFile = config.clan.core.vars.generators.knot-acme-review.files."lego.env".path; 130 + group = config.services.nginx.group; 131 + }; 132 + 133 + services.nginx.virtualHosts.${domain} = { 134 + serverAliases = [ "*.${domain}" ]; 135 + useACMEHost = domain; 136 + forceSSL = true; 137 + locations."/" = { 138 + proxyPass = "http://${cfg.settings.server.host}:${toString cfg.settings.server.port}"; 139 + recommendedProxySettings = true; 140 + # com.atproto.sync.subscribeRepos 141 + proxyWebsockets = true; 142 + extraConfig = '' 143 + proxy_read_timeout 1d; 144 + client_max_body_size 10G; 145 + ''; 146 + }; 147 + # password, passkey, delegated and migrated (service-auth) accounts 148 + locations."= /xrpc/com.atproto.server.createAccount".return = "403"; 149 + locations."= /xrpc/_account.createPasskeyAccount".return = "403"; 150 + locations."= /xrpc/_delegation.createDelegatedAccount".return = "403"; 151 + }; 152 + }

History

3 rounds 0 comments
Sign up or Login to add to the discussion
11 commits
Expand
pkgs.tranquil-pds: init at 0.7.0
configs.dns: _acme-challenge zone for *.pds.lassul.us
configs.services.git: tranquil PDS on neoprism with id.lassul.us login
configs.services.git: resolve the ACME zone via ns1
pkgs.tangled: init at 0-unstable-2026-10-18
configs.services.git: tangled knot and appview on git.lassul.us
configs.services.git: own knotmirror
configs.ssh-redirect: only reject 22 that wasn't redirected elsewhere
configs.services.git: knot owner, git ssh on port 22
configs.services.git: give the knot a memory limit
pkgs.tangled: appview asks knot2 for members by did:web
Checking mergeability…
Expand 0 comments
37 commits
Expand
tools.hermes-voice: headset button starts Assist listening
tools.hermes-voice: serve the page with a query string
tools.hermes-voice: headset hang-up button ends the call
configs.gg23.audio: switch phones left on the off profile to A2DP
configs.gg23.audio: crossed out speaker for routes that are off
modules.zitadel: refreshTokens option for OIDC apps
configs.roundcube: replace snappymail, log in via id.lassul.us
tools.hermes-voice: native Android app answers the headset button
tools.hermes-voice: always delegate action requests
skills.hedgedoc: pad-edit.py usage on --help, refuse empty content
flake.lock: update
modules.backup: per-service backup state, move neoprism paths into their services
modules.backup: encrypted repos in per-machine storage box sub-accounts
machines: backups for coaxmetal, styx, acheron and yellow
vars: update via generator borgbackup (machine: styx)
vars: update via generator borgbackup (machine: acheron)
vars: update via generator borgbackup (machine: yellow)
vars: update via generator borgbackup (machine: coaxmetal)
pkgs.zitadel: init at 4.19.5
configs.zitadel: run zitadel 4.19.5
configs.hass: log in via id.lassul.us (auth_oidc)
configs.hass: only allow the id.lassul.us login
tools.zitadel-invite: init
configs.gg23.audio.room: start pipewire at boot
configs.monitoring: OOMKill ignores data gaps
pkgs.calino: 0.39.1 -> 0.40.0
pkgs.tranquil-pds: init at 0.7.0
configs.dns: _acme-challenge zone for *.pds.lassul.us
configs.services.git: tranquil PDS on neoprism with id.lassul.us login
configs.services.git: resolve the ACME zone via ns1
pkgs.tangled: init at 0-unstable-2026-10-18
configs.services.git: tangled knot and appview on git.lassul.us
configs.services.git: own knotmirror
configs.ssh-redirect: only reject 22 that wasn't redirected elsewhere
configs.services.git: knot owner, git ssh on port 22
configs.services.git: give the knot a memory limit
pkgs.tangled: appview asks knot2 for members by did:web
Expand 0 comments
1 commit
Expand
AGENTS.md: mention PRs from git.lassul.us forks
Expand 0 comments