feat(hermes-mobile): backend de l'appli mobile sur le profil default

services/hermes-pocket devient services/hermes-mobile : le backend de l'appli F-Droid sert le profil default (meme historique, memoire, skills et modele que le gateway Matrix et le dashboard) au lieu du profil isole pocket, en gardant la porte d'auth native (mobile-serve.env). Le nom de ressource Pangolin reste hermes-pocket : le renommer recreerait une ressource sur un domaine deja pris.
This commit is contained in:
Hermes Agent
2026-10-24 15:08:11 -04:00
parent 6ee385ce93
commit f875aaf9de
3 changed files with 44 additions and 32 deletions
+1 -1
View File
@@ -40,7 +40,7 @@
./hermes
./hermes-webui
./hermes-dashboard
./hermes-pocket
./hermes-mobile
./discord-bot
# ./discord-hermes-bridge # inutile : un seul gateway par hote, celui du
# profil default sert deja le profil discord-bridge (l'unite faisait
+1 -1
View File
@@ -50,7 +50,7 @@ in {
# Proxy local : le backend refuse toute requête dont le Host diffère de
# l'adresse sur laquelle il a bindé (défense anti-DNS-rebinding), d'où la
# réécriture. Même mécanique que services/hermes-pocket.
# réécriture. Même mécanique que services/hermes-mobile.
services.nginx.virtualHosts."hermes-dashboard-proxy" = {
listen = [
{
@@ -1,31 +1,37 @@
# services/hermes-pocket/default.nix
# Backend de l'appli mobile Hermes Pocket : `hermes serve --isolated` sur le
# profil « pocket », instance séparée du gateway Matrix (hermes-agent) et du
# dashboard web (hermes-dashboard).
# services/hermes-mobile/default.nix
# Backend de l'appli mobile « Hermes Mobile » (F-Droid, com.m57.hermescontrol) :
# `hermes serve --isolated` sur le profil DEFAULT de tower, donc le MÊME agent
# que le gateway Matrix (hermes-agent), le dashboard (hermes-dashboard) et les
# sessions du desktop. Ce module remplace l'ancien backend sur le profil isolé
# « pocket » (services/hermes-pocket) : l'appli mobile voit désormais
# l'historique, la mémoire, les skills et le modèle du profil default au lieu
# d'un profil séparé.
#
# Chaîne : Pangolin (hermes-mobile.marcpartensky.com, SSO DÉSACTIVÉ)
# -> newt (site tower)
# -> 127.0.0.1:9129 (nginx, réécrit Host: 127.0.0.1:9119)
# -> 127.0.0.1:9119 (hermes serve --isolated, profil pocket).
# -> 127.0.0.1:9119 (hermes serve --isolated, HERMES_HOME default).
#
# Depuis le 19/10/2026, la porte d'auth native du serve est ACTIVE : serve.env
# déclare dashboard.public_url (HERMES_DASHBOARD_PUBLIC_URL), ce qui engage
# auth_required=true même sur un bind loopback (doctrine upstream : derrière
# un proxy, le token de session local ne doit jamais être servi sans auth).
# Sans cette porte, le serve headless servait la page racine avec
# window.__HERMES_SESSION_TOKEN__ en clair ; publiée par Pangolin (SSO
# désactivé), elle exposait le token de session au monde entier. Fuite fermée
# et token rotaté le 19/10/2026.
# Pourquoi --isolated : le serveur « unifié » de la machine est déjà celui du
# dashboard (127.0.0.1:9120). Sans --isolated, `hermes serve` refuse de démarrer
# (exit 78, « this host is already served by PID … »). --isolated crée un backend
# dédié, comme le fait Hermes Desktop pour ses connexions SSH. Plusieurs backends
# sur le même state.db sont normaux : c'est déjà le cas du gateway, du dashboard
# et des backends SSH du desktop.
#
# Porte d'auth native ACTIVE : mobile-serve.env (0600, hors store nix) déclare
# HERMES_DASHBOARD_PUBLIC_URL=https://hermes-mobile.marcpartensky.com +
# HERMES_DASHBOARD_BASIC_AUTH_* ; un public_url non-loopback engage
# auth_required=true même sur un bind loopback (doctrine upstream). Sans cette
# porte, le serve headless servirait la page racine avec
# window.__HERMES_SESSION_TOKEN__ en clair derrière une ressource Pangolin sans
# SSO : fuite du token de session (incident du 19/10/2026, cf. historique pocket).
#
# Flux client (appli « Hermes Mobile » F-Droid, mode Username & Password) :
# POST /auth/password-login (provider basic, identifiants dans serve.env)
# POST /auth/password-login (provider basic, identifiants dans mobile-serve.env)
# -> cookies de session -> POST /api/auth/ws-ticket
# -> ws(s)://<domaine>/api/ws?ticket=<billet> (usage unique, TTL 30 s).
#
# Notes de la période NON gardée (27/09/2026) : le token n'était accepté qu'en
# paramètre de requête (Bearer refusé), et le serveur refuse tout Host non
# loopback (anti DNS-rebinding), d'où la réécriture Host par nginx comme dans
# hermes-webui et hermes-dashboard.
# Ne jamais ouvrir 9119 ou 9129 au firewall : le serveur expose l'API complète
# de l'agent (outils, fichiers, terminal) ; la porte d'auth protège l'accès.
{
@@ -33,14 +39,17 @@
lib,
...
}: let
profileDir = "/var/lib/hermes/.hermes/profiles/pocket";
# Profil default : /var/lib/hermes/.hermes (config Nix-managed, marqueur
# .managed écrit par l'activation NixOS, donc les écritures de config depuis
# l'appli sont refusées comme depuis le dashboard).
profileDir = "/var/lib/hermes/.hermes";
backendPort = 9119;
proxyPort = 9129;
backendUrl = "http://127.0.0.1:${toString backendPort}";
backendHost = "127.0.0.1:${toString backendPort}";
in {
systemd.services.hermes-pocket-serve = {
description = "Hermes serve (profil pocket) : backend de l'appli mobile Hermes Pocket";
systemd.services.hermes-mobile-serve = {
description = "Hermes serve (profil default) : backend de l'appli mobile Hermes Mobile";
wantedBy = ["multi-user.target"];
after = ["network.target"];
serviceConfig = {
@@ -50,19 +59,18 @@ in {
# même cwd que la session d'origine de l'appli (cf. README du projet)
WorkingDirectory = "/var/lib/hermes/workspace";
# HERMES_HOME pointe sur le profil, et --isolated empêche le rattachement
# au serveur unifié de la machine (qui est celui du dashboard).
# HERMES_HOME = profil default, et --isolated pour ne pas entrer en
# conflit avec le backend machine du dashboard.
Environment = [
"HERMES_HOME=${profileDir}"
"HERMES_REAL_HOME=/var/lib/hermes"
];
# .env = clés de provider du profil ; serve.env = réglages de la porte
# d'auth (HERMES_DASHBOARD_PUBLIC_URL, HERMES_DASHBOARD_BASIC_AUTH_*) et
# token de session legacy — les deux hors du store nix.
# .env = clés de provider du profil default ; mobile-serve.env = réglages
# de la porte d'auth (public_url, basic auth). Les deux hors du store nix.
EnvironmentFile = [
"${profileDir}/.env"
"${profileDir}/serve.env"
"${profileDir}/mobile-serve.env"
];
ExecStart = "${config.services.hermes-agent.package}/bin/hermes serve --isolated --port ${toString backendPort} --host 127.0.0.1";
@@ -75,7 +83,7 @@ in {
# Proxy local : seul point d'entrée loopback ; il réécrit le Host que le
# serveur impose, laisse passer l'Upgrade WebSocket, et annonce au backend
# https + l'IP cliente réelle (cookies Secure, audit) via X-Forwarded-*.
services.nginx.virtualHosts."hermes-pocket-proxy" = {
services.nginx.virtualHosts."hermes-mobile-proxy" = {
listen = [
{
addr = "127.0.0.1";
@@ -98,6 +106,10 @@ in {
# Ressource publique Pangolin, appliquée par le site qui porte le blueprint
# (donc tower) : `site` est omis volontairement, cf. services/newt.
# Le nom de ressource « hermes-pocket » est conservé volontairement : c'est
# l'identité existante côté Pangolin pour ce domaine, la renommer ferait
# créer une seconde ressource sur un domaine déjà pris. Seul le backend
# derrière a changé (profil default).
# Pas de bloc `auth` : c'est le seul moyen de laisser l'appli native se
# connecter (SSO navigateur impossible), exactement comme la ressource
# jellyfin. La cible reste loopback, seul nginx est publié.
@@ -134,11 +146,11 @@ in {
assertions = [
{
assertion = !(lib.elem backendPort config.networking.firewall.allowedTCPPorts);
message = "hermes-pocket : le port ${toString backendPort} (API agent complète, protégée par un simple token) ne doit pas être ouvert au firewall.";
message = "hermes-mobile : le port ${toString backendPort} (API agent complète, protégée par une porte d'auth) ne doit pas être ouvert au firewall.";
}
{
assertion = !(lib.elem proxyPort config.networking.firewall.allowedTCPPorts);
message = "hermes-pocket : le port ${toString proxyPort} (proxy du backend) ne doit pas être ouvert au firewall.";
message = "hermes-mobile : le port ${toString proxyPort} (proxy du backend) ne doit pas être ouvert au firewall.";
}
];
}