Skip to main content
Retour au blog
Claude CodeAIAutomationMonitoringlaunchdResilience

system-droid : un gardien qui vérifie et répare mes systèmes tout seul

Mes services locaux mouraient en silence : le brief du matin n'arrivait pas, les jetons expiraient, la sauvegarde se figeait. Alors j'ai construit un droïde sur launchd qui vérifie tout toutes les deux heures, répare via un agent Claude ce qui peut l'être sans risque, et ne m'écrit que lorsqu'il faut vraiment un humain.

Publié 17 août 202610 min de lecture

TL;DR

system-droid est un petit programme en Go que launchd lance toutes les deux heures et après chaque réveil du Mac. Il déroule une liste de vérifications sur tous mes services locaux, reste silencieux tant que tout fonctionne, et n'écrit sur Telegram que lorsque quelque chose est réellement cassé. Une fois par jour, il envoie un bref résumé « tout va bien », pour que le silence ne signifie jamais « inconnu ». Et quand quelque chose tombe, il essaie d'abord de réparer lui-même : un agent Claude (Opus) dédié remonte à la cause et la corrige — il ne m'appelle que s'il échoue.

◆ Усі системи в нормі · 10/10 · 2026-08-17 09:00

— OpenClaw / Resonance
◆ styletts2-ua (tts) — локальний український TTS :8123
◆ openclaw gateway (:18789) — ядро агента Resonance (launchd + порт)
◆ openclaw hooks — hooks-ендпоінт увімкнено

— Інфраструктура
◆ cli-proxy Claude auth — робочий Claude-токен (валідність, авто-оновлення)
◆ system-droid — сам вартовий (надсилає ці звіти)
◆ otel-block — корпоративна телеметрія заблокована у firewall
◆ vault git backup — git-копія Obsidian-сховища на GitHub

Pourquoi tout cela

Ces derniers mois, j'avais accumulé sans m'en rendre compte tout un parc d'automatisation locale : un agent OpenClaw (« Resonance »), un serveur vocal ukrainien StyleTTS2, un proxy LLM devant Claude, des générateurs quotidiens de notes pour Obsidian, une copie git de mon coffre, une règle de pare-feu bloquant la télémétrie d'entreprise. Chacune de ces briques est utile exactement tant qu'elle tourne. Et elles cassaient toujours de la même façon — en silence.

Le brief du matin n'arrivait tout simplement pas, et je ne le remarquais que le soir. Le planificateur tuait un processus, mais sa dernière exécution paraissait toujours « réussie ». Un jeton d'authentification expirait, et la génération pouvait vivre des semaines sur un texte de secours. Aucune de ces pannes ne se signale : un service ne se plaint pas — il disparaît. Il me fallait quelqu'un pour le remarquer à ma place.

Comment c'est fait

Tout le système tient dans un binaire et un config.json avec deux types de vérifications : HTTP (le service répond sur /health) et commande (un script se termine avec le code 0). launchd lance le droïde selon l'horaire ; celui-ci exécute les vérifications en parallèle et décide s'il y a même lieu de me déranger. Mettre un nouveau système sous surveillance, c'est une ligne dans la configuration — sans recompilation.

Le principe central : l'économie d'attention. Pas de tableaux de bord à ouvrir, pas de spam « tout va bien » toutes les deux heures. Il existe exactement trois types de messages : le résumé quotidien où tout est au vert ; « c'était cassé — j'ai réparé » ; et « c'est cassé — j'ai besoin de toi ». Tout le reste, c'est le silence.

Auto-réparation

Quand une vérification échoue, le droïde ne court pas me voir tout de suite. Il lance d'abord un agent Claude avec un jeu d'outils restreint et un droit d'écriture limité à certains répertoires. Avant chaque tentative, un instantané git de tous les répertoires de travail est pris : tout changement de l'agent s'annule en une commande — les hachages arrivent directement dans le message :

🔧 system droid — полагоджено автоматично, 1 спроба (2026-08-02 07:41)

Було зламано:
• news digest freshness — застарілий дайджест: 2026-07-31 (2 дні тому)

