spb/ka-guardian
Public
Python 57.1%
Shell 13.8%
CSS 12%
JavaScript 10%
HTML 7.2%
1#!/bin/zsh2# ============================================3# KA Guardian — déploiement complet depuis le laptop4# ./deploy.sh runners → runners sur CHAQUE nœud qui héberge un service surveillé (d'après le registre mld)5# ./deploy.sh courier → courrier zsh sur les nœuds runners + M4M366# ./deploy.sh registry-sync → service launchd com.ka.registry-sync sur M4M36 (tirage du registre + sonde des runners)7# ./deploy.sh orchestrators → ka2/ka4/ka6 sur M4M368# ./deploy.sh pousseur → pousseur git sur les nœuds runners (repos Ka du registre, fusion avec ~/.ka-pousseur-repos)9# ./deploy.sh all10# ./deploy.sh nodes → affiche nœuds + repos calculés depuis le registre (sans rien déployer)11# ./deploy.sh <cible> <nœud> → même chose limité à UN nœud (ex. `deploy.sh runners M2U64` après un `mld move`)12# Idempotent. Le token partagé vit dans ~/.ka-guardian.env (laptop) et est13# poussé sur chaque nœud. Les tunnels ngrok existants (www.kaX.bot) sont gardés.14#15# 2026-09-04 — la liste des nœuds n'est PLUS codée en dur : elle est dérivée du16# registre mld (M1M32:~/dispatch/registry.json) croisé avec topology.json17# (services → registry_app). Un service qui déménage sur un nœud jamais équipé18# apparaît ici automatiquement → relancer `deploy.sh runners` (et courier/pousseur).19#20# launchd : sur un nœud AVEC session graphique → LaunchAgent gui/<uid> (bootstrap, repli `load`).21# Sur un nœud SANS session graphique (personne de connecté à la console, ex. M2U64 : le domaine22# gui/<uid> n'existe pas, `launchctl load` avorte rc 134) → LaunchDaemon système exécuté sous23# notre utilisateur (UserName + HOME), via sudo (mot de passe KA_SUDO_PW dans ~/.ka-guardian.env24# du laptop — jamais poussé sur les nœuds). Les daemons ne sont pas soumis à Local Network Privacy.25# ============================================26set -e27ROOT="$(cd "$(dirname "$0")/.." && pwd)"28ORCH_NODE=M4M3629GATEWAY=M1M323031# --- token partagé (+ mot de passe sudo des nœuds pour le mode LaunchDaemon)32TOKEN_FILE="$HOME/.ka-guardian.env"33if [[ ! -f $TOKEN_FILE ]]; then34 echo "KA_GUARDIAN_TOKEN=$(openssl rand -hex 24)" > "$TOKEN_FILE"35 echo "token généré → $TOKEN_FILE"36fi37TOKEN_LINE=$(grep '^KA_GUARDIAN_TOKEN=' "$TOKEN_FILE")38SUDO_PW=$(grep '^KA_SUDO_PW=' "$TOKEN_FILE" | cut -d= -f2- || true)3940# --- registre mld : source de vérité des emplacements41REG_JSON="$(mktemp /tmp/ka-registry.XXXXXX)"42if ! ssh -o ConnectTimeout=8 $GATEWAY 'cat ~/dispatch/registry.json' > "$REG_JSON" 2>/dev/null || ! grep -q '"apps"' "$REG_JSON"; then43 if [[ -f "$HOME/Desktop/cluster-skill/cluster-registry.json" ]]; then44 echo "⚠ passerelle $GATEWAY injoignable — copie laptop cluster-skill/cluster-registry.json utilisée"45 cp "$HOME/Desktop/cluster-skill/cluster-registry.json" "$REG_JSON"46 else47 echo "✗ registre mld introuvable (ni $GATEWAY, ni copie laptop) — impossible de déterminer les nœuds"; exit 148 fi49fi5051# nœuds hébergeant au moins un service surveillé (+ liste "nœud<TAB>dir" des repos Ka pour le pousseur)52registry_lines() { # $1 = nodes | repos53 /usr/bin/python3 - "$ROOT/topology.json" "$REG_JSON" "$1" <<'PYEOF'54import json, sys55topo = json.load(open(sys.argv[1])); reg = json.load(open(sys.argv[2]))["apps"]; mode = sys.argv[3]56rows = []57for key, svc in topo["services"].items():58 app = svc.get("registry_app") or key59 e = reg.get(app)60 if not e or not e.get("node"):61 print(f"# {app}: absent du registre — repli topology.json ({svc.get('node')})", file=sys.stderr)62 alias = next((n["alias"] for k, n in topo["nodes"].items() if k == svc.get("node")), None)63 d = svc.get("dir")64 else:65 alias, d = e["node"], e.get("dir")66 if alias:67 rows.append((alias, d))68if mode == "nodes":69 print("\n".join(sorted({a for a, _ in rows})))70else:71 for a, d in sorted(set(rows)):72 print(f"{a}\t{d}")73PYEOF74}75RUNNER_NODES=(${(f)"$(registry_lines nodes)"})76if (( ${#RUNNER_NODES} == 0 )); then echo "✗ aucun nœud dérivé du registre"; exit 1; fi77# 2e argument optionnel = restreindre à un nœud (ex. `deploy.sh runners M2U64` après un déménagement)78if [[ -n ${2:-} ]]; then79 (( ${RUNNER_NODES[(Ie)$2]} )) || echo "⚠ $2 n'héberge aucun service surveillé d'après le registre — déploiement forcé"80 RUNNER_NODES=($2)81fi82echo "nœuds hébergeurs (registre mld) : ${RUNNER_NODES[*]}"8384# --- helper distant : (re)charge un service launchd, agent gui/<uid> ou daemon système selon le nœud.85# Injecté en tête de chaque script distant ; usage : lc_reload <label> <plist-agent>86# Le mot de passe sudo n'est transmis que dans la commande ssh (jamais écrit sur le nœud).87LC_HELPER='88lc_reload() {89 local label=$1 plist=$2 uid=$(id -u)90 if launchctl print gui/$uid >/dev/null 2>&1; then91 launchctl bootout gui/$uid/$label 2>/dev/null || true92 # bootout est asynchrone : bootstrap trop tôt échoue « 5: Input/output error » → attendre la disparition93 for i in {1..20}; do launchctl print gui/$uid/$label >/dev/null 2>&1 || break; sleep 0.5; done94 if launchctl bootstrap gui/$uid $plist 2>/tmp/$label.lc.err; then95 echo " launchd: $label (agent gui/$uid)"96 else97 echo " launchd: bootstrap $label a échoué ($(tr -d "\n" </tmp/$label.lc.err)) — repli launchctl load"98 launchctl load $plist && echo " launchd: $label (agent gui/$uid, via load)"99 fi100 rm -f /tmp/$label.lc.err101 else102 [[ -n "$KA_SUDO_PW" ]] || { echo " ✗ $label : pas de session graphique et KA_SUDO_PW absent (mode daemon impossible)"; return 1; }103 local tmp=/tmp/$label.daemon.plist104 cp $plist $tmp105 /usr/bin/plutil -replace UserName -string "$USER" $tmp106 /usr/bin/plutil -replace EnvironmentVariables.HOME -string "$HOME" $tmp 2>/dev/null \107 || /usr/bin/plutil -insert EnvironmentVariables -json "{\"HOME\":\"$HOME\"}" $tmp108 # les journaux doivent rester à nous : les créer AVANT que launchd (root) ne les ouvre109 for k in StandardOutPath StandardErrorPath; do110 f=$(/usr/bin/plutil -extract $k raw -o - $tmp 2>/dev/null) && [[ -n $f ]] && { mkdir -p ${f:h}; touch $f; }111 done112 print -r -- "$KA_SUDO_PW" | sudo -S -p "" launchctl bootout system/$label 2>/dev/null || true113 for i in {1..20}; do launchctl print system/$label >/dev/null 2>&1 || break; sleep 0.5; done # bootout asynchrone114 print -r -- "$KA_SUDO_PW" | sudo -S -p "" install -m 644 -o root -g wheel $tmp /Library/LaunchDaemons/$label.plist115 print -r -- "$KA_SUDO_PW" | sudo -S -p "" launchctl bootstrap system /Library/LaunchDaemons/$label.plist \116 && echo " launchd: $label (DAEMON système sous $USER — nœud sans session graphique)"117 rm -f $tmp118 fi119}120'121# préfixe des scripts distants : helper + mot de passe (variable d'environnement de la session ssh)122remote() { # remote <nœud> <script>123 ssh "$1" "export KA_SUDO_PW=$(printf %q "$SUDO_PW"); $LC_HELPER124$2"125}126127deploy_courier() {128 # Courrier zsh sur TOUS les nœuds (orchestrateur + runners): expédie le129 # trafic LAN inter-nœuds via ssh (macOS 26 Local Network Privacy).130 for n in $RUNNER_NODES $ORCH_NODE; do131 echo "=== courrier → $n"132 scp -q "$ROOT/deploy/courier.sh" $n:ka-guardian-courier.sh133 remote $n 'chmod +x ~/ka-guardian-courier.sh134 cat > ~/Library/LaunchAgents/com.ka.guardian-courier.plist <<PLIST135<?xml version="1.0" encoding="UTF-8"?>136<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">137<plist version="1.0"><dict>138 <key>Label</key><string>com.ka.guardian-courier</string>139 <key>ProgramArguments</key><array>140 <string>/bin/zsh</string>141 <string>/Users/simon-pierreboucher/ka-guardian-courier.sh</string>142 </array>143 <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>144 <key>StandardOutPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/courier.log</string>145 <key>StandardErrorPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/courier.log</string>146</dict></plist>147PLIST148 mkdir -p ~/ka-guardian-spool149 lc_reload com.ka.guardian-courier ~/Library/LaunchAgents/com.ka.guardian-courier.plist' \150 && echo " ✓ $n courrier ok" || echo " ✗ $n courrier KO"151 done152}153154deploy_registry_sync() {155 # Tirage périodique du registre mld + sonde des runners, sur le nœud orchestrateur156 # (chaîne zsh pure — LNP). Le push mld (abonné ka2) reste le chemin rapide.157 echo "=== registry-sync → $ORCH_NODE"158 scp -q "$ROOT/deploy/registry-sync.sh" $ORCH_NODE:ka-registry-sync.sh159 remote $ORCH_NODE 'chmod +x ~/ka-registry-sync.sh160 cat > ~/Library/LaunchAgents/com.ka.registry-sync.plist <<PLIST161<?xml version="1.0" encoding="UTF-8"?>162<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">163<plist version="1.0"><dict>164 <key>Label</key><string>com.ka.registry-sync</string>165 <key>ProgramArguments</key><array>166 <string>/bin/zsh</string>167 <string>/Users/simon-pierreboucher/ka-registry-sync.sh</string>168 </array>169 <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>170 <key>StandardOutPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/registry-sync.log</string>171 <key>StandardErrorPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/registry-sync.log</string>172</dict></plist>173PLIST174 mkdir -p ~/ka-guardian-spool175 lc_reload com.ka.registry-sync ~/Library/LaunchAgents/com.ka.registry-sync.plist176 sleep 12; ls -la ~/ka-guardian-spool/registry.json ~/ka-guardian-spool/runners.json && tail -2 ~/ka-guardian-spool/registry-sync.log' \177 && echo " ✓ registry-sync ok" || echo " ✗ registry-sync KO"178}179180deploy_runners() {181 for n in $RUNNER_NODES; do182 echo "=== runner → $n"183 if ! ssh -o ConnectTimeout=8 -o BatchMode=yes $n true 2>/dev/null; then184 echo " ✗ $n injoignable en SSH — runner NON déployé"; continue185 fi186 # GOTCHA: redémarrer un runner TUE la mission claude en cours (cause de la187 # mission fantôme du 2026-08-23). On refuse si le runner est occupé.188 busy=$(ssh $n "curl -s -m 5 localhost:7791/health 2>/dev/null" | grep -o '"busy":true' || true)189 if [[ -n $busy ]]; then190 echo " ⏸ $n occupé (mission en cours) — runner NON redéployé, relance plus tard"191 continue192 fi193 # pré-requis d'un nœud runner : claude CLI + clés dans ~/.claude/.env194 ssh $n 'test -x /opt/homebrew/bin/claude && test -s ~/.claude/.env' \195 || { echo " ✗ $n : /opt/homebrew/bin/claude ou ~/.claude/.env manquant — équiper le nœud avant (npm i -g @anthropic-ai/claude-code ; copier ~/.claude/.env)"; continue; }196 ssh $n "mkdir -p ~/ka-guardian-runner/transcripts"197 scp -q "$ROOT/runner/runner.py" $n:ka-guardian-runner/runner.py198 remote $n "printf '%s\nKA_GUARDIAN_NODE=%s\n' '$TOKEN_LINE' '$n' > ~/.ka-guardian.env199 cd ~/ka-guardian-runner200 PY=\$(command -v /opt/homebrew/bin/python3 || command -v /opt/homebrew/bin/python3.13 || command -v /opt/homebrew/bin/python3.14)201 [[ -d .venv ]] || \$PY -m venv .venv202 ./.venv/bin/pip -q install 'fastapi>=0.110' 'uvicorn>=0.29' 'httpx>=0.27' 'pydantic>=2' 2>&1 | grep -v '^\[notice\]' || true203 cat > ~/Library/LaunchAgents/com.ka.guardian-runner.plist <<'PLIST'204<?xml version=\"1.0\" encoding=\"UTF-8\"?>205<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">206<plist version=\"1.0\"><dict>207 <key>Label</key><string>com.ka.guardian-runner</string>208 <key>ProgramArguments</key><array>209 <string>/Users/simon-pierreboucher/ka-guardian-runner/.venv/bin/python</string>210 <string>/Users/simon-pierreboucher/ka-guardian-runner/runner.py</string>211 </array>212 <key>EnvironmentVariables</key><dict>213 <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>214 </dict>215 <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>216 <key>StandardOutPath</key><string>/Users/simon-pierreboucher/ka-guardian-runner/runner.log</string>217 <key>StandardErrorPath</key><string>/Users/simon-pierreboucher/ka-guardian-runner/runner.log</string>218</dict></plist>219PLIST220 lc_reload com.ka.guardian-runner ~/Library/LaunchAgents/com.ka.guardian-runner.plist"221 sleep 4222 ssh $n "curl -sf localhost:7791/health" && echo " ✓ $n runner ok" || echo " ✗ $n runner KO"223 done224}225226deploy_orchestrators() {227 echo "=== orchestrateurs → $ORCH_NODE"228 ssh $ORCH_NODE "printf '%s\n' '$TOKEN_LINE' > ~/.ka-guardian.env; mkdir -p ~/cluster-projects/ka-guardian"229 cd "$ROOT"230 # tar-over-ssh: déterministe (gotcha: rsync silencieusement inopérant depuis231 # ce contexte de script — jamais élucidé, contourné définitivement).232 tar czf - --exclude data --exclude .venv --exclude .git --exclude '__pycache__' --exclude logs . \233 | ssh $ORCH_NODE 'tar xzf - -C ~/cluster-projects/ka-guardian/'234 loc=$(md5 -q orchestrator/main.py); rem=$(ssh $ORCH_NODE 'md5 -q ~/cluster-projects/ka-guardian/orchestrator/main.py')235 [[ "$loc" == "$rem" ]] || { echo "✗ transfert raté: main.py ($loc ≠ $rem)"; exit 1; }236 remote $ORCH_NODE "cd ~/cluster-projects/ka-guardian237 PY=\$(command -v /opt/homebrew/bin/python3 || command -v /opt/homebrew/bin/python3.13)238 [[ -d .venv ]] || \$PY -m venv .venv239 ./.venv/bin/pip -q install 'fastapi>=0.110' 'uvicorn>=0.29' 'httpx>=0.27' >/dev/null240 mkdir -p data logs241 # retirer les anciens bots (web+bot), garder les tunnels ngrok242 for a in ka2 ka4 ka6; do243 launchctl bootout gui/\$(id -u)/com.\$a.web 2>/dev/null || launchctl unload ~/Library/LaunchAgents/com.\$a.web.plist 2>/dev/null || true244 launchctl bootout gui/\$(id -u)/com.\$a.bot 2>/dev/null || launchctl unload ~/Library/LaunchAgents/com.\$a.bot.plist 2>/dev/null || true245 rm -f ~/Library/LaunchAgents/com.\$a.web.plist ~/Library/LaunchAgents/com.\$a.bot.plist246 done"247 # IP LAN de l'orchestrateur (callback des runners) : registre mld, repli .69248 SELF_IP=$(/usr/bin/python3 -c "import json,sys; r=json.load(open(sys.argv[1]))['apps']; print(r.get('ka2',{}).get('ip') or '192.168.2.69')" "$REG_JSON")249 for a in ka2:8799 ka4:8899 ka6:8999; do250 agent=${a%%:*}; port=${a##*:}251 remote $ORCH_NODE "cat > ~/Library/LaunchAgents/com.$agent.guardian.plist <<PLIST252<?xml version=\"1.0\" encoding=\"UTF-8\"?>253<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">254<plist version=\"1.0\"><dict>255 <key>Label</key><string>com.$agent.guardian</string>256 <key>ProgramArguments</key><array>257 <string>/Users/simon-pierreboucher/cluster-projects/ka-guardian/.venv/bin/python</string>258 <string>/Users/simon-pierreboucher/cluster-projects/ka-guardian/orchestrator/main.py</string>259 </array>260 <key>EnvironmentVariables</key><dict>261 <key>AGENT</key><string>$agent</string>262 <key>KA_GUARDIAN_SELF_IP</key><string>$SELF_IP</string>263 <key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>264 </dict>265 <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>266 <key>StandardOutPath</key><string>/Users/simon-pierreboucher/cluster-projects/ka-guardian/logs/$agent.log</string>267 <key>StandardErrorPath</key><string>/Users/simon-pierreboucher/cluster-projects/ka-guardian/logs/$agent.log</string>268</dict></plist>269PLIST270 lc_reload com.$agent.guardian ~/Library/LaunchAgents/com.$agent.guardian.plist"271 sleep 2272 ssh $ORCH_NODE "curl -sf localhost:$port/health" && echo " ✓ $agent ok (:$port)" || echo " ✗ $agent KO"273 done274}275276deploy_pousseur() {277 # Pousseur launchd (binaires Apple) : pousse les commits en attente des repos278 # Ka du nœud — les missions Claude ne peuvent pas pousser elles-mêmes279 # (macOS 26 Local Network Privacy bloque les descendants du runner python).280 # Les repos viennent du registre mld ; ~/.ka-pousseur-repos est FUSIONNÉ (jamais281 # écrasé : mld y inscrit aussi les repos à l'arrivée d'une app sur le nœud).282 local repos_tsv; repos_tsv="$(registry_lines repos)"283 for n in $RUNNER_NODES; do284 echo "=== pousseur → $n"285 local mine; mine=$(print -r -- "$repos_tsv" | awk -F'\t' -v n="$n" '$1==n{print $2}')286 scp -q "$ROOT/deploy/pousseur.sh" $n:ka-pousseur.sh287 remote $n "chmod +x ~/ka-pousseur.sh288 touch ~/.ka-pousseur-repos289 while IFS= read -r d; do [ -n \"\$d\" ] && { grep -qxF \"\$d\" ~/.ka-pousseur-repos || echo \"\$d\" >> ~/.ka-pousseur-repos; }; done <<'REPOS'290$mine291REPOS292 cat > ~/Library/LaunchAgents/com.ka.pousseur.plist <<'PLIST'293<?xml version=\"1.0\" encoding=\"UTF-8\"?>294<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">295<plist version=\"1.0\"><dict>296 <key>Label</key><string>com.ka.pousseur</string>297 <key>ProgramArguments</key><array>298 <string>/bin/zsh</string>299 <string>/Users/simon-pierreboucher/ka-pousseur.sh</string>300 </array>301 <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>302 <key>StandardOutPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/pousseur.log</string>303 <key>StandardErrorPath</key><string>/Users/simon-pierreboucher/ka-guardian-spool/pousseur.log</string>304</dict></plist>305PLIST306 mkdir -p ~/ka-guardian-spool307 lc_reload com.ka.pousseur ~/Library/LaunchAgents/com.ka.pousseur.plist308 echo \" repos: \$(tr '\n' ' ' < ~/.ka-pousseur-repos)\"" \309 && echo " ✓ $n pousseur ok" || echo " ✗ $n pousseur KO"310 done311}312313case "${1:-all}" in314 nodes) registry_lines repos ;;315 courier) deploy_courier ;;316 registry-sync) deploy_registry_sync ;;317 runners) deploy_runners ;;318 orchestrators) deploy_orchestrators ;;319 pousseur) deploy_pousseur ;;320 all) deploy_courier; deploy_registry_sync; deploy_runners; deploy_orchestrators; deploy_pousseur ;;321esac322rm -f "$REG_JSON"323echo "terminé."324