harness-base: matrix-avatar-sync oneshot mirroring forge avatar (#548 phase 2.5)

This commit is contained in:
damocles 2026-05-29 12:34:24 +02:00
commit 7e041e6079

View file

@ -745,6 +745,102 @@
'';
};
# One-shot: upload the agent's configured icon to its matrix profile
# avatar so the icon shows up next to messages in matrix rooms (#548
# phase 2.5). Mirrors the forge-avatar-sync flow above, only differs
# in protocol: matrix avatars are a two-step `media upload` → `set
# avatar_url` dance, both authenticated by the access_token written
# by hive-c0re's `matrix::ensure_user_for`. No-op when the icon
# isn't configured, the matrix token isn't present, or the
# homeserver isn't reachable. *Always* exits 0.
systemd.services.matrix-avatar-sync = {
description = "sync agent icon to matrix profile avatar (best-effort)";
wantedBy = [ "multi-user.target" ];
# No `after = [ "tea-login.service" ]` — matrix has no
# equivalent prerequisite; we just need the homeserver up.
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
};
path = [
pkgs.curl
pkgs.coreutils
pkgs.jq
pkgs.librsvg
];
script = ''
ICON=/etc/hyperhive/icon.svg
if [ ! -f "$ICON" ]; then
echo "matrix-avatar-sync: no icon configured; skipping"
exit 0
fi
# Token written by `hive-c0re::matrix::ensure_user_for` to the
# agent's bind-mounted state dir. Either appears at the legacy
# `/state/` (manager) or per-agent `/agents/<name>/state/` path.
TOKEN_FILE=""
for f in /state/matrix-token /agents/*/state/matrix-token; do
if [ -f "$f" ]; then
TOKEN_FILE="$f"
break
fi
done
if [ -z "$TOKEN_FILE" ]; then
echo "matrix-avatar-sync: no matrix-token found; skipping"
exit 0
fi
TOKEN=$(cat "$TOKEN_FILE")
# Local tuwunel reachable on shared host netns at the
# default matrix-spec port. Override via the future
# `hyperhive.matrix.url` if the operator ever runs the
# homeserver elsewhere (deferred to #548 phase 4).
MATRIX_URL=http://localhost:8008
# whoami → user_id. Needed to scope the avatar set call.
# Tolerant of the homeserver being unreachable (`-f` makes
# curl fail on 4xx/5xx; `|| true` swallows the exit).
USER_ID=$(curl -sf --max-time 5 \
-H "Authorization: Bearer $TOKEN" \
"$MATRIX_URL/_matrix/client/v3/account/whoami" 2>/dev/null \
| jq -r '.user_id // empty' || true)
if [ -z "$USER_ID" ]; then
echo "matrix-avatar-sync: whoami failed or homeserver unreachable; skipping"
exit 0
fi
# Rasterize SVG → PNG (matrix media accepts any image type
# but we already standardise on PNG for the forge sync).
PNG=$(mktemp --suffix=.png)
if ! rsvg-convert -f png -w 512 -h 512 "$ICON" -o "$PNG" 2>/dev/null; then
echo "matrix-avatar-sync: rsvg-convert failed; skipping"
rm -f "$PNG"
exit 0
fi
# Step 1: upload bytes → mxc:// URI.
MXC=$(curl -sf --max-time 10 \
-X POST "$MATRIX_URL/_matrix/media/v3/upload" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: image/png" \
--data-binary "@$PNG" 2>/dev/null \
| jq -r '.content_uri // empty' || true)
rm -f "$PNG"
if [ -z "$MXC" ]; then
echo "matrix-avatar-sync: media upload failed; skipping"
exit 0
fi
# Step 2: set avatar_url on the profile.
PAYLOAD=$(jq -n --arg url "$MXC" '{avatar_url:$url}')
CODE=$(curl -s --max-time 10 \
-X PUT "$MATRIX_URL/_matrix/client/v3/profile/$USER_ID/avatar_url" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
-o /dev/null -w "%{http_code}" 2>/dev/null || true)
if [ "$CODE" = "200" ]; then
echo "matrix-avatar-sync: avatar set on $USER_ID"
else
echo "matrix-avatar-sync: avatar PUT returned HTTP $CODE skipping (non-fatal)"
fi
'';
};
# Write declared dashboardLinks to the state dir so hive-c0re can read
# them without accessing the container's /etc/ from the host.
# Runs every boot; idempotent (overwrite). Always exits 0.