Files
knowledge-base/snippets/openclaw-kb-webhook.md
dttb f7d06c0a35 Phase 2 webhook: snippet и decision (Mac→openclaw FTS лаг 15м → 11с)
Развёрнут push-webhook от Gitea на kb-pull-webhook.service на LXC 137 + auto-reindex FTS в kb-pull.sh после нового HEAD. Грабли: gitea webhook.ALLOWED_HOST_LIST по дефолту режет private IP; flock -n теряет двойные push, заменён на -w 180.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:01:45 +03:00

8.2 KiB
Raw Blame History

date, type, tags, status
date type tags status
2026-05-06 snippet
openclaw
gitea
webhook
kb-sync
deployed

Webhook Gitea → kb-pull на LXC 137 (Максимка)

Push с Mac → видно боту в FTS за ~11 секунд (было 515 минут через 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

#!/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()
chmod +x /usr/local/bin/kb-pull-webhook.py

2. systemd unit /etc/systemd/system/kb-pull-webhook.service

[Unit]
Description=KB pull webhook listener (Gitea -> kb-pull.sh)
After=network-online.target

[Service]
Type=simple
Environment=GITEA_WEBHOOK_SECRET=<openssl rand -hex 32>
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
SECRET=$(openssl rand -hex 32)
sed -i "s|<openssl rand -hex 32>|$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

#!/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)

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:

[webhook]
ALLOWED_HOST_LIST = 10.0.0.0/24,*.dttb.ru,private

Конкретно у нас Gitea в docker, конфиг — /opt/gitea/data/gitea/conf/app.ini на LXC 136. После правки:

docker restart gitea

6. Проверка end-to-end

# на Mac
cd ~/knowledge-base
git commit --allow-empty -m "smoke" && git push

# на 137 (через 510 сек)
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: <old> -> <new>, 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. Если нужно жёстко закрыть:

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 уже отсекает левые запросы.

Откат

# на 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/<id>

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"