✅ Зараз усі перевірки зелені.

↩️ Відкат змін агента (git reset --hard на знімок):
• ~/Developer/system-droid  @ f037cfb
• ~/Developer/obsidian-cron @ e98746c

Et ce n'est pas un jouet : l'agent a réellement régénéré un condensé périmé après une coupure réseau nocturne, rechargé seul une tâche launchd coincée, et diagnostiqué lui-même qu'un problème de jeton ne se réglerait pas sans moi. Dans le message, je vois ce qui était cassé, ce que l'agent a fait et comment cela s'est terminé.

Ce qu'il a déjà attrapé

En sept semaines, le droïde a transformé plusieurs pannes silencieuses — chacune aurait pu durer des jours — en messages que j'ai vus immédiatement :

  • Un générateur de briefs abattu. Le noyau macOS a tué le processus juste après une recompilation du binaire — le brief du matin ne serait tout simplement jamais parti, et personne ne l'aurait remarqué.
  • Un refresh token expiré. Le proxy ne renouvelait plus son jeton d'accès, et chaque appel LLM revenait en 401. Le droïde l'a dit sans détour : ici, seule une nouvelle connexion peut y remédier — une commande, deux minutes.
  • Un texte de secours à la place de l'analyse. La génération se terminait « avec succès », mais la note contenait un texte de remplacement au lieu de l'analyse IA. Vérifier le contenu de la note, et non le code de sortie, l'a attrapé dès le premier jour.
  • Une sauvegarde du coffre figée. Le plugin git continuait à commiter en local pendant que le push vers GitHub était mort en silence — la sauvegarde se serait figée, découverte au pire moment possible.

Ce qu'il ne répare pas — volontairement

La décision la plus importante de ce système n'est pas quoi automatiser, mais quoi ne pas automatiser. Certaines vérifications sont marquées « alerte uniquement » : tout ce qui touche à l'authentification, aux systèmes tiers et aux sessions de mon propre agent. Un OAuth expiré ne se « répare » pas par script — un humain doit le réémettre dans un navigateur. Il est explicitement interdit à l'agent de toucher aux jetons, de redémarrer mes services en fonctionnement ou de tordre une vérification jusqu'à ce qu'elle passe au vert.

Il y a aussi une limite physique : le réparateur est lui-même un CLI Claude, et quand le réseau ou l'authentification tombe, il tombe avec tout le reste. Ce qui casse le système casse aussi les mains qui le réparent. Le comportement honnête, alors, ce ne sont pas des tentatives héroïques mais un clair « j'ai besoin de toi », avec le mode d'emploi exact.

Les détails qui portaient tout

Ce qui a pris le plus de temps, ce ne sont pas les vérifications, mais la fiabilité du gardien lui-même. Premièrement : recompiler un binaire Go sur place change sa signature, et launchd met en cache les exigences de signature — le système tue alors simplement la prochaine exécution planifiée. Désormais, toute compilation se termine par un rechargement de la tâche :

deploy.sh
# Перезбирання бінарника на місці змінює його cdhash, а launchd кешує
# вимоги до підпису (LWCR) на момент bootstrap. Після збирання кеш
# застаріває — і наступний плановий запуск ядро вбиває (exit 9) або
# launchd відмовляється його запускати (exit 78 / EX_CONFIG).
# Тому збирання ЗАВЖДИ закінчується перезавантаженням задачі:
go build -o system-droid .
codesign --verify --strict system-droid
launchctl bootout  "gui/$(id -u)/com.oleksii.system-droid" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.oleksii.system-droid.plist
./check-self.sh   # свіжий запуск має бути зеленим

Deuxièmement : pour les services en KeepAlive, le code de sortie de la dernière exécution est une métrique trompeuse. Après chaque redémarrage, on y lit SIGTERM, et un service parfaitement vivant paraît cassé. La bonne question n'est pas « comment s'est-il terminé » mais « tourne-t-il en ce moment » :

