From b2878b05dd526ec71bf1b057a4b4521b63049dd0 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Mon, 2 Nov 2026 21:38:13 -0500 Subject: [PATCH] feat(macos): Firefox quotidien pilotable par Hermes (launchd + MCP connectExisting) --- modules/home-mac/firefox-hermes/default.nix | 42 +++++++++++ profiles/macos/configuration.nix | 4 + services/firefox-mac-mcp/default.nix | 84 +++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 modules/home-mac/firefox-hermes/default.nix create mode 100644 services/firefox-mac-mcp/default.nix diff --git a/modules/home-mac/firefox-hermes/default.nix b/modules/home-mac/firefox-hermes/default.nix new file mode 100644 index 0000000..a9fb256 --- /dev/null +++ b/modules/home-mac/firefox-hermes/default.nix @@ -0,0 +1,42 @@ +# Firefox du Mac pilotable par Hermes (module nix-darwin, importé par +# profiles/macos/configuration.nix). +# +# Firefox (profil quotidien de marc, session et cookies conservés) est lancé +# par launchd avec Marionette (127.0.0.1:2828) et WebDriver BiDi +# (127.0.0.1:9222). Le serveur MCP « firefox-mac » de tower +# (services/firefox-mac-mcp) s'y attache en --connectExisting à travers ssh : +# l'instance survit aux sessions Hermes, la musique continue après la fin +# d'une conversation. LibreWolf reste le navigateur des sites sensibles. +# +# - Firefox reste l'app impérative de /Applications (auto-update Mozilla, +# binaire universel arm64) ; seul le lancement est déclaratif. +# - Si Firefox tourne déjà SANS ces flags (lancé depuis le Dock, réouverture +# des fenêtres à la connexion), le wrapper attend qu'il soit quitté puis +# prend le relais : jamais deux instances sur le même profil. +# - KeepAlive : un Cmd+Q relance Firefox (avec les flags) après ~30 s. +# Arrêt réel : launchctl bootout gui/$(id -u)/org.nixos.firefox-hermes +# - Effets visibles du mode piloté : icône robot et barre d'adresse rayée, +# navigator.webdriver=true (certains anti-bots sont plus méfiants). +# - Sécurité : ports en loopback seulement, mais sans authentification : tout +# process local du Mac peut piloter ce Firefox. Accepté par marc. +{pkgs, ...}: let + firefox = "/Applications/Firefox.app/Contents/MacOS/firefox"; + wrapper = pkgs.writeShellScript "firefox-hermes" '' + while /usr/bin/pgrep -f '^${firefox}( |$)' >/dev/null; do + sleep 15 + done + exec ${firefox} --marionette --remote-debugging-port 9222 + ''; +in { + launchd.user.agents.firefox-hermes = { + serviceConfig = { + ProgramArguments = ["${wrapper}"]; + RunAtLoad = true; + KeepAlive = true; + ThrottleInterval = 30; + ProcessType = "Interactive"; + StandardOutPath = "/tmp/firefox-hermes.log"; + StandardErrorPath = "/tmp/firefox-hermes.err"; + }; + }; +} diff --git a/profiles/macos/configuration.nix b/profiles/macos/configuration.nix index f3a994d..17d8dcf 100644 --- a/profiles/macos/configuration.nix +++ b/profiles/macos/configuration.nix @@ -43,6 +43,10 @@ in { # sur 127.0.0.1:2223 du VPS, pour que tower (ou marc) joigne le Mac hors # LAN. La clé du Mac est déjà autorisée pour root@rack. ../../modules/home-mac/autossh + + # Firefox quotidien lancé avec Marionette/BiDi en loopback : piloté par + # le MCP firefox-mac de tower (services/firefox-mac-mcp). + ../../modules/home-mac/firefox-hermes ]; environment.systemPackages = [ diff --git a/services/firefox-mac-mcp/default.nix b/services/firefox-mac-mcp/default.nix new file mode 100644 index 0000000..95eb8bd --- /dev/null +++ b/services/firefox-mac-mcp/default.nix @@ -0,0 +1,84 @@ +# services/firefox-mac-mcp/default.nix +# Serveur MCP "firefox-mac" : le navigateur du MAC de marc, piloté par Hermes +# (qui tourne sur tower). Même serveur officiel Mozilla que services/firefox-mcp +# (mozilla/firefox-devtools-mcp 0.10.4), mais exécuté LÀ-BAS via ssh : le MCP +# parle stdio, le wrapper exec simplement `ssh mac ...`, et geckodriver + +# Selenium WebDriver BiDi lancent le LibreWolf du Mac localement (le driver ne +# peut pas lancer un navigateur d'une autre machine, le process vit donc côté +# Mac). +# +# == Depuis le 02/11/2026 : Firefox quotidien en --connectExisting == +# Le serveur ne lance PLUS de navigateur : il s'attache au Firefox de marc, +# démarré par launchd avec --marionette (2828) --remote-debugging-port 9222 +# (module nix-darwin modules/home-mac/firefox-hermes). Raisons : session et +# cookies conservés (LibreWolf les purge à la fermeture), instance qui +# survit aux sessions Hermes (musique YouTube), une seule instance partagée. +# Vérifié sur un Firefox jetable : navigate/list_pages OK, deux connexions +# successives OK, Firefox toujours vivant après la sortie du serveur. +# Marionette n'accepte qu'UN client à la fois : deux sessions Hermes +# simultanées se gênent. Les notes LibreWolf ci-dessous sont historiques. +# +# == Côté Mac (impératif, PAS géré par ce flake) == +# hosts/macos a le module homebrew désactivé, ces deux outils vivent hors nix : +# brew install geckodriver # 0.37.1 (25/10/2026) +# npm install -g @mozilla/firefox-devtools-mcp@0.10.4 # node brew v25.2.1 +# Mise à jour : `brew upgrade geckodriver` + `npm install -g +# @mozilla/firefox-devtools-mcp@latest` (mettre à jour la version ci-dessus). +# +# == Décisions vérifiées le 25/10/2026 == +# - geckodriver accepte /Applications/LibreWolf.app/Contents/MacOS/librewolf : +# version 145.0.1 lue depuis le bundle, pas de piège application.ini comme +# sur le binaire unwrapped nix de tower. Session W3C créée puis fermée OK. +# - --firefoxArg -no-remote : le LibreWolf PERSO de marc tourne en permanence +# sur le Mac (profil ~/Library/Application Support/librewolf). Sans +# -no-remote, une 2e invocation du même binaire risque d'être absorbée par +# l'instance existante. Avec, l'instance d'automation démarre à côté sur son +# profil dédié et le perso n'est jamais touché (vérifié : navigate_page + +# take_snapshot, LibreWolf perso intact, zéro process résiduel après sortie). +# - PAS de --headless : le but est que marc VOIE la fenêtre sur son écran. +# Pour la discrétion, ajouter --headless à remoteCmd. +# - Profil d'automation dédié et persistant : +# /Users/marc/.firefox-mcp-mac/profile/firefox_devtools_mcp_profile +# (sous-dossier créé par le serveur au 1er lancement). Login manuel une fois +# par site : ouvrir CE profil dans un LibreWolf visible (fermer l'instance +# d'automation d'abord, un seul process par profil), se connecter, fermer : +# /Applications/LibreWolf.app/Contents/MacOS/librewolf --no-remote \ +# --profile /Users/marc/.firefox-mcp-mac/profile/firefox_devtools_mcp_profile +# Les cookies restent ensuite disponibles pour l'automation. +# +# == Transport == +# stdio à travers ssh, alias de l'utilisateur hermes (cf ~/.ssh/config) : +# maclan d'abord (LAN direct, rapide), repli mac (tunnel inverse via rack, +# marche depuis n'importe quel réseau). BatchMode strict : clé id_ed25519 +# seulement, aucune interaction possible. Le repli ne se déclenche que si la +# connexion à maclan échoue, aucune lecture de stdin n'a lieu avant connexion. +{ + pkgs, + lib, + ... +}: let + remoteCmd = lib.concatStringsSep " " [ + "export PATH=/opt/homebrew/bin:$PATH;" + "exec /opt/homebrew/bin/firefox-devtools-mcp" + "--connectExisting" + "--marionettePort 2828" + ]; + + ssh = "${pkgs.openssh}/bin/ssh"; + + wrapper = pkgs.writeShellScript "firefox-mac-mcp" '' + HOST_CMD=${lib.escapeShellArg remoteCmd} + SSH=${ssh} + if "$SSH" -T -o BatchMode=yes -o ConnectTimeout=8 -o LogLevel=ERROR -o ServerAliveInterval=30 -o ServerAliveCountMax=6 maclan "$HOST_CMD"; then + exit 0 + fi + exec "$SSH" -T -o BatchMode=yes -o ConnectTimeout=15 -o LogLevel=ERROR -o ServerAliveInterval=30 -o ServerAliveCountMax=6 mac "$HOST_CMD" + ''; +in { + services.hermes-agent.mcpServers.firefox-mac = { + command = "${wrapper}"; + # 1er appel d'outil = démarrage du navigateur (profil + handshake BiDi) via + # ssh ; le défaut de 120s est trop court pour le pire cas. + timeout = 300; + }; +}