Initial commit: autonomous maintenance agent for semprini stack
systemd-deployed daemon that drives the semprini-core stack-support agent headless via Claude Code, monitors health via Uptime Kuma, applies safe upgrades, and escalates to the operator over Matrix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# register-matrix-bot.sh — provision the Matrix bot account the maintenance
|
||||
# agent uses to talk to the operator, and a private room shared with the admin.
|
||||
#
|
||||
# Idempotent-ish: re-running re-uses the existing account (login still works if
|
||||
# the password is unchanged) and creates a fresh room only if one isn't set.
|
||||
#
|
||||
# Requirements: run on the core-stack host, with the `user-synapse` container
|
||||
# running. Uses the registration shared secret already in the synapse config.
|
||||
#
|
||||
# Usage:
|
||||
# ./register-matrix-bot.sh # uses config.json next to this script
|
||||
# CONFIG=/path/config.json ./register-matrix-bot.sh
|
||||
set -euo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
CONFIG="${CONFIG:-$DIR/config.json}"
|
||||
SECRETS="$DIR/.bot-secrets" # gitignored: stores the bot password
|
||||
|
||||
if [ ! -f "$CONFIG" ]; then
|
||||
echo "ERROR: $CONFIG not found. Copy config.example.json to config.json first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jq_get() { python3 -c "import json,sys; print(json.load(open('$CONFIG'))$1)"; }
|
||||
|
||||
HOMESERVER="$(jq_get "['matrix']['homeserver']")"
|
||||
BOT_USER_ID="$(jq_get "['matrix']['user_id']")"
|
||||
ADMIN_USER_ID="$(jq_get "['matrix']['admin_user_id']")"
|
||||
SYNAPSE_CONTAINER="${SYNAPSE_CONTAINER:-user-synapse}"
|
||||
|
||||
# localpart from @maintainer:semprini.me -> maintainer
|
||||
BOT_LOCAL="${BOT_USER_ID#@}"; BOT_LOCAL="${BOT_LOCAL%%:*}"
|
||||
|
||||
echo "→ Homeserver : $HOMESERVER"
|
||||
echo "→ Bot : $BOT_USER_ID"
|
||||
echo "→ Admin : $ADMIN_USER_ID"
|
||||
|
||||
# 1. Password — reuse if we created one before, else generate and persist.
|
||||
if [ -f "$SECRETS" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$SECRETS"
|
||||
fi
|
||||
if [ -z "${BOT_PASSWORD:-}" ]; then
|
||||
BOT_PASSWORD="$(openssl rand -hex 32)"
|
||||
umask 077; echo "BOT_PASSWORD=$BOT_PASSWORD" > "$SECRETS"
|
||||
echo "→ Generated new bot password (saved to $SECRETS)"
|
||||
fi
|
||||
|
||||
# 2. Create the account (no-op if it already exists).
|
||||
echo "→ Registering account in $SYNAPSE_CONTAINER…"
|
||||
if docker exec "$SYNAPSE_CONTAINER" register_new_matrix_user \
|
||||
-u "$BOT_LOCAL" -p "$BOT_PASSWORD" --no-admin \
|
||||
-c /data/homeserver.yaml http://localhost:8008 2>&1 | tee /tmp/reg.out; then
|
||||
:
|
||||
fi
|
||||
grep -qiE "already taken|User ID already" /tmp/reg.out && \
|
||||
echo " (account already exists — continuing)"
|
||||
|
||||
# 3. Log in to obtain an access token.
|
||||
echo "→ Logging in…"
|
||||
LOGIN_JSON="$(curl -fsS -X POST "$HOMESERVER/_matrix/client/v3/login" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"type\":\"m.login.password\",
|
||||
\"identifier\":{\"type\":\"m.id.user\",\"user\":\"$BOT_LOCAL\"},
|
||||
\"password\":\"$BOT_PASSWORD\",
|
||||
\"initial_device_display_name\":\"semprini-maintainer\"}")"
|
||||
ACCESS_TOKEN="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['access_token'])" "$LOGIN_JSON")"
|
||||
[ -n "$ACCESS_TOKEN" ] || { echo "ERROR: no access token"; exit 1; }
|
||||
echo " got access token"
|
||||
|
||||
# 4. Ensure a private room exists with the admin invited.
|
||||
ROOM_ID="$(jq_get "['matrix'].get('room_id','')" 2>/dev/null || echo "")"
|
||||
case "$ROOM_ID" in
|
||||
""|FILLED_BY_*|!*) : ;;
|
||||
esac
|
||||
if [[ "$ROOM_ID" != \!* ]]; then
|
||||
echo "→ Creating maintenance room and inviting $ADMIN_USER_ID…"
|
||||
ROOM_JSON="$(curl -fsS -X POST "$HOMESERVER/_matrix/client/v3/createRoom" \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"name\":\"Stack Maintenance\",
|
||||
\"topic\":\"semprini-core autonomous stack maintenance\",
|
||||
\"preset\":\"trusted_private_chat\",
|
||||
\"is_direct\":true,
|
||||
\"invite\":[\"$ADMIN_USER_ID\"]}")"
|
||||
ROOM_ID="$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['room_id'])" "$ROOM_JSON")"
|
||||
echo " room: $ROOM_ID"
|
||||
else
|
||||
echo "→ Re-using existing room $ROOM_ID"
|
||||
fi
|
||||
|
||||
# 5. Write token + room back into config.json.
|
||||
python3 - "$CONFIG" "$ACCESS_TOKEN" "$ROOM_ID" <<'PY'
|
||||
import json, sys
|
||||
path, token, room = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
cfg = json.load(open(path))
|
||||
cfg.setdefault("matrix", {})
|
||||
cfg["matrix"]["access_token"] = token
|
||||
cfg["matrix"]["room_id"] = room
|
||||
json.dump(cfg, open(path, "w"), indent=2)
|
||||
print(f" wrote access_token + room_id to {path}")
|
||||
PY
|
||||
|
||||
echo
|
||||
echo "✓ Done."
|
||||
echo " ACTION REQUIRED (one-time): in Element (chat.semprini.me) as"
|
||||
echo " $ADMIN_USER_ID, accept the invite to the 'Stack Maintenance' room."
|
||||
Reference in New Issue
Block a user