feat(#2050): set matrix avatar in the daemon over the live client
This commit is contained in:
parent
63e9e80232
commit
c99fa714d9
8 changed files with 118 additions and 210 deletions
|
|
@ -61,6 +61,15 @@ let
|
|||
# cannot drift). Matches the daemon's own built-in default
|
||||
# (`paths::DEFAULT_HOMESERVER`).
|
||||
matrixUrlDefault = "http://localhost:8008";
|
||||
# Rasterize the operator-set agent icon (`hyperhive.icon`, an SVG) to a
|
||||
# 512x512 PNG so the matrix daemon can upload it as each account's avatar
|
||||
# over the live authenticated Client (see hive-matrix-mcp::client::sync_avatar).
|
||||
# Replaces the old `matrix-avatar-sync` curl oneshot. Only forced when an
|
||||
# icon is configured — the `HIVE_ICON_PNG` daemon-env entry is gated on
|
||||
# `hyperhive.icon != null`, so this binding stays lazy when no icon is set.
|
||||
iconPng = pkgs.runCommand "hive-agent-icon.png" { nativeBuildInputs = [ pkgs.librsvg ]; } ''
|
||||
rsvg-convert -f png -w 512 -h 512 ${config.hyperhive.icon} -o $out
|
||||
'';
|
||||
in
|
||||
{
|
||||
# Shared scaffolding for every hyperhive harness container.
|
||||
|
|
@ -406,7 +415,7 @@ in
|
|||
daemon auto-skips when `<state>/matrix-token` is missing,
|
||||
and a `systemd.paths` watcher restarts it the moment
|
||||
hive-c0re provisions the token (same path-trigger shape
|
||||
as `matrix-avatar-sync`).
|
||||
as `forge-avatar-sync`).
|
||||
- exposes the matrix tool surface (send_message, send_dm,
|
||||
send_reaction, send_reply, mark_read, list_rooms,
|
||||
list_room_members, read_room) to claude via an auto-injected
|
||||
|
|
@ -1433,8 +1442,8 @@ in
|
|||
};
|
||||
|
||||
# Path-trigger sibling: re-fires forge-avatar-sync the moment
|
||||
# `<state>/forge-token` appears. Mirrors the matrix-avatar-sync
|
||||
# pattern — on first agent deployment the container boots before
|
||||
# `<state>/forge-token` appears. Mirrors the hive-matrix-daemon
|
||||
# token-watcher pattern — on first agent deployment the container boots before
|
||||
# hive-c0re has provisioned the forge-token, so the service fires
|
||||
# too early and exits with "no forge-token found". Without this path
|
||||
# unit, RemainAfterExit=true would prevent systemd from ever
|
||||
|
|
@ -1553,6 +1562,12 @@ in
|
|||
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
|
||||
) config.hyperhive.matrixAccounts
|
||||
);
|
||||
}
|
||||
# Rasterized agent icon path for the daemon's avatar sync. Only set
|
||||
# when an icon is configured; absent → the daemon skips avatar setting
|
||||
# (hive-matrix-mcp::client::sync_avatar returns early on unset env).
|
||||
// lib.optionalAttrs (config.hyperhive.icon != null) {
|
||||
HIVE_ICON_PNG = "${iconPng}";
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon";
|
||||
|
|
@ -1642,166 +1657,6 @@ in
|
|||
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*";
|
||||
};
|
||||
|
||||
# Path-trigger sibling: re-fires matrix-avatar-sync the moment a
|
||||
# token appears - both the hive-internal `matrix-token` and any
|
||||
# dashboard-provisioned extra `matrix-token-<name>` (so an external
|
||||
# account logged in after boot gets its avatar without a restart).
|
||||
# Same first-boot-ordering pattern as hive-matrix-daemon above.
|
||||
systemd.paths.matrix-avatar-sync = {
|
||||
description = "trigger matrix-avatar-sync when a matrix token appears";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
# Single `matrix-token*` glob (same as hive-matrix-daemon above) so
|
||||
# both the hive-internal `matrix-token` and any dashboard-provisioned
|
||||
# `matrix-token-<name>` re-fire the sync. A list value here is
|
||||
# unverified for `systemd.paths.*.pathConfig`, and the wildcard
|
||||
# covers both cases anyway.
|
||||
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*";
|
||||
};
|
||||
|
||||
# One-shot: hyperhive.icon → matrix profile avatar (two-step media
|
||||
# upload + set avatar_url). Shape contract:
|
||||
# docs/conventions.md::Best-effort oneshot services. Protocol +
|
||||
# why RemainAfterExit = false:
|
||||
# docs/persistence.md::matrix-avatar-sync.
|
||||
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 = false so the .path trigger can re-fire
|
||||
# the unit (see docs/persistence.md::matrix-avatar-sync).
|
||||
RemainAfterExit = false;
|
||||
# Pin the journal identity (else it's the `script` store-path wrapper).
|
||||
SyslogIdentifier = "matrix-avatar-sync";
|
||||
};
|
||||
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
|
||||
# Hash-based idempotency: skip the upload if the icon hasn't
|
||||
# changed since the last successful sync. Every upload mints a
|
||||
# new mxc:// URI which triggers a profile state event in every
|
||||
# joined room — uploading the same bytes again produces timeline
|
||||
# spam without changing the visible avatar. Hash files live in
|
||||
# $HYPERHIVE_STATE_DIR (survives restart, wiped on purge so
|
||||
# purge + re-provision gets a fresh upload). One hash file per
|
||||
# account (the mxc:// URI is homeserver-scoped, so each account
|
||||
# uploads to its own homeserver independently). Delete to force
|
||||
# a re-upload.
|
||||
CURRENT_HASH=$(sha256sum "$ICON" | cut -d' ' -f1)
|
||||
|
||||
# Rasterize SVG → PNG ONCE; the same bytes are reused for every
|
||||
# account (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
|
||||
|
||||
# sync_one <token-file> <matrix-url> <hash-suffix> <label>
|
||||
# Two-step media upload + avatar_url set for a single account.
|
||||
# Best-effort: every failure path logs + returns 0 so one bad
|
||||
# account never aborts the others.
|
||||
sync_one() {
|
||||
token_file=$1
|
||||
matrix_url=$2
|
||||
hash_suffix=$3
|
||||
label=$4
|
||||
if [ ! -f "$token_file" ]; then
|
||||
return 0
|
||||
fi
|
||||
hash_file="$HYPERHIVE_STATE_DIR/matrix-avatar-icon-hash$hash_suffix"
|
||||
if [ -f "$hash_file" ] && [ "$(cat "$hash_file" 2>/dev/null)" = "$CURRENT_HASH" ]; then
|
||||
echo "matrix-avatar-sync[$label]: icon unchanged (hash matches); skipping"
|
||||
return 0
|
||||
fi
|
||||
token=$(cat "$token_file")
|
||||
# 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[$label]: whoami failed or homeserver unreachable; skipping"
|
||||
return 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)
|
||||
if [ -z "$mxc" ]; then
|
||||
echo "matrix-avatar-sync[$label]: media upload failed; skipping"
|
||||
return 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[$label]: avatar set on $user_id"
|
||||
# Persist hash so subsequent runs skip the upload when the
|
||||
# icon hasn't changed.
|
||||
echo "$CURRENT_HASH" > "$hash_file"
|
||||
else
|
||||
echo "matrix-avatar-sync[$label]: avatar PUT returned HTTP $code — skipping (non-fatal)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Hive-internal `main` account: bare `matrix-token`, written by
|
||||
# `hive-c0re::matrix::ensure_user_for` to the agent's
|
||||
# bind-mounted state dir. $HYPERHIVE_STATE_DIR is set system-wide
|
||||
# by the meta flake (systemd.globalEnvironment) to
|
||||
# `/agents/<name>/state`. Local tuwunel reachable on the shared
|
||||
# host netns at the default matrix-spec port.
|
||||
sync_one "$HYPERHIVE_STATE_DIR/matrix-token" "http://localhost:8008" "" "main"
|
||||
|
||||
# Dashboard-provisioned extra accounts: each is a
|
||||
# `matrix-token-<name>` file plus a `matrix-account-<name>.json`
|
||||
# sidecar carrying its (possibly external) homeserver. Mirrors
|
||||
# hive-matrix-mcp::accounts::discover_token_accounts — a token
|
||||
# without a sidecar is skipped because the homeserver is unknown.
|
||||
for token_file in "$HYPERHIVE_STATE_DIR"/matrix-token-*; do
|
||||
[ -f "$token_file" ] || continue
|
||||
name=''${token_file##*/matrix-token-}
|
||||
[ -n "$name" ] || continue
|
||||
sidecar="$HYPERHIVE_STATE_DIR/matrix-account-$name.json"
|
||||
if [ ! -f "$sidecar" ]; then
|
||||
echo "matrix-avatar-sync[$name]: no homeserver sidecar; skipping"
|
||||
continue
|
||||
fi
|
||||
homeserver=$(jq -r '.homeserver // empty' "$sidecar" 2>/dev/null || true)
|
||||
if [ -z "$homeserver" ]; then
|
||||
echo "matrix-avatar-sync[$name]: empty homeserver in sidecar; skipping"
|
||||
continue
|
||||
fi
|
||||
sync_one "$token_file" "$homeserver" "-$name" "$name"
|
||||
done
|
||||
|
||||
rm -f "$PNG"
|
||||
'';
|
||||
};
|
||||
|
||||
# Write declared dashboardLinks to the state dir so hive-c0re can
|
||||
# read them without accessing the container's /etc/ from the host.
|
||||
# Best-effort oneshot (always exit 0):
|
||||
|
|
|
|||
Loading…
Reference in a new issue