feat(matrix): appservice marc-agent pour un token E2EE sur @marc

Stub HTTP + registration synapse + login unique. Registration sous
/var/lib/matrix-marc-agent-as (StateDirectory) : sous /var/lib/hermes/.matrix
(0700) synapse ne pouvait pas la lire, et ReadWritePaths sur un dossier absent
faisait echouer l'unite en 226/NAMESPACE, ce qui bloquait le switch.
This commit is contained in:
Hermes Agent
2026-10-06 21:39:48 -04:00
parent 3d2c987b96
commit 1ea50aa6ab
2 changed files with 150 additions and 0 deletions
+1
View File
@@ -26,6 +26,7 @@
./matrix-discord
./matrix-meta
./matrix-linkedin
./matrix-marc-agent
# ./matrix-telegram # attendre api_id/api_hash de my.telegram.org
./navidrome
./jellyfin
+149
View File
@@ -0,0 +1,149 @@
# services/matrix-marc-agent/default.nix
# Appservice minimal pour que l'agent hermes obtienne, UNE SEULE FOIS, un token
# Matrix LIE A UN APPAREIL sur le compte de @marc. C'est le seul moyen de
# publier des cles E2EE et donc de lire/ecrire dans les salons chiffres
# (ponts WhatsApp/Signal/Discord/Instagram/Messenger/LinkedIn + DM chiffres).
# Contexte complet : skill hermes-matrix-access et
# /var/lib/hermes/workspace/matrix-marc/README.md (outil marcmsg qui consomme
# le token produit ici).
#
# Effet de bord assume (valide avec marc) : tant que cette registration est
# chargee par synapse, TOUS les evenements des salons ou @marc est membre sont
# pousses vers notre stub (namespace users non exclusif, large par construction).
# Le stub ne fait que repondre 200 a tout, sans rien dechiffrer ni journaliser :
# aucune charge utile n'est conservee ici.
{pkgs, ...}: let
# Registration hors de /var/lib/hermes : synapse (groupe hermes) doit pouvoir
# la lire, or /var/lib/hermes/.matrix est en 0700 (non traversable par le
# groupe). StateDirectory cree le dossier avant le namespace : avec
# ReadWritePaths sur un dossier absent, l'unite echouait en 226/NAMESPACE.
dataDir = "/var/lib/matrix-marc-agent-as";
appservicePort = 29340;
registrationFile = "${dataDir}/registration.yaml";
tokenDir = "/var/lib/hermes/.matrix/marc-agent";
tokenFile = "${tokenDir}/token";
stubServer = pkgs.writeText "matrix-marc-agent-stub.py" ''
import http.server
import socketserver
PORT = ${toString appservicePort}
class Handler(http.server.BaseHTTPRequestHandler):
def _ok(self):
body = b"{}"
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _drain(self):
length = int(self.headers.get("Content-Length", 0) or 0)
if length:
self.rfile.read(length)
def do_GET(self):
self._ok()
def do_PUT(self):
self._drain()
self._ok()
def do_POST(self):
self._drain()
self._ok()
def log_message(self, fmt, *args):
pass
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
with Server(("127.0.0.1", PORT), Handler) as httpd:
httpd.serve_forever()
'';
in {
systemd.services.matrix-marc-agent-as = {
description = "Stub appservice HTTP (encaisse les pushs synapse pour la registration marc-agent)";
wantedBy = ["multi-user.target"];
before = ["matrix-synapse.service"];
preStart = ''
if [ ! -f '${registrationFile}' ]; then
AS_TOKEN=$(${pkgs.openssl}/bin/openssl rand -hex 32)
HS_TOKEN=$(${pkgs.openssl}/bin/openssl rand -hex 32)
umask 0137
cat > '${registrationFile}' <<EOF
id: hermes-marc-agent
url: http://127.0.0.1:${toString appservicePort}
as_token: $AS_TOKEN
hs_token: $HS_TOKEN
sender_localpart: hermes-marc-agent-bot
rate_limited: false
namespaces:
users:
- exclusive: false
regex: '@marc:matrix\.marcpartensky\.com'
aliases: []
rooms: []
EOF
chmod 640 '${registrationFile}'
fi
'';
serviceConfig = {
Type = "simple";
User = "hermes";
Group = "hermes";
ExecStart = "${pkgs.python3}/bin/python3 ${stubServer}";
Restart = "on-failure";
RestartSec = 10;
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
StateDirectory = "matrix-marc-agent-as";
StateDirectoryMode = "0750";
};
};
# Login unique (ConditionPathExists saute l'etape si le token existe deja) :
# authentifie l'appservice (m.login.application_service) pour obtenir un
# access_token+device sur @marc, consomme ensuite par marcmsg.
systemd.services.matrix-marc-agent-login = {
description = "Login appservice -> token lie a un appareil sur @marc (une seule fois)";
after = ["matrix-synapse.service" "matrix-marc-agent-as.service"];
requires = ["matrix-synapse.service"];
wantedBy = ["multi-user.target"];
unitConfig.ConditionPathExists = "!${tokenFile}";
path = [pkgs.curl pkgs.yq pkgs.coreutils];
serviceConfig = {
Type = "oneshot";
User = "hermes";
Group = "hermes";
};
script = ''
set -euo pipefail
for _ in 1 2 3 4 5 6 7 8 9 10; do
[ -f '${registrationFile}' ] && break
sleep 1
done
AS_TOKEN=$(yq -r '.as_token' '${registrationFile}')
mkdir -p '${tokenDir}'
chmod 700 '${tokenDir}'
RESP=$(curl -sf -X POST http://127.0.0.1:8008/_matrix/client/v3/login \
-H "Authorization: Bearer $AS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"type":"m.login.application_service","identifier":{"type":"m.id.user","user":"@marc:matrix.marcpartensky.com"},"device_id":"HERMESMARC01","initial_device_display_name":"hermes-agent-e2ee"}')
TOKEN=$(printf '%s' "$RESP" | yq -r '.access_token')
test -n "$TOKEN"
test "$TOKEN" != "null"
printf '%s' "$TOKEN" > '${tokenFile}'
chmod 400 '${tokenFile}'
'';
};
services.matrix-synapse.settings.app_service_config_files = [registrationFile];
systemd.services.matrix-synapse.serviceConfig.SupplementaryGroups = ["hermes"];
systemd.services.matrix-synapse.after = ["matrix-marc-agent-as.service"];
systemd.services.matrix-synapse.wants = ["matrix-marc-agent-as.service"];
}