check-launchd-running.sh
# У KeepAlive-демона LastExitStatus після КОЖНОГО перезапуску
# ненульовий (15/SIGTERM) — тож перевірка "останній запуск успішний"
# хибно валить цілком живий сервіс. Правильне питання інше:
# чи працює він ПРЯМО ЗАРАЗ?
info=$(launchctl list "$1" 2>/dev/null) || { echo "not loaded"; exit 1; }
pid=$(printf '%s\n' "$info" | awk -F'= ' '/"PID"/{gsub(/[ ;]/,"",$2);print $2}')
[ -n "$pid" ] && [ "$pid" != "0" ] && { echo "running (pid $pid)"; exit 0; }
echo "loaded but not running"; exit 1

Troisièmement : il faut vérifier l'objet exact dont dépend le système. Ma première vérification du jeton calculait « date d'émission plus un an » — et restait obstinément au vert pendant que le vrai jeton renvoyait 401 depuis un jour :

# Календарна перевірка сяяла зеленим — річному токену ще жити й жити:
claude oauth token — OK: valid, 365d left

# А справжній робочий токен тим часом помер, і логи проксі це знали:
token refresh failed: {"error":"invalid_grant","error_description":"Refresh token expired"}
POST /v1/chat/completions → 401 "OAuth access token has expired. Re-authenticate."

Et quatrièmement : une coupure réseau d'un seul cycle à trois heures du matin n'est pas une raison de réveiller un humain. L'alarme « intervention requise » ne part désormais que si le problème survit à deux exécutions consécutives. Les incidents ponctuels se résorbent d'eux-mêmes ; les vrais passent quand même — juste deux heures plus tard :

main.go
// Тривога "потрібне втручання" — лише якщо перевірка провалилася
// два запуски поспіль. Нічний обрив мережі, демон посеред перезапуску,
// пробудження зі сну — все це минає само до наступного циклу,
// і будити людину через таке не можна. Успішне лікування,
// навпаки, повідомляється одразу — це добра новина, а не шум.
counts := loadHealCounts()          // ~/.system-droid-heal-state.json
for _, r := range stillFailing {
    counts[r.Name]++
}
var persistent []Result
for _, r := range stillFailing {
    if counts[r.Name] >= 2 {        // поріг ескалації
        persistent = append(persistent, r)
    }
}
// сповіщення — лише якщо len(persistent) > 0

Des yeux pour un autre agent

En plus de Telegram, le droïde tourne aussi comme serveur MCP. Mon agent local peut donc l'interroger directement sur l'état des systèmes : au lieu de lire des rapports, je dis « vérifie que tout marche » — l'agent appelle get_health, exécute chaque vérification en direct et répond. La supervision a cessé d'être une page qu'on regarde pour devenir un service qu'on consulte.

# Дроїд працює і як MCP-сервер — локальний агент питає його сам:
Resonance › виклич get_health у system-droid і скажи, чи все зелене

get_health →
overall: 🟢 OK — 10/10 green (2026-08-17 09:00)
🟢 openclaw gateway (:18789) — ядро агента Resonance (launchd + порт)
🟢 cli-proxy Claude auth — робочий Claude-токен (валідність, авто-оновлення)
...

Bilan

Techniquement, rien de compliqué ici : Go, launchd, quelques scripts shell et un bot Telegram. Mais c'est précisément cet assemblage simple qui a changé la sensation de tout le parc d'automatisation : je ne tiens plus de liste mentale de choses « à ne pas oublier de vérifier ». C'est désormais le travail du droïde.

La formule est courte : vérifie le comportement réel, pas ses indices ; répare automatiquement ce qui peut l'être sans risque ; appelle honnêtement l'humain là où rien d'autre ne suffit ; et tais-toi le reste du temps.

Depuis sept semaines, il se tait la plupart du temps. Chacun de ses messages a été soit une bonne nouvelle sur une panne déjà réparée, soit une indication précise de l'endroit où mes deux minutes étaient nécessaires. C'est ça, le confort : un système qui garde les autres systèmes — et respecte ton silence.

Vous voyez une erreur ?

Un fait erroné, une traduction bancale, quelque chose qui sonne faux dans cet article ? Écrivez-moi — dans votre propre langue.