--- date: 2026-05-06 type: snippet tags: [openclaw, gitea, webhook, kb-sync] status: deployed --- # Webhook Gitea → kb-pull на LXC 137 (Максимка) Push с Mac → видно боту в FTS за **~11 секунд** (было 5–15 минут через cron `*/15`). Развёрнуто 2026-05-06. Подробности — [[../decisions/2026-05-06-openclaw-kb-webhook-deployment]]. Cron `*/15` оставлен safety net — спасает, если listener умер или Gitea недоступна. ## Архитектура ``` Mac git push ──► Gitea (LXC 136, 10.0.0.189) │ webhook POST + HMAC-SHA256 ▼ LXC 137 :18790 kb-pull-webhook.service │ subprocess.Popen ▼ /usr/local/bin/kb-pull.sh │ git fetch + ff-only / auto-reset │ if HEAD changed → openclaw memory index ▼ FTS реиндекс ~38 сек на 875 файлов / 1791 chunk ``` ## 1. Listener `/usr/local/bin/kb-pull-webhook.py` ```python #!/usr/bin/env python3 """Listens for Gitea push webhooks, triggers kb-pull.sh.""" import hmac, hashlib, http.server, os, subprocess, sys SECRET = os.environ.get("GITEA_WEBHOOK_SECRET", "").encode() PULL = "/usr/local/bin/kb-pull.sh" class H(http.server.BaseHTTPRequestHandler): def do_POST(self): n = int(self.headers.get("Content-Length", 0) or 0) body = self.rfile.read(n) sys.stderr.write("WH POST event=%s sig=%s len=%d\n" % ( self.headers.get("X-Gitea-Event", "-"), (self.headers.get("X-Gitea-Signature", "-") or "-")[:12], n)); sys.stderr.flush() if SECRET: sig = self.headers.get("X-Gitea-Signature", "") mac = hmac.new(SECRET, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(sig, mac): sys.stderr.write("WH 401 sig-mismatch\n"); sys.stderr.flush() self.send_error(401); return subprocess.Popen([PULL], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) self.send_response(202); self.end_headers() self.wfile.write(b"queued\n") sys.stderr.write("WH 202 kb-pull launched\n"); sys.stderr.flush() def do_GET(self): # health-check self.send_response(200); self.end_headers() self.wfile.write(b"ok\n") def log_message(self, fmt, *a): # стандартный access log в journal sys.stderr.write("WH-base " + (fmt % a) + "\n"); sys.stderr.flush() if __name__ == "__main__": port = int(os.environ.get("PORT", "18790")) sys.stderr.write("WH listener up on :%d secret=%s\n" % (port, "yes" if SECRET else "NO")) sys.stderr.flush() http.server.HTTPServer(("0.0.0.0", port), H).serve_forever() ``` ```bash chmod +x /usr/local/bin/kb-pull-webhook.py ``` ## 2. systemd unit `/etc/systemd/system/kb-pull-webhook.service` ```ini [Unit] Description=KB pull webhook listener (Gitea -> kb-pull.sh) After=network-online.target [Service] Type=simple Environment=GITEA_WEBHOOK_SECRET= Environment=PORT=18790 ExecStart=/usr/bin/python3 /usr/local/bin/kb-pull-webhook.py Restart=on-failure RestartSec=5 NoNewPrivileges=yes PrivateTmp=yes [Install] WantedBy=multi-user.target ``` ```bash SECRET=$(openssl rand -hex 32) sed -i "s||$SECRET|" /etc/systemd/system/kb-pull-webhook.service systemctl daemon-reload systemctl enable --now kb-pull-webhook.service ss -ltnp | grep 18790 # должен слушать на 0.0.0.0:18790 ``` ## 3. Хук в `kb-pull.sh` — реиндекс при новом HEAD ```bash #!/bin/bash # read-only pull knowledge-base for openclaw context # Auto-reset at divergence; reindex FTS on new commits. set -u exec 9>/tmp/kb-pull.lock flock -w 180 9 || exit 0 # ← ждать до 3 мин, не -n (race на двойном webhook) cd /root/knowledge-base LOG=/var/log/kb-pull.log { echo "--- $(date -Iseconds) ---" HEAD_BEFORE=$(git rev-parse HEAD 2>/dev/null) git fetch --quiet origin main if ! git merge-base --is-ancestor HEAD origin/main 2>/dev/null; then echo "divergence detected, resetting to origin/main" git reset --hard origin/main else git pull --ff-only --quiet origin main fi HEAD_AFTER=$(git rev-parse HEAD 2>/dev/null) if [ "$HEAD_BEFORE" != "$HEAD_AFTER" ]; then echo "HEAD: $HEAD_BEFORE -> $HEAD_AFTER, triggering FTS reindex" timeout 120 openclaw memory index 2>&1 | tail -5 else echo "no new commits, skip reindex" fi } >> $LOG 2>&1 ``` `flock -w 180` — критично. Если webhook прилетает во время предыдущего pull/reindex (быстрые двойные push), `-n` молча выходит и пропускает push. ## 4. Регистрация webhook в Gitea (через API) ```bash SECRET=... # тот же, что в systemd unit curl -u oleg:OL260380eg -X POST http://10.0.0.189:3000/api/v1/repos/oleg/knowledge-base/hooks \ -H "Content-Type: application/json" \ -d "{\"type\":\"gitea\",\"config\":{\"url\":\"http://10.0.0.239:18790/\",\"content_type\":\"json\",\"secret\":\"$SECRET\"},\"events\":[\"push\"],\"active\":true,\"branch_filter\":\"main\"}" ``` Через UI: `git.dttb.ru` → репо → Settings → Webhooks → Add → Gitea, заполнить теми же полями. ## 5. **Обязательно:** разрешить private IP в Gitea По умолчанию Gitea **блокирует webhooks на private адреса** (SSRF-protection). В docker-логах будет: > `webhook can only call allowed HTTP servers (check your webhook.ALLOWED_HOST_LIST setting), deny '10.0.0.239'` Listener при этом ничего не получит, в его journal пусто. Лечится в `app.ini` Gitea: ```ini [webhook] ALLOWED_HOST_LIST = 10.0.0.0/24,*.dttb.ru,private ``` Конкретно у нас Gitea в docker, конфиг — `/opt/gitea/data/gitea/conf/app.ini` на LXC 136. После правки: ```bash docker restart gitea ``` ## 6. Проверка end-to-end ```bash # на Mac cd ~/knowledge-base git commit --allow-empty -m "smoke" && git push # на 137 (через 5–10 сек) pct exec 137 -- journalctl -u kb-pull-webhook.service --since "30 sec ago" -n 5 # WH POST event=push sig=... len=... # WH 202 kb-pull launched pct exec 137 -- tail -3 /var/log/kb-pull.log # HEAD: -> , triggering FTS reindex # Memory index updated (main). ``` Измеренный тайминг real push (2026-05-06): T0 git push → T+11s `git rev-parse HEAD` на 137 уже новый. ## 7. Опционально: ограничить порт по источнику Listener слушает `0.0.0.0:18790`. Если нужно жёстко закрыть: ```bash iptables -A INPUT -p tcp --dport 18790 -s 10.0.0.189 -j ACCEPT iptables -A INPUT -p tcp --dport 18790 -j DROP ``` Не делал — homelab LAN trusted, secret-HMAC уже отсекает левые запросы. ## Откат ```bash # на 137 systemctl disable --now kb-pull-webhook.service rm /etc/systemd/system/kb-pull-webhook.service /usr/local/bin/kb-pull-webhook.py # восстановить kb-pull.sh из бэкапа cp /usr/local/bin/kb-pull.sh.bak.* /usr/local/bin/kb-pull.sh # в Gitea curl -u oleg:OL260380eg -X DELETE http://10.0.0.189:3000/api/v1/repos/oleg/knowledge-base/hooks/ ``` Cron `*/15` продолжит работать. ## Грабли (выписаны из реального деплоя) | Симптом | Причина | Где смотреть | |---|---|---| | Test delivery 204 от Gitea, но listener не получил | `webhook.ALLOWED_HOST_LIST` не разрешает private IP | `docker logs gitea` на LXC 136 | | Двойной push — второй пропускается | `flock -n` без ожидания | Заменить на `flock -w 180` | | FTS не обновился, хотя git pull прошёл | `openclaw memory` не дёргается | Хук в `kb-pull.sh` после ff-only | | Listener в restart-loop после правки | sed побил python синтаксис | `journalctl -u kb-pull-webhook --since "1 min ago"` |