From e0cfed7fe84d601b60685bee45f5be31bd4943e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Mon, 13 Jul 2026 22:14:25 +0200 Subject: [PATCH] refactor: jq for tea-login, build-time avatar png, shared leaf-sign script --- nix/agent-modules/forge.nix | 38 +++++++-------- nix/host-modules/hive-tls.nix | 91 ++++++++++++++++------------------- 2 files changed, 59 insertions(+), 70 deletions(-) diff --git a/nix/agent-modules/forge.nix b/nix/agent-modules/forge.nix index 5619fb34..202ffa2c 100644 --- a/nix/agent-modules/forge.nix +++ b/nix/agent-modules/forge.nix @@ -10,6 +10,13 @@ let userName = config.hyperhive.user.name; homeDir = "/home/${userName}"; + # Same 512×512 rasterization of the agent icon the matrix avatar + # sync uses (./matrix.nix — identical derivation, same store path). + # Only forced when an icon is configured (the avatar-sync unit below + # is gated on `hyperhive.icon != null`). + iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } '' + rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out + ''; in { options.hyperhive.forge.url = lib.mkOption { @@ -65,7 +72,7 @@ in }; path = [ pkgs.curl - pkgs.python3 + pkgs.jq pkgs.coreutils ]; environment.HOME_DIR = homeDir; @@ -84,8 +91,7 @@ in USER=$(curl -sf --max-time 5 \ -H "Authorization: token $TOKEN" \ "$FORGE_URL/api/v1/user" \ - | python3 -c 'import sys,json; print(json.load(sys.stdin).get("login",""))' \ - 2>/dev/null || true) + | jq -r '.login // empty' 2>/dev/null || true) if [ -z "$USER" ]; then echo "tea-login: could not resolve username from forge API; skipping" exit 0 @@ -123,7 +129,7 @@ in # Without this path unit, RemainAfterExit=true would prevent systemd # from ever re-running the service. See # docs/persistence.md::forge-avatar-sync. - systemd.paths.forge-avatar-sync = { + systemd.paths.forge-avatar-sync = lib.mkIf (config.hyperhive.icon != null) { description = "trigger forge-avatar-sync when forge-token appears"; wantedBy = [ "multi-user.target" ]; pathConfig.PathExistsGlob = "/agents/*/state/forge-token"; @@ -132,8 +138,12 @@ in # One-shot: hyperhive.icon → Forgejo profile avatar. Shape contract: # docs/conventions.md::Best-effort oneshot services. # RemainAfterExit = false so the .path trigger above can re-fire - # this unit when the forge-token arrives after boot. - systemd.services.forge-avatar-sync = { + # this unit when the forge-token arrives after boot. The PNG is + # rasterized at build time (`iconPng`, shared shape with the matrix + # avatar sync), so the unit only exists when an icon is configured + # and needs no librsvg at runtime — Forgejo's Go image library + # can't decode SVG, hence PNG. + systemd.services.forge-avatar-sync = lib.mkIf (config.hyperhive.icon != null) { description = "sync agent icon to Forgejo user avatar (best-effort)"; wantedBy = [ "multi-user.target" ]; after = [ "tea-login.service" ]; @@ -147,14 +157,8 @@ in pkgs.curl pkgs.coreutils pkgs.jq - pkgs.librsvg ]; script = '' - ICON=/etc/hyperhive/icon.svg - if [ ! -f "$ICON" ]; then - echo "forge-avatar-sync: no icon configured; skipping" - exit 0 - fi FORGE_URL=${lib.escapeShellArg config.hyperhive.forge.url} # $HYPERHIVE_STATE_DIR is set system-wide by the meta flake # (systemd.globalEnvironment) to `/agents//state`. @@ -164,15 +168,7 @@ in exit 0 fi TOKEN=$(cat "$TOKEN_FILE") - # Rasterize SVG → PNG (Forgejo's Go image library can't decode SVG). - PNG=$(mktemp --suffix=.png) - if ! rsvg-convert -f png -w 512 -h 512 "$ICON" -o "$PNG" 2>/dev/null; then - echo "forge-avatar-sync: rsvg-convert failed; skipping" - rm -f "$PNG" - exit 0 - fi - IMAGE=$(base64 -w 0 < "$PNG") - rm -f "$PNG" + IMAGE=$(base64 -w 0 < ${iconPng}) # Forgejo POST /user/avatar expects {"image":""} — just the # raw base64 string, NOT a data URI (data:image/png;base64,...). # Use jq to build the payload so the large base64 value is safely quoted. diff --git a/nix/host-modules/hive-tls.nix b/nix/host-modules/hive-tls.nix index 2d3d4bea..765887a4 100644 --- a/nix/host-modules/hive-tls.nix +++ b/nix/host-modules/hive-tls.nix @@ -18,6 +18,46 @@ let # module's single source of truth (`gateway.useSelfSigned`): true when # neither an operator cert (`tls.certDir`) nor ACME is set. active = hyperhiveCfg.enable && gatewayCfg.useSelfSigned; + + # The leaf-signing action shared by the boot-time `hive-tls-ca` + # generation and the weekly `hive-tls-resign` renewal: fresh key + + # CSR, SAN ext-file, sign under the (stable) CA, tighten modes. + # Takes the TLS state dir as `$1`; each caller keeps its own + # when-to-sign condition. The leaf covers the bare hive domain plus + # `forge.`, `matrix.` and `*.` so all sub-domains validate + # under the same cert + the hive CA. + signLeafScript = pkgs.writeShellScript "hive-tls-sign-leaf" '' + set -euo pipefail + d="$1" + ca="$d/ca.pem" + cak="$d/ca-key.pem" + leaf="$d/gateway.pem" + leafk="$d/gateway-key.pem" + csr="$(mktemp "$d/gateway.csr.XXXXXX")" + ext="$(mktemp "$d/leaf.ext.XXXXXX")" + trap 'rm -f "$csr" "$ext"' EXIT + + openssl req -newkey rsa:4096 -nodes -sha256 \ + -keyout "$leafk" -out "$csr" \ + -subj "/CN=${domain}" + + # printf (not a heredoc) so the ext-file lines carry no leading + # whitespace once nix has stripped the indented-string indent. + { + printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \ + ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \ + ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} + printf 'basicConstraints=critical,CA:FALSE\n' + printf 'keyUsage=critical,digitalSignature,keyEncipherment\n' + printf 'extendedKeyUsage=serverAuth\n' + } > "$ext" + + openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \ + -CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \ + -extfile "$ext" -out "$leaf" + chmod 0600 "$leafk" + chmod 0644 "$leaf" + ''; in { # Host-side TLS trust root for the self-signed gateway mode. @@ -132,30 +172,7 @@ in if [ ! -s "$leaf" ] || [ ! -s "$leafk" ] \ || ! openssl x509 -in "$leaf" -noout -checkend 2592000 >/dev/null 2>&1; then echo "signing fresh gateway leaf at $leaf" - csr="$(mktemp "$d/gateway.csr.XXXXXX")" - ext="$(mktemp "$d/leaf.ext.XXXXXX")" - trap 'rm -f "$csr" "$ext"' EXIT - - openssl req -newkey rsa:4096 -nodes -sha256 \ - -keyout "$leafk" -out "$csr" \ - -subj "/CN=${domain}" - - # printf (not a heredoc) so the ext-file lines carry no leading - # whitespace once nix has stripped the indented-string indent. - { - printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \ - ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \ - ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} - printf 'basicConstraints=critical,CA:FALSE\n' - printf 'keyUsage=critical,digitalSignature,keyEncipherment\n' - printf 'extendedKeyUsage=serverAuth\n' - } > "$ext" - - openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \ - -CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \ - -extfile "$ext" -out "$leaf" - chmod 0600 "$leafk" - chmod 0644 "$leaf" + ${signLeafScript} "$d" fi ''; }; @@ -200,10 +217,7 @@ in script = '' set -euo pipefail d=${lib.escapeShellArg cfg.stateDir} - ca="$d/ca.pem" - cak="$d/ca-key.pem" leaf="$d/gateway.pem" - leafk="$d/gateway-key.pem" # Re-sign only when the leaf is within half its validity of expiry. # The weekly cadence catches this window well before the leaf lapses. @@ -217,28 +231,7 @@ in echo "gateway leaf missing or near expiry — re-signing under current CA" before="$(sha256sum "$leaf" 2>/dev/null || true)" - csr="$(mktemp "$d/gateway.csr.XXXXXX")" - ext="$(mktemp "$d/leaf.ext.XXXXXX")" - trap 'rm -f "$csr" "$ext"' EXIT - - openssl req -newkey rsa:4096 -nodes -sha256 \ - -keyout "$leafk" -out "$csr" \ - -subj "/CN=${domain}" - - { - printf 'subjectAltName=DNS:%s,DNS:forge.%s,DNS:matrix.%s,DNS:*.%s\n' \ - ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} \ - ${lib.escapeShellArg domain} ${lib.escapeShellArg domain} - printf 'basicConstraints=critical,CA:FALSE\n' - printf 'keyUsage=critical,digitalSignature,keyEncipherment\n' - printf 'extendedKeyUsage=serverAuth\n' - } > "$ext" - - openssl x509 -req -in "$csr" -CA "$ca" -CAkey "$cak" \ - -CAcreateserial -days ${toString cfg.leafValidityDays} -sha256 \ - -extfile "$ext" -out "$leaf" - chmod 0600 "$leafk" - chmod 0644 "$leaf" + ${signLeafScript} "$d" after="$(sha256sum "$leaf" 2>/dev/null || true)" if [ "$before" != "$after" ]; then