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
|
|
@ -165,9 +165,8 @@ Tree-shape version:
|
||||||
`systemd.globalEnvironment` in `meta.rs` for every container
|
`systemd.globalEnvironment` in `meta.rs` for every container
|
||||||
(manager included), so all token/state paths resolve through
|
(manager included), so all token/state paths resolve through
|
||||||
`$HYPERHIVE_STATE_DIR`. The harness-base shell scripts
|
`$HYPERHIVE_STATE_DIR`. The harness-base shell scripts
|
||||||
(tea-login, forge-avatar-sync, matrix-avatar-sync) simplified
|
(tea-login, forge-avatar-sync) simplified from glob+for loops to a
|
||||||
from glob+for loops to a direct `$HYPERHIVE_STATE_DIR/<token>`
|
direct `$HYPERHIVE_STATE_DIR/<token>` read.
|
||||||
read.
|
|
||||||
|
|
||||||
### E — prompt + tools
|
### E — prompt + tools
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -474,10 +474,10 @@ the state dir at provisioning time:
|
||||||
- `forge-avatar-sync` — uploads `hyperhive.icon` SVG to the
|
- `forge-avatar-sync` — uploads `hyperhive.icon` SVG to the
|
||||||
agent's Forgejo profile, so the icon shows up on commits / PRs /
|
agent's Forgejo profile, so the icon shows up on commits / PRs /
|
||||||
issue comments.
|
issue comments.
|
||||||
- `matrix-avatar-sync` — same idea for matrix profile avatars
|
|
||||||
(two-step `media upload` → `set avatar_url` dance — see
|
(The matrix profile avatar is **not** a oneshot — `hive-matrix-daemon`
|
||||||
`docs/persistence.md::matrix-avatar-sync` for the protocol
|
sets it over its live authenticated Client; see
|
||||||
detail).
|
`docs/persistence.md::matrix avatar`.)
|
||||||
|
|
||||||
Shape contract — every one of these:
|
Shape contract — every one of these:
|
||||||
|
|
||||||
|
|
@ -498,7 +498,7 @@ Shape contract — every one of these:
|
||||||
5. **Re-runnable**: a second invocation produces the same final
|
5. **Re-runnable**: a second invocation produces the same final
|
||||||
state (idempotent uploads, idempotent config rewrites). Used
|
state (idempotent uploads, idempotent config rewrites). Used
|
||||||
by the `.path` watchers that re-fire on token appearance (see
|
by the `.path` watchers that re-fire on token appearance (see
|
||||||
`docs/persistence.md::matrix-avatar-sync`).
|
`docs/persistence.md::Matrix per-agent daemon`).
|
||||||
|
|
||||||
The artefact lives under the agent user's home where applicable
|
The artefact lives under the agent user's home where applicable
|
||||||
(`~/.config/tea/config.yml`) and is chown'd to that user, but the
|
(`~/.config/tea/config.yml`) and is chown'd to that user, but the
|
||||||
|
|
|
||||||
|
|
@ -396,39 +396,35 @@ agent containers come up. Without the path-trigger sibling
|
||||||
first time it ran and the MCP would have no backend until the next
|
first time it ran and the MCP would have no backend until the next
|
||||||
restart. The `.path` unit makes the appearance of the token re-fire
|
restart. The `.path` unit makes the appearance of the token re-fire
|
||||||
the service so the daemon comes alive in the same boot cycle as
|
the service so the daemon comes alive in the same boot cycle as
|
||||||
provisioning. `matrix-avatar-sync.path` uses the same pattern for
|
provisioning. The same token watcher also drives avatar setting: on a
|
||||||
the icon-upload oneshot.
|
restart the daemon re-runs each account's bring-up, which sets the
|
||||||
|
avatar (see below).
|
||||||
|
|
||||||
### matrix-avatar-sync (two-step media + profile dance)
|
### matrix avatar (set by the daemon over the live Client)
|
||||||
|
|
||||||
Mirrors the forge-avatar oneshot's shape (`docs/conventions.md::
|
The agent icon (`hyperhive.icon`, an SVG) is published as each matrix
|
||||||
Best-effort oneshot services`) but differs in protocol: matrix
|
account's profile avatar by `hive-matrix-daemon` itself
|
||||||
avatars are a two-step `POST /media/r0/upload` → `PUT
|
(`hive-matrix-mcp::client::sync_avatar`), not a separate oneshot. After
|
||||||
/profile/<user_id>/avatar_url` dance, both authenticated by the
|
the daemon builds + restores an account's `Client` (authenticated,
|
||||||
`access_token` written by `hive-c0re::matrix::ensure_user_for` to
|
pointed at that account's resolved homeserver), it calls matrix-sdk's
|
||||||
`<state>/matrix-token`.
|
`account().upload_avatar()` — one call that uploads the media and sets
|
||||||
|
`avatar_url`. Because it reuses the live Client, there is no hardcoded
|
||||||
|
homeserver URL, no token re-read, and no token-file globbing: the daemon
|
||||||
|
already iterates every configured + dashboard-discovered account in its
|
||||||
|
bring-up loop, so the avatar is set for **every** account.
|
||||||
|
|
||||||
The oneshot syncs the avatar for **every** account, not just the
|
Nix rasterizes the SVG to a 512x512 PNG at build time (`iconPng`, via
|
||||||
hive-internal one: a `sync_one` shell function runs the upload + set
|
librsvg) and forwards its store path as `HIVE_ICON_PNG` on the daemon
|
||||||
dance for the bare `matrix-token` (`main`, against `localhost:8008`)
|
unit, gated on `hyperhive.icon != null`. No icon configured → the env is
|
||||||
and then for each dashboard-provisioned `matrix-token-<name>`, reading
|
unset → `sync_avatar` returns early and no avatar is set.
|
||||||
that account's homeserver from its `matrix-account-<name>.json` sidecar
|
|
||||||
(mirrors `accounts::discover_token_accounts`). Idempotency is
|
|
||||||
**per-account**: a `matrix-avatar-icon-hash[-<name>]` file each, because
|
|
||||||
the `mxc://` URI is homeserver-scoped so every account uploads to its own
|
|
||||||
homeserver independently.
|
|
||||||
|
|
||||||
Triggered by EITHER boot (`wantedBy = multi-user.target`) OR the
|
Idempotency is **per-account**: an `avatar-icon-hash` file in each
|
||||||
sibling `matrix-avatar-sync.path` firing on token appearance — its glob
|
account's matrix-sdk `state_dir`. The daemon hashes the PNG bytes and
|
||||||
covers both `matrix-token` and `matrix-token-*`, so an account logged in
|
skips the upload when unchanged, because every upload mints a fresh
|
||||||
after boot gets its avatar without a restart. Both paths re-run the
|
`mxc://` URI that emits a profile state event in every joined room —
|
||||||
oneshot idempotently — running the avatar set twice is harmless.
|
re-uploading identical bytes is timeline spam. A dashboard-provisioned
|
||||||
|
account gets its avatar when the `systemd.paths.hive-matrix-daemon` token
|
||||||
Critically: **`RemainAfterExit = false`** (not the more common
|
watcher restarts the daemon (which re-runs the per-account bring-up), so
|
||||||
`true` for oneshots). systemd treats `RemainAfterExit = true`
|
no separate avatar trigger is needed. Avatar failures are swallowed
|
||||||
oneshots as "still running" after the first exit, so the second
|
(logged, non-fatal) so they never break account bring-up or sync.
|
||||||
trigger from the `.path` watcher becomes a no-op. Setting it to
|
|
||||||
`false` lets re-fires actually re-execute. The trade-off is the
|
|
||||||
service unit shows `inactive (dead)` between fires — visible in
|
|
||||||
`journalctl` but harmless; the `.path` unit drives the lifecycle.
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,9 +317,9 @@ hyperhive.icon = ./icon.svg; # default: null (falls back to shared hyperhive lo
|
||||||
Path to an SVG file used as this agent's visual identity — shown in
|
Path to an SVG file used as this agent's visual identity — shown in
|
||||||
the per-agent page header, as the page favicon, and uploaded to the
|
the per-agent page header, as the page favicon, and uploaded to the
|
||||||
agent's Forgejo profile avatar (via the `forge-avatar-sync` boot
|
agent's Forgejo profile avatar (via the `forge-avatar-sync` boot
|
||||||
unit) and Matrix profile avatar (via `matrix-avatar-sync`). Commit
|
unit) and Matrix profile avatar (set by `hive-matrix-daemon` over its
|
||||||
the SVG next to `agent.nix` in the config repo and reference it as a
|
live Client). Commit the SVG next to `agent.nix` in the config repo
|
||||||
relative path.
|
and reference it as a relative path.
|
||||||
|
|
||||||
When `null` (the default), the agent falls back to the shared
|
When `null` (the default), the agent falls back to the shared
|
||||||
hyperhive branding mark. The harness serves whichever icon is active
|
hyperhive branding mark. The harness serves whichever icon is active
|
||||||
|
|
|
||||||
|
|
@ -1011,7 +1011,7 @@ where
|
||||||
# `environment.variables` only writes /etc/environment (login
|
# `environment.variables` only writes /etc/environment (login
|
||||||
# shells); `systemd.globalEnvironment` is the analogue for
|
# shells); `systemd.globalEnvironment` is the analogue for
|
||||||
# systemd units so tea-login / forge-avatar-sync /
|
# systemd units so tea-login / forge-avatar-sync /
|
||||||
# matrix-avatar-sync etc. can read `$HYPERHIVE_STATE_DIR`
|
# hive-matrix-daemon etc. can read `$HYPERHIVE_STATE_DIR`
|
||||||
# without each service having to redeclare it.
|
# without each service having to redeclare it.
|
||||||
environment.variables = {
|
environment.variables = {
|
||||||
HIVE_LABEL = name;
|
HIVE_LABEL = name;
|
||||||
|
|
|
||||||
|
|
@ -184,3 +184,57 @@ async fn whoami(homeserver: &str, token: &str) -> Result<(OwnedUserId, OwnedDevi
|
||||||
let device_id: OwnedDeviceId = device_id_raw.into();
|
let device_id: OwnedDeviceId = device_id_raw.into();
|
||||||
Ok((user_id, device_id))
|
Ok((user_id, device_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Best-effort: set this account's matrix avatar from the rasterized icon
|
||||||
|
/// PNG. The path comes from `HIVE_ICON_PNG` (a nix-built derivation the
|
||||||
|
/// harness forwards into the daemon env; absent when no icon resolves).
|
||||||
|
/// Idempotent via a per-account `avatar-icon-hash` file in the account's
|
||||||
|
/// sdk `state_dir` — a re-upload only happens when the icon bytes change,
|
||||||
|
/// so daemon restarts don't re-spam the homeserver.
|
||||||
|
///
|
||||||
|
/// This replaces the old `matrix-avatar-sync` systemd curl oneshot: the
|
||||||
|
/// daemon already holds an authenticated `Client` pointed at the correct
|
||||||
|
/// homeserver, so it uploads over the live connection — no hardcoded URL,
|
||||||
|
/// no token re-read, no token-file globbing. Any failure is logged and
|
||||||
|
/// swallowed: avatar trouble must never break account bring-up or sync.
|
||||||
|
pub async fn sync_avatar(client: &Client, state_dir: &Path, account: &str) {
|
||||||
|
let Ok(png_path) = std::env::var("HIVE_ICON_PNG") else {
|
||||||
|
return; // no icon forwarded → nothing to sync
|
||||||
|
};
|
||||||
|
let bytes = match fs::read(&png_path).await {
|
||||||
|
Ok(b) => b,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(account, path = %png_path, error = %e, "matrix avatar: icon PNG unreadable; skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// A non-crypto content hash is enough to answer "did the icon change?".
|
||||||
|
let hash = {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
bytes.hash(&mut h);
|
||||||
|
format!("{:016x}", h.finish())
|
||||||
|
};
|
||||||
|
let hash_file = state_dir.join("avatar-icon-hash");
|
||||||
|
if let Ok(prev) = fs::read_to_string(&hash_file).await
|
||||||
|
&& prev.trim() == hash
|
||||||
|
{
|
||||||
|
tracing::debug!(account, "matrix avatar: icon unchanged; skipping upload");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match client
|
||||||
|
.account()
|
||||||
|
.upload_avatar(&mime::IMAGE_PNG, bytes)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(mxc) => {
|
||||||
|
tracing::info!(account, mxc = %mxc, "matrix avatar set");
|
||||||
|
if let Err(e) = fs::write(&hash_file, &hash).await {
|
||||||
|
tracing::warn!(account, error = %e, "matrix avatar: set ok but hash write failed (re-uploads next start)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(account, error = %format!("{e:#}"), "matrix avatar: upload failed; skipping (non-fatal)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,10 @@ async fn bring_up_account(
|
||||||
client::build_and_restore(&homeserver, &cfg.token_file, &cfg.state_dir, is_primary)
|
client::build_and_restore(&homeserver, &cfg.token_file, &cfg.state_dir, is_primary)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("build matrix client for account {}", cfg.name))?;
|
.with_context(|| format!("build matrix client for account {}", cfg.name))?;
|
||||||
|
// Best-effort: sync the agent icon to this account's matrix avatar over
|
||||||
|
// the live (authenticated, correct-homeserver) Client. Replaces the old
|
||||||
|
// curl oneshot; failures are swallowed inside sync_avatar.
|
||||||
|
client::sync_avatar(&client, &cfg.state_dir, &cfg.name).await;
|
||||||
timeline::install_message_handler(&client, hyperhive_socket.to_path_buf(), tag.clone());
|
timeline::install_message_handler(&client, hyperhive_socket.to_path_buf(), tag.clone());
|
||||||
|
|
||||||
let sync_client = client.clone();
|
let sync_client = client.clone();
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,15 @@ let
|
||||||
# cannot drift). Matches the daemon's own built-in default
|
# cannot drift). Matches the daemon's own built-in default
|
||||||
# (`paths::DEFAULT_HOMESERVER`).
|
# (`paths::DEFAULT_HOMESERVER`).
|
||||||
matrixUrlDefault = "http://localhost:8008";
|
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
|
in
|
||||||
{
|
{
|
||||||
# Shared scaffolding for every hyperhive harness container.
|
# Shared scaffolding for every hyperhive harness container.
|
||||||
|
|
@ -406,7 +415,7 @@ in
|
||||||
daemon auto-skips when `<state>/matrix-token` is missing,
|
daemon auto-skips when `<state>/matrix-token` is missing,
|
||||||
and a `systemd.paths` watcher restarts it the moment
|
and a `systemd.paths` watcher restarts it the moment
|
||||||
hive-c0re provisions the token (same path-trigger shape
|
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,
|
- exposes the matrix tool surface (send_message, send_dm,
|
||||||
send_reaction, send_reply, mark_read, list_rooms,
|
send_reaction, send_reply, mark_read, list_rooms,
|
||||||
list_room_members, read_room) to claude via an auto-injected
|
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
|
# Path-trigger sibling: re-fires forge-avatar-sync the moment
|
||||||
# `<state>/forge-token` appears. Mirrors the matrix-avatar-sync
|
# `<state>/forge-token` appears. Mirrors the hive-matrix-daemon
|
||||||
# pattern — on first agent deployment the container boots before
|
# token-watcher pattern — on first agent deployment the container boots before
|
||||||
# hive-c0re has provisioned the forge-token, so the service fires
|
# hive-c0re has provisioned the forge-token, so the service fires
|
||||||
# too early and exits with "no forge-token found". Without this path
|
# too early and exits with "no forge-token found". Without this path
|
||||||
# unit, RemainAfterExit=true would prevent systemd from ever
|
# unit, RemainAfterExit=true would prevent systemd from ever
|
||||||
|
|
@ -1553,6 +1562,12 @@ in
|
||||||
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
|
// lib.optionalAttrs (a.homeserver != null) { inherit (a) homeserver; }
|
||||||
) config.hyperhive.matrixAccounts
|
) 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 = {
|
serviceConfig = {
|
||||||
ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon";
|
ExecStart = "${pkgs.hyperhive}/bin/hive-matrix-daemon";
|
||||||
|
|
@ -1642,166 +1657,6 @@ in
|
||||||
pathConfig.PathExistsGlob = "/agents/*/state/matrix-token*";
|
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
|
# Write declared dashboardLinks to the state dir so hive-c0re can
|
||||||
# read them without accessing the container's /etc/ from the host.
|
# read them without accessing the container's /etc/ from the host.
|
||||||
# Best-effort oneshot (always exit 0):
|
# Best-effort oneshot (always exit 0):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue