SPB Git forge

spb/ka-guardian

Public
26commits 1branches 0releases
4.5 MBsize
maindefault branch
19 days agolast push
Python 57.1% Shell 13.8% CSS 12% JavaScript 10% HTML 7.2%
17.4 KB · 324 lines shellscript
Raw Blame History
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