Развёрнут 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>
8.2 KiB
date, type, tags, status
| date | type | tags | status | ||||
|---|---|---|---|---|---|---|---|
| 2026-05-06 | snippet |
|
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
#!/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 (через 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: <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" |